use std::path::Path;
use cosh_sdk::lsp::lsp_types::{
WorkspaceEdit,
request::{Rename as RenameRequest, Request as _},
};
use serde_json::json;
use super::{
support,
types::{RenameFilePlan, RenameInput, RenameOutput},
};
pub async fn run_rename(
deps: &super::support::Deps<'_>,
input: &RenameInput,
) -> Result<RenameOutput, String> {
if input.new_name.trim().is_empty() {
return Err("`new_name` cannot be empty".into());
}
let path = deps.manager.root().join(&input.file_path);
let (position, clients) =
support::resolve_target(deps, &path, &input.position, &input.symbol).await?;
let params = json!({
"textDocument": { "uri": support::uri(&path)? },
"position": position,
"newName": input.new_name,
});
let response: Option<WorkspaceEdit> = super::support::first_answer(
&clients,
deps.request_timeout,
|caps| super::support::provider_enabled(&caps.rename_provider),
|_client| (RenameRequest::METHOD.to_owned(), params.clone()),
)
.await?;
let workspace_edit =
response.ok_or_else(|| "the server cannot rename this symbol".to_owned())?;
let encoding = clients[0].position_encoding();
let plans = support::flatten_workspace_edit(&workspace_edit, encoding)?;
if plans.is_empty() {
return Err("the server returned an empty rename plan".into());
}
let total_edits: usize = plans.values().map(|edits| edits.len()).sum();
let files: Vec<RenameFilePlan> = plans
.iter()
.map(|(file_path, edits)| RenameFilePlan {
path: file_path.clone(),
edits: edits.len(),
preview: edits.iter().take(3).map(preview_line).collect(),
})
.collect();
if !input.confirm.unwrap_or(false) {
let formatted = render_plan(&files, total_edits, false);
return Ok(RenameOutput {
applied: false,
files,
total_edits,
formatted,
});
}
let mut applied_files = Vec::with_capacity(plans.len());
for (file_path, planned) in &plans {
let path = Path::new(file_path);
support::apply_edits(path, planned)?;
for client in &clients {
let _ = client.touch_file(path).await;
}
applied_files.push(RenameFilePlan {
path: file_path.clone(),
edits: planned.len(),
preview: Vec::new(),
});
}
let formatted = render_plan(&applied_files, total_edits, true);
Ok(RenameOutput {
applied: true,
files: applied_files,
total_edits,
formatted,
})
}
fn preview_line(edit: &support::PlannedEdit) -> String {
format!("L{} → {}", edit.line, ellipsize(&edit.new_text, 60))
}
fn ellipsize(text: &str, max_chars: usize) -> String {
let single = text.replace('\n', "⏎ ");
if single.chars().count() <= max_chars {
single
} else {
let cut: String = single.chars().take(max_chars).collect();
format!("{cut}…")
}
}
fn render_plan(files: &[RenameFilePlan], total_edits: usize, applied: bool) -> String {
let mut lines = Vec::new();
if applied {
lines.push(format!(
"Renamed across {} file(s), {total_edits} edit(s):",
files.len()
));
} else {
lines.push(format!(
"Rename plan — {total_edits} edit(s) across {} file(s). \
Re-call with `confirm: true` to apply:",
files.len()
));
}
for file in files {
lines.push(format!("{} ({} edit(s))", file.path, file.edits));
for preview in &file.preview {
lines.push(format!(" {preview}"));
}
}
lines.join("\n")
}