diff --git a/crates/tinymist-query/src/analysis/global.rs b/crates/tinymist-query/src/analysis/global.rs index 0c2efbbf..f565252e 100644 --- a/crates/tinymist-query/src/analysis/global.rs +++ b/crates/tinymist-query/src/analysis/global.rs @@ -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. diff --git a/crates/tinymist-query/src/completion.rs b/crates/tinymist-query/src/completion.rs index 8638e962..67b56327 100644 --- a/crates/tinymist-query/src/completion.rs +++ b/crates/tinymist-query/src/completion.rs @@ -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 diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_args.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_args.typ.snap index ff18d1b4..48f0a627 100644 --- a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_args.typ.snap +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_args.typ.snap @@ -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": { diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_builtin_args.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_builtin_args.typ.snap index 21f9db40..bdd8e4ce 100644 --- a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_builtin_args.typ.snap +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_builtin_args.typ.snap @@ -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": { diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_with_args.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_with_args.typ.snap index da4c927e..b9803f7f 100644 --- a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_with_args.typ.snap +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_with_args.typ.snap @@ -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": { diff --git a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-core-slides.typ-2.snap b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-core-slides.typ-2.snap index 550ce214..1a6c90de 100644 --- a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-core-slides.typ-2.snap +++ b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-core-slides.typ-2.snap @@ -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": { diff --git a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-core-slides.typ.snap b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-core-slides.typ.snap index ea80d711..8e062fcf 100644 --- a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-core-slides.typ.snap +++ b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-core-slides.typ.snap @@ -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": { diff --git a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-cover-with-rect.typ.snap b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-cover-with-rect.typ.snap index c1ec150f..373a28a1 100644 --- a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-cover-with-rect.typ.snap +++ b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-cover-with-rect.typ.snap @@ -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": { diff --git a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-current-heading.typ-2.snap b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-current-heading.typ-2.snap index 168dda0d..06765202 100644 --- a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-current-heading.typ-2.snap +++ b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-current-heading.typ-2.snap @@ -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": { diff --git a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-markup-text.typ.snap b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-markup-text.typ.snap index 5f238d82..51053d43 100644 --- a/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-markup-text.typ.snap +++ b/crates/tinymist-query/src/fixtures/pkgs/snaps/test@touying-utils-markup-text.typ.snap @@ -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": { diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs index f038346b..65f84348 100644 --- a/crates/tinymist-query/src/lib.rs +++ b/crates/tinymist-query/src/lib.rs @@ -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::*; diff --git a/crates/tinymist-query/src/upstream/complete.rs b/crates/tinymist-query/src/upstream/complete.rs index 9efd5193..82836428 100644 --- a/crates/tinymist-query/src/upstream/complete.rs +++ b/crates/tinymist-query/src/upstream/complete.rs @@ -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, /// An optional short description, at most one sentence. pub detail: Option, + /// 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>, /// 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) { +fn field_access_completions( + ctx: &mut CompletionContext, + node: &LinkedNode, + value: &Value, + styles: &Option, +) { 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() { diff --git a/crates/tinymist-query/src/upstream/complete/ext.rs b/crates/tinymist-query/src/upstream/complete/ext.rs index a9023e0d..580756f7 100644 --- a/crates/tinymist-query/src/upstream/complete/ext.rs +++ b/crates/tinymist-query/src/upstream/complete/ext.rs @@ -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, + /// Whether to enable ufcs completion. + pub postfix_ufcs: Option, + /// Whether to enable ufcs completion (left variant). + pub postfix_ufcs_left: Option, + /// Whether to enable ufcs completion (right variant). + pub postfix_ufcs_right: Option, +} + +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,73 +346,78 @@ impl<'a> CompletionContext<'a> { ..Completion::default() }); } - - fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option { - use SurroundingSyntax::*; - let mut met_args = false; - while let Some(parent) = leaf.parent() { - log::debug!( - "check_surrounding_syntax: {:?}::{:?}", - parent.kind(), - leaf.kind() - ); - match parent.kind() { - SyntaxKind::CodeBlock | SyntaxKind::ContentBlock | SyntaxKind::Equation => { - return Some(Regular); - } - SyntaxKind::Named => { - return Some(Regular); - } - SyntaxKind::Args => { - met_args = true; - } - SyntaxKind::SetRule => { - let rule = parent.get().cast::()?; - if met_args || encolsed_by(parent, rule.condition().map(|s| s.span()), leaf) - { - return Some(Regular); - } else { - return Some(SetRule); - } - } - SyntaxKind::ShowRule => { - let rule = parent.get().cast::()?; - if encolsed_by(parent, Some(rule.transform().span()), leaf) { - return Some(Regular); - } else { - return Some(Selector); // query's first argument - } - } - _ => {} - } - - leaf = parent; - } - - None - } - - fn check_previous_syntax(leaf: &LinkedNode) -> Option { - let mut leaf = leaf.clone(); - if leaf.kind().is_trivia() { - leaf = leaf.prev_sibling()?; - } - if matches!(leaf.kind(), SyntaxKind::ShowRule | SyntaxKind::SetRule) { - return check_surrounding_syntax(&leaf.rightmost_leaf()?); - } - - if matches!(leaf.kind(), SyntaxKind::Show) { - return Some(SurroundingSyntax::Selector); - } - if matches!(leaf.kind(), SyntaxKind::Set) { - return Some(SurroundingSyntax::SetRule); - } - - None - } } } +enum SurroundingSyntax { + Regular, + Selector, + SetRule, +} + +fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option { + use SurroundingSyntax::*; + let mut met_args = false; + while let Some(parent) = leaf.parent() { + log::debug!( + "check_surrounding_syntax: {:?}::{:?}", + parent.kind(), + leaf.kind() + ); + match parent.kind() { + SyntaxKind::CodeBlock | SyntaxKind::ContentBlock | SyntaxKind::Equation => { + return Some(Regular); + } + SyntaxKind::Named => { + return Some(Regular); + } + SyntaxKind::Args => { + met_args = true; + } + SyntaxKind::SetRule => { + let rule = parent.get().cast::()?; + if met_args || encolsed_by(parent, rule.condition().map(|s| s.span()), leaf) { + return Some(Regular); + } else { + return Some(SetRule); + } + } + SyntaxKind::ShowRule => { + let rule = parent.get().cast::()?; + if encolsed_by(parent, Some(rule.transform().span()), leaf) { + return Some(Regular); + } else { + return Some(Selector); // query's first argument + } + } + _ => {} + } + + leaf = parent; + } + + None +} + +fn check_previous_syntax(leaf: &LinkedNode) -> Option { + let mut leaf = leaf.clone(); + if leaf.kind().is_trivia() { + leaf = leaf.prev_sibling()?; + } + if matches!(leaf.kind(), SyntaxKind::ShowRule | SyntaxKind::SetRule) { + return check_surrounding_syntax(&leaf.rightmost_leaf()?); + } + + if matches!(leaf.kind(), SyntaxKind::Show) { + return Some(SurroundingSyntax::Selector); + } + if matches!(leaf.kind(), SyntaxKind::Set) { + return Some(SurroundingSyntax::SetRule); + } + + None +} + #[derive(BindTyCtx)] #[bind(types)] struct Defines { @@ -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, + min_named: Option, + 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)) => { - 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:?}"), + Ty::Builtin(b) => match b { + BuiltinTy::Element(func) => { + self.is_element = true; + let func = (*func).into(); + let sig = func_signature(func).type_sig(); + self.check_sig(&sig, pos); + } + 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,56 +784,80 @@ 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 { - Some(CompletionKind::Variable) => Some(CompletionKind::Variable), - Some(acc) => { - let kind = ty_to_completion_kind(ty); - if acc == kind { - Some(acc) - } else { - Some(CompletionKind::Variable) - } - } - None => Some(ty_to_completion_kind(ty)), - }) - .unwrap_or(CompletionKind::Variable), - _ => panic!("ty_to_completion_kind {ty:?}"), + 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) -> CompletionKind { + tys.fold(None, |acc, ty| match acc { + Some(CompletionKind::Variable) => Some(CompletionKind::Variable), + Some(acc) => { + let kind = ty_to_completion_kind(ty); + if acc == kind { + Some(acc) + } else { + Some(CompletionKind::Variable) + } + } + None => Some(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 &[ diff --git a/crates/tinymist/src/actor/mod.rs b/crates/tinymist/src/actor/mod.rs index f9be4f47..68e0239c 100644 --- a/crates/tinymist/src/actor/mod.rs +++ b/crates/tinymist/src/actor/mod.rs @@ -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, diff --git a/crates/tinymist/src/init.rs b/crates/tinymist/src/init.rs index 44d1a36b..dbb02cf0 100644 --- a/crates/tinymist/src/init.rs +++ b/crates/tinymist/src/init.rs @@ -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) -> 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); + 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() } diff --git a/editors/neovim/Configuration.md b/editors/neovim/Configuration.md index 41fc8663..28da8749 100644 --- a/editors/neovim/Configuration.md +++ b/editors/neovim/Configuration.md @@ -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` diff --git a/editors/vscode/Configuration.md b/editors/vscode/Configuration.md index 8ecb6a77..a59a598a 100644 --- a/editors/vscode/Configuration.md +++ b/editors/vscode/Configuration.md @@ -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. diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 76c16a3a..a36c67bb 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -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.", diff --git a/editors/vscode/scripts/config-man.cjs b/editors/vscode/scripts/config-man.cjs index 37a4e763..87098257 100644 --- a/editors/vscode/scripts/config-man.cjs +++ b/editors/vscode/scripts/config-man.cjs @@ -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) => { diff --git a/tests/e2e/main.rs b/tests/e2e/main.rs index 878203ab..d8d93abd 100644 --- a/tests/e2e/main.rs +++ b/tests/e2e/main.rs @@ -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"); } }