2026-07-16 07:42:03 +07:00
|
|
|
#![allow(
|
|
|
|
|
clippy::cast_possible_truncation,
|
|
|
|
|
clippy::cast_sign_loss,
|
|
|
|
|
clippy::cast_precision_loss,
|
|
|
|
|
clippy::cast_possible_wrap
|
|
|
|
|
)]
|
2026-07-17 09:03:37 +07:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-07-16 07:42:03 +07:00
|
|
|
use anyhow::{anyhow, Result};
|
2026-07-17 09:03:37 +07:00
|
|
|
use serde_json::Value;
|
2026-07-12 13:40:58 +07:00
|
|
|
|
|
|
|
|
use crate::app::lsp::path_to_lsp_uri;
|
2026-07-17 09:03:37 +07:00
|
|
|
use crate::tool::ToolCtx;
|
2026-07-12 14:47:01 +07:00
|
|
|
|
|
|
|
|
/// 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"],
|
|
|
|
|
_ => &[],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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
|
2026-07-16 07:42:03 +07:00
|
|
|
/// -> return the first match's `language_id`.
|
2026-07-12 14:47:01 +07:00
|
|
|
///
|
|
|
|
|
/// 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> {
|
2026-07-16 07:42:03 +07:00
|
|
|
let ext = std::path::Path::new(path)
|
|
|
|
|
.extension()
|
|
|
|
|
.and_then(|e| e.to_str())?;
|
2026-07-13 08:12:02 +07:00
|
|
|
let dot_ext = format!(".{ext}");
|
2026-07-12 14:47:01 +07:00
|
|
|
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()) {
|
2026-07-16 07:42:03 +07:00
|
|
|
return Some(s.language_id.clone());
|
2026-07-12 14:47:01 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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
|
2026-07-16 07:42:03 +07:00
|
|
|
/// registry lookup by delegating to `auto_detect_server`. If that also fails,
|
|
|
|
|
/// build a helpful error message
|
2026-07-12 14:47:01 +07:00
|
|
|
/// 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()
|
2026-07-16 07:42:03 +07:00
|
|
|
.and_then(|e| e.to_str())
|
|
|
|
|
.map_or_else(|| "<none>".to_string(), |e| format!(".{e}"));
|
2026-07-12 14:47:01 +07:00
|
|
|
|
2026-07-16 07:42:03 +07:00
|
|
|
let available = ctx
|
|
|
|
|
.lsp_manager
|
|
|
|
|
.lock()
|
|
|
|
|
.ok()
|
2026-07-12 14:47:01 +07:00
|
|
|
.map(|mgr| {
|
|
|
|
|
mgr.list_servers()
|
|
|
|
|
.iter()
|
2026-07-16 07:42:03 +07:00
|
|
|
.map(|(lang, _)| lang.clone())
|
2026-07-12 14:47:01 +07:00
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(", ")
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default();
|
2026-07-16 07:42:03 +07:00
|
|
|
let available = if available.is_empty() {
|
|
|
|
|
"none".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
available
|
|
|
|
|
};
|
2026-07-12 14:47:01 +07:00
|
|
|
|
|
|
|
|
Err(anyhow!(
|
2026-07-13 08:12:02 +07:00
|
|
|
"LSP server not found for extension '{ext}'. Use lsp_connect to connect one. Available servers: {available}"
|
2026-07-12 14:47:01 +07:00
|
|
|
))
|
|
|
|
|
}
|
2026-07-17 09:03:37 +07:00
|
|
|
|
|
|
|
|
/// 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))
|
|
|
|
|
}
|