use std::path::Path;
use harn_vm::VmValue;
use crate::ast::{api as ast_api, Language};
use crate::error::HostlibError;
use super::super::builtins::SharedIndex;
use super::super::lsp::{self, RenameRequest, Resolution};
use super::super::refactor_backend::{BackendOutcome, FileEdits, ServerRun};
use super::super::refactor_core::{
collect_identifier_spans, files_in_scope, first_syntax_error, plan_file, read_source,
write_plans, EditSpan, FilePlan, Scope, ShadowSite,
};
use super::super::rename_rust_residue::{doc_link_mentions, span_at, Residue};
use super::super::state::IndexState;
use super::super::symbol_graph::Node as SymbolNode;
use super::*;
pub(super) enum Answer {
Response(VmValue),
Fallback(String),
}
pub(super) type WarmResult = Result<lsp::Warmed, (String, Option<ServerRun>)>;
pub(super) fn warm(index: &SharedIndex, symbol_path: &str) -> Option<WarmResult> {
let (root, path) = {
let guard = index.lock().expect("code_index mutex poisoned");
let state = guard.as_ref()?;
(
state.root.clone(),
super::super::builtins::normalize_relative_path_for(state, symbol_path),
)
};
let language = Language::detect(Path::new(&path), None)?;
lsp::serves(language).then(|| lsp::warm(&root, &path, language))
}
pub(super) struct Request<'a> {
pub state: &'a IndexState,
pub seed: &'a SymbolNode,
pub in_scope_files: &'a [String],
pub session_id: Option<&'a str>,
pub dry_run: bool,
pub validate: bool,
pub warmed: Option<WarmResult>,
}
pub(super) fn rename(
env: &mut ResponseEnv<'_>,
mut request: Request<'_>,
) -> Result<Answer, HostlibError> {
let seed_path = request.seed.path.as_ref();
let Some(language) = Language::detect(Path::new(seed_path), None) else {
return Ok(Answer::Fallback(
"the declaration's language is unknown".into(),
));
};
if !lsp::serves(language) {
return Ok(Answer::Fallback(format!(
"no language server is registered for {}",
language.name()
)));
}
let source = read_source(BUILTIN, &request.state.root, seed_path, request.session_id)?;
let Some(offset) = declaration_offset(&source, language, env.symbol_name, request.seed.line)
else {
return Ok(Answer::Fallback(format!(
"no `{}` identifier on line {}",
env.symbol_name, request.seed.line
)));
};
let mentions = if env.scope == Scope::Workspace {
request.in_scope_files.to_vec()
} else {
files_in_scope(
request.state,
Scope::Workspace,
env.symbol_name,
seed_path,
request.session_id,
)
};
let warmed = match request.warmed.take() {
Some(Ok(warmed)) => warmed,
Some(Err((reason, run))) => {
env.backend.language_server = run;
return Ok(Answer::Fallback(reason));
}
None => {
return Ok(Answer::Fallback(format!(
"no language server could start for {}",
language.name()
)))
}
};
let lsp_request = RenameRequest {
index_root: &request.state.root,
path: seed_path,
language,
source: &source,
offset,
old_name: env.symbol_name,
new_name: env.new_name,
session_id: request.session_id,
mentions: &mentions,
};
let (outcome, run) = lsp::rename(&lsp_request, &warmed);
env.backend.language_server = Some(run);
let mut changeset = match outcome {
BackendOutcome::Changeset(changeset) => changeset,
BackendOutcome::Declined(details) => {
return Ok(Answer::Response(emit_response(
env,
"declined",
ResponseExtras {
details: format!("rename aborted without writes: {details}"),
..Default::default()
},
)))
}
BackendOutcome::Unavailable(reason) => return Ok(Answer::Fallback(reason)),
};
let changed: Vec<&str> = changeset.iter().map(|file| file.path.as_str()).collect();
if let Some(response) = scope_exceeded(env, request.in_scope_files, &changed) {
return Ok(Answer::Response(response));
}
if language == Language::Rust {
super::super::rename_rust_residue::complete(
Residue {
builtin: BUILTIN,
state: request.state,
seed_path,
name: env.symbol_name,
new_name: env.new_name,
files: &mentions,
session_id: request.session_id,
module_level: request.seed.container.is_none(),
is_type: request.seed.kind == NodeKind::Type,
},
&mut changeset,
)?;
}
let mut checked: Vec<String> = mentions.clone();
checked.extend(changeset.iter().map(|file| file.path.clone()));
checked.sort();
checked.dedup();
if language == Language::Rust {
let links = doc_link_sites(
&request,
env.symbol_name,
&checked,
&changeset,
&mut env.stale,
)?;
if !links.is_empty() {
let queries: Vec<(String, usize)> = links
.iter()
.map(|site| (site.path.clone(), site.offset))
.collect();
let resolutions = match lsp::resolve(&lsp_request, &warmed, &queries, false) {
Ok(resolutions) => resolutions,
Err(reason) => return Ok(Answer::Fallback(reason)),
};
let seed_file = request
.state
.root
.join(seed_path)
.canonicalize()
.unwrap_or_else(|_| request.state.root.join(seed_path));
let seed_modules = module_path(&source, language, offset);
let sole_type =
type_declarations(&source, language, env.symbol_name, &seed_modules) == 1;
for (site, resolution) in links.iter().zip(resolutions) {
let own_member = site.qualifies
&& sole_type
&& site.path == seed_path
&& matches!(&resolution, Resolution::Elsewhere(Some(target))
if target.path == seed_file
&& body_owner_is(&source, language, target.offset, env.symbol_name)
&& module_path(&source, language, target.offset) == seed_modules);
if site.renameable && (resolution == Resolution::Seed || own_member) {
add_edit(
&request,
&mut changeset,
site,
env.symbol_name,
env.new_name,
)?;
} else {
env.stale.push(format!(
"{}:{} (doc link left unchanged)",
site.path, site.line
));
}
}
}
}
if changeset.is_empty() {
return Ok(Answer::Response(no_match_response(env)));
}
let changed: Vec<&str> = changeset.iter().map(|file| file.path.as_str()).collect();
if let Some(response) = scope_exceeded(env, request.in_scope_files, &changed) {
return Ok(Answer::Response(response));
}
let sites = remaining_mentions(&request, env.symbol_name, language, &checked, &changeset)?;
if sites.len() > MAX_RESOLVED_MENTIONS {
return Ok(Answer::Fallback(format!(
"{} mentions of `{}` are left after the server's edit; more than the {MAX_RESOLVED_MENTIONS} Harn checks",
sites.len(),
env.symbol_name
)));
}
let queries: Vec<(String, usize)> = sites
.iter()
.map(|site| (site.path.clone(), site.offset))
.collect();
let stop_at_miss = env.scope == Scope::Workspace;
let resolutions = match lsp::resolve(&lsp_request, &warmed, &queries, stop_at_miss) {
Ok(resolutions) => resolutions,
Err(reason) => return Ok(Answer::Fallback(reason)),
};
let missed: Vec<(&Site, Resolution)> = sites
.iter()
.zip(resolutions)
.filter(|(_, resolution)| !resolution.is_elsewhere())
.collect();
if let Some((site, resolution)) = missed.first() {
let missed_paths: Vec<&str> = missed.iter().map(|(site, _)| site.path.as_str()).collect();
if let Some(response) = scope_exceeded(env, request.in_scope_files, &missed_paths) {
return Ok(Answer::Response(response));
}
let what = match resolution {
Resolution::Seed => "left a reference to",
Resolution::Encloses => "could not tell what this refers to:",
_ => "could not resolve",
};
return Ok(Answer::Fallback(format!(
"the language server {what} `{}` at {}:{}",
env.symbol_name, site.path, site.line
)));
}
let mut plans: Vec<FilePlan> = Vec::new();
let mut shadows: Vec<ShadowSite> = Vec::new();
for file in changeset {
let Some(file_language) = Language::detect(Path::new(&file.path), None) else {
return Ok(Answer::Response(unsupported_language_response(
env, &file.path, None,
)));
};
let tree = match ast_api::parse_tree(&file.source, file_language) {
Ok(tree) => tree,
Err(err) => {
return Ok(Answer::Response(syntax_error_response(
env,
format!("`{}` failed to parse: {err}", file.path),
)))
}
};
if let Some(kinds) = file_language.rename_identifier_kinds() {
let mut ignored = Vec::new();
collect_identifier_spans(
tree.root_node(),
file.source.as_bytes(),
"",
env.new_name,
kinds,
&file.path,
&mut ignored,
&mut shadows,
);
}
if request.validate {
if let Some(detail) = first_syntax_error(&file.source, file_language) {
return Ok(Answer::Response(syntax_error_response(
env,
format!(
"`{}` does not parse before the edit ({detail}); fix it first",
file.path
),
)));
}
}
let path = file.path.clone();
match plan_file(
file.path,
file_language,
file.source,
file.edits,
request.validate,
) {
Ok(plan) => plans.push(plan),
Err(detail) => {
return Ok(Answer::Response(syntax_error_response(
env,
format!("rewriting `{path}` produced syntax errors: {detail}"),
)))
}
}
}
if !shadows.is_empty() {
return Ok(Answer::Response(conflict_response(env, &shadows)));
}
let failed = if request.dry_run {
Vec::new()
} else {
let failed = write_plans(BUILTIN, &request.state.root, &plans, request.session_id)?;
if request.session_id.is_none() {
let written: Vec<String> = plans.iter().map(|plan| plan.path.clone()).collect();
lsp::files_written(&request.state.root, &written);
}
failed
};
Ok(Answer::Response(applied_response(
env,
&plans,
request.dry_run,
failed,
)))
}
const MAX_RESOLVED_MENTIONS: usize = 200;
struct Site {
path: String,
offset: usize,
line: usize,
qualifies: bool,
renameable: bool,
}
fn scope_exceeded(env: &ResponseEnv<'_>, in_scope: &[String], paths: &[&str]) -> Option<VmValue> {
if env.scope == Scope::Workspace {
return None;
}
let mut outside: Vec<&str> = paths
.iter()
.copied()
.filter(|path| !in_scope.iter().any(|kept| kept == path))
.collect();
outside.sort_unstable();
outside.dedup();
if outside.is_empty() {
return None;
}
Some(emit_response(
env,
"scope_exceeded",
ResponseExtras {
warnings: outside.into_iter().map(str_value).collect(),
details: format!(
"references to `{}` exist outside scope `{}` (listed in `warnings`); renaming only inside the scope would break them. Re-run with scope `workspace`.",
env.symbol_name,
env.scope.as_str()
),
..Default::default()
},
))
}
fn remaining_mentions(
request: &Request<'_>,
name: &str,
language: Language,
mentions: &[String],
changeset: &[FileEdits],
) -> Result<Vec<Site>, HostlibError> {
let mut sites = Vec::new();
for path in mentions {
let Some(file_language) = Language::detect(Path::new(path), None) else {
continue;
};
if !lsp::same_server(language, file_language) {
continue;
}
let Some(kinds) = file_language.rename_identifier_kinds() else {
continue;
};
let edited = changeset.iter().find(|file| &file.path == path);
let source = match edited {
Some(file) => file.source.clone(),
None => read_source(BUILTIN, &request.state.root, path, request.session_id)?,
};
let Ok(tree) = ast_api::parse_tree(&source, file_language) else {
continue;
};
let mut found = Vec::new();
let raw = format!("r#{name}");
let spellings: &[&str] = if file_language == Language::Rust {
&[name, &raw]
} else {
&[name]
};
for spelling in spellings {
collect_identifier_spans(
tree.root_node(),
source.as_bytes(),
spelling,
"",
kinds,
path,
&mut found,
&mut Vec::new(),
);
}
for span in found {
let covered = edited.is_some_and(|file| {
file.edits
.iter()
.any(|edit| edit.span.start_byte == span.start_byte)
});
if !covered {
sites.push(Site {
path: path.clone(),
offset: span.start_byte,
line: span.start_row + 1,
qualifies: false,
renameable: true,
});
}
}
}
sites.sort_by(|left, right| (&left.path, left.offset).cmp(&(&right.path, right.offset)));
Ok(sites)
}
fn doc_link_sites(
request: &Request<'_>,
name: &str,
paths: &[String],
changeset: &[FileEdits],
stale: &mut Vec<String>,
) -> Result<Vec<Site>, HostlibError> {
let mut sites = Vec::new();
for path in paths {
if Language::detect(Path::new(path), None) != Some(Language::Rust) {
continue;
}
let edited = changeset.iter().find(|file| &file.path == path);
let source = match edited {
Some(file) => file.source.clone(),
None => read_source(BUILTIN, &request.state.root, path, request.session_id)?,
};
let Ok(tree) = ast_api::parse_tree(&source, Language::Rust) else {
continue;
};
let reference_definitions = has_reference_definition(&source, name);
if reference_definitions {
for (index, line) in source.lines().enumerate() {
if is_reference_definition(line, name) {
stale.push(format!("{path}:{} (doc link left unchanged)", index + 1));
}
}
}
for offset in doc_link_mentions(tree.root_node(), &source, name) {
let covered = edited
.is_some_and(|file| file.edits.iter().any(|edit| edit.span.start_byte == offset));
if !covered {
let before = source.get(..offset).unwrap_or_default();
let after = source.get(offset + name.len()..).unwrap_or_default();
sites.push(Site {
path: path.clone(),
offset,
line: before.matches('\n').count() + 1,
qualifies: after.starts_with("::") && !before.ends_with("::"),
renameable: !reference_definitions,
});
}
}
}
Ok(sites)
}
fn add_edit(
request: &Request<'_>,
changeset: &mut Vec<FileEdits>,
site: &Site,
name: &str,
new_name: &str,
) -> Result<(), HostlibError> {
let at = match changeset.iter().position(|file| file.path == site.path) {
Some(at) => at,
None => {
let source = read_source(BUILTIN, &request.state.root, &site.path, request.session_id)?;
changeset.push(FileEdits {
path: site.path.clone(),
source,
edits: Vec::new(),
});
changeset.len() - 1
}
};
let file = &mut changeset[at];
let edit = EditSpan {
span: span_at(&file.source, site.offset, site.offset + name.len()),
before: name.to_string(),
after: new_name.to_string(),
};
file.edits.push(edit);
file.edits.sort_by_key(|edit| edit.span.start_byte);
Ok(())
}
fn has_reference_definition(source: &str, name: &str) -> bool {
source
.lines()
.any(|line| is_reference_definition(line, name))
}
fn is_reference_definition(line: &str, name: &str) -> bool {
let doc = line.trim_start();
let body = doc
.strip_prefix("///")
.or_else(|| doc.strip_prefix("//!"))
.unwrap_or_default()
.trim_start();
body.starts_with('[') && body.contains("]:") && body.contains(name)
}
fn body_owner_is(source: &str, language: Language, offset: usize, name: &str) -> bool {
let Ok(tree) = ast_api::parse_tree(source, language) else {
return false;
};
let text = |node: tree_sitter::Node<'_>| node.utf8_text(source.as_bytes()).unwrap_or_default();
let mut node = tree.root_node().descendant_for_byte_range(offset, offset);
while let Some(current) = node {
let owner = match current.kind() {
"impl_item" => current
.child_by_field_name("type")
.map(|ty| ty.child_by_field_name("type").unwrap_or(ty)),
"enum_item" | "trait_item" => current.child_by_field_name("name"),
_ => None,
};
if let Some(owner) = owner {
return text(owner) == name;
}
node = current.parent();
}
false
}
fn type_declarations(source: &str, language: Language, name: &str, modules: &[String]) -> usize {
let Ok(tree) = ast_api::parse_tree(source, language) else {
return 0;
};
let mut count = 0;
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
let declares = matches!(
node.kind(),
"struct_item" | "enum_item" | "union_item" | "trait_item" | "type_item"
) && node
.child_by_field_name("name")
.is_some_and(|ident| ident.utf8_text(source.as_bytes()) == Ok(name));
count +=
usize::from(declares && module_path(source, language, node.start_byte()) == modules);
let mut cursor = node.walk();
stack.extend(node.children(&mut cursor));
}
count
}
fn module_path(source: &str, language: Language, offset: usize) -> Vec<String> {
let Ok(tree) = ast_api::parse_tree(source, language) else {
return Vec::new();
};
let mut names = Vec::new();
let mut node = tree.root_node().descendant_for_byte_range(offset, offset);
while let Some(current) = node {
if current.kind() == "mod_item" {
if let Some(name) = current.child_by_field_name("name") {
names.push(
name.utf8_text(source.as_bytes())
.unwrap_or_default()
.to_string(),
);
}
}
node = current.parent();
}
names.reverse();
names
}
fn declaration_offset(source: &str, language: Language, name: &str, line: u32) -> Option<usize> {
let kinds = language.rename_identifier_kinds()?;
let tree = ast_api::parse_tree(source, language).ok()?;
let mut targets = Vec::new();
collect_identifier_spans(
tree.root_node(),
source.as_bytes(),
name,
"",
kinds,
"",
&mut targets,
&mut Vec::new(),
);
targets
.into_iter()
.filter(|span| span.start_row + 1 == line as usize)
.map(|span| span.start_byte)
.min()
}