1use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7use strop_core::id::{BufferRevision, ByteColumn, DocumentId, LineIndex};
8use strop_lsp::{
9 Client, ReplyContext, RequestInput, RequestKind, RequestRefusal, RequestStamp, ServerId,
10};
11
12use super::super::Editor;
13use super::attach::AttachState;
14
15pub(crate) struct Binding {
16 pub server: ServerId,
17 pub path: PathBuf,
18 pub root: PathBuf,
19 pub language: String,
23 pub target: strop_workspace::Filesystem,
26 pub revision: BufferRevision,
27}
28
29#[derive(Debug, serde::Serialize)]
33pub(crate) struct SyncArgs {
34 pub server: ServerId,
35 pub document: DocumentId,
36 pub revision: BufferRevision,
37 #[serde(with = "strop_core::path_serde")]
38 pub path: PathBuf,
39 pub bytes: usize,
40}
41
42#[derive(Debug, serde::Serialize)]
43pub(crate) struct CloseArgs {
44 pub server: ServerId,
45 pub document: DocumentId,
46 #[serde(with = "strop_core::path_serde")]
47 pub path: PathBuf,
48}
49
50pub(crate) struct JumpContext {
54 pub server: ServerId,
55 pub root: PathBuf,
56 pub language: String,
57 pub target: strop_workspace::Filesystem,
58}
59
60pub(crate) struct LspState {
61 pub bindings: HashMap<DocumentId, Binding>,
62 pub jump_contexts: HashMap<DocumentId, JumpContext>,
65 pub hover: Option<RequestStamp>,
66 pub navigation: Option<RequestStamp>,
67 pub attach: AttachState,
68}
69
70impl Default for LspState {
71 fn default() -> Self {
72 Self {
73 bindings: HashMap::new(),
74 jump_contexts: HashMap::new(),
75 hover: None,
76 navigation: None,
77 attach: AttachState::new(),
78 }
79 }
80}
81
82impl Editor {
83 pub(super) fn lsp_live_client(&self, server: ServerId) -> Option<Client> {
84 self.lsp_servers
85 .iter()
86 .find(|s| s.id == server)
87 .and_then(|s| s.client.clone())
88 }
89
90 pub(super) fn lsp_doc_language(&self, document: DocumentId, path: &Path) -> Option<String> {
95 if let Some(binding) = self.lsp_state.bindings.get(&document) {
96 return Some(binding.language.clone());
97 }
98 if let Some(context) = self.lsp_state.jump_contexts.get(&document) {
99 return Some(context.language.clone());
100 }
101 super::lsp_language(path).map(str::to_string)
102 }
103
104 pub(super) fn lsp_did_open_current(&mut self) {
105 let document = self.current();
106 let Some(doc) = self.lsp_current_doc_path() else {
107 return;
108 };
109 let Some(language) = self.lsp_doc_language(document, &doc.path) else {
110 return;
111 };
112 let Some((server, root)) =
113 self.lsp_server_for(document, &doc.path, &language, &doc.filesystem)
114 else {
115 return;
116 };
117 if let Some(binding) = self.lsp_state.bindings.get(&document) {
118 if binding.server == server
119 && binding.path == doc.path
120 && binding.target == doc.filesystem
121 {
122 return;
123 }
124 self.lsp_close_document(document);
125 }
126 let revision = self.buf().revision();
127 let text = self.buf().snapshot();
128 let args = SyncArgs {
129 server,
130 document,
131 revision,
132 path: doc.path.clone(),
133 bytes: text.len_bytes(),
134 };
135 let lang_id = match super::lsp_language(&doc.path) {
140 Some(own) if own == language => super::lang_id(&doc.path).to_string(),
141 _ => language.clone(),
142 };
143 let opened = self.tape.call("lsp.open", &args, || {
144 self.lsp_live_client(server)
145 .map(|client| client.did_open(document, revision, &doc.path, &lang_id, text))
146 });
147 match opened {
148 Ok(Some(true)) => {
149 self.lsp_state.jump_contexts.remove(&document);
150 self.lsp_state.bindings.insert(
151 document,
152 Binding {
153 server,
154 path: doc.path,
155 root,
156 language,
157 target: doc.filesystem,
158 revision,
159 },
160 );
161 }
162 Ok(_) => {}
164 Err(error) => self.message = format!("lsp open diverged from trace: {error}"),
165 }
166 }
167
168 pub fn lsp_sync_changed(&mut self) {
169 let mut changed: Vec<_> = self
172 .lsp_state
173 .bindings
174 .iter()
175 .filter_map(|(&id, binding)| {
176 let doc = self.docs.get(id)?;
177 let revision = doc.buf.revision();
178 (revision != binding.revision).then(|| {
179 (
180 id,
181 binding.server,
182 binding.path.clone(),
183 revision,
184 doc.buf.snapshot(),
185 )
186 })
187 })
188 .collect();
189 changed.sort_by_key(|(id, _, _, _, _)| *id);
190 for (document, server, path, revision, text) in changed {
191 let args = SyncArgs {
192 server,
193 document,
194 revision,
195 path: path.clone(),
196 bytes: text.len_bytes(),
197 };
198 match self.tape.call("lsp.change", &args, || {
199 self.lsp_live_client(server)
200 .map(|client| client.did_change(document, revision, &path, text))
201 }) {
202 Ok(Some(true)) => {
203 if let Some(binding) = self.lsp_state.bindings.get_mut(&document) {
204 binding.revision = revision;
205 }
206 }
207 Ok(_) => self.message = "lsp: document change refused".into(),
208 Err(error) => self.message = error.to_string(),
209 }
210 }
211 }
212
213 pub(crate) fn lsp_close_document(&mut self, document: DocumentId) {
214 self.diags.remove(&document);
215 if !self.docs.is_empty() && document == self.current() {
216 self.hover_card = None;
217 }
218 if let Some(binding) = self.lsp_state.bindings.remove(&document) {
221 let args = CloseArgs {
222 server: binding.server,
223 document,
224 path: binding.path.clone(),
225 };
226 match self.tape.request("lsp.close", &args) {
227 Ok(true) => {
228 if let Some(client) = self.lsp_live_client(binding.server) {
229 client.did_close(document, &binding.path);
230 }
231 }
232 Ok(false) => {}
233 Err(error) => self.message = format!("lsp close diverged from trace: {error}"),
234 }
235 }
236 if self.lsp_state.hover.is_some_and(|r| r.document == document) {
237 self.lsp_state.hover = None;
238 self.hover_card = None;
239 }
240 if self
241 .lsp_state
242 .navigation
243 .is_some_and(|r| r.document == document)
244 {
245 self.lsp_state.navigation = None;
246 }
247 if self
248 .picker
249 .as_ref()
250 .and_then(|p| p.lsp_context)
251 .is_some_and(|c| c.stamp.document == document)
252 {
253 self.close_picker();
254 }
255 self.lsp_retire_remote_servers();
257 }
258
259 pub(crate) fn lsp_reply_fresh(&self, context: &ReplyContext) -> bool {
260 let stamp = context.stamp;
261 let expected = if context.kind == RequestKind::Hover {
262 self.lsp_state.hover
263 } else {
264 self.lsp_state.navigation
265 };
266 expected == Some(stamp) && self.lsp_context_fresh(context)
267 }
268
269 pub(crate) fn lsp_context_fresh(&self, context: &ReplyContext) -> bool {
273 let stamp = context.stamp;
274 let newer = if context.kind == RequestKind::Hover {
275 self.lsp_state.hover
276 } else {
277 self.lsp_state.navigation
278 };
279 !self.docs.is_empty()
280 && stamp.document == self.current()
281 && newer.is_none_or(|owner| owner == stamp)
282 && self
283 .docs
284 .get(stamp.document)
285 .is_some_and(|d| d.buf.revision() == stamp.revision)
286 && self
287 .lsp_state
288 .bindings
289 .get(&stamp.document)
290 .is_some_and(|b| b.server == stamp.server && b.revision == stamp.revision)
291 }
292
293 pub(super) fn finish_lsp_reply(&mut self, context: &ReplyContext) -> bool {
294 let fresh = self.lsp_reply_fresh(context);
295 let slot = if context.kind == RequestKind::Hover {
296 &mut self.lsp_state.hover
297 } else {
298 &mut self.lsp_state.navigation
299 };
300 if *slot == Some(context.stamp) {
301 *slot = None;
302 }
303 fresh
304 }
305
306 pub(super) fn lsp_request(&mut self, kind: RequestKind) {
307 self.lsp_request_with(kind, None);
308 }
309
310 pub(super) fn lsp_change_request(&mut self, kind: RequestKind, rename_to: Option<String>) {
315 self.lsp_request_with(kind, rename_to);
316 }
317
318 fn lsp_request_with(&mut self, kind: RequestKind, rename_to: Option<String>) {
319 let hover = kind == RequestKind::Hover;
320 if hover {
321 self.lsp_state.hover = None;
322 } else {
323 self.lsp_state.navigation = None;
324 self.cancel_open(strop_core::worker::CancelReason::Superseded);
325 }
326 let Some(doc) = self.lsp_current_doc_path() else {
327 self.message =
328 "language services require a complete file buffer, not a partial/follow view"
329 .into();
330 return;
331 };
332 let Some(language) = self.lsp_doc_language(self.current(), &doc.path) else {
333 self.message = "no language server for this file type".into();
334 return;
335 };
336 let Some((server, _)) =
337 self.lsp_server_for(self.current(), &doc.path, &language, &doc.filesystem)
338 else {
339 self.message = match doc.filesystem {
340 strop_workspace::Filesystem::Local => {
341 let covered_language = self
345 .lsp_state
346 .attach
347 .attached
348 .iter()
349 .any(|a| a.language == language);
350 if covered_language {
351 "no language context for this file — reach it via gd from a served file, or add its root to languages.toml"
352 .into()
353 } else {
354 "no language server — install it or fix languages.toml".into()
355 }
356 }
357 strop_workspace::Filesystem::Remote(endpoint) => {
358 format!(
359 "no language server on {endpoint} — install it there or fix languages.toml"
360 )
361 }
362 strop_workspace::Filesystem::Container(_) => {
363 "language services in containers are not wired yet".into()
364 }
365 };
366 return;
367 };
368 self.lsp_did_open_current();
369 self.lsp_sync_changed();
370 let Some(doc) = self.lsp_current_doc_path() else {
371 return;
372 };
373 let line = self.buf().line_of(self.head());
374 let input = RequestInput {
375 document: self.current(),
376 revision: self.buf().revision(),
377 path: doc.path.clone(),
378 line: LineIndex::new(line),
379 byte_col: ByteColumn::new(self.buf().col_of(self.head())),
380 line_text: strop_lsp::FrozenLine::from_slice(
381 self.buf()
382 .text()
383 .byte_slice(self.buf().line_start(line)..self.buf().line_end(line)),
384 ),
385 kind,
386 rename_to,
387 };
388 let native_input = input.clone();
389 let prepared = self.tape.call("lsp.prepare", &input, || {
390 let client = self
391 .lsp_live_client(server)
392 .ok_or(RequestRefusal::NotOpen)?;
393 client.prepare_request(native_input)
394 });
395 match prepared {
396 Ok(Ok(mut prepared)) => {
397 if kind == RequestKind::Format {
398 prepared.tab_width = Some(self.config.tab_size);
401 }
402 if hover {
405 self.lsp_state.hover = Some(prepared.stamp);
406 } else {
407 self.lsp_state.navigation = Some(prepared.stamp);
408 }
409 if matches!(
410 kind,
411 RequestKind::Locations(_)
412 | RequestKind::Format
413 | RequestKind::Rename
414 | RequestKind::CodeAction
415 ) {
416 let label = match kind {
417 RequestKind::Locations(k) => k.label(),
418 other => other.label(),
419 };
420 self.message = format!("lsp: {label} …");
421 }
422 match self.tape.request("lsp.launch", &prepared) {
423 Ok(true) => {
424 if let Some(client) = self.lsp_live_client(server) {
425 client.launch_request(prepared);
426 }
427 }
428 Ok(false) => {}
429 Err(error) => {
430 if hover {
431 self.lsp_state.hover = None;
432 } else {
433 self.lsp_state.navigation = None;
434 }
435 self.message = format!("lsp request diverged from trace: {error}");
436 }
437 }
438 }
439 Ok(Err(refusal)) => {
440 self.message = match refusal {
441 RequestRefusal::NotOpen => {
442 "lsp: the document is not open on this server".into()
443 }
444 RequestRefusal::StaleRevision => format!(
445 "lsp: buffer changed while syncing — repeat {}",
446 kind.label()
447 ),
448 RequestRefusal::Unsupported => {
449 format!("lsp: {} is not supported by this server", kind.label())
450 }
451 RequestRefusal::IdentityExhausted => "lsp: request identities exhausted".into(),
452 };
453 }
454 Err(error) => self.message = format!("lsp prepare diverged from trace: {error}"),
455 }
456 }
457
458 pub(super) fn lsp_failed(&mut self, server: ServerId) {
459 let mut docs: Vec<_> = self
460 .lsp_state
461 .bindings
462 .iter()
463 .filter_map(|(&id, b)| (b.server == server).then_some(id))
464 .collect();
465 docs.sort();
466 for document in docs {
467 self.lsp_close_document(document);
468 }
469 self.lsp_state
470 .attach
471 .attached
472 .retain(|a| a.server != server);
473 self.lsp_state
474 .jump_contexts
475 .retain(|_, context| context.server != server);
476 if let Some(index) = self.lsp_servers.iter().position(|s| s.id == server) {
477 let connection = self.lsp_servers.remove(index);
478 if let Some(client) = connection.client {
479 std::thread::spawn(move || {
481 client.shutdown();
482 client.wait(std::time::Duration::from_secs(2));
483 });
484 }
485 }
486 }
487}