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