use crate::cli::{ListSymbolsArgs, LspWorkspaceQueryArgs, WorkspaceQueryArgs};
use crate::commands::common::{PreparedWorkspace, connect_lsp_client, prepare_workspace};
use crate::config::ConfigStore;
use crate::detect::matching_files;
use crate::lsp::{
LspClient, SourceCache, SymbolMatch, document_symbol_matches_from_response,
document_symbol_supported, ensure_call_hierarchy_support, ensure_workspace_symbol_support,
function_matches_from_document_response, is_function_symbol_kind,
location_matches_from_response, location_matches_from_response_with_full_content,
path_to_file_uri, prepare_call_hierarchy_response, should_skip_document_symbol_error,
symbol_full_content_from_document_response, symbol_matches_from_response,
};
use crate::suggest::SuggestedLanguage;
use std::collections::{BTreeSet, HashSet};
use std::path::{Path, PathBuf};
use std::time::Duration;
mod kinds;
mod render;
#[cfg(test)]
mod tests;
use kinds::{CallHierarchyDirection, LocationQueryKind, zero_based_col, zero_based_line};
pub(super) use render::{
render_file_list_json, render_list_symbols_json, render_paths_text,
render_symbol_match_paths_text, render_symbol_matches_text, render_symbol_matches_text_full,
render_symbol_names_text, render_workspace_symbol_json, render_workspace_symbol_json_full,
truncate_items,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum ListSymbolsTarget {
File,
Directory,
}
pub(super) struct WorkspaceSymbolQueryResult {
pub detected_filetypes: BTreeSet<String>,
pub server: SuggestedLanguage,
pub matches: Vec<SymbolMatch>,
}
pub(super) struct FileListQueryResult {
pub detected_filetypes: BTreeSet<String>,
pub server: SuggestedLanguage,
pub files: Vec<PathBuf>,
}
pub(super) fn run_workspace_symbol_query(
args: &LspWorkspaceQueryArgs,
query: &str,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
let (workspace, matches) = with_initialized_client(
&args.query.directory,
args.query.lsp.as_deref(),
args.query.lang.as_deref(),
args.detach,
args.download,
args.query.wait_for_index,
args.query.debug,
args.query.timeout,
config,
|workspace, initialize, client| {
ensure_workspace_symbol_support(initialize)?;
let response = client
.workspace_symbol(query)
.map_err(|error| format!("failed to query {}: {error}", workspace.server.server))?;
symbol_matches_from_response(&response)
},
)?;
Ok(WorkspaceSymbolQueryResult {
detected_filetypes: workspace.detection.filetypes,
server: workspace.server,
matches,
})
}
pub(super) fn run_document_symbol_query(
args: &LspWorkspaceQueryArgs,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
let (workspace, matches) = with_initialized_client(
&args.query.directory,
args.query.lsp.as_deref(),
args.query.lang.as_deref(),
args.detach,
args.download,
args.query.wait_for_index,
args.query.debug,
args.query.timeout,
config,
|workspace, initialize, client| {
ensure_list_functions_support(initialize, &workspace.server.server)?;
let files = matching_files(
&args.query.directory,
&config.filetypes,
&workspace.allowed_filetypes,
)
.map_err(|error| {
format!("failed to scan {}: {error}", args.query.directory.display())
})?;
let mut source_cache = SourceCache::default();
let mut matches = Vec::new();
for file in &files {
let uri = path_to_file_uri(file)?;
client.open_document(file, &uri).map_err(|error| {
format!(
"failed to open {} with {}: {error}",
file.display(),
workspace.server.server
)
})?;
let response = match client.document_symbol(&uri) {
Ok(response) => response,
Err(error) if should_skip_document_symbol_error(&error) => continue,
Err(error) => {
return Err(format!(
"failed to query {} for {}: {error}",
workspace.server.server,
file.display()
));
}
};
matches.extend(function_matches_from_document_response(
&response,
file,
&mut source_cache,
)?);
}
Ok(matches)
},
)?;
Ok(WorkspaceSymbolQueryResult {
detected_filetypes: workspace.detection.filetypes,
server: workspace.server,
matches,
})
}
pub(super) fn run_list_symbols_query(
args: &ListSymbolsArgs,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
let target = list_symbols_target(&args.path)?;
let (workspace, matches) = with_initialized_client(
&args.path,
args.lsp.as_deref(),
args.lang.as_deref(),
args.detach,
args.download,
args.wait_for_index,
args.debug,
args.timeout,
config,
|workspace, initialize, client| {
ensure_list_symbols_support(initialize, &workspace.server.server)?;
let files = match target {
ListSymbolsTarget::File => vec![args.path.clone()],
ListSymbolsTarget::Directory => {
matching_files(&args.path, &config.filetypes, &workspace.allowed_filetypes)
.map_err(|error| {
format!("failed to scan {}: {error}", args.path.display())
})?
}
};
let mut source_cache = SourceCache::default();
let mut matches = Vec::new();
for file in &files {
let uri = path_to_file_uri(file)?;
client.open_document(file, &uri).map_err(|error| {
format!(
"failed to open {} with {}: {error}",
file.display(),
workspace.server.server
)
})?;
let response = match client.document_symbol(&uri) {
Ok(response) => response,
Err(error)
if target == ListSymbolsTarget::Directory
&& should_skip_document_symbol_error(&error) =>
{
continue;
}
Err(error) => {
return Err(format!(
"failed to query {} for {}: {error}",
workspace.server.server,
file.display()
));
}
};
matches.extend(document_symbol_matches_from_response(
&response,
file,
&mut source_cache,
)?);
}
Ok(matches)
},
)?;
Ok(WorkspaceSymbolQueryResult {
detected_filetypes: workspace.detection.filetypes,
server: workspace.server,
matches,
})
}
pub(super) fn list_symbols_target(path: &Path) -> Result<ListSymbolsTarget, String> {
let metadata = std::fs::metadata(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
format!(
"list-symbols expected a file or directory path, but {} does not exist",
path.display()
)
} else {
format!("failed to inspect {}: {error}", path.display())
}
})?;
if metadata.is_dir() {
return Ok(ListSymbolsTarget::Directory);
}
if metadata.is_file() {
return Ok(ListSymbolsTarget::File);
}
Err(format!(
"list-symbols expected a regular file or directory path, but {} is not supported",
path.display()
))
}
fn ensure_list_functions_support(
initialize: &crate::lsp::InitializeResponse,
server_name: &str,
) -> Result<(), String> {
if !document_symbol_supported(initialize) {
return Err(format!(
"{server_name} does not support list-functions because it does not advertise textDocument/documentSymbol"
));
}
Ok(())
}
fn ensure_list_symbols_support(
initialize: &crate::lsp::InitializeResponse,
server_name: &str,
) -> Result<(), String> {
if !document_symbol_supported(initialize) {
return Err(format!(
"{server_name} does not support list-symbols because it does not advertise textDocument/documentSymbol"
));
}
Ok(())
}
pub(super) fn run_list_files_query(
args: &WorkspaceQueryArgs,
config: &ConfigStore,
) -> Result<FileListQueryResult, String> {
let workspace = prepare_workspace(
&args.directory,
args.lsp.as_deref(),
args.lang.as_deref(),
false,
config,
)?;
let files = matching_files(
&args.directory,
&config.filetypes,
&workspace.allowed_filetypes,
)
.map_err(|error| format!("failed to scan {}: {error}", args.directory.display()))?;
Ok(FileListQueryResult {
detected_filetypes: workspace.detection.filetypes,
server: workspace.server,
files,
})
}
pub(super) fn run_references_query(
args: &LspWorkspaceQueryArgs,
name: &str,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
run_named_location_query(args, name, LocationQueryKind::References, false, config)
}
pub(super) fn run_definition_query(
args: &LspWorkspaceQueryArgs,
name: &str,
full: bool,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
run_named_location_query(args, name, LocationQueryKind::Definition, full, config)
}
pub(super) fn run_declaration_query(
args: &LspWorkspaceQueryArgs,
name: &str,
full: bool,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
run_named_location_query(args, name, LocationQueryKind::Declaration, full, config)
}
pub(super) fn run_callers_query(
args: &LspWorkspaceQueryArgs,
name: &str,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
run_call_hierarchy_query(args, name, CallHierarchyDirection::Incoming, config)
}
pub(super) fn run_callees_query(
args: &LspWorkspaceQueryArgs,
name: &str,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
run_call_hierarchy_query(args, name, CallHierarchyDirection::Outgoing, config)
}
#[allow(clippy::fn_params_excessive_bools, clippy::too_many_arguments)]
fn with_initialized_client<T, F>(
path: &Path,
selected_server: Option<&str>,
selected_language: Option<&str>,
detach: bool,
download: bool,
wait_for_index_requested: bool,
debug: bool,
timeout: Duration,
config: &ConfigStore,
run: F,
) -> Result<(PreparedWorkspace, T), String>
where
F: FnOnce(
&PreparedWorkspace,
&crate::lsp::InitializeResponse,
&mut LspClient,
) -> Result<T, String>,
{
let workspace = prepare_workspace(path, selected_server, selected_language, download, config)?;
let wait_for_index = wait_for_index_requested || workspace.server.wait_for_index;
let mut client = connect_lsp_client(&workspace, detach, debug, timeout)?;
let initialize = client
.initialize(
&workspace.root_uri,
&workspace.workspace_name,
wait_for_index,
)
.map_err(|error| format!("failed to initialize {}: {error}", workspace.server.server))?;
let response = (if wait_for_index {
client.wait_for_background_work().map_err(|error| {
format!(
"failed to wait for background work with {}: {error}",
workspace.server.server
)
})
} else {
Ok(())
})
.and_then(|()| run(&workspace, &initialize, &mut client));
let shutdown = client.shutdown();
let response = response?;
shutdown.map_err(|error| {
format!(
"failed to stop {} cleanly: {error}",
workspace.server.server
)
})?;
Ok((workspace, response))
}
fn run_named_location_query(
args: &LspWorkspaceQueryArgs,
name: &str,
kind: LocationQueryKind,
include_full_content: bool,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
let (workspace, matches) = with_initialized_client(
&args.query.directory,
args.query.lsp.as_deref(),
args.query.lang.as_deref(),
args.detach,
args.download,
args.query.wait_for_index,
args.query.debug,
args.query.timeout,
config,
|workspace, initialize, client| {
ensure_workspace_symbol_support(initialize)?;
kind.ensure_support(initialize)?;
let anchors = client.workspace_symbol(name).map_err(|error| {
format!(
"failed to find matching symbols for {name:?} with {}: {error}",
workspace.server.server
)
})?;
let workspace_anchors = symbol_matches_from_response(&anchors)?;
let anchors = select_named_anchors(
workspace,
initialize,
client,
config,
NamedAnchorRequest {
directory: &args.query.directory,
name,
function_only: false,
},
workspace_anchors,
)?;
let mut source_cache = SourceCache::default();
let mut matches = Vec::new();
for anchor in anchors {
let uri = path_to_file_uri(&anchor.path)?;
client.open_document(&anchor.path, &uri).map_err(|error| {
format!(
"failed to open {} with {}: {error}",
anchor.path.display(),
workspace.server.server
)
})?;
let response = kind.query(client, &uri, &anchor).map_err(|error| {
format!(
"failed to query {} for {} of {name:?}: {error}",
workspace.server.server,
kind.label()
)
})?;
matches.extend(if include_full_content {
location_matches_from_response_with_full_content(
&response,
&anchor.name,
anchor.kind,
&mut source_cache,
)?
} else {
location_matches_from_response(
&response,
&anchor.name,
anchor.kind,
&mut source_cache,
)?
});
}
let mut matches = dedupe_symbol_matches(matches);
if include_full_content && document_symbol_supported(initialize) {
fill_definition_full_content(workspace, client, &mut source_cache, &mut matches)?;
}
Ok(matches)
},
)?;
Ok(WorkspaceSymbolQueryResult {
detected_filetypes: workspace.detection.filetypes,
server: workspace.server,
matches,
})
}
fn run_call_hierarchy_query(
args: &LspWorkspaceQueryArgs,
name: &str,
direction: CallHierarchyDirection,
config: &ConfigStore,
) -> Result<WorkspaceSymbolQueryResult, String> {
let (workspace, matches) = with_initialized_client(
&args.query.directory,
args.query.lsp.as_deref(),
args.query.lang.as_deref(),
args.detach,
args.download,
args.query.wait_for_index,
args.query.debug,
args.query.timeout,
config,
|workspace, initialize, client| {
ensure_workspace_symbol_support(initialize)?;
ensure_call_hierarchy_support(initialize)?;
let anchors = client.workspace_symbol(name).map_err(|error| {
format!(
"failed to find matching symbols for {name:?} with {}: {error}",
workspace.server.server
)
})?;
let workspace_anchors = symbol_matches_from_response(&anchors)?;
let anchors = select_named_anchors(
workspace,
initialize,
client,
config,
NamedAnchorRequest {
directory: &args.query.directory,
name,
function_only: true,
},
workspace_anchors,
)?;
let mut source_cache = SourceCache::default();
let mut matches = Vec::new();
for anchor in anchors {
let uri = path_to_file_uri(&anchor.path)?;
client.open_document(&anchor.path, &uri).map_err(|error| {
format!(
"failed to open {} with {}: {error}",
anchor.path.display(),
workspace.server.server
)
})?;
let prepared = client
.prepare_call_hierarchy(&uri, zero_based_line(&anchor), zero_based_col(&anchor))
.map_err(|error| {
format!(
"failed to prepare call hierarchy with {} for {name:?}: {error}",
workspace.server.server
)
})?;
let items = prepare_call_hierarchy_response(&prepared)?;
for item in &items {
let response = direction.query(client, item).map_err(|error| {
format!(
"failed to query {} for {} of {name:?}: {error}",
workspace.server.server,
direction.label()
)
})?;
matches.extend(direction.decode(&response, &mut source_cache)?);
}
}
Ok(dedupe_symbol_matches(matches))
},
)?;
Ok(WorkspaceSymbolQueryResult {
detected_filetypes: workspace.detection.filetypes,
server: workspace.server,
matches,
})
}
fn dedupe_symbol_matches(matches: Vec<SymbolMatch>) -> Vec<SymbolMatch> {
let mut seen = HashSet::new();
let mut deduped = Vec::new();
for matched in matches {
let key = (
matched.path.clone(),
matched.line,
matched.col,
matched.name.clone(),
);
if seen.insert(key) {
deduped.push(matched);
}
}
deduped
}
fn preferred_name_matches(matches: Vec<SymbolMatch>, name: &str) -> Vec<SymbolMatch> {
let exact_matches = matches
.iter()
.filter(|matched| matched.name == name)
.cloned()
.collect::<Vec<_>>();
if exact_matches.is_empty() {
matches
} else {
exact_matches
}
}
fn preferred_function_name_matches(matches: Vec<SymbolMatch>, name: &str) -> Vec<SymbolMatch> {
let matches = preferred_name_matches(matches, name);
matches
.into_iter()
.filter(|matched| is_function_symbol_kind(matched.kind))
.collect()
}
fn select_named_anchors(
workspace: &PreparedWorkspace,
initialize: &crate::lsp::InitializeResponse,
client: &mut LspClient,
config: &ConfigStore,
request: NamedAnchorRequest<'_>,
workspace_anchors: Vec<SymbolMatch>,
) -> Result<Vec<SymbolMatch>, String> {
if document_symbol_supported(initialize) {
let document_anchors = exact_named_document_anchors(workspace, client, config, request)?;
if !document_anchors.is_empty() {
return Ok(document_anchors);
}
}
Ok(if request.function_only {
preferred_function_name_matches(workspace_anchors, request.name)
} else {
preferred_name_matches(workspace_anchors, request.name)
})
}
#[derive(Clone, Copy)]
struct NamedAnchorRequest<'a> {
directory: &'a Path,
name: &'a str,
function_only: bool,
}
fn exact_named_document_anchors(
workspace: &PreparedWorkspace,
client: &mut LspClient,
config: &ConfigStore,
request: NamedAnchorRequest<'_>,
) -> Result<Vec<SymbolMatch>, String> {
let files = matching_files(
request.directory,
&config.filetypes,
&workspace.allowed_filetypes,
)
.map_err(|error| format!("failed to scan {}: {error}", request.directory.display()))?;
let mut source_cache = SourceCache::default();
let mut matches = Vec::new();
for file in &files {
let uri = path_to_file_uri(file)?;
client.open_document(file, &uri).map_err(|error| {
format!(
"failed to open {} with {}: {error}",
file.display(),
workspace.server.server
)
})?;
let response = match client.document_symbol(&uri) {
Ok(response) => response,
Err(error) if should_skip_document_symbol_error(&error) => continue,
Err(_) => continue,
};
let file_matches = if request.function_only {
function_matches_from_document_response(&response, file, &mut source_cache)?
} else {
document_symbol_matches_from_response(&response, file, &mut source_cache)?
};
matches.extend(
file_matches
.into_iter()
.filter(|matched| matched.name == request.name),
);
}
Ok(dedupe_symbol_matches(matches))
}
fn fill_definition_full_content(
workspace: &PreparedWorkspace,
client: &mut LspClient,
source_cache: &mut SourceCache,
matches: &mut [SymbolMatch],
) -> Result<(), String> {
let mut responses = std::collections::HashMap::new();
for matched in matches {
if !responses.contains_key(&matched.path) {
let uri = path_to_file_uri(&matched.path)?;
client.open_document(&matched.path, &uri).map_err(|error| {
format!(
"failed to open {} with {}: {error}",
matched.path.display(),
workspace.server.server
)
})?;
let response = match client.document_symbol(&uri) {
Ok(response) => Some(response),
Err(error) if should_skip_document_symbol_error(&error) => None,
Err(_) => None,
};
responses.insert(matched.path.clone(), response);
}
if let Some(Some(response)) = responses.get(&matched.path) {
matched.full_content = symbol_full_content_from_document_response(
response,
&matched.path,
matched,
source_cache,
)?
.or_else(|| matched.full_content.clone())
.or_else(|| Some(matched.line_content.clone()));
} else if matched.full_content.is_none() {
matched.full_content = Some(matched.line_content.clone());
}
}
Ok(())
}