mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 01:42:14 +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]
|
[workspace.metadata.typos.files]
|
||||||
ignore-hidden = false
|
ignore-hidden = false
|
||||||
extend-exclude = ["/.git", "fixtures", "upstream"]
|
extend-exclude = ["/.git", "fixtures"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ use std::path::Path;
|
||||||
pub(crate) use bib::*;
|
pub(crate) use bib::*;
|
||||||
pub mod call;
|
pub mod call;
|
||||||
pub use call::*;
|
pub use call::*;
|
||||||
|
pub mod completion;
|
||||||
|
pub use completion::*;
|
||||||
pub mod code_action;
|
pub mod code_action;
|
||||||
pub use code_action::*;
|
pub use code_action::*;
|
||||||
pub mod color_expr;
|
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::prelude::*;
|
||||||
use crate::analysis::{
|
use crate::analysis::{
|
||||||
analyze_bib, analyze_expr_, analyze_import_, analyze_signature, definition, post_type_check,
|
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,
|
SemanticTokenCache, SemanticTokenContext, SemanticTokens, Signature, SignatureTarget, Ty,
|
||||||
TypeInfo,
|
TypeInfo,
|
||||||
};
|
};
|
||||||
|
@ -35,7 +35,7 @@ use crate::syntax::{
|
||||||
classify_syntax, construct_module_dependencies, resolve_id_by_path, scan_workspace_files, Decl,
|
classify_syntax, construct_module_dependencies, resolve_id_by_path, scan_workspace_files, Decl,
|
||||||
DefKind, ExprInfo, ExprRoute, LexicalScope, ModuleDependency, SyntaxClass,
|
DefKind, ExprInfo, ExprRoute, LexicalScope, ModuleDependency, SyntaxClass,
|
||||||
};
|
};
|
||||||
use crate::upstream::{tooltip_, CompletionFeat, Tooltip};
|
use crate::upstream::{tooltip_, Tooltip};
|
||||||
use crate::{
|
use crate::{
|
||||||
ColorTheme, CompilerQueryRequest, LspPosition, LspRange, LspWorldExt, PositionEncoding,
|
ColorTheme, CompilerQueryRequest, LspPosition, LspRange, LspWorldExt, PositionEncoding,
|
||||||
VersionedDocument,
|
VersionedDocument,
|
||||||
|
|
|
@ -7,16 +7,14 @@ use regex::{Captures, Regex};
|
||||||
use typst_shim::syntax::LinkedNodeExt;
|
use typst_shim::syntax::LinkedNodeExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
analysis::{InsTy, Ty},
|
analysis::{complete_type_and_syntax, CompletionWorker, InsTy, Ty},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
syntax::{is_ident_like, SyntaxClass},
|
syntax::{is_ident_like, SyntaxClass},
|
||||||
upstream::{autocomplete, CompletionContext},
|
|
||||||
StatefulRequest,
|
StatefulRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) type LspCompletion = lsp_types::CompletionItem;
|
pub(crate) type LspCompletion = lsp_types::CompletionItem;
|
||||||
pub(crate) type LspCompletionKind = lsp_types::CompletionItemKind;
|
pub(crate) type LspCompletionKind = lsp_types::CompletionItemKind;
|
||||||
pub(crate) type TypstCompletionKind = crate::upstream::CompletionKind;
|
|
||||||
|
|
||||||
pub(crate) mod snippet;
|
pub(crate) mod snippet;
|
||||||
|
|
||||||
|
@ -130,11 +128,10 @@ impl StatefulRequest for CompletionRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut completion_items_rest = None;
|
|
||||||
let is_incomplete = false;
|
let is_incomplete = false;
|
||||||
|
|
||||||
let mut cc_ctx =
|
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
|
// Exclude it self from auto completion
|
||||||
// e.g. `#let x = (1.);`
|
// e.g. `#let x = (1.);`
|
||||||
|
@ -147,10 +144,15 @@ impl StatefulRequest for CompletionRequest {
|
||||||
cc_ctx.seen_types.insert(self_ty);
|
cc_ctx.seen_types.insert(self_ty);
|
||||||
};
|
};
|
||||||
|
|
||||||
let (offset, ic, mut completions, completions_items2) = autocomplete(cc_ctx)?;
|
let _ = complete_type_and_syntax(&mut cc_ctx);
|
||||||
if !completions_items2.is_empty() {
|
let CompletionWorker {
|
||||||
completion_items_rest = Some(completions_items2);
|
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
|
// 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.
|
// path completion, but now we've scanned all the paths at the same time.
|
||||||
// is_incomplete = ic;
|
// is_incomplete = ic;
|
||||||
|
@ -223,7 +225,7 @@ impl StatefulRequest for CompletionRequest {
|
||||||
|
|
||||||
LspCompletion {
|
LspCompletion {
|
||||||
label: typst_completion.label.to_string(),
|
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),
|
detail: typst_completion.detail.as_ref().map(String::from),
|
||||||
sort_text: typst_completion.sort_text.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),
|
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();
|
let mut items = completions.collect_vec();
|
||||||
|
items.append(&mut completion_items_rest);
|
||||||
if let Some(items_rest) = completion_items_rest.as_mut() {
|
|
||||||
items.append(items_rest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// To response completions in fine-grained manner, we need to mark result as
|
// To response completions in fine-grained manner, we need to mark result as
|
||||||
// incomplete. This follows what rust-analyzer does.
|
// 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> =
|
static TYPST_SNIPPET_PLACEHOLDER_RE: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
|
Lazy::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(content, delta: int) => strong"
|
"description": "(content, delta: int) => strong"
|
||||||
},
|
},
|
||||||
|
"sortText": "180",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "strong(${1:})",
|
"newText": "strong(${1:})",
|
||||||
"range": {
|
"range": {
|
||||||
|
@ -35,6 +36,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(content, delta: int) => strong"
|
"description": "(content, delta: int) => strong"
|
||||||
},
|
},
|
||||||
|
"sortText": "181",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "strong[${1:}]",
|
"newText": "strong[${1:}]",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,8 +15,9 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(content, b: content | none, bl: content | none, br: content | none, t: content | none, tl: content | none, tr: content | none) => attach"
|
"description": "(content, b: content | none, bl: content | none, br: content | none, t: content | none, tl: content | none, tr: content | none) => attach"
|
||||||
},
|
},
|
||||||
|
"sortText": "075",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "attach(${1:})",
|
"newText": " attach(${1:})",
|
||||||
"range": {
|
"range": {
|
||||||
"end": {
|
"end": {
|
||||||
"character": 2,
|
"character": 2,
|
||||||
|
|
|
@ -15,8 +15,9 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(content, b: content | none, bl: content | none, br: content | none, t: content | none, tl: content | none, tr: content | none) => attach"
|
"description": "(content, b: content | none, bl: content | none, br: content | none, t: content | none, tl: content | none, tr: content | none) => attach"
|
||||||
},
|
},
|
||||||
|
"sortText": "075",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "attach(${1:})",
|
"newText": " attach(${1:})",
|
||||||
"range": {
|
"range": {
|
||||||
"end": {
|
"end": {
|
||||||
"character": 2,
|
"character": 2,
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||||
},
|
},
|
||||||
|
"sortText": "127",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "pagebreak()${1:}",
|
"newText": "pagebreak()${1:}",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||||
},
|
},
|
||||||
|
"sortText": "127",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "pagebreak()${1:}",
|
"newText": "pagebreak()${1:}",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(any, any, any) => none"
|
"description": "(any, any, any) => none"
|
||||||
},
|
},
|
||||||
|
"sortText": "000",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "aa(${1:})",
|
"newText": "aa(${1:})",
|
||||||
"range": {
|
"range": {
|
||||||
|
@ -35,6 +36,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "any"
|
"description": "any"
|
||||||
},
|
},
|
||||||
|
"sortText": "001",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "aab",
|
"newText": "aab",
|
||||||
"range": {
|
"range": {
|
||||||
|
@ -55,6 +57,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "any"
|
"description": "any"
|
||||||
},
|
},
|
||||||
|
"sortText": "002",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "aabc",
|
"newText": "aabc",
|
||||||
"range": {
|
"range": {
|
||||||
|
@ -75,6 +78,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "any"
|
"description": "any"
|
||||||
},
|
},
|
||||||
|
"sortText": "003",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "aac",
|
"newText": "aac",
|
||||||
"range": {
|
"range": {
|
||||||
|
@ -100,6 +104,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "any"
|
"description": "any"
|
||||||
},
|
},
|
||||||
|
"sortText": "002",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "aabc",
|
"newText": "aabc",
|
||||||
"range": {
|
"range": {
|
||||||
|
@ -120,6 +125,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "any"
|
"description": "any"
|
||||||
},
|
},
|
||||||
|
"sortText": "003",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "aac",
|
"newText": "aac",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||||
},
|
},
|
||||||
|
"sortText": "126",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "pagebreak()${1:}",
|
"newText": "pagebreak()${1:}",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||||
},
|
},
|
||||||
|
"sortText": "126",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "pagebreak()${1:}",
|
"newText": "pagebreak()${1:}",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||||
},
|
},
|
||||||
|
"sortText": "126",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "pagebreak()${1:}",
|
"newText": "pagebreak()${1:}",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||||
},
|
},
|
||||||
|
"sortText": "126",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "pagebreak()${1:}",
|
"newText": "pagebreak()${1:}",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -17,7 +17,7 @@ snapshot_kind: text
|
||||||
"kind": 3,
|
"kind": 3,
|
||||||
"label": "table-prefix",
|
"label": "table-prefix",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "table-prefix(${1:})",
|
"newText": "table-prefix",
|
||||||
"range": {
|
"range": {
|
||||||
"end": {
|
"end": {
|
||||||
"character": 24,
|
"character": 24,
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "module(base)"
|
"description": "module(base)"
|
||||||
},
|
},
|
||||||
|
"sortText": "008",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "base",
|
"newText": "base",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "module(base)"
|
"description": "module(base)"
|
||||||
},
|
},
|
||||||
|
"sortText": "008",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "baz",
|
"newText": "baz",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "module(base)"
|
"description": "module(base)"
|
||||||
},
|
},
|
||||||
|
"sortText": "012",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "baz",
|
"newText": "baz",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "1"
|
"description": "1"
|
||||||
},
|
},
|
||||||
|
"sortText": "000",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "a",
|
"newText": "a",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||||
},
|
},
|
||||||
|
"sortText": "128",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "pagebreak()${1:}",
|
"newText": "pagebreak()${1:}",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||||
},
|
},
|
||||||
|
"sortText": "128",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "pagebreak()${1:}",
|
"newText": "pagebreak()${1:}",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
"description": "(to: \"even\" | \"odd\" | none, weak: bool) => pagebreak"
|
||||||
},
|
},
|
||||||
|
"sortText": "128",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "pagebreak()${1:}",
|
"newText": "pagebreak()${1:}",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"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"
|
"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": {
|
"textEdit": {
|
||||||
"newText": "box(${1:})",
|
"newText": "box(${1:})",
|
||||||
"range": {
|
"range": {
|
||||||
|
@ -35,6 +36,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"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"
|
"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": {
|
"textEdit": {
|
||||||
"newText": "box[${1:}]",
|
"newText": "box[${1:}]",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ snapshot_kind: text
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "(content) => content"
|
"description": "(content) => content"
|
||||||
},
|
},
|
||||||
|
"sortText": "086",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "bold(${1:})",
|
"newText": "bold(${1:})",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -8,6 +8,28 @@ snapshot_kind: text
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"isIncomplete": false,
|
"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": {
|
"labelDetails": {
|
||||||
"description": "ℜ"
|
"description": "ℜ"
|
||||||
},
|
},
|
||||||
|
"sortText": "039",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "Re",
|
"newText": "Re",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -12,6 +12,7 @@ snapshot_kind: text
|
||||||
{
|
{
|
||||||
"kind": 21,
|
"kind": 21,
|
||||||
"label": "Typst",
|
"label": "Typst",
|
||||||
|
"sortText": "142",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "typ",
|
"newText": "typ",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -12,6 +12,7 @@ snapshot_kind: text
|
||||||
{
|
{
|
||||||
"kind": 21,
|
"kind": 21,
|
||||||
"label": "Typst",
|
"label": "Typst",
|
||||||
|
"sortText": "142",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "typ",
|
"newText": "typ",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -12,6 +12,7 @@ snapshot_kind: text
|
||||||
{
|
{
|
||||||
"kind": 21,
|
"kind": 21,
|
||||||
"label": "Typst",
|
"label": "Typst",
|
||||||
|
"sortText": "142",
|
||||||
"textEdit": {
|
"textEdit": {
|
||||||
"newText": "typ",
|
"newText": "typ",
|
||||||
"range": {
|
"range": {
|
||||||
|
|
|
@ -15,9 +15,9 @@ pub mod syntax;
|
||||||
pub mod ty;
|
pub mod ty;
|
||||||
mod upstream;
|
mod upstream;
|
||||||
|
|
||||||
pub use analysis::{LocalContext, LocalContextGuard, LspWorldExt};
|
pub use analysis::{CompletionFeat, LocalContext, LocalContextGuard, LspWorldExt};
|
||||||
pub use snippet::PostfixSnippet;
|
pub use snippet::PostfixSnippet;
|
||||||
pub use upstream::{with_vm, CompletionFeat};
|
pub use upstream::with_vm;
|
||||||
|
|
||||||
mod entry;
|
mod entry;
|
||||||
pub use entry::*;
|
pub use entry::*;
|
||||||
|
|
|
@ -851,7 +851,13 @@ fn check_previous_syntax(leaf: &LinkedNode) -> Option<SurroundingSyntax> {
|
||||||
if leaf.kind().is_trivia() {
|
if leaf.kind().is_trivia() {
|
||||||
leaf = leaf.prev_sibling()?;
|
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()?);
|
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;
|
mod tooltip;
|
||||||
pub use tooltip::*;
|
pub use tooltip::*;
|
||||||
mod complete;
|
|
||||||
pub use complete::*;
|
|
||||||
|
|
||||||
/// Extract the first sentence of plain text of a piece of documentation.
|
/// 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.
|
/// 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();
|
let mut infos: Vec<_> = variants.collect();
|
||||||
infos.sort_by_key(|info: &&FontInfo| info.variant);
|
infos.sort_by_key(|info: &&FontInfo| info.variant);
|
||||||
|
|
||||||
|
|
|
@ -374,7 +374,7 @@ fn e2e() {
|
||||||
});
|
});
|
||||||
|
|
||||||
let hash = replay_log(&tinymist_binary, &root.join("neovim"));
|
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"));
|
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