mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
feat: support {,prepare}rename api
This commit is contained in:
parent
964def25a9
commit
720c355a68
13 changed files with 654 additions and 63 deletions
89
crates/tinymist-query/src/analysis/definition.rs
Normal file
89
crates/tinymist-query/src/analysis/definition.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use log::trace;
|
||||
use typst::{
|
||||
foundations::{Func, Value},
|
||||
syntax::{
|
||||
ast::{self, AstNode},
|
||||
LinkedNode,
|
||||
},
|
||||
};
|
||||
use typst_ts_compiler::TypstSystemWorld;
|
||||
|
||||
use crate::{prelude::analyze_expr, TypstSpan};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FuncDefinition<'a> {
|
||||
pub value: Func,
|
||||
pub use_site: LinkedNode<'a>,
|
||||
pub span: TypstSpan,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Definition<'a> {
|
||||
Func(FuncDefinition<'a>),
|
||||
}
|
||||
|
||||
// todo: field definition
|
||||
pub(crate) fn find_definition<'a>(
|
||||
world: &TypstSystemWorld,
|
||||
node: LinkedNode<'a>,
|
||||
) -> Option<Definition<'a>> {
|
||||
let mut ancestor = &node;
|
||||
while !ancestor.is::<ast::Expr>() {
|
||||
ancestor = ancestor.parent()?;
|
||||
}
|
||||
|
||||
let may_ident = ancestor.cast::<ast::Expr>()?;
|
||||
if !may_ident.hash() && !matches!(may_ident, ast::Expr::MathIdent(_)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut is_ident_only = false;
|
||||
trace!("got ast_node kind {kind:?}", kind = ancestor.kind());
|
||||
let callee_node = match may_ident {
|
||||
// todo: label, reference
|
||||
// todo: import
|
||||
// todo: include
|
||||
ast::Expr::FuncCall(call) => call.callee(),
|
||||
ast::Expr::Set(set) => set.target(),
|
||||
ast::Expr::Ident(..) | ast::Expr::MathIdent(..) | ast::Expr::FieldAccess(..) => {
|
||||
is_ident_only = true;
|
||||
may_ident
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
trace!("got callee_node {callee_node:?} {is_ident_only:?}");
|
||||
|
||||
let use_site = if is_ident_only {
|
||||
ancestor.clone()
|
||||
} else {
|
||||
ancestor.find(callee_node.span())?
|
||||
};
|
||||
|
||||
let values = analyze_expr(world, &use_site);
|
||||
|
||||
let func_or_module = values.into_iter().find_map(|v| match &v {
|
||||
Value::Args(a) => {
|
||||
trace!("got args {a:?}");
|
||||
None
|
||||
}
|
||||
Value::Func(..) | Value::Module(..) => Some(v),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
Some(match func_or_module {
|
||||
Some(Value::Func(f)) => Definition::Func(FuncDefinition {
|
||||
value: f.clone(),
|
||||
span: f.span(),
|
||||
use_site,
|
||||
}),
|
||||
Some(Value::Module(m)) => {
|
||||
trace!("find module. {m:?}");
|
||||
// todo
|
||||
return None;
|
||||
}
|
||||
_ => {
|
||||
trace!("find value by lexical result. {use_site:?}");
|
||||
return None;
|
||||
}
|
||||
})
|
||||
}
|
71
crates/tinymist-query/src/analysis/import.rs
Normal file
71
crates/tinymist-query/src/analysis/import.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use std::path::Path;
|
||||
|
||||
use log::debug;
|
||||
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind, VirtualPath};
|
||||
use typst_ts_core::{typst::prelude::EcoVec, TypstFileId};
|
||||
|
||||
pub fn find_imports(
|
||||
source: &Source,
|
||||
def_id: Option<TypstFileId>,
|
||||
) -> EcoVec<(VirtualPath, LinkedNode<'_>)> {
|
||||
let root = LinkedNode::new(source.root());
|
||||
if let Some(def_id) = def_id.as_ref() {
|
||||
debug!("find imports for {def_id:?}");
|
||||
}
|
||||
|
||||
struct ImportWorker<'a> {
|
||||
current: TypstFileId,
|
||||
def_id: Option<TypstFileId>,
|
||||
imports: EcoVec<(VirtualPath, LinkedNode<'a>)>,
|
||||
}
|
||||
|
||||
impl<'a> ImportWorker<'a> {
|
||||
fn analyze(&mut self, node: LinkedNode<'a>) -> Option<()> {
|
||||
match node.kind() {
|
||||
SyntaxKind::ModuleImport => {
|
||||
let i = node.cast::<ast::ModuleImport>().unwrap();
|
||||
let src = i.source();
|
||||
match src {
|
||||
ast::Expr::Str(s) => {
|
||||
let s = s.get();
|
||||
let path = Path::new(s.as_str());
|
||||
let vpath = if path.is_relative() {
|
||||
self.current.vpath().join(path)
|
||||
} else {
|
||||
VirtualPath::new(path)
|
||||
};
|
||||
debug!("found import {vpath:?}");
|
||||
|
||||
if self.def_id.is_some_and(|e| e.vpath() != &vpath) {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.imports.push((vpath, node));
|
||||
}
|
||||
// todo: handle dynamic import
|
||||
ast::Expr::FieldAccess(..) | ast::Expr::Ident(..) => {}
|
||||
_ => {}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
SyntaxKind::ModuleInclude => {}
|
||||
_ => {}
|
||||
}
|
||||
for child in node.children() {
|
||||
self.analyze(child);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
let mut worker = ImportWorker {
|
||||
current: source.id(),
|
||||
def_id,
|
||||
imports: EcoVec::new(),
|
||||
};
|
||||
|
||||
worker.analyze(root);
|
||||
|
||||
worker.imports
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
pub mod track_values;
|
||||
pub use track_values::*;
|
||||
|
||||
pub mod lexical_hierarchy;
|
||||
pub(crate) use lexical_hierarchy::*;
|
||||
pub mod definition;
|
||||
pub use definition::*;
|
||||
pub mod import;
|
||||
pub use import::*;
|
||||
pub mod reference;
|
||||
pub use reference::*;
|
||||
|
|
74
crates/tinymist-query/src/analysis/reference.rs
Normal file
74
crates/tinymist-query/src/analysis/reference.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use typst::syntax::{
|
||||
ast::{self, AstNode},
|
||||
LinkedNode, SyntaxKind,
|
||||
};
|
||||
use typst_ts_core::typst::prelude::{eco_vec, EcoVec};
|
||||
|
||||
pub fn find_lexical_references_after<'a, 'b: 'a>(
|
||||
parent: LinkedNode<'a>,
|
||||
node: LinkedNode<'a>,
|
||||
target: &'b str,
|
||||
) -> EcoVec<LinkedNode<'a>> {
|
||||
let mut worker = Worker {
|
||||
idents: eco_vec![],
|
||||
target,
|
||||
};
|
||||
worker.analyze_after(parent, node);
|
||||
|
||||
worker.idents
|
||||
}
|
||||
|
||||
struct Worker<'a> {
|
||||
target: &'a str,
|
||||
idents: EcoVec<LinkedNode<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Worker<'a> {
|
||||
fn analyze_after(&mut self, parent: LinkedNode<'a>, node: LinkedNode<'a>) -> Option<()> {
|
||||
let mut after_node = false;
|
||||
|
||||
for child in parent.children() {
|
||||
if child.offset() > node.offset() {
|
||||
after_node = true;
|
||||
}
|
||||
if after_node {
|
||||
self.analyze(child);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn analyze(&mut self, node: LinkedNode<'a>) -> Option<()> {
|
||||
match node.kind() {
|
||||
SyntaxKind::LetBinding => {
|
||||
let lb = node.cast::<ast::LetBinding>().unwrap();
|
||||
let name = lb.kind().idents();
|
||||
for n in name {
|
||||
if n.get() == self.target {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(init) = lb.init() {
|
||||
let init_expr = node.find(init.span())?;
|
||||
self.analyze(init_expr);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
// todo: analyze import effect
|
||||
SyntaxKind::Import => {}
|
||||
SyntaxKind::Ident | SyntaxKind::MathIdent => {
|
||||
if self.target == node.text() {
|
||||
self.idents.push(node.clone());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for child in node.children() {
|
||||
self.analyze(child);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
use log::debug;
|
||||
use tower_lsp::lsp_types::LocationLink;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
analysis::{find_definition, Definition},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GotoDefinitionRequest {
|
||||
|
@ -20,61 +23,10 @@ impl GotoDefinitionRequest {
|
|||
|
||||
let ast_node = LinkedNode::new(source.root()).leaf_at(typst_offset)?;
|
||||
|
||||
let mut ancestor = &ast_node;
|
||||
while !ancestor.is::<ast::Expr>() {
|
||||
ancestor = ancestor.parent()?;
|
||||
}
|
||||
let Definition::Func(func) = find_definition(world, ast_node)?;
|
||||
|
||||
let may_ident = ancestor.cast::<ast::Expr>()?;
|
||||
if !may_ident.hash() && !matches!(may_ident, ast::Expr::MathIdent(_)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut is_ident_only = false;
|
||||
trace!("got ast_node kind {kind:?}", kind = ancestor.kind());
|
||||
let callee_node = match may_ident {
|
||||
// todo: label, reference
|
||||
// todo: import
|
||||
// todo: include
|
||||
ast::Expr::FuncCall(call) => call.callee(),
|
||||
ast::Expr::Set(set) => set.target(),
|
||||
ast::Expr::Ident(..) | ast::Expr::MathIdent(..) | ast::Expr::FieldAccess(..) => {
|
||||
is_ident_only = true;
|
||||
may_ident
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
trace!("got callee_node {callee_node:?} {is_ident_only:?}");
|
||||
|
||||
let callee_link = if is_ident_only {
|
||||
ancestor.clone()
|
||||
} else {
|
||||
ancestor.find(callee_node.span())?
|
||||
};
|
||||
|
||||
let values = analyze_expr(world, &callee_link);
|
||||
|
||||
let func_or_module = values.into_iter().find_map(|v| match &v {
|
||||
Value::Args(a) => {
|
||||
trace!("got args {a:?}");
|
||||
None
|
||||
}
|
||||
Value::Func(..) | Value::Module(..) => Some(v),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let span = match func_or_module {
|
||||
Some(Value::Func(f)) => f.span(),
|
||||
Some(Value::Module(m)) => {
|
||||
trace!("find module. {m:?}");
|
||||
// todo
|
||||
return None;
|
||||
}
|
||||
_ => {
|
||||
trace!("find value by lexical result. {callee_link:?}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let span = func.span;
|
||||
let callee_link = func.use_site;
|
||||
|
||||
if span.is_detached() {
|
||||
return None;
|
||||
|
|
|
@ -27,6 +27,10 @@ pub(crate) mod goto_definition;
|
|||
pub use goto_definition::*;
|
||||
pub(crate) mod inlay_hint;
|
||||
pub use inlay_hint::*;
|
||||
pub(crate) mod prepare_rename;
|
||||
pub use prepare_rename::*;
|
||||
pub(crate) mod rename;
|
||||
pub use rename::*;
|
||||
|
||||
pub mod lsp_typst_boundary;
|
||||
pub use lsp_typst_boundary::*;
|
||||
|
@ -58,6 +62,8 @@ mod polymorphic {
|
|||
InlayHint(InlayHintRequest),
|
||||
Completion(CompletionRequest),
|
||||
SignatureHelp(SignatureHelpRequest),
|
||||
Rename(RenameRequest),
|
||||
PrepareRename(PrepareRenameRequest),
|
||||
DocumentSymbol(DocumentSymbolRequest),
|
||||
Symbol(SymbolRequest),
|
||||
SemanticTokensFull(SemanticTokensFullRequest),
|
||||
|
@ -76,6 +82,8 @@ mod polymorphic {
|
|||
CompilerQueryRequest::InlayHint(..) => Unique,
|
||||
CompilerQueryRequest::Completion(..) => Mergable,
|
||||
CompilerQueryRequest::SignatureHelp(..) => PinnedFirst,
|
||||
CompilerQueryRequest::Rename(..) => Mergable,
|
||||
CompilerQueryRequest::PrepareRename(..) => Mergable,
|
||||
CompilerQueryRequest::DocumentSymbol(..) => ContextFreeUnique,
|
||||
CompilerQueryRequest::Symbol(..) => Mergable,
|
||||
CompilerQueryRequest::SemanticTokensFull(..) => ContextFreeUnique,
|
||||
|
@ -93,6 +101,8 @@ mod polymorphic {
|
|||
CompilerQueryRequest::InlayHint(req) => &req.path,
|
||||
CompilerQueryRequest::Completion(req) => &req.path,
|
||||
CompilerQueryRequest::SignatureHelp(req) => &req.path,
|
||||
CompilerQueryRequest::Rename(req) => &req.path,
|
||||
CompilerQueryRequest::PrepareRename(req) => &req.path,
|
||||
CompilerQueryRequest::DocumentSymbol(req) => &req.path,
|
||||
CompilerQueryRequest::Symbol(..) => return None,
|
||||
CompilerQueryRequest::SemanticTokensFull(req) => &req.path,
|
||||
|
@ -111,6 +121,8 @@ mod polymorphic {
|
|||
InlayHint(Option<Vec<InlayHint>>),
|
||||
Completion(Option<CompletionResponse>),
|
||||
SignatureHelp(Option<SignatureHelp>),
|
||||
PrepareRename(Option<PrepareRenameResponse>),
|
||||
Rename(Option<WorkspaceEdit>),
|
||||
DocumentSymbol(Option<DocumentSymbolResponse>),
|
||||
Symbol(Option<Vec<SymbolInformation>>),
|
||||
SemanticTokensFull(Option<SemanticTokensResult>),
|
||||
|
|
|
@ -10,9 +10,9 @@ pub use log::{error, trace};
|
|||
pub use tower_lsp::lsp_types::{
|
||||
CompletionResponse, DiagnosticRelatedInformation, DocumentSymbol, DocumentSymbolResponse,
|
||||
Documentation, FoldingRange, GotoDefinitionResponse, Hover, InlayHint, Location as LspLocation,
|
||||
MarkupContent, MarkupKind, Position as LspPosition, SelectionRange, SemanticTokens,
|
||||
SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult, SignatureHelp,
|
||||
SignatureInformation, SymbolInformation, Url,
|
||||
MarkupContent, MarkupKind, Position as LspPosition, PrepareRenameResponse, SelectionRange,
|
||||
SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult,
|
||||
SignatureHelp, SignatureInformation, SymbolInformation, Url, WorkspaceEdit,
|
||||
};
|
||||
pub use typst::diag::{EcoString, FileError, FileResult, Tracepoint};
|
||||
pub use typst::foundations::{Func, ParamInfo, Value};
|
||||
|
|
68
crates/tinymist-query/src/prepare_rename.rs
Normal file
68
crates/tinymist-query/src/prepare_rename.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use log::debug;
|
||||
|
||||
use crate::{
|
||||
analysis::{find_definition, Definition},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrepareRenameRequest {
|
||||
pub path: PathBuf,
|
||||
pub position: LspPosition,
|
||||
}
|
||||
|
||||
impl PrepareRenameRequest {
|
||||
/// See <https://github.com/microsoft/vscode-go/issues/2714>.
|
||||
/// The prepareRename feature is sent before a rename request. If the user
|
||||
/// is trying to rename a symbol that should not be renamed (inside a
|
||||
/// string or comment, on a builtin identifier, etc.), VSCode won't even
|
||||
/// show the rename pop-up.
|
||||
pub fn request(
|
||||
self,
|
||||
world: &TypstSystemWorld,
|
||||
position_encoding: PositionEncoding,
|
||||
) -> Option<PrepareRenameResponse> {
|
||||
let source = get_suitable_source_in_workspace(world, &self.path).ok()?;
|
||||
let typst_offset = lsp_to_typst::position(self.position, position_encoding, &source)?;
|
||||
|
||||
let ast_node = LinkedNode::new(source.root()).leaf_at(typst_offset)?;
|
||||
|
||||
let Definition::Func(func) = find_definition(world, ast_node)?;
|
||||
|
||||
use typst::foundations::func::Repr;
|
||||
let mut f = func.value.clone();
|
||||
loop {
|
||||
match f.inner() {
|
||||
// native functions can't be renamed
|
||||
Repr::Native(..) | Repr::Element(..) => return None,
|
||||
// todo: rename with site
|
||||
Repr::With(w) => f = w.0.clone(),
|
||||
Repr::Closure(..) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// todo: unwrap parentheses
|
||||
let ident = match func.use_site.kind() {
|
||||
SyntaxKind::Ident | SyntaxKind::MathIdent => func.use_site.text(),
|
||||
_ => return None,
|
||||
};
|
||||
debug!("prepare_rename: {ident}");
|
||||
|
||||
let id = func.span.id()?;
|
||||
if id.package().is_some() {
|
||||
debug!(
|
||||
"prepare_rename: {ident} is in a package {pkg:?}",
|
||||
pkg = id.package()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let origin_selection_range =
|
||||
typst_to_lsp::range(func.use_site.range(), &source, position_encoding);
|
||||
|
||||
Some(PrepareRenameResponse::RangeWithPlaceholder {
|
||||
range: origin_selection_range,
|
||||
placeholder: ident.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
307
crates/tinymist-query/src/rename.rs
Normal file
307
crates/tinymist-query/src/rename.rs
Normal file
|
@ -0,0 +1,307 @@
|
|||
use std::{collections::HashSet, os::windows::fs::FileTypeExt};
|
||||
|
||||
use log::{debug, warn};
|
||||
use tower_lsp::lsp_types::TextEdit;
|
||||
|
||||
use crate::{
|
||||
analysis::{find_definition, find_imports, find_lexical_references_after, Definition},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenameRequest {
|
||||
pub path: PathBuf,
|
||||
pub position: LspPosition,
|
||||
pub new_name: String,
|
||||
}
|
||||
|
||||
impl RenameRequest {
|
||||
pub fn request(
|
||||
self,
|
||||
world: &TypstSystemWorld,
|
||||
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 ast_node = LinkedNode::new(source.root()).leaf_at(typst_offset)?;
|
||||
|
||||
let Definition::Func(func) = find_definition(world, ast_node)?;
|
||||
|
||||
// todo: unwrap parentheses
|
||||
|
||||
let ident = match func.use_site.kind() {
|
||||
SyntaxKind::Ident | SyntaxKind::MathIdent => func.use_site.text(),
|
||||
_ => return None,
|
||||
};
|
||||
debug!("prepare_rename: {ident}");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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().idents();
|
||||
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,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 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() && !de.file_type().is_symlink_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;
|
||||
};
|
||||
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