Skip to content

Commit ccf30bc

Browse files
Increase surrounding pair performance (#3186)
Main ``` Select surroundingPair.any 199 / 300 ms Select every surroundingPair.any 169 / 300 ms Select previous surroundingPair.any 152 / 300 ms Select surroundingPair.any with multiple cursors 228 / 400 ms ``` PR ``` Select surroundingPair.any 131 / 200 ms Select every surroundingPair.any 107 / 200 ms Select previous surroundingPair.any 102 / 200 ms Select surroundingPair.any with multiple cursors 224 / 400 ms ```
1 parent 845cd19 commit ccf30bc

12 files changed

Lines changed: 282 additions & 127 deletions

File tree

packages/common/src/types/command/PartialTargetDescriptor.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export interface SimpleScopeType {
230230
type: SimpleScopeTypeType;
231231
}
232232

233-
export type ScopeTypeType = SimpleScopeTypeType | ScopeType["type"];
233+
export type ScopeTypeType = ScopeType["type"];
234234

235235
export interface CustomRegexScopeType {
236236
type: "customRegex";

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,22 @@ export class LanguageDefinition {
8787
* document. We use this in our surrounding pair code.
8888
*
8989
* @param document The document to search
90-
* @param captureName The name of a capture to search for
90+
* @param captureNames Optional capture names to include
9191
* @returns A map of captures in the document
9292
*/
93-
getCapturesMap(document: TextDocument) {
94-
const matches = this.query.matches(document);
95-
const result: Partial<Record<SimpleScopeTypeType, QueryCapture[]>> = {};
93+
getCapturesMap<T extends SimpleScopeTypeType>(
94+
document: TextDocument,
95+
captureNames: readonly T[],
96+
) {
97+
const matches = this.query.matchesForCaptures(
98+
document,
99+
new Set(captureNames),
100+
);
101+
const result: Partial<Record<T, QueryCapture[]>> = {};
96102

97103
for (const match of matches) {
98104
for (const capture of match.captures) {
99-
const name = capture.name as SimpleScopeTypeType;
105+
const name = capture.name as T;
100106
if (result[name] == null) {
101107
result[name] = [];
102108
}

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

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Position, TextDocument, TreeSitter } from "@cursorless/common";
22
import type * as treeSitter from "web-tree-sitter";
33
import { ide } from "../../singletons/ide.singleton";
4+
import { getNormalizedCaptureName } from "./captureNames";
45
import { checkCaptureStartEnd } from "./checkCaptureStartEnd";
56
import { getNodeRange } from "./getNodeRange";
67
import { isContainedInErrorNode } from "./isContainedInErrorNode";
7-
import { normalizeCaptureName } from "./normalizeCaptureName";
88
import { parsePredicatesWithErrorHandling } from "./parsePredicatesWithErrorHandling";
99
import { positionToPoint } from "./positionToPoint";
1010
import type {
@@ -55,7 +55,7 @@ export class TreeSitterQuery {
5555

5656
hasCapture(name: string): boolean {
5757
return this.query.captureNames.some(
58-
(n) => normalizeCaptureName(n) === name,
58+
(n) => getNormalizedCaptureName(n) === name,
5959
);
6060
}
6161

@@ -64,29 +64,86 @@ export class TreeSitterQuery {
6464
start?: Position,
6565
end?: Position,
6666
): QueryMatch[] {
67-
if (!treeSitterQueryCache.isValid(document, start, end)) {
68-
const matches = this.getAllMatches(document, start, end);
69-
treeSitterQueryCache.update(document, start, end, matches);
67+
return this.getMatches(document, start, end, undefined);
68+
}
69+
70+
matchesForCaptures(
71+
document: TextDocument,
72+
captureNames: Set<string>,
73+
): QueryMatch[] {
74+
return this.getMatches(document, undefined, undefined, captureNames);
75+
}
76+
77+
private getMatches(
78+
document: TextDocument,
79+
start: Position | undefined,
80+
end: Position | undefined,
81+
captureNameFilter: Set<string> | undefined,
82+
): QueryMatch[] {
83+
if (
84+
!treeSitterQueryCache.isValid(document, start, end, captureNameFilter)
85+
) {
86+
const matches = this.calculateMatches(
87+
document,
88+
start,
89+
end,
90+
captureNameFilter,
91+
);
92+
treeSitterQueryCache.update(
93+
document,
94+
start,
95+
end,
96+
captureNameFilter,
97+
matches,
98+
);
7099
}
71100
return treeSitterQueryCache.get();
72101
}
73102

74-
private getAllMatches(
103+
private calculateMatches(
75104
document: TextDocument,
76-
start?: Position,
77-
end?: Position,
105+
start: Position | undefined,
106+
end: Position | undefined,
107+
captureNameFilter: Set<string> | undefined,
78108
): QueryMatch[] {
79109
const matches = this.getTreeMatches(document, start, end);
80110
const results: QueryMatch[] = [];
81111

82112
for (const match of matches) {
83-
const mutableMatch = this.createMutableQueryMatch(document, match);
113+
if (
114+
captureNameFilter != null &&
115+
!match.captures.some((capture) =>
116+
captureNameFilter.has(getNormalizedCaptureName(capture.name)),
117+
)
118+
) {
119+
continue;
120+
}
121+
122+
const hasPatternPredicates =
123+
this.patternPredicates[match.patternIndex].length > 0;
124+
125+
const mutableMatch = this.createMutableQueryMatch(
126+
document,
127+
match,
128+
// If there are pattern predicates, we need to include all captures when
129+
// creating the mutable match, since the predicates may depend on any of
130+
// the captures.
131+
!hasPatternPredicates ? captureNameFilter : undefined,
132+
);
84133

85134
if (!this.runPredicates(mutableMatch)) {
86135
continue;
87136
}
88137

89-
results.push(this.createQueryMatch(mutableMatch));
138+
const queryMatch = this.createQueryMatch(
139+
mutableMatch,
140+
// We only need to filter here if we didn't filter in createMutableQueryMatch()
141+
hasPatternPredicates ? captureNameFilter : undefined,
142+
);
143+
144+
if (queryMatch != null) {
145+
results.push(queryMatch);
146+
}
90147
}
91148

92149
return results;
@@ -107,18 +164,32 @@ export class TreeSitterQuery {
107164
private createMutableQueryMatch(
108165
document: TextDocument,
109166
match: treeSitter.QueryMatch,
167+
captureNameFilter?: Set<string>,
110168
): MutableQueryMatch {
111-
return {
112-
patternIdx: match.patternIndex,
113-
captures: match.captures.map(({ name, node }) => ({
169+
const captures: MutableQueryCapture[] = [];
170+
171+
for (const { name, node } of match.captures) {
172+
if (
173+
captureNameFilter != null &&
174+
!captureNameFilter.has(getNormalizedCaptureName(name))
175+
) {
176+
continue;
177+
}
178+
179+
captures.push({
114180
name,
115181
node,
116182
document,
117183
range: getNodeRange(node),
118184
insertionDelimiter: undefined,
119185
allowMultiple: false,
120186
hasError: () => isContainedInErrorNode(node),
121-
})),
187+
});
188+
}
189+
190+
return {
191+
patternIdx: match.patternIndex,
192+
captures,
122193
};
123194
}
124195

@@ -131,7 +202,10 @@ export class TreeSitterQuery {
131202
return true;
132203
}
133204

134-
private createQueryMatch(match: MutableQueryMatch): QueryMatch {
205+
private createQueryMatch(
206+
match: MutableQueryMatch,
207+
captureNameFilter?: Set<string>,
208+
): QueryMatch | undefined {
135209
const result: MutableQueryCapture[] = [];
136210
const map = new Map<
137211
string,
@@ -144,7 +218,10 @@ export class TreeSitterQuery {
144218
// name, for which we'd return a capture with name `foo`.
145219

146220
for (const capture of match.captures) {
147-
const name = normalizeCaptureName(capture.name);
221+
const name = getNormalizedCaptureName(capture.name);
222+
if (captureNameFilter != null && !captureNameFilter.has(name)) {
223+
continue;
224+
}
148225
const range = getStartOfEndOfRange(capture);
149226
const existing = map.get(name);
150227

@@ -168,6 +245,10 @@ export class TreeSitterQuery {
168245
}
169246
}
170247

248+
if (result.length === 0) {
249+
return undefined;
250+
}
251+
171252
if (this.shouldCheckCaptures) {
172253
this.checkCaptures(Array.from(map.values()));
173254
}

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,47 @@ export class TreeSitterQueryCache {
88
private startPosition: Position | undefined;
99
private endPosition: Position | undefined;
1010
private matches: QueryMatch[] = [];
11+
private captureNames: Set<string> | undefined;
1112

1213
clear() {
1314
this.documentUri = "";
1415
this.documentVersion = -1;
1516
this.documentLanguageId = "";
1617
this.startPosition = undefined;
1718
this.endPosition = undefined;
19+
this.captureNames = undefined;
1820
this.matches = [];
1921
}
2022

2123
isValid(
2224
document: TextDocument,
2325
startPosition: Position | undefined,
2426
endPosition: Position | undefined,
27+
captureNames: Set<string> | undefined,
2528
) {
2629
return (
2730
this.documentVersion === document.version &&
2831
this.documentUri === document.uri.toString() &&
2932
this.documentLanguageId === document.languageId &&
3033
positionsEqual(this.startPosition, startPosition) &&
31-
positionsEqual(this.endPosition, endPosition)
34+
positionsEqual(this.endPosition, endPosition) &&
35+
setEqual(this.captureNames, captureNames)
3236
);
3337
}
3438

3539
update(
3640
document: TextDocument,
3741
startPosition: Position | undefined,
3842
endPosition: Position | undefined,
43+
captureNames: Set<string> | undefined,
3944
matches: QueryMatch[],
4045
) {
4146
this.documentVersion = document.version;
4247
this.documentUri = document.uri.toString();
4348
this.documentLanguageId = document.languageId;
4449
this.startPosition = startPosition;
4550
this.endPosition = endPosition;
51+
this.captureNames = captureNames;
4652
this.matches = matches;
4753
}
4854

@@ -58,4 +64,19 @@ function positionsEqual(a: Position | undefined, b: Position | undefined) {
5864
return a.isEqual(b);
5965
}
6066

67+
function setEqual(a: Set<string> | undefined, b: Set<string> | undefined) {
68+
if (a == null || b == null) {
69+
return a === b;
70+
}
71+
if (a.size !== b.size) {
72+
return false;
73+
}
74+
for (const item of a) {
75+
if (!b.has(item)) {
76+
return false;
77+
}
78+
}
79+
return true;
80+
}
81+
6182
export const treeSitterQueryCache = new TreeSitterQueryCache();
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { pseudoScopes, simpleScopeTypeTypes } from "@cursorless/common";
2+
3+
const wildcard = "_";
4+
const captureNames = [
5+
...simpleScopeTypeTypes.filter((s) => !pseudoScopes.has(s)),
6+
wildcard,
7+
"interior",
8+
];
9+
10+
const positionRelationships = ["prefix", "leading", "trailing"];
11+
const positionSuffixes = [
12+
"startOf",
13+
"endOf",
14+
"start.startOf",
15+
"start.endOf",
16+
"end.startOf",
17+
"end.endOf",
18+
];
19+
20+
const rangeRelationships = [
21+
"domain",
22+
"removal",
23+
"iteration",
24+
"iteration.domain",
25+
];
26+
const rangeSuffixes = [
27+
"start",
28+
"end",
29+
"start.startOf",
30+
"start.endOf",
31+
"end.startOf",
32+
"end.endOf",
33+
];
34+
35+
const allowedCaptures = new Set<string>();
36+
37+
for (const captureName of captureNames) {
38+
// Wildcard is not allowed by itself without a relationship
39+
if (captureName !== wildcard) {
40+
// eg: statement
41+
allowedCaptures.add(captureName);
42+
43+
// eg: statement.start | statement.start.endOf
44+
for (const suffix of rangeSuffixes) {
45+
allowedCaptures.add(`${captureName}.${suffix}`);
46+
}
47+
}
48+
49+
for (const relationship of positionRelationships) {
50+
// eg: statement.leading
51+
allowedCaptures.add(`${captureName}.${relationship}`);
52+
53+
for (const suffix of positionSuffixes) {
54+
// eg: statement.leading.endOf
55+
allowedCaptures.add(`${captureName}.${relationship}.${suffix}`);
56+
}
57+
}
58+
59+
for (const relationship of rangeRelationships) {
60+
// eg: statement.domain
61+
allowedCaptures.add(`${captureName}.${relationship}`);
62+
63+
for (const suffix of rangeSuffixes) {
64+
// eg: statement.domain.start | statement.domain.start.endOf
65+
allowedCaptures.add(`${captureName}.${relationship}.${suffix}`);
66+
}
67+
}
68+
}
69+
70+
const normalizedCaptureNamesMap = new Map<string, string>();
71+
72+
for (const captureName of allowedCaptures) {
73+
normalizedCaptureNamesMap.set(captureName, normalizeCaptureName(captureName));
74+
}
75+
76+
function normalizeCaptureName(name: string): string {
77+
return name.replace(/(\.(start|end))?(\.(startOf|endOf))?$/, "");
78+
}
79+
80+
export function isCaptureAllowed(captureName: string): boolean {
81+
return allowedCaptures.has(captureName);
82+
}
83+
84+
export function getNormalizedCaptureName(captureName: string): string {
85+
return normalizedCaptureNamesMap.get(captureName) ?? captureName;
86+
}

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

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)