feat: provide UFCS-style completion on content types (#849)

* feat: UFCS completion on content types

* dev: cleanup panics

* feat: add configuration about postfix completion

* test: update snapshot

* fix: lazily determine default values
This commit is contained in:
Myriad-Dreamin 2024-11-19 12:48:04 +08:00 committed by GitHub
parent a1a15a6795
commit d0b40dbfa6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 570 additions and 178 deletions

View file

@ -36,7 +36,7 @@ use crate::syntax::{
scan_workspace_files, Decl, DefKind, DerefTarget, ExprInfo, ExprRoute, LexicalScope,
ModuleDependency,
};
use crate::upstream::{tooltip_, Tooltip};
use crate::upstream::{tooltip_, CompletionFeat, Tooltip};
use crate::{
lsp_to_typst, typst_to_lsp, ColorTheme, CompilerQueryRequest, LspPosition, LspRange,
LspWorldExt, PositionEncoding, TypstRange, VersionedDocument,
@ -55,6 +55,8 @@ pub struct Analysis {
pub allow_multiline_token: bool,
/// Whether to remove html from markup content in responses.
pub remove_html: bool,
/// Tinymist's completion features.
pub completion_feat: CompletionFeat,
/// The editor's color theme.
pub color_theme: ColorTheme,
/// The periscope provider.

View file

@ -242,6 +242,7 @@ impl StatefulRequest for CompletionRequest {
}
}),
text_edit: Some(text_edit),
additional_text_edits: typst_completion.additional_text_edits.clone(),
insert_text_format: Some(InsertTextFormat::SNIPPET),
commit_characters: typst_completion
.commit_char

View file

@ -77,7 +77,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args.typ
"labelDetails": {
"description": "type"
},
"sortText": "053",
"sortText": "052",
"textEdit": {
"newText": "content",
"range": {

View file

@ -35,7 +35,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_builtin_args.typ
"labelDetails": {
"description": "(int, content, gutter: relative) => columns"
},
"sortText": "057",
"sortText": "056",
"textEdit": {
"newText": "columns(${1:})",
"range": {

View file

@ -77,7 +77,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_with_args.typ
"labelDetails": {
"description": "type"
},
"sortText": "053",
"sortText": "052",
"textEdit": {
"newText": "content",
"range": {

View file

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

View file

@ -56,7 +56,7 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-core-slides.typ
"labelDetails": {
"description": "(content, gap: length, justify: bool) => repeat"
},
"sortText": "250",
"sortText": "249",
"textEdit": {
"newText": "repeat[${1:}]",
"range": {

View file

@ -35,7 +35,7 @@ input_file: crates/tinymist-query/src/fixtures/pkgs/touying-utils-cover-with-rec
"labelDetails": {
"description": "type"
},
"sortText": "296",
"sortText": "295",
"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": "134",
"sortText": "133",
"textEdit": {
"newText": "int(${1:})",
"range": {

View file

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

View file

@ -16,7 +16,7 @@ pub mod ty;
mod upstream;
pub use analysis::{LocalContext, LocalContextGuard, LspWorldExt};
pub use upstream::with_vm;
pub use upstream::{with_vm, CompletionFeat};
mod diagnostics;
pub use diagnostics::*;

View file

@ -4,6 +4,7 @@ use std::ops::Range;
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use lsp_types::TextEdit;
use serde::{Deserialize, Serialize};
use typst::foundations::{fields_on, format_str, repr, Repr, StyleChain, Styles, Value};
use typst::model::Document;
@ -15,12 +16,11 @@ use unscanny::Scanner;
use super::{plain_docs_sentence, summarize_font_family};
use crate::adt::interner::Interned;
use crate::analysis::{analyze_labels, DynLabel, Ty};
use crate::LocalContext;
use crate::analysis::{analyze_labels, DynLabel, LocalContext, Ty};
mod ext;
pub use ext::complete_path;
use ext::*;
pub use ext::{complete_path, CompletionFeat};
/// Autocomplete a cursor position in a source file.
///
@ -73,6 +73,10 @@ pub struct Completion {
pub apply: Option<EcoString>,
/// An optional short description, at most one sentence.
pub detail: Option<EcoString>,
/// An optional array of additional text edits that are applied when
/// selecting this completion. Edits must not overlap with the main edit
/// nor with themselves.
pub additional_text_edits: Option<Vec<TextEdit>>,
/// An optional command to run when the completion is selected.
pub command: Option<&'static str>,
}
@ -382,7 +386,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
if let Some((value, styles)) = ctx.ctx.analyze_expr(&prev).into_iter().next();
then {
ctx.from = ctx.cursor;
field_access_completions(ctx, &value, &styles);
field_access_completions(ctx, &prev, &value, &styles);
return true;
}
}
@ -397,7 +401,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
if let Some((value, styles)) = ctx.ctx.analyze_expr(&prev_prev).into_iter().next();
then {
ctx.from = ctx.leaf.offset();
field_access_completions(ctx, &value, &styles);
field_access_completions(ctx,&prev_prev, &value, &styles);
return true;
}
}
@ -406,7 +410,12 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
}
/// Add completions for all fields on a value.
fn field_access_completions(ctx: &mut CompletionContext, value: &Value, styles: &Option<Styles>) {
fn field_access_completions(
ctx: &mut CompletionContext,
node: &LinkedNode,
value: &Value,
styles: &Option<Styles>,
) {
for (name, value, _) in value.ty().scope().iter() {
ctx.value_completion(Some(name.clone()), value, true, None);
}
@ -443,11 +452,15 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value, styles:
});
}
}
ctx.ufcs_completions(node, value);
}
Value::Content(content) => {
for (name, value) in content.fields() {
ctx.value_completion(Some(name.into()), &value, false, None);
}
ctx.ufcs_completions(node, value);
}
Value::Dict(dict) => {
for (name, value) in dict.iter() {

View file

@ -4,6 +4,7 @@ use ecow::{eco_format, EcoString};
use hashbrown::HashSet;
use lsp_types::{CompletionItem, CompletionTextEdit, InsertTextFormat, TextEdit};
use reflexo::path::unix_slash;
use serde::{Deserialize, Serialize};
use tinymist_derive::BindTyCtx;
use tinymist_world::LspWorld;
use typst::foundations::{AutoValue, Func, Label, NoneValue, Scope, Type, Value};
@ -20,6 +21,35 @@ use crate::upstream::complete::complete_code;
use crate::{completion_kind, prelude::*, LspCompletion};
/// Tinymist's completion features.
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionFeat {
/// Whether to enable postfix completion.
pub postfix: Option<bool>,
/// Whether to enable ufcs completion.
pub postfix_ufcs: Option<bool>,
/// Whether to enable ufcs completion (left variant).
pub postfix_ufcs_left: Option<bool>,
/// Whether to enable ufcs completion (right variant).
pub postfix_ufcs_right: Option<bool>,
}
impl CompletionFeat {
pub(crate) fn any_ufcs(&self) -> bool {
self.ufcs() || self.ufcs_left() || self.ufcs_right()
}
pub(crate) fn ufcs(&self) -> bool {
self.postfix.unwrap_or(true) && self.postfix_ufcs.unwrap_or(true)
}
pub(crate) fn ufcs_left(&self) -> bool {
self.postfix.unwrap_or(true) && self.postfix_ufcs_left.unwrap_or(true)
}
pub(crate) fn ufcs_right(&self) -> bool {
self.postfix.unwrap_or(true) && self.postfix_ufcs_right.unwrap_or(true)
}
}
impl<'a> CompletionContext<'a> {
pub fn world(&self) -> &LspWorld {
self.ctx.world()
@ -33,17 +63,14 @@ impl<'a> CompletionContext<'a> {
!self.seen_fields.insert(field)
}
/// Add completions for definitions that are available at the cursor.
///
/// Filters the global/math scope with the given filter.
pub fn scope_completions_(&mut self, parens: bool, filter: impl Fn(Option<&Value>) -> bool) {
log::debug!("scope_completions: {parens}");
let Some(fid) = self.root.span().id() else {
return;
};
let Ok(src) = self.ctx.source_by_id(fid) else {
return;
};
fn surrounding_syntax(&mut self) -> SurroundingSyntax {
check_surrounding_syntax(&self.leaf)
.or_else(|| check_previous_syntax(&self.leaf))
.unwrap_or(SurroundingSyntax::Regular)
}
fn defines(&mut self) -> Option<(Source, Defines)> {
let src = self.ctx.source_by_id(self.root.span().id()?).ok()?;
let mut defines = Defines {
types: self.ctx.type_check(&src),
@ -84,17 +111,136 @@ impl<'a> CompletionContext<'a> {
None
});
enum SurroundingSyntax {
Regular,
Selector,
SetRule,
Some((src, defines))
}
pub fn ufcs_completions(&mut self, node: &LinkedNode, value: &Value) {
if !self.ctx.analysis.completion_feat.any_ufcs() {
return;
}
let _ = value;
let surrounding_syntax = self.surrounding_syntax();
if !matches!(surrounding_syntax, SurroundingSyntax::Regular) {
return;
}
let Some((src, defines)) = self.defines() else {
return;
};
log::debug!("defines: {:?}", defines.defines.len());
let mut kind_checker = CompletionKindChecker {
symbols: HashSet::default(),
functions: HashSet::default(),
};
let rng = node.range();
let is_content_block = node.kind() == SyntaxKind::ContentBlock;
let lb = if is_content_block { "" } else { "(" };
let rb = if is_content_block { "" } else { ")" };
// we don't check literal type here for faster completion
for (name, ty) in defines.defines {
// todo: filter ty
if name.is_empty() {
continue;
}
kind_checker.check(&ty);
if kind_checker.symbols.iter().min().copied().is_some() {
continue;
}
if kind_checker.functions.is_empty() {
continue;
}
let label_detail = ty.describe().map(From::from).or_else(|| Some("any".into()));
let base = Completion {
kind: CompletionKind::Func,
label_detail,
apply: Some("".into()),
// range: Some(range),
command: self
.trigger_parameter_hints
.then_some("editor.action.triggerParameterHints"),
..Default::default()
};
let fn_feat = FnCompletionFeat::default().check(kind_checker.functions.iter());
log::debug!("fn_feat: {name} {ty:?} -> {fn_feat:?}");
if fn_feat.min_pos() < 1 || !fn_feat.next_arg_is_content {
continue;
}
log::debug!("checked ufcs: {ty:?}");
if self.ctx.analysis.completion_feat.ufcs() && fn_feat.min_pos() == 1 {
let before = TextEdit {
range: self.ctx.to_lsp_range(rng.start..rng.start, &src),
new_text: format!("{name}{lb}"),
};
let after = TextEdit {
range: self.ctx.to_lsp_range(rng.end..self.from, &src),
new_text: rb.into(),
};
self.completions.push(Completion {
label: name.clone(),
additional_text_edits: Some(vec![before, after]),
..base.clone()
});
}
let more_args = fn_feat.min_pos() > 1 || fn_feat.min_named() > 0;
if self.ctx.analysis.completion_feat.ufcs_left() && more_args {
let node_content = node.get().clone().into_text();
let before = TextEdit {
range: self.ctx.to_lsp_range(rng.start..self.from, &src),
new_text: format!("{name}{lb}"),
};
self.completions.push(Completion {
apply: if is_content_block {
Some(eco_format!("(${{}}){node_content}"))
} else {
Some(eco_format!("${{}}, {node_content})"))
},
label: eco_format!("{name}("),
additional_text_edits: Some(vec![before]),
..base.clone()
});
}
if self.ctx.analysis.completion_feat.ufcs_right() && more_args {
let before = TextEdit {
range: self.ctx.to_lsp_range(rng.start..rng.start, &src),
new_text: format!("{name}("),
};
let after = TextEdit {
range: self.ctx.to_lsp_range(rng.end..self.from, &src),
new_text: "".into(),
};
self.completions.push(Completion {
apply: Some(eco_format!("${{}})")),
label: eco_format!("{name})"),
additional_text_edits: Some(vec![before, after]),
..base
});
}
}
}
/// Add completions for definitions that are available at the cursor.
///
/// Filters the global/math scope with the given filter.
pub fn scope_completions_(&mut self, parens: bool, filter: impl Fn(Option<&Value>) -> bool) {
let Some((_, defines)) = self.defines() else {
return;
};
let defines = defines.defines;
let surrounding_syntax = check_surrounding_syntax(&self.leaf)
.or_else(|| check_previous_syntax(&self.leaf))
.unwrap_or(SurroundingSyntax::Regular);
let surrounding_syntax = self.surrounding_syntax();
let mut kind_checker = CompletionKindChecker {
symbols: HashSet::default(),
@ -142,7 +288,9 @@ impl<'a> CompletionContext<'a> {
log::debug!("fn_feat: {name} {ty:?} -> {fn_feat:?}");
if !fn_feat.zero_args && matches!(surrounding_syntax, SurroundingSyntax::Regular) {
if matches!(surrounding_syntax, SurroundingSyntax::Regular)
&& (fn_feat.min_pos() > 0 || fn_feat.min_named() > 0)
{
self.completions.push(Completion {
label: eco_format!("{}.with", name),
apply: Some(eco_format!("{}.with(${{}})", name)),
@ -167,14 +315,14 @@ impl<'a> CompletionContext<'a> {
label: name,
..base
});
} else if fn_feat.zero_args {
} else if fn_feat.min_pos() < 1 && !fn_feat.has_rest {
self.completions.push(Completion {
apply: Some(eco_format!("{}()${{}}", name)),
label: name,
..base
});
} else {
let apply = if fn_feat.prefer_content_bracket {
let apply = if fn_feat.next_arg_is_content && !fn_feat.has_rest {
eco_format!("{name}[${{}}]")
} else {
eco_format!("{name}(${{}})")
@ -198,8 +346,16 @@ impl<'a> CompletionContext<'a> {
..Completion::default()
});
}
}
}
fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option<SurroundingSyntax> {
enum SurroundingSyntax {
Regular,
Selector,
SetRule,
}
fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option<SurroundingSyntax> {
use SurroundingSyntax::*;
let mut met_args = false;
while let Some(parent) = leaf.parent() {
@ -220,8 +376,7 @@ impl<'a> CompletionContext<'a> {
}
SyntaxKind::SetRule => {
let rule = parent.get().cast::<ast::SetRule>()?;
if met_args || encolsed_by(parent, rule.condition().map(|s| s.span()), leaf)
{
if met_args || encolsed_by(parent, rule.condition().map(|s| s.span()), leaf) {
return Some(Regular);
} else {
return Some(SetRule);
@ -242,9 +397,9 @@ impl<'a> CompletionContext<'a> {
}
None
}
}
fn check_previous_syntax(leaf: &LinkedNode) -> Option<SurroundingSyntax> {
fn check_previous_syntax(leaf: &LinkedNode) -> Option<SurroundingSyntax> {
let mut leaf = leaf.clone();
if leaf.kind().is_trivia() {
leaf = leaf.prev_sibling()?;
@ -261,8 +416,6 @@ impl<'a> CompletionContext<'a> {
}
None
}
}
}
#[derive(BindTyCtx)]
@ -388,16 +541,31 @@ impl CompletionKindChecker {
self.check(ty);
}
}
Ty::Any | Ty::Builtin(..) => {}
_ => panic!("check kind {ty:?}"),
Ty::Any
| Ty::Builtin(..)
| Ty::Boolean(..)
| Ty::Param(..)
| Ty::Union(..)
| Ty::Var(..)
| Ty::Dict(..)
| Ty::Array(..)
| Ty::Tuple(..)
| Ty::Args(..)
| Ty::Pattern(..)
| Ty::Select(..)
| Ty::Unary(..)
| Ty::Binary(..)
| Ty::If(..) => {}
}
}
}
#[derive(Default, Debug)]
struct FnCompletionFeat {
zero_args: bool,
prefer_content_bracket: bool,
min_pos: Option<usize>,
min_named: Option<usize>,
has_rest: bool,
next_arg_is_content: bool,
is_element: bool,
}
@ -410,10 +578,20 @@ impl FnCompletionFeat {
self
}
fn min_pos(&self) -> usize {
self.min_pos.unwrap_or_default()
}
fn min_named(&self) -> usize {
self.min_named.unwrap_or_default()
}
fn check_one(&mut self, ty: &Ty, pos: usize) {
match ty {
Ty::Value(val) => match &val.val {
Value::Type(..) => {}
Value::Type(ty) => {
self.check_one(&Ty::Builtin(BuiltinTy::Type(*ty)), pos);
}
Value::Func(func) => {
if func.element().is_some() {
self.is_element = true;
@ -421,31 +599,118 @@ impl FnCompletionFeat {
let sig = func_signature(func.clone()).type_sig();
self.check_sig(&sig, pos);
}
_ => panic!("FnCompletionFeat check_one {val:?}"),
Value::None
| Value::Auto
| Value::Bool(_)
| Value::Int(_)
| Value::Float(..)
| Value::Length(..)
| Value::Angle(..)
| Value::Ratio(..)
| Value::Relative(..)
| Value::Fraction(..)
| Value::Color(..)
| Value::Gradient(..)
| Value::Pattern(..)
| Value::Symbol(..)
| Value::Version(..)
| Value::Str(..)
| Value::Bytes(..)
| Value::Label(..)
| Value::Datetime(..)
| Value::Decimal(..)
| Value::Duration(..)
| Value::Content(..)
| Value::Styles(..)
| Value::Array(..)
| Value::Dict(..)
| Value::Args(..)
| Value::Module(..)
| Value::Plugin(..)
| Value::Dyn(..) => {}
},
Ty::Func(sig) => self.check_sig(sig, pos),
Ty::With(w) => {
self.check_one(&w.sig, pos + w.with.positional_params().len());
}
Ty::Builtin(BuiltinTy::Element(func)) => {
Ty::Builtin(b) => match b {
BuiltinTy::Element(func) => {
self.is_element = true;
let sig = (*func).into();
let sig = func_signature(sig).type_sig();
let func = (*func).into();
let sig = func_signature(func).type_sig();
self.check_sig(&sig, pos);
}
Ty::Builtin(BuiltinTy::TypeType(..)) => {}
_ => panic!("FnCompletionFeat check_one {ty:?}"),
BuiltinTy::Type(ty) => {
let func = ty.constructor().ok();
if let Some(func) = func {
let sig = func_signature(func).type_sig();
self.check_sig(&sig, pos);
}
}
BuiltinTy::TypeType(..) => {}
BuiltinTy::Clause
| BuiltinTy::Undef
| BuiltinTy::Content
| BuiltinTy::Space
| BuiltinTy::None
| BuiltinTy::Break
| BuiltinTy::Continue
| BuiltinTy::Infer
| BuiltinTy::FlowNone
| BuiltinTy::Auto
| BuiltinTy::Args
| BuiltinTy::Color
| BuiltinTy::TextSize
| BuiltinTy::TextFont
| BuiltinTy::TextLang
| BuiltinTy::TextRegion
| BuiltinTy::Label
| BuiltinTy::CiteLabel
| BuiltinTy::RefLabel
| BuiltinTy::Dir
| BuiltinTy::Length
| BuiltinTy::Float
| BuiltinTy::Stroke
| BuiltinTy::Margin
| BuiltinTy::Inset
| BuiltinTy::Outset
| BuiltinTy::Radius
| BuiltinTy::Tag(..)
| BuiltinTy::Module(..)
| BuiltinTy::Path(..) => {}
},
Ty::Any
| Ty::Boolean(..)
| Ty::Param(..)
| Ty::Union(..)
| Ty::Let(..)
| Ty::Var(..)
| Ty::Dict(..)
| Ty::Array(..)
| Ty::Tuple(..)
| Ty::Args(..)
| Ty::Pattern(..)
| Ty::Select(..)
| Ty::Unary(..)
| Ty::Binary(..)
| Ty::If(..) => {}
}
}
// 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;
self.has_rest = self.has_rest || sig.rest_param().is_some();
self.next_arg_is_content =
self.next_arg_is_content || sig.pos(idx).map_or(false, |ty| ty.is_content(&()));
let name_size = sig.named_params().len();
self.zero_args = pos_size <= idx && name_size == 0;
let left_pos = pos_size.saturating_sub(idx);
self.min_pos = self
.min_pos
.map_or(Some(left_pos), |v| Some(v.min(left_pos)));
self.min_named = self
.min_named
.map_or(Some(name_size), |v| Some(v.min(name_size)));
}
}
@ -505,8 +770,7 @@ fn sort_and_explicit_code_completion(ctx: &mut CompletionContext) {
}
log::debug!(
"sort_and_explicit_code_completion after: {:#?} {:#?}",
completions,
"sort_and_explicit_code_completion after: {completions:#?} {:#?}",
ctx.completions
);
@ -520,14 +784,30 @@ pub fn ty_to_completion_kind(ty: &Ty) -> CompletionKind {
Ty::Value(ty) => value_to_completion_kind(&ty.val),
Ty::Func(..) | Ty::With(..) => CompletionKind::Func,
Ty::Any => CompletionKind::Variable,
Ty::Builtin(BuiltinTy::Module(..)) => CompletionKind::Module,
Ty::Builtin(BuiltinTy::TypeType(..)) => CompletionKind::Type,
Ty::Builtin(..) => CompletionKind::Variable,
Ty::Let(l) => l
.ubs
.iter()
.chain(l.lbs.iter())
.fold(None, |acc, ty| match acc {
Ty::Builtin(b) => match b {
BuiltinTy::Module(..) => CompletionKind::Module,
BuiltinTy::Type(..) | BuiltinTy::TypeType(..) => CompletionKind::Type,
_ => CompletionKind::Variable,
},
Ty::Let(l) => fold_ty_kind(l.ubs.iter().chain(l.lbs.iter())),
Ty::Union(u) => fold_ty_kind(u.iter()),
Ty::Boolean(..)
| Ty::Param(..)
| Ty::Var(..)
| Ty::Dict(..)
| Ty::Array(..)
| Ty::Tuple(..)
| Ty::Args(..)
| Ty::Pattern(..)
| Ty::Select(..)
| Ty::Unary(..)
| Ty::Binary(..)
| Ty::If(..) => CompletionKind::Constant,
}
}
fn fold_ty_kind<'a>(tys: impl Iterator<Item = &'a Ty>) -> CompletionKind {
tys.fold(None, |acc, ty| match acc {
Some(CompletionKind::Variable) => Some(CompletionKind::Variable),
Some(acc) => {
let kind = ty_to_completion_kind(ty);
@ -539,37 +819,45 @@ pub fn ty_to_completion_kind(ty: &Ty) -> CompletionKind {
}
None => Some(ty_to_completion_kind(ty)),
})
.unwrap_or(CompletionKind::Variable),
_ => panic!("ty_to_completion_kind {ty:?}"),
}
.unwrap_or(CompletionKind::Variable)
}
pub fn value_to_completion_kind(value: &Value) -> CompletionKind {
match value {
Value::Func(..) => CompletionKind::Func,
Value::Module(..) => CompletionKind::Module,
Value::Plugin(..) | Value::Module(..) => CompletionKind::Module,
Value::Type(..) => CompletionKind::Type,
Value::Symbol(s) => CompletionKind::Symbol(s.get()),
_ => CompletionKind::Variable,
Value::None
| Value::Auto
| Value::Bool(..)
| Value::Int(..)
| Value::Float(..)
| Value::Length(..)
| Value::Angle(..)
| Value::Ratio(..)
| Value::Relative(..)
| Value::Fraction(..)
| Value::Color(..)
| Value::Gradient(..)
| Value::Pattern(..)
| Value::Version(..)
| Value::Str(..)
| Value::Bytes(..)
| Value::Label(..)
| Value::Datetime(..)
| Value::Decimal(..)
| Value::Duration(..)
| Value::Content(..)
| Value::Styles(..)
| Value::Array(..)
| Value::Dict(..)
| Value::Args(..)
| Value::Dyn(..) => CompletionKind::Variable,
}
}
// if ctx.before.ends_with(',') {
// ctx.enrich(" ", "");
// }
// if param.attrs.named {
// let compl = Completion {
// kind: CompletionKind::Field,
// label: param.name.as_ref().into(),
// apply: Some(eco_format!("{}: ${{}}", param.name)),
// detail: docs(),
// label_detail: None,
// command: ctx
// .trigger_named_completion
// .then_some("tinymist.triggerNamedCompletion"),
// ..Completion::default()
// };
// match param.ty {
// Ty::Builtin(BuiltinTy::TextSize) => {
// for size_template in &[

View file

@ -109,7 +109,8 @@ impl LanguageState {
position_encoding: const_config.position_encoding,
allow_overlapping_token: const_config.tokens_overlapping_token_support,
allow_multiline_token: const_config.tokens_multiline_token_support,
remove_html: self.config.remove_html,
remove_html: !self.config.support_html_in_markdown,
completion_feat: self.config.completion,
color_theme: match self.compile_config().color_theme.as_deref() {
Some("dark") => tinymist_query::ColorTheme::Dark,
_ => tinymist_query::ColorTheme::Light,

View file

@ -15,7 +15,7 @@ use serde_json::{json, Map, Value as JsonValue};
use strum::IntoEnumIterator;
use task::FormatUserConfig;
use tinymist_query::analysis::{Modifier, TokenType};
use tinymist_query::PositionEncoding;
use tinymist_query::{CompletionFeat, PositionEncoding};
use tinymist_render::PeriscopeArgs;
use typst::foundations::IntoValue;
use typst::syntax::{FileId, VirtualPath};
@ -268,6 +268,7 @@ const CONFIG_ITEMS: &[&str] = &[
"semanticTokens",
"formatterMode",
"formatterPrintWidth",
"completion",
"fontPaths",
"systemFonts",
"typstExtraArgs",
@ -299,7 +300,9 @@ pub struct Config {
/// Whether to trigger parameter hint, a.k.a. signature help.
pub trigger_parameter_hints: bool,
/// Whether to remove html from markup content in responses.
pub remove_html: bool,
pub support_html_in_markdown: bool,
/// Tinymist's completion features.
pub completion: CompletionFeat,
}
impl Config {
@ -357,22 +360,25 @@ impl Config {
/// # Errors
/// Errors if the update is invalid.
pub fn update_by_map(&mut self, update: &Map<String, JsonValue>) -> anyhow::Result<()> {
macro_rules! deser_or_default {
($key:expr, $ty:ty) => {
try_or_default(|| <$ty>::deserialize(update.get($key)?).ok())
macro_rules! assign_config {
($( $field_path:ident ).+ := $bind:literal?: $ty:ty) => {
let v = try_(|| <$ty>::deserialize(update.get($bind)?).ok());
self.$($field_path).+ = v.unwrap_or_default();
};
($( $field_path:ident ).+ := $bind:literal: $ty:ty = $default_value:expr) => {
let v = try_(|| <$ty>::deserialize(update.get($bind)?).ok());
self.$($field_path).+ = v.unwrap_or_else(|| $default_value);
};
}
try_(|| SemanticTokensMode::deserialize(update.get("semanticTokens")?).ok())
.inspect(|v| self.semantic_tokens = *v);
try_(|| FormatterMode::deserialize(update.get("formatterMode")?).ok())
.inspect(|v| self.formatter_mode = *v);
try_(|| u32::deserialize(update.get("formatterPrintWidth")?).ok())
.inspect(|v| self.formatter_print_width = Some(*v));
self.trigger_suggest = deser_or_default!("triggerSuggest", bool);
self.trigger_parameter_hints = deser_or_default!("triggerParameterHints", bool);
self.trigger_named_completion = deser_or_default!("triggerNamedCompletion", bool);
self.remove_html = !deser_or_default!("supportHtmlInMarkdown", bool);
assign_config!(semantic_tokens := "semanticTokens"?: SemanticTokensMode);
assign_config!(formatter_mode := "formatterMode"?: FormatterMode);
assign_config!(formatter_print_width := "formatterPrintWidth"?: Option<u32>);
assign_config!(trigger_suggest := "triggerSuggest"?: bool);
assign_config!(trigger_named_completion := "triggerNamedCompletion"?: bool);
assign_config!(trigger_parameter_hints := "triggerParameterHints"?: bool);
assign_config!(support_html_in_markdown := "supportHtmlInMarkdown"?: bool);
assign_config!(completion := "completion"?: CompletionFeat);
self.compile.update_by_map(update)?;
self.compile.validate()
}

View file

@ -81,3 +81,31 @@ Set the print width for the formatter, which is a **soft limit** of characters p
- **Type**: `number`
- **Default**: `120`
## `completion.postfix`
Whether to enable postfix code completion. For example, `[A].box|` will be completed to `box[A]|`. Hint: Restarting the editor is required to change this setting.
- **Type**: `boolean`
- **Default**: `true`
## `completion.postfixUfcs`
Whether to enable UFCS-style completion. For example, `[A].box|` will be completed to `box[A]|`. Hint: Restarting the editor is required to change this setting.
- **Type**: `boolean`
- **Default**: `true`
## `completion.postfixUfcsLeft`
Whether to enable left-variant UFCS-style completion. For example, `[A].table|` will be completed to `table(|)[A]`. Hint: Restarting the editor is required to change this setting.
- **Type**: `boolean`
- **Default**: `true`
## `completion.postfixUfcsRight`
Whether to enable right-variant UFCS-style completion. For example, `[A].table|` will be completed to `table([A], |)`. Hint: Restarting the editor is required to change this setting.
- **Type**: `boolean`
- **Default**: `true`

View file

@ -138,6 +138,34 @@ Whether to handle drag-and-drop of resources into the editing typst document. No
- `disable`
- **Default**: `"enable"`
## `tinymist.completion.postfix`
Whether to enable postfix code completion. For example, `[A].box|` will be completed to `box[A]|`. Hint: Restarting the editor is required to change this setting.
- **Type**: `boolean`
- **Default**: `true`
## `tinymist.completion.postfixUfcs`
Whether to enable UFCS-style completion. For example, `[A].box|` will be completed to `box[A]|`. Hint: Restarting the editor is required to change this setting.
- **Type**: `boolean`
- **Default**: `true`
## `tinymist.completion.postfixUfcsLeft`
Whether to enable left-variant UFCS-style completion. For example, `[A].table|` will be completed to `table(|)[A]`. Hint: Restarting the editor is required to change this setting.
- **Type**: `boolean`
- **Default**: `true`
## `tinymist.completion.postfixUfcsRight`
Whether to enable right-variant UFCS-style completion. For example, `[A].table|` will be completed to `table([A], |)`. Hint: Restarting the editor is required to change this setting.
- **Type**: `boolean`
- **Default**: `true`
## `tinymist.previewFeature`
Enable or disable preview features of Typst. Note: restarting the editor is required to change this setting.

View file

@ -446,6 +446,30 @@
"disable"
]
},
"tinymist.completion.postfix": {
"title": "Enable Postfix Code Completion",
"description": "Whether to enable postfix code completion. For example, `[A].box|` will be completed to `box[A]|`. Hint: Restarting the editor is required to change this setting.",
"type": "boolean",
"default": true
},
"tinymist.completion.postfixUfcs": {
"title": "Completion: Convert Field Access to Call",
"description": "Whether to enable UFCS-style completion. For example, `[A].box|` will be completed to `box[A]|`. Hint: Restarting the editor is required to change this setting.",
"type": "boolean",
"default": true
},
"tinymist.completion.postfixUfcsLeft": {
"title": "Completion: Convert Field Access to Call (Left Variant)",
"description": "Whether to enable left-variant UFCS-style completion. For example, `[A].table|` will be completed to `table(|)[A]`. Hint: Restarting the editor is required to change this setting.",
"type": "boolean",
"default": true
},
"tinymist.completion.postfixUfcsRight": {
"title": "Completion: Convert Field Access to Call (Right Variant)",
"description": "Whether to enable right-variant UFCS-style completion. For example, `[A].table|` will be completed to `table([A], |)`. Hint: Restarting the editor is required to change this setting.",
"type": "boolean",
"default": true
},
"tinymist.previewFeature": {
"title": "Enable preview features",
"description": "Enable or disable preview features of Typst. Note: restarting the editor is required to change this setting.",

View file

@ -59,7 +59,8 @@ const serverSideKeys = (() => {
}
return strings.map((x) => `tinymist.${x}`);
})();
const isServerSideConfig = (key) => serverSideKeys.includes(key);
const isServerSideConfig = (key) => serverSideKeys.includes(key) || serverSideKeys
.some((serverSideKey) => key.startsWith(`${serverSideKey}.`));
const configMd = (editor, prefix) =>
Object.keys(config)
.map((key) => {

View file

@ -374,7 +374,7 @@ fn e2e() {
});
let hash = replay_log(&tinymist_binary, &root.join("neovim"));
insta::assert_snapshot!(hash, @"siphash128_13:9bbc0892ae5974b0f43f50ef5a61ce2");
insta::assert_snapshot!(hash, @"siphash128_13:1739b86d5e2de99b19db308496ff94ae");
}
{
@ -385,7 +385,7 @@ fn e2e() {
});
let hash = replay_log(&tinymist_binary, &root.join("vscode"));
insta::assert_snapshot!(hash, @"siphash128_13:164530d2511ec0c6da2bcad239727add");
insta::assert_snapshot!(hash, @"siphash128_13:360f6d60de40f590e63ebf23521e3d50");
}
}