use lsp_types::{Location, Position};
use serde_json::Value;
use std::path::Path;
use crate::lsp::client::uri_to_file_path;
pub fn handle(args: &Value, project_root: &str) -> String {
let action = args
.get("action")
.and_then(Value::as_str)
.unwrap_or("references");
let Some(path) = args.get("path").and_then(Value::as_str) else {
return "ERROR: 'path' parameter is required.".to_string();
};
let line = args.get("line").and_then(Value::as_u64).unwrap_or(1) as u32;
let column = args.get("column").and_then(Value::as_u64).unwrap_or(0) as u32;
let abs_path = if Path::new(path).is_absolute() {
path.to_string()
} else {
format!("{project_root}/{path}")
};
let uri = match crate::lsp::router::open_file(&abs_path, project_root) {
Ok(u) => u,
Err(e) => return format!("ERROR: {e}"),
};
let position = Position::new(line.saturating_sub(1), column);
match action {
"rename" => handle_rename(args, &abs_path, project_root, &uri, position),
"references" => handle_references(&abs_path, project_root, &uri, position),
"definition" => handle_definition(&abs_path, project_root, &uri, position),
"implementations" => handle_implementations(&abs_path, project_root, &uri, position),
_ => format!(
"ERROR: Unknown action '{action}'. Available: rename, references, definition, implementations."
),
}
}
fn handle_rename(
args: &Value,
file_path: &str,
project_root: &str,
uri: &lsp_types::Uri,
position: Position,
) -> String {
let Some(new_name) = args.get("new_name").and_then(Value::as_str) else {
return "ERROR: 'new_name' parameter is required for rename.".to_string();
};
let result = crate::lsp::router::with_client(file_path, project_root, |client, _| {
client.rename(uri, position, new_name)
});
match result {
Ok(Some(edit)) => format_workspace_edit(&edit, project_root),
Ok(None) => "No rename edits returned by language server.".to_string(),
Err(e) => format!("ERROR: {e}"),
}
}
fn handle_references(
file_path: &str,
project_root: &str,
uri: &lsp_types::Uri,
position: Position,
) -> String {
let result = crate::lsp::router::with_client(file_path, project_root, |client, _| {
client.references(uri, position)
});
match result {
Ok(locations) => format_locations(&locations, project_root),
Err(e) => format!("ERROR: {e}"),
}
}
fn handle_definition(
file_path: &str,
project_root: &str,
uri: &lsp_types::Uri,
position: Position,
) -> String {
let result = crate::lsp::router::with_client(file_path, project_root, |client, _| {
client.definition(uri, position)
});
match result {
Ok(resp) => {
let locations = match resp {
lsp_types::GotoDefinitionResponse::Scalar(loc) => vec![loc],
lsp_types::GotoDefinitionResponse::Array(locs) => locs,
lsp_types::GotoDefinitionResponse::Link(links) => links
.into_iter()
.map(|l| Location {
uri: l.target_uri,
range: l.target_selection_range,
})
.collect(),
};
format_locations(&locations, project_root)
}
Err(e) => format!("ERROR: {e}"),
}
}
fn handle_implementations(
file_path: &str,
project_root: &str,
uri: &lsp_types::Uri,
position: Position,
) -> String {
let result = crate::lsp::router::with_client(file_path, project_root, |client, _| {
client.implementations(uri, position)
});
match result {
Ok(locations) => format_locations(&locations, project_root),
Err(e) => format!("ERROR: {e}"),
}
}
fn format_locations(locations: &[Location], project_root: &str) -> String {
if locations.is_empty() {
return "No results found.".to_string();
}
let mut out = format!("{} location(s):\n", locations.len());
for loc in locations {
let path = uri_to_file_path(&loc.uri).map_or_else(
|| loc.uri.as_str().to_string(),
|p| {
p.strip_prefix(project_root)
.map(|s| s.strip_prefix('/').unwrap_or(s).to_string())
.unwrap_or(p)
},
);
let line = loc.range.start.line + 1;
let col = loc.range.start.character;
out.push_str(&format!(" {path}:{line}:{col}\n"));
}
out
}
fn format_workspace_edit(edit: &lsp_types::WorkspaceEdit, project_root: &str) -> String {
let mut out = String::from("Rename edits:\n");
let mut file_count = 0;
let mut edit_count = 0;
if let Some(ref changes) = edit.changes {
for (uri, edits) in changes {
let path = uri_to_file_path(uri).map_or_else(
|| uri.as_str().to_string(),
|p| {
p.strip_prefix(project_root)
.map(|s| s.strip_prefix('/').unwrap_or(s).to_string())
.unwrap_or(p)
},
);
file_count += 1;
out.push_str(&format!(" {path}: {} edit(s)\n", edits.len()));
for e in edits {
edit_count += 1;
let line = e.range.start.line + 1;
out.push_str(&format!(" L{line}: -> \"{}\"\n", e.new_text));
}
}
}
if let Some(ref doc_changes) = edit.document_changes {
match doc_changes {
lsp_types::DocumentChanges::Edits(edits) => {
for text_edit in edits {
let path = uri_to_file_path(&text_edit.text_document.uri)
.unwrap_or_else(|| text_edit.text_document.uri.as_str().to_string());
file_count += 1;
let edits_len = text_edit.edits.len();
edit_count += edits_len;
out.push_str(&format!(" {path}: {edits_len} edit(s)\n"));
}
}
lsp_types::DocumentChanges::Operations(ops) => {
for op in ops {
if let lsp_types::DocumentChangeOperation::Edit(text_edit) = op {
let path = uri_to_file_path(&text_edit.text_document.uri)
.unwrap_or_else(|| text_edit.text_document.uri.as_str().to_string());
file_count += 1;
let edits_len = text_edit.edits.len();
edit_count += edits_len;
out.push_str(&format!(" {path}: {edits_len} edit(s)\n"));
}
}
}
}
}
out.push_str(&format!(
"\nTotal: {edit_count} edit(s) across {file_count} file(s)."
));
out
}