mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-25 05:22:52 +00:00
feat: detect explicit completion from vscode (#1496)
* fix: prevent completion list from showing on bad pos * feat: detect explicit completion from vscode * fix: cases * test: update snapshot
This commit is contained in:
parent
8424df13f9
commit
7399fccd5a
21 changed files with 262 additions and 19 deletions
|
|
@ -10,7 +10,7 @@ use lsp_types::InsertTextFormat;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tinymist_analysis::syntax::bad_completion_cursor;
|
use tinymist_analysis::syntax::{bad_completion_cursor, BadCompletionCursor};
|
||||||
use tinymist_derive::BindTyCtx;
|
use tinymist_derive::BindTyCtx;
|
||||||
use tinymist_project::LspWorld;
|
use tinymist_project::LspWorld;
|
||||||
use tinymist_std::path::unix_slash;
|
use tinymist_std::path::unix_slash;
|
||||||
|
|
@ -175,10 +175,6 @@ impl<'a> CompletionCursor<'a> {
|
||||||
let syntax_context = classify_context(leaf.clone(), Some(cursor));
|
let syntax_context = classify_context(leaf.clone(), Some(cursor));
|
||||||
let surrounding_syntax = surrounding_syntax(&leaf);
|
let surrounding_syntax = surrounding_syntax(&leaf);
|
||||||
|
|
||||||
if bad_completion_cursor(syntax.as_ref(), syntax_context.as_ref(), &leaf).is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::log_debug_ct!("CompletionCursor: syntax {leaf:?} -> {syntax:#?}");
|
crate::log_debug_ct!("CompletionCursor: syntax {leaf:?} -> {syntax:#?}");
|
||||||
crate::log_debug_ct!("CompletionCursor: context {leaf:?} -> {syntax_context:#?}");
|
crate::log_debug_ct!("CompletionCursor: context {leaf:?} -> {syntax_context:#?}");
|
||||||
crate::log_debug_ct!("CompletionCursor: surrounding {leaf:?} -> {surrounding_syntax:#?}");
|
crate::log_debug_ct!("CompletionCursor: surrounding {leaf:?} -> {surrounding_syntax:#?}");
|
||||||
|
|
@ -637,12 +633,21 @@ impl CompletionPair<'_, '_, '_> {
|
||||||
| None => {}
|
| None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cursor_pos = bad_completion_cursor(
|
||||||
|
self.cursor.syntax.as_ref(),
|
||||||
|
self.cursor.syntax_context.as_ref(),
|
||||||
|
&self.cursor.leaf,
|
||||||
|
);
|
||||||
|
|
||||||
// Triggers a complete type checking.
|
// Triggers a complete type checking.
|
||||||
let ty = self
|
let ty = self
|
||||||
.worker
|
.worker
|
||||||
.ctx
|
.ctx
|
||||||
.post_type_of_node(self.cursor.leaf.clone())
|
.post_type_of_node(self.cursor.leaf.clone())
|
||||||
.filter(|ty| !matches!(ty, Ty::Any));
|
.filter(|ty| !matches!(ty, Ty::Any))
|
||||||
|
// Forbids argument completion list if the cursor is in a bad position. This will
|
||||||
|
// prevent the completion list from showing up.
|
||||||
|
.filter(|_| !matches!(cursor_pos, Some(BadCompletionCursor::ArgListPos)));
|
||||||
|
|
||||||
crate::log_debug_ct!(
|
crate::log_debug_ct!(
|
||||||
"complete_type: {:?} -> ({surrounding_syntax:?}, {ty:#?})",
|
"complete_type: {:?} -> ({surrounding_syntax:?}, {ty:#?})",
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,10 @@ impl StatefulRequest for CompletionRequest {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let document = doc.as_ref().map(|doc| &doc.document);
|
||||||
|
let source = ctx.source_by_path(&self.path).ok()?;
|
||||||
|
let cursor = ctx.to_typst_pos_offset(&source, self.position, 0)?;
|
||||||
|
|
||||||
// Please see <https://github.com/nvarner/typst-lsp/commit/2d66f26fb96ceb8e485f492e5b81e9db25c3e8ec>
|
// Please see <https://github.com/nvarner/typst-lsp/commit/2d66f26fb96ceb8e485f492e5b81e9db25c3e8ec>
|
||||||
//
|
//
|
||||||
// FIXME: correctly identify a completion which is triggered
|
// FIXME: correctly identify a completion which is triggered
|
||||||
|
|
@ -68,11 +72,17 @@ impl StatefulRequest for CompletionRequest {
|
||||||
//
|
//
|
||||||
// Hence, we cannot distinguish between the two cases. Conservatively, we
|
// Hence, we cannot distinguish between the two cases. Conservatively, we
|
||||||
// assume that the completion is not explicit.
|
// assume that the completion is not explicit.
|
||||||
let explicit = false;
|
//
|
||||||
|
// Second try: According to VSCode:
|
||||||
let document = doc.as_ref().map(|doc| &doc.document);
|
// - <https://github.com/microsoft/vscode/issues/130953>
|
||||||
let source = ctx.source_by_path(&self.path).ok()?;
|
// - <https://github.com/microsoft/vscode/commit/0984071fe0d8a3c157a1ba810c244752d69e5689>
|
||||||
let cursor = ctx.to_typst_pos_offset(&source, self.position, 0)?;
|
// Checks the previous text to filter out letter explicit completions.
|
||||||
|
let explicit = self.explicit
|
||||||
|
&& (self.trigger_character.is_none() && {
|
||||||
|
let prev_text = &source.text()[..cursor];
|
||||||
|
let prev_char = prev_text.chars().next_back();
|
||||||
|
prev_char.is_none_or(|c| !c.is_alphabetic())
|
||||||
|
});
|
||||||
let mut cursor = CompletionCursor::new(ctx.shared_(), &source, cursor)?;
|
let mut cursor = CompletionCursor::new(ctx.shared_(), &source, cursor)?;
|
||||||
|
|
||||||
let mut worker = CompletionWorker::new(ctx, document, explicit, self.trigger_character)?;
|
let mut worker = CompletionWorker::new(ctx, document, explicit, self.trigger_character)?;
|
||||||
|
|
@ -118,6 +128,11 @@ mod tests {
|
||||||
let trigger_character = properties
|
let trigger_character = properties
|
||||||
.get("trigger_character")
|
.get("trigger_character")
|
||||||
.map(|v| v.chars().next().unwrap());
|
.map(|v| v.chars().next().unwrap());
|
||||||
|
let explicit = match properties.get("explicit").copied().map(str::trim) {
|
||||||
|
Some("true") => true,
|
||||||
|
Some("false") | None => false,
|
||||||
|
Some(v) => panic!("invalid value for 'explicit' property: {v}"),
|
||||||
|
};
|
||||||
|
|
||||||
let mut includes = HashSet::new();
|
let mut includes = HashSet::new();
|
||||||
let mut excludes = HashSet::new();
|
let mut excludes = HashSet::new();
|
||||||
|
|
@ -171,7 +186,7 @@ mod tests {
|
||||||
let request = CompletionRequest {
|
let request = CompletionRequest {
|
||||||
path: ctx.path_for_id(id).unwrap().as_path().to_owned(),
|
path: ctx.path_for_id(id).unwrap().as_path().to_owned(),
|
||||||
position: ctx.to_lsp_pos(s, &source),
|
position: ctx.to_lsp_pos(s, &source),
|
||||||
explicit: false,
|
explicit,
|
||||||
trigger_character,
|
trigger_character,
|
||||||
};
|
};
|
||||||
let result = request
|
let result = request
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
/// contains: AA
|
||||||
|
/// explicit: true
|
||||||
|
$norm(x)/* range 0..1 */$
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
/// contains: AA
|
||||||
|
$norm(x)A/* range 0..1 */$
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
/// contains: AA
|
||||||
|
/// explicit: false
|
||||||
|
$norm(x)/* range 0..1 */$
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
/// contains: label
|
||||||
|
/// explicit: true
|
||||||
|
|
||||||
|
/* range 0..1 */
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
/// contains: label
|
||||||
|
/// explicit: true
|
||||||
|
|
||||||
|
te/* range 0..1 */
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
/// contains: tanh
|
||||||
|
$tan/* range 0..1 */$
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
/// contains: AA
|
||||||
|
/// explicit: true
|
||||||
|
$/* range 0..1 */$
|
||||||
|
|
@ -5,5 +5,8 @@ expression: "JsonRepr::new_pure(results)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/completion/arg_range.typ
|
input_file: crates/tinymist-query/src/fixtures/completion/arg_range.typ
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
null
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,8 @@ expression: "JsonRepr::new_pure(results)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/completion/arg_range3.typ
|
input_file: crates/tinymist-query/src/fixtures/completion/arg_range3.typ
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
null
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,8 @@ expression: "JsonRepr::new_pure(results)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/completion/arg_range4.typ
|
input_file: crates/tinymist-query/src/fixtures/completion/arg_range4.typ
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
null
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,8 @@ expression: "JsonRepr::new_pure(results)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/completion/arg_range_math.typ
|
input_file: crates/tinymist-query/src/fixtures/completion/arg_range_math.typ
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
null
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
source: crates/tinymist-query/src/completion.rs
|
||||||
|
description: Completion on / (44..45)
|
||||||
|
expression: "JsonRepr::new_pure(results)"
|
||||||
|
input_file: crates/tinymist-query/src/fixtures/completion/arg_range_math2.typ
|
||||||
|
---
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"kind": 5,
|
||||||
|
"label": "AA",
|
||||||
|
"labelDetails": {
|
||||||
|
"description": "𝔸"
|
||||||
|
},
|
||||||
|
"sortText": "000",
|
||||||
|
"textEdit": {
|
||||||
|
"newText": "AA",
|
||||||
|
"range": {
|
||||||
|
"end": {
|
||||||
|
"character": 8,
|
||||||
|
"line": 2
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"character": 8,
|
||||||
|
"line": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
source: crates/tinymist-query/src/completion.rs
|
||||||
|
description: Completion on / (26..27)
|
||||||
|
expression: "JsonRepr::new_pure(results)"
|
||||||
|
input_file: crates/tinymist-query/src/fixtures/completion/arg_range_math3.typ
|
||||||
|
---
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"kind": 5,
|
||||||
|
"label": "AA",
|
||||||
|
"labelDetails": {
|
||||||
|
"description": "𝔸"
|
||||||
|
},
|
||||||
|
"sortText": "000",
|
||||||
|
"textEdit": {
|
||||||
|
"newText": "AA",
|
||||||
|
"range": {
|
||||||
|
"end": {
|
||||||
|
"character": 9,
|
||||||
|
"line": 1
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"character": 8,
|
||||||
|
"line": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/tinymist-query/src/completion.rs
|
||||||
|
description: Completion on / (45..46)
|
||||||
|
expression: "JsonRepr::new_pure(results)"
|
||||||
|
input_file: crates/tinymist-query/src/fixtures/completion/arg_range_math4.typ
|
||||||
|
---
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
source: crates/tinymist-query/src/completion.rs
|
||||||
|
description: Completion on / (40..41)
|
||||||
|
expression: "JsonRepr::new_pure(results)"
|
||||||
|
input_file: crates/tinymist-query/src/fixtures/completion/mode_markup.typ
|
||||||
|
---
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"kind": 15,
|
||||||
|
"label": "label",
|
||||||
|
"sortText": "007",
|
||||||
|
"textEdit": {
|
||||||
|
"newText": "<${1:name}>",
|
||||||
|
"range": {
|
||||||
|
"end": {
|
||||||
|
"character": 0,
|
||||||
|
"line": 3
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"character": 0,
|
||||||
|
"line": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: crates/tinymist-query/src/completion.rs
|
||||||
|
description: Completion on / (42..43)
|
||||||
|
expression: "JsonRepr::new_pure(results)"
|
||||||
|
input_file: crates/tinymist-query/src/fixtures/completion/mode_markup2.typ
|
||||||
|
---
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
source: crates/tinymist-query/src/completion.rs
|
||||||
|
description: Completion on / (23..24)
|
||||||
|
expression: "JsonRepr::new_pure(results)"
|
||||||
|
input_file: crates/tinymist-query/src/fixtures/completion/mode_math.typ
|
||||||
|
---
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"kind": 6,
|
||||||
|
"label": "tanh",
|
||||||
|
"labelDetails": {
|
||||||
|
"description": "op(text: [tanh], limits: false)"
|
||||||
|
},
|
||||||
|
"sortText": "318",
|
||||||
|
"textEdit": {
|
||||||
|
"newText": "tanh",
|
||||||
|
"range": {
|
||||||
|
"end": {
|
||||||
|
"character": 4,
|
||||||
|
"line": 1
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"character": 1,
|
||||||
|
"line": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
source: crates/tinymist-query/src/completion.rs
|
||||||
|
description: Completion on / (37..38)
|
||||||
|
expression: "JsonRepr::new_pure(results)"
|
||||||
|
input_file: crates/tinymist-query/src/fixtures/completion/mode_math2.typ
|
||||||
|
---
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"isIncomplete": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"kind": 5,
|
||||||
|
"label": "AA",
|
||||||
|
"labelDetails": {
|
||||||
|
"description": "𝔸"
|
||||||
|
},
|
||||||
|
"sortText": "000",
|
||||||
|
"textEdit": {
|
||||||
|
"newText": "AA",
|
||||||
|
"range": {
|
||||||
|
"end": {
|
||||||
|
"character": 1,
|
||||||
|
"line": 2
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"character": 1,
|
||||||
|
"line": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -228,9 +228,8 @@ impl ServerState {
|
||||||
let context = params.context.as_ref();
|
let context = params.context.as_ref();
|
||||||
let explicit =
|
let explicit =
|
||||||
context.is_some_and(|context| context.trigger_kind == CompletionTriggerKind::INVOKED);
|
context.is_some_and(|context| context.trigger_kind == CompletionTriggerKind::INVOKED);
|
||||||
let trigger_character = params
|
let trigger_character = context
|
||||||
.context
|
.and_then(|c| c.trigger_character.as_ref())
|
||||||
.and_then(|c| c.trigger_character)
|
|
||||||
.and_then(|c| c.chars().next());
|
.and_then(|c| c.chars().next());
|
||||||
|
|
||||||
run_query!(
|
run_query!(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue