Basic Definition Finder for IDE (#4309)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Myriad-Dreamin 2024-07-09 23:46:38 +08:00 committed by Laurenz
parent b0d6cb900c
commit 02d0128dde
7 changed files with 510 additions and 64 deletions

View File

@ -17,8 +17,10 @@ use typst::visualize::Color;
use typst::World;
use unscanny::Scanner;
use crate::analyze::{analyze_expr, analyze_import, analyze_labels};
use crate::{plain_docs_sentence, summarize_font_family};
use crate::{
analyze_expr, analyze_import, analyze_labels, named_items, plain_docs_sentence,
summarize_font_family,
};
/// Autocomplete a cursor position in a source file.
///
@ -1319,62 +1321,12 @@ impl<'a> CompletionContext<'a> {
/// Filters the global/math scope with the given filter.
fn scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) {
let mut defined = BTreeSet::new();
let mut ancestor = Some(self.leaf.clone());
while let Some(node) = &ancestor {
let mut sibling = Some(node.clone());
while let Some(node) = &sibling {
if let Some(v) = node.cast::<ast::LetBinding>() {
for ident in v.kind().bindings() {
defined.insert(ident.get().clone());
}
}
if let Some(v) = node.cast::<ast::ModuleImport>() {
let imports = v.imports();
match imports {
None | Some(ast::Imports::Wildcard) => {
if let Some(value) = node
.children()
.find(|child| child.is::<ast::Expr>())
.and_then(|source| analyze_import(self.world, &source))
{
if imports.is_none() {
defined.extend(value.name().map(Into::into));
} else if let Some(scope) = value.scope() {
for (name, _) in scope.iter() {
defined.insert(name.clone());
}
}
}
}
Some(ast::Imports::Items(items)) => {
for item in items.iter() {
defined.insert(item.bound_name().get().clone());
}
}
}
}
sibling = node.prev_sibling();
named_items(self.world, self.leaf.clone(), |name| {
if name.value().as_ref().map_or(true, &filter) {
defined.insert(name.name().clone());
}
if let Some(parent) = node.parent() {
if let Some(v) = parent.cast::<ast::ForLoop>() {
if node.prev_sibling_kind() != Some(SyntaxKind::In) {
let pattern = v.pattern();
for ident in pattern.bindings() {
defined.insert(ident.get().clone());
}
}
}
ancestor = Some(parent.clone());
continue;
}
break;
}
None::<()>
});
let in_math = matches!(
self.leaf.parent_kind(),

View File

@ -0,0 +1,209 @@
use ecow::EcoString;
use typst::foundations::{Label, Module, Selector, Value};
use typst::model::Document;
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, LinkedNode, Side, Source, Span, SyntaxKind};
use typst::World;
use crate::{analyze_import, deref_target, named_items, DerefTarget, NamedItem};
/// Find the definition of the item under the cursor.
///
/// Passing a `document` (from a previous compilation) is optional, but enhances
/// the definition search. Label definitions, for instance, are only generated
/// when the document is available.
pub fn definition(
world: &dyn World,
document: Option<&Document>,
source: &Source,
cursor: usize,
side: Side,
) -> Option<Definition> {
let root = LinkedNode::new(source.root());
let leaf = root.leaf_at(cursor, side)?;
let target = deref_target(leaf.clone())?;
let mut use_site = match target {
DerefTarget::VarAccess(node) | DerefTarget::Callee(node) => node,
DerefTarget::IncludePath(path) | DerefTarget::ImportPath(path) => {
let import_item =
analyze_import(world, &path).and_then(|v| v.cast::<Module>().ok())?;
return Some(Definition::module(&import_item, path.span(), Span::detached()));
}
DerefTarget::Ref(r) => {
let label = Label::new(r.cast::<ast::Ref>()?.target());
let sel = Selector::Label(label);
let elem = document?.introspector.query_first(&sel)?;
let span = elem.span();
return Some(Definition {
kind: DefinitionKind::Label,
name: label.as_str().into(),
value: Some(Value::Label(label)),
span,
name_span: Span::detached(),
});
}
DerefTarget::Label(..) | DerefTarget::Code(..) => {
return None;
}
};
let mut has_path = false;
while let Some(node) = use_site.cast::<ast::FieldAccess>() {
has_path = true;
use_site = use_site.find(node.target().span())?;
}
let name = use_site.cast::<ast::Ident>()?.get().clone();
let src = named_items(world, use_site, |item: NamedItem| {
if *item.name() != name {
return None;
}
match item {
NamedItem::Var(name) => {
let name_span = name.span();
let span = find_let_binding(source, name_span);
Some(Definition::item(name.get().clone(), span, name_span, None))
}
NamedItem::Fn(name) => {
let name_span = name.span();
let span = find_let_binding(source, name_span);
Some(
Definition::item(name.get().clone(), span, name_span, None)
.with_kind(DefinitionKind::Function),
)
}
NamedItem::Module(item, site) => Some(Definition::module(
item,
site.span(),
matches!(site.kind(), SyntaxKind::Ident)
.then_some(site.span())
.unwrap_or_else(Span::detached),
)),
NamedItem::Import(name, span, value) => Some(Definition::item(
name.clone(),
Span::detached(),
span,
value.cloned(),
)),
}
});
let src = src.or_else(|| {
let in_math = matches!(
leaf.parent_kind(),
Some(SyntaxKind::Equation)
| Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac)
| Some(SyntaxKind::MathAttach)
);
let library = world.library();
let scope = if in_math { library.math.scope() } else { library.global.scope() };
for (item_name, value) in scope.iter() {
if *item_name == name {
return Some(Definition::item(
name,
Span::detached(),
Span::detached(),
Some(value.clone()),
));
}
}
None
})?;
(!has_path).then_some(src)
}
/// A definition of some item.
#[derive(Debug, Clone)]
pub struct Definition {
/// The name of the definition.
pub name: EcoString,
/// The kind of the definition.
pub kind: DefinitionKind,
/// An instance of the definition, if available.
pub value: Option<Value>,
/// The source span of the entire definition. May be detached if unknown.
pub span: Span,
/// The span of the definition's name. May be detached if unknown.
pub name_span: Span,
}
impl Definition {
fn item(name: EcoString, span: Span, name_span: Span, value: Option<Value>) -> Self {
Self {
name,
kind: match value {
Some(Value::Func(_)) => DefinitionKind::Function,
_ => DefinitionKind::Variable,
},
value,
span,
name_span,
}
}
fn module(module: &Module, span: Span, name_span: Span) -> Self {
Definition {
name: module.name().clone(),
kind: DefinitionKind::Module,
value: Some(Value::Module(module.clone())),
span,
name_span,
}
}
fn with_kind(self, kind: DefinitionKind) -> Self {
Self { kind, ..self }
}
}
/// A kind of item that is definition.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum DefinitionKind {
/// ```plain
/// let foo;
/// ^^^^^^^^ span
/// ^^^ name_span
/// ```
Variable,
/// ```plain
/// let foo(it) = it;
/// ^^^^^^^^^^^^^^^^^ span
/// ^^^ name_span
/// ```
Function,
/// Case 1
/// ```plain
/// import "foo.typ": *
/// ^^^^^^^^^ span
/// name_span is detached
/// ```
///
/// Case 2
/// ```plain
/// import "foo.typ" as bar: *
/// span ^^^
/// name_span ^^^
/// ```
Module,
/// ```plain
/// <foo>
/// ^^^^^ span
/// name_span is detached
/// ```
Label,
}
fn find_let_binding(source: &Source, name_span: Span) -> Span {
let node = LinkedNode::new(source.root());
std::iter::successors(node.find(name_span).as_ref(), |n| n.parent())
.find(|n| matches!(n.kind(), SyntaxKind::LetBinding))
.map(|s| s.span())
.unwrap_or_else(Span::detached)
}

View File

@ -2,12 +2,16 @@
mod analyze;
mod complete;
mod definition;
mod jump;
mod matchers;
mod tooltip;
pub use self::analyze::analyze_labels;
pub use self::analyze::{analyze_expr, analyze_import, analyze_labels};
pub use self::complete::{autocomplete, Completion, CompletionKind};
pub use self::definition::{definition, Definition, DefinitionKind};
pub use self::jump::{jump_from_click, jump_from_cursor, Jump};
pub use self::matchers::{deref_target, named_items, DerefTarget, NamedItem};
pub use self::tooltip::{tooltip, Tooltip};
use std::fmt::Write;

View File

@ -0,0 +1,266 @@
use ecow::EcoString;
use typst::foundations::{Module, Value};
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode};
use typst::World;
use crate::analyze_import;
/// Find the named items starting from the given position.
pub fn named_items<T>(
world: &dyn World,
position: LinkedNode,
mut recv: impl FnMut(NamedItem) -> Option<T>,
) -> Option<T> {
let mut ancestor = Some(position);
while let Some(node) = &ancestor {
let mut sibling = Some(node.clone());
while let Some(node) = &sibling {
if let Some(v) = node.cast::<ast::LetBinding>() {
let kind = if matches!(v.kind(), ast::LetBindingKind::Closure(..)) {
NamedItem::Fn
} else {
NamedItem::Var
};
for ident in v.kind().bindings() {
if let Some(res) = recv(kind(ident)) {
return Some(res);
}
}
}
if let Some(v) = node.cast::<ast::ModuleImport>() {
let imports = v.imports();
let source = node
.children()
.find(|child| child.is::<ast::Expr>())
.and_then(|source: LinkedNode| {
Some((analyze_import(world, &source)?, source))
});
let source = source.as_ref();
// Seeing the module itself.
if let Some((value, source)) = source {
let site = match (imports, v.new_name()) {
// ```plain
// import "foo" as name;
// import "foo" as name: ..;
// ```
(_, Some(name)) => Some(name.to_untyped()),
// ```plain
// import "foo";
// ```
(None, None) => Some(source.get()),
// ```plain
// import "foo": ..;
// ```
(Some(..), None) => None,
};
if let Some((site, value)) =
site.zip(value.clone().cast::<Module>().ok())
{
if let Some(res) = recv(NamedItem::Module(&value, site)) {
return Some(res);
}
}
}
// Seeing the imported items.
match imports {
// ```plain
// import "foo";
// ```
None => {}
// ```plain
// import "foo": *;
// ```
Some(ast::Imports::Wildcard) => {
if let Some(scope) = source.and_then(|(value, _)| value.scope()) {
for (name, value) in scope.iter() {
let item = NamedItem::Import(
name,
Span::detached(),
Some(value),
);
if let Some(res) = recv(item) {
return Some(res);
}
}
}
}
// ```plain
// import "foo": items;
// ```
Some(ast::Imports::Items(items)) => {
for item in items.iter() {
let name = item.bound_name();
if let Some(res) =
recv(NamedItem::Import(name.get(), name.span(), None))
{
return Some(res);
}
}
}
}
}
sibling = node.prev_sibling();
}
if let Some(parent) = node.parent() {
if let Some(v) = parent.cast::<ast::ForLoop>() {
if node.prev_sibling_kind() != Some(SyntaxKind::In) {
let pattern = v.pattern();
for ident in pattern.bindings() {
if let Some(res) = recv(NamedItem::Var(ident)) {
return Some(res);
}
}
}
}
ancestor = Some(parent.clone());
continue;
}
break;
}
None
}
/// An item that is named.
pub enum NamedItem<'a> {
/// A variable item.
Var(ast::Ident<'a>),
/// A function item.
Fn(ast::Ident<'a>),
/// A (imported) module item.
Module(&'a Module, &'a SyntaxNode),
/// An imported item.
Import(&'a EcoString, Span, Option<&'a Value>),
}
impl<'a> NamedItem<'a> {
pub(crate) fn name(&self) -> &'a EcoString {
match self {
NamedItem::Var(ident) => ident.get(),
NamedItem::Fn(ident) => ident.get(),
NamedItem::Module(value, _) => value.name(),
NamedItem::Import(name, _, _) => name,
}
}
pub(crate) fn value(&self) -> Option<Value> {
match self {
NamedItem::Var(..) | NamedItem::Fn(..) => None,
NamedItem::Module(value, _) => Some(Value::Module((*value).clone())),
NamedItem::Import(_, _, value) => value.cloned(),
}
}
}
/// Categorize an expression into common classes IDE functionality can operate
/// on.
pub fn deref_target(node: LinkedNode) -> Option<DerefTarget<'_>> {
// Move to the first ancestor that is an expression.
let mut ancestor = node;
while !ancestor.is::<ast::Expr>() {
ancestor = ancestor.parent()?.clone();
}
// Identify convenient expression kinds.
let expr_node = ancestor;
let expr = expr_node.cast::<ast::Expr>()?;
Some(match expr {
ast::Expr::Label(..) => DerefTarget::Label(expr_node),
ast::Expr::Ref(..) => DerefTarget::Ref(expr_node),
ast::Expr::FuncCall(call) => {
DerefTarget::Callee(expr_node.find(call.callee().span())?)
}
ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?),
ast::Expr::Ident(..) | ast::Expr::MathIdent(..) | ast::Expr::FieldAccess(..) => {
DerefTarget::VarAccess(expr_node)
}
ast::Expr::Str(..) => {
let parent = expr_node.parent()?;
if parent.kind() == SyntaxKind::ModuleImport {
DerefTarget::ImportPath(expr_node)
} else if parent.kind() == SyntaxKind::ModuleInclude {
DerefTarget::IncludePath(expr_node)
} else {
DerefTarget::Code(expr_node.kind(), expr_node)
}
}
_ if expr.hash()
|| matches!(expr_node.kind(), SyntaxKind::MathIdent | SyntaxKind::Error) =>
{
DerefTarget::Code(expr_node.kind(), expr_node)
}
_ => return None,
})
}
/// Classes of expressions that can be operated on by IDE functionality.
#[derive(Debug, Clone)]
pub enum DerefTarget<'a> {
/// A label expression.
Label(LinkedNode<'a>),
/// A reference expression.
Ref(LinkedNode<'a>),
/// A variable access expression.
///
/// It can be either an identifier or a field access.
VarAccess(LinkedNode<'a>),
/// A function call expression.
Callee(LinkedNode<'a>),
/// An import path expression.
ImportPath(LinkedNode<'a>),
/// An include path expression.
IncludePath(LinkedNode<'a>),
/// Any code expression.
Code(SyntaxKind, LinkedNode<'a>),
}
#[cfg(test)]
mod tests {
use typst::syntax::{LinkedNode, Side};
use crate::{named_items, tests::TestWorld};
#[track_caller]
fn has_named_items(text: &str, cursor: usize, containing: &str) -> bool {
let world = TestWorld::new(text);
let src = world.main.clone();
let node = LinkedNode::new(src.root());
let leaf = node.leaf_at(cursor, Side::After).unwrap();
let res = named_items(&world, leaf, |s| {
if containing == s.name() {
return Some(true);
}
None
});
res.unwrap_or_default()
}
#[test]
fn test_simple_named_items() {
// Has named items
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "a"));
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 15, "a"));
// Doesn't have named items
assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b"));
}
#[test]
fn test_import_named_items() {
// Cannot test much.
assert!(has_named_items(r#"#import "foo.typ": a; #(a);"#, 24, "a"));
}
}

View File

@ -10,14 +10,13 @@ use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
use typst::util::{round_2, Numeric};
use typst::World;
use crate::analyze::{analyze_expr, analyze_labels};
use crate::{plain_docs_sentence, summarize_font_family};
use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family};
/// Describe the item under the cursor.
///
/// Passing a `document` (from a previous compilation) is optional, but enhances
/// the autocompletions. Label completions, for instance, are only generated
/// when the document is available.
/// the tooltips. Label tooltips, for instance, are only generated when the
/// document is available.
pub fn tooltip(
world: &dyn World,
document: Option<&Document>,

View File

@ -88,7 +88,7 @@ pub fn eval(
.unwrap_or_default()
.to_string_lossy();
Ok(Module::new(name, vm.scopes.top).with_content(output))
Ok(Module::new(name, vm.scopes.top).with_content(output).with_file_id(id))
}
/// Evaluate a string as code and return the resulting value.

View File

@ -5,6 +5,7 @@ use ecow::{eco_format, EcoString};
use crate::diag::StrResult;
use crate::foundations::{repr, ty, Content, Scope, Value};
use crate::syntax::FileId;
/// An evaluated module, either built-in or resulting from a file.
///
@ -43,6 +44,8 @@ struct Repr {
scope: Scope,
/// The module's layoutable contents.
content: Content,
/// The id of the file which defines the module, if any.
file_id: Option<FileId>,
}
impl Module {
@ -50,7 +53,7 @@ impl Module {
pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self {
Self {
name: name.into(),
inner: Arc::new(Repr { scope, content: Content::empty() }),
inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }),
}
}
@ -72,6 +75,12 @@ impl Module {
self
}
/// Update the module's file id.
pub fn with_file_id(mut self, file_id: FileId) -> Self {
Arc::make_mut(&mut self.inner).file_id = Some(file_id);
self
}
/// Get the module's name.
pub fn name(&self) -> &EcoString {
&self.name
@ -82,6 +91,13 @@ impl Module {
&self.inner.scope
}
/// Access the module's file id.
///
/// Some modules are not associated with a file, like the built-in modules.
pub fn file_id(&self) -> Option<FileId> {
self.inner.file_id
}
/// Access the module's scope, mutably.
pub fn scope_mut(&mut self) -> &mut Scope {
&mut Arc::make_mut(&mut self.inner).scope