tinymist-query 0.14.16

Language queries for tinymist.
//! Completion by typst specific semantics, like `font`, `package`, `label`, or
//! `typst::foundations::Value`.

use tinymist_std::time::yyyy_mm_dd;
use typst::foundations::Symbol;
use typst::syntax::is_valid_label_literal_id;

use super::*;
impl CompletionPair<'_, '_, '_> {
    /// Add completions for all font families.
    pub fn font_completions(&mut self) {
        let equation = self.cursor.before_window(25).contains("equation");
        for (family, iter) in self.worker.world().clone().book().families() {
            let detail = summarize_font_family(iter);
            if !equation || family.contains("Math") {
                self.value_completion(
                    None,
                    &Value::Str(family.into()),
                    false,
                    Some(detail.as_str()),
                );
            }
        }
    }

    /// Add completions for current font features.
    pub fn font_feature_completions(&mut self) {
        // todo: add me
    }

    /// Add completions for all available packages.
    pub fn package_completions(&mut self, all_versions: bool) {
        let w = self.worker.world().clone();
        // Finds packages that are in `@preview`
        let mut packages = w.packages().iter().collect_vec();

        // Finds packages that are not in `@preview`
        #[cfg(feature = "local-registry")]
        let other_packages_refs = self.worker.ctx.non_preview_packages();
        #[cfg(feature = "local-registry")]
        packages.extend(other_packages_refs.iter());

        packages.sort_by_key(|entry| {
            (
                &entry.namespace,
                &entry.package.name,
                Reverse(entry.package.version),
            )
        });
        if !all_versions {
            packages.dedup_by_key(|entry| (&entry.namespace, &entry.package.name));
        }
        let completion_info = packages
            .iter()
            .map(|entry| {
                let mut docs = String::new();
                if let Some(desc) = &entry.package.description {
                    docs.push_str(desc);
                    docs.push_str("\n\n");
                }
                docs.push_str(&format!("Authors: {}\n", entry.package.authors.join(", ")));
                docs.push_str(&format!("Version: {}\n", entry.package.version));
                if let Some(updated_at) = &entry.updated_at
                    && let Ok(formatted) = updated_at.format(&yyyy_mm_dd())
                {
                    docs.push_str(&format!("Updated: {formatted}\n"));
                }
                (format_str!("{}", entry.spec()), docs)
            })
            .collect_vec(); // avoid self borrow
        for (value, docs) in completion_info {
            self.value_completion(None, &Value::Str(value), false, Some(&docs));
        }
    }

    /// Add completions for raw block tags.
    pub fn raw_completions(&mut self) {
        for (name, mut tags) in RawElem::languages() {
            let lower = name.to_lowercase();
            if !tags.contains(&lower.as_str()) {
                tags.push(lower.as_str());
            }

            tags.retain(|tag| is_ident(tag));
            if tags.is_empty() {
                continue;
            }

            self.push_completion(Completion {
                kind: CompletionKind::Constant,
                label: name.into(),
                apply: Some(tags[0].into()),
                detail: Some(repr::separated_list(&tags, " or ").into()),
                ..Completion::default()
            });
        }
    }

    /// Add completions for labels and references.
    pub fn ref_completions(&mut self) {
        self.label_completions_(false, true);
    }

    /// Add completions for labels and references.
    pub fn label_completions(&mut self, only_citation: bool) {
        self.label_completions_(only_citation, false);
    }

    fn label_completion_apply(
        &mut self,
        label: &EcoString,
        open: bool,
        close: bool,
    ) -> (EcoString, Option<Vec<EcoTextEdit>>) {
        if is_valid_label_literal_id(label.as_str()) {
            return (
                eco_format!(
                    "{}{}{}",
                    if open { "<" } else { "" },
                    label.as_str(),
                    if close { ">" } else { "" }
                ),
                None,
            );
        }
        let mut edits = vec![];

        let (remove_open, remove_close) = match self.cursor.selected_node() {
            Some(SelectedNode::Label(node)) => {
                let range = node.range();
                let remove_open = node
                    .text()
                    .starts_with('<')
                    .then_some(range.start..range.start + 1);
                let remove_close = node
                    .text()
                    .ends_with('>')
                    .then_some(range.end - 1..range.end);
                (remove_open, remove_close)
            }
            _ => (
                (self.cursor.from > 0 && self.cursor.text[..self.cursor.from].ends_with('<'))
                    .then_some(self.cursor.from - 1..self.cursor.from),
                self.cursor
                    .after
                    .starts_with('>')
                    .then_some(self.cursor.cursor..self.cursor.cursor + 1),
            ),
        };

        if let Some(range) = remove_open {
            edits.push(EcoTextEdit::new(
                self.cursor.lsp_range_of(range),
                EcoString::new(),
            ));
        }
        if let Some(range) = remove_close {
            edits.push(EcoTextEdit::new(
                self.cursor.lsp_range_of(range),
                EcoString::new(),
            ));
        }

        (
            eco_format!("label({})", label.as_str().repr()),
            (!edits.is_empty()).then_some(edits),
        )
    }

    /// Add completions for labels and references.
    pub fn label_completions_(&mut self, only_citation: bool, ref_label: bool) {
        let Some(document) = self.worker.document else {
            return;
        };
        let (labels, split) = analyze_labels(document);

        let head = &self.cursor.text[..self.cursor.from];
        let at = head.ends_with('@');
        let open = !at && !head.ends_with('<');
        let close = !at && !self.cursor.after.starts_with('>');
        let citation = !at && only_citation;

        let (skip, take) = if at || ref_label {
            (0, usize::MAX)
        } else if citation {
            (split, usize::MAX)
        } else {
            (0, split)
        };

        for DynLabel {
            label,
            label_desc,
            detail,
            bib_title,
        } in labels.into_iter().skip(skip).take(take)
        {
            if !self.worker.seen_casts.insert(hash128(&label)) {
                continue;
            }
            let label: EcoString = label.resolve().as_str().into();
            let (apply, additional_text_edits) = self.label_completion_apply(&label, open, close);
            let completion = Completion {
                kind: CompletionKind::Reference,
                apply: Some(apply),
                additional_text_edits,
                label: label.clone(),
                label_details: label_desc.clone(),
                filter_text: Some(label.clone()),
                detail: detail.clone(),
                ..Completion::default()
            };

            if let Some(bib_title) = bib_title {
                // Note that this completion re-uses the above `apply` field to
                // alter the `bib_title` to the corresponding label.
                self.push_completion(Completion {
                    kind: CompletionKind::Constant,
                    label: bib_title.clone(),
                    label_details: Some(label),
                    filter_text: Some(bib_title),
                    detail,
                    ..completion.clone()
                });
            }

            self.push_completion(completion);
        }
    }

    /// Add a completion for a specific value.
    pub fn value_completion(
        &mut self,
        label: Option<EcoString>,
        value: &Value,
        parens: bool,
        docs: Option<&str>,
    ) {
        self.value_completion_(
            value,
            ValueCompletionInfo {
                label,
                parens,
                label_details: None,
                docs,
                bound_self: false,
            },
        );
    }

    /// Add a completion for a specific value.
    pub fn value_completion_(&mut self, value: &Value, extras: ValueCompletionInfo) {
        let ValueCompletionInfo {
            label,
            parens,
            label_details,
            docs,
            bound_self,
        } = extras;

        // Prevent duplicate completions from appearing.
        if !self.worker.seen_casts.insert(hash128(&(&label, &value))) {
            return;
        }

        let at = label.as_deref().is_some_and(|field| !is_ident(field));
        let label = label.unwrap_or_else(|| value.repr());

        let detail = docs.map(Into::into).or_else(|| match value {
            Value::Symbol(symbol) => Some(symbol_detail(symbol.get())),
            Value::Func(func) => func.docs().map(plain_docs_sentence),
            Value::Type(ty) => Some(plain_docs_sentence(ty.docs())),
            v => {
                let repr = v.repr();
                (repr.as_str() != label).then_some(repr)
            }
        });
        let label_details = label_details.or_else(|| match value {
            Value::Symbol(s) => Some(symbol_label_detail(s.get())),
            _ => None,
        });

        let mut apply = None;
        if parens && matches!(value, Value::Func(_)) {
            let mode = self.cursor.leaf_mode();
            let ty = Ty::Value(InsTy::new(value.clone()));
            let kind_checker = CompletionKindChecker {
                symbols: HashSet::default(),
                functions: HashSet::from_iter([&ty]),
            };
            let mut fn_feat = FnCompletionFeat::default();
            // todo: unify bound self checking
            fn_feat.bound_self = bound_self;
            let fn_feat = fn_feat.check(kind_checker.functions.iter().copied());
            self.func_completion(mode, fn_feat, label, label_details, detail, parens);
            return;
        } else if at {
            apply = Some(eco_format!("at(\"{label}\")"));
        } else {
            let apply_label = &mut label.as_str();
            if apply_label.ends_with('"')
                && self.cursor.after.starts_with('"')
                && let Some(trimmed) = apply_label.strip_suffix('"')
            {
                *apply_label = trimmed;
            }
            let from_before = slice_at(self.cursor.text, 0..self.cursor.from);
            if apply_label.starts_with('"')
                && from_before.ends_with('"')
                && let Some(trimmed) = apply_label.strip_prefix('"')
            {
                *apply_label = trimmed;
            }

            if apply_label.len() != label.len() {
                apply = Some((*apply_label).into());
            }
        }

        self.push_completion(Completion {
            kind: value_to_completion_kind(value),
            label,
            apply,
            detail,
            label_details,
            ..Completion::default()
        });
    }

    pub fn symbol_completions(&mut self, label: EcoString, symbol: &Symbol) {
        let sym_val = symbol.get();
        let kind = CompletionKind::Symbol(sym_val.into());
        self.push_completion(Completion {
            kind,
            label: label.clone(),
            label_details: Some(symbol_label_detail(sym_val)),
            detail: Some(symbol_detail(sym_val)),
            ..Completion::default()
        });

        let is_stepless = self.cursor.ctx.analysis.completion_feat.is_stepless();
        if is_stepless {
            self.symbol_var_completions(symbol, Some(&label));
        }
    }

    pub fn symbol_var_completions(&mut self, symbol: &Symbol, prefix: Option<&str>) {
        for modifier in symbol.modifiers() {
            if let Ok(modified) = symbol.clone().modified((), modifier) {
                let label = match &prefix {
                    Some(prefix) => eco_format!("{prefix}.{modifier}"),
                    None => modifier.into(),
                };

                self.symbol_completions(label, &modified);
            }
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct ValueCompletionInfo<'a> {
    pub label: Option<EcoString>,
    pub parens: bool,
    pub label_details: Option<EcoString>,
    pub docs: Option<&'a str>,
    pub bound_self: bool,
}