use crate::error::Result;
use crate::lsp::LanguageServerManager;
use crate::tools::ensure_document_open;
use lsp_types::{DocumentSymbol, DocumentSymbolResponse, SymbolInformation, SymbolKind};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize)]
pub struct SymbolInfo {
pub name: String,
pub kind: String,
pub detail: Option<String>,
pub file: Option<String>,
pub line: u32,
pub character: u32,
pub children: Option<Vec<SymbolInfo>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DocumentSymbolsResult {
pub file: String,
pub symbols: Vec<SymbolInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkspaceSymbolsResult {
pub query: String,
pub symbols: Vec<SymbolInfo>,
}
pub fn document_symbols(
manager: &LanguageServerManager,
file_path: &str,
) -> Result<DocumentSymbolsResult> {
let path = PathBuf::from(file_path);
let (client, uri) = ensure_document_open(manager, &path)?;
let response = client.document_symbols(&uri)?;
let symbols = match response {
Some(DocumentSymbolResponse::Flat(infos)) => {
infos.into_iter().map(convert_symbol_information).collect()
}
Some(DocumentSymbolResponse::Nested(docs)) => {
docs.into_iter().map(convert_document_symbol).collect()
}
None => vec![],
};
Ok(DocumentSymbolsResult {
file: file_path.to_string(),
symbols,
})
}
pub fn workspace_symbols(
manager: &LanguageServerManager,
query: &str,
) -> Result<WorkspaceSymbolsResult> {
let workspace = manager
.active_workspace()
.ok_or(crate::error::LspMcpError::NoActiveWorkspace)?;
let workspaces = manager.list_workspaces();
let workspace_info = workspaces
.iter()
.find(|w| w.path == workspace)
.ok_or(crate::error::LspMcpError::NoActiveWorkspace)?;
let language = workspace_info
.languages
.first()
.ok_or(crate::error::LspMcpError::NoActiveWorkspace)?;
let client = manager.get_client(workspace, *language)?;
let response = client.workspace_symbols(query)?;
let symbols: Vec<SymbolInfo> = response
.unwrap_or_default()
.into_iter()
.map(convert_symbol_information)
.collect();
Ok(WorkspaceSymbolsResult {
query: query.to_string(),
symbols,
})
}
#[allow(deprecated)]
fn convert_symbol_information(info: SymbolInformation) -> SymbolInfo {
SymbolInfo {
name: info.name,
kind: symbol_kind_to_string(info.kind),
detail: None,
file: Some(
info.location
.uri
.to_file_path()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| info.location.uri.to_string()),
),
line: info.location.range.start.line,
character: info.location.range.start.character,
children: None,
}
}
fn convert_document_symbol(doc: DocumentSymbol) -> SymbolInfo {
let children = if doc.children.as_ref().map(|c| c.is_empty()).unwrap_or(true) {
None
} else {
Some(
doc.children
.unwrap_or_default()
.into_iter()
.map(convert_document_symbol)
.collect(),
)
};
SymbolInfo {
name: doc.name,
kind: symbol_kind_to_string(doc.kind),
detail: doc.detail,
file: None,
line: doc.selection_range.start.line,
character: doc.selection_range.start.character,
children,
}
}
fn symbol_kind_to_string(kind: SymbolKind) -> String {
match kind {
SymbolKind::FILE => "file",
SymbolKind::MODULE => "module",
SymbolKind::NAMESPACE => "namespace",
SymbolKind::PACKAGE => "package",
SymbolKind::CLASS => "class",
SymbolKind::METHOD => "method",
SymbolKind::PROPERTY => "property",
SymbolKind::FIELD => "field",
SymbolKind::CONSTRUCTOR => "constructor",
SymbolKind::ENUM => "enum",
SymbolKind::INTERFACE => "interface",
SymbolKind::FUNCTION => "function",
SymbolKind::VARIABLE => "variable",
SymbolKind::CONSTANT => "constant",
SymbolKind::STRING => "string",
SymbolKind::NUMBER => "number",
SymbolKind::BOOLEAN => "boolean",
SymbolKind::ARRAY => "array",
SymbolKind::OBJECT => "object",
SymbolKind::KEY => "key",
SymbolKind::NULL => "null",
SymbolKind::ENUM_MEMBER => "enum_member",
SymbolKind::STRUCT => "struct",
SymbolKind::EVENT => "event",
SymbolKind::OPERATOR => "operator",
SymbolKind::TYPE_PARAMETER => "type_parameter",
_ => "unknown",
}
.to_string()
}