use crate::error::Result;
use crate::lsp::LanguageServerManager;
use crate::tools::{ensure_document_open, make_position};
use lsp_types::{TextEdit, WorkspaceEdit};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize)]
pub struct TextEditInfo {
pub start_line: u32,
pub start_character: u32,
pub end_line: u32,
pub end_character: u32,
pub new_text: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FileEdits {
pub file: String,
pub edits: Vec<TextEditInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PrepareRenameResult {
pub can_rename: bool,
pub placeholder: Option<String>,
pub start_line: Option<u32>,
pub start_character: Option<u32>,
pub end_line: Option<u32>,
pub end_character: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RenameResult {
pub files_affected: usize,
pub total_edits: usize,
pub edits: Vec<FileEdits>,
}
pub fn prepare_rename(
manager: &LanguageServerManager,
file_path: &str,
line: u32,
character: u32,
) -> Result<PrepareRenameResult> {
let path = PathBuf::from(file_path);
let (client, uri) = ensure_document_open(manager, &path)?;
let position = make_position(line, character);
let response = client.prepare_rename(&uri, position)?;
match response {
Some(lsp_types::PrepareRenameResponse::Range(range)) => Ok(PrepareRenameResult {
can_rename: true,
placeholder: None,
start_line: Some(range.start.line),
start_character: Some(range.start.character),
end_line: Some(range.end.line),
end_character: Some(range.end.character),
}),
Some(lsp_types::PrepareRenameResponse::RangeWithPlaceholder { range, placeholder }) => {
Ok(PrepareRenameResult {
can_rename: true,
placeholder: Some(placeholder),
start_line: Some(range.start.line),
start_character: Some(range.start.character),
end_line: Some(range.end.line),
end_character: Some(range.end.character),
})
}
Some(lsp_types::PrepareRenameResponse::DefaultBehavior { default_behavior: _ }) => {
Ok(PrepareRenameResult {
can_rename: true,
placeholder: None,
start_line: None,
start_character: None,
end_line: None,
end_character: None,
})
}
None => Ok(PrepareRenameResult {
can_rename: false,
placeholder: None,
start_line: None,
start_character: None,
end_line: None,
end_character: None,
}),
}
}
pub fn rename_symbol(
manager: &LanguageServerManager,
file_path: &str,
line: u32,
character: u32,
new_name: &str,
) -> Result<RenameResult> {
let path = PathBuf::from(file_path);
let (client, uri) = ensure_document_open(manager, &path)?;
let position = make_position(line, character);
let workspace_edit = client.rename(&uri, position, new_name)?;
match workspace_edit {
Some(edit) => Ok(convert_workspace_edit(edit)),
None => Ok(RenameResult {
files_affected: 0,
total_edits: 0,
edits: vec![],
}),
}
}
fn convert_workspace_edit(edit: WorkspaceEdit) -> RenameResult {
let mut file_edits: HashMap<String, Vec<TextEditInfo>> = HashMap::new();
if let Some(changes) = edit.changes {
for (uri, edits) in changes {
let file_path = uri
.to_file_path()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| uri.to_string());
let edit_infos: Vec<TextEditInfo> = edits.into_iter().map(convert_text_edit).collect();
file_edits
.entry(file_path)
.or_default()
.extend(edit_infos);
}
}
if let Some(doc_changes) = edit.document_changes {
match doc_changes {
lsp_types::DocumentChanges::Edits(edits) => {
for versioned in edits {
let file_path = versioned
.text_document
.uri
.to_file_path()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| versioned.text_document.uri.to_string());
let edit_infos: Vec<TextEditInfo> = versioned
.edits
.into_iter()
.filter_map(|e| match e {
lsp_types::OneOf::Left(edit) => Some(convert_text_edit(edit)),
lsp_types::OneOf::Right(_annotated) => None,
})
.collect();
file_edits
.entry(file_path)
.or_default()
.extend(edit_infos);
}
}
lsp_types::DocumentChanges::Operations(operations) => {
for op in operations {
match op {
lsp_types::DocumentChangeOperation::Edit(versioned) => {
let file_path = versioned
.text_document
.uri
.to_file_path()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| versioned.text_document.uri.to_string());
let edit_infos: Vec<TextEditInfo> = versioned
.edits
.into_iter()
.filter_map(|e| match e {
lsp_types::OneOf::Left(edit) => Some(convert_text_edit(edit)),
lsp_types::OneOf::Right(_annotated) => None,
})
.collect();
file_edits
.entry(file_path)
.or_default()
.extend(edit_infos);
}
lsp_types::DocumentChangeOperation::Op(_) => {
}
}
}
}
}
}
let total_edits: usize = file_edits.values().map(|e| e.len()).sum();
let files_affected = file_edits.len();
let edits: Vec<FileEdits> = file_edits
.into_iter()
.map(|(file, edits)| FileEdits { file, edits })
.collect();
RenameResult {
files_affected,
total_edits,
edits,
}
}
fn convert_text_edit(edit: TextEdit) -> TextEditInfo {
TextEditInfo {
start_line: edit.range.start.line,
start_character: edit.range.start.character,
end_line: edit.range.end.line,
end_character: edit.range.end.character,
new_text: edit.new_text,
}
}