mirror of
https://github.com/erg-lang/erg.git
synced 2025-09-27 11:59:05 +00:00
363 lines
14 KiB
Rust
363 lines
14 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::thread::sleep;
|
|
use std::time::{Duration, SystemTime};
|
|
|
|
use serde::Deserialize;
|
|
use serde_json::json;
|
|
use serde_json::Value;
|
|
|
|
use lsp_types::{
|
|
DocumentChangeOperation, DocumentChanges, OneOf, OptionalVersionedTextDocumentIdentifier,
|
|
RenameFile, RenameFilesParams, RenameParams, ResourceOp, TextDocumentEdit, TextEdit, Url,
|
|
WorkspaceEdit,
|
|
};
|
|
|
|
use erg_common::dict::Dict;
|
|
use erg_common::pathutil::NormalizedPathBuf;
|
|
use erg_common::set::Set;
|
|
use erg_common::traits::{Locational, Stream};
|
|
|
|
use erg_compiler::artifact::BuildRunnable;
|
|
use erg_compiler::erg_parser::parse::Parsable;
|
|
use erg_compiler::hir::{Expr, Literal};
|
|
use erg_compiler::varinfo::{AbsLocation, VarKind};
|
|
|
|
#[allow(unused_imports)]
|
|
use crate::_log;
|
|
use crate::server::{ELSResult, RedirectableStdout, Server};
|
|
use crate::util::{self, NormalizedUrl};
|
|
|
|
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
|
|
pub(crate) fn rename(&mut self, msg: &Value) -> ELSResult<()> {
|
|
let params = RenameParams::deserialize(&msg["params"])?;
|
|
let id = msg["id"].as_i64().unwrap();
|
|
self.send_log(format!("rename request: {params:?}"))?;
|
|
let uri = NormalizedUrl::new(params.text_document_position.text_document.uri);
|
|
let pos = params.text_document_position.position;
|
|
if let Some(tok) = self.file_cache.get_symbol(&uri, pos) {
|
|
// _log!(self, "tok: {tok}");
|
|
if let Some(vi) = self
|
|
.get_visitor(&uri)
|
|
.and_then(|visitor| visitor.get_info(&tok))
|
|
{
|
|
let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
|
|
let is_std = vi
|
|
.def_loc
|
|
.module
|
|
.as_ref()
|
|
.map(|path| path.starts_with(&self.erg_path))
|
|
.unwrap_or(false);
|
|
let kind = if vi.t.is_method() {
|
|
"method"
|
|
} else if vi.t.is_subr() {
|
|
"subroutine"
|
|
} else {
|
|
"variable"
|
|
};
|
|
if vi.def_loc.loc.is_unknown() || is_std {
|
|
let error_reason = match vi.kind {
|
|
VarKind::Builtin => {
|
|
format!("this is a builtin {kind} and cannot be renamed")
|
|
}
|
|
VarKind::FixedAuto => {
|
|
format!("this is a fixed auto {kind} and cannot be renamed")
|
|
}
|
|
_ if is_std => {
|
|
"this is a standard library API and cannot be renamed".to_string()
|
|
}
|
|
_ => format!("this {kind} cannot be renamed"),
|
|
};
|
|
let edit = WorkspaceEdit::new(changes);
|
|
self.send_stdout(&json!({ "jsonrpc": "2.0", "id": id, "result": edit }))?;
|
|
return self.send_error_info(error_reason);
|
|
}
|
|
Self::commit_change(&mut changes, &vi.def_loc, params.new_name.clone());
|
|
if let Some(value) = self.shared.index.get_refs(&vi.def_loc) {
|
|
// self.send_log(format!("referrers: {referrers:?}"))?;
|
|
for referrer in value.referrers.iter() {
|
|
Self::commit_change(&mut changes, referrer, params.new_name.clone());
|
|
}
|
|
}
|
|
let dependencies = self.dependencies_of(&uri);
|
|
self.file_cache
|
|
.editing
|
|
.borrow_mut()
|
|
.extend(dependencies.clone());
|
|
for uri in changes.keys() {
|
|
self.clear_cache(&NormalizedUrl::new(uri.clone()));
|
|
}
|
|
let timestamps = self.get_timestamps(changes.keys());
|
|
let edit = WorkspaceEdit::new(changes);
|
|
self.send_stdout(&json!({ "jsonrpc": "2.0", "id": id, "result": edit }))?;
|
|
for _ in 0..20 {
|
|
self.send_log("waiting for file to be modified...")?;
|
|
if self.all_changed(×tamps) {
|
|
break;
|
|
}
|
|
sleep(Duration::from_millis(50));
|
|
}
|
|
let mut checked = Set::new();
|
|
// recheck dependencies and finally the file itself
|
|
for dep in dependencies {
|
|
let code = self.file_cache.get_entire_code(&dep)?.to_string();
|
|
self.check_file(dep.clone(), code, &mut checked)?;
|
|
self.file_cache.editing.borrow_mut().remove(&dep);
|
|
}
|
|
self.send_empty_diagnostics(checked)?;
|
|
// dependents are checked after changes are committed
|
|
return Ok(());
|
|
}
|
|
}
|
|
self.send_stdout(&json!({ "jsonrpc": "2.0", "id": id, "result": Value::Null }))
|
|
}
|
|
|
|
fn commit_change(
|
|
changes: &mut HashMap<Url, Vec<TextEdit>>,
|
|
abs_loc: &AbsLocation,
|
|
new_name: String,
|
|
) {
|
|
if let Some(path) = &abs_loc.module {
|
|
let Ok(def_uri) = Url::from_file_path(path) else {
|
|
return;
|
|
};
|
|
let Some(range) = util::loc_to_range(abs_loc.loc) else {
|
|
return;
|
|
};
|
|
let edit = TextEdit::new(range, new_name);
|
|
if let Some(edits) = changes.get_mut(&def_uri) {
|
|
edits.push(edit);
|
|
} else {
|
|
changes.insert(def_uri, vec![edit]);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_timestamps<'a, I: Iterator<Item = &'a Url>>(&self, urls: I) -> Dict<Url, SystemTime> {
|
|
urls.map(|url| {
|
|
let timestamp = util::get_metadata_from_uri(url)
|
|
.and_then(|md| Ok(md.modified()?))
|
|
.unwrap_or(SystemTime::now());
|
|
(url.clone(), timestamp)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn all_changed(&self, timestamps: &Dict<Url, SystemTime>) -> bool {
|
|
timestamps.iter().all(|(url, timestamp)| {
|
|
util::get_metadata_from_uri(url)
|
|
.and_then(|md| Ok(md.modified()? != *timestamp))
|
|
.unwrap_or(false)
|
|
})
|
|
}
|
|
|
|
/// self is __included__.
|
|
/// if self is not in the graph, return empty vec
|
|
pub fn dependencies_of(&self, uri: &NormalizedUrl) -> Vec<NormalizedUrl> {
|
|
let graph = &self.shared.graph;
|
|
let path = NormalizedPathBuf::from(util::uri_to_path(uri));
|
|
if let Err(err) = graph.sort() {
|
|
// maybe key not found == self is not in the graph
|
|
crate::_log!(self, "err: {err}");
|
|
return vec![];
|
|
};
|
|
let Some(self_node) = graph.get_node(&path) else {
|
|
return vec![];
|
|
};
|
|
graph
|
|
.ref_inner()
|
|
.iter()
|
|
.filter(|node| node.id == path || self_node.depends_on(&node.id))
|
|
.filter_map(|node| {
|
|
Some(NormalizedUrl::new(
|
|
Url::from_file_path(node.id.to_path_buf()).ok()?,
|
|
))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// self is __not included__
|
|
pub fn dependents_of(&self, uri: &NormalizedUrl) -> Vec<NormalizedUrl> {
|
|
let graph = &self.shared.graph;
|
|
let path = NormalizedPathBuf::from(util::uri_to_path(uri));
|
|
graph
|
|
.ref_inner()
|
|
.iter()
|
|
.filter(|node| node.depends_on(&path))
|
|
.filter_map(|node| {
|
|
Some(NormalizedUrl::new(
|
|
Url::from_file_path(node.id.to_path_buf()).ok()?,
|
|
))
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
|
|
fn collect_module_changes(
|
|
&mut self,
|
|
old_uri: &NormalizedUrl,
|
|
new_uri: &NormalizedUrl,
|
|
) -> HashMap<Url, Vec<TextEdit>> {
|
|
let mut changes = HashMap::new();
|
|
let old_path = util::uri_to_path(old_uri)
|
|
.file_stem()
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.to_string();
|
|
let old_path = old_path.trim_end_matches(".d");
|
|
let new_path = util::uri_to_path(new_uri)
|
|
.file_stem()
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.to_string();
|
|
if old_path.is_empty() || new_path.is_empty() {
|
|
return changes;
|
|
}
|
|
let new_path = new_path.trim_end_matches(".d");
|
|
for dep in self.dependents_of(old_uri) {
|
|
let imports = self.search_imports(&dep, old_path);
|
|
let edits = imports.iter().filter_map(|lit| {
|
|
Some(TextEdit::new(
|
|
util::loc_to_range(lit.loc())?,
|
|
lit.token.content.replace(old_path, new_path),
|
|
))
|
|
});
|
|
changes.insert(dep.raw(), edits.collect());
|
|
}
|
|
changes
|
|
}
|
|
|
|
/// TODO: multi-path imports
|
|
/// returning exprs: import symbol (string literal)
|
|
fn search_imports(&self, target: &NormalizedUrl, needle_module_name: &str) -> Vec<Literal> {
|
|
let mut imports = vec![];
|
|
if let Some(hir) = self.get_hir(target) {
|
|
for chunk in hir.module.iter() {
|
|
imports.extend(Self::extract_import_symbols(chunk, needle_module_name));
|
|
}
|
|
}
|
|
imports
|
|
}
|
|
|
|
fn extract_import_symbols(expr: &Expr, needle_module_name: &str) -> Vec<Literal> {
|
|
match expr {
|
|
Expr::Def(def) if def.def_kind().is_import() => {
|
|
let Some(Expr::Call(import_call)) = def.body.block.first() else {
|
|
return vec![];
|
|
};
|
|
let Some(module_name) = import_call.args.get_left_or_key("Path") else {
|
|
return vec![];
|
|
};
|
|
match module_name {
|
|
Expr::Literal(lit)
|
|
if lit
|
|
.token
|
|
.content
|
|
.trim_start_matches('\"')
|
|
.trim_end_matches('\"')
|
|
.ends_with(needle_module_name) =>
|
|
// FIXME: Possibly a submodule of the same name of another module
|
|
{
|
|
vec![lit.clone()]
|
|
}
|
|
_ => vec![],
|
|
}
|
|
}
|
|
_ => vec![],
|
|
}
|
|
}
|
|
|
|
fn rename_linked_files(
|
|
&self,
|
|
renames: &mut Vec<DocumentChangeOperation>,
|
|
old_uri: &NormalizedUrl,
|
|
new_uri: &NormalizedUrl,
|
|
) {
|
|
if old_uri.as_str().ends_with(".d.er") {
|
|
let Ok(old_uri) = Url::parse(&old_uri.as_str().replace(".d.er", ".py")) else {
|
|
return;
|
|
};
|
|
let Ok(new_uri) = Url::parse(&new_uri.as_str().replace(".d.er", ".py")) else {
|
|
return;
|
|
};
|
|
let rename = DocumentChangeOperation::Op(ResourceOp::Rename(RenameFile {
|
|
old_uri,
|
|
new_uri,
|
|
options: None,
|
|
annotation_id: None,
|
|
}));
|
|
renames.push(rename);
|
|
} else if old_uri.as_str().ends_with(".py") {
|
|
let d_er_file = PathBuf::from(old_uri.as_str().replace(".py", ".d.er"));
|
|
if d_er_file.exists() {
|
|
let Ok(old_uri) = Url::from_file_path(&d_er_file) else {
|
|
return;
|
|
};
|
|
let Ok(new_uri) = Url::parse(&new_uri.as_str().replace(".py", ".d.er")) else {
|
|
return;
|
|
};
|
|
let rename = DocumentChangeOperation::Op(ResourceOp::Rename(RenameFile {
|
|
old_uri,
|
|
new_uri,
|
|
options: None,
|
|
annotation_id: None,
|
|
}));
|
|
renames.push(rename);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rename .er files and rewrite the imports of the dependent files.
|
|
/// This does not update `file_cache`, the editing is done by a `didChange` request.
|
|
pub(crate) fn handle_will_rename_files(
|
|
&mut self,
|
|
params: RenameFilesParams,
|
|
) -> ELSResult<Option<WorkspaceEdit>> {
|
|
self.send_log("workspace/willRenameFiles request")?;
|
|
let mut edits = HashMap::new();
|
|
let mut renames = vec![];
|
|
for file in ¶ms.files {
|
|
let Ok(old) = Url::parse(&file.old_uri) else {
|
|
continue;
|
|
};
|
|
let old_uri = NormalizedUrl::new(old);
|
|
let Ok(new) = Url::parse(&file.new_uri) else {
|
|
continue;
|
|
};
|
|
let new_uri = NormalizedUrl::new(new);
|
|
edits.extend(self.collect_module_changes(&old_uri, &new_uri));
|
|
self.rename_linked_files(&mut renames, &old_uri, &new_uri);
|
|
let Some(entry) = self.remove_module_entry(&old_uri) else {
|
|
continue;
|
|
};
|
|
self.insert_module_entry(new_uri.clone(), entry);
|
|
let Some(entry) = self.steal_entry(&old_uri) else {
|
|
continue;
|
|
};
|
|
self.shared.rename_path(
|
|
&old_uri.to_file_path().unwrap().into(),
|
|
new_uri.to_file_path().unwrap().into(),
|
|
);
|
|
self.restore_entry(new_uri, entry);
|
|
}
|
|
self.file_cache.rename_files(¶ms)?;
|
|
let changes = {
|
|
let edits = edits.into_iter().map(|(uri, edits)| {
|
|
let text_document = OptionalVersionedTextDocumentIdentifier { uri, version: None };
|
|
let edit = TextDocumentEdit {
|
|
text_document,
|
|
edits: edits.into_iter().map(OneOf::Left).collect(),
|
|
};
|
|
DocumentChangeOperation::Edit(edit)
|
|
});
|
|
let ops = edits.chain(renames).collect();
|
|
DocumentChanges::Operations(ops)
|
|
};
|
|
let edit = WorkspaceEdit {
|
|
document_changes: Some(changes),
|
|
..Default::default()
|
|
};
|
|
Ok(Some(edit))
|
|
}
|
|
}
|