mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 18:28:02 +00:00
feat: support import path completion (#134)
* feat: path completion * fix: package snippet order * dev: update snapshot hash * fix: completion
This commit is contained in:
parent
90ef2e6f72
commit
36536bbc6b
14 changed files with 1084 additions and 46 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3756,6 +3756,7 @@ dependencies = [
|
|||
"lsp-types",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pathdiff",
|
||||
"percent-encoding",
|
||||
"reflexo",
|
||||
"regex",
|
||||
|
|
|
@ -38,6 +38,7 @@ lsp-types.workspace = true
|
|||
if_chain = "1"
|
||||
percent-encoding = "2"
|
||||
unscanny = "0.1"
|
||||
pathdiff = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
once_cell.workspace = true
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
use crate::{prelude::*, upstream::autocomplete, StatefulRequest};
|
||||
use ecow::eco_format;
|
||||
use lsp_types::{CompletionItem, CompletionList, CompletionTextEdit, InsertTextFormat, TextEdit};
|
||||
use reflexo::path::{unix_slash, PathClean};
|
||||
|
||||
use crate::{
|
||||
prelude::*,
|
||||
syntax::{get_deref_target, DerefTarget},
|
||||
typst_to_lsp::completion_kind,
|
||||
upstream::{autocomplete_, Completion, CompletionContext, CompletionKind},
|
||||
LspCompletion, StatefulRequest,
|
||||
};
|
||||
|
||||
use self::typst_to_lsp::completion;
|
||||
|
||||
/// The [`textDocument/completion`] request is sent from the client to the
|
||||
/// server to compute completion items at a given cursor position.
|
||||
|
@ -40,7 +52,17 @@ impl StatefulRequest for CompletionRequest {
|
|||
) -> Option<Self::Response> {
|
||||
let doc = doc.as_ref().map(|doc| doc.document.as_ref());
|
||||
let source = ctx.source_by_path(&self.path).ok()?;
|
||||
let cursor = ctx.to_typst_pos(self.position, &source)?;
|
||||
let cursor = {
|
||||
let mut cursor = ctx.to_typst_pos(self.position, &source)?;
|
||||
let text = source.text();
|
||||
|
||||
// while is not char boundary, move cursor to right
|
||||
while cursor < text.len() && !text.is_char_boundary(cursor) {
|
||||
cursor += 1;
|
||||
}
|
||||
|
||||
cursor
|
||||
};
|
||||
|
||||
// Please see <https://github.com/nvarner/typst-lsp/commit/2d66f26fb96ceb8e485f492e5b81e9db25c3e8ec>
|
||||
//
|
||||
|
@ -57,10 +79,278 @@ impl StatefulRequest for CompletionRequest {
|
|||
// assume that the completion is not explicit.
|
||||
let explicit = false;
|
||||
|
||||
let (offset, completions) = autocomplete(ctx.world(), doc, &source, cursor, explicit)?;
|
||||
let root = LinkedNode::new(source.root());
|
||||
let node = root.leaf_at(cursor);
|
||||
let deref_target = node.and_then(|node| get_deref_target(node, cursor));
|
||||
|
||||
let lsp_start_position = ctx.to_lsp_pos(offset, &source);
|
||||
let replace_range = LspRange::new(lsp_start_position, self.position);
|
||||
Some(typst_to_lsp::completions(&completions, replace_range).into())
|
||||
let mut match_ident = None;
|
||||
let mut completion_result = None;
|
||||
match deref_target {
|
||||
Some(DerefTarget::Callee(v) | DerefTarget::VarAccess(v)) => {
|
||||
if v.is::<ast::Ident>() {
|
||||
match_ident = Some(v);
|
||||
}
|
||||
}
|
||||
Some(DerefTarget::ImportPath(v)) => {
|
||||
if !v.text().starts_with(r#""@"#) {
|
||||
completion_result = complete_path(ctx, v, &source, cursor);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
let items = completion_result.or_else(|| {
|
||||
let cc_ctx = CompletionContext::new(ctx.world(), doc, &source, cursor, explicit)?;
|
||||
let (offset, mut completions) = autocomplete_(cc_ctx)?;
|
||||
|
||||
let replace_range;
|
||||
if match_ident.as_ref().is_some_and(|i| i.offset() == offset) {
|
||||
let match_ident = match_ident.unwrap();
|
||||
let rng = match_ident.range();
|
||||
replace_range = ctx.to_lsp_range(match_ident.range(), &source);
|
||||
|
||||
let ident_prefix = source.text()[rng.start..cursor].to_string();
|
||||
completions.retain(|c| {
|
||||
// c.label
|
||||
let mut prefix_matcher = c.label.chars();
|
||||
'ident_matching: for ch in ident_prefix.chars() {
|
||||
for c in prefix_matcher.by_ref() {
|
||||
if c == ch {
|
||||
continue 'ident_matching;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
} else {
|
||||
let lsp_start_position = ctx.to_lsp_pos(offset, &source);
|
||||
replace_range = LspRange::new(lsp_start_position, self.position);
|
||||
}
|
||||
|
||||
Some(
|
||||
completions
|
||||
.iter()
|
||||
.map(|typst_completion| completion(typst_completion, replace_range))
|
||||
.collect_vec(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// To response completions in fine-grained manner, we need to mark result as
|
||||
// incomplete. This follows what rust-analyzer does.
|
||||
// https://github.com/rust-lang/rust-analyzer/blob/f5a9250147f6569d8d89334dc9cca79c0322729f/crates/rust-analyzer/src/handlers/request.rs#L940C55-L940C75
|
||||
Some(CompletionResponse::List(CompletionList {
|
||||
is_incomplete: true,
|
||||
items,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_path(
|
||||
ctx: &AnalysisContext,
|
||||
v: LinkedNode,
|
||||
source: &Source,
|
||||
cursor: usize,
|
||||
) -> Option<Vec<CompletionItem>> {
|
||||
let id = source.id();
|
||||
if id.package().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let vp = v.cast::<ast::Str>()?;
|
||||
// todo: path escape
|
||||
let real_content = vp.get();
|
||||
let text = v.text();
|
||||
let unquoted = &text[1..text.len() - 1];
|
||||
if unquoted != real_content {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = source.text();
|
||||
let vr = v.range();
|
||||
let offset = vr.start + 1;
|
||||
if cursor < offset || vr.end <= cursor || vr.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let path = Path::new(&text[offset..cursor]);
|
||||
let is_abs = path.is_absolute();
|
||||
|
||||
let src_path = id.vpath();
|
||||
let base = src_path.resolve(&ctx.analysis.root)?;
|
||||
let dst_path = src_path.join(path);
|
||||
let mut compl_path = dst_path.as_rootless_path();
|
||||
if !compl_path.is_dir() {
|
||||
compl_path = compl_path.parent().unwrap_or(Path::new(""));
|
||||
}
|
||||
log::info!("compl_path: {src_path:?} + {path:?} -> {compl_path:?}");
|
||||
|
||||
if compl_path.is_absolute() {
|
||||
log::warn!("absolute path completion is not supported for security consideration {path:?}");
|
||||
return None;
|
||||
}
|
||||
|
||||
let dirs = ctx.analysis.root.join(compl_path);
|
||||
log::info!("compl_dirs: {dirs:?}");
|
||||
// find directory or files in the path
|
||||
let mut folder_completions = vec![];
|
||||
let mut module_completions = vec![];
|
||||
// todo: test it correctly
|
||||
for entry in dirs.read_dir().ok()? {
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
let path = entry.path();
|
||||
log::trace!("compl_check_path: {path:?}");
|
||||
if !path.is_dir() && !path.extension().is_some_and(|ext| ext == "typ") {
|
||||
continue;
|
||||
}
|
||||
if path.is_dir()
|
||||
&& path
|
||||
.file_name()
|
||||
.is_some_and(|name| name.to_string_lossy().starts_with('.'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// diff with root
|
||||
let path = dirs.join(path);
|
||||
|
||||
// Skip self smartly
|
||||
if path.clean() == base.clean() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let label = if is_abs {
|
||||
// diff with root
|
||||
let w = path.strip_prefix(&ctx.analysis.root).ok()?;
|
||||
eco_format!("/{}", unix_slash(w))
|
||||
} else {
|
||||
let base = base.parent()?;
|
||||
let w = pathdiff::diff_paths(&path, base)?;
|
||||
unix_slash(&w).into()
|
||||
};
|
||||
log::info!("compl_label: {label:?}");
|
||||
|
||||
if path.is_dir() {
|
||||
folder_completions.push(Completion {
|
||||
label,
|
||||
kind: CompletionKind::Folder,
|
||||
apply: None,
|
||||
detail: None,
|
||||
});
|
||||
} else {
|
||||
module_completions.push(Completion {
|
||||
label,
|
||||
kind: CompletionKind::Module,
|
||||
apply: None,
|
||||
detail: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let rng = offset..vr.end - 1;
|
||||
let replace_range = ctx.to_lsp_range(rng, source);
|
||||
|
||||
module_completions.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
folder_completions.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
|
||||
let mut sorter = 0;
|
||||
let digits = (module_completions.len() + folder_completions.len())
|
||||
.to_string()
|
||||
.len();
|
||||
let completions = module_completions.into_iter().chain(folder_completions);
|
||||
Some(
|
||||
completions
|
||||
.map(|typst_completion| {
|
||||
let lsp_snippet = typst_completion
|
||||
.apply
|
||||
.as_ref()
|
||||
.unwrap_or(&typst_completion.label);
|
||||
let text_edit =
|
||||
CompletionTextEdit::Edit(TextEdit::new(replace_range, lsp_snippet.to_string()));
|
||||
|
||||
let sort_text = format!("{sorter:0>digits$}");
|
||||
sorter += 1;
|
||||
|
||||
let res = LspCompletion {
|
||||
label: typst_completion.label.to_string(),
|
||||
kind: Some(completion_kind(typst_completion.kind.clone())),
|
||||
detail: typst_completion.detail.as_ref().map(String::from),
|
||||
text_edit: Some(text_edit),
|
||||
// don't sort me
|
||||
sort_text: Some(sort_text),
|
||||
filter_text: Some("".to_owned()),
|
||||
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
log::info!("compl_res: {res:?}");
|
||||
|
||||
res
|
||||
})
|
||||
.collect_vec(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::with_settings;
|
||||
use lsp_types::{CompletionItem, CompletionList};
|
||||
|
||||
use super::*;
|
||||
use crate::tests::*;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
snapshot_testing("completion", &|ctx, path| {
|
||||
let source = ctx.source_by_path(&path).unwrap();
|
||||
let rng = find_test_range(&source);
|
||||
let text = source.text()[rng.clone()].to_string();
|
||||
|
||||
let mut results = vec![];
|
||||
for s in rng.clone() {
|
||||
let request = CompletionRequest {
|
||||
path: path.clone(),
|
||||
position: ctx.to_lsp_pos(s, &source),
|
||||
explicit: false,
|
||||
};
|
||||
results.push(request.request(ctx, None).map(|resp| {
|
||||
// CompletionResponse::Array(items)
|
||||
match resp {
|
||||
CompletionResponse::List(l) => CompletionResponse::List(CompletionList {
|
||||
is_incomplete: l.is_incomplete,
|
||||
items: l
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| CompletionItem {
|
||||
label: item.label,
|
||||
kind: item.kind,
|
||||
text_edit: item.text_edit,
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
CompletionResponse::Array(items) => CompletionResponse::Array(
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| CompletionItem {
|
||||
label: item.label,
|
||||
kind: item.kind,
|
||||
text_edit: item.text_edit,
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}));
|
||||
}
|
||||
with_settings!({
|
||||
description => format!("Completion on {text} ({rng:?})"),
|
||||
}, {
|
||||
assert_snapshot!(JsonRepr::new_pure(results));
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
7
crates/tinymist-query/src/fixtures/completion/base.typ
Normal file
7
crates/tinymist-query/src/fixtures/completion/base.typ
Normal file
|
@ -0,0 +1,7 @@
|
|||
|
||||
#let aa() = 1;
|
||||
#let aab() = 1;
|
||||
#let aac() = 1;
|
||||
#let aabc() = 1;
|
||||
|
||||
#aac(/* range -2..0 */);
|
5
crates/tinymist-query/src/fixtures/completion/import.ty_
Normal file
5
crates/tinymist-query/src/fixtures/completion/import.ty_
Normal file
|
@ -0,0 +1,5 @@
|
|||
// path: base.typ
|
||||
#let x = 1;
|
||||
#x
|
||||
-----
|
||||
#import "base.typ" /* range -10..-8 */
|
7
crates/tinymist-query/src/fixtures/completion/let.typ
Normal file
7
crates/tinymist-query/src/fixtures/completion/let.typ
Normal file
|
@ -0,0 +1,7 @@
|
|||
|
||||
#let aa() = 1;
|
||||
#let aab = 1;
|
||||
#let aac() = 1;
|
||||
#let aabc = 1;
|
||||
|
||||
#aac(/* range -2..0 */);
|
|
@ -0,0 +1,290 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on c( (68..70)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/base.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": true,
|
||||
"items": [
|
||||
{
|
||||
"kind": 7,
|
||||
"label": "array",
|
||||
"textEdit": {
|
||||
"newText": "array",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "parbreak",
|
||||
"textEdit": {
|
||||
"newText": "parbreak()${1:}",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "smallcaps",
|
||||
"textEdit": {
|
||||
"newText": "smallcaps(${1:})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "pagebreak",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak(${1:})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "metadata",
|
||||
"textEdit": {
|
||||
"newText": "metadata(${1:})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 21,
|
||||
"label": "aqua",
|
||||
"textEdit": {
|
||||
"newText": "aqua",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "aa",
|
||||
"textEdit": {
|
||||
"newText": "aa",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "aab",
|
||||
"textEdit": {
|
||||
"newText": "aab",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "aabc",
|
||||
"textEdit": {
|
||||
"newText": "aabc",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "aac",
|
||||
"textEdit": {
|
||||
"newText": "aac",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 15,
|
||||
"label": "import package",
|
||||
"textEdit": {
|
||||
"newText": "import \"@${1:}\": ${2:items}",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 15,
|
||||
"label": "include (package)",
|
||||
"textEdit": {
|
||||
"newText": "include \"@${1:}\"",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 15,
|
||||
"label": "array literal",
|
||||
"textEdit": {
|
||||
"newText": "(${1:1, 2, 3})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 15,
|
||||
"label": "dictionary literal",
|
||||
"textEdit": {
|
||||
"newText": "(${1:a: 1, b: 2})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isIncomplete": true,
|
||||
"items": [
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "aabc",
|
||||
"textEdit": {
|
||||
"newText": "aabc",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "aac",
|
||||
"textEdit": {
|
||||
"newText": "aac",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,290 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on c( (64..66)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/let.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": true,
|
||||
"items": [
|
||||
{
|
||||
"kind": 7,
|
||||
"label": "array",
|
||||
"textEdit": {
|
||||
"newText": "array",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "parbreak",
|
||||
"textEdit": {
|
||||
"newText": "parbreak()${1:}",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "smallcaps",
|
||||
"textEdit": {
|
||||
"newText": "smallcaps(${1:})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "pagebreak",
|
||||
"textEdit": {
|
||||
"newText": "pagebreak(${1:})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "metadata",
|
||||
"textEdit": {
|
||||
"newText": "metadata(${1:})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 21,
|
||||
"label": "aqua",
|
||||
"textEdit": {
|
||||
"newText": "aqua",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "aa",
|
||||
"textEdit": {
|
||||
"newText": "aa",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 6,
|
||||
"label": "aab",
|
||||
"textEdit": {
|
||||
"newText": "aab",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 6,
|
||||
"label": "aabc",
|
||||
"textEdit": {
|
||||
"newText": "aabc",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "aac",
|
||||
"textEdit": {
|
||||
"newText": "aac",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 15,
|
||||
"label": "import package",
|
||||
"textEdit": {
|
||||
"newText": "import \"@${1:}\": ${2:items}",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 15,
|
||||
"label": "include (package)",
|
||||
"textEdit": {
|
||||
"newText": "include \"@${1:}\"",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 15,
|
||||
"label": "array literal",
|
||||
"textEdit": {
|
||||
"newText": "(${1:1, 2, 3})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 15,
|
||||
"label": "dictionary literal",
|
||||
"textEdit": {
|
||||
"newText": "(${1:a: 1, b: 2})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isIncomplete": true,
|
||||
"items": [
|
||||
{
|
||||
"kind": 6,
|
||||
"label": "aabc",
|
||||
"textEdit": {
|
||||
"newText": "aabc",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "aac",
|
||||
"textEdit": {
|
||||
"newText": "aac",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 5
|
||||
},
|
||||
"start": {
|
||||
"character": 1,
|
||||
"line": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -267,14 +267,17 @@ pub mod typst_to_lsp {
|
|||
LspRange::new(lsp_start, lsp_end)
|
||||
}
|
||||
|
||||
fn completion_kind(typst_completion_kind: TypstCompletionKind) -> LspCompletionKind {
|
||||
pub 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::Variable => LspCompletionKind::VARIABLE,
|
||||
TypstCompletionKind::Constant => LspCompletionKind::CONSTANT,
|
||||
TypstCompletionKind::Symbol(_) => LspCompletionKind::FIELD,
|
||||
TypstCompletionKind::Type => LspCompletionKind::CLASS,
|
||||
TypstCompletionKind::Module => LspCompletionKind::MODULE,
|
||||
TypstCompletionKind::Folder => LspCompletionKind::FOLDER,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -313,16 +316,6 @@ pub mod typst_to_lsp {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn completions(
|
||||
typst_completions: &[TypstCompletion],
|
||||
lsp_replace: LspRange,
|
||||
) -> Vec<LspCompletion> {
|
||||
typst_completions
|
||||
.iter()
|
||||
.map(|typst_completion| completion(typst_completion, lsp_replace))
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
pub fn tooltip(typst_tooltip: &TypstTooltip) -> LspHoverContents {
|
||||
let lsp_marked_string = match typst_tooltip {
|
||||
TypstTooltip::Text(text) => MarkedString::String(text.to_string()),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use core::fmt;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
|
@ -141,6 +142,23 @@ pub fn run_with_sources<T>(source: &str, f: impl FnOnce(&mut TypstSystemWorld, P
|
|||
f(driver.world_mut(), pw)
|
||||
}
|
||||
|
||||
pub fn find_test_range(s: &Source) -> Range<usize> {
|
||||
// /* range -3..-1 */
|
||||
let re = s.text().find("/* range ").unwrap();
|
||||
let re_base = re;
|
||||
let re = re + "/* range ".len();
|
||||
let re = re..s.text().find(" */").unwrap();
|
||||
let re = &s.text()[re];
|
||||
// split by ".."
|
||||
let mut re = re.split("..");
|
||||
// parse the range
|
||||
let start: isize = re.next().unwrap().parse().unwrap();
|
||||
let end: isize = re.next().unwrap().parse().unwrap();
|
||||
let start = start + re_base as isize;
|
||||
let end = end + re_base as isize;
|
||||
start as usize..end as usize
|
||||
}
|
||||
|
||||
pub fn find_test_position(s: &Source) -> LspPosition {
|
||||
enum AstMatcher {
|
||||
MatchAny { prev: bool },
|
||||
|
|
|
@ -18,6 +18,8 @@ use unscanny::Scanner;
|
|||
use super::{plain_docs_sentence, summarize_font_family};
|
||||
use crate::analysis::{analyze_expr, analyze_import, analyze_labels};
|
||||
|
||||
mod ext;
|
||||
|
||||
/// Autocomplete a cursor position in a source file.
|
||||
///
|
||||
/// Returns the position from which the completions apply and a list of
|
||||
|
@ -51,6 +53,21 @@ pub fn autocomplete(
|
|||
Some((ctx.from, ctx.completions))
|
||||
}
|
||||
|
||||
pub fn autocomplete_(mut ctx: CompletionContext) -> Option<(usize, Vec<Completion>)> {
|
||||
let _ = autocomplete;
|
||||
let _ = complete_comments(&mut ctx)
|
||||
|| complete_field_accesses(&mut ctx)
|
||||
|| complete_open_labels(&mut ctx)
|
||||
|| complete_imports(&mut ctx)
|
||||
|| complete_rules(&mut ctx)
|
||||
|| complete_params(&mut ctx)
|
||||
|| complete_markup(&mut ctx)
|
||||
|| complete_math(&mut ctx)
|
||||
|| complete_code(&mut ctx);
|
||||
|
||||
Some((ctx.from, ctx.completions))
|
||||
}
|
||||
|
||||
/// An autocompletion option.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Completion {
|
||||
|
@ -83,6 +100,12 @@ pub enum CompletionKind {
|
|||
Constant,
|
||||
/// A symbol.
|
||||
Symbol(char),
|
||||
/// A variable.
|
||||
Variable,
|
||||
/// A module.
|
||||
Module,
|
||||
/// A folder.
|
||||
Folder,
|
||||
}
|
||||
|
||||
/// Complete in comments. Or rather, don't!
|
||||
|
@ -312,7 +335,7 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
|
|||
/// Add completions for math snippets.
|
||||
#[rustfmt::skip]
|
||||
fn math_completions(ctx: &mut CompletionContext) {
|
||||
ctx.scope_completions(true, |_| true);
|
||||
ctx.scope_completions_(true, |_| true);
|
||||
|
||||
ctx.snippet_completion(
|
||||
"subscript",
|
||||
|
@ -594,7 +617,7 @@ fn complete_rules(ctx: &mut CompletionContext) -> bool {
|
|||
|
||||
/// Add completions for all functions from the global scope.
|
||||
fn set_rule_completions(ctx: &mut CompletionContext) {
|
||||
ctx.scope_completions(true, |value| {
|
||||
ctx.scope_completions_(true, |value| {
|
||||
matches!(
|
||||
value,
|
||||
Value::Func(func) if func.params()
|
||||
|
@ -607,7 +630,7 @@ fn set_rule_completions(ctx: &mut CompletionContext) {
|
|||
|
||||
/// Add completions for selectors.
|
||||
fn show_rule_selector_completions(ctx: &mut CompletionContext) {
|
||||
ctx.scope_completions(
|
||||
ctx.scope_completions_(
|
||||
false,
|
||||
|value| matches!(value, Value::Func(func) if func.element().is_some()),
|
||||
);
|
||||
|
@ -647,7 +670,7 @@ fn show_rule_recipe_completions(ctx: &mut CompletionContext) {
|
|||
"Transform the element with a function.",
|
||||
);
|
||||
|
||||
ctx.scope_completions(false, |value| matches!(value, Value::Func(_)));
|
||||
ctx.scope_completions_(false, |value| matches!(value, Value::Func(_)));
|
||||
}
|
||||
|
||||
/// Complete call and set rule parameters.
|
||||
|
@ -865,7 +888,7 @@ fn complete_code(ctx: &mut CompletionContext) -> bool {
|
|||
/// Add completions for expression snippets.
|
||||
#[rustfmt::skip]
|
||||
fn code_completions(ctx: &mut CompletionContext, hash: bool) {
|
||||
ctx.scope_completions(true, |value| !hash || {
|
||||
ctx.scope_completions_(true, |value| !hash || {
|
||||
matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_))
|
||||
});
|
||||
|
||||
|
@ -972,13 +995,13 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
|
|||
);
|
||||
|
||||
ctx.snippet_completion(
|
||||
"import (file)",
|
||||
"import \"${file}.typ\": ${items}",
|
||||
"Imports variables from another file.",
|
||||
"import expression",
|
||||
"import ${}",
|
||||
"Imports items from another file.",
|
||||
);
|
||||
|
||||
ctx.snippet_completion(
|
||||
"import (package)",
|
||||
"import package",
|
||||
"import \"@${}\": ${items}",
|
||||
"Imports variables from another file.",
|
||||
);
|
||||
|
@ -1017,25 +1040,25 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
|
|||
}
|
||||
|
||||
/// Context for autocompletion.
|
||||
struct CompletionContext<'a> {
|
||||
world: &'a (dyn World + 'a),
|
||||
document: Option<&'a Document>,
|
||||
global: &'a Scope,
|
||||
math: &'a Scope,
|
||||
text: &'a str,
|
||||
before: &'a str,
|
||||
after: &'a str,
|
||||
leaf: LinkedNode<'a>,
|
||||
cursor: usize,
|
||||
explicit: bool,
|
||||
from: usize,
|
||||
completions: Vec<Completion>,
|
||||
seen_casts: HashSet<u128>,
|
||||
pub struct CompletionContext<'a> {
|
||||
pub world: &'a (dyn World + 'a),
|
||||
pub document: Option<&'a Document>,
|
||||
pub global: &'a Scope,
|
||||
pub math: &'a Scope,
|
||||
pub text: &'a str,
|
||||
pub before: &'a str,
|
||||
pub after: &'a str,
|
||||
pub leaf: LinkedNode<'a>,
|
||||
pub cursor: usize,
|
||||
pub explicit: bool,
|
||||
pub from: usize,
|
||||
pub completions: Vec<Completion>,
|
||||
pub seen_casts: HashSet<u128>,
|
||||
}
|
||||
|
||||
impl<'a> CompletionContext<'a> {
|
||||
/// Create a new autocompletion context.
|
||||
fn new(
|
||||
pub fn new(
|
||||
world: &'a (dyn World + 'a),
|
||||
document: Option<&'a Document>,
|
||||
source: &'a Source,
|
||||
|
@ -1294,7 +1317,7 @@ impl<'a> CompletionContext<'a> {
|
|||
"color.hsl(${h}, ${s}, ${l}, ${a})",
|
||||
"A custom HSLA color.",
|
||||
);
|
||||
self.scope_completions(false, |value| value.ty() == *ty);
|
||||
self.scope_completions_(false, |value| value.ty() == *ty);
|
||||
} else if *ty == Type::of::<Label>() {
|
||||
self.label_completions()
|
||||
} else if *ty == Type::of::<Func>() {
|
||||
|
@ -1310,7 +1333,7 @@ impl<'a> CompletionContext<'a> {
|
|||
apply: Some(eco_format!("${{{ty}}}")),
|
||||
detail: Some(eco_format!("A value of type {ty}.")),
|
||||
});
|
||||
self.scope_completions(false, |value| value.ty() == *ty);
|
||||
self.scope_completions_(false, |value| value.ty() == *ty);
|
||||
}
|
||||
}
|
||||
CastInfo::Union(union) => {
|
||||
|
@ -1324,7 +1347,7 @@ impl<'a> CompletionContext<'a> {
|
|||
/// Add completions for definitions that are available at the cursor.
|
||||
///
|
||||
/// Filters the global/math scope with the given filter.
|
||||
fn scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) {
|
||||
fn _scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) {
|
||||
let mut defined = BTreeSet::new();
|
||||
|
||||
let mut ancestor = Some(self.leaf.clone());
|
||||
|
|
111
crates/tinymist-query/src/upstream/complete/ext.rs
Normal file
111
crates/tinymist-query/src/upstream/complete/ext.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use super::{Completion, CompletionContext, CompletionKind};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use typst::foundations::Value;
|
||||
use typst::syntax::{ast, SyntaxKind};
|
||||
|
||||
use crate::analysis::analyze_import;
|
||||
|
||||
impl<'a> CompletionContext<'a> {
|
||||
/// Add completions for definitions that are available at the cursor.
|
||||
///
|
||||
/// Filters the global/math scope with the given filter.
|
||||
pub fn scope_completions_(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) {
|
||||
let mut defined = BTreeMap::new();
|
||||
|
||||
let mut ancestor = Some(self.leaf.clone());
|
||||
while let Some(node) = &ancestor {
|
||||
let mut sibling = Some(node.clone());
|
||||
while let Some(node) = &sibling {
|
||||
if let Some(v) = node.cast::<ast::LetBinding>() {
|
||||
let kind = match v.kind() {
|
||||
ast::LetBindingKind::Closure(..) => CompletionKind::Func,
|
||||
ast::LetBindingKind::Normal(..) => CompletionKind::Variable,
|
||||
};
|
||||
for ident in v.kind().bindings() {
|
||||
defined.insert(ident.get().clone(), kind.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(v) = node.cast::<ast::ModuleImport>() {
|
||||
let imports = v.imports();
|
||||
match imports {
|
||||
None | Some(ast::Imports::Wildcard) => {
|
||||
if let Some(value) = node
|
||||
.children()
|
||||
.find(|child| child.is::<ast::Expr>())
|
||||
.and_then(|source| analyze_import(self.world, &source))
|
||||
{
|
||||
if imports.is_none() {
|
||||
// todo: correct kind
|
||||
defined.extend(
|
||||
value
|
||||
.name()
|
||||
.map(Into::into)
|
||||
.map(|e| (e, CompletionKind::Variable)),
|
||||
);
|
||||
} else if let Some(scope) = value.scope() {
|
||||
for (name, _) in scope.iter() {
|
||||
defined.insert(name.clone(), CompletionKind::Variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ast::Imports::Items(items)) => {
|
||||
for item in items.iter() {
|
||||
defined.insert(
|
||||
item.bound_name().get().clone(),
|
||||
CompletionKind::Variable,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sibling = node.prev_sibling();
|
||||
}
|
||||
|
||||
if let Some(parent) = node.parent() {
|
||||
if let Some(v) = parent.cast::<ast::ForLoop>() {
|
||||
if node.prev_sibling_kind() != Some(SyntaxKind::In) {
|
||||
let pattern = v.pattern();
|
||||
for ident in pattern.bindings() {
|
||||
defined.insert(ident.get().clone(), CompletionKind::Variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ancestor = Some(parent.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
let in_math = matches!(
|
||||
self.leaf.parent_kind(),
|
||||
Some(SyntaxKind::Equation)
|
||||
| Some(SyntaxKind::Math)
|
||||
| Some(SyntaxKind::MathFrac)
|
||||
| Some(SyntaxKind::MathAttach)
|
||||
);
|
||||
|
||||
let scope = if in_math { self.math } else { self.global };
|
||||
for (name, value) in scope.iter() {
|
||||
if filter(value) && !defined.contains_key(name) {
|
||||
self.value_completion(Some(name.clone()), value, parens, None);
|
||||
}
|
||||
}
|
||||
|
||||
for (name, kind) in defined {
|
||||
if !name.is_empty() {
|
||||
self.completions.push(Completion {
|
||||
kind,
|
||||
label: name,
|
||||
apply: None,
|
||||
detail: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -414,6 +414,8 @@ impl Init {
|
|||
trigger_characters: Some(vec![
|
||||
String::from("#"),
|
||||
String::from("."),
|
||||
String::from("/"),
|
||||
String::from("\""),
|
||||
String::from("@"),
|
||||
]),
|
||||
..Default::default()
|
||||
|
|
|
@ -375,7 +375,7 @@ fn e2e() {
|
|||
});
|
||||
|
||||
let hash = replay_log(&tinymist_binary, &root.join("neovim"));
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:ff13445227b3b86b70905bba912bcd0a");
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:31d9406095865c1f57603564405d0ec2");
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -386,7 +386,7 @@ fn e2e() {
|
|||
});
|
||||
|
||||
let hash = replay_log(&tinymist_binary, &root.join("vscode"));
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:db9523369516f3a16997fc1913381d6e");
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:f2f40ce6a31dd0423a453e051c56a977");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue