mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
feat: forbid bad field access syntax in math mode (#1550)
This commit is contained in:
parent
f555fc3840
commit
e33688336d
29 changed files with 452 additions and 65 deletions
|
@ -209,6 +209,11 @@ impl<'a> CompletionCursor<'a> {
|
|||
matches!(self.syntax, Some(SyntaxClass::Callee(..)))
|
||||
}
|
||||
|
||||
/// Gets the interpret mode at the cursor.
|
||||
pub fn leaf_mode(&self) -> InterpretMode {
|
||||
interpret_mode_at(Some(&self.leaf))
|
||||
}
|
||||
|
||||
/// Gets selected node under cursor.
|
||||
fn selected_node(&self) -> &Option<SelectedNode<'a>> {
|
||||
self.ident_cursor.get_or_init(|| {
|
||||
|
@ -553,7 +558,7 @@ impl CompletionPair<'_, '_, '_> {
|
|||
}
|
||||
|
||||
let surrounding_syntax = self.cursor.surrounding_syntax;
|
||||
let mode = interpret_mode_at(Some(&self.cursor.leaf));
|
||||
let mode = self.cursor.leaf_mode();
|
||||
|
||||
// Special completions 2, we should remove them finally
|
||||
if matches!(surrounding_syntax, ImportList) {
|
||||
|
@ -595,7 +600,7 @@ impl CompletionPair<'_, '_, '_> {
|
|||
|
||||
self.cursor.from = field.offset(&self.cursor.source)?;
|
||||
|
||||
self.field_access_completions(&target);
|
||||
self.doc_access_completions(&target);
|
||||
return Some(());
|
||||
}
|
||||
Some(SyntaxContext::ImportPath(path) | SyntaxContext::IncludePath(path)) => {
|
||||
|
|
|
@ -4,10 +4,21 @@ use crate::analysis::completion::typst_specific::ValueCompletionInfo;
|
|||
|
||||
use super::*;
|
||||
impl CompletionPair<'_, '_, '_> {
|
||||
/// Add completions for all fields on a node.
|
||||
pub fn field_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
|
||||
self.value_field_access_completions(target)
|
||||
.or_else(|| self.type_field_access_completions(target))
|
||||
/// Add completions for all dot targets on a node.
|
||||
pub fn doc_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
|
||||
self.value_dot_access_completions(target)
|
||||
.or_else(|| self.type_dot_access_completions(target))
|
||||
}
|
||||
|
||||
/// Add completions for all fields on a type.
|
||||
fn type_dot_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
|
||||
let mode = self.cursor.leaf_mode();
|
||||
|
||||
if !matches!(mode, InterpretMode::Math) {
|
||||
self.type_field_access_completions(target);
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Add completions for all fields on a type.
|
||||
|
@ -37,19 +48,93 @@ impl CompletionPair<'_, '_, '_> {
|
|||
}
|
||||
|
||||
/// Add completions for all fields on a value.
|
||||
fn value_field_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
|
||||
fn value_dot_access_completions(&mut self, target: &LinkedNode) -> Option<()> {
|
||||
let (value, styles) = self.worker.ctx.analyze_expr(target).into_iter().next()?;
|
||||
|
||||
let mode = self.cursor.leaf_mode();
|
||||
let valid_field_access_syntax =
|
||||
!matches!(mode, InterpretMode::Math) || is_valid_math_field_access(target);
|
||||
|
||||
if valid_field_access_syntax {
|
||||
self.value_field_access_completions(&value, mode);
|
||||
}
|
||||
|
||||
self.postfix_completions(target, Ty::Value(InsTy::new(value.clone())));
|
||||
|
||||
match value {
|
||||
Value::Symbol(symbol) => {
|
||||
for modifier in symbol.modifiers() {
|
||||
if let Ok(modified) = symbol.clone().modified(modifier) {
|
||||
self.push_completion(Completion {
|
||||
kind: CompletionKind::Symbol(modified.get()),
|
||||
label: modifier.into(),
|
||||
label_details: Some(symbol_label_detail(modified.get())),
|
||||
..Completion::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.ufcs_completions(target);
|
||||
}
|
||||
Value::Content(content) => {
|
||||
if valid_field_access_syntax {
|
||||
for (name, value) in content.fields() {
|
||||
self.value_completion(Some(name.into()), &value, false, None);
|
||||
}
|
||||
}
|
||||
|
||||
self.ufcs_completions(target);
|
||||
}
|
||||
Value::Dict(dict) if valid_field_access_syntax => {
|
||||
for (name, value) in dict.iter() {
|
||||
self.value_completion(Some(name.clone().into()), value, false, None);
|
||||
}
|
||||
}
|
||||
Value::Func(func) if valid_field_access_syntax => {
|
||||
// Autocomplete get rules.
|
||||
if let Some((elem, styles)) = func.element().zip(styles.as_ref()) {
|
||||
for param in elem.params().iter().filter(|param| !param.required) {
|
||||
if let Some(value) = elem
|
||||
.field_id(param.name)
|
||||
.map(|id| elem.field_from_styles(id, StyleChain::new(styles)))
|
||||
{
|
||||
self.value_completion(
|
||||
Some(param.name.into()),
|
||||
&value.unwrap(),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn value_field_access_completions(&mut self, value: &Value, mode: InterpretMode) {
|
||||
let elem_parens = !matches!(mode, InterpretMode::Math);
|
||||
for (name, bind) in value.ty().scope().iter() {
|
||||
self.value_completion(Some(name.clone()), bind.read(), true, None);
|
||||
if matches!(mode, InterpretMode::Math) && is_func(bind.read()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.value_completion(Some(name.clone()), bind.read(), elem_parens, None);
|
||||
}
|
||||
|
||||
if let Some(scope) = value.scope() {
|
||||
for (name, bind) in scope.iter() {
|
||||
if matches!(mode, InterpretMode::Math) && is_func(bind.read()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.value_completion_(
|
||||
bind.read(),
|
||||
ValueCompletionInfo {
|
||||
label: Some(name.clone()),
|
||||
parens: true,
|
||||
parens: elem_parens,
|
||||
docs: None,
|
||||
label_details: None,
|
||||
bound_self: true,
|
||||
|
@ -75,57 +160,20 @@ impl CompletionPair<'_, '_, '_> {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
self.postfix_completions(target, Ty::Value(InsTy::new(value.clone())));
|
||||
|
||||
match value {
|
||||
Value::Symbol(symbol) => {
|
||||
for modifier in symbol.modifiers() {
|
||||
if let Ok(modified) = symbol.clone().modified(modifier) {
|
||||
self.push_completion(Completion {
|
||||
kind: CompletionKind::Symbol(modified.get()),
|
||||
label: modifier.into(),
|
||||
label_details: Some(symbol_label_detail(modified.get())),
|
||||
..Completion::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.ufcs_completions(target);
|
||||
}
|
||||
Value::Content(content) => {
|
||||
for (name, value) in content.fields() {
|
||||
self.value_completion(Some(name.into()), &value, false, None);
|
||||
}
|
||||
|
||||
self.ufcs_completions(target);
|
||||
}
|
||||
Value::Dict(dict) => {
|
||||
for (name, value) in dict.iter() {
|
||||
self.value_completion(Some(name.clone().into()), value, false, None);
|
||||
}
|
||||
}
|
||||
Value::Func(func) => {
|
||||
// Autocomplete get rules.
|
||||
if let Some((elem, styles)) = func.element().zip(styles.as_ref()) {
|
||||
for param in elem.params().iter().filter(|param| !param.required) {
|
||||
if let Some(value) = elem
|
||||
.field_id(param.name)
|
||||
.map(|id| elem.field_from_styles(id, StyleChain::new(styles)))
|
||||
{
|
||||
self.value_completion(
|
||||
Some(param.name.into()),
|
||||
&value.unwrap(),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_func(read: &Value) -> bool {
|
||||
matches!(read, Value::Func(func) if func.element().is_none())
|
||||
}
|
||||
|
||||
fn is_valid_math_field_access(target: &SyntaxNode) -> bool {
|
||||
if let Some(fa) = target.cast::<ast::FieldAccess>() {
|
||||
return is_valid_math_field_access(fa.target().to_untyped());
|
||||
}
|
||||
if matches!(target.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ impl CompletionPair<'_, '_, '_> {
|
|||
docs: Default::default(),
|
||||
};
|
||||
|
||||
let mode = interpret_mode_at(Some(&self.cursor.leaf));
|
||||
let mode = self.cursor.leaf_mode();
|
||||
|
||||
previous_decls(self.cursor.leaf.clone(), |node| -> Option<()> {
|
||||
match node {
|
||||
|
@ -111,7 +111,7 @@ impl CompletionPair<'_, '_, '_> {
|
|||
let default_docs = defines.docs;
|
||||
let defines = defines.defines;
|
||||
|
||||
let mode = interpret_mode_at(Some(&self.cursor.leaf));
|
||||
let mode = self.cursor.leaf_mode();
|
||||
let surrounding_syntax = self.cursor.surrounding_syntax;
|
||||
|
||||
let mut kind_checker = CompletionKindChecker {
|
||||
|
|
|
@ -76,7 +76,7 @@ impl CompletionPair<'_, '_, '_> {
|
|||
return None;
|
||||
}
|
||||
|
||||
let cursor_mode = interpret_mode_at(Some(node));
|
||||
let cursor_mode = self.cursor.leaf_mode();
|
||||
let is_content = ty.is_content(&());
|
||||
crate::log_debug_ct!("post snippet is_content: {is_content}");
|
||||
|
||||
|
|
|
@ -205,7 +205,7 @@ impl CompletionPair<'_, '_, '_> {
|
|||
|
||||
let mut apply = None;
|
||||
if parens && matches!(value, Value::Func(_)) {
|
||||
let mode = interpret_mode_at(Some(&self.cursor.leaf));
|
||||
let mode = self.cursor.leaf_mode();
|
||||
let kind_checker = CompletionKindChecker {
|
||||
symbols: HashSet::default(),
|
||||
functions: HashSet::from_iter([Ty::Value(InsTy::new(value.clone()))]),
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
/// contains: at, text
|
||||
|
||||
#let aa = text[Test];
|
||||
|
||||
$aa.fields()./* range 0..1 */$
|
|
@ -0,0 +1,5 @@
|
|||
/// contains: abs, func
|
||||
|
||||
#let aa = text[Test];
|
||||
|
||||
$aa./* range 0..1 */$
|
|
@ -0,0 +1,3 @@
|
|||
/// contains: fill
|
||||
|
||||
$#context text.f/* range 0..1 */$
|
|
@ -0,0 +1,3 @@
|
|||
/// contains: fill
|
||||
|
||||
#context $text.fi/* range 0..1 */$
|
|
@ -0,0 +1,3 @@
|
|||
/// contains: fill
|
||||
|
||||
#context $std.text.fi/* range 0..1 */$
|
|
@ -0,0 +1,5 @@
|
|||
/// contains: test
|
||||
|
||||
#let aa = (test: 0);
|
||||
|
||||
$aa.te/* range 0..1 */$
|
|
@ -0,0 +1,3 @@
|
|||
/// contains: where
|
||||
|
||||
$text./* range 0..1 */$
|
|
@ -0,0 +1,3 @@
|
|||
/// contains: where
|
||||
|
||||
$std.text./* range 0..1 */$
|
|
@ -0,0 +1,3 @@
|
|||
/// contains: align, text
|
||||
|
||||
$std./* range 0..1 */$
|
|
@ -0,0 +1,5 @@
|
|||
/// contains: at
|
||||
|
||||
#{
|
||||
"a"./* range 0..1 */
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
/// contains: at
|
||||
|
||||
"a"./* range 0..1 */
|
|
@ -0,0 +1,3 @@
|
|||
/// contains: at
|
||||
|
||||
$"a"./* range 0..1 */$
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (60..61)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_call_math.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": []
|
||||
}
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (52..53)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_content_math.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": [
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "abs",
|
||||
"labelDetails": {
|
||||
"description": "(content, size: relative) => content"
|
||||
},
|
||||
"textEdit": {
|
||||
"newText": "",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 4,
|
||||
"line": 4
|
||||
},
|
||||
"start": {
|
||||
"character": 4,
|
||||
"line": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (36..37)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_contextual_math.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": [
|
||||
{
|
||||
"kind": 6,
|
||||
"label": "fill",
|
||||
"textEdit": {
|
||||
"newText": "fill",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 16,
|
||||
"line": 2
|
||||
},
|
||||
"start": {
|
||||
"character": 15,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
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/dot_contextual_math2.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": [
|
||||
{
|
||||
"kind": 6,
|
||||
"label": "fill",
|
||||
"textEdit": {
|
||||
"newText": "fill",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 17,
|
||||
"line": 2
|
||||
},
|
||||
"start": {
|
||||
"character": 15,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (41..42)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_contexual_math3.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": [
|
||||
{
|
||||
"kind": 6,
|
||||
"label": "fill",
|
||||
"textEdit": {
|
||||
"newText": "fill",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 21,
|
||||
"line": 2
|
||||
},
|
||||
"start": {
|
||||
"character": 19,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (48..49)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_dict_math.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": [
|
||||
{
|
||||
"kind": 6,
|
||||
"label": "test",
|
||||
"textEdit": {
|
||||
"newText": "test",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 6,
|
||||
"line": 4
|
||||
},
|
||||
"start": {
|
||||
"character": 4,
|
||||
"line": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (27..28)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_element_math.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": []
|
||||
}
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (31..32)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_element_math2.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": []
|
||||
}
|
||||
]
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (32..33)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_module_math.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": [
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "align",
|
||||
"textEdit": {
|
||||
"newText": "align",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 5,
|
||||
"line": 2
|
||||
},
|
||||
"start": {
|
||||
"character": 5,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "text",
|
||||
"textEdit": {
|
||||
"newText": "text",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 5,
|
||||
"line": 2
|
||||
},
|
||||
"start": {
|
||||
"character": 5,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (27..28)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_str_code.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": [
|
||||
{
|
||||
"kind": 3,
|
||||
"label": "at",
|
||||
"textEdit": {
|
||||
"newText": "at(${1:})",
|
||||
"range": {
|
||||
"end": {
|
||||
"character": 6,
|
||||
"line": 3
|
||||
},
|
||||
"start": {
|
||||
"character": 6,
|
||||
"line": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (22..23)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/dot_str_markup.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": []
|
||||
}
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
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/dot_str_math.typ
|
||||
---
|
||||
[
|
||||
{
|
||||
"isIncomplete": false,
|
||||
"items": []
|
||||
}
|
||||
]
|
Loading…
Add table
Add a link
Reference in a new issue