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.diags.remove(&document);
228 if !self.docs.is_empty() && document == self.current() {
229 self.hover_card = None;
230 }
231 if let Some(binding) = self.lsp_state.bindings.remove(&document) {
234 let args = CloseArgs {
235 server: binding.server,
236 document,
237 path: binding.path.clone(),
238 };
239 match self.tape.request("lsp.close", &args) {
240 Ok(true) => {
241 if let Some(client) = self.lsp_live_client(binding.server) {
242 client.did_close(document, &binding.path);
243 }
244 }
245 Ok(false) => {}
246 Err(error) => self.message = format!("lsp close diverged from trace: {error}"),
247 }
248 }
249 if self.lsp_state.hover.is_some_and(|r| r.document == document) {
250 self.lsp_state.hover = None;
251 self.hover_card = None;
252 }
253 if self
254 .lsp_state
255 .navigation
256 .is_some_and(|r| r.document == document)
257 {
258 self.lsp_state.navigation = None;
259 }
260 if self
261 .picker
262 .as_ref()
263 .and_then(|p| p.lsp_context)
264 .is_some_and(|c| c.stamp.document == document)
265 {
266 self.close_picker();
267 }
268 self.lsp_retire_remote_servers();
270 }
271
272 pub(crate) fn lsp_reply_fresh(&self, context: &ReplyContext) -> bool {
273 let stamp = context.stamp;
274 let expected = if context.kind == RequestKind::Hover {
275 self.lsp_state.hover
276 } else {
277 self.lsp_state.navigation
278 };
279 expected == Some(stamp) && self.lsp_context_fresh(context)
280 }
281
282 pub(crate) fn lsp_context_fresh(&self, context: &ReplyContext) -> bool {
286 let stamp = context.stamp;
287 let newer = if context.kind == RequestKind::Hover {
288 self.lsp_state.hover
289 } else {
290 self.lsp_state.navigation
291 };
292 !self.docs.is_empty()
293 && stamp.document == self.current()
294 && newer.is_none_or(|owner| owner == stamp)
295 && self
296 .docs
297 .get(stamp.document)
298 .is_some_and(|d| d.buf.revision() == stamp.revision)
299 && self
300 .lsp_state
301 .bindings
302 .get(&stamp.document)
303 .is_some_and(|b| b.server == stamp.server && b.revision == stamp.revision)
304 }
305
306 pub(super) fn finish_lsp_reply(&mut self, context: &ReplyContext) -> bool {
307 let fresh = self.lsp_reply_fresh(context);
308 let slot = if context.kind == RequestKind::Hover {
309 &mut self.lsp_state.hover
310 } else {
311 &mut self.lsp_state.navigation
312 };
313 if *slot == Some(context.stamp) {
314 *slot = None;
315 }
316 fresh
317 }
318
319 pub(super) fn lsp_request(&mut self, kind: RequestKind) {
320 self.lsp_request_with(kind, None);
321 }
322
323 pub(super) fn lsp_change_request(&mut self, kind: RequestKind, rename_to: Option<String>) {
328 self.lsp_request_with(kind, rename_to);
329 }
330
331 fn lsp_request_with(&mut self, kind: RequestKind, rename_to: Option<String>) {
332 let hover = kind == RequestKind::Hover;
333 if hover {
334 self.lsp_state.hover = None;
335 } else {
336 self.lsp_state.navigation = None;
337 self.cancel_open(strop_core::worker::CancelReason::Superseded);
338 }
339 let Some(doc) = self.lsp_current_doc_path() else {
340 self.message =
341 "language services require a complete file buffer, not a partial/follow view"
342 .into();
343 return;
344 };
345 let Some(language) = self.lsp_doc_language(self.current(), &doc.path) else {
346 self.message = "no language server for this file type".into();
347 return;
348 };
349 let Some((server, _)) =
350 self.lsp_server_for(self.current(), &doc.path, &language, &doc.filesystem)
351 else {
352 self.message = match doc.filesystem {
353 strop_workspace::Filesystem::Local => {
354 let covered_language = self
358 .lsp_state
359 .attach
360 .attached
361 .iter()
362 .any(|a| a.language == language);
363 if covered_language {
364 "no language context for this file — reach it via gd from a served file, or add its root to languages.toml"
365 .into()
366 } else {
367 "no language server — install it or fix languages.toml".into()
368 }
369 }
370 strop_workspace::Filesystem::Remote(endpoint) => {
371 format!(
372 "no language server on {endpoint} — install it there or fix languages.toml"
373 )
374 }
375 strop_workspace::Filesystem::Container(_) => {
376 "language services in containers are not wired yet".into()
377 }
378 };
379 return;
380 };
381 self.lsp_did_open_current();
382 self.lsp_sync_changed();
383 let Some(doc) = self.lsp_current_doc_path() else {
384 return;
385 };
386 let line = self.buf().line_of(self.head());
387 let input = RequestInput {
388 document: self.current(),
389 revision: self.buf().revision(),
390 path: doc.path.clone(),
391 line: LineIndex::new(line),
392 byte_col: ByteColumn::new(self.buf().col_of(self.head())),
393 line_text: strop_lsp::FrozenLine::from_slice(
394 self.buf()
395 .text()
396 .byte_slice(self.buf().line_start(line)..self.buf().line_end(line)),
397 ),
398 kind,
399 rename_to,
400 };
401 let native_input = input.clone();
402 let prepared = self.tape.call("lsp.prepare", &input, || {
403 let client = self
404 .lsp_live_client(server)
405 .ok_or(RequestRefusal::NotOpen)?;
406 client.prepare_request(native_input)
407 });
408 match prepared {
409 Ok(Ok(mut prepared)) => {
410 if kind == RequestKind::Format {
411 prepared.tab_width = Some(self.cur_indent().width);
414 }
415 if hover {
418 self.lsp_state.hover = Some(prepared.stamp);
419 } else {
420 self.lsp_state.navigation = Some(prepared.stamp);
421 }
422 if matches!(
423 kind,
424 RequestKind::Locations(_)
425 | RequestKind::Format
426 | RequestKind::Rename
427 | RequestKind::CodeAction
428 ) {
429 let label = match kind {
430 RequestKind::Locations(k) => k.label(),
431 other => other.label(),
432 };
433 self.message = format!("lsp: {label} …");
434 }
435 match self.tape.request("lsp.launch", &prepared) {
436 Ok(true) => {
437 if let Some(client) = self.lsp_live_client(server) {
438 client.launch_request(prepared);
439 }
440 }
441 Ok(false) => {}
442 Err(error) => {
443 if hover {
444 self.lsp_state.hover = None;
445 } else {
446 self.lsp_state.navigation = None;
447 }
448 self.message = format!("lsp request diverged from trace: {error}");
449 }
450 }
451 }
452 Ok(Err(refusal)) => {
453 self.message = match refusal {
454 RequestRefusal::NotOpen => {
455 "lsp: the document is not open on this server".into()
456 }
457 RequestRefusal::StaleRevision => format!(
458 "lsp: buffer changed while syncing — repeat {}",
459 kind.label()
460 ),
461 RequestRefusal::Unsupported => {
462 format!("lsp: {} is not supported by this server", kind.label())
463 }
464 RequestRefusal::IdentityExhausted => "lsp: request identities exhausted".into(),
465 };
466 }
467 Err(error) => self.message = format!("lsp prepare diverged from trace: {error}"),
468 }
469 }
470
471 pub(super) fn lsp_failed(&mut self, server: ServerId) {
472 let mut docs: Vec<_> = self
473 .lsp_state
474 .bindings
475 .iter()
476 .filter_map(|(&id, b)| (b.server == server).then_some(id))
477 .collect();
478 docs.sort();
479 for document in docs {
480 self.lsp_close_document(document);
481 }
482 self.lsp_state
483 .attach
484 .attached
485 .retain(|a| a.server != server);
486 self.lsp_state
487 .jump_contexts
488 .retain(|_, context| context.server != server);
489 if let Some(index) = self.lsp_servers.iter().position(|s| s.id == server) {
490 let connection = self.lsp_servers.remove(index);
491 if let Some(client) = connection.client {
492 std::thread::spawn(move || {
494 client.shutdown();
495 client.wait(std::time::Duration::from_secs(2));
496 });
497 }
498 }
499 }
500}