Smarter filtering of scope completions

This commit is contained in:
Laurenz 2024-11-12 15:29:26 +01:00
parent de59d64d10
commit 7add9b459a
2 changed files with 120 additions and 19 deletions

View File

@ -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")]);

View File

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