use lsp_types::{
DocumentSymbol, DocumentSymbolParams, PartialResultParams, TextDocumentIdentifier,
WorkDoneProgressParams, WorkspaceSymbolParams as LspWorkspaceSymbolParams,
};
use super::Translator;
use super::dto::{DocumentSymbolsResult, Location, Symbol, WorkspaceSymbol, WorkspaceSymbolResult};
use super::encoding_ctx::EncodingCtx;
use crate::bridge::lock_std;
use crate::config::{NoServerReason, ToolKind};
use crate::error::{Error, Result};
fn validate_workspace_symbol_params(query: &str, kind_filter: Option<&str>) -> Result<()> {
const MAX_QUERY_LENGTH: usize = 1000;
const VALID_SYMBOL_KINDS: &[&str] = &[
"File",
"Module",
"Namespace",
"Package",
"Class",
"Method",
"Property",
"Field",
"Constructor",
"Enum",
"Interface",
"Function",
"Variable",
"Constant",
"String",
"Number",
"Boolean",
"Array",
"Object",
"Key",
"Null",
"EnumMember",
"Struct",
"Event",
"Operator",
"TypeParameter",
];
if query.len() > MAX_QUERY_LENGTH {
return Err(Error::InvalidToolParams(format!(
"Query too long: {} bytes (max {MAX_QUERY_LENGTH})",
query.len()
)));
}
if let Some(kind) = kind_filter
&& !VALID_SYMBOL_KINDS
.iter()
.any(|k| k.eq_ignore_ascii_case(kind))
{
return Err(Error::InvalidToolParams(format!(
"Invalid kind_filter: '{kind}'. Valid values: {VALID_SYMBOL_KINDS:?}"
)));
}
Ok(())
}
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: format!("{:?}", symbol.kind),
range,
selection_range,
children,
}
})
}
impl Translator {
pub async fn handle_document_symbols(
&self,
file_path: String,
) -> Result<DocumentSymbolsResult> {
let (server_id, client, uri) = self
.prepare_gated_document(
&file_path,
ToolKind::DocumentSymbols,
"documentSymbolProvider",
|caps| {
matches!(
caps.document_symbol_provider,
Some(lsp_types::OneOf::Left(true) | lsp_types::OneOf::Right(_))
)
},
)
.await?;
let ctx = self.encoding_ctx(&server_id);
let response_uri = uri.clone();
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let response: Option<lsp_types::DocumentSymbolResponse> = client
.request(
"textDocument/documentSymbol",
params,
client.request_timeout(),
)
.await?;
let symbols = match response {
Some(lsp_types::DocumentSymbolResponse::Flat(symbols)) => {
let mut result = Vec::with_capacity(symbols.len());
for sym in symbols {
let range = ctx
.normalize_range(&sym.location.uri, sym.location.range)
.await;
let selection_range = ctx
.normalize_range(&sym.location.uri, sym.location.range)
.await;
result.push(Symbol {
name: sym.name,
kind: format!("{:?}", sym.kind),
range,
selection_range,
children: None,
});
}
result
}
Some(lsp_types::DocumentSymbolResponse::Nested(symbols)) => {
let mut result = Vec::with_capacity(symbols.len());
for sym in symbols {
result.push(convert_document_symbol(sym, &ctx, &response_uri).await);
}
result
}
None => vec![],
};
Ok(DocumentSymbolsResult { symbols })
}
pub async fn handle_workspace_symbol(
&self,
query: String,
kind_filter: Option<String>,
limit: u32,
) -> Result<WorkspaceSymbolResult> {
validate_workspace_symbol_params(&query, kind_filter.as_deref())?;
let server_id = lock_std(&self.router)
.resolve_any(ToolKind::WorkspaceSymbols)
.cloned()
.map_err(|reason| match reason {
NoServerReason::NothingRegistered => {
if lock_std(&self.expected_servers).is_empty() {
Error::NoServerConfigured
} else {
Error::WorkspaceServersInitializing
}
}
NoServerReason::NoClaimant => Error::NoServerForWorkspaceTool {
tool: ToolKind::WorkspaceSymbols,
},
})?;
self.respawn_if_dead(&server_id).await?;
let client = lock_std(&self.lsp_clients).get(&server_id).cloned();
let client = client.ok_or_else(|| {
if lock_std(&self.expected_servers).contains(&server_id) {
Error::ServerInitializing {
server_id: server_id.clone(),
}
} else {
Error::NoServerConfigured
}
})?;
self.require_capability(&server_id, "workspaceSymbolProvider", |caps| {
matches!(
caps.workspace_symbol_provider,
Some(lsp_types::OneOf::Left(true) | lsp_types::OneOf::Right(_))
)
})?;
let params = LspWorkspaceSymbolParams {
query,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let response: Option<Vec<lsp_types::SymbolInformation>> = client
.request("workspace/symbol", params, client.request_timeout())
.await?;
let ctx = self.encoding_ctx(&server_id);
let mut symbols: Vec<WorkspaceSymbol> = Vec::new();
for sym in response.unwrap_or_default() {
let range = ctx
.normalize_range(&sym.location.uri, sym.location.range)
.await;
symbols.push(WorkspaceSymbol {
name: sym.name,
kind: format!("{:?}", sym.kind),
location: Location {
uri: sym.location.uri.to_string(),
range,
},
container_name: sym.container_name,
});
}
if let Some(kind) = kind_filter {
symbols.retain(|s| s.kind.eq_ignore_ascii_case(&kind));
}
symbols.truncate(limit as usize);
Ok(WorkspaceSymbolResult { symbols })
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use std::collections::{HashMap, HashSet};
use super::*;
use crate::config::{ServerId, ToolRouter};
#[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("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_no_claimant_names_tool() {
let configs = vec![crate::config::LspServerConfig {
language_id: "python".to_string(),
command: "pyright-langserver".to_string(),
args: vec![],
env: HashMap::new(),
file_patterns: vec![],
initialization_options: None,
timeout_seconds: 30,
request_timeout_seconds: 30,
heuristics: None,
name: Some("pyright".to_string()),
handles: Some(vec![ToolKind::Hover]),
}];
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
})
));
}
}