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 if let Some(err) = resp.get("error") {
690 eprintln!("[scope-engine/lsp] textDocument/references error: {err}");
691 return vec![];
692 }
693
694 let locations = match resp.get("result") {
695 Some(serde_json::Value::Array(arr)) => arr.clone(),
696 Some(serde_json::Value::Null) | None => {
697 eprintln!("[scope-engine/lsp] references returned null, waiting and retrying...");
698 std::thread::sleep(std::time::Duration::from_secs(2));
699 let retry_id = inner.next_id;
700 inner.next_id += 1;
701 let retry_resp = match Self::send_request_raw(
702 writer,
703 reader,
704 retry_id,
705 "textDocument/references",
706 params,
707 ) {
708 Ok(r) => r,
709 Err(e) => {
710 eprintln!(
711 "[scope-engine/lsp] retry textDocument/references also failed: {e}"
712 );
713 return vec![];
714 }
715 };
716 if let Some(err) = retry_resp.get("error") {
717 eprintln!("[scope-engine/lsp] retry textDocument/references error: {err}");
718 return vec![];
719 }
720 match retry_resp.get("result") {
721 Some(serde_json::Value::Array(arr)) => arr.clone(),
722 _ => return vec![],
723 }
724 }
725 _ => return vec![],
726 };
727
728 eprintln!(
729 "[scope-engine/lsp] found {} reference locations",
730 locations.len()
731 );
732
733 let ts = TreeSitterAnalyzer::new();
734 let mut results = Vec::new();
735
736 for loc in locations {
737 let loc_uri = loc.get("uri").and_then(|v| v.as_str()).unwrap_or("");
738 let loc_line = loc
739 .get("range")
740 .and_then(|r| r.get("start"))
741 .and_then(|s| s.get("line"))
742 .and_then(|v| v.as_u64())
743 .unwrap_or(0) as usize;
744
745 let loc_path = uri_to_path(loc_uri);
746 let rel_path = loc_path
747 .strip_prefix(project_root)
748 .ok()
749 .map(|p| p.to_string_lossy().to_string())
750 .unwrap_or_else(|| loc_path.to_string_lossy().to_string());
751
752 let context_line = std::fs::read_to_string(&loc_path)
753 .ok()
754 .and_then(|content| content.lines().nth(loc_line).map(|l| l.to_string()))
755 .unwrap_or_default();
756
757 if ts.is_import_only_reference(&loc_path, loc_line + 1) {
758 continue;
759 }
760
761 let selector = ts
762 .find_containing_symbol(&loc_path, loc_line + 1, project_root)
763 .unwrap_or_else(|| format!("{rel_path}::line {}", loc_line + 1));
764
765 let lsp_ref = (selector.clone(), loc_line + 1, context_line.clone());
766
767 results.push(PropagationResult {
768 selector,
769 reason: format!("LSP reference found at {}:{}", rel_path, loc_line + 1),
770 source: PropagationSource::Lsp,
771 lsp_references: Some(vec![lsp_ref]),
772 diff_summary: None,
773 file_snippet: None,
774 project_files: None,
775 });
776 }
777
778 results
779 }
780
781 fn send_request_raw(
784 writer: &mut BufWriter<ChildStdin>,
785 reader: &mut std::io::BufReader<ChildStdout>,
786 id: u64,
787 method: &str,
788 params: serde_json::Value,
789 ) -> Result<serde_json::Value, String> {
790 let request = serde_json::json!({
791 "jsonrpc": "2.0",
792 "id": id,
793 "method": method,
794 "params": params,
795 });
796 Self::write_message(writer, &request)?;
797 Self::read_response(reader, id)
798 }
799
800 fn send_notification(
801 writer: &mut BufWriter<ChildStdin>,
802 method: &str,
803 params: serde_json::Value,
804 ) -> Result<(), String> {
805 let notification = serde_json::json!({
806 "jsonrpc": "2.0",
807 "method": method,
808 "params": params,
809 });
810 Self::write_message(writer, ¬ification)
811 }
812
813 fn write_message(
814 writer: &mut BufWriter<ChildStdin>,
815 msg: &serde_json::Value,
816 ) -> Result<(), String> {
817 let body = serde_json::to_string(msg).map_err(|e| format!("json serialize failed: {e}"))?;
818 let header = format!("Content-Length: {}\r\n\r\n", body.len());
819 writer
820 .write_all(header.as_bytes())
821 .map_err(|e| format!("write header failed: {e}"))?;
822 writer
823 .write_all(body.as_bytes())
824 .map_err(|e| format!("write body failed: {e}"))?;
825 writer.flush().map_err(|e| format!("flush failed: {e}"))?;
826 Ok(())
827 }
828
829 fn read_response(
830 reader: &mut std::io::BufReader<ChildStdout>,
831 expected_id: u64,
832 ) -> Result<serde_json::Value, String> {
833 loop {
834 let mut header_line = String::new();
835 loop {
836 let mut byte = [0u8; 1];
837 reader
838 .read_exact(&mut byte)
839 .map_err(|e| format!("read header byte failed: {e}"))?;
840 let ch = byte[0] as char;
841 header_line.push(ch);
842 if header_line.ends_with("\r\n\r\n") {
843 break;
844 }
845 if header_line.len() > 4096 {
846 return Err("header too long, possibly malformed LSP response".to_string());
847 }
848 }
849
850 let content_length: usize = header_line
851 .lines()
852 .find_map(|line| {
853 line.strip_prefix("Content-Length: ")
854 .and_then(|v| v.trim().parse().ok())
855 })
856 .ok_or("missing Content-Length header")?;
857
858 let mut body_buf = vec![0u8; content_length];
859 reader
860 .read_exact(&mut body_buf)
861 .map_err(|e| format!("read body failed: {e}"))?;
862 let body: serde_json::Value =
863 serde_json::from_slice(&body_buf).map_err(|e| format!("json parse failed: {e}"))?;
864
865 if let Some(resp_id) = body.get("id").and_then(|v| v.as_u64()) {
866 let is_response = body.get("result").is_some() || body.get("error").is_some();
867 if resp_id == expected_id && is_response {
868 return Ok(body);
869 }
870 }
871 }
872 }
873}
874
875impl Drop for LspClient {
876 fn drop(&mut self) {
877 let inner = self.inner.get_mut();
878 if !inner.initialized {
879 return;
880 }
881 if let (Some(writer), Some(reader)) = (&mut inner.stdin_writer, &mut inner.stdout_reader) {
882 let _ = Self::send_request_raw(
883 writer,
884 reader,
885 inner.next_id,
886 "shutdown",
887 serde_json::json!(null),
888 );
889 let _ = Self::send_notification(writer, "exit", serde_json::json!(null));
890 }
891 if let Some(ref mut child) = inner.process {
892 let _ = child.kill();
893 let _ = child.wait();
894 }
895 inner.initialized = false;
896 inner.process = None;
897 inner.stdin_writer = None;
898 inner.stdout_reader = None;
899 eprintln!("[scope-engine/lsp] language server shut down");
900 }
901}
902
903fn path_to_file_uri(path: &Path) -> String {
906 let abs = if path.is_absolute() {
907 path.to_path_buf()
908 } else {
909 std::env::current_dir().unwrap_or_default().join(path)
910 };
911 file_uri_from_absolute_path_string(&abs.to_string_lossy())
912}
913
914fn file_uri_from_absolute_path_string(path: &str) -> String {
915 let mut normalized = path.replace('\\', "/");
916
917 if let Some(rest) = normalized.strip_prefix("//?/UNC/") {
918 return unc_path_to_file_uri(rest);
919 }
920 if let Some(rest) = normalized.strip_prefix("//?/") {
921 normalized = rest.to_string();
922 } else if let Some(rest) = normalized.strip_prefix("//./") {
923 normalized = rest.to_string();
924 }
925
926 if let Some(rest) = normalized.strip_prefix("//") {
927 return unc_path_to_file_uri(rest);
928 }
929
930 if has_windows_drive_prefix(&normalized) {
931 return format!("file:///{}", percent_encode_file_path(&normalized));
932 }
933
934 if normalized.starts_with('/') {
935 return format!("file://{}", percent_encode_file_path(&normalized));
936 }
937
938 format!("file:///{}", percent_encode_file_path(&normalized))
939}
940
941fn unc_path_to_file_uri(path: &str) -> String {
942 let (host, rest) = path.split_once('/').unwrap_or((path, ""));
943 if rest.is_empty() {
944 format!("file://{}", percent_encode_file_path(host))
945 } else {
946 format!(
947 "file://{}/{}",
948 percent_encode_file_path(host),
949 percent_encode_file_path(rest)
950 )
951 }
952}
953
954fn has_windows_drive_prefix(path: &str) -> bool {
955 let bytes = path.as_bytes();
956 bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
957}
958
959fn percent_encode_file_path(path: &str) -> String {
960 let mut encoded = String::new();
961 for &byte in path.as_bytes() {
962 match byte {
963 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' | b':' => {
964 encoded.push(byte as char)
965 }
966 _ => encoded.push_str(&format!("%{byte:02X}")),
967 }
968 }
969 encoded
970}
971
972fn percent_decode_file_path(path: &str) -> String {
973 let bytes = path.as_bytes();
974 let mut decoded = Vec::with_capacity(bytes.len());
975 let mut i = 0;
976 while i < bytes.len() {
977 if bytes[i] == b'%'
978 && i + 2 < bytes.len()
979 && let (Some(hi), Some(lo)) = (hex_value(bytes[i + 1]), hex_value(bytes[i + 2]))
980 {
981 decoded.push((hi << 4) | lo);
982 i += 3;
983 } else {
984 decoded.push(bytes[i]);
985 i += 1;
986 }
987 }
988 String::from_utf8_lossy(&decoded).into_owned()
989}
990
991fn hex_value(byte: u8) -> Option<u8> {
992 match byte {
993 b'0'..=b'9' => Some(byte - b'0'),
994 b'a'..=b'f' => Some(byte - b'a' + 10),
995 b'A'..=b'F' => Some(byte - b'A' + 10),
996 _ => None,
997 }
998}
999
1000fn uri_to_path(uri: &str) -> PathBuf {
1001 PathBuf::from(file_uri_to_path_string(uri))
1002}
1003
1004fn file_uri_to_path_string(uri: &str) -> String {
1005 let Some(rest) = uri.strip_prefix("file://") else {
1006 return uri.to_string();
1007 };
1008 let decoded = percent_decode_file_path(rest);
1009 if let Some(stripped) = decoded.strip_prefix('/') {
1010 if has_windows_drive_prefix(stripped) {
1011 return stripped.to_string();
1012 }
1013 return decoded;
1014 }
1015 format!("//{decoded}")
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::{file_uri_from_absolute_path_string, file_uri_to_path_string};
1021
1022 #[test]
1023 fn windows_verbatim_paths_become_valid_file_uris() {
1024 let uri =
1025 file_uri_from_absolute_path_string(r"\\?\C:\Users\Name With Space\src\main#test.rs");
1026
1027 assert_eq!(
1028 uri,
1029 "file:///C:/Users/Name%20With%20Space/src/main%23test.rs"
1030 );
1031 }
1032
1033 #[test]
1034 fn unix_paths_become_valid_file_uris() {
1035 let uri = file_uri_from_absolute_path_string("/tmp/name with space/main#test.rs");
1036
1037 assert_eq!(uri, "file:///tmp/name%20with%20space/main%23test.rs");
1038 }
1039
1040 #[test]
1041 fn file_uris_decode_windows_drive_paths() {
1042 let path =
1043 file_uri_to_path_string("file:///C:/Users/Name%20With%20Space/src/main%23test.rs");
1044
1045 assert_eq!(path, "C:/Users/Name With Space/src/main#test.rs");
1046 }
1047}
1048
1049impl Analyzer for LspClient {
1052 fn find_references_for_symbol(
1053 &self,
1054 file_path: &Path,
1055 line: usize,
1056 character: usize,
1057 project_root: &Path,
1058 ) -> Vec<PropagationResult> {
1059 LspClient::find_references_for_symbol(self, file_path, line, character, project_root)
1060 }
1061
1062 fn notify_did_open(&self, file_path: &Path, text: &str) {
1063 LspClient::notify_did_open(self, file_path, text);
1064 }
1065
1066 fn notify_did_change(&self, file_path: &Path, version: i32, text: &str) {
1067 LspClient::notify_did_change(self, file_path, version, text);
1068 }
1069
1070 fn notify_did_close(&self, file_path: &Path) {
1071 LspClient::notify_did_close(self, file_path);
1072 }
1073
1074 fn is_initialized(&self) -> bool {
1075 self.inner.borrow().initialized
1076 }
1077}
1078
1079pub type LspAnalyzer = LspClient;