Add the cast-allow block to zesdex-entities/src/lib.rs and zesdex-utils/src/lib.rs (which lacked it), then remove from 65 sub-files across all 8 crates. Build and all 223 tests continue to pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
230 lines
7.9 KiB
Rust
230 lines
7.9 KiB
Rust
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::Value;
|
|
|
|
use crate::app::lsp::path_to_lsp_uri;
|
|
use crate::tool::ToolCtx;
|
|
|
|
/// Return the default file extensions associated with a language id.
|
|
///
|
|
/// Flow: pure `match` on `language_id` -> static slice of extension
|
|
/// strings (with leading dot). Returns an empty slice for unknown
|
|
/// languages, so callers can safely chain lookups without a special case.
|
|
///
|
|
/// Used by `lsp_connect` to auto-register extensions for a newly connected
|
|
/// server, and by `auto_detect_server` as a fallback when the manager's own
|
|
/// `extension_registry` has no entry yet.
|
|
fn known_extensions_for(language_id: &str) -> &[&'static str] {
|
|
match language_id {
|
|
"rust" => &[".rs"],
|
|
"typescript" => &[".ts", ".tsx", ".js", ".jsx"],
|
|
"go" => &[".go"],
|
|
"java" => &[".java"],
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
/// Build the standard `server` + `path` + `line` + `column` parameter schema
|
|
/// used by cursor-based LSP tools (definition, references, completion).
|
|
///
|
|
/// When `with_language_id` is `true`, an optional `language_id` property is
|
|
/// included (for tools like hover that pass it to `didOpen`).
|
|
pub fn lsp_cursor_params(with_language_id: bool) -> serde_json::Value {
|
|
let mut props = serde_json::json!({
|
|
"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)"
|
|
}
|
|
});
|
|
if with_language_id {
|
|
if let Some(obj) = props.as_object_mut() {
|
|
obj.insert(
|
|
"language_id".to_string(),
|
|
serde_json::json!({
|
|
"type": "string",
|
|
"description": "Language identifier (e.g. 'rust', 'typescript'). Optional if already set via lsp_connect."
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": props,
|
|
"required": ["path", "line", "column"]
|
|
})
|
|
}
|
|
|
|
/// Guess which connected LSP server should handle `path` based on its extension.
|
|
///
|
|
/// Flow: extract extension from `path` -> for each connected server, check
|
|
/// whether `known_extensions_for(server.language_id)` contains the extension
|
|
/// -> return the first match's `language_id`.
|
|
///
|
|
/// This is a fallback used only when the caller omits `server` and the file's
|
|
/// extension is not (yet) present in `LspManager::extension_registry` — e.g.
|
|
/// 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 auto_detect_server(ctx: &ToolCtx, path: &str) -> Option<String> {
|
|
let ext = std::path::Path::new(path)
|
|
.extension()
|
|
.and_then(|e| e.to_str())?;
|
|
let dot_ext = format!(".{ext}");
|
|
if let Ok(mgr) = ctx.lsp_manager.lock() {
|
|
for s in &mgr.servers {
|
|
let exts = known_extensions_for(&s.language_id);
|
|
if exts.contains(&dot_ext.as_str()) {
|
|
return Some(s.language_id.clone());
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Resolve the LSP server name to use for a tool call: explicit `server`
|
|
/// argument if present, otherwise auto-detected from `path`'s extension.
|
|
///
|
|
/// Flow: `args["server"]` present -> use it as-is. Otherwise -> try
|
|
/// registry lookup by delegating to `auto_detect_server`. If that also fails,
|
|
/// build a helpful error message
|
|
/// listing the currently connected servers (via `LspManager::list_servers`)
|
|
/// so the caller knows whether to connect one first.
|
|
///
|
|
/// Return: `Ok(server_name)` on success. `Err` only when no `server` was
|
|
/// given and auto-detection could not resolve one — never fails just
|
|
/// because the caller provided an explicit (possibly wrong) server name,
|
|
/// since downstream `get_client`/`get_language_id` calls report that error.
|
|
fn resolve_server_name(ctx: &ToolCtx, args: &Value, path: &str) -> Result<String> {
|
|
if let Some(server) = args.get("server").and_then(|v| v.as_str()) {
|
|
return Ok(server.to_string());
|
|
}
|
|
|
|
if let Some(name) = auto_detect_server(ctx, path) {
|
|
return Ok(name);
|
|
}
|
|
|
|
let ext = std::path::Path::new(path)
|
|
.extension()
|
|
.and_then(|e| e.to_str())
|
|
.map_or_else(|| "<none>".to_string(), |e| format!(".{e}"));
|
|
|
|
let available = ctx
|
|
.lsp_manager
|
|
.lock()
|
|
.ok()
|
|
.map(|mgr| {
|
|
mgr.list_servers()
|
|
.iter()
|
|
.map(|(lang, _)| lang.clone())
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
})
|
|
.unwrap_or_default();
|
|
let available = if available.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
available
|
|
};
|
|
|
|
Err(anyhow!(
|
|
"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.
|
|
///
|
|
/// When `text` is `Some`, the provided content is used instead of reading
|
|
/// from disk (used by `LspDiagnostics` which receives the full text as an
|
|
/// argument).
|
|
fn run_lsp_query<F, R>(
|
|
ctx: &ToolCtx,
|
|
args: &Value,
|
|
text: Option<&str>,
|
|
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 = match text {
|
|
Some(t) => t.to_string(),
|
|
None => 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))
|
|
}
|