mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
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:
parent
a1a15a6795
commit
d0b40dbfa6
20 changed files with 570 additions and 178 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,6 +346,14 @@ impl<'a> CompletionContext<'a> {
|
|||
..Completion::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SurroundingSyntax {
|
||||
Regular,
|
||||
Selector,
|
||||
SetRule,
|
||||
}
|
||||
|
||||
fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option<SurroundingSyntax> {
|
||||
use SurroundingSyntax::*;
|
||||
|
@ -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);
|
||||
|
@ -262,8 +417,6 @@ impl<'a> CompletionContext<'a> {
|
|||
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BindTyCtx)]
|
||||
#[bind(types)]
|
||||
|
@ -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 &[
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue