feat: support {,prepare}rename api

This commit is contained in:
Myriad-Dreamin 2024-03-09 00:59:39 +08:00
parent 964def25a9
commit 720c355a68
13 changed files with 654 additions and 63 deletions

View 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;
}
})
}

View 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
}

View file

@ -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::*;

View 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
}
}

View file

@ -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;

View file

@ -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>),

View file

@ -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};

View 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(),
})
}
}

View 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(())
}