1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::process::{Child, Command, Stdio};
4use std::sync::mpsc;
5use std::sync::{Arc, Condvar, Mutex};
6use std::thread;
7use std::time::{Duration, Instant};
8
9use anyhow::{Result, bail};
10use serde_json::{Value, json};
11
12use crate::data::lsp::registry;
13use crate::data::lsp::types::{
14 CompletionItem, DocumentSymbol, HoverInfo, Language, Location, LspEvent, LspServerInfo,
15 SelectionRange, SemanticToken, ServerState, SymbolKind, SymbolRange,
16};
17
18use super::installer::InstallProgress;
19
20use super::detector;
21use super::health::{self, HealthStatus};
22use super::installer;
23use super::transport::LspTransport;
24
25const SHUTDOWN_GRACE: Duration = Duration::from_millis(1_500);
26
27pub struct LspEngineConfig {
28 pub auto_install: bool,
29 pub startup_timeout: Duration,
30 binary_name_override: Option<String>,
31 binary_args_override: Vec<String>,
32 check_command_override: Option<String>,
33}
34
35impl Default for LspEngineConfig {
36 fn default() -> Self {
37 Self {
38 auto_install: true,
39 startup_timeout: Duration::from_secs(30),
40 binary_name_override: None,
41 binary_args_override: Vec::new(),
42 check_command_override: None,
43 }
44 }
45}
46
47impl LspEngineConfig {
48 pub fn with_startup_timeout(mut self, timeout: Duration) -> Self {
49 self.startup_timeout = timeout;
50 self
51 }
52
53 pub fn with_auto_install(mut self, auto: bool) -> Self {
54 self.auto_install = auto;
55 self
56 }
57
58 pub fn with_server_override(
59 mut self,
60 binary: impl Into<String>,
61 args: Vec<String>,
62 check_cmd: impl Into<String>,
63 ) -> Self {
64 self.binary_name_override = Some(binary.into());
65 self.binary_args_override = args;
66 self.check_command_override = Some(check_cmd.into());
67 self
68 }
69}
70
71#[derive(Clone)]
72struct ServerOverrides {
73 binary_name: Option<String>,
74 binary_args: Vec<String>,
75 check_command: Option<String>,
76}
77
78struct StartupContext {
79 lang: Language,
80 server_info: &'static LspServerInfo,
81 root_path: PathBuf,
82 auto_install: bool,
83 overrides: ServerOverrides,
84 startup_timeout: Duration,
85 install_progress: Option<Arc<dyn InstallProgress>>,
86 install_lock: Arc<Mutex<()>>,
87}
88
89struct ServerInstance {
90 state: Arc<(Mutex<ServerState>, Condvar)>,
91 server_info: &'static LspServerInfo,
92 child: Arc<Mutex<Option<Child>>>,
94 transport: Option<LspTransport>,
95 startup_rx: Option<mpsc::Receiver<LspTransport>>,
96 opened_files: HashSet<PathBuf>,
97 root_path: PathBuf,
98}
99
100impl ServerInstance {
101 fn get_state(&self) -> ServerState {
102 *self.state.0.lock().unwrap()
103 }
104}
105
106fn try_transition(
110 state: &Arc<(Mutex<ServerState>, Condvar)>,
111 lang: Language,
112 new_state: ServerState,
113 event_tx: &mpsc::Sender<LspEvent>,
114) -> Option<ServerState> {
115 let (lock, cvar) = &**state;
116 let mut s = lock.lock().unwrap();
117 let old = *s;
118 if !old.can_transition_to(new_state) {
119 return None;
120 }
121 *s = new_state;
122 drop(s);
123 cvar.notify_all();
124 let _ = event_tx.send(LspEvent::StateChanged {
125 language: lang,
126 old,
127 new: new_state,
128 });
129 Some(old)
130}
131
132pub struct LspEngine {
133 servers: HashMap<Language, ServerInstance>,
134 config: LspEngineConfig,
135 event_tx: mpsc::Sender<LspEvent>,
136 event_rx: mpsc::Receiver<LspEvent>,
137 install_progress: Option<Arc<dyn InstallProgress>>,
138 install_lock: Arc<Mutex<()>>,
139 #[cfg(any(test, feature = "test-support"))]
140 test_symbols: HashMap<PathBuf, Vec<DocumentSymbol>>,
141 #[cfg(any(test, feature = "test-support"))]
142 test_selection_ranges: HashMap<(PathBuf, usize, usize), SelectionRange>,
143 #[cfg(any(test, feature = "test-support"))]
144 test_semantic_tokens: HashMap<PathBuf, Vec<SemanticToken>>,
145 #[cfg(any(test, feature = "test-support"))]
146 pub did_open_log: Vec<(PathBuf, String)>,
147 #[cfg(any(test, feature = "test-support"))]
148 pub test_semantic_tokens_delay: Option<Duration>,
149}
150
151impl LspEngine {
152 pub fn new(config: LspEngineConfig) -> Self {
153 let (event_tx, event_rx) = mpsc::channel();
154 Self {
155 servers: HashMap::new(),
156 config,
157 event_tx,
158 event_rx,
159 install_progress: None,
160 install_lock: Arc::new(Mutex::new(())),
161 #[cfg(any(test, feature = "test-support"))]
162 test_symbols: HashMap::new(),
163 #[cfg(any(test, feature = "test-support"))]
164 test_selection_ranges: HashMap::new(),
165 #[cfg(any(test, feature = "test-support"))]
166 test_semantic_tokens: HashMap::new(),
167 #[cfg(any(test, feature = "test-support"))]
168 did_open_log: Vec::new(),
169 #[cfg(any(test, feature = "test-support"))]
170 test_semantic_tokens_delay: None,
171 }
172 }
173
174 #[cfg(any(test, feature = "test-support"))]
175 pub fn inject_test_symbols(&mut self, path: PathBuf, symbols: Vec<DocumentSymbol>) {
176 self.test_symbols.insert(path, symbols);
177 }
178
179 #[cfg(any(test, feature = "test-support"))]
180 pub fn inject_test_selection_range(
181 &mut self,
182 path: PathBuf,
183 line: usize,
184 col: usize,
185 sel_range: SelectionRange,
186 ) {
187 self.test_selection_ranges
188 .insert((path, line, col), sel_range);
189 }
190
191 #[cfg(any(test, feature = "test-support"))]
192 pub fn inject_test_semantic_tokens(&mut self, path: PathBuf, tokens: Vec<SemanticToken>) {
193 self.test_semantic_tokens.insert(path, tokens);
194 }
195
196 pub fn set_install_progress(&mut self, progress: Arc<dyn InstallProgress>) {
197 self.install_progress = Some(progress);
198 }
199
200 pub fn startup_timeout(&self) -> Duration {
201 self.config.startup_timeout
202 }
203
204 pub fn start_for_context(&mut self, root_path: &Path, files: &[&Path]) -> Result<()> {
205 let languages = detector::detect_languages(root_path, files);
206
207 for lang in languages {
208 if !lang.capabilities().has_lsp {
209 continue;
210 }
211
212 if self.servers.contains_key(&lang) {
213 continue;
214 }
215
216 let server_info = match registry::server_for_language(lang) {
217 Some(info) => info,
218 None => continue,
219 };
220
221 let resolved_root = registry::workspace_root_for_language(root_path, lang)
222 .unwrap_or_else(|| root_path.to_path_buf());
223 self.spawn_server(lang, server_info, resolved_root);
224 }
225
226 Ok(())
227 }
228
229 fn spawn_server(
230 &mut self,
231 lang: Language,
232 server_info: &'static LspServerInfo,
233 root_path: PathBuf,
234 ) {
235 let state = Arc::new((Mutex::new(ServerState::Undetected), Condvar::new()));
236 let child: Arc<Mutex<Option<Child>>> = Arc::new(Mutex::new(None));
237 let (transport_tx, transport_rx) = mpsc::channel();
238 let event_tx = self.event_tx.clone();
239 let state_clone = Arc::clone(&state);
240 let child_clone = Arc::clone(&child);
241 let ctx = StartupContext {
242 lang,
243 server_info,
244 root_path: root_path.clone(),
245 auto_install: self.config.auto_install,
246 overrides: ServerOverrides {
247 binary_name: self.config.binary_name_override.clone(),
248 binary_args: self.config.binary_args_override.clone(),
249 check_command: self.config.check_command_override.clone(),
250 },
251 startup_timeout: self.config.startup_timeout,
252 install_progress: self.install_progress.clone(),
253 install_lock: Arc::clone(&self.install_lock),
254 };
255
256 thread::spawn(move || {
257 startup_thread(ctx, state_clone, child_clone, transport_tx, event_tx);
258 });
259
260 self.servers.insert(
261 lang,
262 ServerInstance {
263 state,
264 server_info,
265 child,
266 transport: None,
267 startup_rx: Some(transport_rx),
268 opened_files: HashSet::new(),
269 root_path,
270 },
271 );
272 }
273
274 pub fn server_state(&self, lang: Language) -> ServerState {
275 self.servers
276 .get(&lang)
277 .map(|s| s.get_state())
278 .unwrap_or(ServerState::Undetected)
279 }
280
281 pub fn await_ready(&mut self, lang: Language, timeout: Duration) -> Result<ServerState> {
282 let state_arc = {
283 let server = self
284 .servers
285 .get(&lang)
286 .ok_or_else(|| anyhow::anyhow!("no server registered for {:?}", lang))?;
287 Arc::clone(&server.state)
288 };
289
290 let (lock, cvar) = &*state_arc;
291 let started = Instant::now();
292 let mut state = lock.lock().unwrap();
293
294 while !state.is_terminal() {
295 let elapsed = started.elapsed();
296 if elapsed >= timeout {
297 break;
298 }
299 let remaining = timeout - elapsed;
300 let (new_state, wait_result) = cvar.wait_timeout(state, remaining).unwrap();
301 state = new_state;
302 if wait_result.timed_out() {
303 break;
304 }
305 }
306 let final_state = *state;
307 drop(state);
308
309 if final_state == ServerState::Running {
310 self.try_recv_startup(lang);
311 }
312
313 Ok(final_state)
314 }
315
316 pub fn any_pending(&self) -> bool {
317 self.servers.values().any(|s| s.get_state().is_pending())
318 }
319
320 pub fn poll_events(&mut self) -> Vec<LspEvent> {
321 let mut events = Vec::new();
322 while let Ok(event) = self.event_rx.try_recv() {
323 events.push(event);
324 }
325 events
326 }
327
328 pub fn shutdown_all(&mut self) {
329 let languages: Vec<Language> = self.servers.keys().copied().collect();
330 for lang in languages {
331 self.shutdown_server(lang);
332 }
333 }
334
335 pub fn install_server(&mut self, lang: Language) -> Result<()> {
336 let event_tx = self.event_tx.clone();
337
338 let server = self
339 .servers
340 .get(&lang)
341 .ok_or_else(|| anyhow::anyhow!("no server registered for {:?}", lang))?;
342
343 let current = server.get_state();
344 if current != ServerState::Missing {
345 bail!(
346 "server for {:?} is not in Missing state (current: {:?})",
347 lang,
348 current
349 );
350 }
351
352 let server_info = server.server_info;
353 let state = Arc::clone(&server.state);
354
355 if try_transition(&state, lang, ServerState::Installing, &event_tx).is_none() {
356 bail!("could not transition to Installing");
357 }
358
359 match installer::install(server_info, self.install_progress.as_ref()) {
360 Ok(()) => {
361 try_transition(&state, lang, ServerState::Available, &event_tx);
362 Ok(())
363 }
364 Err(e) => {
365 try_transition(&state, lang, ServerState::Failed, &event_tx);
366 let _ = event_tx.send(LspEvent::Error {
367 language: lang,
368 error: e.to_string(),
369 });
370 Err(e)
371 }
372 }
373 }
374
375 pub fn restart_server(&mut self, lang: Language) -> Result<()> {
378 let (server_info, root_path) = {
379 let server = self
380 .servers
381 .get(&lang)
382 .ok_or_else(|| anyhow::anyhow!("no server registered for {:?}", lang))?;
383 let s = server.get_state();
384 if !matches!(s, ServerState::Failed | ServerState::Stopped) {
385 bail!(
386 "server for {:?} is not Failed or Stopped (current: {:?})",
387 lang,
388 s
389 );
390 }
391 (server.server_info, server.root_path.clone())
392 };
393 self.shutdown_server(lang);
394 self.servers.remove(&lang);
395 self.spawn_server(lang, server_info, root_path);
396 Ok(())
397 }
398
399 pub fn status_summary(&self) -> Vec<(Language, ServerState)> {
400 self.servers
401 .iter()
402 .map(|(lang, server)| (*lang, server.get_state()))
403 .collect()
404 }
405
406 pub fn document_symbols(&mut self, file_path: &Path) -> Result<Vec<DocumentSymbol>> {
409 #[cfg(any(test, feature = "test-support"))]
410 if let Some(syms) = self.test_symbols.get(file_path) {
411 return Ok(syms.clone());
412 }
413 let lang = self.language_for_file(file_path)?;
414 self.ensure_open(lang, file_path)?;
415 let uri = path_to_uri(file_path);
416 let params = json!({ "textDocument": { "uri": uri } });
417 let result = self.send_request(lang, "textDocument/documentSymbol", params)?;
418 parse_document_symbols(&result)
419 }
420
421 pub fn symbol_at_position(
422 &mut self,
423 file_path: &Path,
424 line: usize,
425 col: usize,
426 ) -> Result<Option<DocumentSymbol>> {
427 let symbols = self.document_symbols(file_path)?;
428 Ok(find_symbol_at_position(&symbols, line, col))
429 }
430
431 pub fn symbol_range(
432 &mut self,
433 file_path: &Path,
434 symbol_name: &str,
435 ) -> Result<Option<SymbolRange>> {
436 let symbols = self.document_symbols(file_path)?;
437 Ok(find_symbol_by_name(&symbols, symbol_name).map(|s| s.range))
438 }
439
440 pub fn notify_document_open(&mut self, file_path: &Path, content: &str) -> Result<()> {
441 let lang = self.language_for_file(file_path)?;
442 self.do_notify_open(lang, file_path, content)?;
443 if let Some(server) = self.servers.get_mut(&lang) {
444 server.opened_files.insert(file_path.to_path_buf());
445 }
446 Ok(())
447 }
448
449 pub fn notify_document_change(
450 &mut self,
451 file_path: &Path,
452 content: &str,
453 version: i32,
454 ) -> Result<()> {
455 let lang = self.language_for_file(file_path)?;
456 self.ensure_open(lang, file_path)?;
457 let uri = path_to_uri(file_path);
458 let params = json!({
459 "textDocument": { "uri": uri, "version": version },
460 "contentChanges": [{ "text": content }]
461 });
462 self.send_notification(lang, "textDocument/didChange", params)
463 }
464
465 pub fn completions(
466 &mut self,
467 file_path: &Path,
468 line: usize,
469 col: usize,
470 ) -> Result<Vec<CompletionItem>> {
471 let lang = self.language_for_file(file_path)?;
472 self.ensure_open(lang, file_path)?;
473 let uri = path_to_uri(file_path);
474 let params = json!({
475 "textDocument": { "uri": uri },
476 "position": { "line": line, "character": col }
477 });
478 let result = self.send_request(lang, "textDocument/completion", params)?;
479 parse_completions(&result)
480 }
481
482 pub fn hover(
483 &mut self,
484 file_path: &Path,
485 line: usize,
486 col: usize,
487 ) -> Result<Option<HoverInfo>> {
488 let lang = self.language_for_file(file_path)?;
489 self.ensure_open(lang, file_path)?;
490 let uri = path_to_uri(file_path);
491 let params = json!({
492 "textDocument": { "uri": uri },
493 "position": { "line": line, "character": col }
494 });
495 let result = self.send_request(lang, "textDocument/hover", params)?;
496 Ok(parse_hover(&result))
497 }
498
499 pub fn goto_definition(
500 &mut self,
501 file_path: &Path,
502 line: usize,
503 col: usize,
504 ) -> Result<Option<Location>> {
505 let lang = self.language_for_file(file_path)?;
506 self.ensure_open(lang, file_path)?;
507 let uri = path_to_uri(file_path);
508 let params = json!({
509 "textDocument": { "uri": uri },
510 "position": { "line": line, "character": col }
511 });
512 let result = self.send_request(lang, "textDocument/definition", params)?;
513 Ok(parse_location(&result))
514 }
515
516 pub fn semantic_tokens(
517 &mut self,
518 file_path: &Path,
519 content: &str,
520 ) -> Result<Vec<SemanticToken>> {
521 #[cfg(any(test, feature = "test-support"))]
522 if let Some(tokens) = self.test_semantic_tokens.get(file_path) {
523 if let Some(delay) = self.test_semantic_tokens_delay {
524 thread::sleep(delay);
525 }
526 return Ok(tokens.clone());
527 }
528 let lang = self.language_for_file(file_path)?;
529 self.ensure_open(lang, file_path)?;
530 let _ = self.notify_document_change(file_path, content, 2);
531 let uri = path_to_uri(file_path);
532 let params = json!({ "textDocument": { "uri": uri } });
533 let result = self.send_request(lang, "textDocument/semanticTokens/full", params)?;
534 parse_semantic_tokens(&result)
535 }
536
537 pub fn selection_range(
538 &mut self,
539 file_path: &Path,
540 line: usize,
541 col: usize,
542 ) -> Result<SelectionRange> {
543 #[cfg(any(test, feature = "test-support"))]
544 if let Some(sr) = self
545 .test_selection_ranges
546 .get(&(file_path.to_path_buf(), line, col))
547 {
548 return Ok(sr.clone());
549 }
550 let lang = self.language_for_file(file_path)?;
551 self.ensure_open(lang, file_path)?;
552 let uri = path_to_uri(file_path);
553 let params = json!({
554 "textDocument": { "uri": uri },
555 "positions": [{ "line": line, "character": col }]
556 });
557 let result = self.send_request(lang, "textDocument/selectionRange", params)?;
558 parse_selection_range(&result)
559 }
560
561 fn try_recv_startup(&mut self, lang: Language) {
564 if let Some(server) = self.servers.get_mut(&lang)
565 && let Some(ref rx) = server.startup_rx
566 {
567 match rx.try_recv() {
568 Ok(transport) => {
569 server.transport = Some(transport);
570 server.startup_rx = None;
571 }
572 Err(mpsc::TryRecvError::Disconnected) => {
573 server.startup_rx = None;
574 }
575 Err(mpsc::TryRecvError::Empty) => {}
576 }
577 }
578 }
579
580 fn language_for_file(&self, file_path: &Path) -> Result<Language> {
581 registry::detect_language_from_path(file_path)
582 .ok_or_else(|| anyhow::anyhow!("no language detected for {}", file_path.display()))
583 }
584
585 fn send_request(&mut self, lang: Language, method: &str, params: Value) -> Result<Value> {
588 self.try_recv_startup(lang);
589 let result = {
590 let transport = self.get_transport(lang)?;
591 transport.send_request(method, params)
592 };
593 if result.is_err() {
594 self.detect_and_report_crash(lang);
595 }
596 result
597 }
598
599 fn send_notification(&mut self, lang: Language, method: &str, params: Value) -> Result<()> {
600 self.try_recv_startup(lang);
601 let result = {
602 let transport = self.get_transport(lang)?;
603 transport.send_notification(method, params)
604 };
605 if result.is_err() {
606 self.detect_and_report_crash(lang);
607 }
608 result
609 }
610
611 fn get_transport(&mut self, lang: Language) -> Result<&mut LspTransport> {
612 let exited = match self.servers.get(&lang) {
614 Some(server) => {
615 let mut child = server.child.lock().unwrap();
616 child
617 .as_mut()
618 .map(|c| matches!(health::check_process(c), HealthStatus::ProcessExited(_)))
619 .unwrap_or(false)
620 }
621 None => bail!("no server for {:?}", lang),
622 };
623 if exited {
624 let current = self
625 .servers
626 .get(&lang)
627 .map(|s| s.get_state())
628 .unwrap_or(ServerState::Failed);
629 self.report_crash(lang, current, Some(None));
630 bail!("LSP server for {:?} has exited", lang);
631 }
632
633 let server = self
634 .servers
635 .get_mut(&lang)
636 .ok_or_else(|| anyhow::anyhow!("no server for {:?}", lang))?;
637 let current_state = server.get_state();
638 server.transport.as_mut().ok_or_else(|| {
639 anyhow::anyhow!(
640 "LSP server for {:?} is not running (state: {:?})",
641 lang,
642 current_state
643 )
644 })
645 }
646
647 fn detect_and_report_crash(&mut self, lang: Language) {
650 let exited = {
651 if let Some(server) = self.servers.get(&lang) {
652 let mut child = server.child.lock().unwrap();
653 child
654 .as_mut()
655 .map(|c| match health::check_process(c) {
656 HealthStatus::ProcessExited(code) => Some(code),
657 HealthStatus::Healthy => None,
658 })
659 .unwrap_or(Some(None))
660 } else {
661 return;
662 }
663 };
664 if let Some(code) = exited {
665 let current = self.servers.get(&lang).map(|s| s.get_state());
666 if let Some(s) = current {
667 self.report_crash(lang, s, Some(code));
668 }
669 } else {
670 let current = self.servers.get(&lang).map(|s| s.get_state());
673 if let Some(s) = current {
674 self.report_crash(lang, s, None);
675 }
676 }
677 }
678
679 fn report_crash(&mut self, lang: Language, current: ServerState, code: Option<Option<i32>>) {
680 if let Some(server) = self.servers.get_mut(&lang) {
681 let state_arc = Arc::clone(&server.state);
682 let event_tx = self.event_tx.clone();
683 server.transport = None;
685 server.opened_files.clear();
686 if let Some(mut c) = server.child.lock().unwrap().take() {
688 let _ = c.wait();
689 }
690 if !current.is_terminal() || current == ServerState::Running {
691 try_transition(&state_arc, lang, ServerState::Failed, &event_tx);
692 }
693 let msg = match code {
694 Some(Some(c)) => format!("LSP process exited with code {}", c),
695 Some(None) => "LSP process exited (no status)".to_string(),
696 None => "LSP transport failure".to_string(),
697 };
698 let _ = event_tx.send(LspEvent::Error {
699 language: lang,
700 error: msg,
701 });
702 }
703 }
704
705 fn shutdown_server(&mut self, lang: Language) {
706 let server = match self.servers.get_mut(&lang) {
707 Some(s) => s,
708 None => return,
709 };
710 let state_arc = Arc::clone(&server.state);
711 let event_tx = self.event_tx.clone();
712
713 if let Some(mut transport) = server.transport.take() {
717 let (done_tx, done_rx) = mpsc::channel::<()>();
718 let handle = thread::spawn(move || {
719 let _ = transport.send_request("shutdown", Value::Null);
720 let _ = transport.send_notification("exit", Value::Null);
721 let _ = done_tx.send(());
722 });
723 let _ = done_rx.recv_timeout(SHUTDOWN_GRACE);
724 if handle.is_finished() {
728 let _ = handle.join();
729 }
730 }
731
732 if let Some(mut child) = server.child.lock().unwrap().take() {
734 let _ = child.kill();
735 let _ = child.wait();
736 }
737 server.opened_files.clear();
738 server.startup_rx = None;
739
740 try_transition(&state_arc, lang, ServerState::Stopped, &event_tx);
743 }
744
745 fn ensure_open(&mut self, lang: Language, file_path: &Path) -> Result<()> {
746 let already_open = self
747 .servers
748 .get(&lang)
749 .map(|s| s.opened_files.contains(file_path))
750 .unwrap_or(false);
751 if already_open {
752 return Ok(());
753 }
754 let content = std::fs::read_to_string(file_path).unwrap_or_default();
755 self.do_notify_open(lang, file_path, &content)?;
756 if let Some(server) = self.servers.get_mut(&lang) {
757 server.opened_files.insert(file_path.to_path_buf());
758 }
759 Ok(())
760 }
761
762 fn do_notify_open(&mut self, lang: Language, file_path: &Path, content: &str) -> Result<()> {
763 let uri = path_to_uri(file_path);
764 let language_id = Language::language_id_for_path(file_path).unwrap_or(lang.name());
765 #[cfg(any(test, feature = "test-support"))]
766 self.did_open_log
767 .push((file_path.to_path_buf(), language_id.to_string()));
768 let params = json!({
769 "textDocument": {
770 "uri": uri,
771 "languageId": language_id,
772 "version": 1,
773 "text": content
774 }
775 });
776 self.send_notification(lang, "textDocument/didOpen", params)
777 }
778}
779
780impl Drop for LspEngine {
781 fn drop(&mut self) {
782 self.shutdown_all();
783 }
784}
785
786fn startup_thread(
789 ctx: StartupContext,
790 state: Arc<(Mutex<ServerState>, Condvar)>,
791 child_slot: Arc<Mutex<Option<Child>>>,
792 transport_tx: mpsc::Sender<LspTransport>,
793 event_tx: mpsc::Sender<LspEvent>,
794) {
795 let lang = ctx.lang;
796 let server_info = ctx.server_info;
797
798 let installed = if let Some(ref check_cmd) = ctx.overrides.check_command {
800 Command::new("sh")
801 .arg("-c")
802 .arg(check_cmd)
803 .output()
804 .map(|o| o.status.success())
805 .unwrap_or(false)
806 } else {
807 installer::is_installed(server_info)
808 };
809
810 if !installed {
811 if ctx.auto_install {
812 let _guard = ctx.install_lock.lock().unwrap();
813 if !installer::is_installed(server_info) {
815 try_transition(&state, lang, ServerState::Installing, &event_tx);
816 if let Err(e) = installer::install(server_info, ctx.install_progress.as_ref()) {
817 try_transition(&state, lang, ServerState::Failed, &event_tx);
818 let _ = event_tx.send(LspEvent::Error {
819 language: lang,
820 error: format!("install failed: {}", e),
821 });
822 return;
823 }
824 }
825 drop(_guard);
827 try_transition(&state, lang, ServerState::Available, &event_tx);
828 } else {
829 try_transition(&state, lang, ServerState::Missing, &event_tx);
830 return;
831 }
832 } else {
833 try_transition(&state, lang, ServerState::Available, &event_tx);
834 }
835
836 try_transition(&state, lang, ServerState::Starting, &event_tx);
837
838 let init_options: Value = if server_info.init_options_json.is_empty() {
841 Value::Null
842 } else {
843 match serde_json::from_str(server_info.init_options_json) {
844 Ok(v) => v,
845 Err(e) => {
846 try_transition(&state, lang, ServerState::Failed, &event_tx);
847 let _ = event_tx.send(LspEvent::Error {
848 language: lang,
849 error: format!(
850 "invalid init_options_json for {}: {}",
851 server_info.server_name, e
852 ),
853 });
854 return;
855 }
856 }
857 };
858
859 let binary = ctx
860 .overrides
861 .binary_name
862 .as_deref()
863 .unwrap_or(server_info.binary_name);
864 let mut cmd = Command::new(binary);
865 if ctx.overrides.binary_name.is_some() {
866 cmd.args(&ctx.overrides.binary_args);
867 } else {
868 cmd.args(server_info.default_args);
869 }
870 cmd.stdin(Stdio::piped())
871 .stdout(Stdio::piped())
872 .stderr(Stdio::null());
873
874 let mut child = match cmd.spawn() {
875 Ok(child) => child,
876 Err(e) => {
877 try_transition(&state, lang, ServerState::Failed, &event_tx);
878 let _ = event_tx.send(LspEvent::Error {
879 language: lang,
880 error: format!("failed to spawn {}: {}", binary, e),
881 });
882 return;
883 }
884 };
885
886 let stdin = match child.stdin.take() {
887 Some(s) => s,
888 None => {
889 try_transition(&state, lang, ServerState::Failed, &event_tx);
890 let _ = child.kill();
891 let _ = child.wait();
892 return;
893 }
894 };
895 let stdout = match child.stdout.take() {
896 Some(s) => s,
897 None => {
898 try_transition(&state, lang, ServerState::Failed, &event_tx);
899 let _ = child.kill();
900 let _ = child.wait();
901 return;
902 }
903 };
904
905 *child_slot.lock().unwrap() = Some(child);
907
908 let watchdog_state = Arc::clone(&state);
911 let watchdog_child = Arc::clone(&child_slot);
912 let timeout = ctx.startup_timeout;
913 let watchdog_event_tx = event_tx.clone();
914 thread::spawn(move || {
915 let deadline = Instant::now() + timeout;
916 let (lock, cvar) = &*watchdog_state;
917 let mut s = lock.lock().unwrap();
918 while !matches!(
919 *s,
920 ServerState::Running | ServerState::Stopped | ServerState::Failed
921 ) {
922 let now = Instant::now();
923 if now >= deadline {
924 break;
925 }
926 let (new_s, _) = cvar.wait_timeout(s, deadline - now).unwrap();
927 s = new_s;
928 }
929 let still_pending = !matches!(
930 *s,
931 ServerState::Running | ServerState::Stopped | ServerState::Failed
932 );
933 drop(s);
934 if still_pending {
935 if let Some(mut c) = watchdog_child.lock().unwrap().take() {
936 let _ = c.kill();
937 let _ = c.wait();
938 }
939 let _ = watchdog_event_tx.send(LspEvent::Error {
940 language: lang,
941 error: format!("startup timed out after {:?}", timeout),
942 });
943 }
944 });
945
946 let mut transport = LspTransport::new(stdin, stdout);
947
948 let root_uri = path_to_uri(&ctx.root_path);
949 let init_params = json!({
950 "processId": std::process::id(),
951 "rootUri": root_uri,
952 "capabilities": {
953 "textDocument": {
954 "documentSymbol": { "hierarchicalDocumentSymbolSupport": true }
955 }
956 },
957 "initializationOptions": init_options
958 });
959
960 if let Err(e) = transport.send_request("initialize", init_params) {
961 try_transition(&state, lang, ServerState::Failed, &event_tx);
962 if let Some(mut c) = child_slot.lock().unwrap().take() {
963 let _ = c.kill();
964 let _ = c.wait();
965 }
966 let _ = event_tx.send(LspEvent::Error {
967 language: lang,
968 error: format!("initialize handshake failed: {}", e),
969 });
970 return;
971 }
972
973 if let Err(e) = transport.send_notification("initialized", json!({})) {
974 try_transition(&state, lang, ServerState::Failed, &event_tx);
975 if let Some(mut c) = child_slot.lock().unwrap().take() {
976 let _ = c.kill();
977 let _ = c.wait();
978 }
979 let _ = event_tx.send(LspEvent::Error {
980 language: lang,
981 error: format!("initialized notification failed: {}", e),
982 });
983 return;
984 }
985
986 if transport_tx.send(transport).is_err() {
987 if let Some(mut c) = child_slot.lock().unwrap().take() {
989 let _ = c.kill();
990 let _ = c.wait();
991 }
992 return;
993 }
994
995 try_transition(&state, lang, ServerState::Running, &event_tx);
996}
997
998fn path_to_uri(path: &Path) -> String {
1001 let abs = if path.is_absolute() {
1002 path.to_path_buf()
1003 } else {
1004 std::env::current_dir().unwrap_or_default().join(path)
1005 };
1006 let s = abs.to_string_lossy();
1007 let mut encoded = String::with_capacity(s.len() + 7);
1008 encoded.push_str("file://");
1009 for byte in s.bytes() {
1010 match byte {
1011 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' | b':' => {
1012 encoded.push(byte as char)
1013 }
1014 _ => {
1015 use std::fmt::Write;
1016 let _ = write!(&mut encoded, "%{:02X}", byte);
1017 }
1018 }
1019 }
1020 encoded
1021}
1022
1023fn parse_document_symbols(value: &Value) -> Result<Vec<DocumentSymbol>> {
1026 match value {
1027 Value::Array(arr) => {
1028 let mut symbols = Vec::new();
1029 for item in arr {
1030 if let Some(sym) = parse_single_symbol(item) {
1031 symbols.push(sym);
1032 }
1033 }
1034 Ok(symbols)
1035 }
1036 Value::Null => Ok(Vec::new()),
1037 _ => bail!("unexpected documentSymbol response format"),
1038 }
1039}
1040
1041fn parse_single_symbol(value: &Value) -> Option<DocumentSymbol> {
1042 let name = value.get("name")?.as_str()?.to_string();
1043 let kind_num = value.get("kind")?.as_u64()? as u32;
1044 let range = parse_range(value.get("range")?)?;
1045
1046 let children = value
1047 .get("children")
1048 .and_then(|c| c.as_array())
1049 .map(|arr| arr.iter().filter_map(parse_single_symbol).collect())
1050 .unwrap_or_default();
1051
1052 let selection_range = value.get("selectionRange").and_then(parse_range);
1053
1054 Some(DocumentSymbol {
1055 name,
1056 kind: convert_symbol_kind(kind_num),
1057 range,
1058 selection_range,
1059 children,
1060 })
1061}
1062
1063fn parse_range(value: &Value) -> Option<SymbolRange> {
1064 let start = value.get("start")?;
1065 let end = value.get("end")?;
1066 Some(SymbolRange {
1067 start_line: start.get("line")?.as_u64()? as usize,
1068 start_col: start.get("character")?.as_u64()? as usize,
1069 end_line: end.get("line")?.as_u64()? as usize,
1070 end_col: end.get("character")?.as_u64()? as usize,
1071 })
1072}
1073
1074fn convert_symbol_kind(kind: u32) -> SymbolKind {
1075 match kind {
1076 1 | 2 => SymbolKind::Module,
1077 5 => SymbolKind::Struct,
1078 6 => SymbolKind::Method,
1079 8 => SymbolKind::Field,
1080 10 => SymbolKind::Enum,
1081 12 => SymbolKind::Function,
1082 13 => SymbolKind::Variable,
1083 14 => SymbolKind::Const,
1084 23 => SymbolKind::Struct,
1085 _ => SymbolKind::Other(format!("kind_{}", kind)),
1086 }
1087}
1088
1089fn find_symbol_at_position(
1090 symbols: &[DocumentSymbol],
1091 line: usize,
1092 col: usize,
1093) -> Option<DocumentSymbol> {
1094 for sym in symbols {
1095 if contains_position(&sym.range, line, col) {
1096 if let Some(child) = find_symbol_at_position(&sym.children, line, col) {
1097 return Some(child);
1098 }
1099 return Some(sym.clone());
1100 }
1101 }
1102 None
1103}
1104
1105fn contains_position(range: &SymbolRange, line: usize, col: usize) -> bool {
1106 if line < range.start_line || line > range.end_line {
1107 return false;
1108 }
1109 if line == range.start_line && col < range.start_col {
1110 return false;
1111 }
1112 if line == range.end_line && col > range.end_col {
1113 return false;
1114 }
1115 true
1116}
1117
1118fn find_symbol_by_name<'a>(
1119 symbols: &'a [DocumentSymbol],
1120 name: &str,
1121) -> Option<&'a DocumentSymbol> {
1122 for sym in symbols {
1123 if sym.name == name {
1124 return Some(sym);
1125 }
1126 if let Some(found) = find_symbol_by_name(&sym.children, name) {
1127 return Some(found);
1128 }
1129 }
1130 None
1131}
1132
1133fn parse_completions(value: &Value) -> Result<Vec<CompletionItem>> {
1134 let items = if let Some(arr) = value.as_array() {
1135 arr
1136 } else if let Some(items) = value.get("items").and_then(|v| v.as_array()) {
1137 items
1138 } else if value.is_null() {
1139 return Ok(Vec::new());
1140 } else {
1141 bail!("unexpected completion response format");
1142 };
1143
1144 Ok(items
1145 .iter()
1146 .filter_map(|item| {
1147 let label = item.get("label")?.as_str()?.to_string();
1148 let detail = item
1149 .get("detail")
1150 .and_then(|d| d.as_str())
1151 .map(String::from);
1152 let kind = item
1153 .get("kind")
1154 .and_then(|k| k.as_u64())
1155 .map(|k| format!("{}", k));
1156 Some(CompletionItem {
1157 label,
1158 detail,
1159 kind,
1160 })
1161 })
1162 .collect())
1163}
1164
1165fn parse_hover(value: &Value) -> Option<HoverInfo> {
1166 if value.is_null() {
1167 return None;
1168 }
1169
1170 let contents = value.get("contents")?;
1171 let text = if let Some(s) = contents.as_str() {
1172 s.to_string()
1173 } else if let Some(obj) = contents.as_object() {
1174 obj.get("value")?.as_str()?.to_string()
1175 } else if let Some(arr) = contents.as_array() {
1176 arr.iter()
1177 .filter_map(|v| {
1178 v.as_str()
1179 .map(String::from)
1180 .or_else(|| v.get("value").and_then(|v| v.as_str()).map(String::from))
1181 })
1182 .collect::<Vec<_>>()
1183 .join("\n")
1184 } else {
1185 return None;
1186 };
1187
1188 Some(HoverInfo { contents: text })
1189}
1190
1191const STANDARD_TOKEN_TYPES: &[&str] = &[
1192 "namespace",
1193 "type",
1194 "class",
1195 "enum",
1196 "interface",
1197 "struct",
1198 "typeParameter",
1199 "parameter",
1200 "variable",
1201 "property",
1202 "enumMember",
1203 "event",
1204 "function",
1205 "method",
1206 "macro",
1207 "keyword",
1208 "modifier",
1209 "comment",
1210 "string",
1211 "number",
1212 "regexp",
1213 "operator",
1214 "decorator",
1215];
1216
1217fn parse_semantic_tokens(value: &Value) -> Result<Vec<SemanticToken>> {
1218 let data = match value.get("data").and_then(|d| d.as_array()) {
1219 Some(arr) => arr,
1220 None => return Ok(Vec::new()),
1221 };
1222
1223 let nums: Vec<usize> = data
1224 .iter()
1225 .filter_map(|v| v.as_u64().map(|n| n as usize))
1226 .collect();
1227
1228 if !nums.len().is_multiple_of(5) {
1229 return Ok(Vec::new());
1230 }
1231
1232 let mut tokens = Vec::new();
1233 let mut current_line: usize = 0;
1234 let mut current_col: usize = 0;
1235
1236 for chunk in nums.chunks(5) {
1237 let delta_line = chunk[0];
1238 let delta_start = chunk[1];
1239 let length = chunk[2];
1240 let token_type_idx = chunk[3];
1241
1242 if delta_line > 0 {
1243 current_line += delta_line;
1244 current_col = delta_start;
1245 } else {
1246 current_col += delta_start;
1247 }
1248
1249 let token_type = STANDARD_TOKEN_TYPES
1250 .get(token_type_idx)
1251 .unwrap_or(&"unknown")
1252 .to_string();
1253
1254 tokens.push(SemanticToken {
1255 line: current_line,
1256 start_col: current_col,
1257 length,
1258 token_type,
1259 });
1260 }
1261
1262 Ok(tokens)
1263}
1264
1265fn parse_location(value: &Value) -> Option<Location> {
1266 if value.is_null() {
1267 return None;
1268 }
1269
1270 let loc = if let Some(arr) = value.as_array() {
1271 arr.first()?
1272 } else {
1273 value
1274 };
1275
1276 let uri = loc.get("uri")?.as_str()?;
1277 let file_path = uri.strip_prefix("file://").map(PathBuf::from)?;
1278 let range = parse_range(loc.get("range")?)?;
1279
1280 Some(Location { file_path, range })
1281}
1282
1283fn parse_selection_range(value: &Value) -> Result<SelectionRange> {
1284 let item = if let Some(arr) = value.as_array() {
1285 arr.first()
1286 .ok_or_else(|| anyhow::anyhow!("empty selectionRange response"))?
1287 } else {
1288 value
1289 };
1290 parse_selection_range_node(item)
1291 .ok_or_else(|| anyhow::anyhow!("failed to parse selectionRange"))
1292}
1293
1294fn parse_selection_range_node(value: &Value) -> Option<SelectionRange> {
1295 let range = parse_range(value.get("range")?)?;
1296 let parent = value
1297 .get("parent")
1298 .filter(|v| !v.is_null())
1299 .and_then(|v| parse_selection_range_node(v).map(Box::new));
1300 Some(SelectionRange { range, parent })
1301}
1302
1303#[cfg(test)]
1304mod tests {
1305 use super::*;
1306 use std::time::Duration;
1307
1308 #[test]
1309 fn auto_install_true_by_default() {
1310 assert!(LspEngineConfig::default().auto_install);
1311 }
1312
1313 #[test]
1314 fn server_state_undetected_when_no_server_registered() {
1315 let engine = LspEngine::new(LspEngineConfig::default());
1316 assert_eq!(engine.server_state(Language::Rust), ServerState::Undetected);
1317 }
1318
1319 #[test]
1320 fn any_pending_false_with_no_servers() {
1321 let engine = LspEngine::new(LspEngineConfig::default());
1322 assert!(!engine.any_pending());
1323 }
1324
1325 #[test]
1326 fn await_ready_errors_for_unregistered_language() {
1327 let mut engine = LspEngine::new(LspEngineConfig::default());
1328 let result = engine.await_ready(Language::Rust, Duration::from_millis(100));
1329 assert!(result.is_err());
1330 }
1331
1332 #[test]
1333 fn install_server_errors_for_unregistered_language() {
1334 let mut engine = LspEngine::new(LspEngineConfig::default());
1335 let result = engine.install_server(Language::Rust);
1336 assert!(result.is_err());
1337 assert!(
1338 result
1339 .unwrap_err()
1340 .to_string()
1341 .contains("no server registered")
1342 );
1343 }
1344
1345 #[test]
1346 fn restart_server_errors_for_unregistered_language() {
1347 let mut engine = LspEngine::new(LspEngineConfig::default());
1348 let result = engine.restart_server(Language::Rust);
1349 assert!(result.is_err());
1350 }
1351
1352 #[test]
1353 fn parse_document_symbols_handles_null() {
1354 let result = parse_document_symbols(&serde_json::Value::Null).unwrap();
1355 assert!(result.is_empty());
1356 }
1357
1358 #[test]
1359 fn parse_document_symbols_handles_array() {
1360 let json = serde_json::json!([{
1361 "name": "my_fn",
1362 "kind": 12,
1363 "range": {
1364 "start": {"line": 0, "character": 0},
1365 "end": {"line": 5, "character": 1}
1366 },
1367 "children": []
1368 }]);
1369 let symbols = parse_document_symbols(&json).unwrap();
1370 assert_eq!(symbols.len(), 1);
1371 assert_eq!(symbols[0].name, "my_fn");
1372 assert_eq!(symbols[0].kind, SymbolKind::Function);
1373 }
1374
1375 #[test]
1376 fn parse_document_symbols_rejects_unexpected_format() {
1377 let result = parse_document_symbols(&serde_json::json!({"not": "an array"}));
1378 assert!(result.is_err());
1379 }
1380
1381 #[test]
1382 fn symbol_at_position_finds_innermost() {
1383 let outer = DocumentSymbol {
1384 name: "outer".to_string(),
1385 kind: SymbolKind::Function,
1386 range: SymbolRange {
1387 start_line: 0,
1388 start_col: 0,
1389 end_line: 10,
1390 end_col: 1,
1391 },
1392 selection_range: None,
1393 children: vec![DocumentSymbol {
1394 name: "inner".to_string(),
1395 kind: SymbolKind::Variable,
1396 range: SymbolRange {
1397 start_line: 3,
1398 start_col: 4,
1399 end_line: 3,
1400 end_col: 20,
1401 },
1402 selection_range: None,
1403 children: vec![],
1404 }],
1405 };
1406 let result = find_symbol_at_position(&[outer], 3, 10);
1407 assert_eq!(result.unwrap().name, "inner");
1408 }
1409
1410 #[test]
1411 fn symbol_at_position_returns_parent_when_no_child_matches() {
1412 let sym = DocumentSymbol {
1413 name: "parent".to_string(),
1414 kind: SymbolKind::Function,
1415 range: SymbolRange {
1416 start_line: 0,
1417 start_col: 0,
1418 end_line: 10,
1419 end_col: 1,
1420 },
1421 selection_range: None,
1422 children: vec![],
1423 };
1424 let result = find_symbol_at_position(&[sym], 5, 0);
1425 assert_eq!(result.unwrap().name, "parent");
1426 }
1427
1428 #[test]
1429 fn symbol_at_position_returns_none_outside_all_symbols() {
1430 let sym = DocumentSymbol {
1431 name: "fn1".to_string(),
1432 kind: SymbolKind::Function,
1433 range: SymbolRange {
1434 start_line: 0,
1435 start_col: 0,
1436 end_line: 5,
1437 end_col: 1,
1438 },
1439 selection_range: None,
1440 children: vec![],
1441 };
1442 let result = find_symbol_at_position(&[sym], 10, 0);
1443 assert!(result.is_none());
1444 }
1445
1446 #[test]
1447 fn convert_symbol_kind_maps_known_kinds() {
1448 assert_eq!(convert_symbol_kind(12), SymbolKind::Function);
1449 assert_eq!(convert_symbol_kind(5), SymbolKind::Struct);
1450 assert_eq!(convert_symbol_kind(13), SymbolKind::Variable);
1451 assert_eq!(convert_symbol_kind(10), SymbolKind::Enum);
1452 assert_eq!(convert_symbol_kind(6), SymbolKind::Method);
1453 assert_eq!(convert_symbol_kind(8), SymbolKind::Field);
1454 assert_eq!(convert_symbol_kind(14), SymbolKind::Const);
1455 assert!(matches!(convert_symbol_kind(99), SymbolKind::Other(_)));
1456 }
1457
1458 #[test]
1459 fn path_to_uri_encodes_spaces_and_specials() {
1460 let p = std::path::Path::new("/tmp/has space/foo#bar.rs");
1461 let uri = path_to_uri(p);
1462 assert!(uri.starts_with("file:///tmp/has%20space/foo%23bar.rs"));
1463 }
1464
1465 #[test]
1466 fn path_to_uri_passes_through_safe_chars() {
1467 let p = std::path::Path::new("/tmp/normal_path-1.rs");
1468 let uri = path_to_uri(p);
1469 assert_eq!(uri, "file:///tmp/normal_path-1.rs");
1470 }
1471
1472 #[test]
1473 fn do_notify_open_tsx_sends_typescriptreact_language_id() {
1474 let tmp = tempfile::tempdir().unwrap();
1475 let tsx_file = tmp.path().join("app.tsx");
1476 let mut engine = LspEngine::new(LspEngineConfig::default());
1477 let _ = engine.semantic_tokens(&tsx_file, "export default function App() {}");
1479 assert!(
1480 engine
1481 .did_open_log
1482 .iter()
1483 .any(|(p, id)| p == &tsx_file && id == "typescriptreact"),
1484 "did_open_log should contain typescriptreact for .tsx; got: {:?}",
1485 engine.did_open_log
1486 );
1487 }
1488
1489 #[test]
1490 fn install_lock_serializes_concurrent_installs() {
1491 use std::time::Instant;
1492
1493 let lock = Arc::new(Mutex::new(()));
1494 let log: Arc<Mutex<Vec<(u64, u64)>>> = Arc::new(Mutex::new(Vec::new()));
1495
1496 let start_epoch = Instant::now();
1497 let threads: Vec<_> = (0..2)
1498 .map(|_| {
1499 let lock = Arc::clone(&lock);
1500 let log = Arc::clone(&log);
1501 let epoch = start_epoch;
1502 thread::spawn(move || {
1503 let _guard = lock.lock().unwrap();
1504 let start = epoch.elapsed().as_millis() as u64;
1505 thread::sleep(Duration::from_millis(30));
1506 let end = epoch.elapsed().as_millis() as u64;
1507 log.lock().unwrap().push((start, end));
1508 })
1509 })
1510 .collect();
1511
1512 for t in threads {
1513 t.join().unwrap();
1514 }
1515
1516 let log = log.lock().unwrap();
1517 assert_eq!(log.len(), 2);
1518 let (s0, e0) = log[0];
1519 let (s1, e1) = log[1];
1520 let overlaps = s0 < e1 && s1 < e0;
1521 assert!(
1522 !overlaps,
1523 "install intervals overlap: [{s0},{e0}) and [{s1},{e1})"
1524 );
1525 }
1526}