mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-22 12:34:39 +00:00
dev: reimplements definition analysis (#43)
* dev: reimplements definition based on def use analysis * dev: reimplements lsp apis based on new definition api * fix: most cases of references * fix: scope of params
This commit is contained in:
parent
5fa4f8f94f
commit
da7028f59c
48 changed files with 746 additions and 1110 deletions
|
|
@ -1,11 +1,11 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use log::{debug, warn};
|
||||
use log::debug;
|
||||
use lsp_types::TextEdit;
|
||||
|
||||
use crate::{
|
||||
analysis::{find_definition, find_imports, find_lexical_references_after, Definition},
|
||||
analysis::{get_def_use, get_deref_target},
|
||||
find_definition, find_references,
|
||||
prelude::*,
|
||||
validate_renaming_definition,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -18,295 +18,59 @@ pub struct RenameRequest {
|
|||
impl RenameRequest {
|
||||
pub fn request(
|
||||
self,
|
||||
world: &TypstSystemWorld,
|
||||
ctx: &mut AnalysisContext,
|
||||
position_encoding: PositionEncoding,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
let source = get_suitable_source_in_workspace(world, &self.path).ok()?;
|
||||
let typst_offset = lsp_to_typst::position(self.position, position_encoding, &source)?;
|
||||
let source = ctx.source_by_path(&self.path).ok()?;
|
||||
|
||||
let ast_node = LinkedNode::new(source.root()).leaf_at(typst_offset + 1)?;
|
||||
let offset = lsp_to_typst::position(self.position, position_encoding, &source)?;
|
||||
let cursor = offset + 1;
|
||||
|
||||
let t: &dyn World = world;
|
||||
let Definition::Func(func) = find_definition(t.track(), source.id(), ast_node)? else {
|
||||
// todo: handle other definitions
|
||||
return None;
|
||||
};
|
||||
let ast_node = LinkedNode::new(source.root()).leaf_at(cursor)?;
|
||||
debug!("ast_node: {ast_node:?}", ast_node = ast_node);
|
||||
|
||||
// todo: unwrap parentheses
|
||||
let deref_target = get_deref_target(ast_node)?;
|
||||
|
||||
let ident = match func.use_site.kind() {
|
||||
SyntaxKind::Ident | SyntaxKind::MathIdent => func.use_site.text(),
|
||||
_ => return None,
|
||||
};
|
||||
debug!("prepare_rename: {ident}");
|
||||
let lnk = find_definition(ctx, source.clone(), deref_target.clone())?;
|
||||
|
||||
let def_id = func.span.id()?;
|
||||
if def_id.package().is_some() {
|
||||
debug!(
|
||||
"prepare_rename: {ident} is in a package {pkg:?}",
|
||||
pkg = def_id.package()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
validate_renaming_definition(&lnk)?;
|
||||
|
||||
let def_use = get_def_use(ctx, source.clone())?;
|
||||
let references = find_references(ctx, def_use, deref_target, position_encoding)?;
|
||||
|
||||
let mut editions = HashMap::new();
|
||||
|
||||
let def_source = world.source(def_id).ok()?;
|
||||
let def_id = def_source.id();
|
||||
let def_path = world.path_for_id(def_id).ok()?;
|
||||
let def_node = def_source.find(func.span)?;
|
||||
let mut def_node = &def_node;
|
||||
loop {
|
||||
if def_node.kind() == SyntaxKind::LetBinding {
|
||||
break;
|
||||
let def_loc = {
|
||||
let def_source = ctx.source_by_id(lnk.fid).ok()?;
|
||||
|
||||
let span_path = ctx.world.path_for_id(lnk.fid).ok()?;
|
||||
let uri = Url::from_file_path(span_path).ok()?;
|
||||
|
||||
let Some(range) = lnk.name_range else {
|
||||
log::warn!("rename: no name range");
|
||||
return None;
|
||||
};
|
||||
|
||||
LspLocation {
|
||||
uri,
|
||||
range: typst_to_lsp::range(range, &def_source, position_encoding),
|
||||
}
|
||||
def_node = def_node.parent()?;
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"rename: def_node found: {def_node:?} in {path}",
|
||||
path = def_path.display()
|
||||
);
|
||||
|
||||
let def_func = def_node.cast::<ast::LetBinding>()?;
|
||||
let def_names = def_func.kind().bindings();
|
||||
if def_names.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let def_name = def_names.first().unwrap();
|
||||
let def_name_node = def_node.find(def_name.span())?;
|
||||
|
||||
// find after function definition
|
||||
let def_root = LinkedNode::new(def_source.root());
|
||||
let parent = def_node.parent().unwrap_or(&def_root).clone();
|
||||
let idents = find_lexical_references_after(parent, def_node.clone(), ident);
|
||||
debug!("rename: in file idents found: {idents:?}");
|
||||
|
||||
let def_uri = Url::from_file_path(def_path).unwrap();
|
||||
for i in (Some(def_name_node).into_iter()).chain(idents) {
|
||||
let range = typst_to_lsp::range(i.range(), &def_source, position_encoding);
|
||||
|
||||
editions.insert(
|
||||
def_uri.clone(),
|
||||
vec![TextEdit {
|
||||
range,
|
||||
new_text: self.new_name.clone(),
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
// check whether it is in a sub scope
|
||||
if is_rooted_definition(def_node) {
|
||||
let mut wq = WorkQueue::default();
|
||||
wq.push(def_id);
|
||||
while let Some(id) = wq.pop() {
|
||||
search_in_workspace(
|
||||
world,
|
||||
id,
|
||||
ident,
|
||||
&self.new_name,
|
||||
&mut editions,
|
||||
&mut wq,
|
||||
position_encoding,
|
||||
)?;
|
||||
}
|
||||
for i in (Some(def_loc).into_iter()).chain(references) {
|
||||
let uri = i.uri;
|
||||
let range = i.range;
|
||||
let edits = editions.entry(uri).or_insert_with(Vec::new);
|
||||
edits.push(TextEdit {
|
||||
range,
|
||||
new_text: self.new_name.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// todo: conflict analysis
|
||||
|
||||
Some(WorkspaceEdit {
|
||||
changes: Some(editions),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct WorkQueue {
|
||||
searched: HashSet<TypstFileId>,
|
||||
queue: Vec<TypstFileId>,
|
||||
}
|
||||
|
||||
impl WorkQueue {
|
||||
fn push(&mut self, id: TypstFileId) {
|
||||
if self.searched.contains(&id) {
|
||||
return;
|
||||
}
|
||||
self.searched.insert(id);
|
||||
self.queue.push(id);
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<TypstFileId> {
|
||||
let id = self.queue.pop()?;
|
||||
Some(id)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_rooted_definition(node: &LinkedNode) -> bool {
|
||||
// check whether it is in a sub scope
|
||||
let mut parent_has_block = false;
|
||||
let mut parent = node.parent();
|
||||
while let Some(p) = parent {
|
||||
if matches!(p.kind(), SyntaxKind::CodeBlock | SyntaxKind::ContentBlock) {
|
||||
parent_has_block = true;
|
||||
break;
|
||||
}
|
||||
parent = p.parent();
|
||||
}
|
||||
|
||||
!parent_has_block
|
||||
}
|
||||
|
||||
fn search_in_workspace(
|
||||
world: &TypstSystemWorld,
|
||||
def_id: TypstFileId,
|
||||
ident: &str,
|
||||
new_name: &str,
|
||||
editions: &mut HashMap<Url, Vec<TextEdit>>,
|
||||
wq: &mut WorkQueue,
|
||||
position_encoding: PositionEncoding,
|
||||
) -> Option<()> {
|
||||
for path in walkdir::WalkDir::new(world.root.clone())
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
{
|
||||
let Ok(de) = path else {
|
||||
continue;
|
||||
};
|
||||
if !de.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
if !de
|
||||
.path()
|
||||
.extension()
|
||||
.is_some_and(|e| e == "typ" || e == "typc")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(source) = get_suitable_source_in_workspace(world, de.path()) else {
|
||||
warn!("rename: failed to get source for {}", de.path().display());
|
||||
return None;
|
||||
};
|
||||
|
||||
let use_id = source.id();
|
||||
// todo: whether we can rename identifiers in packages?
|
||||
if use_id.package().is_some() || wq.searched.contains(&use_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// todo: find dynamically
|
||||
let mut res = vec![];
|
||||
|
||||
if def_id != use_id {
|
||||
// find import statement
|
||||
let imports = find_imports(&source, Some(def_id));
|
||||
debug!("rename: imports found: {imports:?}");
|
||||
|
||||
// todo: precise import analysis
|
||||
if imports.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let root = LinkedNode::new(source.root());
|
||||
|
||||
for i in imports {
|
||||
let stack_store = i.1.clone();
|
||||
let Some(import_node) = stack_store.cast::<ast::ModuleImport>() else {
|
||||
continue;
|
||||
};
|
||||
// todo: don't ignore import node
|
||||
if import_node.new_name().is_some() {
|
||||
continue;
|
||||
}
|
||||
let Some(imports) = import_node.imports() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut found = false;
|
||||
let mut found_ident = None;
|
||||
match imports {
|
||||
ast::Imports::Wildcard => found = true,
|
||||
ast::Imports::Items(items) => {
|
||||
for handle in items.iter() {
|
||||
match handle {
|
||||
ast::ImportItem::Simple(e) => {
|
||||
if e.get() == ident {
|
||||
found = true;
|
||||
found_ident = Some((e, false));
|
||||
break;
|
||||
}
|
||||
}
|
||||
ast::ImportItem::Renamed(e) => {
|
||||
let o = e.original_name();
|
||||
if o.get() == ident {
|
||||
found = true;
|
||||
found_ident = Some((o, true));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
continue;
|
||||
}
|
||||
debug!("rename: import ident found in {:?}", de.path().display());
|
||||
|
||||
let is_renamed = found_ident.as_ref().map(|e| e.1).unwrap_or(false);
|
||||
let found_ident = found_ident.map(|e| e.0);
|
||||
|
||||
if !is_renamed && is_rooted_definition(&i.1) {
|
||||
wq.push(use_id);
|
||||
debug!("rename: push {use_id:?} to work queue");
|
||||
}
|
||||
|
||||
let idents = if !is_renamed {
|
||||
let parent = i.1.parent().unwrap_or(&root).clone();
|
||||
Some(find_lexical_references_after(parent, i.1.clone(), ident))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
debug!("rename: idents found: {idents:?}");
|
||||
|
||||
let found_ident = found_ident.map(|found_ident| {
|
||||
let Some(found_ident) = i.1.find(found_ident.span()) else {
|
||||
warn!(
|
||||
"rename: found_ident not found: {found_ident:?} in {:?} in {}",
|
||||
i.1,
|
||||
de.path().display()
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(found_ident)
|
||||
});
|
||||
|
||||
// we do early return because there may be some unreliability during
|
||||
// analysis
|
||||
if found_ident.as_ref().is_some_and(Option::is_none) {
|
||||
return None;
|
||||
}
|
||||
let found_ident = found_ident.flatten();
|
||||
|
||||
for i in idents.into_iter().flatten().chain(found_ident.into_iter()) {
|
||||
let range = typst_to_lsp::range(i.range(), &source, position_encoding);
|
||||
|
||||
res.push(TextEdit {
|
||||
range,
|
||||
new_text: new_name.to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if !res.is_empty() {
|
||||
let use_path = world.path_for_id(use_id).unwrap();
|
||||
let uri = Url::from_file_path(use_path).unwrap();
|
||||
editions.insert(uri, res);
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue