feat: complete on import item path (#949)

The import item path syntax was introduced in typst 0.12.0 and had ruin the completion on import items.

* fix: grandparent is ImportItems

Fix: for `import "lib.typ": tes|`, the parent syntax is import item
path, then parent is import items

* feat: identify syntax context in import list

* feat: check the path under cursor for import completion

* fix: adjust from correctly

* dev: remove useless log

* fix: the unknown font on windows

---------

Co-authored-by: Myriad-Dreamin <camiyoru@gmail.com>
This commit is contained in:
Yifan Song 2024-12-06 04:35:59 +01:00 committed by GitHub
parent a7f203a31c
commit b4ecb7f14b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 332 additions and 14 deletions

View file

@ -0,0 +1,6 @@
/// path: base.typ
#let aa() = 1;
-----
/// contains: base,aa
#import "base.typ": /* range -1..1 */

View file

@ -0,0 +1,9 @@
/// path: base.typ
#let table-prefix() = 1;
-----
/// path: lib.typ
#import "base.typ"
-----
/// contains: table,table-prefix
#import "lib.typ": base./* range -1..1 */

View file

@ -0,0 +1,9 @@
/// path: base.typ
#let table-prefix() = 1;
-----
/// path: lib.typ
#import "base.typ"
-----
/// contains: table,table-prefix
#import "lib.typ": base.table/* range -1..1 */

View file

@ -0,0 +1,10 @@
/// path: base.typ
#let table-prefix() = 1;
#let table-prefix2() = 1;
-----
/// path: lib.typ
#import "base.typ"
-----
/// contains: table,table-prefix,table-prefix2
#import "lib.typ": base.table-prefix, base.table/* range -1..1 */

View file

@ -0,0 +1,6 @@
/// path: base.typ
#let table-prefix() = 1;
-----
/// contains: base,table,table-prefix
#import "base.typ": table/* range -1..1 */

View file

@ -0,0 +1,35 @@
---
source: crates/tinymist-query/src/completion.rs
description: Completion on / (41..43)
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/import_item.typ
snapshot_kind: text
---
[
{
"isIncomplete": false,
"items": []
},
{
"isIncomplete": false,
"items": [
{
"kind": 3,
"label": "aa",
"textEdit": {
"newText": "aa",
"range": {
"end": {
"character": 20,
"line": 1
},
"start": {
"character": 20,
"line": 1
}
}
}
}
]
}
]

View file

@ -0,0 +1,35 @@
---
source: crates/tinymist-query/src/completion.rs
description: Completion on ./ (56..58)
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/import_item_dot.typ
snapshot_kind: text
---
[
{
"isIncomplete": false,
"items": []
},
{
"isIncomplete": false,
"items": [
{
"kind": 3,
"label": "table-prefix",
"textEdit": {
"newText": "table-prefix",
"range": {
"end": {
"character": 24,
"line": 1
},
"start": {
"character": 24,
"line": 1
}
}
}
}
]
}
]

View file

@ -0,0 +1,53 @@
---
source: crates/tinymist-query/src/completion.rs
description: Completion on e/ (61..63)
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/import_item_path.typ
snapshot_kind: text
---
[
{
"isIncomplete": false,
"items": [
{
"kind": 3,
"label": "table-prefix",
"textEdit": {
"newText": "table-prefix",
"range": {
"end": {
"character": 29,
"line": 1
},
"start": {
"character": 24,
"line": 1
}
}
}
}
]
},
{
"isIncomplete": false,
"items": [
{
"kind": 3,
"label": "table-prefix",
"textEdit": {
"newText": "table-prefix",
"range": {
"end": {
"character": 29,
"line": 1
},
"start": {
"character": 24,
"line": 1
}
}
}
}
]
}
]

View file

@ -0,0 +1,53 @@
---
source: crates/tinymist-query/src/completion.rs
description: Completion on e/ (94..96)
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/import_item_path_filter.typ
snapshot_kind: text
---
[
{
"isIncomplete": false,
"items": [
{
"kind": 3,
"label": "table-prefix2",
"textEdit": {
"newText": "table-prefix2",
"range": {
"end": {
"character": 48,
"line": 1
},
"start": {
"character": 43,
"line": 1
}
}
}
}
]
},
{
"isIncomplete": false,
"items": [
{
"kind": 3,
"label": "table-prefix2",
"textEdit": {
"newText": "table-prefix2",
"range": {
"end": {
"character": 48,
"line": 1
},
"start": {
"character": 43,
"line": 1
}
}
}
}
]
}
]

View file

@ -0,0 +1,53 @@
---
source: crates/tinymist-query/src/completion.rs
description: Completion on e/ (62..64)
expression: "JsonRepr::new_pure(results)"
input_file: crates/tinymist-query/src/fixtures/completion/import_item_shadow.typ
snapshot_kind: text
---
[
{
"isIncomplete": false,
"items": [
{
"kind": 3,
"label": "table-prefix",
"textEdit": {
"newText": "table-prefix",
"range": {
"end": {
"character": 25,
"line": 1
},
"start": {
"character": 20,
"line": 1
}
}
}
}
]
},
{
"isIncomplete": false,
"items": [
{
"kind": 3,
"label": "table-prefix",
"textEdit": {
"newText": "table-prefix",
"range": {
"end": {
"character": 25,
"line": 1
},
"start": {
"character": 20,
"line": 1
}
}
}
}
]
}
]

View file

@ -8,6 +8,7 @@ use lsp_types::TextEdit;
use serde::{Deserialize, Serialize};
use typst::foundations::{fields_on, format_str, repr, Repr, StyleChain, Styles, Value};
use typst::model::Document;
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind};
use typst::text::RawElem;
use typst::World;
@ -40,8 +41,8 @@ pub fn autocomplete(
|| complete_type(&mut ctx).is_none() && {
crate::log_debug_ct!("continue after completing type");
complete_labels(&mut ctx)
|| complete_field_accesses(&mut ctx)
|| complete_imports(&mut ctx)
|| complete_field_accesses(&mut ctx)
|| complete_markup(&mut ctx)
|| complete_math(&mut ctx)
|| complete_code(&mut ctx, false)
@ -538,7 +539,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>());
then {
ctx.from = ctx.cursor;
import_item_completions(ctx, items, &source);
import_item_completions(ctx, items, vec![], &source);
return true;
}
}
@ -546,16 +547,21 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
// Behind a half-started identifier in an import list:
// "#import "path.typ": thi|",
if_chain! {
if ctx.leaf.kind() == SyntaxKind::Ident;
if let Some(parent) = ctx.leaf.parent();
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 {
ctx.from = ctx.leaf.offset();
import_item_completions(ctx, items, &source);
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;
}
}
@ -567,22 +573,43 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
fn import_item_completions<'a>(
ctx: &mut CompletionContext<'a>,
existing: ast::ImportItems<'a>,
comps: Vec<ast::Ident>,
source: &LinkedNode,
) {
let Some(value) = ctx.ctx.analyze_import(source).1 else {
// 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;
};
let Some(scope) = value.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 existing
.iter()
.all(|item| item.original_name().as_str() != name)
{
if seen.iter().all(|item| item.as_str() != name) {
ctx.value_completion(Some(name.clone()), value, false, None);
}
}

View file

@ -424,6 +424,7 @@ impl CompletionContext<'_> {
let filter = |c: &CompletionKindChecker| {
match surrounding_syntax {
SurroundingSyntax::Regular => true,
SurroundingSyntax::ImportList => false,
SurroundingSyntax::Selector => 'selector: {
for func in &c.functions {
if func.element().is_some() {
@ -564,6 +565,7 @@ pub(crate) enum SurroundingSyntax {
Regular,
Selector,
ShowTransform,
ImportList,
SetRule,
}
@ -580,6 +582,23 @@ fn check_surrounding_syntax(mut leaf: &LinkedNode) -> Option<SurroundingSyntax>
SyntaxKind::CodeBlock | SyntaxKind::ContentBlock | SyntaxKind::Equation => {
return Some(Regular);
}
SyntaxKind::ImportItemPath
| SyntaxKind::ImportItems
| SyntaxKind::RenamedImportItem => {
return Some(ImportList);
}
SyntaxKind::ModuleImport => {
let colon = parent.children().find(|s| s.kind() == SyntaxKind::Colon);
let Some(colon) = colon else {
return Some(Regular);
};
if leaf.offset() >= colon.offset() {
return Some(ImportList);
} else {
return Some(Regular);
}
}
SyntaxKind::Named => {
return Some(Regular);
}
@ -1332,6 +1351,7 @@ fn type_completion(
/// Complete call and set rule parameters.
pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
use crate::syntax::get_check_target;
use SurroundingSyntax::*;
let check_target = get_check_target(ctx.leaf.clone());
crate::log_debug_ct!("complete_type: pos {:?} -> {check_target:#?}", ctx.leaf);
@ -1379,7 +1399,7 @@ pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
let scope = ctx.surrounding_syntax();
crate::log_debug_ct!("complete_type: {:?} -> ({scope:?}, {ty:#?})", ctx.leaf);
if matches!((scope, &ty), (SurroundingSyntax::Regular, None)) {
if matches!((scope, &ty), (Regular, None)) || matches!(scope, ImportList) {
return None;
}
@ -1407,6 +1427,7 @@ pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
match scope {
SurroundingSyntax::Regular => {}
SurroundingSyntax::ImportList => {}
SurroundingSyntax::Selector => {
ctx.snippet_completion(
"text selector",
@ -1507,6 +1528,7 @@ pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
match scope {
SurroundingSyntax::Regular => {}
SurroundingSyntax::ImportList => {}
SurroundingSyntax::Selector => {
ctx.enrich("", ": ${}");
}