678 lines
26 KiB
Rust
678 lines
26 KiB
Rust
use serde_json::{json, Value};
|
|||
|
|
use anyhow::{Result, anyhow};
|
||
|
|
|
||
|
|
use crate::tool::{Tool, ToolCtx};
|
||
|
|
use crate::app::lsp::path_to_lsp_uri;
|
||
|
|
|
||
|
|
pub struct LspConnect;
|
||
|
|
|
||
|
|
impl Tool for LspConnect {
|
||
|
|
fn name(&self) -> &'static str {
|
||
|
|
"lsp_connect"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn description(&self) -> &'static str {
|
||
|
|
"Connect to a Language Server Protocol (LSP) server for a programming language"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parameters(&self) -> Value {
|
||
|
|
json!({
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"name": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Short name for this LSP connection (e.g. 'rust', 'typescript')"
|
||
|
|
},
|
||
|
|
"command": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "The LSP server binary to spawn (e.g. 'rust-analyzer', 'typescript-language-server')"
|
||
|
|
},
|
||
|
|
"args": {
|
||
|
|
"type": "array",
|
||
|
|
"items": { "type": "string" },
|
||
|
|
"description": "Command-line arguments for the LSP server"
|
||
|
|
},
|
||
|
|
"language_id": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Language identifier (e.g. 'rust', 'typescript', 'python')"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["name", "command", "language_id"]
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||
|
|
let name = args.get("name")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: name"))?;
|
||
|
|
let command = args.get("command")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: command"))?;
|
||
|
|
let language_id = args.get("language_id")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: language_id"))?;
|
||
|
|
let extra_args: Vec<String> = args.get("args")
|
||
|
|
.and_then(|v| v.as_array())
|
||
|
|
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||
|
|
.unwrap_or_default();
|
||
|
|
|
||
|
|
let mut manager = ctx.lsp_manager.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
|
||
|
|
manager.connect(name, command, &extra_args, language_id)?;
|
||
|
|
|
||
|
|
let client_arc = manager.get_client(name);
|
||
|
|
let caps = client_arc.map(|c| {
|
||
|
|
c.lock().ok().map(|guard| guard.server_capabilities().clone())
|
||
|
|
}).flatten().unwrap_or_default();
|
||
|
|
|
||
|
|
let caps_summary = serde_json::to_string_pretty(&caps)
|
||
|
|
.unwrap_or_else(|_| "{}".to_string());
|
||
|
|
|
||
|
|
Ok(format!(
|
||
|
|
"Connected to LSP server '{}' (language: {})\nServer capabilities:\n{}",
|
||
|
|
name, language_id, caps_summary
|
||
|
|
))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct LspDiagnostics;
|
||
|
|
|
||
|
|
impl Tool for LspDiagnostics {
|
||
|
|
fn name(&self) -> &'static str {
|
||
|
|
"lsp_diagnostics"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn description(&self) -> &'static str {
|
||
|
|
"Get diagnostics (errors, warnings, hints) for a file from an LSP server"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parameters(&self) -> Value {
|
||
|
|
json!({
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"server": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Name of the connected LSP server"
|
||
|
|
},
|
||
|
|
"path": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Path to the file to analyze (relative to workspace root)"
|
||
|
|
},
|
||
|
|
"text": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "The full text content of the file"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["server", "path", "text"]
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||
|
|
let server_name = args.get("server")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: server"))?;
|
||
|
|
let rel_path = args.get("path")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: path"))?;
|
||
|
|
let text = args.get("text")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: text"))?;
|
||
|
|
|
||
|
|
let abs_path = crate::tool::resolve_path(&ctx.workspaces, rel_path)?;
|
||
|
|
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
|
||
|
|
|
||
|
|
let manager = ctx.lsp_manager.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
|
||
|
|
let language_id = manager.get_language_id(server_name)
|
||
|
|
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
|
||
|
|
let client_arc = manager.get_client(server_name)
|
||
|
|
.ok_or_else(|| anyhow!("LSP server '{}' not found", server_name))?;
|
||
|
|
drop(manager);
|
||
|
|
|
||
|
|
let mut client = client_arc.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
|
||
|
|
|
||
|
|
match client.collect_diagnostics(&uri, &language_id, text) {
|
||
|
|
Ok(diags) => {
|
||
|
|
let diags_array = diags.as_array().cloned().unwrap_or_default();
|
||
|
|
if diags_array.is_empty() {
|
||
|
|
return Ok("No diagnostics found for this file.".to_string());
|
||
|
|
}
|
||
|
|
let mut output = String::from("Diagnostics:\n");
|
||
|
|
for d in &diags_array {
|
||
|
|
let range = d.get("range").and_then(|r| r.get("start"));
|
||
|
|
let severity = match d.get("severity").and_then(|s| s.as_i64()).unwrap_or(0) {
|
||
|
|
1 => "ERROR",
|
||
|
|
2 => "WARNING",
|
||
|
|
3 => "INFO",
|
||
|
|
4 => "HINT",
|
||
|
|
_ => "NOTE",
|
||
|
|
};
|
||
|
|
let message = d.get("message").and_then(|m| m.as_str()).unwrap_or("?");
|
||
|
|
let line = range.and_then(|r| r.get("line")).and_then(|l| l.as_i64()).unwrap_or(0);
|
||
|
|
let col = range.and_then(|r| r.get("character")).and_then(|c| c.as_i64()).unwrap_or(0);
|
||
|
|
let code = d.get("code")
|
||
|
|
.and_then(|c| c.as_str().or_else(|| c.as_i64().map(|n| Box::leak(Box::new(n.to_string()))).map(|s| s.as_str())))
|
||
|
|
.unwrap_or("");
|
||
|
|
let code_str = if code.is_empty() { String::new() } else { format!(" [{}]", code) };
|
||
|
|
output.push_str(&format!(" {}:{}:{} - {}{}: {}\n", rel_path, line + 1, col, severity, code_str, message));
|
||
|
|
}
|
||
|
|
Ok(output)
|
||
|
|
}
|
||
|
|
Err(e) => {
|
||
|
|
if e.to_string().contains("timed out") {
|
||
|
|
Ok("Diagnostics request timed out. The server may still be initializing. Try again in a moment.".to_string())
|
||
|
|
} else {
|
||
|
|
Err(e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct LspHover;
|
||
|
|
|
||
|
|
impl Tool for LspHover {
|
||
|
|
fn name(&self) -> &'static str {
|
||
|
|
"lsp_hover"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn description(&self) -> &'static str {
|
||
|
|
"Get hover information (type signature, documentation) at a cursor position in a file"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parameters(&self) -> Value {
|
||
|
|
json!({
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"server": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Name of the connected LSP server"
|
||
|
|
},
|
||
|
|
"path": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Path to the file (relative to workspace root)"
|
||
|
|
},
|
||
|
|
"line": {
|
||
|
|
"type": "integer",
|
||
|
|
"description": "Line number (0-based)"
|
||
|
|
},
|
||
|
|
"column": {
|
||
|
|
"type": "integer",
|
||
|
|
"description": "Column number (0-based)"
|
||
|
|
},
|
||
|
|
"language_id": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Language identifier (e.g. 'rust', 'typescript'). Optional if already set via lsp_connect."
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["server", "path", "line", "column"]
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||
|
|
let server_name = args.get("server")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: server"))?;
|
||
|
|
let rel_path = args.get("path")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: path"))?;
|
||
|
|
let line = args.get("line")
|
||
|
|
.and_then(|v| v.as_i64())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: line"))? as u32;
|
||
|
|
let column = args.get("column")
|
||
|
|
.and_then(|v| v.as_i64())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: column"))? as u32;
|
||
|
|
|
||
|
|
let abs_path = crate::tool::resolve_path(&ctx.workspaces, rel_path)?;
|
||
|
|
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
|
||
|
|
|
||
|
|
let file_content = std::fs::read_to_string(&abs_path)
|
||
|
|
.map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?;
|
||
|
|
|
||
|
|
let manager = ctx.lsp_manager.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
|
||
|
|
let language_id = manager.get_language_id(server_name)
|
||
|
|
.unwrap_or_else(|| {
|
||
|
|
args.get("language_id").and_then(|v| v.as_str()).unwrap_or("plaintext").to_string()
|
||
|
|
});
|
||
|
|
let client_arc = manager.get_client(server_name)
|
||
|
|
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
|
||
|
|
drop(manager);
|
||
|
|
|
||
|
|
let mut client = client_arc.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
|
||
|
|
|
||
|
|
client.did_open(&uri, &language_id, 1, &file_content)?;
|
||
|
|
let result = client.hover(&uri, line, column);
|
||
|
|
let _ = client.did_close(&uri);
|
||
|
|
|
||
|
|
match result {
|
||
|
|
Ok(hover_result) => {
|
||
|
|
if hover_result == Value::Null {
|
||
|
|
return Ok("No hover information available at this position.".to_string());
|
||
|
|
}
|
||
|
|
let contents = hover_result.get("contents");
|
||
|
|
let range = hover_result.get("range");
|
||
|
|
let mut output = String::new();
|
||
|
|
if let Some(range_val) = range {
|
||
|
|
if let Some(start) = range_val.get("start") {
|
||
|
|
let rl = start.get("line").and_then(|l| l.as_i64()).unwrap_or(0);
|
||
|
|
let rc = start.get("character").and_then(|c| c.as_i64()).unwrap_or(0);
|
||
|
|
output.push_str(&format!("Range: {}:{}\n", rl + 1, rc + 1));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if let Some(contents_val) = contents {
|
||
|
|
output.push_str(&format_hover_contents(contents_val));
|
||
|
|
} else {
|
||
|
|
output.push_str(&serde_json::to_string_pretty(&hover_result).unwrap_or_default());
|
||
|
|
}
|
||
|
|
Ok(output)
|
||
|
|
}
|
||
|
|
Err(e) => Err(e),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn format_hover_contents(contents: &Value) -> String {
|
||
|
|
let mut out = String::new();
|
||
|
|
match contents {
|
||
|
|
Value::String(s) => {
|
||
|
|
out.push_str(s);
|
||
|
|
}
|
||
|
|
Value::Object(map) => {
|
||
|
|
if let Some(kind) = map.get("kind").and_then(|k| k.as_str()) {
|
||
|
|
out.push_str(&format!("[{kind}] "));
|
||
|
|
}
|
||
|
|
if let Some(value) = map.get("value").and_then(|v| v.as_str()) {
|
||
|
|
out.push_str(value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Value::Array(arr) => {
|
||
|
|
for (i, item) in arr.iter().enumerate() {
|
||
|
|
if i > 0 {
|
||
|
|
out.push('\n');
|
||
|
|
}
|
||
|
|
out.push_str(&format_hover_contents(item));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
out.push_str(&serde_json::to_string_pretty(contents).unwrap_or_default());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
out
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct LspCompletion;
|
||
|
|
|
||
|
|
impl Tool for LspCompletion {
|
||
|
|
fn name(&self) -> &'static str {
|
||
|
|
"lsp_completion"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn description(&self) -> &'static str {
|
||
|
|
"Get code completion suggestions at a cursor position from an LSP server"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parameters(&self) -> Value {
|
||
|
|
json!({
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"server": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Name of the connected LSP server"
|
||
|
|
},
|
||
|
|
"path": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Path to the file (relative to workspace root)"
|
||
|
|
},
|
||
|
|
"line": {
|
||
|
|
"type": "integer",
|
||
|
|
"description": "Line number (0-based)"
|
||
|
|
},
|
||
|
|
"column": {
|
||
|
|
"type": "integer",
|
||
|
|
"description": "Column number (0-based)"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["server", "path", "line", "column"]
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||
|
|
let server_name = args.get("server")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: server"))?;
|
||
|
|
let rel_path = args.get("path")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: path"))?;
|
||
|
|
let line = args.get("line")
|
||
|
|
.and_then(|v| v.as_i64())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: line"))? as u32;
|
||
|
|
let column = args.get("column")
|
||
|
|
.and_then(|v| v.as_i64())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: column"))? as u32;
|
||
|
|
|
||
|
|
let abs_path = crate::tool::resolve_path(&ctx.workspaces, rel_path)?;
|
||
|
|
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
|
||
|
|
|
||
|
|
let file_content = std::fs::read_to_string(&abs_path)
|
||
|
|
.map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?;
|
||
|
|
|
||
|
|
let manager = ctx.lsp_manager.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
|
||
|
|
let language_id = manager.get_language_id(server_name)
|
||
|
|
.unwrap_or_else(|| "plaintext".to_string());
|
||
|
|
let client_arc = manager.get_client(server_name)
|
||
|
|
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
|
||
|
|
drop(manager);
|
||
|
|
|
||
|
|
let mut client = client_arc.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
|
||
|
|
|
||
|
|
client.did_open(&uri, &language_id, 1, &file_content)?;
|
||
|
|
let result = client.completion(&uri, line, column);
|
||
|
|
let _ = client.did_close(&uri);
|
||
|
|
|
||
|
|
match result {
|
||
|
|
Ok(completion_result) => {
|
||
|
|
let items = if let Some(items) = completion_result.as_array() {
|
||
|
|
items.clone()
|
||
|
|
} else if let Some(arr) = completion_result.get("items").and_then(|v| v.as_array()) {
|
||
|
|
arr.clone()
|
||
|
|
} else {
|
||
|
|
Vec::new()
|
||
|
|
};
|
||
|
|
|
||
|
|
if items.is_empty() {
|
||
|
|
return Ok("No completions available at this position.".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut output = format!("{} completion suggestions at {}:{}:\n", items.len(), line + 1, column + 1);
|
||
|
|
for (i, item) in items.iter().enumerate().take(50) {
|
||
|
|
let label = item.get("label").and_then(|l| l.as_str()).unwrap_or("?");
|
||
|
|
let kind = match item.get("kind").and_then(|k| k.as_i64()).unwrap_or(0) {
|
||
|
|
1 => "Text",
|
||
|
|
2 => "Method",
|
||
|
|
3 => "Function",
|
||
|
|
4 => "Constructor",
|
||
|
|
5 => "Field",
|
||
|
|
6 => "Variable",
|
||
|
|
7 => "Class",
|
||
|
|
8 => "Interface",
|
||
|
|
9 => "Module",
|
||
|
|
10 => "Property",
|
||
|
|
11 => "Unit",
|
||
|
|
12 => "Value",
|
||
|
|
13 => "Enum",
|
||
|
|
14 => "Keyword",
|
||
|
|
15 => "Snippet",
|
||
|
|
16 => "Color",
|
||
|
|
17 => "File",
|
||
|
|
18 => "Reference",
|
||
|
|
19 => "Folder",
|
||
|
|
20 => "EnumMember",
|
||
|
|
21 => "Constant",
|
||
|
|
22 => "Struct",
|
||
|
|
23 => "Event",
|
||
|
|
24 => "Operator",
|
||
|
|
25 => "TypeParameter",
|
||
|
|
_ => "Other",
|
||
|
|
};
|
||
|
|
let detail = item.get("detail").and_then(|d| d.as_str()).unwrap_or("");
|
||
|
|
let detail_str = if detail.is_empty() { String::new() } else { format!(" - {}", detail) };
|
||
|
|
output.push_str(&format!(" {}. [{}] {}{}\n", i + 1, kind, label, detail_str));
|
||
|
|
}
|
||
|
|
if items.len() > 50 {
|
||
|
|
output.push_str(&format!(" ... and {} more\n", items.len() - 50));
|
||
|
|
}
|
||
|
|
Ok(output)
|
||
|
|
}
|
||
|
|
Err(e) => Err(e),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct LspDefinition;
|
||
|
|
|
||
|
|
impl Tool for LspDefinition {
|
||
|
|
fn name(&self) -> &'static str {
|
||
|
|
"lsp_definition"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn description(&self) -> &'static str {
|
||
|
|
"Go to definition: find the location where a symbol is defined"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parameters(&self) -> Value {
|
||
|
|
json!({
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"server": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Name of the connected LSP server"
|
||
|
|
},
|
||
|
|
"path": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Path to the file (relative to workspace root)"
|
||
|
|
},
|
||
|
|
"line": {
|
||
|
|
"type": "integer",
|
||
|
|
"description": "Line number (0-based)"
|
||
|
|
},
|
||
|
|
"column": {
|
||
|
|
"type": "integer",
|
||
|
|
"description": "Column number (0-based)"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["server", "path", "line", "column"]
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||
|
|
let server_name = args.get("server")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: server"))?;
|
||
|
|
let rel_path = args.get("path")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: path"))?;
|
||
|
|
let line = args.get("line")
|
||
|
|
.and_then(|v| v.as_i64())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: line"))? as u32;
|
||
|
|
let column = args.get("column")
|
||
|
|
.and_then(|v| v.as_i64())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: column"))? as u32;
|
||
|
|
|
||
|
|
let abs_path = crate::tool::resolve_path(&ctx.workspaces, rel_path)?;
|
||
|
|
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
|
||
|
|
|
||
|
|
let file_content = std::fs::read_to_string(&abs_path)
|
||
|
|
.map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?;
|
||
|
|
|
||
|
|
let manager = ctx.lsp_manager.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
|
||
|
|
let language_id = manager.get_language_id(server_name)
|
||
|
|
.unwrap_or_else(|| "plaintext".to_string());
|
||
|
|
let client_arc = manager.get_client(server_name)
|
||
|
|
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
|
||
|
|
drop(manager);
|
||
|
|
|
||
|
|
let mut client = client_arc.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
|
||
|
|
|
||
|
|
client.did_open(&uri, &language_id, 1, &file_content)?;
|
||
|
|
let result = client.goto_definition(&uri, line, column);
|
||
|
|
let _ = client.did_close(&uri);
|
||
|
|
|
||
|
|
match result {
|
||
|
|
Ok(def_result) => {
|
||
|
|
if def_result == Value::Null {
|
||
|
|
return Ok("No definition found at this position.".to_string());
|
||
|
|
}
|
||
|
|
let locations = if let Some(loc) = def_result.as_array() {
|
||
|
|
loc.clone()
|
||
|
|
} else {
|
||
|
|
vec![def_result.clone()]
|
||
|
|
};
|
||
|
|
|
||
|
|
if locations.is_empty() {
|
||
|
|
return Ok("No definition found.".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut output = String::from("Definition(s):\n");
|
||
|
|
for (i, loc) in locations.iter().enumerate().take(10) {
|
||
|
|
let target_uri = loc.get("uri").and_then(|u| u.as_str()).unwrap_or("?");
|
||
|
|
let target_range = loc.get("range").or_else(|| loc.get("targetRange"));
|
||
|
|
let target_start = target_range.and_then(|r| r.get("start"));
|
||
|
|
let tl = target_start.and_then(|s| s.get("line")).and_then(|l| l.as_i64()).unwrap_or(0);
|
||
|
|
let tc = target_start.and_then(|s| s.get("character")).and_then(|c| c.as_i64()).unwrap_or(0);
|
||
|
|
let path_str = target_uri.strip_prefix("file://").unwrap_or(target_uri);
|
||
|
|
output.push_str(&format!(" {}. {}:{}:{}\n", i + 1, path_str, tl + 1, tc + 1));
|
||
|
|
}
|
||
|
|
if locations.len() > 10 {
|
||
|
|
output.push_str(&format!(" ... and {} more locations\n", locations.len() - 10));
|
||
|
|
}
|
||
|
|
Ok(output)
|
||
|
|
}
|
||
|
|
Err(e) => Err(e),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct LspReferences;
|
||
|
|
|
||
|
|
impl Tool for LspReferences {
|
||
|
|
fn name(&self) -> &'static str {
|
||
|
|
"lsp_references"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn description(&self) -> &'static str {
|
||
|
|
"Find all references to a symbol at a cursor position"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parameters(&self) -> Value {
|
||
|
|
json!({
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"server": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Name of the connected LSP server"
|
||
|
|
},
|
||
|
|
"path": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Path to the file (relative to workspace root)"
|
||
|
|
},
|
||
|
|
"line": {
|
||
|
|
"type": "integer",
|
||
|
|
"description": "Line number (0-based)"
|
||
|
|
},
|
||
|
|
"column": {
|
||
|
|
"type": "integer",
|
||
|
|
"description": "Column number (0-based)"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["server", "path", "line", "column"]
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||
|
|
let server_name = args.get("server")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: server"))?;
|
||
|
|
let rel_path = args.get("path")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: path"))?;
|
||
|
|
let line = args.get("line")
|
||
|
|
.and_then(|v| v.as_i64())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: line"))? as u32;
|
||
|
|
let column = args.get("column")
|
||
|
|
.and_then(|v| v.as_i64())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: column"))? as u32;
|
||
|
|
|
||
|
|
let abs_path = crate::tool::resolve_path(&ctx.workspaces, rel_path)?;
|
||
|
|
let uri = path_to_lsp_uri(&abs_path.to_string_lossy());
|
||
|
|
|
||
|
|
let file_content = std::fs::read_to_string(&abs_path)
|
||
|
|
.map_err(|e| anyhow!("failed to read file '{}': {}", rel_path, e))?;
|
||
|
|
|
||
|
|
let manager = ctx.lsp_manager.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
|
||
|
|
let language_id = manager.get_language_id(server_name)
|
||
|
|
.unwrap_or_else(|| "plaintext".to_string());
|
||
|
|
let client_arc = manager.get_client(server_name)
|
||
|
|
.ok_or_else(|| anyhow!("LSP server '{}' not found. Use lsp_connect first.", server_name))?;
|
||
|
|
drop(manager);
|
||
|
|
|
||
|
|
let mut client = client_arc.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP client lock error: {}", e))?;
|
||
|
|
|
||
|
|
client.did_open(&uri, &language_id, 1, &file_content)?;
|
||
|
|
let result = client.references(&uri, line, column);
|
||
|
|
let _ = client.did_close(&uri);
|
||
|
|
|
||
|
|
match result {
|
||
|
|
Ok(ref_result) => {
|
||
|
|
let locations = ref_result.as_array().cloned().unwrap_or_default();
|
||
|
|
if locations.is_empty() {
|
||
|
|
return Ok("No references found for this symbol.".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut output = format!("{} reference(s) found:\n", locations.len());
|
||
|
|
for (i, loc) in locations.iter().enumerate().take(50) {
|
||
|
|
let target_uri = loc.get("uri").and_then(|u| u.as_str()).unwrap_or("?");
|
||
|
|
let range = loc.get("range").and_then(|r| r.get("start"));
|
||
|
|
let rl = range.and_then(|s| s.get("line")).and_then(|l| l.as_i64()).unwrap_or(0);
|
||
|
|
let rc = range.and_then(|s| s.get("character")).and_then(|c| c.as_i64()).unwrap_or(0);
|
||
|
|
let path_str = target_uri.strip_prefix("file://").unwrap_or(target_uri);
|
||
|
|
output.push_str(&format!(" {}. {}:{}:{}\n", i + 1, path_str, rl + 1, rc + 1));
|
||
|
|
}
|
||
|
|
if locations.len() > 50 {
|
||
|
|
output.push_str(&format!(" ... and {} more references\n", locations.len() - 50));
|
||
|
|
}
|
||
|
|
Ok(output)
|
||
|
|
}
|
||
|
|
Err(e) => Err(e),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct LspDisconnect;
|
||
|
|
|
||
|
|
impl Tool for LspDisconnect {
|
||
|
|
fn name(&self) -> &'static str {
|
||
|
|
"lsp_disconnect"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn description(&self) -> &'static str {
|
||
|
|
"Disconnect from a running LSP server and release its resources"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parameters(&self) -> Value {
|
||
|
|
json!({
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"name": {
|
||
|
|
"type": "string",
|
||
|
|
"description": "Name of the LSP server to disconnect"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["name"]
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||
|
|
let name = args.get("name")
|
||
|
|
.and_then(|v| v.as_str())
|
||
|
|
.ok_or_else(|| anyhow!("missing required argument: name"))?;
|
||
|
|
|
||
|
|
let mut manager = ctx.lsp_manager.lock()
|
||
|
|
.map_err(|e| anyhow!("LSP manager lock error: {}", e))?;
|
||
|
|
|
||
|
|
if manager.disconnect(name) {
|
||
|
|
Ok(format!("Disconnected from LSP server '{}'", name))
|
||
|
|
} else {
|
||
|
|
Err(anyhow!("LSP server '{}' not found", name))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|