Skip to main content

cgraph/fetch/
lsp.rs

1//! Minimal stdio LSP client used by the fetch layer.
2//!
3//! The transport is implemented locally because `tower-lsp` supplies protocol
4//! types and server infrastructure, but cgraph needs to act as an LSP client.
5
6mod 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
59// A corrupt Content-Length must not turn into an attacker-controlled allocation.
60const 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    /// Builds the command line expected by a supported language-server binary.
84    ///
85    /// Most servers enter LSP mode directly. Pyrefly exposes it as the
86    /// `pyrefly lsp` subcommand, so callers should use this constructor when
87    /// the configured value is a server executable rather than a raw command.
88    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)]
123/// A provider-level status event, kept separate from individual request results.
124///
125/// Language servers may run several work-done tasks concurrently. The JSON-RPC
126/// actor collapses those protocol tokens into the most useful current update;
127/// the TUI then maps this LSP-specific type into its backend-neutral status.
128pub 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        // rust-analyzer's call hierarchy exposes only a signature in `detail`.
457        // The map lock only creates a per-URI cell; the LSP round trip happens
458        // outside it, so different documents can resolve concurrently.
459        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        // The clone owns only an actor sender. Child process ownership stays in
750        // LspProvider, which keeps shutdown deterministic in main.
751        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            // Drop cannot await actor I/O. The unbounded control channel makes
883            // cancellation reliable even when the bounded request queue is full.
884            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    // The reader must run even while no user request is active: servers commonly
965    // send workspace/configuration immediately after initialization and may wait
966    // for its response before indexing. The actor is the sole stdin writer so
967    // concurrent client requests and server-request replies cannot interleave.
968    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}