1use crate::analyzer::Analyzer;
2use std::cell::RefCell;
3use std::io::{Read, Write};
4use std::path::{Path, PathBuf};
5use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
6
7use crate::api::{PropagationResult, PropagationSource};
8use crate::treesitter::TreeSitterAnalyzer;
9
10pub trait LspServerConfig: Send + Sync {
18 fn server_name(&self) -> &str;
20
21 fn binary_name(&self) -> &str;
23
24 fn language_id(&self) -> &str;
26
27 fn cached_binary_name(&self) -> String;
29
30 fn download_url(&self) -> Option<String>;
32
33 fn init_params_extra(&self, _root_uri: &str) -> serde_json::Value {
35 serde_json::json!({})
36 }
37
38 fn post_init_delay_secs(&self) -> u64 {
40 3
41 }
42
43 fn spawn_args(&self) -> Vec<String> {
45 vec![]
46 }
47
48 fn install_command(&self) -> Option<(String, Vec<String>)> {
52 None
53 }
54
55 fn setup_hints(&self) -> String {
58 format!(
59 "LSP server '{}' (binary: '{}') for language '{}'. ",
60 self.server_name(),
61 self.binary_name(),
62 self.language_id()
63 )
64 }
65}
66
67pub struct RustAnalyzerConfig;
70
71const RA_VERSION: &str = "2025-05-05";
72
73impl LspServerConfig for RustAnalyzerConfig {
74 fn server_name(&self) -> &str {
75 "rust-analyzer"
76 }
77 fn binary_name(&self) -> &str {
78 "rust-analyzer"
79 }
80 fn language_id(&self) -> &str {
81 "rust"
82 }
83 fn cached_binary_name(&self) -> String {
84 format!("rust-analyzer-{RA_VERSION}")
85 }
86 fn download_url(&self) -> Option<String> {
87 Some(format!(
88 "https://github.com/rust-lang/rust-analyzer/releases/download/{RA_VERSION}/rust-analyzer-x86_64-apple-darwin"
89 ))
90 }
91
92 fn setup_hints(&self) -> String {
93 "For Rust: rust-analyzer is auto-downloaded by scope-engine. No manual setup needed."
94 .to_string()
95 }
96}
97
98pub struct PyrightConfig;
101
102impl LspServerConfig for PyrightConfig {
103 fn server_name(&self) -> &str {
104 "pyright-langserver"
105 }
106 fn binary_name(&self) -> &str {
107 "pyright-langserver"
108 }
109 fn language_id(&self) -> &str {
110 "python"
111 }
112 fn cached_binary_name(&self) -> String {
113 "pyright-langserver".to_string()
114 }
115 fn download_url(&self) -> Option<String> {
116 None
117 } fn spawn_args(&self) -> Vec<String> {
119 vec!["--stdio".to_string()]
120 }
121 fn post_init_delay_secs(&self) -> u64 {
122 2
123 }
124
125 fn setup_hints(&self) -> String {
126 "For Python: install pyright-langserver via 'npm install -g pyright' or 'pip install pyright'.".to_string()
127 }
128}
129
130pub struct TsJsConfig;
133
134impl LspServerConfig for TsJsConfig {
135 fn server_name(&self) -> &str {
136 "typescript-language-server"
137 }
138 fn binary_name(&self) -> &str {
139 "typescript-language-server"
140 }
141 fn language_id(&self) -> &str {
142 "typescript"
143 }
144 fn cached_binary_name(&self) -> String {
145 "typescript-language-server".to_string()
146 }
147 fn download_url(&self) -> Option<String> {
148 None
149 } fn spawn_args(&self) -> Vec<String> {
151 vec!["--stdio".to_string()]
152 }
153 fn post_init_delay_secs(&self) -> u64 {
154 3
155 }
156
157 fn setup_hints(&self) -> String {
158 "For TypeScript/JavaScript: install typescript-language-server via 'npm install -g typescript-language-server typescript'.".to_string()
159 }
160}
161
162const GOPLS_VERSION: &str = "v0.21.1";
165
166pub struct GoplsConfig;
167
168impl LspServerConfig for GoplsConfig {
169 fn server_name(&self) -> &str {
170 "gopls"
171 }
172 fn binary_name(&self) -> &str {
173 "gopls"
174 }
175 fn language_id(&self) -> &str {
176 "go"
177 }
178 fn cached_binary_name(&self) -> String {
179 format!("gopls-{GOPLS_VERSION}")
180 }
181 fn download_url(&self) -> Option<String> {
182 None
183 }
184 fn spawn_args(&self) -> Vec<String> {
185 vec!["serve".to_string()]
186 }
187 fn post_init_delay_secs(&self) -> u64 {
188 4
189 }
190 fn install_command(&self) -> Option<(String, Vec<String>)> {
191 Some((
192 "go".to_string(),
193 vec![
194 "install".to_string(),
195 format!("golang.org/x/tools/gopls@{GOPLS_VERSION}"),
196 ],
197 ))
198 }
199}
200
201pub struct JdtlsConfig;
204
205impl LspServerConfig for JdtlsConfig {
206 fn server_name(&self) -> &str {
207 "jdtls"
208 }
209 fn binary_name(&self) -> &str {
210 "jdtls"
211 }
212 fn language_id(&self) -> &str {
213 "java"
214 }
215 fn cached_binary_name(&self) -> String {
216 "jdtls".to_string()
217 }
218 fn download_url(&self) -> Option<String> {
219 None
220 }
221 fn spawn_args(&self) -> Vec<String> {
222 vec![]
223 }
224 fn post_init_delay_secs(&self) -> u64 {
225 5
226 }
227
228 fn setup_hints(&self) -> String {
229 "For Java: install Eclipse JDT Language Server (jdtls). On macOS: 'brew install eclipse-jdtls'. On Linux: download from https://download.eclipse.org/jdtls/snapshots/ and add 'jdtls' to PATH. Requires JDK 17+.".to_string()
230 }
231}
232
233struct LspClientInner {
237 process: Option<Child>,
238 stdin_writer: Option<BufWriter<ChildStdin>>,
239 stdout_reader: Option<std::io::BufReader<ChildStdout>>,
240 next_id: u64,
241 initialized: bool,
242 language_id: String,
244}
245
246pub struct LspClient {
258 inner: RefCell<LspClientInner>,
259}
260
261struct BufWriter<W: Write>(W);
263
264impl<W: Write> Write for BufWriter<W> {
265 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
266 self.0.write(buf)
267 }
268 fn flush(&mut self) -> std::io::Result<()> {
269 self.0.flush()
270 }
271}
272
273impl LspClient {
274 pub fn new(project_root: &Path, config: &dyn LspServerConfig) -> Self {
275 let project_root = project_root.to_path_buf();
276 let language_id = config.language_id().to_string();
277
278 let binary_path = match Self::locate_or_download(config) {
279 Ok(p) => p,
280 Err(e) => {
281 eprintln!(
282 "[scope-engine/lsp] cannot locate {}: {e}",
283 config.server_name()
284 );
285 return Self {
286 inner: RefCell::new(LspClientInner {
287 process: None,
288 stdin_writer: None,
289 stdout_reader: None,
290 next_id: 0,
291 initialized: false,
292 language_id,
293 }),
294 };
295 }
296 };
297
298 match Self::spawn_and_initialize(&binary_path, &project_root, config) {
299 Ok((process, stdin_w, stdout_r)) => Self {
300 inner: RefCell::new(LspClientInner {
301 process: Some(process),
302 stdin_writer: Some(stdin_w),
303 stdout_reader: Some(stdout_r),
304 next_id: 1,
305 initialized: true,
306 language_id,
307 }),
308 },
309 Err(e) => {
310 eprintln!(
311 "[scope-engine/lsp] failed to spawn/initialize {}: {e}",
312 config.server_name()
313 );
314 Self {
315 inner: RefCell::new(LspClientInner {
316 process: None,
317 stdin_writer: None,
318 stdout_reader: None,
319 next_id: 0,
320 initialized: false,
321 language_id,
322 }),
323 }
324 }
325 }
326 }
327
328 fn locate_or_download(config: &dyn LspServerConfig) -> Result<PathBuf, String> {
331 if let Ok(output) = Command::new("which").arg(config.binary_name()).output()
333 && output.status.success()
334 {
335 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
336 if !path.is_empty() {
337 eprintln!(
338 "[scope-engine/lsp] found {} on PATH: {path}",
339 config.server_name()
340 );
341 return Ok(PathBuf::from(path));
342 }
343 }
344
345 let cache_dir = Self::cache_dir()?;
347 let cached = cache_dir.join(config.cached_binary_name());
348 if cached.is_file() {
349 eprintln!(
350 "[scope-engine/lsp] found cached {}: {}",
351 config.server_name(),
352 cached.display()
353 );
354 return Ok(cached);
355 }
356
357 match config.download_url() {
359 Some(url) => Self::download_binary(&cache_dir, &cached, &url, config.server_name()),
360 None => {
361 if let Some((cmd, args)) = config.install_command() {
363 eprintln!(
364 "[scope-engine/lsp] attempting to install {} via: {} {}",
365 config.server_name(),
366 cmd,
367 args.join(" ")
368 );
369 let install_output = Command::new(&cmd).args(&args).output().map_err(|e| {
370 format!(
371 "failed to run install command '{} {}': {e}",
372 cmd,
373 args.join(" ")
374 )
375 })?;
376 if !install_output.status.success() {
377 let stderr = String::from_utf8_lossy(&install_output.stderr);
378 return Err(format!(
379 "install command for {} failed: {stderr}",
380 config.server_name()
381 ));
382 }
383 if let Ok(output) = Command::new("which").arg(config.binary_name()).output()
385 && output.status.success()
386 {
387 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
388 if !path.is_empty() {
389 eprintln!(
390 "[scope-engine/lsp] installed {} and found on PATH: {path}",
391 config.server_name()
392 );
393 return Ok(PathBuf::from(path));
394 }
395 }
396 let go_bin_dirs: Vec<PathBuf> = [
398 std::env::var("GOBIN").ok().map(PathBuf::from),
399 std::env::var("GOPATH")
400 .ok()
401 .map(|g| PathBuf::from(g).join("bin")),
402 Command::new("go")
403 .args(["env", "GOPATH"])
404 .output()
405 .ok()
406 .map(|o| {
407 PathBuf::from(String::from_utf8_lossy(&o.stdout).trim()).join("bin")
408 }),
409 Command::new("go")
410 .args(["env", "GOBIN"])
411 .output()
412 .ok()
413 .and_then(|o| {
414 let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
415 if s.is_empty() {
416 None
417 } else {
418 Some(PathBuf::from(s))
419 }
420 }),
421 ]
422 .into_iter()
423 .flatten()
424 .collect();
425 for dir in go_bin_dirs {
426 let candidate = dir.join(config.binary_name());
427 if candidate.is_file() {
428 eprintln!(
429 "[scope-engine/lsp] installed {} and found at: {}",
430 config.server_name(),
431 candidate.display()
432 );
433 return Ok(candidate);
434 }
435 }
436 Err(format!(
437 "{} was installed via '{}' but could not be found on PATH, GOPATH/bin, or GOBIN",
438 config.server_name(),
439 cmd
440 ))
441 } else {
442 Err(format!(
443 "{} not found on PATH and no download URL or install command configured",
444 config.server_name()
445 ))
446 }
447 }
448 }
449 }
450
451 fn cache_dir() -> Result<PathBuf, String> {
452 let base =
453 dirs::cache_dir().ok_or_else(|| "cannot determine cache directory".to_string())?;
454 let dir = base.join("daat-locus").join("lsp-binaries");
455 std::fs::create_dir_all(&dir)
456 .map_err(|e| format!("cannot create cache dir {}: {e}", dir.display()))?;
457 Ok(dir)
458 }
459
460 #[cfg(feature = "download-ra")]
461 fn download_binary(
462 cache_dir: &Path,
463 target: &Path,
464 url: &str,
465 name: &str,
466 ) -> Result<PathBuf, String> {
467 eprintln!("[scope-engine/lsp] downloading {name}...");
468 let tmp = cache_dir.join("download.tmp");
469 let mut resp = reqwest::blocking::Client::builder()
470 .timeout(std::time::Duration::from_secs(120))
471 .build()
472 .map_err(|e| format!("HTTP client build failed: {e}"))?
473 .get(url)
474 .send()
475 .map_err(|e| format!("download failed: {e}"))?;
476
477 if !resp.status().is_success() {
478 return Err(format!("download returned HTTP {}", resp.status()));
479 }
480
481 let mut file =
482 std::fs::File::create(&tmp).map_err(|e| format!("cannot create tmp file: {e}"))?;
483 resp.copy_to(&mut file)
484 .map_err(|e| format!("download write failed: {e}"))?;
485 drop(file);
486
487 std::fs::rename(&tmp, target)
488 .map_err(|e| format!("cannot rename tmp to final path: {e}"))?;
489
490 #[cfg(unix)]
491 {
492 use std::os::unix::fs::PermissionsExt;
493 std::fs::set_permissions(target, std::fs::Permissions::from_mode(0o755))
494 .map_err(|e| format!("cannot chmod: {e}"))?;
495 }
496
497 eprintln!(
498 "[scope-engine/lsp] downloaded {} to {}",
499 name,
500 target.display()
501 );
502 Ok(target.to_path_buf())
503 }
504
505 #[cfg(not(feature = "download-ra"))]
506 fn download_binary(
507 _cache_dir: &Path,
508 _target: &Path,
509 _url: &str,
510 name: &str,
511 ) -> Result<PathBuf, String> {
512 Err(format!(
513 "{name} download not available (feature 'download-ra' disabled)"
514 ))
515 }
516
517 fn spawn_and_initialize(
520 binary_path: &Path,
521 project_root: &Path,
522 config: &dyn LspServerConfig,
523 ) -> Result<
524 (
525 Child,
526 BufWriter<ChildStdin>,
527 std::io::BufReader<ChildStdout>,
528 ),
529 String,
530 > {
531 let mut child = Command::new(binary_path)
532 .args(config.spawn_args())
533 .current_dir(project_root)
534 .stdin(Stdio::piped())
535 .stdout(Stdio::piped())
536 .stderr(Stdio::null())
537 .spawn()
538 .map_err(|e| format!("cannot spawn {}: {e}", config.server_name()))?;
539
540 let stdin = child.stdin.take().ok_or("cannot take stdin")?;
541 let stdout = child.stdout.take().ok_or("cannot take stdout")?;
542
543 let mut writer = BufWriter(stdin);
544 let mut reader = std::io::BufReader::new(stdout);
545
546 let root_uri = path_to_file_uri(project_root);
548 let mut init_params = serde_json::json!({
549 "rootUri": root_uri.clone(),
550 "capabilities": {},
551 });
552 let extra = config.init_params_extra(&root_uri);
554 if let serde_json::Value::Object(extra_map) = extra
555 && let serde_json::Value::Object(ref mut params_map) = init_params
556 {
557 for (k, v) in extra_map {
558 params_map.insert(k, v);
559 }
560 }
561
562 let resp = Self::send_request_raw(&mut writer, &mut reader, 0, "initialize", init_params)
563 .map_err(|e| {
564 let _ = child.kill();
565 format!("initialize failed: {e}")
566 })?;
567
568 if let Some(err) = resp.get("error") {
569 let _ = child.kill();
570 return Err(format!("initialize error: {err}"));
571 }
572
573 Self::send_notification(&mut writer, "initialized", serde_json::json!({})).map_err(
575 |e| {
576 let _ = child.kill();
577 format!("initialized notification failed: {e}")
578 },
579 )?;
580
581 eprintln!(
582 "[scope-engine/lsp] {} initialized for {}",
583 config.server_name(),
584 project_root.display()
585 );
586 std::thread::sleep(std::time::Duration::from_secs(
587 config.post_init_delay_secs(),
588 ));
589 Ok((child, writer, reader))
590 }
591
592 pub fn notify_did_open(&self, file_path: &Path, text: &str) {
595 let mut inner = self.inner.borrow_mut();
596 if !inner.initialized {
597 return;
598 }
599 let uri = path_to_file_uri(file_path);
600 let lang_id = inner.language_id.clone();
601 let params = serde_json::json!({
602 "textDocument": {
603 "uri": uri,
604 "languageId": lang_id,
605 "version": 0,
606 "text": text,
607 }
608 });
609 if let Some(ref mut writer) = inner.stdin_writer {
610 let _ = Self::send_notification(writer, "textDocument/didOpen", params);
611 }
612 }
613
614 pub fn notify_did_change(&self, file_path: &Path, version: i32, text: &str) {
615 let mut inner = self.inner.borrow_mut();
616 if !inner.initialized {
617 return;
618 }
619 let uri = path_to_file_uri(file_path);
620 let params = serde_json::json!({
621 "textDocument": { "uri": uri, "version": version },
622 "contentChanges": [{ "text": text }]
623 });
624 if let Some(ref mut writer) = inner.stdin_writer {
625 let _ = Self::send_notification(writer, "textDocument/didChange", params);
626 }
627 }
628
629 pub fn notify_did_close(&self, file_path: &Path) {
630 let mut inner = self.inner.borrow_mut();
631 if !inner.initialized {
632 return;
633 }
634 let uri = path_to_file_uri(file_path);
635 let params = serde_json::json!({
636 "textDocument": { "uri": uri }
637 });
638 if let Some(ref mut writer) = inner.stdin_writer {
639 let _ = Self::send_notification(writer, "textDocument/didClose", params);
640 }
641 }
642
643 pub fn find_references_for_symbol(
646 &self,
647 file_path: &Path,
648 line: usize,
649 character: usize,
650 project_root: &Path,
651 ) -> Vec<PropagationResult> {
652 let mut guard = self.inner.borrow_mut();
653 let inner = &mut *guard;
654
655 if !inner.initialized {
656 return vec![];
657 }
658
659 let uri = path_to_file_uri(file_path);
660 let params = serde_json::json!({
661 "textDocument": { "uri": uri },
662 "position": { "line": line.saturating_sub(1), "character": character },
663 "context": { "includeDeclaration": false }
664 });
665
666 let (writer, reader) = match (&mut inner.stdin_writer, &mut inner.stdout_reader) {
667 (Some(w), Some(r)) => (w, r),
668 _ => return vec![],
669 };
670
671 let id = inner.next_id;
672 inner.next_id += 1;
673
674 let params = params.clone();
675 let resp = match Self::send_request_raw(
676 writer,
677 reader,
678 id,
679 "textDocument/references",
680 params.clone(),
681 ) {
682 Ok(r) => r,
683 Err(e) => {
684 eprintln!("[scope-engine/lsp] textDocument/references failed: {e}");
685 return vec![];
686 }
687 };
688
689 let locations = match resp.get("result") {
690 Some(serde_json::Value::Array(arr)) => arr.clone(),
691 Some(serde_json::Value::Null) | None => {
692 eprintln!("[scope-engine/lsp] references returned null, waiting and retrying...");
693 std::thread::sleep(std::time::Duration::from_secs(2));
694 let retry_id = inner.next_id;
695 inner.next_id += 1;
696 let retry_resp = match Self::send_request_raw(
697 writer,
698 reader,
699 retry_id,
700 "textDocument/references",
701 params,
702 ) {
703 Ok(r) => r,
704 Err(e) => {
705 eprintln!(
706 "[scope-engine/lsp] retry textDocument/references also failed: {e}"
707 );
708 return vec![];
709 }
710 };
711 eprintln!(
712 "[scope-engine/lsp] retry response: {}",
713 serde_json::to_string(&retry_resp).unwrap_or_default()
714 );
715 match retry_resp.get("result") {
716 Some(serde_json::Value::Array(arr)) => arr.clone(),
717 _ => return vec![],
718 }
719 }
720 _ => return vec![],
721 };
722
723 eprintln!(
724 "[scope-engine/lsp] found {} reference locations",
725 locations.len()
726 );
727
728 let ts = TreeSitterAnalyzer::new();
729 let mut results = Vec::new();
730
731 for loc in locations {
732 let loc_uri = loc.get("uri").and_then(|v| v.as_str()).unwrap_or("");
733 let loc_line = loc
734 .get("range")
735 .and_then(|r| r.get("start"))
736 .and_then(|s| s.get("line"))
737 .and_then(|v| v.as_u64())
738 .unwrap_or(0) as usize;
739
740 let loc_path = uri_to_path(loc_uri);
741 let rel_path = loc_path
742 .strip_prefix(project_root)
743 .ok()
744 .map(|p| p.to_string_lossy().to_string())
745 .unwrap_or_else(|| loc_path.to_string_lossy().to_string());
746
747 let context_line = std::fs::read_to_string(&loc_path)
748 .ok()
749 .and_then(|content| content.lines().nth(loc_line).map(|l| l.to_string()))
750 .unwrap_or_default();
751
752 if ts.is_import_only_reference(&loc_path, loc_line + 1) {
753 continue;
754 }
755
756 let selector = ts
757 .find_containing_symbol(&loc_path, loc_line + 1, project_root)
758 .unwrap_or_else(|| format!("{rel_path}::line {}", loc_line + 1));
759
760 let lsp_ref = (selector.clone(), loc_line + 1, context_line.clone());
761
762 results.push(PropagationResult {
763 selector,
764 reason: format!("LSP reference found at {}:{}", rel_path, loc_line + 1),
765 source: PropagationSource::Lsp,
766 lsp_references: Some(vec![lsp_ref]),
767 diff_summary: None,
768 file_snippet: None,
769 project_files: None,
770 });
771 }
772
773 results
774 }
775
776 fn send_request_raw(
779 writer: &mut BufWriter<ChildStdin>,
780 reader: &mut std::io::BufReader<ChildStdout>,
781 id: u64,
782 method: &str,
783 params: serde_json::Value,
784 ) -> Result<serde_json::Value, String> {
785 let request = serde_json::json!({
786 "jsonrpc": "2.0",
787 "id": id,
788 "method": method,
789 "params": params,
790 });
791 Self::write_message(writer, &request)?;
792 Self::read_response(reader, id)
793 }
794
795 fn send_notification(
796 writer: &mut BufWriter<ChildStdin>,
797 method: &str,
798 params: serde_json::Value,
799 ) -> Result<(), String> {
800 let notification = serde_json::json!({
801 "jsonrpc": "2.0",
802 "method": method,
803 "params": params,
804 });
805 Self::write_message(writer, ¬ification)
806 }
807
808 fn write_message(
809 writer: &mut BufWriter<ChildStdin>,
810 msg: &serde_json::Value,
811 ) -> Result<(), String> {
812 let body = serde_json::to_string(msg).map_err(|e| format!("json serialize failed: {e}"))?;
813 let header = format!("Content-Length: {}\r\n\r\n", body.len());
814 writer
815 .write_all(header.as_bytes())
816 .map_err(|e| format!("write header failed: {e}"))?;
817 writer
818 .write_all(body.as_bytes())
819 .map_err(|e| format!("write body failed: {e}"))?;
820 writer.flush().map_err(|e| format!("flush failed: {e}"))?;
821 Ok(())
822 }
823
824 fn read_response(
825 reader: &mut std::io::BufReader<ChildStdout>,
826 expected_id: u64,
827 ) -> Result<serde_json::Value, String> {
828 loop {
829 let mut header_line = String::new();
830 loop {
831 let mut byte = [0u8; 1];
832 reader
833 .read_exact(&mut byte)
834 .map_err(|e| format!("read header byte failed: {e}"))?;
835 let ch = byte[0] as char;
836 header_line.push(ch);
837 if header_line.ends_with("\r\n\r\n") {
838 break;
839 }
840 if header_line.len() > 4096 {
841 return Err("header too long, possibly malformed LSP response".to_string());
842 }
843 }
844
845 let content_length: usize = header_line
846 .lines()
847 .find_map(|line| {
848 line.strip_prefix("Content-Length: ")
849 .and_then(|v| v.trim().parse().ok())
850 })
851 .ok_or("missing Content-Length header")?;
852
853 let mut body_buf = vec![0u8; content_length];
854 reader
855 .read_exact(&mut body_buf)
856 .map_err(|e| format!("read body failed: {e}"))?;
857 let body: serde_json::Value =
858 serde_json::from_slice(&body_buf).map_err(|e| format!("json parse failed: {e}"))?;
859
860 if let Some(resp_id) = body.get("id").and_then(|v| v.as_u64()) {
861 let is_response = body.get("result").is_some() || body.get("error").is_some();
862 if resp_id == expected_id && is_response {
863 return Ok(body);
864 }
865 }
866 }
867 }
868}
869
870impl Drop for LspClient {
871 fn drop(&mut self) {
872 let inner = self.inner.get_mut();
873 if !inner.initialized {
874 return;
875 }
876 if let (Some(writer), Some(reader)) = (&mut inner.stdin_writer, &mut inner.stdout_reader) {
877 let _ = Self::send_request_raw(
878 writer,
879 reader,
880 inner.next_id,
881 "shutdown",
882 serde_json::json!(null),
883 );
884 let _ = Self::send_notification(writer, "exit", serde_json::json!(null));
885 }
886 if let Some(ref mut child) = inner.process {
887 let _ = child.kill();
888 let _ = child.wait();
889 }
890 inner.initialized = false;
891 inner.process = None;
892 inner.stdin_writer = None;
893 inner.stdout_reader = None;
894 eprintln!("[scope-engine/lsp] language server shut down");
895 }
896}
897
898fn path_to_file_uri(path: &Path) -> String {
901 let abs = if path.is_absolute() {
902 path.to_path_buf()
903 } else {
904 std::env::current_dir().unwrap_or_default().join(path)
905 };
906 let s = abs.to_string_lossy();
907 format!("file://{s}")
908}
909
910fn uri_to_path(uri: &str) -> PathBuf {
911 let stripped = uri.strip_prefix("file://").unwrap_or(uri);
912 PathBuf::from(stripped)
913}
914
915impl Analyzer for LspClient {
918 fn find_references_for_symbol(
919 &self,
920 file_path: &Path,
921 line: usize,
922 character: usize,
923 project_root: &Path,
924 ) -> Vec<PropagationResult> {
925 LspClient::find_references_for_symbol(self, file_path, line, character, project_root)
926 }
927
928 fn notify_did_open(&self, file_path: &Path, text: &str) {
929 LspClient::notify_did_open(self, file_path, text);
930 }
931
932 fn notify_did_change(&self, file_path: &Path, version: i32, text: &str) {
933 LspClient::notify_did_change(self, file_path, version, text);
934 }
935
936 fn notify_did_close(&self, file_path: &Path) {
937 LspClient::notify_did_close(self, file_path);
938 }
939
940 fn is_initialized(&self) -> bool {
941 self.inner.borrow().initialized
942 }
943}
944
945pub type LspAnalyzer = LspClient;