diff --git a/crates/tinymist-query/src/analysis/completion.rs b/crates/tinymist-query/src/analysis/completion.rs index fca4664c..4bde1941 100644 --- a/crates/tinymist-query/src/analysis/completion.rs +++ b/crates/tinymist-query/src/analysis/completion.rs @@ -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> { 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)) => { diff --git a/crates/tinymist-query/src/analysis/completion/field_access.rs b/crates/tinymist-query/src/analysis/completion/field_access.rs index d563e4af..18ca3826 100644 --- a/crates/tinymist-query/src/analysis/completion/field_access.rs +++ b/crates/tinymist-query/src/analysis/completion/field_access.rs @@ -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::() { + return is_valid_math_field_access(fa.target().to_untyped()); + } + if matches!(target.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent) { + return true; + } + + false +} diff --git a/crates/tinymist-query/src/analysis/completion/scope.rs b/crates/tinymist-query/src/analysis/completion/scope.rs index e9763c20..2de8b0f1 100644 --- a/crates/tinymist-query/src/analysis/completion/scope.rs +++ b/crates/tinymist-query/src/analysis/completion/scope.rs @@ -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 { diff --git a/crates/tinymist-query/src/analysis/completion/snippet.rs b/crates/tinymist-query/src/analysis/completion/snippet.rs index 6f12627c..c762834b 100644 --- a/crates/tinymist-query/src/analysis/completion/snippet.rs +++ b/crates/tinymist-query/src/analysis/completion/snippet.rs @@ -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}"); diff --git a/crates/tinymist-query/src/analysis/completion/typst_specific.rs b/crates/tinymist-query/src/analysis/completion/typst_specific.rs index 16a4166b..24f8c6e3 100644 --- a/crates/tinymist-query/src/analysis/completion/typst_specific.rs +++ b/crates/tinymist-query/src/analysis/completion/typst_specific.rs @@ -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()))]), diff --git a/crates/tinymist-query/src/fixtures/completion/dot_call_math.typ b/crates/tinymist-query/src/fixtures/completion/dot_call_math.typ new file mode 100644 index 00000000..03c09925 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_call_math.typ @@ -0,0 +1,5 @@ +/// contains: at, text + +#let aa = text[Test]; + +$aa.fields()./* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_content_math.typ b/crates/tinymist-query/src/fixtures/completion/dot_content_math.typ new file mode 100644 index 00000000..525e5a56 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_content_math.typ @@ -0,0 +1,5 @@ +/// contains: abs, func + +#let aa = text[Test]; + +$aa./* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_contextual_math.typ b/crates/tinymist-query/src/fixtures/completion/dot_contextual_math.typ new file mode 100644 index 00000000..493401b3 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_contextual_math.typ @@ -0,0 +1,3 @@ +/// contains: fill + +$#context text.f/* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_contextual_math2.typ b/crates/tinymist-query/src/fixtures/completion/dot_contextual_math2.typ new file mode 100644 index 00000000..0da48529 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_contextual_math2.typ @@ -0,0 +1,3 @@ +/// contains: fill + +#context $text.fi/* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_contexual_math3.typ b/crates/tinymist-query/src/fixtures/completion/dot_contexual_math3.typ new file mode 100644 index 00000000..751ef0ce --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_contexual_math3.typ @@ -0,0 +1,3 @@ +/// contains: fill + +#context $std.text.fi/* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_dict_math.typ b/crates/tinymist-query/src/fixtures/completion/dot_dict_math.typ new file mode 100644 index 00000000..f6a4154f --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_dict_math.typ @@ -0,0 +1,5 @@ +/// contains: test + +#let aa = (test: 0); + +$aa.te/* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_element_math.typ b/crates/tinymist-query/src/fixtures/completion/dot_element_math.typ new file mode 100644 index 00000000..b0deff5f --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_element_math.typ @@ -0,0 +1,3 @@ +/// contains: where + +$text./* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_element_math2.typ b/crates/tinymist-query/src/fixtures/completion/dot_element_math2.typ new file mode 100644 index 00000000..fd2d73d6 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_element_math2.typ @@ -0,0 +1,3 @@ +/// contains: where + +$std.text./* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_module_math.typ b/crates/tinymist-query/src/fixtures/completion/dot_module_math.typ new file mode 100644 index 00000000..4d938363 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_module_math.typ @@ -0,0 +1,3 @@ +/// contains: align, text + +$std./* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_str_code.typ b/crates/tinymist-query/src/fixtures/completion/dot_str_code.typ new file mode 100644 index 00000000..062e6d9b --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_str_code.typ @@ -0,0 +1,5 @@ +/// contains: at + +#{ + "a"./* range 0..1 */ +} diff --git a/crates/tinymist-query/src/fixtures/completion/dot_str_markup.typ b/crates/tinymist-query/src/fixtures/completion/dot_str_markup.typ new file mode 100644 index 00000000..b64910ad --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_str_markup.typ @@ -0,0 +1,3 @@ +/// contains: at + +"a"./* range 0..1 */ diff --git a/crates/tinymist-query/src/fixtures/completion/dot_str_math.typ b/crates/tinymist-query/src/fixtures/completion/dot_str_math.typ new file mode 100644 index 00000000..96cc72ee --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/dot_str_math.typ @@ -0,0 +1,3 @@ +/// contains: at + +$"a"./* range 0..1 */$ diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_call_math.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_call_math.typ.snap new file mode 100644 index 00000000..e3a877b2 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_call_math.typ.snap @@ -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": [] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_content_math.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_content_math.typ.snap new file mode 100644 index 00000000..2b53e12a --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_content_math.typ.snap @@ -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 + } + } + } + } + ] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_contextual_math.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_contextual_math.typ.snap new file mode 100644 index 00000000..2732c648 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_contextual_math.typ.snap @@ -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 + } + } + } + } + ] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_contextual_math2.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_contextual_math2.typ.snap new file mode 100644 index 00000000..7d700de3 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_contextual_math2.typ.snap @@ -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 + } + } + } + } + ] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_contexual_math3.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_contexual_math3.typ.snap new file mode 100644 index 00000000..c5ff8c2b --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_contexual_math3.typ.snap @@ -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 + } + } + } + } + ] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_dict_math.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_dict_math.typ.snap new file mode 100644 index 00000000..a3482f35 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_dict_math.typ.snap @@ -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 + } + } + } + } + ] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_element_math.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_element_math.typ.snap new file mode 100644 index 00000000..2eb146c5 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_element_math.typ.snap @@ -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": [] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_element_math2.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_element_math2.typ.snap new file mode 100644 index 00000000..af29f072 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_element_math2.typ.snap @@ -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": [] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_module_math.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_module_math.typ.snap new file mode 100644 index 00000000..102855be --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_module_math.typ.snap @@ -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 + } + } + } + } + ] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_str_code.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_str_code.typ.snap new file mode 100644 index 00000000..60626c12 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_str_code.typ.snap @@ -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 + } + } + } + } + ] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_str_markup.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_str_markup.typ.snap new file mode 100644 index 00000000..834a70e8 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_str_markup.typ.snap @@ -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": [] + } +] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_str_math.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_str_math.typ.snap new file mode 100644 index 00000000..e2763ab0 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@dot_str_math.typ.snap @@ -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": [] + } +]