1mod clangd;
7mod pyrefly;
8mod symbol_names;
9
10use std::{
11 collections::HashMap,
12 ffi::{OsStr, OsString},
13 fmt,
14 path::{Path, PathBuf},
15 process::Stdio,
16 sync::{Arc, Mutex as StdMutex},
17 time::Duration,
18};
19
20use anyhow::{Context, Result, bail};
21use serde::{Serialize, de::DeserializeOwned};
22use serde_json::{Value, json};
23use tokio::{
24 io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader},
25 process::{Child, Command},
26 sync::{Mutex, OnceCell, mpsc, oneshot},
27 task::JoinHandle,
28 time::timeout,
29};
30use tower_lsp::lsp_types::{
31 CallHierarchyClientCapabilities, CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams,
32 CallHierarchyItem, CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams,
33 CallHierarchyPrepareParams, CallHierarchyServerCapability, ClientCapabilities, ClientInfo,
34 DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentSymbol,
35 DocumentSymbolClientCapabilities, DocumentSymbolParams, DocumentSymbolResponse,
36 GeneralClientCapabilities, InitializeParams, InitializeResult, Location, NumberOrString, OneOf,
37 PartialResultParams, Position, PositionEncodingKind, ProgressParams, ProgressParamsValue,
38 Range, ServerInfo, SymbolInformation, SymbolKind, TextDocumentClientCapabilities,
39 TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams,
40 TypeHierarchyClientCapabilities, TypeHierarchyItem, TypeHierarchyPrepareParams,
41 TypeHierarchySubtypesParams, TypeHierarchySupertypesParams, Url, WindowClientCapabilities,
42 WorkDoneProgress, WorkDoneProgressParams, WorkspaceClientCapabilities, WorkspaceFolder,
43 WorkspaceSymbolClientCapabilities, WorkspaceSymbolParams, WorkspaceSymbolResponse,
44 request::{
45 CallHierarchyIncomingCalls, CallHierarchyOutgoingCalls, CallHierarchyPrepare,
46 DocumentSymbolRequest, Initialize, Request, Shutdown, TypeHierarchyPrepare,
47 TypeHierarchySubtypes, TypeHierarchySupertypes, WorkspaceSymbolRequest,
48 },
49};
50
51use crate::{
52 fetch::{FetchSource, HierarchyQuery, HierarchyResponse},
53 state::{HierarchyDirection, HierarchyKind, SourceLocation, SymbolIdentity},
54};
55use symbol_names::SymbolNameAdapter;
56
57pub use crate::fetch::WorkspaceSymbolMatch;
58
59const MAX_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
61const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2);
62
63#[derive(Clone, Debug)]
64pub struct LspConfig {
65 pub program: OsString,
66 pub args: Vec<OsString>,
67 pub workspace_root: PathBuf,
68 pub initialization_options: Option<Value>,
69 pub workspace_only: bool,
70}
71
72impl LspConfig {
73 pub fn new(program: impl Into<OsString>, workspace_root: impl Into<PathBuf>) -> Self {
74 Self {
75 program: program.into(),
76 args: Vec::new(),
77 workspace_root: workspace_root.into(),
78 initialization_options: None,
79 workspace_only: true,
80 }
81 }
82
83 pub fn for_server(program: impl Into<OsString>, workspace_root: impl Into<PathBuf>) -> Self {
89 let program = program.into();
90 let mut config = Self::new(program.clone(), workspace_root);
91 if is_pyrefly_program(&program) {
92 config.args.push(OsString::from("lsp"));
93 }
94 config
95 }
96
97 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
98 self.args.push(arg.into());
99 self
100 }
101
102 pub fn args<I, S>(mut self, args: I) -> Self
103 where
104 I: IntoIterator<Item = S>,
105 S: Into<OsString>,
106 {
107 self.args.extend(args.into_iter().map(Into::into));
108 self
109 }
110
111 pub fn initialization_options(mut self, options: Value) -> Self {
112 self.initialization_options = Some(options);
113 self
114 }
115
116 pub fn workspace_only(mut self, workspace_only: bool) -> Self {
117 self.workspace_only = workspace_only;
118 self
119 }
120}
121
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub enum LspStatusUpdate {
129 Ready {
130 message: Option<String>,
131 },
132 Progress {
133 title: String,
134 message: Option<String>,
135 percentage: Option<u32>,
136 },
137 Warning(String),
138 Error(String),
139 Disconnected(String),
140}
141
142#[derive(Clone, Debug)]
143struct ActiveProgress {
144 sequence: u64,
145 title: String,
146 message: Option<String>,
147 percentage: Option<u32>,
148}
149
150#[derive(Default)]
151struct LspProgressTracker {
152 next_sequence: u64,
153 active: HashMap<String, ActiveProgress>,
154}
155
156#[derive(Clone)]
157pub struct WorkspaceSymbolClient {
158 client: JsonRpcClient,
159 workspace_root: PathBuf,
160 symbol_names: SymbolNameAdapter,
161 workspace_only: bool,
162}
163
164impl fmt::Debug for WorkspaceSymbolClient {
165 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
166 formatter
167 .debug_struct("WorkspaceSymbolClient")
168 .finish_non_exhaustive()
169 }
170}
171
172impl WorkspaceSymbolClient {
173 pub async fn query(&self, query: &str) -> Result<Vec<WorkspaceSymbolMatch>> {
174 let params = WorkspaceSymbolParams {
175 query: query.to_owned(),
176 ..WorkspaceSymbolParams::default()
177 };
178 let response: Option<WorkspaceSymbolResponse> = self
179 .client
180 .request(WorkspaceSymbolRequest::METHOD, params)
181 .await
182 .with_context(|| format!("workspace symbol query failed for {query:?}"))?;
183
184 let symbols = response
185 .map(|response| normalize_symbols(response, self.symbol_names))
186 .unwrap_or_default();
187 Ok(deduplicate_symbols(symbols.into_iter().filter(|symbol| {
188 workspace_symbol_is_visible(symbol, &self.workspace_root, self.workspace_only)
189 })))
190 }
191
192 pub fn set_workspace_only(&mut self, workspace_only: bool) {
193 self.workspace_only = workspace_only;
194 }
195}
196
197#[derive(Clone)]
198pub struct HierarchyClient {
199 client: JsonRpcClient,
200 workspace_root: PathBuf,
201 symbol_names: SymbolNameAdapter,
202 document_symbols: DocumentSymbolCache,
203 capabilities: Arc<ServerHierarchyCapabilities>,
204 workspace_only: bool,
205}
206
207type DocumentSymbolCache = Arc<Mutex<HashMap<Url, Arc<OnceCell<Vec<DocumentSymbolOwner>>>>>>;
208
209#[derive(Debug, Default)]
210struct ServerHierarchyCapabilities {
211 static_call: std::sync::atomic::AtomicBool,
212 dynamic_registrations: StdMutex<HashMap<String, String>>,
213}
214
215impl ServerHierarchyCapabilities {
216 fn set_static_call(&self, supported: bool) {
217 self.static_call
218 .store(supported, std::sync::atomic::Ordering::Release);
219 }
220
221 fn supports(&self, kind: HierarchyKind) -> bool {
222 if kind == HierarchyKind::Call
223 && self.static_call.load(std::sync::atomic::Ordering::Acquire)
224 {
225 return true;
226 }
227 let method = prepare_hierarchy_method(kind);
228 self.dynamic_registrations
229 .lock()
230 .expect("LSP hierarchy capability mutex poisoned")
231 .values()
232 .any(|registered| registered == method)
233 }
234
235 fn register(&self, id: &str, method: &str) {
236 if is_hierarchy_registration(method) {
237 self.dynamic_registrations
238 .lock()
239 .expect("LSP hierarchy capability mutex poisoned")
240 .insert(id.to_owned(), method.to_owned());
241 }
242 }
243
244 fn unregister(&self, id: &str) {
245 self.dynamic_registrations
246 .lock()
247 .expect("LSP hierarchy capability mutex poisoned")
248 .remove(id);
249 }
250}
251
252fn hierarchy_name(kind: HierarchyKind) -> &'static str {
253 match kind {
254 HierarchyKind::Call => "call",
255 HierarchyKind::Type => "type",
256 }
257}
258
259fn prepare_hierarchy_method(kind: HierarchyKind) -> &'static str {
260 match kind {
261 HierarchyKind::Call => CallHierarchyPrepare::METHOD,
262 HierarchyKind::Type => TypeHierarchyPrepare::METHOD,
263 }
264}
265
266fn is_hierarchy_registration(method: &str) -> bool {
267 method == CallHierarchyPrepare::METHOD || method == TypeHierarchyPrepare::METHOD
268}
269
270impl fmt::Debug for HierarchyClient {
271 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
272 formatter
273 .debug_struct("HierarchyClient")
274 .finish_non_exhaustive()
275 }
276}
277
278impl HierarchyClient {
279 pub async fn query(&self, mut query: HierarchyQuery) -> Result<HierarchyResponse> {
280 if !self.supports(query.symbol.kind) {
281 bail!(
282 "language server does not advertise {} hierarchy support",
283 hierarchy_name(query.symbol.kind)
284 );
285 }
286 let (document_position, resolved_location) =
287 self.resolve_document_position(&query.symbol).await?;
288 query.symbol.location = Some(resolved_location);
289 let children = match query.symbol.kind {
290 HierarchyKind::Call => {
291 self.call_children(document_position, query.direction)
292 .await?
293 }
294 HierarchyKind::Type => {
295 self.type_children(document_position, query.direction)
296 .await?
297 }
298 };
299
300 Ok(HierarchyResponse {
301 query,
302 children: deduplicate_identities(children),
303 source: FetchSource::Lsp,
304 })
305 }
306
307 pub fn supports(&self, kind: HierarchyKind) -> bool {
308 self.capabilities.supports(kind)
309 }
310
311 async fn resolve_document_position(
312 &self,
313 symbol: &SymbolIdentity,
314 ) -> Result<(TextDocumentPositionParams, SourceLocation)> {
315 if let Some(location) = symbol.location.as_ref()
316 && let (Some(line), Some(character)) = (location.line, location.character)
317 {
318 let uri = Url::parse(&location.uri)
319 .with_context(|| format!("invalid symbol URI: {}", location.uri))?;
320 return Ok(document_position(uri, Position::new(line, character)));
321 }
322
323 let lookup_name = symbol_leaf_name(&symbol.symbol);
324 let candidates = WorkspaceSymbolClient {
325 client: self.client.clone(),
326 workspace_root: self.workspace_root.clone(),
327 symbol_names: self.symbol_names,
328 workspace_only: self.workspace_only,
329 }
330 .query(lookup_name)
331 .await?
332 .into_iter()
333 .filter(|candidate| {
334 candidate.range.is_some()
335 && symbol_leaf_name(&candidate.name) == lookup_name
336 && symbol_kind_matches_hierarchy(symbol.kind, candidate.kind)
337 })
338 .collect::<Vec<_>>();
339
340 let candidate = match candidates.as_slice() {
341 [candidate] => candidate,
342 [] => bail!(
343 "could not resolve {:?} to a workspace symbol with a source position",
344 symbol.symbol
345 ),
346 _ => bail!(
347 "symbol {:?} is ambiguous; add it through ac/at to select an exact location",
348 symbol.symbol
349 ),
350 };
351 let position = candidate
352 .range
353 .expect("workspace symbol candidates were filtered to exact locations")
354 .start;
355 Ok(document_position(candidate.uri.clone(), position))
356 }
357
358 async fn call_children(
359 &self,
360 document_position: TextDocumentPositionParams,
361 direction: HierarchyDirection,
362 ) -> Result<Vec<SymbolIdentity>> {
363 let prepared: Option<Vec<CallHierarchyItem>> = self
364 .client
365 .request(
366 CallHierarchyPrepare::METHOD,
367 CallHierarchyPrepareParams {
368 text_document_position_params: document_position,
369 work_done_progress_params: WorkDoneProgressParams::default(),
370 },
371 )
372 .await
373 .context("failed to prepare call hierarchy")?;
374 let Some(item) = prepared.and_then(|items| items.into_iter().next()) else {
375 return Ok(Vec::new());
376 };
377
378 match direction {
379 HierarchyDirection::Incoming => {
380 let calls: Option<Vec<CallHierarchyIncomingCall>> = self
381 .client
382 .request(
383 CallHierarchyIncomingCalls::METHOD,
384 CallHierarchyIncomingCallsParams {
385 item,
386 work_done_progress_params: WorkDoneProgressParams::default(),
387 partial_result_params: PartialResultParams::default(),
388 },
389 )
390 .await
391 .context("failed to query incoming calls")?;
392 self.call_item_identities(
393 calls
394 .unwrap_or_default()
395 .into_iter()
396 .map(|call| call.from)
397 .collect(),
398 )
399 .await
400 }
401 HierarchyDirection::Outgoing => {
402 let calls: Option<Vec<CallHierarchyOutgoingCall>> = self
403 .client
404 .request(
405 CallHierarchyOutgoingCalls::METHOD,
406 CallHierarchyOutgoingCallsParams {
407 item,
408 work_done_progress_params: WorkDoneProgressParams::default(),
409 partial_result_params: PartialResultParams::default(),
410 },
411 )
412 .await
413 .context("failed to query outgoing calls")?;
414 self.call_item_identities(
415 calls
416 .unwrap_or_default()
417 .into_iter()
418 .map(|call| call.to)
419 .collect(),
420 )
421 .await
422 }
423 }
424 }
425
426 async fn call_item_identities(
427 &self,
428 items: Vec<CallHierarchyItem>,
429 ) -> Result<Vec<SymbolIdentity>> {
430 let mut identities = Vec::with_capacity(items.len());
431 for item in items.into_iter().filter(|item| {
432 !self.workspace_only || uri_belongs_to_workspace(&item.uri, &self.workspace_root)
433 }) {
434 let container = if self.symbol_names.uses_document_symbols() {
435 self.document_symbol_container(&item).await
436 } else {
437 None
438 };
439 identities.push(call_item_identity(
440 item,
441 self.symbol_names,
442 container.as_deref(),
443 ));
444 }
445 Ok(identities)
446 }
447
448 async fn document_symbol_container(&self, item: &CallHierarchyItem) -> Option<String> {
449 if !matches!(
450 item.kind,
451 SymbolKind::FUNCTION | SymbolKind::METHOD | SymbolKind::CONSTRUCTOR
452 ) {
453 return None;
454 }
455
456 let document_symbols = {
460 let mut cache = self.document_symbols.lock().await;
461 Arc::clone(
462 cache
463 .entry(item.uri.clone())
464 .or_insert_with(|| Arc::new(OnceCell::new())),
465 )
466 };
467 let symbols = document_symbols
468 .get_or_init(|| async {
469 let response: Option<DocumentSymbolResponse> = self
470 .client
471 .request(
472 DocumentSymbolRequest::METHOD,
473 DocumentSymbolParams {
474 text_document: TextDocumentIdentifier::new(item.uri.clone()),
475 work_done_progress_params: WorkDoneProgressParams::default(),
476 partial_result_params: PartialResultParams::default(),
477 },
478 )
479 .await
480 .ok()
481 .flatten();
482 response.map(normalize_document_symbols).unwrap_or_default()
483 })
484 .await;
485 find_document_symbol_container(symbols, item).map(str::to_owned)
486 }
487
488 async fn type_children(
489 &self,
490 document_position: TextDocumentPositionParams,
491 direction: HierarchyDirection,
492 ) -> Result<Vec<SymbolIdentity>> {
493 let prepared: Option<Vec<TypeHierarchyItem>> = self
494 .client
495 .request(
496 TypeHierarchyPrepare::METHOD,
497 TypeHierarchyPrepareParams {
498 text_document_position_params: document_position,
499 work_done_progress_params: WorkDoneProgressParams::default(),
500 },
501 )
502 .await
503 .context("failed to prepare type hierarchy")?;
504 let Some(item) = prepared.and_then(|items| items.into_iter().next()) else {
505 return Ok(Vec::new());
506 };
507
508 let items: Option<Vec<TypeHierarchyItem>> = match direction {
509 HierarchyDirection::Incoming => self
510 .client
511 .request(
512 TypeHierarchySupertypes::METHOD,
513 TypeHierarchySupertypesParams {
514 item,
515 work_done_progress_params: WorkDoneProgressParams::default(),
516 partial_result_params: PartialResultParams::default(),
517 },
518 )
519 .await
520 .context("failed to query supertypes")?,
521 HierarchyDirection::Outgoing => self
522 .client
523 .request(
524 TypeHierarchySubtypes::METHOD,
525 TypeHierarchySubtypesParams {
526 item,
527 work_done_progress_params: WorkDoneProgressParams::default(),
528 partial_result_params: PartialResultParams::default(),
529 },
530 )
531 .await
532 .context("failed to query subtypes")?,
533 };
534 Ok(items
535 .unwrap_or_default()
536 .into_iter()
537 .filter(|item| {
538 !self.workspace_only || uri_belongs_to_workspace(&item.uri, &self.workspace_root)
539 })
540 .map(type_item_identity)
541 .collect())
542 }
543
544 pub fn set_workspace_only(&mut self, workspace_only: bool) {
545 self.workspace_only = workspace_only;
546 }
547}
548
549pub struct LspProvider {
550 child: Child,
551 client: JsonRpcClient,
552 connection_task: JoinHandle<Result<()>>,
553 workspace_root: PathBuf,
554 server_info: Option<ServerInfo>,
555 symbol_names: SymbolNameAdapter,
556 document_symbols: DocumentSymbolCache,
557 hierarchy_capabilities: Arc<ServerHierarchyCapabilities>,
558 bootstrap_document: Option<Url>,
559 status_receiver: Option<mpsc::UnboundedReceiver<LspStatusUpdate>>,
560 workspace_only: bool,
561}
562
563impl fmt::Debug for LspProvider {
564 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
565 formatter
566 .debug_struct("LspProvider")
567 .field("workspace_root", &self.workspace_root)
568 .field("server_info", &self.server_info)
569 .finish_non_exhaustive()
570 }
571}
572
573impl LspProvider {
574 pub async fn start(config: LspConfig) -> Result<Self> {
575 let workspace_root = config.workspace_root.canonicalize().with_context(|| {
576 format!(
577 "failed to resolve workspace root {}",
578 config.workspace_root.display()
579 )
580 })?;
581 if !workspace_root.is_dir() {
582 bail!(
583 "workspace root is not a directory: {}",
584 workspace_root.display()
585 );
586 }
587
588 let workspace_uri = Url::from_directory_path(&workspace_root).map_err(|()| {
589 anyhow::anyhow!(
590 "workspace root cannot be represented as a file URI: {}",
591 workspace_root.display()
592 )
593 })?;
594 let workspace_name = workspace_name(&workspace_root);
595
596 let mut command = Command::new(&config.program);
597 command
598 .args(&config.args)
599 .current_dir(&workspace_root)
600 .stdin(Stdio::piped())
601 .stdout(Stdio::piped())
602 .stderr(Stdio::null())
603 .kill_on_drop(true);
604
605 let mut child = command.spawn().with_context(|| {
606 format!(
607 "failed to start language server {}",
608 config.program.to_string_lossy()
609 )
610 })?;
611 let stdin = child
612 .stdin
613 .take()
614 .context("language server did not expose stdin")?;
615 let stdout = child
616 .stdout
617 .take()
618 .context("language server did not expose stdout")?;
619 let (client, status_receiver, connection_task, hierarchy_capabilities) = spawn_json_rpc(
620 BufReader::new(stdout),
621 stdin,
622 workspace_uri.clone(),
623 workspace_name.clone(),
624 );
625
626 let capabilities = client_capabilities();
627 let initialization_options = workspace_symbol_initialization_options(
628 &config.program,
629 config.initialization_options.clone(),
630 );
631 let initialize_params = InitializeParams {
632 process_id: Some(std::process::id()),
633 root_uri: Some(workspace_uri.clone()),
634 initialization_options,
635 capabilities,
636 workspace_folders: Some(vec![WorkspaceFolder {
637 uri: workspace_uri,
638 name: workspace_name,
639 }]),
640 client_info: Some(ClientInfo {
641 name: env!("CARGO_PKG_NAME").to_owned(),
642 version: Some(env!("CARGO_PKG_VERSION").to_owned()),
643 }),
644 ..InitializeParams::default()
645 };
646
647 let initialize_result: InitializeResult = client
648 .request(Initialize::METHOD, initialize_params)
649 .await
650 .context("language server initialization failed")?;
651 if !uses_utf16_positions(initialize_result.capabilities.position_encoding.as_ref()) {
652 bail!("language server selected a position encoding other than UTF-16");
653 }
654 if !workspace_symbol_supported(&initialize_result) {
655 bail!("language server does not support workspace/symbol");
656 }
657 hierarchy_capabilities.set_static_call(call_hierarchy_supported(&initialize_result));
658
659 client
660 .notify("initialized", json!({}))
661 .await
662 .context("failed to notify language server that initialization completed")?;
663
664 let symbol_names = SymbolNameAdapter::detect(
665 &config.program,
666 initialize_result
667 .server_info
668 .as_ref()
669 .map(|info| info.name.as_str()),
670 );
671 let bootstrap_document = if symbol_names.is_pyrefly() {
672 match pyrefly::bootstrap_document(&workspace_root) {
673 Some(document) => {
674 client
675 .notify(
676 "textDocument/didOpen",
677 DidOpenTextDocumentParams {
678 text_document: TextDocumentItem {
679 uri: document.uri.clone(),
680 language_id: "python".to_owned(),
681 version: 0,
682 text: document.text,
683 },
684 },
685 )
686 .await
687 .context("failed to open Pyrefly index bootstrap document")?;
688 Some(document.uri)
689 }
690 None => None,
691 }
692 } else if is_clangd_program(&config.program)
693 || initialize_result
694 .server_info
695 .as_ref()
696 .is_some_and(|info| info.name.eq_ignore_ascii_case("clangd"))
697 {
698 match clangd::bootstrap_document(&workspace_root) {
699 Some(document) => {
700 client
701 .notify(
702 "textDocument/didOpen",
703 DidOpenTextDocumentParams {
704 text_document: TextDocumentItem {
705 uri: document.uri.clone(),
706 language_id: document.language_id.to_owned(),
707 version: 0,
708 text: document.text,
709 },
710 },
711 )
712 .await
713 .context("failed to open clangd index bootstrap document")?;
714 Some(document.uri)
715 }
716 None => None,
717 }
718 } else {
719 None
720 };
721 Ok(Self {
722 child,
723 client,
724 connection_task,
725 workspace_root,
726 server_info: initialize_result.server_info,
727 symbol_names,
728 document_symbols: Arc::new(Mutex::new(HashMap::new())),
729 hierarchy_capabilities,
730 bootstrap_document,
731 status_receiver: Some(status_receiver),
732 workspace_only: config.workspace_only,
733 })
734 }
735
736 pub fn workspace_root(&self) -> &Path {
737 &self.workspace_root
738 }
739
740 pub fn server_info(&self) -> Option<&ServerInfo> {
741 self.server_info.as_ref()
742 }
743
744 pub fn take_status_receiver(&mut self) -> Option<mpsc::UnboundedReceiver<LspStatusUpdate>> {
745 self.status_receiver.take()
746 }
747
748 pub fn workspace_symbol_client(&self) -> WorkspaceSymbolClient {
749 WorkspaceSymbolClient {
752 client: self.client.clone(),
753 workspace_root: self.workspace_root.clone(),
754 symbol_names: self.symbol_names,
755 workspace_only: self.workspace_only,
756 }
757 }
758
759 pub fn hierarchy_client(&self) -> HierarchyClient {
760 HierarchyClient {
761 client: self.client.clone(),
762 workspace_root: self.workspace_root.clone(),
763 symbol_names: self.symbol_names,
764 document_symbols: Arc::clone(&self.document_symbols),
765 capabilities: Arc::clone(&self.hierarchy_capabilities),
766 workspace_only: self.workspace_only,
767 }
768 }
769
770 pub async fn workspace_symbols(&self, query: &str) -> Result<Vec<WorkspaceSymbolMatch>> {
771 self.workspace_symbol_client().query(query).await
772 }
773
774 pub async fn shutdown(mut self) -> Result<()> {
775 if let Some(uri) = self.bootstrap_document.take() {
776 let _ = self
777 .client
778 .notify(
779 "textDocument/didClose",
780 DidCloseTextDocumentParams {
781 text_document: TextDocumentIdentifier::new(uri),
782 },
783 )
784 .await;
785 }
786 let shutdown_result = self
787 .client
788 .request::<_, ()>(Shutdown::METHOD, ())
789 .await
790 .context("language server shutdown request failed");
791 let _ = self.client.notify("exit", Value::Null).await;
792
793 if timeout(SHUTDOWN_TIMEOUT, self.child.wait()).await.is_err() {
794 self.child
795 .kill()
796 .await
797 .context("failed to stop language server after shutdown timeout")?;
798 self.child
799 .wait()
800 .await
801 .context("failed to reap language server process")?;
802 }
803
804 self.connection_task.abort();
805 let _ = self.connection_task.await;
806
807 shutdown_result
808 }
809}
810
811fn client_capabilities() -> ClientCapabilities {
812 ClientCapabilities {
813 text_document: Some(TextDocumentClientCapabilities {
814 call_hierarchy: Some(CallHierarchyClientCapabilities {
815 dynamic_registration: Some(true),
816 }),
817 document_symbol: Some(DocumentSymbolClientCapabilities::default()),
818 type_hierarchy: Some(TypeHierarchyClientCapabilities {
819 dynamic_registration: Some(true),
820 }),
821 ..TextDocumentClientCapabilities::default()
822 }),
823 workspace: Some(WorkspaceClientCapabilities {
824 symbol: Some(WorkspaceSymbolClientCapabilities::default()),
825 workspace_folders: Some(true),
826 configuration: Some(true),
827 ..WorkspaceClientCapabilities::default()
828 }),
829 window: Some(WindowClientCapabilities {
830 work_done_progress: Some(true),
831 ..WindowClientCapabilities::default()
832 }),
833 general: Some(GeneralClientCapabilities {
834 position_encodings: Some(vec![PositionEncodingKind::UTF16]),
835 ..GeneralClientCapabilities::default()
836 }),
837 experimental: Some(json!({
838 "serverStatusNotification": true,
839 })),
840 }
841}
842
843fn uses_utf16_positions(position_encoding: Option<&PositionEncodingKind>) -> bool {
844 position_encoding.is_none_or(|encoding| encoding == &PositionEncodingKind::UTF16)
845}
846
847#[derive(Clone)]
848struct JsonRpcClient {
849 commands: mpsc::Sender<JsonRpcCommand>,
850 cancellations: mpsc::UnboundedSender<u64>,
851}
852
853enum JsonRpcCommand {
854 Request {
855 method: String,
856 params: Value,
857 started: oneshot::Sender<u64>,
858 response: oneshot::Sender<std::result::Result<Value, String>>,
859 },
860 Notify {
861 method: String,
862 params: Value,
863 response: oneshot::Sender<std::result::Result<(), String>>,
864 },
865}
866
867struct RequestCancellationGuard {
868 request_id: u64,
869 cancellations: mpsc::UnboundedSender<u64>,
870 armed: bool,
871}
872
873impl RequestCancellationGuard {
874 fn disarm(&mut self) {
875 self.armed = false;
876 }
877}
878
879impl Drop for RequestCancellationGuard {
880 fn drop(&mut self) {
881 if self.armed {
882 let _ = self.cancellations.send(self.request_id);
885 }
886 }
887}
888
889impl JsonRpcClient {
890 async fn request<P, T>(&self, method: &str, params: P) -> Result<T>
891 where
892 P: Serialize,
893 T: DeserializeOwned,
894 {
895 let params = serde_json::to_value(params)
896 .with_context(|| format!("failed to encode parameters for LSP request {method}"))?;
897 let (started_sender, started_receiver) = oneshot::channel();
898 let (response_sender, response_receiver) = oneshot::channel();
899 self.commands
900 .send(JsonRpcCommand::Request {
901 method: method.to_owned(),
902 params,
903 started: started_sender,
904 response: response_sender,
905 })
906 .await
907 .map_err(|_| anyhow::anyhow!("LSP connection closed before request {method}"))?;
908 let request_id = started_receiver.await.map_err(|_| {
909 anyhow::anyhow!("LSP connection closed while starting request {method}")
910 })?;
911 let mut cancellation_guard = RequestCancellationGuard {
912 request_id,
913 cancellations: self.cancellations.clone(),
914 armed: true,
915 };
916 let response = response_receiver
917 .await
918 .map_err(|_| anyhow::anyhow!("LSP connection closed during request {method}"))?
919 .map_err(anyhow::Error::msg)?;
920 cancellation_guard.disarm();
921
922 serde_json::from_value(response)
923 .with_context(|| format!("invalid response to LSP request {method}"))
924 }
925
926 async fn notify<P>(&self, method: &str, params: P) -> Result<()>
927 where
928 P: Serialize,
929 {
930 let params = serde_json::to_value(params).with_context(|| {
931 format!("failed to encode parameters for LSP notification {method}")
932 })?;
933 let (response_sender, response_receiver) = oneshot::channel();
934 self.commands
935 .send(JsonRpcCommand::Notify {
936 method: method.to_owned(),
937 params,
938 response: response_sender,
939 })
940 .await
941 .map_err(|_| anyhow::anyhow!("LSP connection closed before notification {method}"))?;
942 response_receiver
943 .await
944 .map_err(|_| anyhow::anyhow!("LSP connection closed during notification {method}"))?
945 .map_err(anyhow::Error::msg)
946 }
947}
948
949fn spawn_json_rpc<R, W>(
950 reader: R,
951 writer: W,
952 workspace_uri: Url,
953 workspace_name: String,
954) -> (
955 JsonRpcClient,
956 mpsc::UnboundedReceiver<LspStatusUpdate>,
957 JoinHandle<Result<()>>,
958 Arc<ServerHierarchyCapabilities>,
959)
960where
961 R: AsyncBufRead + Send + Unpin + 'static,
962 W: AsyncWrite + Send + Unpin + 'static,
963{
964 let (command_sender, command_receiver) = mpsc::channel(32);
969 let (cancellation_sender, cancellation_receiver) = mpsc::unbounded_channel();
970 let (status_sender, status_receiver) = mpsc::unbounded_channel();
971 let (incoming_sender, incoming_receiver) = mpsc::channel(64);
972 let hierarchy_capabilities = Arc::new(ServerHierarchyCapabilities::default());
973 let reader_task = tokio::spawn(read_messages(reader, incoming_sender));
974 let actor_hierarchy_capabilities = Arc::clone(&hierarchy_capabilities);
975 let server_context = LspServerContext {
976 workspace_uri,
977 workspace_name,
978 hierarchy_capabilities: actor_hierarchy_capabilities,
979 };
980 let connection_task = tokio::spawn(async move {
981 let result = run_json_rpc(
982 writer,
983 command_receiver,
984 cancellation_receiver,
985 incoming_receiver,
986 status_sender,
987 server_context,
988 )
989 .await;
990 reader_task.abort();
991 let _ = reader_task.await;
992 result
993 });
994
995 let client = JsonRpcClient {
996 commands: command_sender,
997 cancellations: cancellation_sender,
998 };
999 (
1000 client,
1001 status_receiver,
1002 connection_task,
1003 hierarchy_capabilities,
1004 )
1005}
1006
1007struct LspServerContext {
1008 workspace_uri: Url,
1009 workspace_name: String,
1010 hierarchy_capabilities: Arc<ServerHierarchyCapabilities>,
1011}
1012
1013async fn read_messages<R>(mut reader: R, sender: mpsc::Sender<std::result::Result<Value, String>>)
1014where
1015 R: AsyncBufRead + Unpin,
1016{
1017 loop {
1018 match read_message(&mut reader).await {
1019 Ok(message) => {
1020 if sender.send(Ok(message)).await.is_err() {
1021 break;
1022 }
1023 }
1024 Err(error) => {
1025 let _ = sender.send(Err(error.to_string())).await;
1026 break;
1027 }
1028 }
1029 }
1030}
1031
1032async fn run_json_rpc<W>(
1033 mut writer: W,
1034 mut commands: mpsc::Receiver<JsonRpcCommand>,
1035 mut cancellations: mpsc::UnboundedReceiver<u64>,
1036 mut incoming: mpsc::Receiver<std::result::Result<Value, String>>,
1037 status_sender: mpsc::UnboundedSender<LspStatusUpdate>,
1038 server_context: LspServerContext,
1039) -> Result<()>
1040where
1041 W: AsyncWrite + Unpin,
1042{
1043 let mut next_request_id = 1_u64;
1044 let mut pending = HashMap::new();
1045 let mut progress_tracker = LspProgressTracker::default();
1046
1047 let connection_result = loop {
1048 tokio::select! {
1049 command = commands.recv() => {
1050 let Some(command) = command else {
1051 break Ok(());
1052 };
1053 match command {
1054 JsonRpcCommand::Request { method, params, started, response } => {
1055 let request_id = next_request_id;
1056 next_request_id += 1;
1057 let message = json!({
1058 "jsonrpc": "2.0",
1059 "id": request_id,
1060 "method": method,
1061 "params": params,
1062 });
1063 if let Err(error) = write_message(&mut writer, &message).await {
1064 let _ = response.send(Err(error.to_string()));
1065 break Err(error);
1066 }
1067 pending.insert(request_id, response);
1068 if started.send(request_id).is_err() {
1069 pending.remove(&request_id);
1070 write_cancel_request(&mut writer, request_id).await?;
1071 }
1072 }
1073 JsonRpcCommand::Notify { method, params, response } => {
1074 let message = json!({
1075 "jsonrpc": "2.0",
1076 "method": method,
1077 "params": params,
1078 });
1079 match write_message(&mut writer, &message).await {
1080 Ok(()) => {
1081 let _ = response.send(Ok(()));
1082 }
1083 Err(error) => {
1084 let _ = response.send(Err(error.to_string()));
1085 break Err(error);
1086 }
1087 }
1088 }
1089 }
1090 }
1091 request_id = cancellations.recv() => {
1092 let Some(request_id) = request_id else {
1093 break Ok(());
1094 };
1095 if pending.remove(&request_id).is_some() {
1096 write_cancel_request(&mut writer, request_id).await?;
1097 }
1098 }
1099 message = incoming.recv() => {
1100 let Some(message) = message else {
1101 break Err(anyhow::anyhow!("LSP message reader stopped unexpectedly"));
1102 };
1103 let message = match message {
1104 Ok(message) => message,
1105 Err(error) => break Err(anyhow::Error::msg(error)),
1106 };
1107
1108 if let Some(request_id) = response_id(&message) {
1109 if let Some(response) = pending.remove(&request_id) {
1110 let result = match message.get("error") {
1111 Some(error) if !error.is_null() => Err(format!(
1112 "LSP request failed: {error}"
1113 )),
1114 _ => Ok(message.get("result").cloned().unwrap_or(Value::Null)),
1115 };
1116 let _ = response.send(result);
1117 }
1118 } else if message.get("method").is_some() {
1119 handle_server_notification(
1120 &message,
1121 &mut progress_tracker,
1122 &status_sender,
1123 );
1124 if let Err(error) = handle_server_message(
1125 &mut writer,
1126 &message,
1127 &server_context.workspace_uri,
1128 &server_context.workspace_name,
1129 &server_context.hierarchy_capabilities,
1130 )
1131 .await
1132 {
1133 break Err(error);
1134 }
1135 }
1136 }
1137 }
1138 };
1139
1140 let failure = connection_result
1141 .as_ref()
1142 .err()
1143 .map_or_else(|| "LSP connection closed".to_owned(), ToString::to_string);
1144 let _ = status_sender.send(LspStatusUpdate::Disconnected(failure.clone()));
1145 for (_, response) in pending {
1146 let _ = response.send(Err(failure.clone()));
1147 }
1148
1149 connection_result
1150}
1151
1152async fn write_cancel_request<W>(writer: &mut W, request_id: u64) -> Result<()>
1153where
1154 W: AsyncWrite + Unpin,
1155{
1156 write_message(
1157 writer,
1158 &json!({
1159 "jsonrpc": "2.0",
1160 "method": "$/cancelRequest",
1161 "params": { "id": request_id },
1162 }),
1163 )
1164 .await
1165}
1166
1167fn handle_server_notification(
1168 message: &Value,
1169 tracker: &mut LspProgressTracker,
1170 sender: &mpsc::UnboundedSender<LspStatusUpdate>,
1171) {
1172 match message.get("method").and_then(Value::as_str) {
1173 Some("$/progress") => {
1174 let Some(params) = message.get("params").cloned() else {
1175 return;
1176 };
1177 let Ok(params) = serde_json::from_value::<ProgressParams>(params) else {
1178 return;
1179 };
1180 let ProgressParamsValue::WorkDone(progress) = params.value;
1181 tracker.update(params.token, progress, sender);
1182 }
1183 Some("experimental/serverStatus") => {
1184 let Some(params) = message.get("params") else {
1185 return;
1186 };
1187 let health = params.get("health").and_then(Value::as_str).unwrap_or("ok");
1188 let quiescent = params
1189 .get("quiescent")
1190 .and_then(Value::as_bool)
1191 .unwrap_or(false);
1192 let message = params
1193 .get("message")
1194 .and_then(Value::as_str)
1195 .map(str::to_owned);
1196
1197 let update = match health {
1198 "warning" => LspStatusUpdate::Warning(
1199 message.unwrap_or_else(|| "Language server reported a warning".to_owned()),
1200 ),
1201 "error" => LspStatusUpdate::Error(
1202 message.unwrap_or_else(|| "Language server reported an error".to_owned()),
1203 ),
1204 _ if quiescent => {
1205 if tracker.emit_latest(sender) {
1206 return;
1207 }
1208 LspStatusUpdate::Ready { message }
1209 }
1210 _ => {
1211 if tracker.emit_latest(sender) {
1212 return;
1213 }
1214 LspStatusUpdate::Progress {
1215 title: "rust-analyzer".to_owned(),
1216 message: message.or_else(|| Some("Background work in progress".to_owned())),
1217 percentage: None,
1218 }
1219 }
1220 };
1221 let _ = sender.send(update);
1222 }
1223 _ => {}
1224 }
1225}
1226
1227impl LspProgressTracker {
1228 fn update(
1229 &mut self,
1230 token: NumberOrString,
1231 progress: WorkDoneProgress,
1232 sender: &mpsc::UnboundedSender<LspStatusUpdate>,
1233 ) {
1234 let token = progress_token_key(token);
1235 self.next_sequence = self.next_sequence.wrapping_add(1);
1236 match progress {
1237 WorkDoneProgress::Begin(progress) => {
1238 self.active.insert(
1239 token,
1240 ActiveProgress {
1241 sequence: self.next_sequence,
1242 title: progress.title,
1243 message: progress.message,
1244 percentage: progress.percentage,
1245 },
1246 );
1247 self.emit_latest(sender);
1248 }
1249 WorkDoneProgress::Report(progress) => {
1250 if let Some(active) = self.active.get_mut(&token) {
1251 active.sequence = self.next_sequence;
1252 if progress.message.is_some() {
1253 active.message = progress.message;
1254 }
1255 if progress.percentage.is_some() {
1256 active.percentage = progress.percentage;
1257 }
1258 self.emit_latest(sender);
1259 }
1260 }
1261 WorkDoneProgress::End(progress) => {
1262 self.active.remove(&token);
1263 if !self.emit_latest(sender) {
1264 let _ = sender.send(LspStatusUpdate::Ready {
1265 message: progress.message,
1266 });
1267 }
1268 }
1269 }
1270 }
1271
1272 fn emit_latest(&self, sender: &mpsc::UnboundedSender<LspStatusUpdate>) -> bool {
1273 let Some(progress) = self
1274 .active
1275 .values()
1276 .max_by_key(|progress| progress.sequence)
1277 else {
1278 return false;
1279 };
1280 let _ = sender.send(LspStatusUpdate::Progress {
1281 title: progress.title.clone(),
1282 message: progress.message.clone(),
1283 percentage: progress.percentage,
1284 });
1285 true
1286 }
1287}
1288
1289fn progress_token_key(token: NumberOrString) -> String {
1290 match token {
1291 NumberOrString::Number(number) => format!("number:{number}"),
1292 NumberOrString::String(string) => format!("string:{string}"),
1293 }
1294}
1295
1296async fn handle_server_message<W>(
1297 writer: &mut W,
1298 message: &Value,
1299 workspace_uri: &Url,
1300 workspace_name: &str,
1301 hierarchy_capabilities: &ServerHierarchyCapabilities,
1302) -> Result<()>
1303where
1304 W: AsyncWrite + Unpin,
1305{
1306 let Some(id) = message.get("id").cloned() else {
1307 return Ok(());
1308 };
1309 let method = message
1310 .get("method")
1311 .and_then(Value::as_str)
1312 .context("LSP server request has no method")?;
1313
1314 let response = match method {
1315 "workspace/configuration" => {
1316 let values = message
1317 .pointer("/params/items")
1318 .and_then(Value::as_array)
1319 .map(|items| {
1320 items
1321 .iter()
1322 .map(|item| {
1323 requested_configuration(item.get("section").and_then(Value::as_str))
1324 })
1325 .collect::<Vec<_>>()
1326 })
1327 .unwrap_or_default();
1328 json!({
1329 "jsonrpc": "2.0",
1330 "id": id,
1331 "result": values,
1332 })
1333 }
1334 "workspace/workspaceFolders" => json!({
1335 "jsonrpc": "2.0",
1336 "id": id,
1337 "result": [{
1338 "uri": workspace_uri,
1339 "name": workspace_name,
1340 }],
1341 }),
1342 "client/registerCapability" => {
1343 register_hierarchy_capabilities(message, hierarchy_capabilities);
1344 json!({
1345 "jsonrpc": "2.0",
1346 "id": id,
1347 "result": null,
1348 })
1349 }
1350 "client/unregisterCapability" => {
1351 unregister_hierarchy_capabilities(message, hierarchy_capabilities);
1352 json!({
1353 "jsonrpc": "2.0",
1354 "id": id,
1355 "result": null,
1356 })
1357 }
1358 "window/workDoneProgress/create" | "window/showMessageRequest" => json!({
1359 "jsonrpc": "2.0",
1360 "id": id,
1361 "result": null,
1362 }),
1363 _ => json!({
1364 "jsonrpc": "2.0",
1365 "id": id,
1366 "error": {
1367 "code": -32601,
1368 "message": format!("cgraph does not implement {method}"),
1369 },
1370 }),
1371 };
1372
1373 write_message(writer, &response).await
1374}
1375
1376fn register_hierarchy_capabilities(message: &Value, capabilities: &ServerHierarchyCapabilities) {
1377 let Some(registrations) = message
1378 .pointer("/params/registrations")
1379 .and_then(Value::as_array)
1380 else {
1381 return;
1382 };
1383 for registration in registrations {
1384 let Some(id) = registration.get("id").and_then(Value::as_str) else {
1385 continue;
1386 };
1387 let Some(method) = registration.get("method").and_then(Value::as_str) else {
1388 continue;
1389 };
1390 capabilities.register(id, method);
1391 }
1392}
1393
1394fn unregister_hierarchy_capabilities(message: &Value, capabilities: &ServerHierarchyCapabilities) {
1395 let Some(unregistrations) = message
1396 .pointer("/params/unregistrations")
1397 .or_else(|| message.pointer("/params/unregisterations"))
1398 .and_then(Value::as_array)
1399 else {
1400 return;
1401 };
1402 for unregistration in unregistrations {
1403 if let Some(id) = unregistration.get("id").and_then(Value::as_str) {
1404 capabilities.unregister(id);
1405 }
1406 }
1407}
1408
1409fn requested_configuration(section: Option<&str>) -> Value {
1410 match section {
1411 Some("rust-analyzer") => json!({
1412 "workspace": {
1413 "symbol": {
1414 "search": {
1415 "kind": "all_symbols",
1416 "scope": "workspace",
1417 }
1418 }
1419 }
1420 }),
1421 Some("rust-analyzer.workspace.symbol.search.kind") => json!("all_symbols"),
1422 Some("rust-analyzer.workspace.symbol.search.scope") => json!("workspace"),
1423 Some("python") => json!({}),
1424 _ => Value::Null,
1425 }
1426}
1427
1428fn is_pyrefly_program(program: &OsStr) -> bool {
1429 Path::new(program)
1430 .file_name()
1431 .unwrap_or(program)
1432 .to_string_lossy()
1433 .trim_end_matches(".exe")
1434 .eq_ignore_ascii_case("pyrefly")
1435}
1436
1437fn is_clangd_program(program: &OsStr) -> bool {
1438 Path::new(program)
1439 .file_name()
1440 .unwrap_or(program)
1441 .to_string_lossy()
1442 .trim_end_matches(".exe")
1443 .eq_ignore_ascii_case("clangd")
1444}
1445
1446fn symbol_leaf_name(symbol: &str) -> &str {
1447 let symbol = symbol
1448 .rsplit_once("::")
1449 .map(|(_, name)| name)
1450 .unwrap_or(symbol);
1451 symbol
1452 .rsplit_once('.')
1453 .map(|(_, name)| name)
1454 .unwrap_or(symbol)
1455}
1456
1457fn workspace_name(workspace_root: &Path) -> String {
1458 workspace_root
1459 .file_name()
1460 .and_then(OsStr::to_str)
1461 .unwrap_or("workspace")
1462 .to_owned()
1463}
1464
1465fn workspace_symbol_supported(initialize_result: &InitializeResult) -> bool {
1466 match &initialize_result.capabilities.workspace_symbol_provider {
1467 Some(OneOf::Left(supported)) => *supported,
1468 Some(OneOf::Right(_)) => true,
1469 None => false,
1470 }
1471}
1472
1473fn call_hierarchy_supported(initialize_result: &InitializeResult) -> bool {
1474 match initialize_result.capabilities.call_hierarchy_provider {
1475 Some(CallHierarchyServerCapability::Simple(supported)) => supported,
1476 Some(CallHierarchyServerCapability::Options(_)) => true,
1477 None => false,
1478 }
1479}
1480
1481fn workspace_symbol_initialization_options(
1482 program: &OsStr,
1483 options: Option<Value>,
1484) -> Option<Value> {
1485 if !is_rust_analyzer_program(program) {
1486 return options;
1487 }
1488
1489 let mut options = options.unwrap_or_else(|| json!({}));
1490 merge_json(
1491 &mut options,
1492 json!({
1493 "workspace": {
1494 "symbol": {
1495 "search": {
1496 "kind": "all_symbols",
1497 "scope": "workspace",
1498 }
1499 }
1500 }
1501 }),
1502 );
1503 Some(options)
1504}
1505
1506fn is_rust_analyzer_program(program: &OsStr) -> bool {
1507 let program_name = Path::new(program)
1508 .file_name()
1509 .and_then(OsStr::to_str)
1510 .unwrap_or_default();
1511 program_name.eq_ignore_ascii_case("rust-analyzer")
1512 || program_name.eq_ignore_ascii_case("rust-analyzer.exe")
1513}
1514
1515fn merge_json(target: &mut Value, overlay: Value) {
1516 match (target, overlay) {
1517 (Value::Object(target), Value::Object(overlay)) => {
1518 for (key, value) in overlay {
1519 merge_json(target.entry(key).or_insert(Value::Null), value);
1520 }
1521 }
1522 (target, overlay) => *target = overlay,
1523 }
1524}
1525
1526fn symbol_belongs_to_workspace(symbol: &WorkspaceSymbolMatch, workspace_root: &Path) -> bool {
1527 uri_belongs_to_workspace(&symbol.uri, workspace_root)
1528}
1529
1530fn workspace_symbol_is_visible(
1531 symbol: &WorkspaceSymbolMatch,
1532 workspace_root: &Path,
1533 workspace_only: bool,
1534) -> bool {
1535 !workspace_only || symbol_belongs_to_workspace(symbol, workspace_root)
1536}
1537
1538fn uri_belongs_to_workspace(uri: &Url, workspace_root: &Path) -> bool {
1539 uri.to_file_path()
1540 .is_ok_and(|path| path.starts_with(workspace_root))
1541}
1542
1543fn deduplicate_symbols(
1544 symbols: impl IntoIterator<Item = WorkspaceSymbolMatch>,
1545) -> Vec<WorkspaceSymbolMatch> {
1546 let mut unique = Vec::new();
1547 for symbol in symbols {
1548 let duplicate = unique.iter().any(|existing: &WorkspaceSymbolMatch| {
1549 existing.name == symbol.name
1550 && existing.kind == symbol.kind
1551 && existing.uri == symbol.uri
1552 && existing.range == symbol.range
1553 && existing.container_name == symbol.container_name
1554 });
1555 if !duplicate {
1556 unique.push(symbol);
1557 }
1558 }
1559 unique
1560}
1561
1562fn document_position(uri: Url, position: Position) -> (TextDocumentPositionParams, SourceLocation) {
1563 let location = SourceLocation {
1564 uri: uri.to_string(),
1565 line: Some(position.line),
1566 character: Some(position.character),
1567 };
1568 (
1569 TextDocumentPositionParams::new(TextDocumentIdentifier::new(uri), position),
1570 location,
1571 )
1572}
1573
1574fn symbol_kind_matches_hierarchy(kind: HierarchyKind, symbol_kind: SymbolKind) -> bool {
1575 match kind {
1576 HierarchyKind::Call => matches!(
1577 symbol_kind,
1578 SymbolKind::FUNCTION | SymbolKind::METHOD | SymbolKind::CONSTRUCTOR
1579 ),
1580 HierarchyKind::Type => matches!(
1581 symbol_kind,
1582 SymbolKind::CLASS
1583 | SymbolKind::INTERFACE
1584 | SymbolKind::STRUCT
1585 | SymbolKind::ENUM
1586 | SymbolKind::TYPE_PARAMETER
1587 ),
1588 }
1589}
1590
1591fn call_item_identity(
1592 item: CallHierarchyItem,
1593 symbol_names: SymbolNameAdapter,
1594 document_container: Option<&str>,
1595) -> SymbolIdentity {
1596 let symbol = symbol_names.call_hierarchy_item(
1597 &item.name,
1598 item.kind,
1599 item.detail.as_deref(),
1600 document_container,
1601 );
1602 SymbolIdentity {
1603 symbol,
1604 kind: HierarchyKind::Call,
1605 location: Some(SourceLocation {
1606 uri: item.uri.to_string(),
1607 line: Some(item.selection_range.start.line),
1608 character: Some(item.selection_range.start.character),
1609 }),
1610 }
1611}
1612
1613#[derive(Clone, Debug)]
1614struct DocumentSymbolOwner {
1615 name: String,
1616 kind: SymbolKind,
1617 range: Range,
1618 container_name: Option<String>,
1619}
1620
1621fn normalize_document_symbols(response: DocumentSymbolResponse) -> Vec<DocumentSymbolOwner> {
1622 match response {
1623 DocumentSymbolResponse::Flat(symbols) => {
1624 symbols.into_iter().map(document_symbol_owner).collect()
1625 }
1626 DocumentSymbolResponse::Nested(symbols) => {
1627 let mut normalized = Vec::new();
1628 normalize_nested_document_symbols(&symbols, None, &mut normalized);
1629 normalized
1630 }
1631 }
1632}
1633
1634#[allow(deprecated)]
1635fn document_symbol_owner(symbol: SymbolInformation) -> DocumentSymbolOwner {
1636 DocumentSymbolOwner {
1637 name: symbol.name,
1638 kind: symbol.kind,
1639 range: symbol.location.range,
1640 container_name: symbol.container_name,
1641 }
1642}
1643
1644fn normalize_nested_document_symbols(
1645 symbols: &[DocumentSymbol],
1646 container_name: Option<&str>,
1647 normalized: &mut Vec<DocumentSymbolOwner>,
1648) {
1649 for symbol in symbols {
1650 normalized.push(DocumentSymbolOwner {
1651 name: symbol.name.clone(),
1652 kind: symbol.kind,
1653 range: symbol.range,
1654 container_name: container_name.map(str::to_owned),
1655 });
1656 if let Some(children) = symbol.children.as_deref() {
1657 normalize_nested_document_symbols(children, Some(&symbol.name), normalized);
1658 }
1659 }
1660}
1661
1662fn find_document_symbol_container<'a>(
1663 symbols: &'a [DocumentSymbolOwner],
1664 item: &CallHierarchyItem,
1665) -> Option<&'a str> {
1666 symbols
1667 .iter()
1668 .filter(|symbol| {
1669 symbol.name == item.name
1670 && matches!(
1671 symbol.kind,
1672 SymbolKind::FUNCTION | SymbolKind::METHOD | SymbolKind::CONSTRUCTOR
1673 )
1674 && range_contains_position(symbol.range, item.selection_range.start)
1675 })
1676 .min_by_key(|symbol| range_span_key(symbol.range))
1677 .and_then(|symbol| symbol.container_name.as_deref())
1678}
1679
1680fn range_contains_position(range: Range, position: Position) -> bool {
1681 position_after_or_equal(position, range.start) && position_after_or_equal(range.end, position)
1682}
1683
1684fn position_after_or_equal(left: Position, right: Position) -> bool {
1685 (left.line, left.character) >= (right.line, right.character)
1686}
1687
1688fn range_span_key(range: Range) -> (u32, u32) {
1689 (
1690 range.end.line.saturating_sub(range.start.line),
1691 range.end.character.saturating_sub(range.start.character),
1692 )
1693}
1694
1695fn type_item_identity(item: TypeHierarchyItem) -> SymbolIdentity {
1696 SymbolIdentity {
1697 symbol: item.name,
1698 kind: HierarchyKind::Type,
1699 location: Some(SourceLocation {
1700 uri: item.uri.to_string(),
1701 line: Some(item.selection_range.start.line),
1702 character: Some(item.selection_range.start.character),
1703 }),
1704 }
1705}
1706
1707fn deduplicate_identities(
1708 identities: impl IntoIterator<Item = SymbolIdentity>,
1709) -> Vec<SymbolIdentity> {
1710 let mut unique = Vec::new();
1711 for identity in identities {
1712 if !unique.contains(&identity) {
1713 unique.push(identity);
1714 }
1715 }
1716 unique
1717}
1718
1719fn normalize_symbols(
1720 response: WorkspaceSymbolResponse,
1721 symbol_names: SymbolNameAdapter,
1722) -> Vec<WorkspaceSymbolMatch> {
1723 match response {
1724 WorkspaceSymbolResponse::Flat(symbols) => symbols
1725 .into_iter()
1726 .map(|symbol| {
1727 let name = symbol_names.workspace_symbol(
1728 &symbol.name,
1729 symbol.kind,
1730 symbol.container_name.as_deref(),
1731 );
1732 WorkspaceSymbolMatch {
1733 name,
1734 kind: symbol.kind,
1735 container_name: symbol.container_name,
1736 uri: symbol.location.uri,
1737 range: Some(symbol.location.range),
1738 }
1739 })
1740 .collect(),
1741 WorkspaceSymbolResponse::Nested(symbols) => symbols
1742 .into_iter()
1743 .map(|symbol| {
1744 let (uri, range) = match symbol.location {
1745 OneOf::Left(Location { uri, range }) => (uri, Some(range)),
1746 OneOf::Right(location) => (location.uri, None),
1747 };
1748 let name = symbol_names.workspace_symbol(
1749 &symbol.name,
1750 symbol.kind,
1751 symbol.container_name.as_deref(),
1752 );
1753 WorkspaceSymbolMatch {
1754 name,
1755 kind: symbol.kind,
1756 container_name: symbol.container_name,
1757 uri,
1758 range,
1759 }
1760 })
1761 .collect(),
1762 }
1763}
1764
1765fn response_id(message: &Value) -> Option<u64> {
1766 message.get("id").and_then(Value::as_u64)
1767}
1768
1769async fn read_message<R>(reader: &mut R) -> Result<Value>
1770where
1771 R: AsyncBufRead + Unpin,
1772{
1773 let mut content_length = None;
1774
1775 loop {
1776 let mut header = String::new();
1777 if reader.read_line(&mut header).await? == 0 {
1778 bail!("language server closed its output stream");
1779 }
1780 let header = header.trim_end_matches(['\r', '\n']);
1781 if header.is_empty() {
1782 break;
1783 }
1784
1785 let Some((name, value)) = header.split_once(':') else {
1786 bail!("malformed LSP header: {header:?}");
1787 };
1788 if name.eq_ignore_ascii_case("Content-Length") {
1789 content_length = Some(
1790 value
1791 .trim()
1792 .parse::<usize>()
1793 .context("invalid LSP Content-Length header")?,
1794 );
1795 }
1796 }
1797
1798 let content_length = content_length.context("LSP message has no Content-Length header")?;
1799 if content_length > MAX_MESSAGE_SIZE {
1800 bail!("LSP message is too large: {content_length} bytes (limit: {MAX_MESSAGE_SIZE} bytes)");
1801 }
1802
1803 let mut body = vec![0; content_length];
1804 reader
1805 .read_exact(&mut body)
1806 .await
1807 .context("language server closed its output stream mid-message")?;
1808 serde_json::from_slice(&body).context("language server sent invalid JSON")
1809}
1810
1811async fn write_message<W>(writer: &mut W, message: &Value) -> Result<()>
1812where
1813 W: AsyncWrite + Unpin,
1814{
1815 let body = serde_json::to_vec(message).context("failed to encode LSP message")?;
1816 let header = format!("Content-Length: {}\r\n\r\n", body.len());
1817 writer.write_all(header.as_bytes()).await?;
1818 writer.write_all(&body).await?;
1819 writer.flush().await?;
1820 Ok(())
1821}
1822
1823#[cfg(test)]
1824mod tests {
1825 use std::{
1826 ffi::OsStr,
1827 fs,
1828 path::{Path, PathBuf},
1829 sync::Arc,
1830 time::{Duration, SystemTime, UNIX_EPOCH},
1831 };
1832
1833 use serde_json::{Value, json};
1834 use tokio::io::{BufReader, duplex, split};
1835 use tokio::time::timeout;
1836 use tower_lsp::lsp_types::{
1837 SymbolKind, Url, WorkspaceSymbolParams, WorkspaceSymbolResponse,
1838 request::{DocumentSymbolRequest, Request},
1839 };
1840
1841 use super::symbol_names::SymbolNameAdapter;
1842 use super::{
1843 HierarchyClient, LspConfig, LspProgressTracker, LspProvider, LspStatusUpdate,
1844 WorkspaceSymbolMatch, client_capabilities, deduplicate_symbols, handle_server_notification,
1845 normalize_symbols, read_message, requested_configuration, response_id, spawn_json_rpc,
1846 symbol_belongs_to_workspace, symbol_leaf_name, uses_utf16_positions,
1847 workspace_symbol_initialization_options, workspace_symbol_is_visible, write_message,
1848 };
1849 use crate::fetch::treesitter::{TreeSitterLanguage, TreeSitterProvider};
1850 use crate::{
1851 fetch::{FetchSource, HierarchyClient as FetchHierarchyClient, HierarchyQuery},
1852 state::{HierarchyDirection, HierarchyKind, SourceLocation, SymbolIdentity},
1853 };
1854
1855 #[test]
1856 fn excludes_symbols_outside_the_workspace() {
1857 let project_symbol = symbol("file:///workspace/src/main.rs");
1858 let dependency_symbol = symbol("file:///registry/dependency/src/lib.rs");
1859 let sibling_symbol = symbol("file:///workspace-other/src/lib.rs");
1860
1861 assert!(symbol_belongs_to_workspace(
1862 &project_symbol,
1863 Path::new("/workspace")
1864 ));
1865 assert!(!symbol_belongs_to_workspace(
1866 &dependency_symbol,
1867 Path::new("/workspace")
1868 ));
1869 assert!(!symbol_belongs_to_workspace(
1870 &sibling_symbol,
1871 Path::new("/workspace")
1872 ));
1873 assert!(workspace_symbol_is_visible(
1874 &dependency_symbol,
1875 Path::new("/workspace"),
1876 false
1877 ));
1878 assert!(!workspace_symbol_is_visible(
1879 &dependency_symbol,
1880 Path::new("/workspace"),
1881 true
1882 ));
1883 }
1884
1885 #[test]
1886 fn configures_rust_analyzer_for_project_only_all_symbol_queries() {
1887 assert_eq!(
1888 requested_configuration(Some("rust-analyzer.workspace.symbol.search.kind")),
1889 json!("all_symbols")
1890 );
1891 assert_eq!(
1892 requested_configuration(Some("rust-analyzer.workspace.symbol.search.scope")),
1893 json!("workspace")
1894 );
1895 assert_eq!(
1896 requested_configuration(Some("rust-analyzer.workspace.symbol.search.limit")),
1897 Value::Null
1898 );
1899 assert_eq!(requested_configuration(Some("clangd")), Value::Null);
1900 assert_eq!(requested_configuration(Some("python")), json!({}));
1901
1902 let options = workspace_symbol_initialization_options(
1903 OsStr::new("rust-analyzer"),
1904 Some(json!({ "cargo": { "features": "all" } })),
1905 )
1906 .unwrap();
1907 assert_eq!(options["cargo"]["features"], "all");
1908 assert_eq!(
1909 options["workspace"]["symbol"]["search"]["kind"],
1910 "all_symbols"
1911 );
1912 assert_eq!(
1913 options["workspace"]["symbol"]["search"]["scope"],
1914 "workspace"
1915 );
1916 assert!(
1917 options["workspace"]["symbol"]["search"]
1918 .get("limit")
1919 .is_none()
1920 );
1921
1922 assert_eq!(
1923 workspace_symbol_initialization_options(
1924 OsStr::new("clangd"),
1925 Some(json!({ "clangd": true })),
1926 ),
1927 Some(json!({ "clangd": true }))
1928 );
1929 assert!(
1930 !LspConfig::for_server("clangd", "/workspace")
1931 .workspace_only(false)
1932 .workspace_only
1933 );
1934 }
1935
1936 #[test]
1937 fn configures_pyrefly_command_and_python_symbol_leaf_names() {
1938 let config = LspConfig::for_server("/tools/pyrefly.exe", "/workspace")
1939 .arg("--indexing-mode")
1940 .arg("lazy-blocking");
1941 assert_eq!(config.program, OsStr::new("/tools/pyrefly.exe"));
1942 assert_eq!(
1943 config.args,
1944 ["lsp", "--indexing-mode", "lazy-blocking"].map(std::ffi::OsString::from)
1945 );
1946 assert!(LspConfig::for_server("pylsp", "/workspace").args.is_empty());
1947 assert_eq!(symbol_leaf_name("Worker.run"), "run");
1948 assert_eq!(symbol_leaf_name("Worker::run"), "run");
1949 assert_eq!(symbol_leaf_name("run"), "run");
1950 }
1951
1952 #[test]
1953 fn negotiates_only_utf16_source_positions() {
1954 let capabilities = serde_json::to_value(client_capabilities()).unwrap();
1955 assert_eq!(
1956 capabilities["general"]["positionEncodings"],
1957 json!(["utf-16"])
1958 );
1959 assert_eq!(
1960 capabilities["textDocument"]["callHierarchy"]["dynamicRegistration"],
1961 json!(true)
1962 );
1963 assert_eq!(
1964 capabilities["textDocument"]["typeHierarchy"]["dynamicRegistration"],
1965 json!(true)
1966 );
1967 assert!(uses_utf16_positions(None));
1968 assert!(uses_utf16_positions(Some(
1969 &tower_lsp::lsp_types::PositionEncodingKind::UTF16
1970 )));
1971 assert!(!uses_utf16_positions(Some(
1972 &tower_lsp::lsp_types::PositionEncodingKind::UTF8
1973 )));
1974 }
1975
1976 #[test]
1977 fn deduplicates_identical_workspace_symbols() {
1978 let duplicate = symbol("file:///workspace/src/main.rs");
1979 assert_eq!(
1980 deduplicate_symbols([duplicate.clone(), duplicate.clone(), duplicate]),
1981 vec![symbol("file:///workspace/src/main.rs")]
1982 );
1983 }
1984
1985 #[tokio::test]
1986 async fn prepares_and_queries_outgoing_call_hierarchy() {
1987 let (client_stream, server_stream) = duplex(8 * 1024);
1988 let (client_reader, client_writer) = split(client_stream);
1989 let (server_reader, mut server_writer) = split(server_stream);
1990 let workspace_uri = Url::parse("file:///workspace").unwrap();
1991 let (rpc_client, _status_receiver, connection_task, capabilities) = spawn_json_rpc(
1992 BufReader::new(client_reader),
1993 client_writer,
1994 workspace_uri,
1995 "workspace".to_owned(),
1996 );
1997 capabilities.set_static_call(true);
1998 let hierarchy_client = HierarchyClient {
1999 client: rpc_client.clone(),
2000 workspace_root: PathBuf::from("/workspace"),
2001 symbol_names: SymbolNameAdapter::RustAnalyzer,
2002 document_symbols: Default::default(),
2003 capabilities,
2004 workspace_only: true,
2005 };
2006 let query = HierarchyQuery {
2007 symbol: SymbolIdentity {
2008 symbol: "root".to_owned(),
2009 kind: HierarchyKind::Call,
2010 location: Some(SourceLocation {
2011 uri: "file:///workspace/src/main.rs".to_owned(),
2012 line: Some(4),
2013 character: Some(3),
2014 }),
2015 },
2016 direction: HierarchyDirection::Outgoing,
2017 };
2018 let client_task = tokio::spawn(async move { hierarchy_client.query(query).await.unwrap() });
2019 let mut server_reader = BufReader::new(server_reader);
2020
2021 let prepare = read_message(&mut server_reader).await.unwrap();
2022 assert_eq!(prepare["method"], "textDocument/prepareCallHierarchy");
2023 assert_eq!(
2024 prepare["params"]["position"],
2025 json!({ "line": 4, "character": 3 })
2026 );
2027 write_message(
2028 &mut server_writer,
2029 &json!({
2030 "jsonrpc": "2.0",
2031 "id": response_id(&prepare).unwrap(),
2032 "result": [call_item("root", 4)]
2033 }),
2034 )
2035 .await
2036 .unwrap();
2037
2038 let outgoing = read_message(&mut server_reader).await.unwrap();
2039 assert_eq!(outgoing["method"], "callHierarchy/outgoingCalls");
2040 assert_eq!(outgoing["params"]["item"]["name"], "root");
2041 write_message(
2042 &mut server_writer,
2043 &json!({
2044 "jsonrpc": "2.0",
2045 "id": response_id(&outgoing).unwrap(),
2046 "result": [
2047 { "to": rust_method_item("child", 8), "fromRanges": [] },
2048 { "to": rust_method_item("child", 8), "fromRanges": [] },
2049 { "to": external_call_item("printf", 12), "fromRanges": [] }
2050 ]
2051 }),
2052 )
2053 .await
2054 .unwrap();
2055
2056 let document_symbols = read_message(&mut server_reader).await.unwrap();
2057 assert_eq!(document_symbols["method"], DocumentSymbolRequest::METHOD);
2058 assert_eq!(
2059 document_symbols["params"]["textDocument"]["uri"],
2060 "file:///workspace/src/main.rs"
2061 );
2062 write_message(
2063 &mut server_writer,
2064 &json!({
2065 "jsonrpc": "2.0",
2066 "id": response_id(&document_symbols).unwrap(),
2067 "result": [{
2068 "name": "child",
2069 "kind": 12,
2070 "location": {
2071 "uri": "file:///workspace/src/main.rs",
2072 "range": {
2073 "start": { "line": 8, "character": 0 },
2074 "end": { "line": 10, "character": 1 }
2075 }
2076 },
2077 "containerName": "impl Worker"
2078 }]
2079 }),
2080 )
2081 .await
2082 .unwrap();
2083
2084 let response = client_task.await.unwrap();
2085 assert_eq!(response.source, FetchSource::Lsp);
2086 assert_eq!(response.children.len(), 1);
2087 assert_eq!(response.children[0].symbol, "Worker::child");
2088 assert_eq!(response.children[0].kind, HierarchyKind::Call);
2089 assert!(
2090 !response
2091 .children
2092 .iter()
2093 .any(|child| child.symbol == "printf")
2094 );
2095 assert_eq!(
2096 response.children[0].location.as_ref().unwrap().line,
2097 Some(8)
2098 );
2099
2100 drop(rpc_client);
2101 connection_task.abort();
2102 let _ = connection_task.await;
2103 }
2104
2105 #[tokio::test]
2106 async fn prepares_and_queries_type_supertypes() {
2107 let (client_stream, server_stream) = duplex(8 * 1024);
2108 let (client_reader, client_writer) = split(client_stream);
2109 let (server_reader, mut server_writer) = split(server_stream);
2110 let workspace_uri = Url::parse("file:///workspace").unwrap();
2111 let (rpc_client, _status_receiver, connection_task, capabilities) = spawn_json_rpc(
2112 BufReader::new(client_reader),
2113 client_writer,
2114 workspace_uri,
2115 "workspace".to_owned(),
2116 );
2117 let mut server_reader = BufReader::new(server_reader);
2118 write_message(
2119 &mut server_writer,
2120 &json!({
2121 "jsonrpc": "2.0",
2122 "id": "register-type-hierarchy",
2123 "method": "client/registerCapability",
2124 "params": {
2125 "registrations": [{
2126 "id": "type-hierarchy",
2127 "method": "textDocument/prepareTypeHierarchy",
2128 "registerOptions": {}
2129 }]
2130 }
2131 }),
2132 )
2133 .await
2134 .unwrap();
2135 let registration = read_message(&mut server_reader).await.unwrap();
2136 assert_eq!(registration["id"], "register-type-hierarchy");
2137 assert!(capabilities.supports(HierarchyKind::Type));
2138 let hierarchy_client = HierarchyClient {
2139 client: rpc_client.clone(),
2140 workspace_root: PathBuf::from("/workspace"),
2141 symbol_names: SymbolNameAdapter::Standard,
2142 document_symbols: Default::default(),
2143 capabilities: Arc::clone(&capabilities),
2144 workspace_only: true,
2145 };
2146 let query = HierarchyQuery {
2147 symbol: SymbolIdentity {
2148 symbol: "Child".to_owned(),
2149 kind: HierarchyKind::Type,
2150 location: Some(SourceLocation {
2151 uri: "file:///workspace/src/main.rs".to_owned(),
2152 line: Some(10),
2153 character: Some(7),
2154 }),
2155 },
2156 direction: HierarchyDirection::Incoming,
2157 };
2158 let client_task = tokio::spawn(async move { hierarchy_client.query(query).await.unwrap() });
2159 let prepare = read_message(&mut server_reader).await.unwrap();
2160 assert_eq!(prepare["method"], "textDocument/prepareTypeHierarchy");
2161 write_message(
2162 &mut server_writer,
2163 &json!({
2164 "jsonrpc": "2.0",
2165 "id": response_id(&prepare).unwrap(),
2166 "result": [type_item("Child", 10)]
2167 }),
2168 )
2169 .await
2170 .unwrap();
2171
2172 let supertypes = read_message(&mut server_reader).await.unwrap();
2173 assert_eq!(supertypes["method"], "typeHierarchy/supertypes");
2174 write_message(
2175 &mut server_writer,
2176 &json!({
2177 "jsonrpc": "2.0",
2178 "id": response_id(&supertypes).unwrap(),
2179 "result": [type_item("Parent", 2)]
2180 }),
2181 )
2182 .await
2183 .unwrap();
2184
2185 let response = client_task.await.unwrap();
2186 assert_eq!(response.children.len(), 1);
2187 assert_eq!(response.children[0].symbol, "Parent");
2188 assert_eq!(response.children[0].kind, HierarchyKind::Type);
2189
2190 write_message(
2191 &mut server_writer,
2192 &json!({
2193 "jsonrpc": "2.0",
2194 "id": "unregister-type-hierarchy",
2195 "method": "client/unregisterCapability",
2196 "params": {
2197 "unregistrations": [{
2198 "id": "type-hierarchy",
2199 "method": "textDocument/prepareTypeHierarchy"
2200 }]
2201 }
2202 }),
2203 )
2204 .await
2205 .unwrap();
2206 let unregistration = read_message(&mut server_reader).await.unwrap();
2207 assert_eq!(unregistration["id"], "unregister-type-hierarchy");
2208 assert!(!capabilities.supports(HierarchyKind::Type));
2209
2210 drop(rpc_client);
2211 connection_task.abort();
2212 let _ = connection_task.await;
2213 }
2214
2215 #[tokio::test]
2216 async fn falls_back_to_tree_sitter_for_unregistered_type_hierarchy() {
2217 let workspace = external_server_workspace("type-fallback");
2218 fs::write(
2219 workspace.join("lib.rs"),
2220 "trait Command {}\nstruct Cli;\nimpl Command for Cli {}\n",
2221 )
2222 .unwrap();
2223 let tree_sitter = TreeSitterProvider::start(&workspace, TreeSitterLanguage::Rust).unwrap();
2224 let symbols = tree_sitter
2225 .workspace_symbol_client()
2226 .query("")
2227 .await
2228 .unwrap();
2229 let cli = symbols.iter().find(|symbol| symbol.name == "Cli").unwrap();
2230 let position = cli.range.unwrap().start;
2231
2232 let (client_stream, server_stream) = duplex(8 * 1024);
2233 let (client_reader, client_writer) = split(client_stream);
2234 let (server_reader, _server_writer) = split(server_stream);
2235 let workspace_uri = Url::from_directory_path(&workspace).unwrap();
2236 let (rpc_client, _status_receiver, connection_task, capabilities) = spawn_json_rpc(
2237 BufReader::new(client_reader),
2238 client_writer,
2239 workspace_uri,
2240 workspace
2241 .file_name()
2242 .unwrap()
2243 .to_string_lossy()
2244 .into_owned(),
2245 );
2246 let lsp = HierarchyClient {
2247 client: rpc_client.clone(),
2248 workspace_root: workspace.clone(),
2249 symbol_names: SymbolNameAdapter::RustAnalyzer,
2250 document_symbols: Default::default(),
2251 capabilities,
2252 workspace_only: true,
2253 };
2254 let hybrid = FetchHierarchyClient::with_fallback(lsp, tree_sitter.hierarchy_client());
2255
2256 let response = hybrid
2257 .query(HierarchyQuery {
2258 symbol: SymbolIdentity {
2259 symbol: "Cli".to_owned(),
2260 kind: HierarchyKind::Type,
2261 location: Some(SourceLocation {
2262 uri: cli.uri.to_string(),
2263 line: Some(position.line),
2264 character: Some(position.character),
2265 }),
2266 },
2267 direction: HierarchyDirection::Incoming,
2268 })
2269 .await
2270 .unwrap();
2271
2272 assert_eq!(response.source, FetchSource::TreeSitter);
2273 assert_eq!(
2274 response
2275 .children
2276 .iter()
2277 .map(|child| child.symbol.as_str())
2278 .collect::<Vec<_>>(),
2279 ["Command"]
2280 );
2281 let mut server_reader = BufReader::new(server_reader);
2282 assert!(
2283 timeout(Duration::from_millis(20), read_message(&mut server_reader))
2284 .await
2285 .is_err(),
2286 "unsupported type hierarchy must not reach the LSP server"
2287 );
2288
2289 drop(rpc_client);
2290 connection_task.abort();
2291 let _ = connection_task.await;
2292 fs::remove_dir_all(workspace).unwrap();
2293 }
2294
2295 #[test]
2296 fn tracks_work_done_progress_until_the_last_operation_ends() {
2297 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2298 let mut tracker = LspProgressTracker::default();
2299
2300 handle_server_notification(
2301 &json!({
2302 "method": "$/progress",
2303 "params": {
2304 "token": "index",
2305 "value": {
2306 "kind": "begin",
2307 "title": "Indexing",
2308 "message": "1/2 crates",
2309 "percentage": 50
2310 }
2311 }
2312 }),
2313 &mut tracker,
2314 &sender,
2315 );
2316 assert_eq!(
2317 receiver.try_recv().unwrap(),
2318 LspStatusUpdate::Progress {
2319 title: "Indexing".to_owned(),
2320 message: Some("1/2 crates".to_owned()),
2321 percentage: Some(50),
2322 }
2323 );
2324
2325 handle_server_notification(
2326 &json!({
2327 "method": "$/progress",
2328 "params": {
2329 "token": "index",
2330 "value": { "kind": "end", "message": "Indexed" }
2331 }
2332 }),
2333 &mut tracker,
2334 &sender,
2335 );
2336 assert_eq!(
2337 receiver.try_recv().unwrap(),
2338 LspStatusUpdate::Ready {
2339 message: Some("Indexed".to_owned())
2340 }
2341 );
2342 }
2343
2344 #[test]
2345 fn translates_rust_analyzer_server_status() {
2346 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2347 let mut tracker = LspProgressTracker::default();
2348 handle_server_notification(
2349 &json!({
2350 "method": "experimental/serverStatus",
2351 "params": {
2352 "health": "warning",
2353 "quiescent": true,
2354 "message": "proc macro unavailable"
2355 }
2356 }),
2357 &mut tracker,
2358 &sender,
2359 );
2360
2361 assert_eq!(
2362 receiver.try_recv().unwrap(),
2363 LspStatusUpdate::Warning("proc macro unavailable".to_owned())
2364 );
2365 }
2366
2367 #[tokio::test]
2368 async fn handles_server_requests_while_waiting_for_symbols() {
2369 let (client_stream, server_stream) = duplex(8 * 1024);
2370 let (client_reader, client_writer) = split(client_stream);
2371 let (server_reader, mut server_writer) = split(server_stream);
2372 let workspace_uri = Url::parse("file:///workspace").unwrap();
2373 let (client, _status_receiver, connection_task, _capabilities) = spawn_json_rpc(
2374 BufReader::new(client_reader),
2375 client_writer,
2376 workspace_uri,
2377 "workspace".to_owned(),
2378 );
2379
2380 let mut server_reader = BufReader::new(server_reader);
2381 write_message(
2382 &mut server_writer,
2383 &json!({
2384 "jsonrpc": "2.0",
2385 "id": "server-request",
2386 "method": "workspace/configuration",
2387 "params": { "items": [{}, {}] },
2388 }),
2389 )
2390 .await
2391 .unwrap();
2392 let configuration_response = read_message(&mut server_reader).await.unwrap();
2393 assert_eq!(configuration_response["id"], "server-request");
2394 assert_eq!(configuration_response["result"], json!([null, null]));
2395
2396 let query_client = client.clone();
2397 let client_task = tokio::spawn(async move {
2398 let response: Option<WorkspaceSymbolResponse> = query_client
2399 .request(
2400 "workspace/symbol",
2401 WorkspaceSymbolParams {
2402 query: "run".to_owned(),
2403 ..WorkspaceSymbolParams::default()
2404 },
2405 )
2406 .await
2407 .unwrap();
2408 normalize_symbols(response.unwrap(), SymbolNameAdapter::Standard)
2409 });
2410
2411 let request = read_message(&mut server_reader).await.unwrap();
2412 assert_eq!(request["method"], "workspace/symbol");
2413 assert_eq!(request["params"]["query"], "run");
2414 let request_id = response_id(&request).unwrap();
2415
2416 write_message(
2417 &mut server_writer,
2418 &json!({
2419 "jsonrpc": "2.0",
2420 "method": "window/logMessage",
2421 "params": { "type": 3, "message": "indexed" },
2422 }),
2423 )
2424 .await
2425 .unwrap();
2426 write_message(
2427 &mut server_writer,
2428 &json!({
2429 "jsonrpc": "2.0",
2430 "id": request_id,
2431 "result": [{
2432 "name": "run",
2433 "kind": 12,
2434 "location": {
2435 "uri": "file:///workspace/src/main.rs",
2436 "range": {
2437 "start": { "line": 4, "character": 3 },
2438 "end": { "line": 4, "character": 6 }
2439 }
2440 },
2441 "containerName": "App"
2442 }]
2443 }),
2444 )
2445 .await
2446 .unwrap();
2447
2448 let symbols = client_task.await.unwrap();
2449 assert_eq!(symbols.len(), 1);
2450 assert_eq!(symbols[0].name, "run");
2451 assert_eq!(symbols[0].kind, SymbolKind::FUNCTION);
2452 assert_eq!(symbols[0].container_name.as_deref(), Some("App"));
2453 assert_eq!(symbols[0].uri.as_str(), "file:///workspace/src/main.rs");
2454 assert_eq!(symbols[0].range.unwrap().start.line, 4);
2455
2456 drop(client);
2457 connection_task.abort();
2458 let _ = connection_task.await;
2459 }
2460
2461 #[tokio::test]
2462 async fn cancels_an_lsp_request_when_its_future_is_dropped() {
2463 let (client_stream, server_stream) = duplex(8 * 1024);
2464 let (client_reader, client_writer) = split(client_stream);
2465 let (server_reader, _server_writer) = split(server_stream);
2466 let workspace_uri = Url::parse("file:///workspace").unwrap();
2467 let (client, _status_receiver, connection_task, _capabilities) = spawn_json_rpc(
2468 BufReader::new(client_reader),
2469 client_writer,
2470 workspace_uri,
2471 "workspace".to_owned(),
2472 );
2473 let mut server_reader = BufReader::new(server_reader);
2474
2475 let query_client = client.clone();
2476 let client_task = tokio::spawn(async move {
2477 let _: Option<WorkspaceSymbolResponse> = query_client
2478 .request(
2479 "workspace/symbol",
2480 WorkspaceSymbolParams {
2481 query: "first".to_owned(),
2482 ..WorkspaceSymbolParams::default()
2483 },
2484 )
2485 .await
2486 .unwrap();
2487 });
2488
2489 let request = read_message(&mut server_reader).await.unwrap();
2490 let request_id = response_id(&request).unwrap();
2491 client_task.abort();
2492 let _ = client_task.await;
2493
2494 let cancellation = timeout(Duration::from_secs(1), read_message(&mut server_reader))
2495 .await
2496 .expect("client did not send $/cancelRequest")
2497 .unwrap();
2498 assert_eq!(cancellation["method"], "$/cancelRequest");
2499 assert_eq!(cancellation["params"]["id"], request_id);
2500
2501 drop(client);
2502 connection_task.abort();
2503 let _ = connection_task.await;
2504 }
2505
2506 #[tokio::test]
2507 async fn integrates_with_installed_pyrefly() {
2508 if !external_server_available("pyrefly").await {
2509 eprintln!("skipping real Pyrefly integration test: pyrefly is not in PATH");
2510 return;
2511 }
2512
2513 let workspace = external_server_workspace("pyrefly");
2514 fs::write(workspace.join("pyrefly.toml"), "").unwrap();
2515 fs::write(
2516 workspace.join("main.py"),
2517 "class Worker:\n def run(self) -> None:\n helper()\n\n\ndef helper() -> None:\n pass\n\n\ndef main() -> None:\n Worker().run()\n",
2518 )
2519 .unwrap();
2520
2521 let lsp = timeout(
2522 Duration::from_secs(30),
2523 LspProvider::start(
2524 LspConfig::for_server("pyrefly", &workspace)
2525 .arg("--indexing-mode")
2526 .arg("lazy-blocking"),
2527 ),
2528 )
2529 .await
2530 .expect("Pyrefly initialization timed out")
2531 .unwrap();
2532 assert_eq!(
2533 lsp.server_info().map(|info| info.name.as_str()),
2534 Some("pyrefly-lsp")
2535 );
2536
2537 let helper = wait_for_workspace_symbol(&lsp, "helper").await;
2538 let position = helper.range.unwrap().start;
2539 let response = timeout(
2540 Duration::from_secs(30),
2541 lsp.hierarchy_client().query(HierarchyQuery {
2542 symbol: SymbolIdentity {
2543 symbol: helper.name,
2544 kind: HierarchyKind::Call,
2545 location: Some(SourceLocation {
2546 uri: helper.uri.to_string(),
2547 line: Some(position.line),
2548 character: Some(position.character),
2549 }),
2550 },
2551 direction: HierarchyDirection::Incoming,
2552 }),
2553 )
2554 .await
2555 .expect("Pyrefly call hierarchy timed out")
2556 .unwrap();
2557 assert!(
2558 response
2559 .children
2560 .iter()
2561 .any(|child| child.symbol == "Worker.run")
2562 );
2563
2564 lsp.shutdown().await.unwrap();
2565 fs::remove_dir_all(workspace).unwrap();
2566 }
2567
2568 #[tokio::test]
2569 async fn integrates_with_installed_rust_analyzer() {
2570 if !external_server_available("rust-analyzer").await {
2571 eprintln!("skipping real rust-analyzer integration test: rust-analyzer is not in PATH");
2572 return;
2573 }
2574
2575 let workspace = external_server_workspace("rust-analyzer");
2576 fs::create_dir(workspace.join("src")).unwrap();
2577 fs::write(
2578 workspace.join("Cargo.toml"),
2579 "[package]\nname = \"cgraph-ra-fixture\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
2580 )
2581 .unwrap();
2582 fs::write(
2583 workspace.join("src/main.rs"),
2584 "fn helper() {}\n\nfn main() {\n helper();\n}\n",
2585 )
2586 .unwrap();
2587
2588 let lsp = timeout(
2589 Duration::from_secs(60),
2590 LspProvider::start(LspConfig::for_server("rust-analyzer", &workspace)),
2591 )
2592 .await
2593 .expect("rust-analyzer initialization timed out")
2594 .unwrap();
2595 let main = wait_for_workspace_symbol(&lsp, "main").await;
2596 let position = main.range.unwrap().start;
2597 let query = HierarchyQuery {
2598 symbol: SymbolIdentity {
2599 symbol: main.name,
2600 kind: HierarchyKind::Call,
2601 location: Some(SourceLocation {
2602 uri: main.uri.to_string(),
2603 line: Some(position.line),
2604 character: Some(position.character),
2605 }),
2606 },
2607 direction: HierarchyDirection::Outgoing,
2608 };
2609 let response = timeout(Duration::from_secs(30), async {
2610 loop {
2611 match lsp.hierarchy_client().query(query.clone()).await {
2612 Ok(response) => break response,
2613 Err(error)
2614 if error
2615 .chain()
2616 .any(|cause| cause.to_string().contains("content modified")) =>
2617 {
2618 tokio::time::sleep(Duration::from_millis(100)).await;
2619 }
2620 Err(error) => panic!("rust-analyzer call hierarchy failed: {error:#}"),
2621 }
2622 }
2623 })
2624 .await
2625 .expect("rust-analyzer call hierarchy did not stabilize");
2626 assert!(
2627 response
2628 .children
2629 .iter()
2630 .any(|child| child.symbol == "helper")
2631 );
2632
2633 lsp.shutdown().await.unwrap();
2634 fs::remove_dir_all(workspace).unwrap();
2635 }
2636
2637 #[tokio::test]
2638 async fn integrates_with_installed_clangd_workspace_symbols() {
2639 if !external_server_available("clangd").await {
2640 eprintln!("skipping real clangd integration test: clangd is not in PATH");
2641 return;
2642 }
2643
2644 let workspace = external_server_workspace("clangd");
2645 let source = workspace.join("src/main.cpp");
2646 fs::create_dir(workspace.join("src")).unwrap();
2647 fs::write(
2648 workspace.join("compile_commands.json"),
2649 serde_json::to_vec(&vec![json!({
2650 "directory": workspace,
2651 "file": source,
2652 "arguments": [
2653 "clang++",
2654 "-std=c++17",
2655 "-Wall",
2656 "-c",
2657 source,
2658 ],
2659 })])
2660 .unwrap(),
2661 )
2662 .unwrap();
2663 fs::write(
2664 &source,
2665 "namespace demo {\nclass Worker { public: void run(); };\nvoid Worker::run() {}\n}\n\nvoid helper() {}\nint main() { helper(); return 0; }\n",
2666 )
2667 .unwrap();
2668
2669 let lsp = timeout(
2670 Duration::from_secs(60),
2671 LspProvider::start(LspConfig::for_server("clangd", &workspace)),
2672 )
2673 .await
2674 .expect("clangd initialization timed out")
2675 .unwrap();
2676
2677 let method = wait_for_workspace_symbol_leaf(&lsp, "run").await;
2678 assert_eq!(method.name, "demo::Worker::run");
2679 assert_eq!(method.kind, SymbolKind::METHOD);
2680 assert_eq!(method.uri, Url::from_file_path(&source).unwrap());
2681 assert!(method.range.is_some());
2682
2683 let main = wait_for_workspace_symbol(&lsp, "main").await;
2684 assert_eq!(main.name, "main");
2685 assert_eq!(main.kind, SymbolKind::FUNCTION);
2686 assert_eq!(main.uri, Url::from_file_path(&source).unwrap());
2687
2688 lsp.shutdown().await.unwrap();
2689 fs::remove_dir_all(workspace).unwrap();
2690 }
2691
2692 #[tokio::test]
2693 async fn rejects_oversized_messages() {
2694 use tokio::io::AsyncWriteExt;
2695
2696 let (mut client_stream, server_stream) = duplex(128);
2697 let _server_task = tokio::spawn(async move {
2698 client_stream
2699 .write_all(b"Content-Length: 16777217\r\n\r\n")
2700 .await
2701 .unwrap();
2702 });
2703
2704 let error = read_message(&mut BufReader::new(server_stream))
2705 .await
2706 .unwrap_err();
2707 assert!(error.to_string().contains("too large"));
2708 }
2709
2710 async fn external_server_available(program: &str) -> bool {
2711 tokio::process::Command::new(program)
2712 .arg("--version")
2713 .output()
2714 .await
2715 .is_ok_and(|output| output.status.success())
2716 }
2717
2718 fn external_server_workspace(name: &str) -> PathBuf {
2719 let unique = SystemTime::now()
2720 .duration_since(UNIX_EPOCH)
2721 .unwrap()
2722 .as_nanos();
2723 let workspace = std::env::temp_dir().join(format!("cgraph-{name}-{unique}"));
2724 fs::create_dir(&workspace).unwrap();
2725 workspace
2726 }
2727
2728 async fn wait_for_workspace_symbol(
2729 lsp: &LspProvider,
2730 expected_name: &str,
2731 ) -> WorkspaceSymbolMatch {
2732 timeout(Duration::from_secs(30), async {
2733 loop {
2734 if let Some(symbol) = lsp
2735 .workspace_symbols(expected_name)
2736 .await
2737 .unwrap()
2738 .into_iter()
2739 .find(|symbol| symbol.name == expected_name)
2740 {
2741 return symbol;
2742 }
2743 tokio::time::sleep(Duration::from_millis(100)).await;
2744 }
2745 })
2746 .await
2747 .unwrap_or_else(|_| panic!("{expected_name:?} did not appear in workspace symbols"))
2748 }
2749
2750 async fn wait_for_workspace_symbol_leaf(
2751 lsp: &LspProvider,
2752 expected_name: &str,
2753 ) -> WorkspaceSymbolMatch {
2754 timeout(Duration::from_secs(30), async {
2755 loop {
2756 if let Some(symbol) = lsp
2757 .workspace_symbols(expected_name)
2758 .await
2759 .unwrap()
2760 .into_iter()
2761 .find(|symbol| symbol_leaf_name(&symbol.name) == expected_name)
2762 {
2763 return symbol;
2764 }
2765 tokio::time::sleep(Duration::from_millis(100)).await;
2766 }
2767 })
2768 .await
2769 .unwrap_or_else(|_| panic!("{expected_name:?} did not appear in workspace symbols"))
2770 }
2771
2772 fn symbol(uri: &str) -> WorkspaceSymbolMatch {
2773 WorkspaceSymbolMatch {
2774 name: "symbol".to_owned(),
2775 kind: SymbolKind::FUNCTION,
2776 container_name: None,
2777 uri: Url::parse(uri).unwrap(),
2778 range: None,
2779 }
2780 }
2781
2782 fn call_item(name: &str, line: u32) -> Value {
2783 json!({
2784 "name": name,
2785 "kind": 12,
2786 "uri": "file:///workspace/src/main.rs",
2787 "range": {
2788 "start": { "line": line, "character": 0 },
2789 "end": { "line": line, "character": name.len() }
2790 },
2791 "selectionRange": {
2792 "start": { "line": line, "character": 0 },
2793 "end": { "line": line, "character": name.len() }
2794 }
2795 })
2796 }
2797
2798 fn rust_method_item(name: &str, line: u32) -> Value {
2799 let mut item = call_item(name, line);
2800 item["detail"] = json!(format!("pub fn {name}(&self)"));
2801 item
2802 }
2803
2804 fn external_call_item(name: &str, line: u32) -> Value {
2805 json!({
2806 "name": name,
2807 "kind": 12,
2808 "uri": "file:///usr/include/stdio.h",
2809 "range": {
2810 "start": { "line": line, "character": 0 },
2811 "end": { "line": line, "character": name.len() }
2812 },
2813 "selectionRange": {
2814 "start": { "line": line, "character": 0 },
2815 "end": { "line": line, "character": name.len() }
2816 }
2817 })
2818 }
2819
2820 fn type_item(name: &str, line: u32) -> Value {
2821 json!({
2822 "name": name,
2823 "kind": 23,
2824 "uri": "file:///workspace/src/main.rs",
2825 "range": {
2826 "start": { "line": line, "character": 0 },
2827 "end": { "line": line, "character": name.len() }
2828 },
2829 "selectionRange": {
2830 "start": { "line": line, "character": 0 },
2831 "end": { "line": line, "character": name.len() }
2832 }
2833 })
2834 }
2835}