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