diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index f25e40c6b..a2791e071 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -1,5 +1,5 @@ use std::cmp::Reverse; -use std::collections::{BTreeSet, HashSet}; +use std::collections::{BTreeMap, HashSet}; use std::ffi::OsStr; use ecow::{eco_format, EcoString}; @@ -9,6 +9,7 @@ use typst::foundations::{ fields_on, repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr, StyleChain, Styles, Type, Value, }; +use typst::layout::{Alignment, Dir}; use typst::model::Document; use typst::syntax::ast::AstNode; use typst::syntax::{ @@ -19,7 +20,9 @@ use typst::text::RawElem; use typst::visualize::Color; use unscanny::Scanner; -use crate::utils::{globals, plain_docs_sentence, summarize_font_family}; +use crate::utils::{ + check_value_recursively, globals, plain_docs_sentence, summarize_font_family, +}; use crate::{analyze_expr, analyze_import, analyze_labels, named_items, IdeWorld}; /// Autocomplete a cursor position in a source file. @@ -903,9 +906,18 @@ fn complete_code(ctx: &mut CompletionContext) -> bool { /// Add completions for expression snippets. #[rustfmt::skip] fn code_completions(ctx: &mut CompletionContext, hash: bool) { - ctx.scope_completions(true, |value| !hash || { - matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)) - }); + if hash { + ctx.scope_completions(true, |value| { + // If we are in markup, ignore colors, directions, and alignments. + // They are useless and bloat the autocomplete results. + let ty = value.ty(); + ty != Type::of::() + && ty != Type::of::() + && ty != Type::of::() + }); + } else { + ctx.scope_completions(true, |_| true); + } ctx.snippet_completion( "function call", @@ -1421,30 +1433,40 @@ 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(); - named_items(self.world, self.leaf.clone(), |name| { - if name.value().as_ref().map_or(true, &filter) { - defined.insert(name.name().clone()); + // When any of the constituent parts of the value matches the filter, + // that's ok as well. For example, when autocompleting `#rect(fill: |)`, + // we propose colors, but also dictionaries and modules that contain + // colors. + let filter = |value: &Value| check_value_recursively(value, &filter); + + let mut defined = BTreeMap::>::new(); + named_items(self.world, self.leaf.clone(), |item| { + let name = item.name(); + if !name.is_empty() && item.value().as_ref().map_or(true, filter) { + defined.insert(name.clone(), item.value()); } + None::<()> }); - for (name, value, _) in globals(self.world, self.leaf).iter() { - if filter(value) && !defined.contains(name) { - self.value_completion_full(Some(name.clone()), value, parens, None, None); - } - } - - for name in defined { - if !name.is_empty() { + for (name, value) in &defined { + if let Some(value) = value { + self.value_completion(name.clone(), value); + } else { self.completions.push(Completion { kind: CompletionKind::Constant, - label: name, + label: name.clone(), apply: None, detail: None, }); } } + + for (name, value, _) in globals(self.world, self.leaf).iter() { + if filter(value) && !defined.contains_key(name) { + self.value_completion_full(Some(name.clone()), value, parens, None, None); + } + } } } @@ -1667,6 +1689,21 @@ mod tests { .must_exclude(["string"]); } + /// Test that autocompletion for values of known type picks up nested + /// values. + #[test] + fn test_autocomplete_value_filter() { + let world = TestWorld::new("#import \"design.typ\": clrs; #rect(fill: )") + .with_source( + "design.typ", + "#let clrs = (a: red, b: blue); #let nums = (a: 1, b: 2)", + ); + + test_with_world(&world, -1) + .must_include(["clrs", "aqua"]) + .must_exclude(["nums", "a", "b"]); + } + #[test] fn test_autocomplete_packages() { test("#import \"@\"", -1).must_include([q!("@preview/example:0.1.0")]); diff --git a/crates/typst-ide/src/utils.rs b/crates/typst-ide/src/utils.rs index ad8ed6b50..cd66ec8f0 100644 --- a/crates/typst-ide/src/utils.rs +++ b/crates/typst-ide/src/utils.rs @@ -1,9 +1,10 @@ use std::fmt::Write; +use std::ops::ControlFlow; use comemo::Track; use ecow::{eco_format, EcoString}; use typst::engine::{Engine, Route, Sink, Traced}; -use typst::foundations::Scope; +use typst::foundations::{Scope, Value}; use typst::introspection::Introspector; use typst::syntax::{LinkedNode, SyntaxKind}; use typst::text::{FontInfo, FontStyle}; @@ -125,3 +126,66 @@ pub fn globals<'a>(world: &'a dyn IdeWorld, leaf: &LinkedNode) -> &'a Scope { library.global.scope() } } + +/// Checks whether the given value or any of its constituent parts satisfy the +/// predicate. +pub fn check_value_recursively( + value: &Value, + predicate: impl Fn(&Value) -> bool, +) -> bool { + let mut searcher = Searcher { steps: 0, predicate, max_steps: 1000 }; + match searcher.find(value) { + ControlFlow::Break(matching) => matching, + ControlFlow::Continue(_) => false, + } +} + +/// Recursively searches for a value that passes the filter, but without +/// exceeding a maximum number of search steps. +struct Searcher { + max_steps: usize, + steps: usize, + predicate: F, +} + +impl Searcher +where + F: Fn(&Value) -> bool, +{ + fn find(&mut self, value: &Value) -> ControlFlow { + if (self.predicate)(value) { + return ControlFlow::Break(true); + } + + if self.steps > self.max_steps { + return ControlFlow::Break(false); + } + + self.steps += 1; + + match value { + Value::Dict(dict) => { + self.find_iter(dict.iter().map(|(_, v)| v))?; + } + Value::Content(content) => { + self.find_iter(content.fields().iter().map(|(_, v)| v))?; + } + Value::Module(module) => { + self.find_iter(module.scope().iter().map(|(_, v, _)| v))?; + } + _ => {} + } + + ControlFlow::Continue(()) + } + + fn find_iter<'a>( + &mut self, + iter: impl Iterator, + ) -> ControlFlow { + for item in iter { + self.find(item)?; + } + ControlFlow::Continue(()) + } +}