Skip to content

Commit fc584ff

Browse files
committed
Add mutable query capture interface
1 parent 2fd72d5 commit fc584ff

File tree

11 files changed

+120
-39
lines changed

11 files changed

+120
-39
lines changed

packages/cursorless-engine/src/languages/TreeSitterQuery/PredicateOperatorSchemaTypes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { SyntaxNode } from "web-tree-sitter";
21
import z from "zod";
32
import {
43
SchemaInputType,
54
SchemaOutputType,
65
} from "./operatorArgumentSchemaTypes";
6+
import { MutableQueryCapture } from "./QueryCapture";
77

88
/**
99
* A schema used to validate a list of operands for a given predicate operator.
@@ -38,7 +38,7 @@ export type InferSchemaType<T extends HasSchema> = T["schema"];
3838
type PredicateParameterType<T extends SchemaOutputType> = T extends {
3939
type: "capture";
4040
}
41-
? SyntaxNode
41+
? MutableQueryCapture
4242
: T extends { value: infer V }
4343
? V
4444
: never;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Range } from "@cursorless/common";
2+
import { SyntaxNode } from "web-tree-sitter";
3+
4+
/**
5+
* A capture of a query pattern against a syntax tree. Often corresponds to a
6+
* node within the syntax tree, but can also be a range within a node or be modified
7+
* by query predicate operators.
8+
*/
9+
export interface QueryCapture {
10+
/**
11+
* The name of the capture. Eg for a capture labeled `@foo`, the name is
12+
* `foo`.
13+
*/
14+
readonly name: string;
15+
16+
/** The range of the capture. */
17+
readonly range: Range;
18+
19+
/** Whether it is ok for the same capture to appear multiple times with the
20+
* same domain. If set to `true`, then the scope handler should merge all
21+
* captures with the same name and domain into a single scope with multiple
22+
* content ranges. */
23+
readonly allowMultiple: boolean;
24+
}
25+
26+
/**
27+
* A match of a query pattern against a syntax tree.
28+
*/
29+
export interface QueryMatch {
30+
/**
31+
* The captures of the pattern that was matched.
32+
*/
33+
readonly captures: QueryCapture[];
34+
}
35+
36+
/**
37+
* A capture of a query pattern against a syntax tree. This type is used
38+
* internally by the query engine to allow operators to modify the capture.
39+
*/
40+
export interface MutableQueryCapture extends QueryCapture {
41+
/**
42+
* The tree-sitter node that was captured. Note that the range may have already
43+
* been altered by a prior operator, so please use {@link range} instead of
44+
* trying to retrieve the range from the node.
45+
*/
46+
readonly node: SyntaxNode;
47+
48+
range: Range;
49+
50+
allowMultiple: boolean;
51+
}
52+
53+
/**
54+
* A match of a query pattern against a syntax tree that can be mutated. This
55+
* type is used internally by the query engine to allow operators to modify the
56+
* match.
57+
*/
58+
export interface MutableQueryMatch extends QueryMatch {
59+
/**
60+
* The index of the pattern that was matched.
61+
*/
62+
readonly patternIdx: number;
63+
64+
readonly captures: MutableQueryCapture[];
65+
}

packages/cursorless-engine/src/languages/TreeSitterQuery/QueryPredicateOperator.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { PredicateOperand, QueryMatch } from "web-tree-sitter";
1+
import { PredicateOperand } from "web-tree-sitter";
22
import z from "zod";
33
import {
44
AcceptFunctionArgs,
55
HasSchema,
66
InferSchemaType,
77
} from "./PredicateOperatorSchemaTypes";
8+
import { MutableQueryMatch } from "./QueryCapture";
89
import { constructZodErrorMessages } from "./constructZodErrorMessages";
910

1011
/**
@@ -24,7 +25,7 @@ export abstract class QueryPredicateOperator<T extends HasSchema> {
2425
/**
2526
* The name of the operator, e.g. `not-type?`
2627
*/
27-
abstract readonly name: `${string}?`;
28+
abstract readonly name: `${string}${"?" | "!"}`;
2829

