use cpp_code_analysis::Session;
use lsp_types::{
ClientCapabilities, Location, OneOf, ServerCapabilities, WorkspaceSymbol,
WorkspaceSymbolParams, WorkspaceSymbolResponse,
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::{path_to_uri, position_in_file, symbol_kind};
const SYMBOL_LIMIT: usize = 512;
pub async fn on_workspace_symbol(
context: ServerContextSnapshot,
params: WorkspaceSymbolParams,
cancel_token: CancellationToken,
) -> RequestOutcome<WorkspaceSymbolResponse> {
let query = params.query;
snapshot_query(context.analysis(), cancel_token, move |session| {
symbols(session, &query).map(WorkspaceSymbolResponse::Nested)
})
.await
}
pub fn symbols<F: cpp_code_analysis::FileProvider + Clone>(
session: &Session<F>,
query: &str,
) -> Option<Vec<WorkspaceSymbol>> {
if session.pending() > 0 {
log::info!(
"a symbol search while {} file(s) are still queued: declarations in those files are not in this answer",
session.pending()
);
}
let mut found = session.index().symbols_matching(query, SYMBOL_LIMIT + 1);
if found.len() > SYMBOL_LIMIT {
found.truncate(SYMBOL_LIMIT);
log::info!(
"`{query}` matches more than {SYMBOL_LIMIT} symbols; the client was given the first ones"
);
}
let mut symbols = Vec::new();
for symbol in found {
let Some(held) = session.files().held(&symbol.file) else {
log::warn!(
"no symbol search: {} is not held, so its symbols cannot be placed",
symbol.file.display()
);
return None;
};
let (Some(uri), Some(start), Some(end)) = (
path_to_uri(&symbol.file),
position_in_file(held, symbol.fact.name_range.start_offset),
position_in_file(held, symbol.fact.name_range.end_offset()),
) else {
return None;
};
symbols.push(WorkspaceSymbol {
name: symbol.fact.name.clone(),
kind: symbol_kind(symbol.fact.kind),
tags: None,
container_name: symbol.fact.scope.clone(),
location: OneOf::Left(Location {
uri,
range: lsp_types::Range::new(start, end),
}),
data: None,
});
}
Some(symbols)
}
pub struct WorkspaceSymbolCapabilities;
impl RegisterCapabilities for WorkspaceSymbolCapabilities {
fn register_capabilities(server_capabilities: &mut ServerCapabilities, _: &ClientCapabilities) {
server_capabilities.workspace_symbol_provider = Some(OneOf::Left(true));
}
}
#[cfg(test)]
mod tests {
use super::symbols;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use std::path::PathBuf;
const WIDGET_H: &str = "namespace ns {\nstruct Widget { int size; };\n}\n";
#[test]
fn a_symbol_search_is_answered_while_the_index_has_work_and_complete_after() {
let files = MemoryFiles::new().with_file("/p/widget.h", WIDGET_H);
let providers = SessionFiles::new(OpenDocuments::new(), files);
let mut session = Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.add_project_files([PathBuf::from("/p/widget.h")]);
assert!(
session.pending() > 0,
"the fixture is the state this test is about"
);
assert!(
symbols(&session, "Widget").is_some_and(|found| found.is_empty()),
"a list of what the index has — empty here, because nothing has been read — rather than `null`"
);
session.index_everything();
let found = symbols(&session, "widget").expect("the index is complete now");
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].name, "Widget");
assert_eq!(
found[0].container_name.as_deref(),
Some("ns"),
"the qualification travels, because a client shows it"
);
}
}