From 819d2f7c7822c96e8f6e4714907dfcb3527168eb Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 12 Jan 2026 12:41:13 +0100 Subject: [PATCH] project_symbols: Support rust-analyzer path symbol search (#46511) cc https://github.com/rust-lang/rust-analyzer/pull/21415 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/languages/src/rust.rs | 26 ++++-- crates/project/src/lsp_store.rs | 82 +++++++++++-------- crates/project_symbols/src/project_symbols.rs | 33 +++++++- 3 files changed, 97 insertions(+), 44 deletions(-) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index f91679db850bb79001258213df629f51ab7c9d7c..d2b9d51fd21db58ca877d2a6516362776cb3126c 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -568,13 +568,14 @@ impl LspAdapter for RustLspAdapter { language: &Arc, ) -> Option { let (prefix, suffix) = match kind { - lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", " () {}"), - lsp::SymbolKind::STRUCT => ("struct ", " {}"), - lsp::SymbolKind::ENUM => ("enum ", " {}"), - lsp::SymbolKind::INTERFACE => ("trait ", " {}"), - lsp::SymbolKind::CONSTANT => ("const ", ": () = ();"), - lsp::SymbolKind::MODULE => ("mod ", " {}"), - lsp::SymbolKind::TYPE_PARAMETER => ("type ", " {}"), + lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", "();"), + lsp::SymbolKind::STRUCT => ("struct ", ";"), + lsp::SymbolKind::ENUM => ("enum ", "{}"), + lsp::SymbolKind::INTERFACE => ("trait ", "{}"), + lsp::SymbolKind::CONSTANT => ("const ", ":()=();"), + lsp::SymbolKind::MODULE => ("mod ", ";"), + lsp::SymbolKind::PACKAGE => ("extern crate ", ";"), + lsp::SymbolKind::TYPE_PARAMETER => ("type ", "=();"), _ => return None, }; @@ -1824,6 +1825,17 @@ mod tests { vec![(0..4, highlight_keyword), (5..10, highlight_type)], )) ); + + assert_eq!( + adapter + .label_for_symbol("zed", lsp::SymbolKind::PACKAGE, &language) + .await, + Some(CodeLabel::new( + "extern crate zed".to_string(), + 13..16, + vec![(0..6, highlight_keyword), (7..12, highlight_keyword),], + )) + ); } #[gpui::test] diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 00d26652c2b0d9ef5aaab1e70f6c519365588537..0a5407a7b9bcda80da13a08a7d40a75454f53e0f 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -7792,46 +7792,62 @@ impl LspStore { let worktree_handle = worktree_handle.clone(); let server_id = server.server_id(); requests.push( - server - .request::( - lsp::WorkspaceSymbolParams { - query: query.to_string(), - ..Default::default() - }, - ) - .map(move |response| { - let lsp_symbols = response.into_response() - .context("workspace symbols request") - .log_err() - .flatten() - .map(|symbol_response| match symbol_response { - lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { - flat_responses.into_iter().map(|lsp_symbol| { - (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location) - }).collect::>() - } - lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { - nested_responses.into_iter().filter_map(|lsp_symbol| { + server + .request::( + lsp::WorkspaceSymbolParams { + query: query.to_string(), + ..Default::default() + }, + ) + .map(move |response| { + let lsp_symbols = response + .into_response() + .context("workspace symbols request") + .log_err() + .flatten() + .map(|symbol_response| match symbol_response { + lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { + flat_responses + .into_iter() + .map(|lsp_symbol| { + ( + lsp_symbol.name, + lsp_symbol.kind, + lsp_symbol.location, + ) + }) + .collect::>() + } + lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { + nested_responses + .into_iter() + .filter_map(|lsp_symbol| { let location = match lsp_symbol.location { OneOf::Left(location) => location, OneOf::Right(_) => { - log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); - return None + log::error!( + "Unexpected: client capabilities \ + forbid symbol resolutions in \ + workspace.symbol.resolveSupport" + ); + return None; } }; Some((lsp_symbol.name, lsp_symbol.kind, location)) - }).collect::>() - } - }).unwrap_or_default(); + }) + .collect::>() + } + }) + .unwrap_or_default(); - WorkspaceSymbolsResult { - server_id, - lsp_adapter, - worktree: worktree_handle.downgrade(), - lsp_symbols, - } - }), - ); + WorkspaceSymbolsResult { + server_id, + lsp_adapter, + worktree: worktree_handle.downgrade(), + lsp_symbols, + } + }), + ); } cx.spawn(async move |this, cx| { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 8a97f66406c48082bdba2ed072d50d31ee2c9cdf..2031f22ad8a7e655e040b6672ac9b05fd60dc2c8 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -178,7 +178,15 @@ impl PickerDelegate for ProjectSymbolsDelegate { window: &mut Window, cx: &mut Context>, ) -> Task<()> { - self.filter(&query, window, cx); + // Try to support rust-analyzer's path based symbols feature which + // allows to search by rust path syntax, in that case we only want to + // filter names by the last segment + // Ideally this was a first class LSP feature (rich queries) + let query_filter = query + .rsplit_once("::") + .map_or(&*query, |(_, suffix)| suffix) + .to_owned(); + self.filter(&query_filter, window, cx); self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1; let symbols = self .project @@ -208,7 +216,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { delegate.visible_match_candidates = visible_match_candidates; delegate.external_match_candidates = external_match_candidates; delegate.symbols = symbols; - delegate.filter(&query, window, cx); + delegate.filter(&query_filter, window, cx); }) .log_err(); } @@ -354,17 +362,24 @@ mod tests { let executor = cx.background_executor().clone(); let fake_symbols = fake_symbols.clone(); async move { + let (query, prefixed) = match params.query.strip_prefix("dir::") { + Some(query) => (query, true), + None => (&*params.query, false), + }; let candidates = fake_symbols .iter() .enumerate() + .filter(|(_, symbol)| { + !prefixed || symbol.location.uri.path().contains("dir") + }) .map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.name)) .collect::>(); - let matches = if params.query.is_empty() { + let matches = if query.is_empty() { Vec::new() } else { fuzzy::match_strings( &candidates, - ¶ms.query, + &query, true, true, 100, @@ -434,6 +449,16 @@ mod tests { symbols.read_with(cx, |symbols, _| { assert_eq!(symbols.delegate.matches.len(), 0); }); + + // Check that rust-analyzer path style symbols work + symbols.update_in(cx, |p, window, cx| { + p.update_matches("dir::to".to_string(), window, cx); + }); + + cx.run_until_parked(); + symbols.read_with(cx, |symbols, _| { + assert_eq!(symbols.delegate.matches.len(), 1); + }); } fn init_test(cx: &mut TestAppContext) {