2930
/**
3031
* The schema used to validate the operands to the operator. This should be a
@@ -42,7 +43,7 @@ export abstract class QueryPredicateOperator<T extends HasSchema> {
4243
* in the schema. For example, if the schema is `z.tuple([q.node, q.string])`,
4344
* then `args` will be `SyntaxNode, string`.
4445
*/
45-
protected abstract accept(
46+
protected abstract run(
4647
...args: AcceptFunctionArgs<z.infer<InferSchemaType<T>>>
4748
): boolean;
4849

@@ -61,8 +62,8 @@ export abstract class QueryPredicateOperator<T extends HasSchema> {
6162
return result.success
6263
? {
6364
success: true,
64-
predicate: (match: QueryMatch) =>
65-
this.accept(...this.constructAcceptArgs(result.data, match)),
65+
predicate: (match: MutableQueryMatch) =>
66+
this.run(...this.constructAcceptArgs(result.data, match)),
6667
}
6768
: {
6869
success: false,
@@ -79,25 +80,25 @@ export abstract class QueryPredicateOperator<T extends HasSchema> {
7980
*/
8081
private constructAcceptArgs(
8182
rawOutput: z.output<InferSchemaType<T>>,
82-
match: QueryMatch,
83+
match: MutableQueryMatch,
8384
): AcceptFunctionArgs<z.infer<InferSchemaType<T>>> {
8485
return rawOutput.map((operand) => {
8586
if (operand.type === "capture") {
86-
const node = match.captures.find(
87+
const capture = match.captures.find(
8788
(capture) => capture.name === operand.name,
88-
)?.node;
89+
);
8990

90-
if (node == null) {
91+
if (capture == null) {
9192
// FIXME: We could allow some predicates to be forgiving,
92-
// because it's possible to have a capture on an optional node.
93+
// because it's possible to have a capture on an optional nodeInfo.
9394
// In that case we'd prob just return `true` if any capture was
9495
// `null`, but we should check that the given capture name
9596
// appears statically in the given pattern. But we don't yet
9697
// have a use case so let's leave it for now.
9798
throw new Error(`Could not find capture ${operand.name}`);
9899
}
99100

100-
return node;
101+
return capture;
101102
} else {
102103
return operand.value;
103104
}
@@ -107,7 +108,7 @@ export abstract class QueryPredicateOperator<T extends HasSchema> {
107108

108109
interface SuccessfulPredicateResult {
109110
success: true;
110-
predicate: (match: QueryMatch) => boolean;
111+
predicate: (match: MutableQueryMatch) => boolean;
111112
}
112113

113114
interface FailedPredicateResult {

packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Position, TextDocument, showError } from "@cursorless/common";
2-
import { Point, Query, QueryMatch } from "web-tree-sitter";
2+
import { Point, Query } from "web-tree-sitter";
33
import { ide } from "../../singletons/ide.singleton";
44
import { TreeSitter } from "../../typings/TreeSitter";
5+
import { getNodeRange } from "../../util/nodeSelectors";
56
import { parsePredicates } from "./parsePredicates";
67
import { predicateToString } from "./predicateToString";
8+
import { MutableQueryMatch, QueryMatch } from "./QueryCapture";
79

810
/**
911
* Wrapper around a tree-sitter query that provides a more convenient API, and
@@ -23,7 +25,7 @@ export class TreeSitterQuery {
2325
* array corresponds to a pattern, and each element of the inner array
2426
* corresponds to a predicate for that pattern.
2527
*/
26-
private patternPredicates: ((match: QueryMatch) => boolean)[][],
28+
private patternPredicates: ((match: MutableQueryMatch) => boolean)[][],
2729
) {}
2830

2931
static create(languageId: string, treeSitter: TreeSitter, query: Query) {
@@ -56,16 +58,31 @@ export class TreeSitterQuery {
5658
return new TreeSitterQuery(treeSitter, query, predicates);
5759
}
5860

59-
matches(document: TextDocument, start: Position, end: Position) {
61+
matches(
62+
document: TextDocument,
63+
start: Position,
64+
end: Position,
65+
): QueryMatch[] {
6066
return this.query
6167
.matches(
6268
this.treeSitter.getTree(document).rootNode,
6369
positionToPoint(start),
6470
positionToPoint(end),
6571
)
66-
.filter((rawMatch) =>
67-
this.patternPredicates[rawMatch.pattern].every((predicate) =>
68-
predicate(rawMatch),
72+
.map(
73+
({ pattern, captures }): MutableQueryMatch => ({
74+
patternIdx: pattern,
75+
captures: captures.map(({ name, node }) => ({
76+
name,
77+
node,
78+
range: getNodeRange(node),
79+
allowMultiple: false,
80+
})),
81+
}),
82+
)
83+
.filter((match) =>
84+
this.patternPredicates[match.patternIdx].every((predicate) =>
85+
predicate(match),
6986
),
7087
);
7188
}

packages/cursorless-engine/src/languages/TreeSitterQuery/parsePredicates.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { PredicateResult, QueryMatch } from "web-tree-sitter";
1+
import { PredicateResult } from "web-tree-sitter";
2+
import { MutableQueryMatch } from "./QueryCapture";
23
import { queryPredicateOperators } from "./queryPredicateOperators";
34

45
/**
@@ -15,11 +16,11 @@ import { queryPredicateOperators } from "./queryPredicateOperators";
1516
*/
1617
export function parsePredicates(predicateDescriptors: PredicateResult[][]) {
1718
const errors: PredicateError[] = [];
18-
const predicates: ((match: QueryMatch) => boolean)[][] = [];
19+
const predicates: ((match: MutableQueryMatch) => boolean)[][] = [];
1920

2021
predicateDescriptors.forEach((patternPredicateDescriptors, patternIdx) => {
2122
/** The predicates for a given pattern */
22-
const patternPredicates: ((match: QueryMatch) => boolean)[] = [];
23+
const patternPredicates: ((match: MutableQueryMatch) => boolean)[] = [];
2324

2425
patternPredicateDescriptors.forEach((predicateDescriptor, predicateIdx) => {
2526
const operator = queryPredicateOperators.find(

packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { SyntaxNode } from "web-tree-sitter";
21
import z from "zod";
2+
import { HasSchema } from "./PredicateOperatorSchemaTypes";
3+
import { MutableQueryCapture } from "./QueryCapture";
34
import { QueryPredicateOperator } from "./QueryPredicateOperator";
45
import { q } from "./operatorArgumentSchemaTypes";
5-
import { HasSchema } from "./PredicateOperatorSchemaTypes";
6+
import { Range } from "@cursorless/common";
67

78
/**
89
* A predicate operator that returns true if the node is not of the given type.
@@ -13,7 +14,7 @@ import { HasSchema } from "./PredicateOperatorSchemaTypes";
1314
class NotType extends QueryPredicateOperator<NotType> {
1415
name = "not-type?" as const;
1516
schema = z.tuple([q.node, q.string]).rest(q.string);
16-
accept(node: SyntaxNode, ...types: string[]) {
17+
run({ node }: MutableQueryCapture, ...types: string[]) {
1718
return !types.includes(node.type);
1819
}
1920
}
@@ -27,7 +28,7 @@ class NotType extends QueryPredicateOperator<NotType> {
2728
class NotParentType extends QueryPredicateOperator<NotParentType> {
2829
name = "not-parent-type?" as const;
2930
schema = z.tuple([q.node, q.string]).rest(q.string);
30-
accept(node: SyntaxNode, ...types: string[]) {
31+
run({ node }: MutableQueryCapture, ...types: string[]) {
3132
return node.parent == null || !types.includes(node.parent.type);
3233
}
3334
}
@@ -40,7 +41,7 @@ class NotParentType extends QueryPredicateOperator<NotParentType> {
4041
class IsNthChild extends QueryPredicateOperator<IsNthChild> {
4142
name = "is-nth-child?" as const;
4243
schema = z.tuple([q.node, q.integer]);
43-
accept(node: SyntaxNode, n: number) {
44+
run({ node }: MutableQueryCapture, n: number) {
4445
return node.parent?.children.indexOf(node) === n;
4546
}
4647
}

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
TextDocument,
55
TextEditor,
66
} from "@cursorless/common";
7-
import { QueryMatch } from "web-tree-sitter";
87
import { TreeSitterQuery } from "../../../../languages/TreeSitterQuery";
98
import BaseScopeHandler from "../BaseScopeHandler";
109
import { compareTargetScopes } from "../compareTargetScopes";
@@ -13,6 +12,7 @@ import {
1312
ContainmentPolicy,
1413
ScopeIteratorRequirements,
1514
} from "../scopeHandler.types";
15+
import { QueryMatch } from "../../../../languages/TreeSitterQuery/QueryCapture";
1616

1717
/** Base scope handler to use for both tree-sitter scopes and their iteration scopes */
1818
export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { ScopeType, SimpleScopeType, TextEditor } from "@cursorless/common";
2-
import { QueryMatch } from "web-tree-sitter";
32
import { TreeSitterQuery } from "../../../../languages/TreeSitterQuery";
43
import { PlainTarget } from "../../../targets";
54
import { TargetScope } from "../scope.types";
65
import { BaseTreeSitterScopeHandler } from "./BaseTreeSitterScopeHandler";
76
import { getCaptureRangeByName, getRelatedRange } from "./captureUtils";
7+
import { QueryMatch } from "../../../../languages/TreeSitterQuery/QueryCapture";
88

99
/** Scope handler to be used for iteration scopes of tree-sitter scope types */
1010
export class TreeSitterIterationScopeHandler extends BaseTreeSitterScopeHandler {

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { SimpleScopeType, TextEditor } from "@cursorless/common";
2-
3-
import { QueryMatch } from "web-tree-sitter";
42
import { TreeSitterQuery } from "../../../../languages/TreeSitterQuery";
3+
import { QueryMatch } from "../../../../languages/TreeSitterQuery/QueryCapture";
54
import ScopeTypeTarget from "../../../targets/ScopeTypeTarget";
65
import { TargetScope } from "../scope.types";
76
import { CustomScopeType } from "../scopeHandler.types";

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterTextFragmentScopeHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ScopeType, TextEditor } from "@cursorless/common";
2-
import { QueryMatch } from "web-tree-sitter";
32
import { TreeSitterQuery } from "../../../../languages/TreeSitterQuery";
3+
import { QueryMatch } from "../../../../languages/TreeSitterQuery/QueryCapture";
44
import { TEXT_FRAGMENT_CAPTURE_NAME } from "../../../../languages/captureNames";
55
import { PlainTarget } from "../../../targets";
66
import { TargetScope } from "../scope.types";

0 commit comments

Comments
 (0)