mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 17:58:17 +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
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3441,6 +3441,7 @@ dependencies = [
|
||||||
"typst-ide",
|
"typst-ide",
|
||||||
"typst-ts-compiler",
|
"typst-ts-compiler",
|
||||||
"typst-ts-core",
|
"typst-ts-core",
|
||||||
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -22,6 +22,7 @@ log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
|
walkdir = "2"
|
||||||
|
|
||||||
typst.workspace = true
|
typst.workspace = true
|
||||||
typst-ide.workspace = true
|
typst-ide.workspace = true
|
||||||
|
|
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 mod track_values;
|
||||||
pub use track_values::*;
|
pub use track_values::*;
|
||||||
|
|
||||||
pub mod lexical_hierarchy;
|
pub mod lexical_hierarchy;
|
||||||
pub(crate) use 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 log::debug;
|
||||||
use tower_lsp::lsp_types::LocationLink;
|
use tower_lsp::lsp_types::LocationLink;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::{
|
||||||
|
analysis::{find_definition, Definition},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct GotoDefinitionRequest {
|
pub struct GotoDefinitionRequest {
|
||||||
|
@ -20,61 +23,10 @@ impl GotoDefinitionRequest {
|
||||||
|
|
||||||
let ast_node = LinkedNode::new(source.root()).leaf_at(typst_offset)?;
|
let ast_node = LinkedNode::new(source.root()).leaf_at(typst_offset)?;
|
||||||
|
|
||||||
let mut ancestor = &ast_node;
|
let Definition::Func(func) = find_definition(world, ast_node)?;
|
||||||
while !ancestor.is::<ast::Expr>() {
|
|
||||||
ancestor = ancestor.parent()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let may_ident = ancestor.cast::<ast::Expr>()?;
|
let span = func.span;
|
||||||
if !may_ident.hash() && !matches!(may_ident, ast::Expr::MathIdent(_)) {
|
let callee_link = func.use_site;
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if span.is_detached() {
|
if span.is_detached() {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
@ -27,6 +27,10 @@ pub(crate) mod goto_definition;
|
||||||
pub use goto_definition::*;
|
pub use goto_definition::*;
|
||||||
pub(crate) mod inlay_hint;
|
pub(crate) mod inlay_hint;
|
||||||
pub use 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 mod lsp_typst_boundary;
|
||||||
pub use lsp_typst_boundary::*;
|
pub use lsp_typst_boundary::*;
|
||||||
|
@ -58,6 +62,8 @@ mod polymorphic {
|
||||||
InlayHint(InlayHintRequest),
|
InlayHint(InlayHintRequest),
|
||||||
Completion(CompletionRequest),
|
Completion(CompletionRequest),
|
||||||
SignatureHelp(SignatureHelpRequest),
|
SignatureHelp(SignatureHelpRequest),
|
||||||
|
Rename(RenameRequest),
|
||||||
|
PrepareRename(PrepareRenameRequest),
|
||||||
DocumentSymbol(DocumentSymbolRequest),
|
DocumentSymbol(DocumentSymbolRequest),
|
||||||
Symbol(SymbolRequest),
|
Symbol(SymbolRequest),
|
||||||
SemanticTokensFull(SemanticTokensFullRequest),
|
SemanticTokensFull(SemanticTokensFullRequest),
|
||||||
|
@ -76,6 +82,8 @@ mod polymorphic {
|
||||||
CompilerQueryRequest::InlayHint(..) => Unique,
|
CompilerQueryRequest::InlayHint(..) => Unique,
|
||||||
CompilerQueryRequest::Completion(..) => Mergable,
|
CompilerQueryRequest::Completion(..) => Mergable,
|
||||||
CompilerQueryRequest::SignatureHelp(..) => PinnedFirst,
|
CompilerQueryRequest::SignatureHelp(..) => PinnedFirst,
|
||||||
|
CompilerQueryRequest::Rename(..) => Mergable,
|
||||||
|
CompilerQueryRequest::PrepareRename(..) => Mergable,
|
||||||
CompilerQueryRequest::DocumentSymbol(..) => ContextFreeUnique,
|
CompilerQueryRequest::DocumentSymbol(..) => ContextFreeUnique,
|
||||||
CompilerQueryRequest::Symbol(..) => Mergable,
|
CompilerQueryRequest::Symbol(..) => Mergable,
|
||||||
CompilerQueryRequest::SemanticTokensFull(..) => ContextFreeUnique,
|
CompilerQueryRequest::SemanticTokensFull(..) => ContextFreeUnique,
|
||||||
|
@ -93,6 +101,8 @@ mod polymorphic {
|
||||||
CompilerQueryRequest::InlayHint(req) => &req.path,
|
CompilerQueryRequest::InlayHint(req) => &req.path,
|
||||||
CompilerQueryRequest::Completion(req) => &req.path,
|
CompilerQueryRequest::Completion(req) => &req.path,
|
||||||
CompilerQueryRequest::SignatureHelp(req) => &req.path,
|
CompilerQueryRequest::SignatureHelp(req) => &req.path,
|
||||||
|
CompilerQueryRequest::Rename(req) => &req.path,
|
||||||
|
CompilerQueryRequest::PrepareRename(req) => &req.path,
|
||||||
CompilerQueryRequest::DocumentSymbol(req) => &req.path,
|
CompilerQueryRequest::DocumentSymbol(req) => &req.path,
|
||||||
CompilerQueryRequest::Symbol(..) => return None,
|
CompilerQueryRequest::Symbol(..) => return None,
|
||||||
CompilerQueryRequest::SemanticTokensFull(req) => &req.path,
|
CompilerQueryRequest::SemanticTokensFull(req) => &req.path,
|
||||||
|
@ -111,6 +121,8 @@ mod polymorphic {
|
||||||
InlayHint(Option<Vec<InlayHint>>),
|
InlayHint(Option<Vec<InlayHint>>),
|
||||||
Completion(Option<CompletionResponse>),
|
Completion(Option<CompletionResponse>),
|
||||||
SignatureHelp(Option<SignatureHelp>),
|
SignatureHelp(Option<SignatureHelp>),
|
||||||
|
PrepareRename(Option<PrepareRenameResponse>),
|
||||||
|
Rename(Option<WorkspaceEdit>),
|
||||||
DocumentSymbol(Option<DocumentSymbolResponse>),
|
DocumentSymbol(Option<DocumentSymbolResponse>),
|
||||||
Symbol(Option<Vec<SymbolInformation>>),
|
Symbol(Option<Vec<SymbolInformation>>),
|
||||||
SemanticTokensFull(Option<SemanticTokensResult>),
|
SemanticTokensFull(Option<SemanticTokensResult>),
|
||||||
|
|
|
@ -10,9 +10,9 @@ pub use log::{error, trace};
|
||||||
pub use tower_lsp::lsp_types::{
|
pub use tower_lsp::lsp_types::{
|
||||||
CompletionResponse, DiagnosticRelatedInformation, DocumentSymbol, DocumentSymbolResponse,
|
CompletionResponse, DiagnosticRelatedInformation, DocumentSymbol, DocumentSymbolResponse,
|
||||||
Documentation, FoldingRange, GotoDefinitionResponse, Hover, InlayHint, Location as LspLocation,
|
Documentation, FoldingRange, GotoDefinitionResponse, Hover, InlayHint, Location as LspLocation,
|
||||||
MarkupContent, MarkupKind, Position as LspPosition, SelectionRange, SemanticTokens,
|
MarkupContent, MarkupKind, Position as LspPosition, PrepareRenameResponse, SelectionRange,
|
||||||
SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult, SignatureHelp,
|
SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult,
|
||||||
SignatureInformation, SymbolInformation, Url,
|
SignatureHelp, SignatureInformation, SymbolInformation, Url, WorkspaceEdit,
|
||||||
};
|
};
|
||||||
pub use typst::diag::{EcoString, FileError, FileResult, Tracepoint};
|
pub use typst::diag::{EcoString, FileError, FileResult, Tracepoint};
|
||||||
pub use typst::foundations::{Func, ParamInfo, Value};
|
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(())
|
||||||
|
}
|
|
@ -853,6 +853,8 @@ impl<H: CompilationHandle> CompileNode<H> {
|
||||||
InlayHint(req) => query_world!(self, InlayHint, req),
|
InlayHint(req) => query_world!(self, InlayHint, req),
|
||||||
Completion(req) => query_state!(self, Completion, req),
|
Completion(req) => query_state!(self, Completion, req),
|
||||||
SignatureHelp(req) => query_world!(self, SignatureHelp, req),
|
SignatureHelp(req) => query_world!(self, SignatureHelp, req),
|
||||||
|
Rename(req) => query_world!(self, Rename, req),
|
||||||
|
PrepareRename(req) => query_world!(self, PrepareRename, req),
|
||||||
Symbol(req) => query_world!(self, Symbol, req),
|
Symbol(req) => query_world!(self, Symbol, req),
|
||||||
FoldingRange(..)
|
FoldingRange(..)
|
||||||
| SelectionRange(..)
|
| SelectionRange(..)
|
||||||
|
|
|
@ -174,6 +174,12 @@ impl LanguageServer for TypstServer {
|
||||||
document_symbol_provider: Some(OneOf::Left(true)),
|
document_symbol_provider: Some(OneOf::Left(true)),
|
||||||
workspace_symbol_provider: Some(OneOf::Left(true)),
|
workspace_symbol_provider: Some(OneOf::Left(true)),
|
||||||
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
|
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
|
||||||
|
rename_provider: Some(OneOf::Right(RenameOptions {
|
||||||
|
prepare_provider: Some(true),
|
||||||
|
work_done_progress_options: WorkDoneProgressOptions {
|
||||||
|
work_done_progress: None,
|
||||||
|
},
|
||||||
|
})),
|
||||||
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
|
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
|
||||||
workspace: Some(WorkspaceServerCapabilities {
|
workspace: Some(WorkspaceServerCapabilities {
|
||||||
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
||||||
|
@ -386,15 +392,18 @@ impl LanguageServer for TypstServer {
|
||||||
run_query!(self.SignatureHelp(path, position))
|
run_query!(self.SignatureHelp(path, position))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rename(&self, _params: RenameParams) -> jsonrpc::Result<Option<WorkspaceEdit>> {
|
async fn rename(&self, params: RenameParams) -> jsonrpc::Result<Option<WorkspaceEdit>> {
|
||||||
Ok(None)
|
let (path, position) = as_path_pos(params.text_document_position);
|
||||||
|
let new_name = params.new_name;
|
||||||
|
run_query!(self.Rename(path, position, new_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare_rename(
|
async fn prepare_rename(
|
||||||
&self,
|
&self,
|
||||||
_params: TextDocumentPositionParams,
|
params: TextDocumentPositionParams,
|
||||||
) -> jsonrpc::Result<Option<PrepareRenameResponse>> {
|
) -> jsonrpc::Result<Option<PrepareRenameResponse>> {
|
||||||
Ok(None)
|
let (path, position) = as_path_pos(params);
|
||||||
|
run_query!(self.PrepareRename(path, position))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn symbol(
|
async fn symbol(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue