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