summaryrefslogtreecommitdiff
path: root/crates/typst-ide
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-11-12 15:29:26 +0100
committerLaurenz <laurmaedje@gmail.com>2024-11-13 10:21:40 +0100
commit7add9b459a3ca54fca085e71f3dd4e611941c4cc (patch)
treefe4261b5ecf601fe17de9cc948be70ad39a52d41 /crates/typst-ide
parentde59d64d103c7b2c17f6ec6d51fadbbaaab54a40 (diff)
Smarter filtering of scope completions
Diffstat (limited to 'crates/typst-ide')
-rw-r--r--crates/typst-ide/src/complete.rs73
-rw-r--r--crates/typst-ide/src/utils.rs66
2 files changed, 120 insertions, 19 deletions
diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs
index f25e40c6..a2791e07 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::<Color>()
+ && ty != Type::of::<Dir>()
+ && ty != Type::of::<Alignment>()
+ });
+ } 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::<EcoString, Option<Value>>::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 ad8ed6b5..cd66ec8f 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<F> {
+ max_steps: usize,
+ steps: usize,
+ predicate: F,
+}
+
+impl<F> Searcher<F>
+where
+ F: Fn(&Value) -> bool,
+{
+ fn find(&mut self, value: &Value) -> ControlFlow<bool> {
+ 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<Item = &'a Value>,
+ ) -> ControlFlow<bool> {
+ for item in iter {
+ self.find(item)?;
+ }
+ ControlFlow::Continue(())
+ }
+}