use lsp_types::{
DocumentSymbol, DocumentSymbolParams, PartialResultParams, TextDocumentIdentifier,
WorkDoneProgressParams, WorkspaceSymbolParams as LspWorkspaceSymbolParams,
};
use super::Translator;
use super::dto::{
DocumentSymbolsResult, Location, Symbol, WorkspaceSymbol, WorkspaceSymbolResult,
lsp_kind_to_u32,
};
use super::encoding_ctx::EncodingCtx;
use super::kind_filter::SymbolKindFilter;
use super::navigation::MAX_NORMALIZED_LOCATIONS;
use super::routing::{
Capability, FileLocalCapability, IndexingGate, PreparedDocument, WorkspaceRouteLookup,
lookup_workspace_route,
};
use crate::bridge::ClientPath;
use crate::config::ToolKind;
use crate::error::{Error, Result};
use crate::util::lock_std;
fn validate_query_length(query: &str) -> Result<()> {
const MAX_QUERY_LENGTH: usize = 1000;
if query.len() > MAX_QUERY_LENGTH {
return Err(Error::InvalidToolParams(format!(
"Query too long: {} bytes (max {MAX_QUERY_LENGTH})",
query.len()
)));
}
Ok(())
}
pub(super) struct FetchedSymbols {
pub(super) doc: PreparedDocument,
pub(super) ctx: EncodingCtx,
pub(super) response: Option<lsp_types::DocumentSymbolResponse>,
}
fn convert_document_symbol<'a>(
symbol: DocumentSymbol,
ctx: &'a EncodingCtx,
uri: &'a lsp_types::Uri,
) -> futures::future::BoxFuture<'a, Symbol> {
Box::pin(async move {
let range = ctx.normalize_range(uri, symbol.range).await;
let selection_range = ctx.normalize_range(uri, symbol.selection_range).await;
let children = match symbol.children {
Some(children) => {
let mut result = Vec::with_capacity(children.len());
for child in children {
result.push(convert_document_symbol(child, ctx, uri).await);
}
Some(result)
}
None => None,
};
Symbol {
name: symbol.name,
kind: lsp_kind_to_u32(symbol.kind),
range,
selection_range,
children,
}
})
}
#[derive(Debug)]
pub(super) enum DocumentSymbolTree {
Hierarchical(Vec<Symbol>),
Flat(Vec<FlatSymbol>),
}
impl DocumentSymbolTree {
fn into_symbols(self) -> Vec<Symbol> {
match self {
Self::Hierarchical(symbols) => symbols,
Self::Flat(symbols) => symbols.into_iter().map(|flat| flat.symbol).collect(),
}
}
}
#[derive(Debug)]
pub(super) struct FlatSymbol {
pub(super) symbol: Symbol,
pub(super) container_name: Option<String>,
}
pub(super) async fn symbol_tree(
response: Option<lsp_types::DocumentSymbolResponse>,
ctx: &EncodingCtx,
uri: &lsp_types::Uri,
) -> DocumentSymbolTree {
match response {
Some(lsp_types::DocumentSymbolResponse::SymbolInformationList(symbols)) => {
let mut result = Vec::with_capacity(symbols.len());
for sym in symbols {
let range = ctx.normalize_range(uri, sym.location.range).await;
let selection_range = range.clone();
result.push(FlatSymbol {
symbol: Symbol {
name: sym.base_symbol_information.name,
kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
range,
selection_range,
children: None,
},
container_name: sym.base_symbol_information.container_name,
});
}
DocumentSymbolTree::Flat(result)
}
Some(lsp_types::DocumentSymbolResponse::DocumentSymbolList(symbols)) => {
let mut result = Vec::with_capacity(symbols.len());
for sym in symbols {
result.push(convert_document_symbol(sym, ctx, uri).await);
}
DocumentSymbolTree::Hierarchical(result)
}
None => DocumentSymbolTree::Hierarchical(vec![]),
}
}
impl Translator {
pub async fn handle_document_symbols(
&self,
file_path: ClientPath,
) -> Result<DocumentSymbolsResult> {
let FetchedSymbols { doc, ctx, response } =
self.request_document_symbols(&file_path).await?;
let tree = symbol_tree(response, &ctx, doc.uri()).await;
Ok(DocumentSymbolsResult {
symbols: tree.into_symbols(),
positions_degraded: ctx.positions_degraded(),
})
}
pub(super) async fn request_document_symbols(
&self,
file_path: &ClientPath,
) -> Result<FetchedSymbols> {
let doc = self
.prepare_gated_document(
file_path,
IndexingGate::FileLocal(FileLocalCapability::DocumentSymbols),
)
.await?;
self.fetch_document_symbols(doc).await
}
pub(super) async fn fetch_document_symbols(
&self,
doc: PreparedDocument,
) -> Result<FetchedSymbols> {
let ctx = self.encoding_ctx(doc.server_id());
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier {
uri: doc.uri().clone(),
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let client = doc.client();
let response = client
.request_typed::<lsp_types::DocumentSymbolRequest>(params, client.request_timeout())
.await?;
Ok(FetchedSymbols { doc, ctx, response })
}
#[expect(
clippy::too_many_lines,
reason = "linear request/response flow; splitting would scatter the steps"
)]
pub async fn handle_workspace_symbol(
&self,
query: String,
kind_filter: Option<SymbolKindFilter>,
limit: u32,
) -> Result<WorkspaceSymbolResult> {
validate_query_length(&query)?;
let kind_filter = kind_filter.map(|filter| u32::from(filter.kind()));
let router = self.router_snapshot();
let lookup = {
let servers = lock_std(&self.servers);
lookup_workspace_route(
|| router.resolve_any(ToolKind::WorkspaceSymbols).cloned(),
|id| servers.client(id).is_some(),
|id| servers.is_expected(id),
|| !servers.any_expected(),
)
};
let server_id = match lookup {
WorkspaceRouteLookup::Registered(id) => id,
WorkspaceRouteLookup::Initializing(server_id) => {
return Err(Error::ServerInitializing { server_id });
}
WorkspaceRouteLookup::AllInitializing => {
return Err(Error::WorkspaceServersInitializing);
}
WorkspaceRouteLookup::Dangling(_) => return Err(Error::NoServerConfigured),
WorkspaceRouteLookup::NothingConfigured => {
let failures = self.startup_failures();
return Err(if failures.is_empty() {
Error::NoServerConfigured
} else {
Error::AllServersFailedToInit { failures }
});
}
WorkspaceRouteLookup::NoClaimant => {
return Err(self
.configured_router
.resolve_any(ToolKind::WorkspaceSymbols)
.ok()
.and_then(|id| self.startup_failure(id))
.map_or(
Error::NoServerForWorkspaceTool {
tool: ToolKind::WorkspaceSymbols,
},
|failure| Error::ServerFailedToStart(Box::new(failure)),
));
}
};
self.respawn_if_dead(&server_id).await?;
let (client, expected) = {
let servers = lock_std(&self.servers);
(servers.client(&server_id), servers.is_expected(&server_id))
};
let client = client.ok_or_else(|| {
if expected {
Error::ServerInitializing {
server_id: server_id.clone(),
}
} else {
Error::NoServerConfigured
}
})?;
self.require_capability(&server_id, Capability::WorkspaceSymbols)?;
let params = LspWorkspaceSymbolParams {
query,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let response = client
.request_typed::<lsp_types::WorkspaceSymbolRequest>(params, client.request_timeout())
.await?;
let ctx = self.encoding_ctx(&server_id);
let mut raw_symbols: Vec<RawWorkspaceSymbol> = Vec::new();
match response {
Some(lsp_types::WorkspaceSymbolResponse::SymbolInformationList(list)) => {
for sym in list {
raw_symbols.push(RawWorkspaceSymbol {
name: sym.base_symbol_information.name,
kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
container_name: sym.base_symbol_information.container_name,
out_of_workspace: ctx.is_out_of_workspace(&sym.location.uri),
uri: sym.location.uri,
range: sym.location.range,
});
}
}
Some(lsp_types::WorkspaceSymbolResponse::WorkspaceSymbolList(list)) => {
for sym in list {
let (uri, range) = match sym.location {
lsp_types::WorkspaceSymbolLocation::Location(loc) => (loc.uri, loc.range),
lsp_types::WorkspaceSymbolLocation::LocationUriOnly(_) => continue,
};
raw_symbols.push(RawWorkspaceSymbol {
name: sym.base_symbol_information.name,
kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
container_name: sym.base_symbol_information.container_name,
out_of_workspace: ctx.is_out_of_workspace(&uri),
uri,
range,
});
}
}
None => {}
}
if let Some(target) = kind_filter {
raw_symbols.retain(|s| s.kind == target);
}
let effective_limit = (limit as usize).min(MAX_NORMALIZED_LOCATIONS);
let truncated = raw_symbols.len() > effective_limit;
raw_symbols.truncate(effective_limit);
let mut symbols = Vec::with_capacity(raw_symbols.len());
for raw in raw_symbols {
let range = ctx.normalize_range(&raw.uri, raw.range).await;
symbols.push(WorkspaceSymbol {
name: raw.name,
kind: raw.kind,
location: Location {
uri: raw.uri.to_string(),
range,
out_of_workspace: raw.out_of_workspace,
},
container_name: raw.container_name,
});
}
Ok(WorkspaceSymbolResult {
symbols,
truncated,
positions_degraded: ctx.positions_degraded(),
})
}
}
struct RawWorkspaceSymbol {
name: String,
kind: u32,
container_name: Option<String>,
out_of_workspace: bool,
uri: lsp_types::Uri,
range: lsp_types::Range,
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use std::{assert_matches, fs};
use tempfile::TempDir;
use tokio::io::BufReader;
use tokio::time::timeout;
use url::Url;
use super::*;
use crate::bridge::translator::dto::PositionDegradation;
use crate::bridge::translator::testing::*;
use crate::config::{LanguageId, ServerCommand, ServerId, TimeoutSecs, ToolRouter, ToolSet};
use crate::test_lsp::client_path;
#[test]
fn test_symbol_kind_debug_rendering_is_pinned() {
assert_eq!(
format!("{:?}", lsp_types::SymbolKind::EnumMember),
"EnumMember"
);
}
#[tokio::test]
async fn test_convert_document_symbol_kind_is_numeric() {
let symbol = DocumentSymbol {
name: "my_enum_member".to_string(),
detail: None,
kind: lsp_types::SymbolKind::EnumMember,
tags: None,
#[allow(
deprecated,
reason = "`deprecated` is a deprecated field of the LSP struct literal"
)]
deprecated: None,
range: lsp_types::Range {
start: lsp_types::Position {
line: 0,
character: 0,
},
end: lsp_types::Position {
line: 0,
character: 5,
},
},
selection_range: lsp_types::Range {
start: lsp_types::Position {
line: 0,
character: 0,
},
end: lsp_types::Position {
line: 0,
character: 5,
},
},
children: None,
};
let ctx = test_ctx();
let uri = lsp_types::Uri::from("file:///tmp/test.rs");
let result = convert_document_symbol(symbol, &ctx, &uri).await;
assert_eq!(result.kind, 22u32);
}
#[tokio::test]
async fn test_handle_workspace_symbol_no_server() {
let translator = Translator::new();
let result = translator
.handle_workspace_symbol("test".to_string(), None, 100)
.await;
assert_matches!(result, Err(Error::NoServerConfigured));
}
#[tokio::test]
async fn test_handle_workspace_symbol_reports_initializing_when_expected_but_not_registered() {
let translator = Translator::new();
translator.set_expected_servers(HashSet::from([ServerId::from_static("pyright")]));
let result = translator
.handle_workspace_symbol("test".to_string(), None, 100)
.await;
assert_matches!(result, Err(Error::WorkspaceServersInitializing));
}
#[tokio::test]
async fn test_handle_workspace_symbol_reports_startup_failures() {
let translator = Translator::new();
translator.record_startup_failures(&[crate::error::ServerSpawnFailure {
server_id: ServerId::from_static("pyright"),
language_id: LanguageId::from_static("python"),
command: ServerCommand::from_static("pyright-langserver"),
reason: crate::error::StartupFailure::InitTaskPanicked,
}]);
let result = translator
.handle_workspace_symbol("test".to_string(), None, 100)
.await;
let Err(Error::AllServersFailedToInit { failures }) = result else {
panic!("expected AllServersFailedToInit, got {result:?}");
};
assert_eq!(failures.len(), 1);
assert_eq!(failures[0].server_id, ServerId::from_static("pyright"));
}
#[tokio::test]
async fn test_handle_workspace_symbol_reports_failed_claimant() {
let mut live = crate::config::LspServerConfig::pyright();
live.name = Some(ServerId::from_static("live"));
live.handles = Some(ToolSet::new(vec![ToolKind::Hover]).unwrap());
let mut failing = crate::config::LspServerConfig::rust_analyzer();
failing.name = Some(ServerId::from_static("failing"));
failing.handles = Some(ToolSet::new(vec![ToolKind::WorkspaceSymbols]).unwrap());
let router = ToolRouter::from_configs([&live, &failing]).unwrap();
let failing_id = ServerId::from_static("failing");
let translator = Translator::new().with_router(router);
translator.record_startup_failures(&[crate::error::ServerSpawnFailure {
server_id: failing_id.clone(),
language_id: LanguageId::from_static("rust"),
command: ServerCommand::from_static("rust-analyzer"),
reason: crate::error::StartupFailure::InitTaskPanicked,
}]);
translator.rebind_router(&HashSet::from([ServerId::from_static("live")]));
translator.clear_expected_servers();
let result = translator
.handle_workspace_symbol("test".to_string(), None, 100)
.await;
assert_matches!(&result, Err(Error::ServerFailedToStart(f)) if f.server_id == failing_id,
"got {result:?}"
);
}
#[tokio::test]
async fn test_handle_workspace_symbol_initializing_wins_over_recorded_failure() {
let translator = Translator::new();
translator.set_expected_servers(HashSet::from([ServerId::from_static("pyright")]));
translator.record_startup_failures(&[crate::error::ServerSpawnFailure {
server_id: ServerId::from_static("other"),
language_id: LanguageId::from_static("go"),
command: ServerCommand::from_static("gopls"),
reason: crate::error::StartupFailure::InitTaskPanicked,
}]);
let result = translator
.handle_workspace_symbol("test".to_string(), None, 100)
.await;
assert_matches!(result, Err(Error::WorkspaceServersInitializing));
}
#[tokio::test]
async fn test_handle_workspace_symbol_no_claimant_names_tool() {
let configs = vec![crate::config::LspServerConfig {
language_id: LanguageId::from_static("python"),
command: ServerCommand::from_static("pyright-langserver").into(),
args: vec![],
env: crate::config::ServerEnv::default(),
file_patterns: vec![],
initialization_options: None,
settings: None,
timeout_seconds: TimeoutSecs::new(30).unwrap(),
request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
heuristics: None,
name: Some(ServerId::from_static("pyright")),
handles: Some(ToolSet::new(vec![ToolKind::Hover]).unwrap()),
indexing: crate::bridge::IndexingPolicy::Auto,
}];
let router = ToolRouter::from_configs(&configs).unwrap();
let translator = Translator::new().with_router(router);
let result = translator
.handle_workspace_symbol("test".to_string(), None, 100)
.await;
assert_matches!(
result,
Err(Error::NoServerForWorkspaceTool {
tool: ToolKind::WorkspaceSymbols
})
);
}
#[tokio::test]
async fn test_handle_document_symbols_flat_response_selection_range_matches_range() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let (translator, mut server) = translator_with_capabilities(
&dir,
&server_id,
lsp_types::ServerCapabilities {
document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
..Default::default()
},
);
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}\n").unwrap();
let path_str = path.to_string_lossy().to_string();
let uri = Url::from_file_path(&path).unwrap().to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
tokio::spawn(async move {
translator
.handle_document_symbols(client_path(path_str))
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let symbol_request = read_framed_message(&mut wire).await;
assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
write_response(
&mut server.read_half_stdin,
&symbol_request["id"],
serde_json::json!([{
"name": "main",
"kind": 12,
"location": {
"uri": uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 12},
},
},
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.expect("flat document symbol response should succeed");
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].range, result.symbols[0].selection_range);
}
#[tokio::test]
async fn test_handle_document_symbols_flat_response_normalizes_against_response_uri() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let (translator, mut server) = translator_with_capabilities_and_encoding(
&dir,
&server_id,
lsp_types::ServerCapabilities {
document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
..Default::default()
},
lsp_types::PositionEncodingKind::UTF8,
);
let path = dir.path().join("main.rs");
fs::write(&path, "aöb").unwrap();
let path_str = path.to_string_lossy().to_string();
let outside_uri = "file:///outside/workspace/does-not-exist.rs";
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
tokio::spawn(async move {
translator
.handle_document_symbols(client_path(path_str))
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let symbol_request = read_framed_message(&mut wire).await;
assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
write_response(
&mut server.read_half_stdin,
&symbol_request["id"],
serde_json::json!([{
"name": "sym",
"kind": 12,
"location": {
"uri": outside_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 3},
},
},
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.expect("flat document symbol response should succeed");
assert_eq!(result.symbols.len(), 1);
assert_eq!(
result.symbols[0].range.end.character, 3,
"must convert against the queried document's own content (\"aöb\"), not fail to \
read the entry's own (nonexistent, out-of-workspace) location.uri and fall back to \
the raw byte offset"
);
}
#[tokio::test]
async fn test_handle_document_symbols_sets_positions_degraded_for_unresolvable_line() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let (translator, mut server) = translator_with_capabilities_and_encoding(
&dir,
&server_id,
lsp_types::ServerCapabilities {
document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
..Default::default()
},
lsp_types::PositionEncodingKind::UTF8,
);
let path = dir.path().join("main.rs");
fs::write(&path, "aöb").unwrap();
let path_str = path.to_string_lossy().to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
tokio::spawn(async move {
translator
.handle_document_symbols(client_path(path_str))
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let symbol_request = read_framed_message(&mut wire).await;
assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
write_response(
&mut server.read_half_stdin,
&symbol_request["id"],
serde_json::json!([{
"name": "sym",
"kind": 12,
"range": {
"start": {"line": 5, "character": 0},
"end": {"line": 5, "character": 1},
},
"selectionRange": {
"start": {"line": 5, "character": 0},
"end": {"line": 5, "character": 1},
},
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.expect("document symbol response should still succeed, just degraded");
assert_eq!(result.symbols.len(), 1);
assert_eq!(
result.positions_degraded,
Some(PositionDegradation::Response),
"a range whose line can't be resolved must mark the result degraded"
);
}
#[tokio::test]
async fn test_handle_workspace_symbol_drops_location_uri_only_entries() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let a_uri = Url::from_file_path(dir.path().join("a.rs"))
.unwrap()
.to_string();
let b_uri = Url::from_file_path(dir.path().join("b.rs"))
.unwrap()
.to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
tokio::spawn(async move {
translator
.handle_workspace_symbol("foo".to_string(), None, 100)
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "workspace/symbol");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([
{
"name": "with_range",
"kind": 12,
"location": {
"uri": a_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 5}
}
}
},
{
"name": "without_range",
"kind": 12,
"location": { "uri": b_uri }
}
]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.unwrap();
assert_eq!(
result.symbols.len(),
1,
"the range-less LocationUriOnly symbol must be dropped, not fabricated"
);
assert_eq!(result.symbols[0].name, "with_range");
}
#[tokio::test]
async fn test_handle_workspace_symbol_does_not_filter_out_of_workspace_location() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let inside_uri = Url::from_file_path(canonical_dir(&dir).join("inside.rs"))
.unwrap()
.to_string();
let outside_uri = "file:///outside/workspace/evil.rs";
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
tokio::spawn(async move {
translator
.handle_workspace_symbol("foo".to_string(), None, 100)
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "workspace/symbol");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([
{
"name": "inside",
"kind": 12,
"location": {
"uri": inside_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 5}
}
}
},
{
"name": "outside",
"kind": 12,
"location": {
"uri": outside_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 4}
}
}
}
]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.unwrap();
assert_eq!(
result.symbols.len(),
2,
"both the in-workspace and out-of-workspace symbols must be returned"
);
assert!(result.symbols.iter().any(|s| s.name == "inside"));
assert!(result.symbols.iter().any(|s| s.name == "outside"));
assert!(
!result
.symbols
.iter()
.find(|s| s.name == "inside")
.unwrap()
.location
.out_of_workspace,
"an in-workspace symbol location must not be marked out_of_workspace"
);
assert!(
result
.symbols
.iter()
.find(|s| s.name == "outside")
.unwrap()
.location
.out_of_workspace,
"an out-of-workspace symbol location must be marked out_of_workspace"
);
}
#[tokio::test]
async fn test_handle_workspace_symbol_clamps_limit_to_max_normalized_locations() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let uri = Url::from_file_path(dir.path().join("many.rs"))
.unwrap()
.to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
tokio::spawn(async move {
translator
.handle_workspace_symbol("foo".to_string(), None, u32::MAX)
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "workspace/symbol");
let symbols: Vec<serde_json::Value> = (0..MAX_NORMALIZED_LOCATIONS + 500)
.map(|i| {
serde_json::json!({
"name": format!("sym{i}"),
"kind": 12,
"location": {
"uri": uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 3}
}
}
})
})
.collect();
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!(symbols),
)
.await;
let result = timeout(Duration::from_secs(5), handle)
.await
.expect("handler call should not hang")
.unwrap()
.unwrap();
assert_eq!(
result.symbols.len(),
MAX_NORMALIZED_LOCATIONS,
"limit: u32::MAX must be clamped to MAX_NORMALIZED_LOCATIONS, not left unbounded"
);
assert!(
result.truncated,
"a limit clamped below what the caller asked for must set truncated: true"
);
}
#[tokio::test]
async fn test_handle_document_symbols_dispatches_while_indexing_loading() {
use crate::bridge::NotificationCache;
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let cache = Arc::new(tokio::sync::Mutex::new(NotificationCache::new()));
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": false})),
);
let translator = Arc::new(translator.with_notification_cache(cache));
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().to_string();
tokio::spawn(async move { translator.handle_document_symbols(client_path(path)).await })
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "textDocument/documentSymbol");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang -- document_symbols must not be gated")
.unwrap()
.unwrap();
assert!(result.symbols.is_empty());
}
}