feat: forbid bad field access syntax in math mode (#1550)

This commit is contained in:
Myriad-Dreamin 2025-03-20 19:44:28 +08:00 committed by GitHub
parent f555fc3840
commit e33688336d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 452 additions and 65 deletions

View file

@ -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)) => {

View file

@ -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
}

View file

@ -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 {

View file

@ -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}");

View file

@ -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()))]),

View file

@ -0,0 +1,5 @@
/// contains: at, text
#let aa = text[Test];
$aa.fields()./* range 0..1 */$

View file

@ -0,0 +1,5 @@
/// contains: abs, func
#let aa = text[Test];
$aa./* range 0..1 */$

View file

@ -0,0 +1,3 @@
/// contains: fill
$#context text.f/* range 0..1 */$

View file

@ -0,0 +1,3 @@
/// contains: fill
#context $text.fi/* range 0..1 */$

View file

@ -0,0 +1,3 @@
/// contains: fill
#context $std.text.fi/* range 0..1 */$

View file

@ -0,0 +1,5 @@
/// contains: test
#let aa = (test: 0);
$aa.te/* range 0..1 */$

View file

@ -0,0 +1,3 @@
/// contains: where
$text./* range 0..1 */$

View file

@ -0,0 +1,3 @@
/// contains: where
$std.text./* range 0..1 */$

View file

@ -0,0 +1,3 @@
/// contains: align, text
$std./* range 0..1 */$

View file

@ -0,0 +1,5 @@
/// contains: at
#{
"a"./* range 0..1 */
}

View file

@ -0,0 +1,3 @@
/// contains: at
"a"./* range 0..1 */

View file

@ -0,0 +1,3 @@
/// contains: at
$"a"./* range 0..1 */$

View file

@ -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": []
}
]

View file

@ -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
}
}
}
}
]
}
]

View file

@ -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
}
}
}
}
]
}
]

View file

@ -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
}
}
}
}
]
}
]

View file

@ -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
}
}
}
}
]
}
]

View file

@ -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
}
}
}
}
]
}
]

View file

@ -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": []
}
]

View file

@ -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": []
}
]

View file

@ -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
}
}
}
}
]
}
]

View file

@ -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
}
}
}
}
]
}
]

View file

@ -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": []
}
]

View file

@ -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": []
}
]