mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 05:05:00 +00:00
refactor: merge completion stuff (#1074)
* dev: move two syntax completion to ext * feat: move mode completion in ext.rs * dev: clean code * refactor: merge completion stuff * test: update snapshot
This commit is contained in:
parent
1059ea7e66
commit
9c8d1461b7
35 changed files with 1765 additions and 1733 deletions
|
@ -194,7 +194,7 @@ withs = "withs"
|
|||
|
||||
[workspace.metadata.typos.files]
|
||||
ignore-hidden = false
|
||||
extend-exclude = ["/.git", "fixtures", "upstream"]
|
||||
extend-exclude = ["/.git", "fixtures"]
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ use std::path::Path;
|
|||
pub(crate) use bib::*;
|
||||
pub mod call;
|
||||
pub use call::*;
|
||||
pub mod completion;
|
||||
pub use completion::*;
|
||||
pub mod code_action;
|
||||
pub use code_action::*;
|
||||
pub mod color_expr;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -26,7 +26,7 @@ use crate::adt::revision::{RevisionLock, RevisionManager, RevisionManagerLike, R
|
|||
use crate::analysis::prelude::*;
|
||||
use crate::analysis::{
|
||||
analyze_bib, analyze_expr_, analyze_import_, analyze_signature, definition, post_type_check,
|
||||
AllocStats, AnalysisStats, BibInfo, Definition, PathPreference, QueryStatGuard,
|
||||
AllocStats, AnalysisStats, BibInfo, CompletionFeat, Definition, PathPreference, QueryStatGuard,
|
||||
SemanticTokenCache, SemanticTokenContext, SemanticTokens, Signature, SignatureTarget, Ty,
|
||||
TypeInfo,
|
||||
};
|
||||
|
@ -35,7 +35,7 @@ use crate::syntax::{
|
|||
classify_syntax, construct_module_dependencies, resolve_id_by_path, scan_workspace_files, Decl,
|
||||
DefKind, ExprInfo, ExprRoute, LexicalScope, ModuleDependency, SyntaxClass,
|
||||
};
|
||||
use crate::upstream::{tooltip_, CompletionFeat, Tooltip};
|
||||
use crate::upstream::{tooltip_, Tooltip};
|
||||
use crate::{
|
||||
ColorTheme, CompilerQueryRequest, LspPosition, LspRange, LspWorldExt, PositionEncoding,
|
||||
VersionedDocument,
|
||||
|
|
|
@ -7,16 +7,14 @@ use regex::{Captures, Regex};
|
|||
use typst_shim::syntax::LinkedNodeExt;
|
||||
|
||||
use crate::{
|
||||
analysis::{InsTy, Ty},
|
||||
analysis::{complete_type_and_syntax, CompletionWorker, InsTy, Ty},
|
||||
prelude::*,
|
||||
syntax::{is_ident_like, SyntaxClass},
|
||||
upstream::{autocomplete, CompletionContext},
|
||||
StatefulRequest,
|
||||
};
|
||||
|
||||
pub(crate) type LspCompletion = lsp_types::CompletionItem;
|
||||
pub(crate) type LspCompletionKind = lsp_types::CompletionItemKind;
|
||||
pub(crate) type TypstCompletionKind = crate::upstream::CompletionKind;
|
||||
|
||||
pub(crate) mod snippet;
|
||||
|
||||
|
@ -130,11 +128,10 @@ impl StatefulRequest for CompletionRequest {
|
|||
}
|
||||
}
|
||||
|
||||
let mut completion_items_rest = None;
|
||||
let is_incomplete = false;
|
||||
|
||||
let mut cc_ctx =
|
||||
CompletionContext::new(ctx, doc, &source, cursor, explicit, self.trigger_character)?;
|
||||
CompletionWorker::new(ctx, doc, &source, cursor, explicit, self.trigger_character)?;
|
||||
|
||||
// Exclude it self from auto completion
|
||||
// e.g. `#let x = (1.);`
|
||||
|
@ -147,10 +144,15 @@ impl StatefulRequest for CompletionRequest {
|
|||
cc_ctx.seen_types.insert(self_ty);
|
||||
};
|
||||
|
||||
let (offset, ic, mut completions, completions_items2) = autocomplete(cc_ctx)?;
|
||||
if !completions_items2.is_empty() {
|
||||
completion_items_rest = Some(completions_items2);
|
||||
}
|
||||
let _ = complete_type_and_syntax(&mut cc_ctx);
|
||||
let CompletionWorker {
|
||||
from: offset,
|
||||
incomplete: ic,
|
||||
mut completions,
|
||||
mut completion_items_rest,
|
||||
..
|
||||
} = cc_ctx;
|
||||
|
||||
// todo: define it well, we were needing it because we wanted to do interactive
|
||||
// path completion, but now we've scanned all the paths at the same time.
|
||||
// is_incomplete = ic;
|
||||
|
@ -223,7 +225,7 @@ impl StatefulRequest for CompletionRequest {
|
|||
|
||||
LspCompletion {
|
||||
label: typst_completion.label.to_string(),
|
||||
kind: Some(completion_kind(typst_completion.kind.clone())),
|
||||
kind: Some(typst_completion.kind.into()),
|
||||
detail: typst_completion.detail.as_ref().map(String::from),
|
||||
sort_text: typst_completion.sort_text.as_ref().map(String::from),
|
||||
filter_text: typst_completion.filter_text.as_ref().map(String::from),
|
||||
|
@ -248,10 +250,7 @@ impl StatefulRequest for CompletionRequest {
|
|||
}
|
||||
});
|
||||
let mut items = completions.collect_vec();
|
||||
|
||||
if let Some(items_rest) = completion_items_rest.as_mut() {
|
||||
items.append(items_rest);
|
||||
}
|
||||
items.append(&mut completion_items_rest);
|
||||
|
||||
// To response completions in fine-grained manner, we need to mark result as
|
||||
// incomplete. This follows what rust-analyzer does.
|
||||
|
@ -263,23 +262,6 @@ impl StatefulRequest for CompletionRequest {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn completion_kind(typst_completion_kind: TypstCompletionKind) -> LspCompletionKind {
|
||||
match typst_completion_kind {
|
||||
TypstCompletionKind::Syntax => LspCompletionKind::SNIPPET,
|
||||
TypstCompletionKind::Func => LspCompletionKind::FUNCTION,
|
||||
TypstCompletionKind::Param => LspCompletionKind::VARIABLE,
|
||||
TypstCompletionKind::Field => LspCompletionKind::FIELD,
|
||||
TypstCompletionKind::Variable => LspCompletionKind::VARIABLE,
|
||||
TypstCompletionKind::Constant => LspCompletionKind::CONSTANT,
|
||||
TypstCompletionKind::Reference => LspCompletionKind::REFERENCE,
|
||||
TypstCompletionKind::Symbol(_) => LspCompletionKind::FIELD,
|
||||
TypstCompletionKind::Type => LspCompletionKind::CLASS,
|
||||
TypstCompletionKind::Module => LspCompletionKind::MODULE,
|
||||
TypstCompletionKind::File => LspCompletionKind::FILE,
|
||||
TypstCompletionKind::Folder => LspCompletionKind::FOLDER,
|
||||
}
|
||||
}
|
||||
|
||||
static TYPST_SNIPPET_PLACEHOLDER_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(content, delta: int) => strong"
|
||||
},
|
||||
"sortText": "180",
|
||||
"textEdit": {
|
||||
"newText": "strong(${1:})",
|
||||
"range": {
|
||||
|
@ -35,6 +36,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(content, delta: int) => strong"
|
||||
},
|
||||
"sortText": "181",
|
||||
"textEdit": {
|
||||
"newText": "strong[${1:}]",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(content, b: content | none, bl: content | none, br: content | none, t: content | none, tl: content | none, tr: content | none) => attach"
|
||||
},
|
||||
"sortText": "075",
|
||||
"textEdit": {
|
||||
"newText": " attach(${1:})",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(content, b: content | none, bl: content | none, br: content | none, t: content | none, tl: content | none, tr: content | none) => attach"
|
||||
},
|
||||
"sortText": "075",
|
||||
"textEdit": {
|
||||
"newText": " attach(${1:})",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||
},
|
||||
"sortText": "127",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak()${1:}",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||
},
|
||||
"sortText": "127",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak()${1:}",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(any, any, any) => none"
|
||||
},
|
||||
"sortText": "000",
|
||||
"textEdit": {
|
||||
"newText": "aa(${1:})",
|
||||
"range": {
|
||||
|
@ -35,6 +36,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "any"
|
||||
},
|
||||
"sortText": "001",
|
||||
"textEdit": {
|
||||
"newText": "aab",
|
||||
"range": {
|
||||
|
@ -55,6 +57,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "any"
|
||||
},
|
||||
"sortText": "002",
|
||||
"textEdit": {
|
||||
"newText": "aabc",
|
||||
"range": {
|
||||
|
@ -75,6 +78,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "any"
|
||||
},
|
||||
"sortText": "003",
|
||||
"textEdit": {
|
||||
"newText": "aac",
|
||||
"range": {
|
||||
|
@ -100,6 +104,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "any"
|
||||
},
|
||||
"sortText": "002",
|
||||
"textEdit": {
|
||||
"newText": "aabc",
|
||||
"range": {
|
||||
|
@ -120,6 +125,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "any"
|
||||
},
|
||||
"sortText": "003",
|
||||
"textEdit": {
|
||||
"newText": "aac",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||
},
|
||||
"sortText": "126",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak()${1:}",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||
},
|
||||
"sortText": "126",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak()${1:}",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||
},
|
||||
"sortText": "126",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak()${1:}",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||
},
|
||||
"sortText": "126",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak()${1:}",
|
||||
"range": {
|
||||
|
|
|
@ -17,7 +17,7 @@ snapshot_kind: text
|
|||
"kind": 3,
|
||||
"label": "table-prefix",
|
||||
"textEdit": {
|
||||
"newText": "table-prefix(${1:})",
|
||||
"newText": "table-prefix",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 24,
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "module(base)"
|
||||
},
|
||||
"sortText": "008",
|
||||
"textEdit": {
|
||||
"newText": "base",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "module(base)"
|
||||
},
|
||||
"sortText": "008",
|
||||
"textEdit": {
|
||||
"newText": "baz",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "module(base)"
|
||||
},
|
||||
"sortText": "012",
|
||||
"textEdit": {
|
||||
"newText": "baz",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "1"
|
||||
},
|
||||
"sortText": "000",
|
||||
"textEdit": {
|
||||
"newText": "a",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||
},
|
||||
"sortText": "128",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak()${1:}",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||
},
|
||||
"sortText": "128",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak()${1:}",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||
},
|
||||
"sortText": "128",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak()${1:}",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box"
|
||||
},
|
||||
"sortText": "015",
|
||||
"textEdit": {
|
||||
"newText": "box(${1:})",
|
||||
"range": {
|
||||
|
@ -35,6 +36,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box"
|
||||
},
|
||||
"sortText": "016",
|
||||
"textEdit": {
|
||||
"newText": "box[${1:}]",
|
||||
"range": {
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "(content) => content"
|
||||
},
|
||||
"sortText": "086",
|
||||
"textEdit": {
|
||||
"newText": "bold(${1:})",
|
||||
"range": {
|
||||
|
|
|
@ -8,6 +8,28 @@ snapshot_kind: text
|
|||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": []
|
||||
"items": [
|
||||
{
|
||||
"kind": 5,
|
||||
"label": "Re",
|
||||
"labelDetails": {
|
||||
"description": "ℜ"
|
||||
},
|
||||
"sortText": "039",
|
||||
"textEdit": {
|
||||
"newText": "Re",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 2
|
||||
},
|
||||
"start": {
|
||||
"character": 2,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
|||
"labelDetails": {
|
||||
"description": "ℜ"
|
||||
},
|
||||
"sortText": "039",
|
||||
"textEdit": {
|
||||
"newText": "Re",
|
||||
"range": {
|
||||
|
|
|
@ -12,6 +12,7 @@ snapshot_kind: text
|
|||
{
|
||||
"kind": 21,
|
||||
"label": "Typst",
|
||||
"sortText": "142",
|
||||
"textEdit": {
|
||||
"newText": "typ",
|
||||
"range": {
|
||||
|
|
|
@ -12,6 +12,7 @@ snapshot_kind: text
|
|||
{
|
||||
"kind": 21,
|
||||
"label": "Typst",
|
||||
"sortText": "142",
|
||||
"textEdit": {
|
||||
"newText": "typ",
|
||||
"range": {
|
||||
|
|
|
@ -12,6 +12,7 @@ snapshot_kind: text
|
|||
{
|
||||
"kind": 21,
|
||||
"label": "Typst",
|
||||
"sortText": "142",
|
||||
"textEdit": {
|
||||
"newText": "typ",
|
||||
"range": {
|
||||
|
|
|
@ -15,9 +15,9 @@ pub mod syntax;
|
|||
pub mod ty;
|
||||
mod upstream;
|
||||
|
||||
pub use analysis::{LocalContext, LocalContextGuard, LspWorldExt};
|
||||
pub use analysis::{CompletionFeat, LocalContext, LocalContextGuard, LspWorldExt};
|
||||
pub use snippet::PostfixSnippet;
|
||||
pub use upstream::{with_vm, CompletionFeat};
|
||||
pub use upstream::with_vm;
|
||||
|
||||
mod entry;
|
||||
pub use entry::*;
|
||||
|
|
|
@ -851,7 +851,13 @@ fn check_previous_syntax(leaf: &LinkedNode) -> Option<SurroundingSyntax> {
|
|||
if leaf.kind().is_trivia() {
|
||||
leaf = leaf.prev_sibling()?;
|
||||
}
|
||||
if matches!(leaf.kind(), SyntaxKind::ShowRule | SyntaxKind::SetRule) {
|
||||
if matches!(
|
||||
leaf.kind(),
|
||||
SyntaxKind::ShowRule
|
||||
| SyntaxKind::SetRule
|
||||
| SyntaxKind::ModuleImport
|
||||
| SyntaxKind::ModuleInclude
|
||||
) {
|
||||
return check_surrounding_syntax(&leaf.rightmost_leaf()?);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,905 +0,0 @@
|
|||
use std::cmp::Reverse;
|
||||
use std::collections::HashSet;
|
||||
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::{format_str, repr, Repr, Value};
|
||||
use typst::model::Document;
|
||||
use typst::syntax::ast::{AstNode, Param};
|
||||
use typst::syntax::{ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind};
|
||||
use typst::text::RawElem;
|
||||
use typst::World;
|
||||
use typst_shim::{syntax::LinkedNodeExt, utils::hash128};
|
||||
use unscanny::Scanner;
|
||||
|
||||
use super::{plain_docs_sentence, summarize_font_family};
|
||||
use crate::adt::interner::Interned;
|
||||
use crate::analysis::{analyze_labels, DynLabel, LocalContext, Ty};
|
||||
use crate::snippet::{
|
||||
CompletionCommand, CompletionContextKey, PrefixSnippet, DEFAULT_PREFIX_SNIPPET,
|
||||
};
|
||||
use crate::syntax::{node_ancestors, InterpretMode, SurroundingSyntax};
|
||||
|
||||
mod ext;
|
||||
pub use ext::CompletionFeat;
|
||||
use ext::*;
|
||||
|
||||
/// Autocomplete a cursor position in a source file.
|
||||
///
|
||||
/// Returns the position from which the completions apply and a list of
|
||||
/// completions.
|
||||
///
|
||||
/// When `explicit` is `true`, the user requested the completion by pressing
|
||||
/// control and space or something similar.
|
||||
///
|
||||
/// Passing a `document` (from a previous compilation) is optional, but enhances
|
||||
/// the autocompletions. Label completions, for instance, are only generated
|
||||
/// when the document is available.
|
||||
pub fn autocomplete(
|
||||
mut ctx: CompletionContext,
|
||||
) -> Option<(usize, bool, Vec<Completion>, Vec<lsp_types::CompletionItem>)> {
|
||||
let _ = complete_comments(&mut ctx)
|
||||
|| complete_type_and_syntax(&mut ctx).is_none() && {
|
||||
crate::log_debug_ct!("continue after completing type and syntax");
|
||||
complete_imports(&mut ctx)
|
||||
|| complete_markup(&mut ctx)
|
||||
|| complete_math(&mut ctx)
|
||||
|| complete_code(&mut ctx, false)
|
||||
};
|
||||
|
||||
Some((ctx.from, ctx.incomplete, ctx.completions, ctx.completions2))
|
||||
}
|
||||
|
||||
/// An autocompletion option.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Completion {
|
||||
/// The kind of item this completes to.
|
||||
pub kind: CompletionKind,
|
||||
/// The label the completion is shown with.
|
||||
pub label: EcoString,
|
||||
/// The label the completion is shown with.
|
||||
pub label_detail: Option<EcoString>,
|
||||
/// The label the completion is shown with.
|
||||
pub sort_text: Option<EcoString>,
|
||||
/// The composed text used for filtering.
|
||||
pub filter_text: Option<EcoString>,
|
||||
/// The character that should be committed when selecting this completion.
|
||||
pub commit_char: Option<char>,
|
||||
/// The completed version of the input, possibly described with snippet
|
||||
/// syntax like `${lhs} + ${rhs}`.
|
||||
///
|
||||
/// Should default to the `label` if `None`.
|
||||
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>,
|
||||
}
|
||||
|
||||
/// A kind of item that can be completed.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum CompletionKind {
|
||||
/// A syntactical structure.
|
||||
Syntax,
|
||||
/// A function.
|
||||
Func,
|
||||
/// A type.
|
||||
Type,
|
||||
/// A function parameter.
|
||||
Param,
|
||||
/// A field.
|
||||
Field,
|
||||
/// A constant.
|
||||
#[default]
|
||||
Constant,
|
||||
/// A reference.
|
||||
Reference,
|
||||
/// A symbol.
|
||||
Symbol(char),
|
||||
/// A variable.
|
||||
Variable,
|
||||
/// A module.
|
||||
Module,
|
||||
/// A file.
|
||||
File,
|
||||
/// A folder.
|
||||
Folder,
|
||||
}
|
||||
|
||||
/// Complete in comments. Or rather, don't!
|
||||
fn complete_comments(ctx: &mut CompletionContext) -> bool {
|
||||
if !matches!(
|
||||
ctx.leaf.kind(),
|
||||
SyntaxKind::LineComment | SyntaxKind::BlockComment
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let text = ctx.leaf.get().text();
|
||||
// check if next line defines a function
|
||||
if_chain! {
|
||||
if text == "///" || text == "/// ";
|
||||
// hash node
|
||||
if let Some(next) = ctx.leaf.next_leaf();
|
||||
// let node
|
||||
if let Some(next_next) = next.next_leaf();
|
||||
if let Some(next_next) = next_next.next_leaf();
|
||||
if matches!(next_next.parent_kind(), Some(SyntaxKind::Closure));
|
||||
if let Some(closure) = next_next.parent();
|
||||
if let Some(closure) = closure.cast::<ast::Expr>();
|
||||
if let ast::Expr::Closure(c) = closure;
|
||||
then {
|
||||
let mut doc_snippet: String = if text == "///" {
|
||||
" $0\n///".to_string()
|
||||
} else {
|
||||
"$0\n///".to_string()
|
||||
};
|
||||
let mut i = 0;
|
||||
for param in c.params().children() {
|
||||
// TODO: Properly handle Pos and Spread argument
|
||||
let param: &EcoString = match param {
|
||||
Param::Pos(p) => {
|
||||
match p {
|
||||
ast::Pattern::Normal(ast::Expr::Ident(ident)) => ident.get(),
|
||||
_ => &"_".into()
|
||||
}
|
||||
}
|
||||
Param::Named(n) => n.name().get(),
|
||||
Param::Spread(s) => {
|
||||
if let Some(ident) = s.sink_ident() {
|
||||
&eco_format!("{}", ident.get())
|
||||
} else {
|
||||
&EcoString::new()
|
||||
}
|
||||
}
|
||||
};
|
||||
log::info!("param: {param}, index: {i}");
|
||||
doc_snippet += &format!("\n/// - {param} (${}): ${}", i + 1, i + 2);
|
||||
i += 2;
|
||||
}
|
||||
doc_snippet += &format!("\n/// -> ${}", i + 1);
|
||||
ctx.completions.push(Completion {
|
||||
label: "Document function".into(),
|
||||
apply: Some(doc_snippet.into()),
|
||||
..Completion::default()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Complete in markup mode.
|
||||
fn complete_markup(ctx: &mut CompletionContext) -> bool {
|
||||
let parent_raw = node_ancestors(&ctx.leaf).find(|node| matches!(node.kind(), SyntaxKind::Raw));
|
||||
|
||||
// Bail if we aren't even in markup.
|
||||
if parent_raw.is_none()
|
||||
&& !matches!(
|
||||
ctx.leaf.parent_kind(),
|
||||
None | Some(SyntaxKind::Markup) | Some(SyntaxKind::Ref)
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start of an interpolated identifier: "#|".
|
||||
if ctx.leaf.kind() == SyntaxKind::Hash {
|
||||
ctx.from = ctx.cursor;
|
||||
code_completions(ctx, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// An existing identifier: "#pa|".
|
||||
if ctx.leaf.kind() == SyntaxKind::Ident {
|
||||
ctx.from = ctx.leaf.offset();
|
||||
code_completions(ctx, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Start of a reference: "@|" or "@he|".
|
||||
if ctx.leaf.kind() == SyntaxKind::RefMarker {
|
||||
ctx.from = ctx.leaf.offset() + 1;
|
||||
ctx.ref_completions();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Behind a half-completed binding: "#let x = |" or `#let f(x) = |`.
|
||||
if_chain! {
|
||||
if let Some(prev) = ctx.leaf.prev_leaf();
|
||||
if matches!(prev.kind(), SyntaxKind::Eq | SyntaxKind::Arrow);
|
||||
if matches!( prev.parent_kind(), Some(SyntaxKind::LetBinding | SyntaxKind::Closure));
|
||||
then {
|
||||
ctx.from = ctx.cursor;
|
||||
code_completions(ctx, false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Behind a half-completed context block: "#context |".
|
||||
if_chain! {
|
||||
if let Some(prev) = ctx.leaf.prev_leaf();
|
||||
if prev.kind() == SyntaxKind::Context;
|
||||
then {
|
||||
ctx.from = ctx.cursor;
|
||||
code_completions(ctx, false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Directly after a raw block.
|
||||
if let Some(parent_raw) = parent_raw {
|
||||
let mut s = Scanner::new(ctx.text);
|
||||
s.jump(parent_raw.offset());
|
||||
if s.eat_if("```") {
|
||||
s.eat_while('`');
|
||||
let start = s.cursor();
|
||||
if s.eat_if(is_id_start) {
|
||||
s.eat_while(is_id_continue);
|
||||
}
|
||||
if s.cursor() == ctx.cursor {
|
||||
ctx.from = start;
|
||||
ctx.raw_completions();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Anywhere: "|".
|
||||
if !is_triggered_by_punc(ctx.trigger_character) && ctx.explicit {
|
||||
ctx.from = ctx.cursor;
|
||||
ctx.snippet_completions(Some(InterpretMode::Markup), None);
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Complete in math mode.
|
||||
fn complete_math(ctx: &mut CompletionContext) -> bool {
|
||||
if !matches!(
|
||||
ctx.leaf.parent_kind(),
|
||||
Some(SyntaxKind::Equation)
|
||||
| Some(SyntaxKind::Math)
|
||||
| Some(SyntaxKind::MathFrac)
|
||||
| Some(SyntaxKind::MathAttach)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start of an interpolated identifier: "#|".
|
||||
if ctx.leaf.kind() == SyntaxKind::Hash {
|
||||
ctx.from = ctx.cursor;
|
||||
code_completions(ctx, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Start of an interpolated identifier: "#pa|".
|
||||
if ctx.leaf.kind() == SyntaxKind::Ident {
|
||||
ctx.from = ctx.leaf.offset();
|
||||
code_completions(ctx, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Behind existing atom or identifier: "$a|$" or "$abc|$".
|
||||
if !is_triggered_by_punc(ctx.trigger_character)
|
||||
&& matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent)
|
||||
{
|
||||
ctx.from = ctx.leaf.offset();
|
||||
ctx.scope_completions(true);
|
||||
ctx.snippet_completions(Some(InterpretMode::Math), None);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Anywhere: "$|$".
|
||||
if !is_triggered_by_punc(ctx.trigger_character) && ctx.explicit {
|
||||
ctx.from = ctx.cursor;
|
||||
ctx.scope_completions(true);
|
||||
ctx.snippet_completions(Some(InterpretMode::Math), None);
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Complete imports.
|
||||
fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
||||
// On the colon marker of an import list:
|
||||
// "#import "path.typ":|"
|
||||
if_chain! {
|
||||
if matches!(ctx.leaf.kind(), SyntaxKind::Colon);
|
||||
if let Some(parent) = ctx.leaf.clone().parent();
|
||||
if let Some(ast::Expr::Import(import)) = parent.get().cast();
|
||||
if !matches!(import.imports(), Some(ast::Imports::Wildcard));
|
||||
if let Some(source) = parent.children().find(|child| child.is::<ast::Expr>());
|
||||
then {
|
||||
let items = match import.imports() {
|
||||
Some(ast::Imports::Items(items)) => items,
|
||||
_ => Default::default(),
|
||||
};
|
||||
|
||||
ctx.from = ctx.cursor;
|
||||
|
||||
import_item_completions(ctx, items, vec![], &source);
|
||||
if items.iter().next().is_some() {
|
||||
ctx.enrich("", ", ");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Behind an import list:
|
||||
// "#import "path.typ": |",
|
||||
// "#import "path.typ": a, b, |".
|
||||
if_chain! {
|
||||
if let Some(prev) = ctx.leaf.prev_sibling();
|
||||
if let Some(ast::Expr::Import(import)) = prev.get().cast();
|
||||
if !ctx.text[prev.offset()..ctx.cursor].contains('\n');
|
||||
if let Some(ast::Imports::Items(items)) = import.imports();
|
||||
if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>());
|
||||
then {
|
||||
ctx.from = ctx.cursor;
|
||||
import_item_completions(ctx, items, vec![], &source);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Behind a comma in an import list:
|
||||
// "#import "path.typ": this,|".
|
||||
if_chain! {
|
||||
if matches!(ctx.leaf.kind(), SyntaxKind::Comma);
|
||||
if let Some(parent) = ctx.leaf.clone().parent();
|
||||
if parent.kind() == SyntaxKind::ImportItems;
|
||||
if let Some(grand) = parent.parent();
|
||||
if let Some(ast::Expr::Import(import)) = grand.get().cast();
|
||||
if let Some(ast::Imports::Items(items)) = import.imports();
|
||||
if let Some(source) = grand.children().find(|child| child.is::<ast::Expr>());
|
||||
then {
|
||||
import_item_completions(ctx, items, vec![], &source);
|
||||
ctx.enrich(" ", "");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Behind a half-started identifier in an import list:
|
||||
// "#import "path.typ": th|".
|
||||
if_chain! {
|
||||
if matches!(ctx.leaf.kind(), SyntaxKind::Ident | SyntaxKind::Dot);
|
||||
if let Some(path_ctx) = ctx.leaf.clone().parent();
|
||||
if path_ctx.kind() == SyntaxKind::ImportItemPath;
|
||||
if let Some(parent) = path_ctx.parent();
|
||||
if parent.kind() == SyntaxKind::ImportItems;
|
||||
if let Some(grand) = parent.parent();
|
||||
if let Some(ast::Expr::Import(import)) = grand.get().cast();
|
||||
if let Some(ast::Imports::Items(items)) = import.imports();
|
||||
if let Some(source) = grand.children().find(|child| child.is::<ast::Expr>());
|
||||
then {
|
||||
if ctx.leaf.kind() == SyntaxKind::Ident {
|
||||
ctx.from = ctx.leaf.offset();
|
||||
}
|
||||
let path = path_ctx.cast::<ast::ImportItemPath>().map(|path| path.iter().take_while(|ident| ident.span() != ctx.leaf.span()).collect());
|
||||
import_item_completions(ctx, items, path.unwrap_or_default(), &source);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Add completions for all exports of a module.
|
||||
fn import_item_completions<'a>(
|
||||
ctx: &mut CompletionContext<'a>,
|
||||
existing: ast::ImportItems<'a>,
|
||||
comps: Vec<ast::Ident>,
|
||||
source: &LinkedNode,
|
||||
) {
|
||||
// Select the source by `comps`
|
||||
let value = ctx.ctx.module_by_syntax(source);
|
||||
let value = comps
|
||||
.iter()
|
||||
.fold(value.as_ref(), |value, comp| value?.scope()?.get(comp));
|
||||
let Some(scope) = value.and_then(|v| v.scope()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Check imported items in the scope
|
||||
let seen = existing
|
||||
.iter()
|
||||
.flat_map(|item| {
|
||||
let item_comps = item.path().iter().collect::<Vec<_>>();
|
||||
if item_comps.len() == comps.len() + 1
|
||||
&& item_comps
|
||||
.iter()
|
||||
.zip(comps.as_slice())
|
||||
.all(|(l, r)| l.as_str() == r.as_str())
|
||||
{
|
||||
// item_comps.len() >= 1
|
||||
item_comps.last().cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if existing.iter().next().is_none() {
|
||||
ctx.snippet_completion("*", "*", "Import everything.");
|
||||
}
|
||||
|
||||
for (name, value, _) in scope.iter() {
|
||||
if seen.iter().all(|item| item.as_str() != name) {
|
||||
ctx.value_completion(Some(name.clone()), value, false, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete in code mode.
|
||||
fn complete_code(ctx: &mut CompletionContext, from_type: bool) -> bool {
|
||||
let surrounding_syntax = ctx.surrounding_syntax();
|
||||
|
||||
if matches!(
|
||||
(ctx.leaf.parent_kind(), surrounding_syntax),
|
||||
(
|
||||
None | Some(SyntaxKind::Markup)
|
||||
| Some(SyntaxKind::Math)
|
||||
| Some(SyntaxKind::MathFrac)
|
||||
| Some(SyntaxKind::MathAttach)
|
||||
| Some(SyntaxKind::MathRoot),
|
||||
SurroundingSyntax::Regular
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Behind a half-completed context block: "context |".
|
||||
if_chain! {
|
||||
if let Some(prev) = ctx.leaf.prev_leaf();
|
||||
if prev.kind() == SyntaxKind::Context;
|
||||
then {
|
||||
ctx.from = ctx.cursor;
|
||||
code_completions(ctx, false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// An existing identifier: "{ pa| }".
|
||||
if ctx.leaf.kind() == SyntaxKind::Ident
|
||||
&& !matches!(ctx.leaf.parent_kind(), Some(SyntaxKind::FieldAccess))
|
||||
{
|
||||
ctx.from = ctx.leaf.offset();
|
||||
code_completions(ctx, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A potential label (only at the start of an argument list): "(<|".
|
||||
if !from_type && ctx.before.ends_with("(<") {
|
||||
ctx.from = ctx.cursor;
|
||||
ctx.label_completions(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Anywhere: "{ | }".
|
||||
// But not within or after an expression.
|
||||
if ctx.explicit
|
||||
&& (ctx.leaf.kind().is_trivia()
|
||||
|| (matches!(
|
||||
ctx.leaf.kind(),
|
||||
SyntaxKind::LeftParen | SyntaxKind::LeftBrace
|
||||
) || (matches!(ctx.leaf.kind(), SyntaxKind::Colon)
|
||||
&& ctx.leaf.parent_kind() == Some(SyntaxKind::ShowRule))))
|
||||
{
|
||||
ctx.from = ctx.cursor;
|
||||
code_completions(ctx, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Add completions for expression snippets.
|
||||
#[rustfmt::skip]
|
||||
fn code_completions(ctx: &mut CompletionContext, hash: bool) {
|
||||
// todo: filter code completions
|
||||
// matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_))
|
||||
ctx.scope_completions(true);
|
||||
|
||||
ctx.snippet_completions(Some(InterpretMode::Code), None);
|
||||
|
||||
if !hash {
|
||||
ctx.snippet_completion(
|
||||
"function",
|
||||
"(${params}) => ${output}",
|
||||
"Creates an unnamed function.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Context for autocompletion.
|
||||
pub struct CompletionContext<'a> {
|
||||
pub ctx: &'a mut LocalContext,
|
||||
pub document: Option<&'a Document>,
|
||||
pub text: &'a str,
|
||||
pub before: &'a str,
|
||||
pub after: &'a str,
|
||||
pub root: LinkedNode<'a>,
|
||||
pub leaf: LinkedNode<'a>,
|
||||
pub cursor: usize,
|
||||
pub explicit: bool,
|
||||
pub trigger_character: Option<char>,
|
||||
pub from: usize,
|
||||
pub from_ty: Option<Ty>,
|
||||
pub completions: Vec<Completion>,
|
||||
pub completions2: Vec<lsp_types::CompletionItem>,
|
||||
pub incomplete: bool,
|
||||
pub seen_casts: HashSet<u128>,
|
||||
pub seen_types: HashSet<Ty>,
|
||||
pub seen_fields: HashSet<Interned<str>>,
|
||||
}
|
||||
|
||||
impl<'a> CompletionContext<'a> {
|
||||
/// Create a new autocompletion context.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
ctx: &'a mut LocalContext,
|
||||
document: Option<&'a Document>,
|
||||
source: &'a Source,
|
||||
cursor: usize,
|
||||
explicit: bool,
|
||||
trigger_character: Option<char>,
|
||||
) -> Option<Self> {
|
||||
let text = source.text();
|
||||
let root = LinkedNode::new(source.root());
|
||||
let leaf = root.leaf_at_compat(cursor)?;
|
||||
Some(Self {
|
||||
ctx,
|
||||
document,
|
||||
text,
|
||||
before: &text[..cursor],
|
||||
after: &text[cursor..],
|
||||
root,
|
||||
leaf,
|
||||
cursor,
|
||||
trigger_character,
|
||||
explicit,
|
||||
from: cursor,
|
||||
from_ty: None,
|
||||
incomplete: true,
|
||||
completions: vec![],
|
||||
completions2: vec![],
|
||||
seen_casts: HashSet::new(),
|
||||
seen_types: HashSet::new(),
|
||||
seen_fields: HashSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// A small window of context before the cursor.
|
||||
fn before_window(&self, size: usize) -> &str {
|
||||
slice_at(
|
||||
self.before,
|
||||
self.cursor.saturating_sub(size)..self.before.len(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Add a prefix and suffix to all applications.
|
||||
fn enrich(&mut self, prefix: &str, suffix: &str) {
|
||||
for Completion { label, apply, .. } in &mut self.completions {
|
||||
let current = apply.as_ref().unwrap_or(label);
|
||||
*apply = Some(eco_format!("{prefix}{current}{suffix}"));
|
||||
}
|
||||
}
|
||||
|
||||
fn snippet_completions(
|
||||
&mut self,
|
||||
mode: Option<InterpretMode>,
|
||||
surrounding_syntax: Option<SurroundingSyntax>,
|
||||
) {
|
||||
let mut keys = vec![CompletionContextKey::new(mode, surrounding_syntax)];
|
||||
if mode.is_some() {
|
||||
keys.push(CompletionContextKey::new(None, surrounding_syntax));
|
||||
}
|
||||
if surrounding_syntax.is_some() {
|
||||
keys.push(CompletionContextKey::new(mode, None));
|
||||
if mode.is_some() {
|
||||
keys.push(CompletionContextKey::new(None, None));
|
||||
}
|
||||
}
|
||||
let applies_to = |snippet: &PrefixSnippet| keys.iter().any(|key| snippet.applies_to(key));
|
||||
|
||||
for snippet in DEFAULT_PREFIX_SNIPPET.iter() {
|
||||
if !applies_to(snippet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let analysis = &self.ctx.analysis;
|
||||
let command = match snippet.command {
|
||||
Some(CompletionCommand::TriggerSuggest) => analysis.trigger_suggest(true),
|
||||
None => analysis.trigger_on_snippet(snippet.snippet.contains("${")),
|
||||
};
|
||||
|
||||
self.completions.push(Completion {
|
||||
kind: CompletionKind::Syntax,
|
||||
label: snippet.label.as_ref().into(),
|
||||
apply: Some(snippet.snippet.as_ref().into()),
|
||||
detail: Some(snippet.description.as_ref().into()),
|
||||
command,
|
||||
..Completion::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a snippet completion.
|
||||
fn snippet_completion(&mut self, label: &str, snippet: &str, docs: &str) {
|
||||
self.completions.push(Completion {
|
||||
kind: CompletionKind::Syntax,
|
||||
label: label.into(),
|
||||
apply: Some(snippet.into()),
|
||||
detail: Some(docs.into()),
|
||||
command: self.ctx.analysis.trigger_on_snippet(snippet.contains("${")),
|
||||
..Completion::default()
|
||||
});
|
||||
}
|
||||
|
||||
/// Add completions for all font families.
|
||||
fn font_completions(&mut self) {
|
||||
let equation = self.before_window(25).contains("equation");
|
||||
for (family, iter) in self.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 all available packages.
|
||||
fn package_completions(&mut self, all_versions: bool) {
|
||||
let w = self.world().clone();
|
||||
let mut packages: Vec<_> = w
|
||||
.packages()
|
||||
.iter()
|
||||
.map(|(spec, desc)| (spec, desc.clone()))
|
||||
.collect();
|
||||
// local_packages to references and add them to the packages
|
||||
let local_packages_refs = self.ctx.local_packages();
|
||||
packages.extend(
|
||||
local_packages_refs
|
||||
.iter()
|
||||
.map(|spec| (spec, Some(eco_format!("{} v{}", spec.name, spec.version)))),
|
||||
);
|
||||
|
||||
packages.sort_by_key(|(spec, _)| (&spec.namespace, &spec.name, Reverse(spec.version)));
|
||||
if !all_versions {
|
||||
packages.dedup_by_key(|(spec, _)| (&spec.namespace, &spec.name));
|
||||
}
|
||||
for (package, description) in packages {
|
||||
self.value_completion(
|
||||
None,
|
||||
&Value::Str(format_str!("{package}")),
|
||||
false,
|
||||
description.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add completions for raw block tags.
|
||||
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.completions.push(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.
|
||||
fn ref_completions(&mut self) {
|
||||
self.label_completions_(false, true);
|
||||
}
|
||||
|
||||
/// Add completions for labels and references.
|
||||
fn label_completions(&mut self, only_citation: bool) {
|
||||
self.label_completions_(only_citation, false);
|
||||
}
|
||||
|
||||
/// Add completions for labels and references.
|
||||
fn label_completions_(&mut self, only_citation: bool, ref_label: bool) {
|
||||
let Some(document) = self.document else {
|
||||
return;
|
||||
};
|
||||
let (labels, split) = analyze_labels(document);
|
||||
|
||||
let head = &self.text[..self.from];
|
||||
let at = head.ends_with('@');
|
||||
let open = !at && !head.ends_with('<');
|
||||
let close = !at && !self.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.seen_casts.insert(hash128(&label)) {
|
||||
continue;
|
||||
}
|
||||
let label: EcoString = label.as_str().into();
|
||||
let completion = Completion {
|
||||
kind: CompletionKind::Reference,
|
||||
apply: Some(eco_format!(
|
||||
"{}{}{}",
|
||||
if open { "<" } else { "" },
|
||||
label.as_str(),
|
||||
if close { ">" } else { "" }
|
||||
)),
|
||||
label: label.clone(),
|
||||
label_detail: 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.completions.push(Completion {
|
||||
kind: CompletionKind::Constant,
|
||||
label: bib_title.clone(),
|
||||
label_detail: Some(label),
|
||||
filter_text: Some(bib_title),
|
||||
detail,
|
||||
..completion.clone()
|
||||
});
|
||||
}
|
||||
|
||||
self.completions.push(completion);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a completion for a specific value.
|
||||
fn value_completion(
|
||||
&mut self,
|
||||
label: Option<EcoString>,
|
||||
value: &Value,
|
||||
parens: bool,
|
||||
docs: Option<&str>,
|
||||
) {
|
||||
self.value_completion_(
|
||||
label,
|
||||
value,
|
||||
parens,
|
||||
match value {
|
||||
Value::Symbol(s) => Some(symbol_label_detail(s.get())),
|
||||
_ => None,
|
||||
},
|
||||
docs,
|
||||
);
|
||||
}
|
||||
|
||||
/// Add a completion for a specific value.
|
||||
fn value_completion_(
|
||||
&mut self,
|
||||
label: Option<EcoString>,
|
||||
value: &Value,
|
||||
parens: bool,
|
||||
label_detail: Option<EcoString>,
|
||||
docs: Option<&str>,
|
||||
) {
|
||||
// Prevent duplicate completions from appearing.
|
||||
if !self.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 mut apply = None;
|
||||
let mut command = None;
|
||||
if parens && matches!(value, Value::Func(_)) {
|
||||
if let Value::Func(func) = value {
|
||||
command = self.ctx.analysis.trigger_parameter_hints(true);
|
||||
if func
|
||||
.params()
|
||||
.is_some_and(|params| params.iter().all(|param| param.name == "self"))
|
||||
{
|
||||
apply = Some(eco_format!("{label}()${{}}"));
|
||||
} else {
|
||||
apply = Some(eco_format!("{label}(${{}})"));
|
||||
}
|
||||
}
|
||||
} else if at {
|
||||
apply = Some(eco_format!("at(\"{label}\")"));
|
||||
} else {
|
||||
let apply_label = &mut label.as_str();
|
||||
if apply_label.ends_with('"') && self.after.starts_with('"') {
|
||||
if let Some(trimmed) = apply_label.strip_suffix('"') {
|
||||
*apply_label = trimmed;
|
||||
}
|
||||
}
|
||||
let from_before = slice_at(self.text, 0..self.from);
|
||||
if apply_label.starts_with('"') && from_before.ends_with('"') {
|
||||
if let Some(trimmed) = apply_label.strip_prefix('"') {
|
||||
*apply_label = trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
if apply_label.len() != label.len() {
|
||||
apply = Some((*apply_label).into());
|
||||
}
|
||||
}
|
||||
|
||||
self.completions.push(Completion {
|
||||
kind: value_to_completion_kind(value),
|
||||
label,
|
||||
apply,
|
||||
detail,
|
||||
label_detail,
|
||||
command,
|
||||
..Completion::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Slices a smaller string at character boundaries safely.
|
||||
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]
|
||||
}
|
||||
|
||||
fn is_triggered_by_punc(trigger_character: Option<char>) -> bool {
|
||||
trigger_character.is_some_and(|ch| ch.is_ascii_punctuation())
|
||||
}
|
|
@ -16,8 +16,6 @@ use typst::{
|
|||
|
||||
mod tooltip;
|
||||
pub use tooltip::*;
|
||||
mod complete;
|
||||
pub use complete::*;
|
||||
|
||||
/// Extract the first sentence of plain text of a piece of documentation.
|
||||
///
|
||||
|
@ -369,7 +367,7 @@ pub fn route_of_value(val: &Value) -> Option<&'static String> {
|
|||
}
|
||||
|
||||
/// Create a short description of a font family.
|
||||
fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> EcoString {
|
||||
pub fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> EcoString {
|
||||
let mut infos: Vec<_> = variants.collect();
|
||||
infos.sort_by_key(|info: &&FontInfo| info.variant);
|
||||
|
||||
|
|
|
@ -374,7 +374,7 @@ fn e2e() {
|
|||
});
|
||||
|
||||
let hash = replay_log(&tinymist_binary, &root.join("neovim"));
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:cc7aa05c21bd1a730cc6dea79f0f80f0");
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:7ed80ba766d7520604d29b113cda11a");
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -385,7 +385,7 @@ fn e2e() {
|
|||
});
|
||||
|
||||
let hash = replay_log(&tinymist_binary, &root.join("vscode"));
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:be57d89e605fd2dc929896cce62802b4");
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:4e372dd2cd42b69545f79de01a38e206");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue