mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Basic Definition Finder for IDE (#4309)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
parent
79b77d5669
commit
46ab4edea6
@ -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.
|
||||
///
|
||||
@ -1327,62 +1329,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(),
|
||||
|
264
crates/typst-ide/src/definition.rs
Normal file
264
crates/typst-ide/src/definition.rs
Normal file
@ -0,0 +1,264 @@
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Range;
|
||||
|
||||
use typst::foundations::{IntoValue, Label, NativeElement, Value};
|
||||
use typst::syntax::Side;
|
||||
use typst::WorldExt;
|
||||
|
||||
use super::{definition, DefinitionKind as Kind};
|
||||
use crate::tests::TestWorld;
|
||||
|
||||
#[track_caller]
|
||||
fn test<T>(
|
||||
text: &str,
|
||||
cursor: usize,
|
||||
name: &str,
|
||||
kind: Kind,
|
||||
value: Option<T>,
|
||||
range: Option<Range<usize>>,
|
||||
) where
|
||||
T: IntoValue,
|
||||
{
|
||||
let world = TestWorld::new(text);
|
||||
let doc = typst::compile(&world).output.ok();
|
||||
let actual = definition(&world, doc.as_ref(), &world.main, cursor, Side::After)
|
||||
.map(|d| (d.kind, d.name, world.range(d.span), d.value));
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some((kind, name.into(), range, value.map(IntoValue::into_value)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_definition() {
|
||||
test("#let x; #x", 9, "x", Kind::Variable, None::<Value>, Some(1..6));
|
||||
test("#let x() = {}; #x", 16, "x", Kind::Function, None::<Value>, Some(1..13));
|
||||
test(
|
||||
"#table",
|
||||
1,
|
||||
"table",
|
||||
Kind::Function,
|
||||
Some(typst::model::TableElem::elem()),
|
||||
None,
|
||||
);
|
||||
test(
|
||||
"#figure[] <hi> See @hi",
|
||||
21,
|
||||
"hi",
|
||||
Kind::Label,
|
||||
Some(Label::new("hi")),
|
||||
Some(1..9),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
266
crates/typst-ide/src/matchers.rs
Normal file
266
crates/typst-ide/src/matchers.rs
Normal 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"));
|
||||
}
|
||||
}
|
@ -11,14 +11,13 @@ use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
|
||||
use typst::utils::{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>,
|
||||
|
@ -86,7 +86,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.
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user