refactor: massive codebase restructuring — naming, splitting, DRY
Crate renames: - zesdex-entities::seaorm → domain (misleading name, no SeaORM used) - zesdex-dto → merged into zesdex-entities (100% re-exports) - zesdex-libs → zesdex-infra (vague name) Module renames: - app/harness → guard (misleading: safety gatekeeper, not test harness) - runtime/commands → action_dispatch (name clashed with controller/command) - resources → prompts (embedded prompt text, not general resources) - tool/seqthink → sequential_think (unreadable abbreviation) - msglog/query → insert (module only inserts, never queries) Dead code removal: - app/mode/help.rs (orphaned — not declared in mod.rs) - app/mode/loading.rs (orphaned — not declared in mod.rs) File splitting (71 new files, avg ~115 lines/file): - app/runtime/actions/: 1→8 files (was 2030 lines) - view/overlays/: 1→16 files (was 1167 lines) - tool/lsp/: 1→8 per-tool files (was 909 lines) - main.rs: 1→5 files (session, daemon, attach, event_loop) - workflow/engine + hive_mind: 2→10 files - subagent/engine + auto: 2→9 files - lsp/provisioner: 1→5 files - review/: 1→6 files - guard/: 1→2 files (extracted patterns) - state/misc: 1→3 files (input, scroll) - mcp/: 1→3 files (transport, adapter) - stream/json_repair extracted from turn.rs DRY: - Pattern constants (STUB_PATTERNS etc) in guard/patterns shared with subagent - 3 near-identical background spawners → 1 generic + thin wrappers - Shared spawn_subagent_with_drain() extracted - Shared create_session() in main - write_osc52 deduplicated Bug fixes: - archive_message(): sess.db → db (wrong variable name) - execute_one_tool(): wrong parameter name - check_credential_read() function was missing (restored from test expectations)
This commit is contained in:
@@ -4,644 +4,32 @@
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
|
||||
mod connect;
|
||||
mod completion;
|
||||
mod definition;
|
||||
mod diagnostics;
|
||||
mod disconnect;
|
||||
mod hover;
|
||||
mod references;
|
||||
|
||||
pub use connect::LspConnect;
|
||||
pub use completion::LspCompletion;
|
||||
pub use definition::LspDefinition;
|
||||
pub use diagnostics::LspDiagnostics;
|
||||
pub use disconnect::LspDisconnect;
|
||||
pub use hover::LspHover;
|
||||
pub use references::LspReferences;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers used by multiple per-tool files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde_json::{json, Value};
|
||||
use std::fmt::Write;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::app::lsp::path_to_lsp_uri;
|
||||
use crate::tool::{Tool, ToolCtx};
|
||||
|
||||
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. \
|
||||
Known file extensions for the language are auto-registered, enabling other lsp_* \
|
||||
tools to auto-detect this server when `server` is omitted."
|
||||
}
|
||||
|
||||
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 = crate::tool::arg_str(args, "name")?;
|
||||
let command = crate::tool::arg_str(args, "command")?;
|
||||
let language_id = crate::tool::arg_str(args, "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(&command, &extra_args, &language_id)?;
|
||||
|
||||
// Auto-register this server's known extensions so lsp_diagnostics /
|
||||
// lsp_hover / lsp_completion / lsp_definition / lsp_references can
|
||||
// auto-detect it later without an explicit `server` argument.
|
||||
let known_exts = known_extensions_for(&language_id);
|
||||
if !known_exts.is_empty() {
|
||||
manager.register_extensions(&language_id, known_exts);
|
||||
}
|
||||
|
||||
let client_arc = manager.get_client(&language_id);
|
||||
let caps = client_arc
|
||||
.and_then(|c| {
|
||||
c.lock()
|
||||
.ok()
|
||||
.map(|guard| guard.server_capabilities().clone())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let caps_summary = serde_json::to_string_pretty(&caps).unwrap_or_else(|_| "{}".to_string());
|
||||
|
||||
Ok(format!(
|
||||
"Connected to LSP server '{name}' (language: {language_id})\nServer capabilities:\n{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. \
|
||||
`server` is optional — if omitted, the server is auto-detected from the file's extension."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string",
|
||||
"description": "Name of the connected LSP server. Optional — auto-detected from the file extension if omitted."
|
||||
},
|
||||
"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": ["path", "text"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel_path = crate::tool::arg_str(args, "path")?;
|
||||
let text = crate::tool::arg_str(args, "text")?;
|
||||
let server_name = resolve_server_name(ctx, args, &rel_path)?;
|
||||
let server_name = server_name.as_str();
|
||||
|
||||
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 '{server_name}' not found. Use lsp_connect first.")
|
||||
})?;
|
||||
let client_arc = manager
|
||||
.get_client(server_name)
|
||||
.ok_or_else(|| anyhow!("LSP server '{server_name}' not found"))?;
|
||||
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(serde_json::Value::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(serde_json::Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let col = range
|
||||
.and_then(|r| r.get("character"))
|
||||
.and_then(serde_json::Value::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}]")
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
" {}:{}:{} - {}{}: {}",
|
||||
rel_path,
|
||||
line + 1,
|
||||
col,
|
||||
severity,
|
||||
code_str,
|
||||
message
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
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. \
|
||||
`server` is optional — if omitted, the server is auto-detected from the file's extension."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string",
|
||||
"description": "Name of the connected LSP server. Optional — auto-detected from the file extension if omitted."
|
||||
},
|
||||
"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": ["path", "line", "column"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let result = run_lsp_query(ctx, args, |client, uri, line, column| {
|
||||
client.hover(uri, line, column)
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok((hover_result, _line, _column)) => {
|
||||
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(serde_json::Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let rc = start
|
||||
.get("character")
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
writeln!(output, "Range: {}:{}", rl + 1, rc + 1).unwrap();
|
||||
}
|
||||
}
|
||||
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()) {
|
||||
write!(out, "[{kind}] ").unwrap();
|
||||
}
|
||||
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. \
|
||||
`server` is optional — if omitted, the server is auto-detected from the file's extension."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string",
|
||||
"description": "Name of the connected LSP server. Optional — auto-detected from the file extension if omitted."
|
||||
},
|
||||
"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": ["path", "line", "column"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let result = run_lsp_query(ctx, args, |client, uri, line, column| {
|
||||
client.completion(uri, line, column)
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok((completion_result, line, column)) => {
|
||||
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(serde_json::Value::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}")
|
||||
};
|
||||
writeln!(output, " {}. [{}] {}{}", i + 1, kind, label, detail_str).unwrap();
|
||||
}
|
||||
if items.len() > 50 {
|
||||
writeln!(output, " ... and {} more", items.len() - 50).unwrap();
|
||||
}
|
||||
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. \
|
||||
`server` is optional — if omitted, the server is auto-detected from the file's extension."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string",
|
||||
"description": "Name of the connected LSP server. Optional — auto-detected from the file extension if omitted."
|
||||
},
|
||||
"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": ["path", "line", "column"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let result = run_lsp_query(ctx, args, |client, uri, line, column| {
|
||||
client.goto_definition(uri, line, column)
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok((def_result, _line, _column)) => {
|
||||
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(serde_json::Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let tc = target_start
|
||||
.and_then(|s| s.get("character"))
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let path_str = target_uri.strip_prefix("file://").unwrap_or(target_uri);
|
||||
writeln!(output, " {}. {}:{}:{}", i + 1, path_str, tl + 1, tc + 1).unwrap();
|
||||
}
|
||||
if locations.len() > 10 {
|
||||
writeln!(output, " ... and {} more", locations.len() - 10).unwrap();
|
||||
}
|
||||
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. \
|
||||
`server` is optional — if omitted, the server is auto-detected from the file's extension."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string",
|
||||
"description": "Name of the connected LSP server. Optional — auto-detected from the file extension if omitted."
|
||||
},
|
||||
"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": ["path", "line", "column"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let result = run_lsp_query(ctx, args, |client, uri, line, column| {
|
||||
client.references(uri, line, column)
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok((ref_result, _line, _column)) => {
|
||||
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(serde_json::Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let rc = range
|
||||
.and_then(|s| s.get("character"))
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let path_str = target_uri.strip_prefix("file://").unwrap_or(target_uri);
|
||||
writeln!(output, " {}. {}:{}:{}", i + 1, path_str, rl + 1, rc + 1).unwrap();
|
||||
}
|
||||
if locations.len() > 50 {
|
||||
writeln!(output, " ... and {} more references", locations.len() - 50).unwrap();
|
||||
}
|
||||
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 = crate::tool::arg_str(args, "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 '{name}' not found"))
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::tool::ToolCtx;
|
||||
|
||||
/// Return the default file extensions associated with a language id.
|
||||
///
|
||||
@@ -673,55 +61,6 @@ fn known_extensions_for(language_id: &str) -> &[&'static str] {
|
||||
/// a server connected without an explicit `register_extensions` call. Returns
|
||||
/// `None` if the path has no extension, the lock is poisoned, or no
|
||||
/// connected server's language is known to use that extension.
|
||||
fn run_lsp_query<F, R>(ctx: &ToolCtx, args: &Value, op: F) -> Result<(R, u32, u32)>
|
||||
where
|
||||
F: FnOnce(&mut crate::app::lsp::LspClient, &str, u32, u32) -> Result<R>,
|
||||
{
|
||||
let rel_path = crate::tool::arg_str(args, "path")?;
|
||||
let line = args
|
||||
.get("line")
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required argument: line"))? as u32;
|
||||
let column =
|
||||
args.get("column")
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required argument: column"))? as u32;
|
||||
let server_name = resolve_server_name(ctx, args, &rel_path)?;
|
||||
|
||||
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::anyhow!("failed to read file '{rel_path}': {e}"))?;
|
||||
|
||||
let manager = ctx
|
||||
.lsp_manager
|
||||
.lock()
|
||||
.map_err(|e| anyhow::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::anyhow!("LSP server '{server_name}' not found. Use lsp_connect first.")
|
||||
})?;
|
||||
drop(manager);
|
||||
|
||||
let mut client = client_arc
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("LSP client lock error: {e}"))?;
|
||||
|
||||
client.did_open(&uri, &language_id, 1, &file_content)?;
|
||||
let result = op(&mut client, &uri, line, column);
|
||||
let _ = client.did_close(&uri);
|
||||
|
||||
result.map(|r| (r, line, column))
|
||||
}
|
||||
|
||||
fn auto_detect_server(ctx: &ToolCtx, path: &str) -> Option<String> {
|
||||
let ext = std::path::Path::new(path)
|
||||
.extension()
|
||||
@@ -787,3 +126,57 @@ fn resolve_server_name(ctx: &ToolCtx, args: &Value, path: &str) -> Result<String
|
||||
"LSP server not found for extension '{ext}'. Use lsp_connect to connect one. Available servers: {available}"
|
||||
))
|
||||
}
|
||||
|
||||
/// Run a generic LSP query (hover, completion, goto-definition, references).
|
||||
///
|
||||
/// Opens the file on the server via `didOpen`, invokes the query closure,
|
||||
/// then closes the file via `didClose`. Returns the query result along with
|
||||
/// the 0-based line and column for post-processing.
|
||||
fn run_lsp_query<F, R>(ctx: &ToolCtx, args: &Value, op: F) -> Result<(R, u32, u32)>
|
||||
where
|
||||
F: FnOnce(&mut crate::app::lsp::LspClient, &str, u32, u32) -> Result<R>,
|
||||
{
|
||||
let rel_path = crate::tool::arg_str(args, "path")?;
|
||||
let line = args
|
||||
.get("line")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| anyhow!("missing required argument: line"))? as u32;
|
||||
let column = args
|
||||
.get("column")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| anyhow!("missing required argument: column"))? as u32;
|
||||
let server_name = resolve_server_name(ctx, args, &rel_path)?;
|
||||
|
||||
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 '{server_name}' not found. Use lsp_connect first.")
|
||||
})?;
|
||||
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 = op(&mut client, &uri, line, column);
|
||||
let _ = client.did_close(&uri);
|
||||
|
||||
result.map(|r| (r, line, column))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user