use std::cmp::Reverse;
use std::collections::{BTreeMap, HashSet};
use std::ops::Range;
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use lsp_types::InsertTextFormat;
use regex::{Captures, Regex};
use serde::{Deserialize, Serialize};
use tinymist_analysis::syntax::{bad_completion_cursor, BadCompletionCursor};
use tinymist_analysis::{analyze_labels, func_signature, DynLabel};
use tinymist_derive::BindTyCtx;
use tinymist_project::LspWorld;
use tinymist_std::path::unix_slash;
use tinymist_std::typst::TypstDocument;
use typst::foundations::{
fields_on, format_str, repr, AutoValue, Func, Label, NoneValue, Repr, Scope, StyleChain, Type,
Value,
};
use typst::syntax::ast::{self, AstNode, Param};
use typst::syntax::{is_id_continue, is_id_start, is_ident};
use typst::text::RawElem;
use typst::visualize::Color;
use typst::World;
use typst_shim::{syntax::LinkedNodeExt, utils::hash128};
use unscanny::Scanner;
use crate::adt::interner::Interned;
use crate::analysis::{BuiltinTy, LocalContext, PathPreference, Ty};
use crate::completion::{
Completion, CompletionCommand, CompletionContextKey, CompletionItem, CompletionKind,
EcoTextEdit, ParsedSnippet, PostfixSnippet, PostfixSnippetScope, PrefixSnippet,
DEFAULT_POSTFIX_SNIPPET, DEFAULT_PREFIX_SNIPPET,
};
use crate::prelude::*;
use crate::syntax::{
classify_context, interpret_mode_at, is_ident_like, node_ancestors, previous_decls,
surrounding_syntax, InterpretMode, PreviousDecl, SurroundingSyntax, SyntaxClass, SyntaxContext,
VarClass,
};
use crate::ty::{
DynTypeBounds, Iface, IfaceChecker, InsTy, SigTy, TyCtx, TypeInfo, TypeInterface, TypeVar,
};
use crate::upstream::{plain_docs_sentence, summarize_font_family};
use super::SharedContext;
mod field_access;
mod func;
mod import;
mod kind;
mod mode;
mod param;
mod path;
mod scope;
mod snippet;
#[path = "completion/type.rs"]
mod type_;
mod typst_specific;
use kind::*;
use scope::*;
use type_::*;
type LspCompletion = CompletionItem;
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionFeat {
#[serde(default)]
pub trigger_on_snippet_placeholders: bool,
#[serde(default)]
pub trigger_suggest: bool,
#[serde(default)]
pub trigger_parameter_hints: bool,
#[serde(default)]
pub trigger_suggest_and_parameter_hints: bool,
pub symbol: Option<SymbolCompletionWay>,
pub postfix: Option<bool>,
pub postfix_ufcs: Option<bool>,
pub postfix_ufcs_left: Option<bool>,
pub postfix_ufcs_right: Option<bool>,
pub postfix_snippets: Option<EcoVec<PostfixSnippet>>,
}
impl CompletionFeat {
pub(crate) fn postfix(&self) -> bool {
self.postfix.unwrap_or(true)
}
pub(crate) fn any_ufcs(&self) -> bool {
self.ufcs() || self.ufcs_left() || self.ufcs_right()
}
pub(crate) fn ufcs(&self) -> bool {
self.postfix() && self.postfix_ufcs.unwrap_or(true)
}
pub(crate) fn ufcs_left(&self) -> bool {
self.postfix() && self.postfix_ufcs_left.unwrap_or(true)
}
pub(crate) fn ufcs_right(&self) -> bool {
self.postfix() && self.postfix_ufcs_right.unwrap_or(true)
}
pub(crate) fn postfix_snippets(&self) -> &EcoVec<PostfixSnippet> {
self.postfix_snippets
.as_ref()
.unwrap_or(&DEFAULT_POSTFIX_SNIPPET)
}
pub(crate) fn is_stepless(&self) -> bool {
matches!(self.symbol, Some(SymbolCompletionWay::Stepless))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SymbolCompletionWay {
Step,
Stepless,
}
pub struct CompletionCursor<'a> {
ctx: Arc<SharedContext>,
from: usize,
cursor: usize,
source: Source,
text: &'a str,
before: &'a str,
after: &'a str,
leaf: LinkedNode<'a>,
syntax: Option<SyntaxClass<'a>>,
syntax_context: Option<SyntaxContext<'a>>,
surrounding_syntax: SurroundingSyntax,
last_lsp_range_pair: Option<(Range<usize>, LspRange)>,
ident_cursor: OnceLock<Option<SelectedNode<'a>>>,
arg_cursor: OnceLock<Option<SyntaxNode>>,
}
impl<'a> CompletionCursor<'a> {
pub fn new(ctx: Arc<SharedContext>, source: &'a Source, cursor: usize) -> Option<Self> {
let text = source.text();
let root = LinkedNode::new(source.root());
let leaf = root.leaf_at_compat(cursor)?;
let syntax = classify_syntax(leaf.clone(), cursor);
let syntax_context = classify_context(leaf.clone(), Some(cursor));
let surrounding_syntax = surrounding_syntax(&leaf);
crate::log_debug_ct!("CompletionCursor: syntax {leaf:?} -> {syntax:#?}");
crate::log_debug_ct!("CompletionCursor: context {leaf:?} -> {syntax_context:#?}");
crate::log_debug_ct!("CompletionCursor: surrounding {leaf:?} -> {surrounding_syntax:#?}");
Some(Self {
ctx,
text,
source: source.clone(),
before: &text[..cursor],
after: &text[cursor..],
leaf,
syntax,
syntax_context,
surrounding_syntax,
cursor,
from: cursor,
last_lsp_range_pair: None,
ident_cursor: OnceLock::new(),
arg_cursor: OnceLock::new(),
})
}
fn before_window(&self, size: usize) -> &str {
slice_at(
self.before,
self.cursor.saturating_sub(size)..self.before.len(),
)
}
fn is_callee(&self) -> bool {
matches!(self.syntax, Some(SyntaxClass::Callee(..)))
}
pub fn leaf_mode(&self) -> InterpretMode {
interpret_mode_at(Some(&self.leaf))
}
fn selected_node(&self) -> &Option<SelectedNode<'a>> {
self.ident_cursor.get_or_init(|| {
let is_from_ident = matches!(
self.syntax,
Some(SyntaxClass::Callee(..) | SyntaxClass::VarAccess(..))
) && is_ident_like(&self.leaf)
&& self.leaf.offset() == self.from;
if is_from_ident {
return Some(SelectedNode::Ident(self.leaf.clone()));
}
let is_from_label = matches!(self.syntax, Some(SyntaxClass::Label { .. }))
&& self.leaf.offset() + 1 == self.from;
if is_from_label {
return Some(SelectedNode::Label(self.leaf.clone()));
}
let is_from_ref = matches!(self.syntax, Some(SyntaxClass::Ref(..)))
&& self.leaf.offset() + 1 == self.from;
if is_from_ref {
return Some(SelectedNode::Ref(self.leaf.clone()));
}
None
})
}
fn arg_cursor(&self) -> &Option<SyntaxNode> {
self.arg_cursor.get_or_init(|| {
let mut args_node = None;
match self.syntax_context.clone() {
Some(SyntaxContext::Arg { args, .. }) => {
args_node = Some(args.cast::<ast::Args>()?.to_untyped().clone());
}
Some(SyntaxContext::Normal(node))
if (matches!(node.kind(), SyntaxKind::ContentBlock)
&& matches!(self.leaf.kind(), SyntaxKind::LeftBracket)) =>
{
args_node = node.parent().map(|s| s.get().clone());
}
Some(
SyntaxContext::Element { .. }
| SyntaxContext::ImportPath(..)
| SyntaxContext::IncludePath(..)
| SyntaxContext::VarAccess(..)
| SyntaxContext::Paren { .. }
| SyntaxContext::Label { .. }
| SyntaxContext::Normal(..),
)
| None => {}
}
args_node
})
}
fn lsp_range_of(&mut self, rng: Range<usize>) -> LspRange {
if let Some((last_rng, last_lsp_rng)) = &self.last_lsp_range_pair {
if *last_rng == rng {
return *last_lsp_rng;
}
}
let lsp_rng = self.ctx.to_lsp_range(rng.clone(), &self.source);
self.last_lsp_range_pair = Some((rng, lsp_rng));
lsp_rng
}
fn lsp_item_of(&mut self, item: &Completion) -> LspCompletion {
let mut snippet = item.apply.as_ref().unwrap_or(&item.label).clone();
let replace_range = match self.selected_node() {
Some(SelectedNode::Ident(from_ident)) => {
let mut rng = from_ident.range();
if !self.is_callee() && self.cursor != rng.end && is_arg_like_context(from_ident) {
if !snippet.trim_end().ends_with(',') {
snippet.push_str(", ");
}
rng.end = self.cursor;
}
self.lsp_range_of(rng)
}
Some(SelectedNode::Label(from_label)) => {
let mut rng = from_label.range();
if from_label.text().starts_with('<') && !snippet.starts_with('<') {
rng.start += 1;
}
if from_label.text().ends_with('>') && !snippet.ends_with('>') {
rng.end -= 1;
}
self.lsp_range_of(rng)
}
Some(SelectedNode::Ref(from_ref)) => {
let mut rng = from_ref.range();
if from_ref.text().starts_with('@') && !snippet.starts_with('@') {
rng.start += 1;
}
self.lsp_range_of(rng)
}
None => self.lsp_range_of(self.from..self.cursor),
};
let text_edit = EcoTextEdit::new(replace_range, snippet);
LspCompletion {
label: item.label.clone(),
kind: item.kind,
detail: item.detail.clone(),
sort_text: item.sort_text.clone(),
filter_text: item.filter_text.clone(),
label_details: item.label_details.clone().map(From::from),
text_edit: Some(text_edit),
additional_text_edits: item.additional_text_edits.clone(),
insert_text_format: Some(InsertTextFormat::SNIPPET),
command: item.command.clone(),
..Default::default()
}
}
}
type Cursor<'a> = CompletionCursor<'a>;
enum SelectedNode<'a> {
Ident(LinkedNode<'a>),
Label(LinkedNode<'a>),
Ref(LinkedNode<'a>),
}
pub struct CompletionWorker<'a> {
pub completions: Vec<LspCompletion>,
pub incomplete: bool,
ctx: &'a mut LocalContext,
document: Option<&'a TypstDocument>,
explicit: bool,
trigger_character: Option<char>,
seen_casts: HashSet<u128>,
seen_types: HashSet<Ty>,
seen_fields: HashSet<Interned<str>>,
}
impl<'a> CompletionWorker<'a> {
pub fn new(
ctx: &'a mut LocalContext,
document: Option<&'a TypstDocument>,
explicit: bool,
trigger_character: Option<char>,
) -> Option<Self> {
Some(Self {
ctx,
document,
trigger_character,
explicit,
incomplete: true,
completions: vec![],
seen_casts: HashSet::new(),
seen_types: HashSet::new(),
seen_fields: HashSet::new(),
})
}
pub fn world(&self) -> &LspWorld {
self.ctx.world()
}
fn seen_field(&mut self, field: Interned<str>) -> bool {
!self.seen_fields.insert(field)
}
fn enrich(&mut self, prefix: &str, suffix: &str) {
for LspCompletion { text_edit, .. } in &mut self.completions {
let apply = match text_edit {
Some(EcoTextEdit { new_text, .. }) => new_text,
_ => continue,
};
*apply = eco_format!("{prefix}{apply}{suffix}");
}
}
pub(crate) fn work(&mut self, cursor: &mut Cursor) -> Option<()> {
if let Some(SyntaxClass::VarAccess(var)) = &cursor.syntax {
let node = var.node();
match node.parent_kind() {
Some(SyntaxKind::LetBinding) => {
let parent = node.parent()?;
let parent_init = parent.cast::<ast::LetBinding>()?.init()?;
let parent_init = parent.find(parent_init.span())?;
parent_init.find(node.span())?;
}
Some(SyntaxKind::Closure) => {
let parent = node.parent()?;
let parent_body = parent.cast::<ast::Closure>()?.body();
let parent_body = parent.find(parent_body.span())?;
parent_body.find(node.span())?;
}
_ => {}
}
}
if matches!(
cursor.syntax,
Some(SyntaxClass::Callee(..) | SyntaxClass::VarAccess(..) | SyntaxClass::Normal(..))
) && cursor.leaf.erroneous()
{
let mut chars = cursor.leaf.text().chars();
match chars.next() {
Some(ch) if ch.is_numeric() => return None,
Some('.') => {
if matches!(chars.next(), Some(ch) if ch.is_numeric()) {
return None;
}
}
_ => {}
}
}
let self_ty = cursor.leaf.cast::<ast::Expr>().and_then(|leaf| {
let v = self.ctx.mini_eval(leaf)?;
Some(Ty::Value(InsTy::new(v)))
});
if let Some(self_ty) = self_ty {
self.seen_types.insert(self_ty);
};
let mut pair = Pair {
worker: self,
cursor,
};
let _ = pair.complete_cursor();
if let Some(SelectedNode::Ident(from_ident)) = cursor.selected_node() {
let ident_prefix = cursor.text[from_ident.offset()..cursor.cursor].to_string();
self.completions.retain(|item| {
let mut prefix_matcher = item.label.chars();
'ident_matching: for ch in ident_prefix.chars() {
for item in prefix_matcher.by_ref() {
if item == ch {
continue 'ident_matching;
}
}
return false;
}
true
});
}
for item in &mut self.completions {
if let Some(EcoTextEdit {
ref mut new_text, ..
}) = item.text_edit
{
*new_text = to_lsp_snippet(new_text);
}
}
Some(())
}
}
struct CompletionPair<'a, 'b, 'c> {
worker: &'c mut CompletionWorker<'a>,
cursor: &'c mut Cursor<'b>,
}
type Pair<'a, 'b, 'c> = CompletionPair<'a, 'b, 'c>;
impl CompletionPair<'_, '_, '_> {
pub(crate) fn complete_cursor(&mut self) -> Option<()> {
use SurroundingSyntax::*;
if matches!(
self.cursor.leaf.kind(),
SyntaxKind::LineComment | SyntaxKind::BlockComment
) {
return self.complete_comments().then_some(());
}
let surrounding_syntax = self.cursor.surrounding_syntax;
let mode = self.cursor.leaf_mode();
if matches!(surrounding_syntax, ImportList) {
return self.complete_imports().then_some(());
}
if matches!(surrounding_syntax, ParamList) {
return self.complete_params();
}
match self.cursor.syntax_context.clone() {
Some(SyntaxContext::Element { container, .. }) => {
if let Some(container) = container.cast::<ast::Dict>() {
for named in container.items() {
if let ast::DictItem::Named(named) = named {
self.worker.seen_field(named.name().into());
}
}
};
}
Some(SyntaxContext::Arg { args, .. }) => {
let args = args.cast::<ast::Args>()?;
for arg in args.items() {
if let ast::Arg::Named(named) = arg {
self.worker.seen_field(named.name().into());
}
}
}
Some(SyntaxContext::VarAccess(
var @ (VarClass::FieldAccess { .. } | VarClass::DotAccess { .. }),
)) => {
let target = var.accessed_node()?;
let field = var.accessing_field()?;
self.cursor.from = field.offset(&self.cursor.source)?;
self.doc_access_completions(&target);
return Some(());
}
Some(SyntaxContext::ImportPath(path) | SyntaxContext::IncludePath(path)) => {
let Some(ast::Expr::Str(str)) = path.cast() else {
return None;
};
self.cursor.from = path.offset();
let value = str.get();
if value.starts_with('@') {
let all_versions = value.contains(':');
self.package_completions(all_versions);
return Some(());
} else {
let paths = self.complete_path(&crate::analysis::PathPreference::Source {
allow_package: true,
});
self.worker.completions.extend(paths.unwrap_or_default());
}
return Some(());
}
Some(SyntaxContext::Normal(node)) if (matches!(node.kind(), SyntaxKind::Ref)) => {
self.cursor.from = self.cursor.leaf.offset() + 1;
self.ref_completions();
return Some(());
}
Some(
SyntaxContext::VarAccess(VarClass::Ident { .. })
| SyntaxContext::Paren { .. }
| SyntaxContext::Label { .. }
| SyntaxContext::Normal(..),
)
| None => {}
}
let cursor_pos = bad_completion_cursor(
self.cursor.syntax.as_ref(),
self.cursor.syntax_context.as_ref(),
&self.cursor.leaf,
);
let ty = self
.worker
.ctx
.post_type_of_node(self.cursor.leaf.clone())
.filter(|ty| !matches!(ty, Ty::Any))
.filter(|_| !matches!(cursor_pos, Some(BadCompletionCursor::ArgListPos)));
crate::log_debug_ct!(
"complete_type: {:?} -> ({surrounding_syntax:?}, {ty:#?})",
self.cursor.leaf
);
if is_ident_like(&self.cursor.leaf) {
self.cursor.from = self.cursor.leaf.offset();
} else if let Some(offset) = self
.cursor
.syntax
.as_ref()
.and_then(SyntaxClass::complete_offset)
{
self.cursor.from = offset;
}
if let Some(ty) = ty {
let filter = |ty: &Ty| match surrounding_syntax {
SurroundingSyntax::StringContent => match ty {
Ty::Builtin(
BuiltinTy::Path(..) | BuiltinTy::TextFont | BuiltinTy::TextFeature,
) => true,
Ty::Value(val) => matches!(val.val, Value::Str(..)),
_ => false,
},
_ => true,
};
let mut ctx = TypeCompletionWorker {
base: self,
filter: &filter,
};
ctx.type_completion(&ty, None);
}
let mut type_completions = std::mem::take(&mut self.worker.completions);
match mode {
InterpretMode::Code => {
self.complete_code();
}
InterpretMode::Math => {
self.complete_math();
}
InterpretMode::Raw => {
self.complete_markup();
}
InterpretMode::Markup => match surrounding_syntax {
Regular => {
self.complete_markup();
}
Selector | ShowTransform | SetRule => {
self.complete_code();
}
StringContent | ImportList | ParamList => {}
},
InterpretMode::Comment | InterpretMode::String => {}
};
match surrounding_syntax {
Regular | StringContent | ImportList | ParamList | SetRule => {}
Selector => {
self.snippet_completion(
"text selector",
"\"${text}\"",
"Replace occurrences of specific text.",
);
self.snippet_completion(
"regex selector",
"regex(\"${regex}\")",
"Replace matches of a regular expression.",
);
}
ShowTransform => {
self.snippet_completion(
"replacement",
"[${content}]",
"Replace the selected element with content.",
);
self.snippet_completion(
"replacement (string)",
"\"${text}\"",
"Replace the selected element with a string of text.",
);
self.snippet_completion(
"transformation",
"element => [${content}]",
"Transform the element with a function.",
);
}
}
crate::log_debug_ct!(
"sort completions: {type_completions:#?} {:#?}",
self.worker.completions
);
type_completions.sort_by(|a, b| {
a.sort_text
.as_ref()
.cmp(&b.sort_text.as_ref())
.then_with(|| a.label.cmp(&b.label))
});
self.worker.completions.sort_by(|a, b| {
a.sort_text
.as_ref()
.cmp(&b.sort_text.as_ref())
.then_with(|| a.label.cmp(&b.label))
});
for (idx, compl) in type_completions
.iter_mut()
.chain(self.worker.completions.iter_mut())
.enumerate()
{
compl.sort_text = Some(eco_format!("{idx:03}"));
}
self.worker.completions.append(&mut type_completions);
crate::log_debug_ct!("sort completions after: {:#?}", self.worker.completions);
if let Some(node) = self.cursor.arg_cursor() {
crate::log_debug_ct!("content block compl: args {node:?}");
let is_unclosed = matches!(node.kind(), SyntaxKind::Args)
&& node.children().fold(0i32, |acc, node| match node.kind() {
SyntaxKind::LeftParen => acc + 1,
SyntaxKind::RightParen => acc - 1,
SyntaxKind::Error if node.text() == "(" => acc + 1,
SyntaxKind::Error if node.text() == ")" => acc - 1,
_ => acc,
}) > 0;
if is_unclosed {
self.worker.enrich("", ")");
}
}
if self.cursor.before.ends_with(',') || self.cursor.before.ends_with(':') {
self.worker.enrich(" ", "");
}
match surrounding_syntax {
Regular | ImportList | ParamList | ShowTransform | SetRule | StringContent => {}
Selector => {
self.worker.enrich("", ": ${}");
}
}
crate::log_debug_ct!("enrich completions: {:?}", self.worker.completions);
Some(())
}
fn push_completion(&mut self, completion: Completion) {
self.worker
.completions
.push(self.cursor.lsp_item_of(&completion));
}
}
pub fn symbol_detail(ch: char) -> EcoString {
let ld = symbol_label_detail(ch);
if ld.starts_with("\\u") {
return ld;
}
format!("{}, unicode: `\\u{{{:04x}}}`", ld, ch as u32).into()
}
pub fn symbol_label_detail(ch: char) -> EcoString {
if !ch.is_whitespace() && !ch.is_control() {
return ch.into();
}
match ch {
' ' => "space".into(),
'\t' => "tab".into(),
'\n' => "newline".into(),
'\r' => "carriage return".into(),
'\u{200D}' => "zero width joiner".into(),
'\u{200C}' => "zero width non-joiner".into(),
'\u{200B}' => "zero width space".into(),
'\u{2060}' => "word joiner".into(),
'\u{00A0}' => "non-breaking space".into(),
'\u{202F}' => "narrow no-break space".into(),
'\u{2002}' => "en space".into(),
'\u{2003}' => "em space".into(),
'\u{2004}' => "three-per-em space".into(),
'\u{2005}' => "four-per-em space".into(),
'\u{2006}' => "six-per-em space".into(),
'\u{2007}' => "figure space".into(),
'\u{205f}' => "medium mathematical space".into(),
'\u{2008}' => "punctuation space".into(),
'\u{2009}' => "thin space".into(),
'\u{200A}' => "hair space".into(),
_ => format!("\\u{{{:04x}}}", ch as u32).into(),
}
}
fn slice_at(s: &str, mut rng: Range<usize>) -> &str {
while !rng.is_empty() && !s.is_char_boundary(rng.start) {
rng.start += 1;
}
while !rng.is_empty() && !s.is_char_boundary(rng.end) {
rng.end -= 1;
}
if rng.is_empty() {
return "";
}
&s[rng]
}
static TYPST_SNIPPET_PLACEHOLDER_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
fn to_lsp_snippet(typst_snippet: &str) -> EcoString {
let mut counter = 1;
let result = TYPST_SNIPPET_PLACEHOLDER_RE.replace_all(typst_snippet, |cap: &Captures| {
let substitution = format!("${{{}:{}}}", counter, &cap[1]);
counter += 1;
substitution
});
result.into()
}
fn is_hash_expr(leaf: &LinkedNode<'_>) -> bool {
is_hash_expr_(leaf).is_some()
}
fn is_hash_expr_(leaf: &LinkedNode<'_>) -> Option<()> {
match leaf.kind() {
SyntaxKind::Hash => Some(()),
SyntaxKind::Ident => {
let prev_leaf = leaf.prev_leaf()?;
if prev_leaf.kind() == SyntaxKind::Hash {
Some(())
} else {
None
}
}
_ => None,
}
}
fn is_triggered_by_punc(trigger_character: Option<char>) -> bool {
trigger_character.is_some_and(|ch| ch.is_ascii_punctuation())
}
fn is_arg_like_context(mut matching: &LinkedNode) -> bool {
while let Some(parent) = matching.parent() {
use SyntaxKind::*;
match parent.kind() {
ContentBlock | Equation | CodeBlock | Markup | Math | Code => return false,
Args | Params | Destructuring | Array | Dict => return true,
_ => {}
}
matching = parent;
}
false
}
#[cfg(test)]
mod tests {
use super::slice_at;
#[test]
fn test_before() {
const TEST_UTF8_STR: &str = "我们";
for i in 0..=TEST_UTF8_STR.len() {
for j in 0..=TEST_UTF8_STR.len() {
let _s = std::hint::black_box(slice_at(TEST_UTF8_STR, i..j));
}
}
}
}