From 1b1348409d62587cc9bfcfba1b9ea0217479338d Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 21 Jul 2025 11:03:03 +0200 Subject: [PATCH 1/3] Simplify and clean up query predicate operators --- .../languages/TreeSitterQuery/QueryCapture.ts | 28 +-- .../getChildNodesForFieldName.ts | 6 +- .../src/languages/TreeSitterQuery/getNode.ts | 11 ++ .../src/languages/TreeSitterQuery/isEven.ts | 4 +- .../queryPredicateOperators.ts | 172 +++++++++--------- .../src/languages/TreeSitterQuery/setRange.ts | 8 + queries/c.scm | 4 +- queries/csharp.scm | 4 +- queries/css.scm | 2 +- queries/java.scm | 4 +- queries/javascript.core.scm | 4 +- queries/python.scm | 4 +- queries/r.scm | 6 +- queries/ruby.scm | 6 +- queries/rust.scm | 1 - queries/scala.scm | 6 +- queries/talon.scm | 2 +- 17 files changed, 130 insertions(+), 142 deletions(-) create mode 100644 packages/cursorless-engine/src/languages/TreeSitterQuery/getNode.ts create mode 100644 packages/cursorless-engine/src/languages/TreeSitterQuery/setRange.ts diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts index 0c4eba64b9..0de5d611a4 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/QueryCapture.ts @@ -1,28 +1,5 @@ import type { Range, TextDocument } from "@cursorless/common"; -import type { Point, TreeCursor } from "web-tree-sitter"; - -/** - * Simple representation of the tree sitter syntax node. Used by - * {@link MutableQueryCapture} to avoid using range/text and other mutable - * parameters directly from the node. - */ -export interface SimpleSyntaxNode { - readonly id: number; - readonly type: string; - readonly isNamed: boolean; - readonly parent: SimpleSyntaxNode | null; - readonly children: Array; - walk(): TreeCursor; -} - -/** - * Add start and end position to the simple syntax node. Used by the `child-range!` predicate. - */ -interface SimpleChildSyntaxNode extends SimpleSyntaxNode { - readonly startPosition: Point; - readonly endPosition: Point; - readonly text: string; -} +import type { Node } from "web-tree-sitter"; /** * A capture of a query pattern against a syntax tree. Often corresponds to a @@ -69,8 +46,9 @@ export interface QueryMatch { export interface MutableQueryCapture extends QueryCapture { /** * The tree-sitter node that was captured. + * This may be undefined if the range has been modified by a query predicate. */ - readonly node: SimpleSyntaxNode; + node: Node | undefined; readonly document: TextDocument; range: Range; diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/getChildNodesForFieldName.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/getChildNodesForFieldName.ts index e887e254a0..681f7e163c 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/getChildNodesForFieldName.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/getChildNodesForFieldName.ts @@ -1,9 +1,9 @@ -import type { SimpleSyntaxNode } from "./QueryCapture"; +import type { Node } from "web-tree-sitter"; export function getChildNodesForFieldName( - node: SimpleSyntaxNode, + node: Node, fieldName: string, -): SimpleSyntaxNode[] { +): Node[] { const nodes = []; const treeCursor = node.walk(); let hasNext = treeCursor.gotoFirstChild(); diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/getNode.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/getNode.ts new file mode 100644 index 0000000000..0c7d6c22af --- /dev/null +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/getNode.ts @@ -0,0 +1,11 @@ +import type { Node } from "web-tree-sitter"; +import type { MutableQueryCapture } from "./QueryCapture"; + +export function getNode(capture: MutableQueryCapture): Node { + if (capture.node == null) { + throw Error( + `Capture ${capture.name} has no node. The range of the capture has already been updated and no longer matches a specific node.`, + ); + } + return capture.node; +} diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/isEven.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/isEven.ts index 12e9dacea7..1dff9d5ca8 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/isEven.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/isEven.ts @@ -1,4 +1,4 @@ -import type { SimpleSyntaxNode } from "./QueryCapture"; +import type { Node } from "web-tree-sitter"; /** * Checks if a node is at an even index within its parent's field. @@ -7,7 +7,7 @@ import type { SimpleSyntaxNode } from "./QueryCapture"; * @param fieldName - The name of the field in the parent node. * @returns True if the node is at an even index, false otherwise. */ -export function isEven(node: SimpleSyntaxNode, fieldName: string): boolean { +export function isEven(node: Node, fieldName: string): boolean { if (node.parent == null) { throw Error("Node has no parent"); } diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index dc8594d6ef..4087139bd6 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -1,11 +1,13 @@ import { Position, Range, adjustPosition } from "@cursorless/common"; import type { Point } from "web-tree-sitter"; import { z } from "zod"; +import { getNode } from "./getNode"; import { isEven } from "./isEven"; import { makeRangeFromPositions } from "./makeRangeFromPositions"; import { q } from "./operatorArgumentSchemaTypes"; import type { MutableQueryCapture } from "./QueryCapture"; import { QueryPredicateOperator } from "./QueryPredicateOperator"; +import { setRange } from "./setRange"; /** * A predicate operator that returns true if the node is at an even index within @@ -15,8 +17,8 @@ import { QueryPredicateOperator } from "./QueryPredicateOperator"; class Even extends QueryPredicateOperator { name = "even?" as const; schema = z.tuple([q.node, q.string]); - run({ node }: MutableQueryCapture, fieldName: string) { - return isEven(node, fieldName); + run(capture: MutableQueryCapture, fieldName: string) { + return isEven(getNode(capture), fieldName); } } @@ -28,8 +30,8 @@ class Even extends QueryPredicateOperator { class Odd extends QueryPredicateOperator { name = "odd?" as const; schema = z.tuple([q.node, q.string]); - run({ node }: MutableQueryCapture, fieldName: string) { - return !isEven(node, fieldName); + run(capture: MutableQueryCapture, fieldName: string) { + return !isEven(getNode(capture), fieldName); } } @@ -42,8 +44,8 @@ class Odd extends QueryPredicateOperator { class Text extends QueryPredicateOperator { name = "text?" as const; schema = z.tuple([q.node, q.string]).rest(q.string); - run({ document, range }: MutableQueryCapture, ...texts: string[]) { - return texts.includes(document.getText(range)); + run(capture: MutableQueryCapture, ...texts: string[]) { + return texts.includes(getNode(capture).text); } } @@ -56,8 +58,8 @@ class Text extends QueryPredicateOperator { class Type extends QueryPredicateOperator { name = "type?" as const; schema = z.tuple([q.node, q.string]).rest(q.string); - run({ node }: MutableQueryCapture, ...types: string[]) { - return types.includes(node.type); + run(capture: MutableQueryCapture, ...types: string[]) { + return types.includes(getNode(capture).type); } } @@ -70,8 +72,8 @@ class Type extends QueryPredicateOperator { class NotType extends QueryPredicateOperator { name = "not-type?" as const; schema = z.tuple([q.node, q.string]).rest(q.string); - run({ node }: MutableQueryCapture, ...types: string[]) { - return !types.includes(node.type); + run(capture: MutableQueryCapture, ...types: string[]) { + return !types.includes(getNode(capture).type); } } @@ -84,24 +86,12 @@ class NotType extends QueryPredicateOperator { class NotParentType extends QueryPredicateOperator { name = "not-parent-type?" as const; schema = z.tuple([q.node, q.string]).rest(q.string); - run({ node }: MutableQueryCapture, ...types: string[]) { + run(capture: MutableQueryCapture, ...types: string[]) { + const node = getNode(capture); return node.parent == null || !types.includes(node.parent.type); } } -/** - * A predicate operator that returns true if the node is the nth child of its - * parent. For example, `(#is-nth-child? @foo 0)` will reject the match if the - * `@foo` capture is not the first child of its parent. - */ -class IsNthChild extends QueryPredicateOperator { - name = "is-nth-child?" as const; - schema = z.tuple([q.node, q.integer]); - run({ node }: MutableQueryCapture, n: number) { - return node.parent?.children.findIndex((n) => n.id === node.id) === n; - } -} - /** * A predicate operator that returns true if the node has more than 1 child of * type {@link type} (inclusive). For example, `(#has-multiple-children-of-type? @@ -112,8 +102,10 @@ class HasMultipleChildrenOfType extends QueryPredicateOperator n.type === type).length; + run(capture: MutableQueryCapture, type: string) { + const count = getNode(capture).children.filter( + (n) => n.type === type, + ).length; return count > 1; } } @@ -128,15 +120,13 @@ class ChildRange extends QueryPredicateOperator { ]); run( - nodeInfo: MutableQueryCapture, + capture: MutableQueryCapture, startIndex: number, endIndex?: number, excludeStart?: boolean, excludeEnd?: boolean, ) { - const { - node: { children }, - } = nodeInfo; + const children = getNode(capture).children; startIndex = startIndex < 0 ? children.length + startIndex : startIndex; endIndex = endIndex == null ? -1 : endIndex; @@ -145,9 +135,12 @@ class ChildRange extends QueryPredicateOperator { const start = children[startIndex]; const end = children[endIndex]; - nodeInfo.range = makeRangeFromPositions( - excludeStart ? start.endPosition : start.startPosition, - excludeEnd ? end.startPosition : end.endPosition, + setRange( + capture, + makeRangeFromPositions( + excludeStart ? start.endPosition : start.startPosition, + excludeEnd ? end.startPosition : end.endPosition, + ), ); return true; @@ -161,10 +154,13 @@ class CharacterRange extends QueryPredicateOperator { z.tuple([q.node, q.integer, q.integer]), ]); - run(nodeInfo: MutableQueryCapture, startOffset: number, endOffset?: number) { - nodeInfo.range = new Range( - nodeInfo.range.start.translate(undefined, startOffset), - nodeInfo.range.end.translate(undefined, endOffset ?? 0), + run(capture: MutableQueryCapture, startOffset: number, endOffset?: number) { + setRange( + capture, + new Range( + capture.range.start.translate(undefined, startOffset), + capture.range.end.translate(undefined, endOffset ?? 0), + ), ); return true; @@ -189,9 +185,9 @@ class ShrinkToMatch extends QueryPredicateOperator { name = "shrink-to-match!" as const; schema = z.tuple([q.node, q.string]); - run(nodeInfo: MutableQueryCapture, pattern: string) { - const { document, range } = nodeInfo; - const text = document.getText(range); + run(capture: MutableQueryCapture, pattern: string) { + const { document, range } = capture; + const text = getNode(capture).text; const match = text.match(new RegExp(pattern, "ds")); if (match?.index == null) { @@ -203,9 +199,12 @@ class ShrinkToMatch extends QueryPredicateOperator { const baseOffset = document.offsetAt(range.start); - nodeInfo.range = new Range( - document.positionAt(baseOffset + startOffset), - document.positionAt(baseOffset + endOffset), + setRange( + capture, + new Range( + document.positionAt(baseOffset + startOffset), + document.positionAt(baseOffset + endOffset), + ), ); return true; @@ -225,8 +224,8 @@ class GrowToNamedSiblings extends QueryPredicateOperator { name = "grow-to-named-siblings!" as const; schema = z.union([z.tuple([q.node]), z.tuple([q.node, q.string])]); - run(nodeInfo: MutableQueryCapture, notText?: string) { - const { node, range } = nodeInfo; + run(capture: MutableQueryCapture, notText?: string) { + const node = getNode(capture); if (node.parent == null) { throw Error("Node has no parent"); @@ -254,9 +253,12 @@ class GrowToNamedSiblings extends QueryPredicateOperator { } if (endPosition != null) { - nodeInfo.range = new Range( - range.start, - new Position(endPosition.row, endPosition.column), + setRange( + capture, + new Range( + capture.range.start, + new Position(endPosition.row, endPosition.column), + ), ); } @@ -272,16 +274,21 @@ class TrimEnd extends QueryPredicateOperator { name = "trim-end!" as const; schema = z.tuple([q.node]); - run(nodeInfo: MutableQueryCapture) { - const { document, range } = nodeInfo; - const text = document.getText(range); + run(capture: MutableQueryCapture) { + const { document, range } = capture; + const text = getNode(capture).text; const whitespaceLength = text.length - text.trimEnd().length; + if (whitespaceLength > 0) { - nodeInfo.range = new Range( - range.start, - adjustPosition(document, range.end, -whitespaceLength), + setRange( + capture, + new Range( + range.start, + adjustPosition(document, range.end, -whitespaceLength), + ), ); } + return true; } } @@ -293,9 +300,9 @@ class DocumentRange extends QueryPredicateOperator { name = "document-range!" as const; schema = z.tuple([q.node]).rest(q.node); - run(...nodeInfos: MutableQueryCapture[]) { - for (const nodeInfo of nodeInfos) { - nodeInfo.range = nodeInfo.document.range; + run(...captures: MutableQueryCapture[]) { + for (const capture of captures) { + setRange(capture, capture.document.range); } return true; @@ -321,28 +328,15 @@ class AllowMultiple extends QueryPredicateOperator { return true; } - run(...nodeInfos: MutableQueryCapture[]) { - for (const nodeInfo of nodeInfos) { - nodeInfo.allowMultiple = true; + run(...captures: MutableQueryCapture[]) { + for (const capture of captures) { + capture.allowMultiple = true; } return true; } } -/** - * A predicate operator that logs a node, for debugging. - */ -class Log extends QueryPredicateOperator { - name = "log!" as const; - schema = z.tuple([q.node]); - - run(nodeInfo: MutableQueryCapture) { - console.log(`#log!: ${nodeInfo.name}@${nodeInfo.range}`); - return true; - } -} - /** * A predicate operator that sets the insertion delimiter of the match. For * example, `(#insertion-delimiter! @foo ", ")` will set the insertion delimiter @@ -352,18 +346,18 @@ class InsertionDelimiter extends QueryPredicateOperator { name = "insertion-delimiter!" as const; schema = z.tuple([q.node, q.string]); - run(nodeInfo: MutableQueryCapture, insertionDelimiter: string) { - nodeInfo.insertionDelimiter = insertionDelimiter; + run(capture: MutableQueryCapture, insertionDelimiter: string) { + capture.insertionDelimiter = insertionDelimiter; return true; } } /** - * A predicate operator that sets the insertion delimiter of {@link nodeInfo} to + * A predicate operator that sets the insertion delimiter of {@link capture} to * either {@link insertionDelimiterConsequence} or * {@link insertionDelimiterAlternative} depending on whether - * {@link conditionNodeInfo} is single or multiline, respectively. For example, + * {@link conditionCapture} is single or multiline, respectively. For example, * * ```scm * (#single-or-multi-line-delimiter! @foo @bar ", " ",\n") @@ -377,12 +371,12 @@ class SingleOrMultilineDelimiter extends QueryPredicateOperator child.isNamed, ); - nodeInfo.insertionDelimiter = isEmpty + capture.insertionDelimiter = isEmpty ? insertionDelimiterEmpty - : conditionNodeInfo.range.isSingleLine + : conditionCapture.range.isSingleLine ? insertionDelimiterSingleLine : insertionDelimiterMultiline; @@ -427,7 +421,6 @@ class EmptySingleMultiDelimiter extends QueryPredicateOperator.*)\"#+") - ) [ diff --git a/queries/scala.scm b/queries/scala.scm index f62fac220f..73a706f546 100644 --- a/queries/scala.scm +++ b/queries/scala.scm @@ -157,8 +157,8 @@ "(" @argumentList.removal.start.endOf @argumentOrParameter.iteration.start.endOf ")" @argumentList.removal.end.startOf @argumentOrParameter.iteration.end.startOf ) @argumentList - (#child-range! @argumentList 1 -2) (#empty-single-multi-delimiter! @argumentList @argumentList "" ", " ",\n") + (#child-range! @argumentList 1 -2) ) @argumentList.domain @argumentOrParameter.iteration.domain ;;!! def foo(aaa: Int, bbb: Int) = x @@ -168,8 +168,8 @@ "(" @argumentList.removal.start.endOf @argumentOrParameter.iteration.start.endOf ")" @argumentList.removal.end.startOf @argumentOrParameter.iteration.end.startOf ) @argumentList - (#child-range! @argumentList 1 -2) (#empty-single-multi-delimiter! @argumentList @argumentList "" ", " ",\n") + (#child-range! @argumentList 1 -2) ) @argumentList.domain @argumentOrParameter.iteration.domain ;;!! foo(aaa, bbb) @@ -179,8 +179,8 @@ "(" @argumentList.removal.start.endOf @argumentOrParameter.iteration.start.endOf ")" @argumentList.removal.end.startOf @argumentOrParameter.iteration.end.startOf ) @argumentList - (#child-range! @argumentList 1 -2) (#empty-single-multi-delimiter! @argumentList @argumentList "" ", " ",\n") + (#child-range! @argumentList 1 -2) ) @argumentList.domain @argumentOrParameter.iteration.domain operator: (operator_identifier) @disqualifyDelimiter diff --git a/queries/talon.scm b/queries/talon.scm index ec82915292..74b212f428 100644 --- a/queries/talon.scm +++ b/queries/talon.scm @@ -183,8 +183,8 @@ "(" @argumentList.removal.start.endOf @argumentOrParameter.iteration.start.endOf ")" @argumentList.removal.end.startOf @argumentOrParameter.iteration.end.startOf ) @argumentList - (#child-range! @argumentList 1 -2) (#empty-single-multi-delimiter! @argumentList @argumentList "" ", " ",\n") + (#child-range! @argumentList 1 -2) ) @argumentList.domain @argumentOrParameter.iteration.domain ;;!! # foo From 29a62e43542d93683ea0cb3610d8d84c9c4f6a65 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 21 Jul 2025 11:06:45 +0200 Subject: [PATCH 2/3] typo --- .../cursorless-engine/src/languages/TreeSitterQuery/setRange.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/setRange.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/setRange.ts index 11eb8124b7..d72d9bd5d9 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/setRange.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/setRange.ts @@ -3,6 +3,6 @@ import type { MutableQueryCapture } from "./QueryCapture"; export function setRange(capture: MutableQueryCapture, range: Range) { capture.range = range; - // Clear the node since the range has changed ander ranged no longer matches a specific node + // Clear the node since the range has changed and the range no longer matches a specific node capture.node = undefined; } From ea9219bd9c759c94e97da985871a4a56622f897a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 21 Jul 2025 11:36:33 +0200 Subject: [PATCH 3/3] Restore is nth child predicate operator --- .../TreeSitterQuery/queryPredicateOperators.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts index 4087139bd6..a1678b4378 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts @@ -92,6 +92,20 @@ class NotParentType extends QueryPredicateOperator { } } +/** + * A predicate operator that returns true if the node is the nth child of its + * parent. For example, `(#is-nth-child? @foo 0)` will reject the match if the + * `@foo` capture is not the first child of its parent. + */ +class IsNthChild extends QueryPredicateOperator { + name = "is-nth-child?" as const; + schema = z.tuple([q.node, q.integer]); + run(capture: MutableQueryCapture, n: number) { + const node = getNode(capture); + return node.parent?.children.findIndex((n) => n.id === node.id) === n; + } +} + /** * A predicate operator that returns true if the node has more than 1 child of * type {@link type} (inclusive). For example, `(#has-multiple-children-of-type? @@ -429,6 +443,7 @@ export const queryPredicateOperators = [ new TrimEnd(), new DocumentRange(), new NotParentType(), + new IsNthChild(), new ChildRange(), new CharacterRange(), new ShrinkToMatch(),