use lsp_types::GotoDefinitionResponse;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use mcp_utils::display_meta::{ToolDisplayMeta, ToolResultMeta, basename};
use crate::lsp::common::{LocationResult, uri_to_path};
use crate::lsp::registry::LspRegistry;
use super::call_hierarchy::CallSiteResult;
use super::resolve_symbol_position;
enum CallDirection {
Incoming,
Outgoing,
}
impl CallDirection {
fn as_str(&self) -> &'static str {
match self {
CallDirection::Incoming => "incoming",
CallDirection::Outgoing => "outgoing",
}
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum SymbolLookupOperation {
Definition,
Implementation,
References,
Hover,
#[serde(alias = "incoming_calls")]
IncomingCalls,
#[serde(alias = "outgoing_calls")]
OutgoingCalls,
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct LspSymbolInput {
pub operation: SymbolLookupOperation,
#[serde(alias = "file_path")]
pub file_path: String,
pub symbol: String,
#[serde(default)]
pub line: Option<u32>,
#[serde(default = "default_true", alias = "include_declaration")]
pub include_declaration: bool,
#[serde(default)]
pub limit: Option<usize>,
#[serde(default, alias = "context_lines")]
pub context_lines: Option<u32>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Default, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct LspSymbolOutput {
pub operation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub locations: Option<Vec<LocationResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hover_contents: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_sites: Option<Vec<CallSiteResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncated: Option<bool>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
#[schemars(skip)]
pub meta: Option<ToolResultMeta>,
}
impl LspSymbolOutput {
fn with_locations(
operation: &str,
locations: Vec<LocationResult>,
limit: Option<usize>,
) -> Self {
let total_count = locations.len();
let truncated = limit.is_some_and(|l| total_count > l);
let locations = match limit {
Some(l) if total_count > l => locations.into_iter().take(l).collect(),
_ => locations,
};
Self {
operation: operation.to_string(),
locations: Some(locations),
total_count: Some(total_count),
truncated: if truncated { Some(true) } else { None },
..Self::default()
}
}
}
async fn resolve_line(
file_path: &str,
symbol: &str,
explicit_line: Option<u32>,
tools: &LspRegistry,
) -> Result<u32, String> {
match explicit_line {
Some(line) => Ok(line),
None => resolve_symbol_position(file_path, symbol, tools)
.await
.map_err(|e| e.to_string()),
}
}
#[allow(clippy::too_many_lines)]
pub async fn execute_lsp_symbol(
input: LspSymbolInput,
registry: &LspRegistry,
) -> Result<LspSymbolOutput, String> {
let line = resolve_line(&input.file_path, &input.symbol, input.line, registry).await?;
let mut output = match input.operation {
SymbolLookupOperation::Definition => {
let resolved = registry
.resolve_symbol(&input.file_path, &input.symbol, line)
.await
.map_err(|e| e.to_string())?;
let response = resolved
.client
.goto_definition(resolved.uri, resolved.line, resolved.column)
.await
.map_err(|e| e.to_string())?;
let locations = definition_response_to_locations(response);
let mut output = LspSymbolOutput::with_locations("definition", locations, input.limit);
enrich_locations_with_context(&mut output, input.context_lines).await;
output
}
SymbolLookupOperation::Implementation => {
let resolved = registry
.resolve_symbol(&input.file_path, &input.symbol, line)
.await
.map_err(|e| e.to_string())?;
let response = resolved
.client
.goto_implementation(resolved.uri, resolved.line, resolved.column)
.await
.map_err(|e| e.to_string())?;
let locations = definition_response_to_locations(response);
let mut output =
LspSymbolOutput::with_locations("implementation", locations, input.limit);
enrich_locations_with_context(&mut output, input.context_lines).await;
output
}
SymbolLookupOperation::References => {
let resolved = registry
.resolve_symbol(&input.file_path, &input.symbol, line)
.await
.map_err(|e| e.to_string())?;
let lsp_locations = resolved
.client
.find_references(
resolved.uri,
resolved.line,
resolved.column,
input.include_declaration,
)
.await
.map_err(|e| e.to_string())?;
let locations: Vec<LocationResult> = lsp_locations
.iter()
.map(LocationResult::from_location)
.collect();
let mut output = LspSymbolOutput::with_locations("references", locations, input.limit);
enrich_locations_with_context(&mut output, input.context_lines).await;
output
}
SymbolLookupOperation::Hover => {
let resolved = registry
.resolve_symbol(&input.file_path, &input.symbol, line)
.await
.map_err(|e| e.to_string())?;
let hover = resolved
.client
.hover(resolved.uri, resolved.line, resolved.column)
.await
.map_err(|e| e.to_string())?;
LspSymbolOutput {
operation: "hover".to_string(),
hover_contents: hover.map(|h| format_hover_contents(&h)),
..LspSymbolOutput::default()
}
}
SymbolLookupOperation::IncomingCalls => {
execute_one_step_call_hierarchy(
registry,
&input.file_path,
&input.symbol,
line,
CallDirection::Incoming,
input.limit,
)
.await?
}
SymbolLookupOperation::OutgoingCalls => {
execute_one_step_call_hierarchy(
registry,
&input.file_path,
&input.symbol,
line,
CallDirection::Outgoing,
input.limit,
)
.await?
}
};
#[allow(clippy::used_underscore_binding)]
{
output.meta = Some(symbol_display_meta(&input, &output).into());
}
Ok(output)
}
async fn execute_one_step_call_hierarchy(
registry: &LspRegistry,
file_path: &str,
symbol: &str,
line: u32,
direction: CallDirection,
limit: Option<usize>,
) -> Result<LspSymbolOutput, String> {
let resolved = registry
.resolve_symbol(file_path, symbol, line)
.await
.map_err(|e| e.to_string())?;
let items = resolved
.client
.prepare_call_hierarchy(resolved.uri, resolved.line, resolved.column)
.await
.map_err(|e| e.to_string())?;
let Some(item) = items.into_iter().next() else {
return Ok(LspSymbolOutput {
operation: direction.as_str().to_string(),
call_sites: Some(Vec::new()),
total_count: Some(0),
..LspSymbolOutput::default()
});
};
let item_file_path = uri_to_path(&item.uri);
let item_client = registry
.require_client(&item_file_path)
.await
.map_err(|e| e.to_string())?;
let calls = match direction {
CallDirection::Incoming => {
let incoming = item_client
.incoming_calls(item)
.await
.map_err(|e| e.to_string())?;
super::call_hierarchy::convert_incoming_calls(incoming)
}
CallDirection::Outgoing => {
let outgoing = item_client
.outgoing_calls(item)
.await
.map_err(|e| e.to_string())?;
super::call_hierarchy::convert_outgoing_calls(outgoing)
}
};
let total_count = calls.len();
let truncated = limit.is_some_and(|l| total_count > l);
let calls = match limit {
Some(l) if total_count > l => calls.into_iter().take(l).collect(),
_ => calls,
};
Ok(LspSymbolOutput {
operation: direction.as_str().to_string(),
call_sites: Some(calls),
total_count: Some(total_count),
truncated: if truncated { Some(true) } else { None },
..LspSymbolOutput::default()
})
}
async fn enrich_locations_with_context(output: &mut LspSymbolOutput, context_lines: Option<u32>) {
let Some(n) = context_lines.filter(|&n| n > 0) else {
return;
};
if let Some(locations) = output.locations.as_mut() {
super::super::common::enrich_locations(locations, n).await;
}
}
fn definition_response_to_locations(response: GotoDefinitionResponse) -> Vec<LocationResult> {
match response {
GotoDefinitionResponse::Scalar(loc) => vec![LocationResult::from_location(&loc)],
GotoDefinitionResponse::Array(locs) => {
locs.iter().map(LocationResult::from_location).collect()
}
GotoDefinitionResponse::Link(links) => links
.iter()
.map(|link| {
LocationResult::from_range(
uri_to_path(&link.target_uri),
&link.target_selection_range,
)
})
.collect(),
}
}
fn format_hover_contents(hover: &lsp_types::Hover) -> String {
use lsp_types::HoverContents;
match &hover.contents {
HoverContents::Scalar(marked) => format_marked_string(marked),
HoverContents::Array(arr) => arr
.iter()
.map(format_marked_string)
.collect::<Vec<_>>()
.join("\n\n"),
HoverContents::Markup(markup) => markup.value.clone(),
}
}
fn symbol_display_meta(input: &LspSymbolInput, output: &LspSymbolOutput) -> ToolDisplayMeta {
let symbol = &input.symbol;
let file = basename(&input.file_path);
match input.operation {
SymbolLookupOperation::Definition => {
ToolDisplayMeta::new("LSP definition", format!("{symbol} in {file}"))
}
SymbolLookupOperation::Implementation => {
ToolDisplayMeta::new("LSP implementation", format!("{symbol} in {file}"))
}
SymbolLookupOperation::References => {
let count = output.total_count.unwrap_or(0);
ToolDisplayMeta::new("LSP references", format!("{symbol} ({count} refs)"))
}
SymbolLookupOperation::Hover => {
ToolDisplayMeta::new("LSP hover", format!("{symbol} in {file}"))
}
SymbolLookupOperation::IncomingCalls => {
let count = output.total_count.unwrap_or(0);
ToolDisplayMeta::new("LSP callers", format!("{symbol} ({count} callers)"))
}
SymbolLookupOperation::OutgoingCalls => {
let count = output.total_count.unwrap_or(0);
ToolDisplayMeta::new("LSP callees", format!("{symbol} ({count} callees)"))
}
}
}
fn format_marked_string(marked: &lsp_types::MarkedString) -> String {
match marked {
lsp_types::MarkedString::String(s) => s.clone(),
lsp_types::MarkedString::LanguageString(ls) => {
format!("```{}\n{}\n```", ls.language, ls.value)
}
}
}