use std::path::Path;
use lsp_types::GotoDefinitionResponse;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utils::display_meta::{ToolDisplayMeta, ToolResultMeta, basename};
use crate::lsp::common::{DEFAULT_RESULT_LIMIT, truncate_results, uri_to_path};
use crate::lsp::error::LspError;
use crate::lsp::registry::LspRegistry;
use crate::lsp::render::{SourceLine, render_locations};
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum SymbolLookupOperation {
Definition,
References,
Hover,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hover_contents: Option<String>,
#[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 {
async fn locations(
operation: &str,
mut locations: Vec<SourceLine>,
input: &LspSymbolInput,
project_root: &Path,
) -> Self {
let truncation = truncate_results(&mut locations, input.limit.unwrap_or(DEFAULT_RESULT_LIMIT));
Self {
operation: operation.to_string(),
locations: Some(render_locations(&locations, project_root, input.context_lines).await),
total_count: Some(truncation.total_count),
truncated: truncation.truncated,
..Self::default()
}
}
}
pub async fn execute_lsp_symbol(
mut input: LspSymbolInput,
registry: &LspRegistry,
) -> Result<LspSymbolOutput, LspError> {
input.file_path = registry.resolve_file(&input.file_path)?;
let resolved = registry.resolve_symbol(&input.file_path, &input.symbol, input.line).await?;
let root = registry.root_path();
let mut output = match input.operation {
SymbolLookupOperation::Definition => {
let response = resolved.client.goto_definition(resolved.uri, resolved.line, resolved.column).await?;
LspSymbolOutput::locations("definition", definition_response_to_lines(response), &input, root).await
}
SymbolLookupOperation::References => {
let lsp_locations = resolved
.client
.find_references(resolved.uri, resolved.line, resolved.column, input.include_declaration)
.await?;
let locations = lsp_locations.iter().map(SourceLine::from_location).collect();
LspSymbolOutput::locations("references", locations, &input, root).await
}
SymbolLookupOperation::Hover => {
let hover = resolved.client.hover(resolved.uri, resolved.line, resolved.column).await?;
LspSymbolOutput {
operation: "hover".to_string(),
hover_contents: hover.map(|h| format_hover_contents(&h)),
..LspSymbolOutput::default()
}
}
};
#[allow(clippy::used_underscore_binding)]
{
output.meta = Some(symbol_display_meta(&input, &output).into());
}
Ok(output)
}
fn definition_response_to_lines(response: GotoDefinitionResponse) -> Vec<SourceLine> {
match response {
GotoDefinitionResponse::Scalar(loc) => vec![SourceLine::from_location(&loc)],
GotoDefinitionResponse::Array(locs) => locs.iter().map(SourceLine::from_location).collect(),
GotoDefinitionResponse::Link(links) => links
.iter()
.map(|link| SourceLine::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::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}")),
}
}
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)
}
}
}