1use super::attach::{AttachKey, AttachRecord};
3use super::*;
4use std::sync::mpsc::channel;
5use strop_core::id::DocumentId;
6
7impl Editor {
8 pub fn lsp_start_services(&mut self) {
17 self.lsp_state.attach.enabled = true;
18 self.lsp_maybe_attach();
19 }
20
21 pub(crate) fn lsp_maybe_attach(&mut self) {
22 if !self.lsp_state.attach.enabled {
23 return;
24 }
25 if self.cur().remote_metadata().is_some() && !self.remote_window_complete() {
26 self.message = "lsp unavailable — partial remote window".into();
29 return;
30 }
31 let Some(doc) = self.lsp_current_doc_path() else {
32 return;
33 };
34 let Some(language) = self.lsp_doc_language(self.current(), &doc.path) else {
37 return;
38 };
39 if self
40 .lsp_server_for(self.current(), &doc.path, &language, &doc.filesystem)
41 .is_some()
42 {
43 self.lsp_did_open_current();
44 return;
45 }
46 let Some(ext) = doc
49 .path
50 .extension()
51 .map(|e| format!(".{}", e.to_string_lossy()))
52 else {
53 return;
54 };
55 let Some(language) = registry::language_for_extension(&ext) else {
56 return;
57 };
58 let key = AttachKey {
59 target: doc.filesystem.clone(),
60 language: language.to_string(),
61 path: doc.path.clone(),
62 };
63 match self.lsp_state.attach.refused.get(&key) {
64 Some(attach::AttachDecision::TrustRequired { .. })
66 | Some(attach::AttachDecision::TrustError { .. })
67 | Some(attach::AttachDecision::RemoteIo { .. }) => {}
70 Some(_) => return,
72 None => {}
73 }
74 if self.lsp_state.attach.pending.contains_key(&key) {
75 return;
76 }
77 let ticket = match self.worker_ids.allocate() {
78 Ok(ticket) => ticket,
79 Err(error) => {
80 self.message = error.message;
81 return;
82 }
83 };
84 self.lsp_state.attach.pending.insert(key, ticket);
85 let args = attach::AttachArgs {
86 ticket,
87 path: doc.path.clone(),
88 language: language.to_string(),
89 target: doc.filesystem.clone(),
90 };
91 match self.tape.request("lsp.attach", &args) {
94 Ok(true) => self.lsp_spawn_discovery(ticket, doc, ext, language),
95 Ok(false) => {}
96 Err(error) => {
97 self.lsp_state.attach.pending.remove(&args_key(&args));
98 self.message = format!("lsp attach diverged from trace: {error}");
99 }
100 }
101 }
102
103 fn lsp_spawn_discovery(
104 &mut self,
105 ticket: strop_core::worker::WorkerId,
106 doc: ResourceLocation,
107 ext: String,
108 language: &'static str,
109 ) {
110 let place = match doc.filesystem.clone() {
111 Filesystem::Local => attach::DiscoverPlace::Local {
112 abs: doc.path.clone(),
113 cwd: self.cwd.clone(),
114 git_workdir: self.git.as_ref().map(|g| g.workdir().to_path_buf()),
115 },
116 Filesystem::Remote(_) => {
119 let Some(file) = self.remote_file().cloned() else {
120 return;
121 };
122 attach::DiscoverPlace::Remote {
123 file,
124 client: self.remote_client(),
125 }
126 }
127 Filesystem::Container(id) => attach::DiscoverPlace::Container {
130 root: doc.path.parent().unwrap_or(Path::new("/")).to_path_buf(),
131 id,
132 },
133 };
134 let input = attach::DiscoverInput {
135 ticket,
136 place,
137 ext,
138 language,
139 state_dir: self.state_dir.clone(),
140 xdg: strop_lsp::languages::xdg_path(),
141 transport: self.lsp_state.attach.transport.clone(),
142 };
143 let cancelled = attach::AttachRecord {
144 ticket,
145 server: None,
146 language: language.to_owned(),
147 name: language.to_owned(),
148 root: doc.path.parent().unwrap_or(Path::new("/")).to_owned(),
149 target: doc.filesystem,
150 outcome: attach::AttachDecision::Cancelled,
151 layers: Vec::new(),
152 };
153 let done = self.lsp_state.attach.attach_channel();
154 let handle = strop_core::worker::spawn(
158 "strop-lsp-attach",
159 move |outcome| {
160 let record = match outcome {
161 strop_core::worker::Outcome::Success(record) => record,
162 strop_core::worker::Outcome::Cancelled(_) => cancelled,
163 strop_core::worker::Outcome::Failed { failure, .. } => attach::AttachRecord {
164 outcome: attach::AttachDecision::SpawnFailed {
165 reason: failure.message,
166 },
167 ..cancelled
168 },
169 };
170 let _ = done.send(record);
171 },
172 move |token| match attach::discover(input, &token) {
173 Some(record) => strop_core::worker::Outcome::Success(record),
174 None => strop_core::worker::Outcome::Cancelled(
175 strop_core::worker::CancelReason::OwnerClosed,
176 ),
177 },
178 );
179 self.worker_handles.insert(ticket, handle);
180 }
181
182 pub(crate) fn lsp_server_for(
191 &self,
192 document: DocumentId,
193 path: &Path,
194 language: &str,
195 target: &Filesystem,
196 ) -> Option<(ServerId, PathBuf)> {
197 if let Some(binding) = self.lsp_state.bindings.get(&document) {
201 if binding.target == *target && binding.language == language {
202 return Some((binding.server, binding.root.clone()));
203 }
204 }
205 if let Some(context) = self.lsp_state.jump_contexts.get(&document) {
206 if context.target == *target && context.language == language {
207 return Some((context.server, context.root.clone()));
208 }
209 }
210 let attach = &self.lsp_state.attach;
211 let best = attach
212 .attached
213 .iter()
214 .filter(|a| a.language == language && &a.target == target && path.starts_with(&a.root))
215 .max_by_key(|a| a.root.as_os_str().len())?;
216 Some((best.server, best.root.clone()))
217 }
218
219 pub(crate) fn handle_lsp_attach(&mut self, record: AttachRecord) {
220 trace_attach(&record);
221 self.worker_handles.remove(&record.ticket);
222 let key = self
223 .lsp_state
224 .attach
225 .pending
226 .iter()
227 .find_map(|(key, &owner)| (owner == record.ticket).then(|| key.clone()));
228 let Some(key) = key else {
229 trace::services::rejected("lsp", "attach completion superseded");
230 self.retire_superseded_transport(record.server);
231 return;
232 };
233 self.lsp_state.attach.pending.remove(&key);
234 if key.target != record.target || key.language != record.language {
235 self.retire_superseded_transport(record.server);
236 trace::services::rejected("lsp", "attach result differs from its requested workspace");
237 return;
238 }
239 let attach::AttachRecord {
240 ticket: _,
241 server,
242 language,
243 name,
244 root,
245 target,
246 outcome,
247 layers,
248 } = record;
249 self.record_layer_diagnostics(&layers);
252 let warning = layer_suffix(&layers);
253 match outcome {
254 attach::AttachDecision::Cancelled => {}
255 attach::AttachDecision::Attached => {
256 let Some(server) = server else { return };
257 if self.lsp_state.attach.attached.iter().any(|attachment| {
258 attachment.language == language
259 && attachment.root == root
260 && attachment.target == target
261 }) {
262 self.retire_superseded_transport(Some(server));
263 self.lsp_did_open_current();
264 return;
265 }
266 self.lsp_state
267 .attach
268 .attached
269 .retain(|a| !(a.language == language && a.root == root && a.target == target));
270 self.lsp_state.attach.attached.push(attach::Attachment {
273 language: language.clone(),
274 root: root.clone(),
275 server,
276 target: target.clone(),
277 });
278 match warning {
279 Some(warning) => self.message = format!("lsp: {warning}"),
280 None => self.message = format!("lsp: {name} starting"),
281 }
282 let transport = self
283 .lsp_state
284 .attach
285 .transport
286 .lock()
287 .ok()
288 .and_then(|mut table| table.remove(&server));
289 match transport {
290 Some(attach::LiveTransport { client, rx }) => {
291 if let Some(app_tx) = &self.app_tx {
293 let tx = app_tx.clone();
294 std::thread::spawn(move || {
295 while let Ok(event) = rx.recv() {
296 if tx
297 .send(crate::editor::events::AppEvent::Lsp(event))
298 .is_err()
299 {
300 break;
301 }
302 }
303 });
304 let (_, empty) = channel();
305 self.lsp_servers.push(LspServer {
306 id: server,
307 client: Some(client),
308 rx: empty,
309 ready: false,
310 });
311 } else {
312 self.lsp_servers.push(LspServer {
313 id: server,
314 client: Some(client),
315 rx,
316 ready: false,
317 });
318 }
319 }
320 None => {
321 self.lsp_servers.push(LspServer {
324 id: server,
325 client: None,
326 rx: channel().1,
327 ready: false,
328 });
329 }
330 }
331 self.lsp_did_open_current();
332 }
333 decision => {
334 if matches!(
335 decision,
336 attach::AttachDecision::TrustRequired { .. }
337 | attach::AttachDecision::TrustError { .. }
338 ) {
339 self.lsp_state
340 .attach
341 .trust_roots
342 .insert(key.clone(), root.clone());
343 }
344 let sticky = !matches!(
345 decision,
346 attach::AttachDecision::TrustRequired { .. }
347 | attach::AttachDecision::TrustError { .. }
348 | attach::AttachDecision::RemoteIo { .. }
349 );
350 let first = self
351 .lsp_state
352 .attach
353 .refused
354 .insert(key, decision.clone())
355 .is_none();
356 if sticky && !first {
357 return;
358 }
359 self.message = match decision {
360 attach::AttachDecision::NoServer => format!("no language server for {name}"),
361 attach::AttachDecision::TrustRequired { command } => {
362 format!("project config wants to run `{command}` — :trust to allow (once)")
363 }
364 attach::AttachDecision::TrustError { error } => {
365 format!("project trust: {error}")
366 }
367 attach::AttachDecision::NotExecutable {
368 command,
369 reason,
370 hint,
371 } => {
372 format!("lsp: {command} {reason} — {hint}")
373 }
374 attach::AttachDecision::SpawnFailed { reason } => {
375 format!("lsp: {name} could not start — {reason}")
376 }
377 attach::AttachDecision::RemoteIo { reason } => {
378 format!("lsp: remote discovery failed — {reason}")
379 }
380 attach::AttachDecision::Attached | attach::AttachDecision::Cancelled => {
381 unreachable!("matched above")
382 }
383 };
384 if let Some(warning) = warning {
385 self.message = format!("{} — {}", self.message, warning);
386 }
387 }
388 }
389 }
390
391 fn record_layer_diagnostics(&mut self, layers: &[strop_lsp::languages::LayerDiagnostic]) {
395 for diagnostic in layers {
396 let state = &mut self.lsp_state.attach;
397 if !state.layer_diagnostics.contains(diagnostic) {
398 state.layer_diagnostics.push(diagnostic.clone());
399 }
400 }
401 }
402
403 pub(super) fn layer_warning(&self) -> Option<String> {
406 layer_suffix(&self.lsp_state.attach.layer_diagnostics)
407 }
408
409 fn retire_superseded_transport(&mut self, server: Option<ServerId>) {
413 let Some(server) = server else { return };
414 let transport = self
415 .lsp_state
416 .attach
417 .transport
418 .lock()
419 .ok()
420 .and_then(|mut table| table.remove(&server));
421 if let Some(attach::LiveTransport { client, .. }) = transport {
422 std::thread::spawn(move || {
425 client.shutdown();
426 client.wait(std::time::Duration::from_secs(2));
427 });
428 }
429 }
430
431 pub(crate) fn lsp_retire_remote_servers(&mut self) {
436 let retired: Vec<ServerId> = self
437 .lsp_state
438 .attach
439 .attached
440 .iter()
441 .filter(|a| a.target.is_remote())
442 .filter(|a| {
443 let endpoint = match &a.target {
444 Filesystem::Remote(endpoint) => endpoint,
445 _ => return false,
446 };
447 !self.docs.iter().any(|(id, document)| {
448 document.remote_metadata().is_some_and(|source| {
449 source.file.endpoint() == endpoint
450 && source.file.path().starts_with(&a.root)
451 && lsp_language(source.file.path()) == Some(a.language.as_str())
452 && source.window.is_complete()
453 && !self.remote_following(id)
454 })
455 })
456 })
457 .map(|a| a.server)
458 .collect();
459 for server in retired {
460 self.lsp_retire_server(server, "remote workspace closed");
461 }
462 }
463
464 fn lsp_retire_server(&mut self, server: ServerId, reason: &str) {
467 self.lsp_state
471 .attach
472 .attached
473 .retain(|a| a.server != server);
474 let connection = self
475 .lsp_servers
476 .iter()
477 .position(|s| s.id == server)
478 .map(|index| self.lsp_servers.remove(index));
479 let mut documents: Vec<_> = self
480 .lsp_state
481 .bindings
482 .iter()
483 .filter(|(_, binding)| binding.server == server)
484 .map(|(document, _)| *document)
485 .collect();
486 documents.sort();
487 for document in documents {
488 self.lsp_close_document(document);
489 }
490 if let Some(connection) = connection {
491 if let Some(client) = connection.client {
492 std::thread::spawn(move || {
493 client.shutdown();
494 client.wait(std::time::Duration::from_secs(2));
495 });
496 }
497 }
498 trace::services::rejected("lsp", reason);
499 }
500}
501
502impl Editor {
503 pub(crate) fn remote_trust_target(&self) -> Result<strop_workspace::RemoteFile, String> {
504 let doc = self
505 .lsp_current_doc_path()
506 .ok_or("trust requires a file buffer")?;
507 let Filesystem::Remote(endpoint) = doc.filesystem else {
508 return Err("not a remote workspace".into());
509 };
510 let language = lsp_language(&doc.path).ok_or("no language server for this file")?;
511 let key = AttachKey {
512 target: Filesystem::Remote(endpoint.clone()),
513 language: language.to_owned(),
514 path: doc.path,
515 };
516 let root = self
517 .lsp_state
518 .attach
519 .trust_roots
520 .get(&key)
521 .ok_or("no pending remote project trust request")?;
522 strop_workspace::RemoteFile::from_path(endpoint, root.clone())
523 .map_err(|error| error.to_string())
524 }
525}
526
527fn args_key(args: &attach::AttachArgs) -> AttachKey {
528 AttachKey {
529 target: args.target.clone(),
530 language: args.language.clone(),
531 path: args.path.clone(),
532 }
533}
534
535fn layer_suffix(layers: &[strop_lsp::languages::LayerDiagnostic]) -> Option<String> {
538 let first = layers.first()?;
539 Some(if layers.len() == 1 {
540 first.display()
541 } else {
542 format!("{} (+{} more)", first.display(), layers.len() - 1)
543 })
544}
545
546fn trace_attach(record: &attach::AttachRecord) {
550 use strop_trace::{record_with, EventKind};
551 record_with(EventKind::JobFinished, || {
552 let mut value = serde_json::json!({
553 "service": "lsp",
554 "result": "attach",
555 "outcome": record.outcome.label(),
556 "language": record.language,
557 "name": record.name,
558 "server": record.server,
559 "target": record.target.label(),
560 "root": trace::services::NativePath(record.root.clone()),
561 "layers": &record.layers,
562 });
563 match &record.outcome {
564 attach::AttachDecision::NotExecutable {
565 command,
566 reason,
567 hint,
568 } => {
569 value["command"] = serde_json::json!(command);
570 value["reason"] = serde_json::json!(reason);
571 value["hint"] = serde_json::json!(hint);
572 }
573 attach::AttachDecision::SpawnFailed { reason }
574 | attach::AttachDecision::RemoteIo { reason } => {
575 value["reason"] = serde_json::json!(reason);
576 }
577 _ => {}
578 }
579 value
580 });
581}