feat: complete bracket if the function accepts an only content arg (#848)

* feat: complete bracket if the function will accept an only content argument

* test: update snapshot
This commit is contained in:
Myriad-Dreamin 2024-11-19 11:04:27 +08:00 committed by GitHub
parent ccd3cea08c
commit a1a15a6795
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 315 additions and 47 deletions

View file

@ -81,19 +81,7 @@ fn check_signature<'a>(
.iter()
.map(|args| args.positional_params().len())
.sum::<usize>();
let pos_idx = bound_pos + positional;
let nth = sig_ins.pos(pos_idx).cloned();
let nth = nth.or_else(|| {
let rest_idx = || pos_idx.saturating_sub(sig_ins.positional_params().len());
let rest_ty = sig_ins.rest_param()?;
match rest_ty {
Ty::Array(ty) => Some(ty.as_ref().clone()),
Ty::Tuple(tys) => tys.get(rest_idx()).cloned(),
_ => None,
}
});
if let Some(nth) = nth {
if let Some(nth) = sig_ins.pos_or_rest(bound_pos + positional) {
receiver.insert(nth, !pol);
}

View file

@ -243,6 +243,10 @@ impl StatefulRequest for CompletionRequest {
}),
text_edit: Some(text_edit),
insert_text_format: Some(InsertTextFormat::SNIPPET),
commit_characters: typst_completion
.commit_char
.as_ref()
.map(|v| vec![v.to_string()]),
command: typst_completion.command.as_ref().map(|c| Command {
command: c.to_string(),
..Default::default()

View file

@ -0,0 +1,3 @@
/// contains: strong, strong[]
#(/* range after 1..2 */st);

View file

@ -0,0 +1,3 @@
/// contains: delta
#strong(/* range after 1..2 */[];

View file

@ -0,0 +1,3 @@
/// contains: delta
#strong(/* range after 1..2 */[]);

View file

@ -0,0 +1,33 @@
---
source: crates/tinymist-query/src/completion.rs
description: Completion on t (57..58)
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/bracket_strong.typ
---
[
{
"isIncomplete": false,
"items": [
{
"kind": 3,
"label": "strong",
"labelDetails": {
"description": "(content, delta: int) => strong"
},
"textEdit": {
"newText": "strong[${1:}]",
"range": {
"end": {
"character": 26,
"line": 2
},
"start": {
"character": 24,
"line": 2
}
}
}
}
]
}
]

View file

@ -0,0 +1,34 @@
---
source: crates/tinymist-query/src/completion.rs
description: "Completion on ] (52..53)"
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/bracket_strong_delta.typ
---
[
{
"isIncomplete": false,
"items": [
{
"kind": 5,
"label": "delta",
"labelDetails": {
"description": "int"
},
"sortText": "001",
"textEdit": {
"newText": "delta: ${1:})",
"range": {
"end": {
"character": 31,
"line": 2
},
"start": {
"character": 31,
"line": 2
}
}
}
}
]
}
]

View file

@ -0,0 +1,34 @@
---
source: crates/tinymist-query/src/completion.rs
description: "Completion on ] (52..53)"
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/bracket_strong_delta2.typ
---
[
{
"isIncomplete": false,
"items": [
{
"kind": 5,
"label": "delta",
"labelDetails": {
"description": "int"
},
"sortText": "001",
"textEdit": {
"newText": "delta: ${1:}",
"range": {
"end": {
"character": 31,
"line": 2
},
"start": {
"character": 31,
"line": 2
}
}
}
}
]
}
]

View file

@ -15,7 +15,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/colon_markup.typ
"description": "(content, b: content | none, bl: content | none, br: content | none, t: content | none, tl: content | none, tr: content | none) => attach"
},
"textEdit": {
"newText": "attach(${1:})",
"newText": "attach[${1:}]",
"range": {
"end": {
"character": 2,

View file

@ -15,7 +15,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/colon_math.typ
"description": "(content, b: content | none, bl: content | none, br: content | none, t: content | none, tl: content | none, tr: content | none) => attach"
},
"textEdit": {
"newText": "attach(${1:})",
"newText": "attach[${1:}]",
"range": {
"end": {
"character": 2,

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/element_where.typ
{
"kind": 5,
"label": "caption",
"labelDetails": {
"description": "content | none"
},
"sortText": "000",
"textEdit": {
"newText": "caption: ${1:}",

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args.typ
{
"kind": 5,
"label": "authors",
"labelDetails": {
"description": "array | str"
},
"sortText": "000",
"textEdit": {
"newText": "authors: ${1:}",
@ -29,6 +32,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args.typ
{
"kind": 5,
"label": "class",
"labelDetails": {
"description": "\"article\" | \"letter\" | str"
},
"sortText": "001",
"textEdit": {
"newText": "class: ${1:}",
@ -47,6 +53,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args.typ
{
"kind": 5,
"label": "font",
"labelDetails": {
"description": "array | none | text.font"
},
"sortText": "002",
"textEdit": {
"newText": "font: ${1:}",
@ -68,7 +77,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args.typ
"labelDetails": {
"description": "type"
},
"sortText": "052",
"sortText": "053",
"textEdit": {
"newText": "content",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args2.typ
{
"kind": 5,
"label": "class",
"labelDetails": {
"description": "\"article\" | \"letter\" | str"
},
"sortText": "000",
"textEdit": {
"newText": " class: ${1:}",
@ -29,6 +32,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args2.typ
{
"kind": 5,
"label": "font",
"labelDetails": {
"description": "array | none | text.font"
},
"sortText": "001",
"textEdit": {
"newText": " font: ${1:}",

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args_after.typ
{
"kind": 5,
"label": "class",
"labelDetails": {
"description": "\"article\" | \"letter\" | str"
},
"sortText": "000",
"textEdit": {
"newText": "class: ${1:}, ",

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_builtin_args.typ
{
"kind": 5,
"label": "columns",
"labelDetails": {
"description": "array | auto | length | type"
},
"sortText": "002",
"textEdit": {
"newText": "columns: ${1:}",
@ -32,7 +35,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_builtin_args.typ
"labelDetails": {
"description": "(int, content, gutter: relative) => columns"
},
"sortText": "056",
"sortText": "057",
"textEdit": {
"newText": "columns(${1:})",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_with_args.typ
{
"kind": 5,
"label": "authors",
"labelDetails": {
"description": "array | str"
},
"sortText": "000",
"textEdit": {
"newText": "authors: ${1:}",
@ -29,6 +32,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_with_args.typ
{
"kind": 5,
"label": "class",
"labelDetails": {
"description": "\"article\" | \"letter\" | str"
},
"sortText": "001",
"textEdit": {
"newText": "class: ${1:}",
@ -47,6 +53,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_with_args.typ
{
"kind": 5,
"label": "font",
"labelDetails": {
"description": "array | none | text.font"
},
"sortText": "002",
"textEdit": {
"newText": "font: ${1:}",
@ -68,7 +77,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_with_args.typ
"labelDetails": {
"description": "type"
},
"sortText": "052",
"sortText": "053",
"textEdit": {
"newText": "content",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/half_completion.typ
{
"kind": 5,
"label": "font",
"labelDetails": {
"description": "array | text.font"
},
"sortText": "011",
"textEdit": {
"newText": "font: ${1:}, ",
@ -34,6 +37,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/half_completion.typ
{
"kind": 5,
"label": "font",
"labelDetails": {
"description": "array | text.font"
},
"sortText": "011",
"textEdit": {
"newText": "font: ${1:}",

View file

@ -1,6 +1,6 @@
---
source: crates/tinymist-query/src/completion.rs
description: Completion on n (31..32)
description: Completion on n (32..33)
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/keyword_ident.typ
---
@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/keyword_ident.typ
{
"kind": 5,
"label": "inset",
"labelDetails": {
"description": "inset"
},
"sortText": "007",
"textEdit": {
"newText": "inset: ${1:}",

View file

@ -14,7 +14,7 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-core-slides.typ
"labelDetails": {
"description": "() => any"
},
"sortText": "050",
"sortText": "051",
"textEdit": {
"newText": "config-xxx()${1:}",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-core-slides.typ
{
"kind": 5,
"label": "config",
"labelDetails": {
"description": "dictionary"
},
"sortText": "003",
"textEdit": {
"newText": "config: ${1:}",
@ -29,6 +32,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-core-slides.typ
{
"kind": 5,
"label": "repeat",
"labelDetails": {
"description": "auto"
},
"sortText": "004",
"textEdit": {
"newText": "repeat: ${1:}",
@ -50,9 +56,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-core-slides.typ
"labelDetails": {
"description": "(content, gap: length, justify: bool) => repeat"
},
"sortText": "245",
"sortText": "250",
"textEdit": {
"newText": "repeat(${1:})",
"newText": "repeat[${1:}]",
"range": {
"end": {
"character": 7,

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-cover-with-rec
{
"kind": 5,
"label": "fill",
"labelDetails": {
"description": "auto | color | int | ratio | type"
},
"sortText": "001",
"textEdit": {
"newText": "fill: ${1:}",
@ -32,7 +35,7 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-cover-with-rec
"labelDetails": {
"description": "type"
},
"sortText": "289",
"sortText": "296",
"textEdit": {
"newText": "stroke(${1:})",
"range": {

View file

@ -32,7 +32,7 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-current-headin
"labelDetails": {
"description": "type"
},
"sortText": "131",
"sortText": "134",
"textEdit": {
"newText": "int(${1:})",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-current-headin
{
"kind": 5,
"label": "depth",
"labelDetails": {
"description": "9999"
},
"sortText": "000",
"textEdit": {
"newText": "depth: ${1:}",
@ -29,6 +32,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-current-headin
{
"kind": 5,
"label": "hierachical",
"labelDetails": {
"description": "true"
},
"sortText": "001",
"textEdit": {
"newText": "hierachical: ${1:}",
@ -47,6 +53,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-current-headin
{
"kind": 5,
"label": "level",
"labelDetails": {
"description": "auto | int"
},
"sortText": "002",
"textEdit": {
"newText": "level: ${1:}",

View file

@ -68,7 +68,7 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-markup-text.ty
"labelDetails": {
"description": "type"
},
"sortText": "281",
"sortText": "288",
"textEdit": {
"newText": "str(${1:})",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-reconstruct.ty
{
"kind": 5,
"label": "body-name",
"labelDetails": {
"description": "\"body\""
},
"sortText": "000",
"textEdit": {
"newText": "body-name: ${1:}",
@ -29,6 +32,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-reconstruct.ty
{
"kind": 5,
"label": "labeled",
"labelDetails": {
"description": "true"
},
"sortText": "001",
"textEdit": {
"newText": "labeled: ${1:}",
@ -47,6 +53,9 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-reconstruct.ty
{
"kind": 5,
"label": "named",
"labelDetails": {
"description": "false"
},
"sortText": "002",
"textEdit": {
"newText": "named: ${1:}",

View file

@ -21,6 +21,25 @@ pub trait BoundChecker: Sized + TyCtx {
}
}
#[derive(BindTyCtx)]
#[bind(0)]
pub struct BoundPred<'a, T: TyCtx, F>(pub &'a T, pub F);
impl<'a, T: TyCtx, F> BoundPred<'a, T, F> {
pub fn new(t: &'a T, f: F) -> Self {
Self(t, f)
}
}
impl<'a, T: TyCtx, F> BoundChecker for BoundPred<'a, T, F>
where
F: FnMut(&Ty, bool),
{
fn collect(&mut self, ty: &Ty, pol: bool) {
self.1(ty, pol);
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DocSource {
Var(Interned<TypeVar>),

View file

@ -15,11 +15,11 @@ use reflexo_typst::TypstFileId;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Serialize};
use typst::{
foundations::{ParamInfo, Value},
foundations::{Content, ParamInfo, Type, Value},
syntax::{ast, Span, SyntaxKind, SyntaxNode},
};
use super::PackageId;
use super::{BoundPred, PackageId};
use crate::{
adt::{interner::impl_internable, snapshot_map},
analysis::BuiltinTy,
@ -221,6 +221,24 @@ impl Ty {
_ => None,
}
}
pub(crate) fn is_content<T: TyCtx>(&self, ctx: &T) -> bool {
let mut res = false;
self.bounds(
false,
&mut BoundPred::new(ctx, |ty: &Ty, _pol| {
res = res || {
match ty {
Ty::Value(v) => matches!(v.val, Value::Content(..)),
Ty::Builtin(BuiltinTy::Content | BuiltinTy::Element(..)) => true,
Ty::Builtin(BuiltinTy::Type(v)) => *v == Type::of::<Content>(),
_ => false,
}
}
}),
);
res
}
}
/// A function parameter type
@ -847,6 +865,21 @@ impl SigTy {
.and_then(|_| self.inputs.get(idx))
}
/// Get the parameter or the rest parameter at the given index
pub fn pos_or_rest(&self, idx: usize) -> Option<Ty> {
let nth = self.pos(idx).cloned();
nth.or_else(|| {
let rest_idx = || idx.saturating_sub(self.positional_params().len());
let rest_ty = self.rest_param()?;
match rest_ty {
Ty::Array(ty) => Some(ty.as_ref().clone()),
Ty::Tuple(tys) => tys.get(rest_idx()).cloned(),
_ => None,
}
})
}
/// Get the named parameters of the function
pub fn named_params(&self) -> impl ExactSizeIterator<Item = (&StrRef, &Ty)> {
let named_names = self.names.names.iter();

View file

@ -32,6 +32,16 @@ pub trait TyCtx {
fn global_bounds(&self, _var: &Interned<TypeVar>, _pol: bool) -> Option<TypeBounds>;
}
impl TyCtx for () {
fn local_bind_of(&self, _var: &Interned<TypeVar>) -> Option<Ty> {
None
}
fn global_bounds(&self, _var: &Interned<TypeVar>, _pol: bool) -> Option<TypeBounds> {
None
}
}
/// A mutable type context.
pub trait TyCtxMut: TyCtx {
/// The type of a snapshot of the scope.

View file

@ -64,6 +64,8 @@ pub struct Completion {
pub sort_text: Option<EcoString>,
/// The composed text used for filtering.
pub filter_text: Option<EcoString>,
/// The character that should be committed when selecting this completion.
pub commit_char: Option<char>,
/// The completed version of the input, possibly described with snippet
/// syntax like `${lhs} + ${rhs}`.
///

View file

@ -13,7 +13,7 @@ use typst::visualize::Color;
use super::{Completion, CompletionContext, CompletionKind};
use crate::adt::interner::Interned;
use crate::analysis::{BuiltinTy, PathPreference, Ty};
use crate::analysis::{func_signature, BuiltinTy, PathPreference, Ty};
use crate::syntax::{descending_decls, is_ident_like, CheckTarget, DescentDecl};
use crate::ty::{Iface, IfaceChecker, InsTy, SigTy, TyCtx, TypeBounds, TypeScheme, TypeVar};
use crate::upstream::complete::complete_code;
@ -174,8 +174,13 @@ impl<'a> CompletionContext<'a> {
..base
});
} else {
let apply = if fn_feat.prefer_content_bracket {
eco_format!("{name}[${{}}]")
} else {
eco_format!("{name}(${{}})")
};
self.completions.push(Completion {
apply: Some(eco_format!("{}(${{}})", name)),
apply: Some(apply),
label: name,
..base
});
@ -392,6 +397,7 @@ impl CompletionKindChecker {
#[derive(Default, Debug)]
struct FnCompletionFeat {
zero_args: bool,
prefer_content_bracket: bool,
is_element: bool,
}
@ -408,15 +414,12 @@ impl FnCompletionFeat {
match ty {
Ty::Value(val) => match &val.val {
Value::Type(..) => {}
Value::Func(sig) => {
if sig.element().is_some() {
Value::Func(func) => {
if func.element().is_some() {
self.is_element = true;
}
let ps = sig.params().into_iter().flatten();
let pos_size = ps.filter(|s| s.positional).count();
if pos_size <= pos {
self.zero_args = true;
}
let sig = func_signature(func.clone()).type_sig();
self.check_sig(&sig, pos);
}
_ => panic!("FnCompletionFeat check_one {val:?}"),
},
@ -424,18 +427,25 @@ impl FnCompletionFeat {
Ty::With(w) => {
self.check_one(&w.sig, pos + w.with.positional_params().len());
}
Ty::Builtin(BuiltinTy::Element(..)) => {
Ty::Builtin(BuiltinTy::Element(func)) => {
self.is_element = true;
let sig = (*func).into();
let sig = func_signature(sig).type_sig();
self.check_sig(&sig, pos);
}
Ty::Builtin(BuiltinTy::TypeType(..)) => {}
_ => panic!("FnCompletionFeat check_one {ty:?}"),
}
}
fn check_sig(&mut self, sig: &SigTy, pos: usize) {
if pos >= sig.positional_params().len() {
self.zero_args = true;
}
// todo: sig is element
fn check_sig(&mut self, sig: &SigTy, idx: usize) {
let pos_size = sig.positional_params().len();
let prefer_content_bracket =
sig.rest_param().is_none() && sig.pos(idx).map_or(false, |ty| ty.is_content(&()));
self.prefer_content_bracket = self.prefer_content_bracket || prefer_content_bracket;
let name_size = sig.named_params().len();
self.zero_args = pos_size <= idx && name_size == 0;
}
}
@ -462,8 +472,7 @@ fn sort_and_explicit_code_completion(ctx: &mut CompletionContext) {
// ctx.strict_scope_completions(false, |value| value.ty() == ty);
log::debug!(
"sort_and_explicit_code_completion: {:#?} {:#?}",
completions,
"sort_and_explicit_code_completion: {completions:#?} {:#?}",
ctx.completions
);
@ -617,6 +626,7 @@ fn type_completion(
Ty::Param(p) => {
// todo: variadic
let docs = docs.or_else(|| p.docs.as_deref());
if p.attrs.positional {
type_completion(ctx, &p.ty, docs);
}
@ -637,12 +647,11 @@ fn type_completion(
return Some(());
}
// todo: label details
let docs = docs.or_else(|| p.docs.as_deref());
ctx.completions.push(Completion {
kind: CompletionKind::Field,
label: f.into(),
apply: Some(eco_format!("{}: ${{}}", f)),
label_detail: p.ty.describe(),
detail: docs.map(Into::into),
command: ctx
.trigger_named_completion
@ -871,6 +880,7 @@ pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
let check_target = get_check_target(ctx.leaf.clone());
log::debug!("complete_type: pos {:?} -> {check_target:#?}", ctx.leaf);
let mut args_node = None;
match check_target {
Some(CheckTarget::Element { container, .. }) => {
@ -889,6 +899,13 @@ pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
ctx.seen_field(named.name().into());
}
}
args_node = Some(args.to_untyped().clone());
}
Some(CheckTarget::Normal(e))
if (matches!(e.kind(), SyntaxKind::ContentBlock)
&& matches!(ctx.leaf.kind(), SyntaxKind::LeftBracket)) =>
{
args_node = e.parent().map(|s| s.get().clone());
}
Some(CheckTarget::Normal(e))
if matches!(
@ -920,6 +937,22 @@ pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
}
sort_and_explicit_code_completion(ctx);
if let Some(c) = args_node {
log::debug!("content block compl: args {c:?}");
let is_unclosed = matches!(c.kind(), SyntaxKind::Args)
&& c.children().fold(0i32, |acc, node| match node.kind() {
SyntaxKind::LeftParen => acc + 1,
SyntaxKind::RightParen => acc - 1,
SyntaxKind::Error if node.text() == "(" => acc + 1,
SyntaxKind::Error if node.text() == ")" => acc - 1,
_ => acc,
}) > 0;
if is_unclosed {
ctx.enrich("", ")");
}
}
Some(())
}