Skip to content

Commit 9d90d16

Browse files
Use narrow search range instead of full document for Tree sitter queries (#2714)
Today we generate all captures in the entire document whenever we do a Tree sitter query. This is of course quite expensive for large documents and it would be better if we could narrow the search range. That is what's implemented in this pull request. I also found some incorrect scm patterns that broke with this implementation that I have fixed. Fixes #1769 ## Checklist - [/] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [/] I have not broken the cheatsheet --------- Co-authored-by: Phil Cohen <[email protected]>
1 parent 785a750 commit 9d90d16

File tree

5 files changed

+110
-16
lines changed

5 files changed

+110
-16
lines changed

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { showError } from "@cursorless/common";
33
import { uniqWith } from "lodash-es";
44
import type { TreeSitterQuery } from "../../../../languages/TreeSitterQuery";
55
import type { QueryMatch } from "../../../../languages/TreeSitterQuery/QueryCapture";
6+
import { ide } from "../../../../singletons/ide.singleton";
67
import { BaseScopeHandler } from "../BaseScopeHandler";
78
import { compareTargetScopes } from "../compareTargetScopes";
89
import type { TargetScope } from "../scope.types";
910
import type { ScopeIteratorRequirements } from "../scopeHandler.types";
11+
import { getQuerySearchRange } from "./getQuerySearchRange";
1012
import { mergeAdjacentBy } from "./mergeAdjacentBy";
11-
import { ide } from "../../../../singletons/ide.singleton";
1213

1314
/** Base scope handler to use for both tree-sitter scopes and their iteration scopes */
1415
export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {
@@ -20,18 +21,20 @@ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {
2021
editor: TextEditor,
2122
position: Position,
2223
direction: Direction,
23-
_hints: ScopeIteratorRequirements,
24+
hints: ScopeIteratorRequirements,
2425
): Iterable<TargetScope> {
2526
const { document } = editor;
2627

27-
// Due to a tree-sitter bug, we generate all scopes from the entire file
28-
// instead of using `_hints` to restrict the search range to scopes we care
29-
// about. The actual scopes yielded to the client are filtered by
30-
// `BaseScopeHandler` anyway, so there's no impact on correctness, just
31-
// performance. We'd like to roll this back; see #1769.
28+
/** Narrow the range within which tree-sitter searches, for performance */
29+
const { start, end } = getQuerySearchRange(
30+
document,
31+
position,
32+
direction,
33+
hints,
34+
);
3235

3336
const scopes = this.query
34-
.matches(document)
37+
.matches(document, start, end)
3538
.map((match) => this.matchToScope(editor, match))
3639
.filter((scope): scope is ExtendedTargetScope => scope != null)
3740
.sort((a, b) => compareTargetScopes(direction, position, a, b));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { Direction, Position, TextDocument } from "@cursorless/common";
2+
import type {
3+
ContainmentPolicy,
4+
ScopeIteratorRequirements,
5+
} from "../scopeHandler.types";
6+
7+
/**
8+
* Constructs a range to pass to {@link Query.matches} to find scopes. Note
9+
* that {@link Query.matches} will only return scopes that have non-empty
10+
* intersection with this range. Also note that the base
11+
* {@link BaseScopeHandler.generateScopes} will filter out any extra scopes
12+
* that we yield, so we don't need to be totally precise.
13+
*
14+
* @returns Range to pass to {@link Query.matches}
15+
*/
16+
export function getQuerySearchRange(
17+
document: TextDocument,
18+
position: Position,
19+
direction: Direction,
20+
{
21+
containment,
22+
distalPosition,
23+
allowAdjacentScopes,
24+
}: ScopeIteratorRequirements,
25+
) {
26+
const { start, end } = getQuerySearchRangeCore(
27+
document.offsetAt(position),
28+
document.offsetAt(distalPosition),
29+
direction,
30+
containment,
31+
allowAdjacentScopes,
32+
);
33+
34+
return {
35+
start: document.positionAt(start),
36+
end: document.positionAt(end),
37+
};
38+
}
39+
40+
/** Helper function for {@link getQuerySearchRange} */
41+
function getQuerySearchRangeCore(
42+
offset: number,
43+
distalOffset: number,
44+
direction: Direction,
45+
containment: ContainmentPolicy | null,
46+
allowAdjacentScopes: boolean,
47+
) {
48+
/**
49+
* If we allow adjacent scopes, we need to shift some of our offsets by one
50+
* character
51+
*/
52+
const adjacentShift = allowAdjacentScopes ? 1 : 0;
53+
54+
if (containment === "required") {
55+
// If containment is required, we smear the position left and right by one
56+
// character so that we have a non-empty intersection with any scope that
57+
// touches position. Note that we can skip shifting the initial position
58+
// if we disallow adjacent scopes.
59+
return direction === "forward"
60+
? {
61+
start: offset - adjacentShift,
62+
end: offset + 1,
63+
}
64+
: {
65+
start: offset - 1,
66+
end: offset + adjacentShift,
67+
};
68+
}
69+
70+
// If containment is disallowed, we can shift the position forward by a
71+
// character to avoid matching scopes that touch position. Otherwise, we
72+
// shift the position backward by a character to ensure we get scopes that
73+
// touch position, if we allow adjacent scopes.
74+
const proximalShift = containment === "disallowed" ? 1 : -adjacentShift;
75+
76+
// FIXME: Don't go all the way to end of document when there is no
77+
// distalOffset? Seems wasteful to query all the way to end of document for
78+
// something like "next funk". Might be better to start smaller and
79+
// exponentially grow
80+
return direction === "forward"
81+
? {
82+
start: offset + proximalShift,
83+
end: distalOffset + adjacentShift,
84+
}
85+
: {
86+
start: distalOffset - adjacentShift,
87+
end: offset - proximalShift,
88+
};
89+
}

queries/c.scm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@
182182
) @_.domain
183183

184184
;;!! aaa = 0;
185-
(
185+
(_
186186
(assignment_expression
187187
left: (_) @name @value.leading.endOf
188188
right: (_) @value @name.trailing.startOf

queries/javascript.scm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
;; Define this here because the `field_definition` node type doesn't exist
88
;; in typescript.
9-
(
9+
(_
1010
;;!! class Foo {
1111
;;!! foo = () => {};
1212
;;! ^^^^^^^^^^^^^^^
@@ -31,7 +31,7 @@
3131
";"? @namedFunction.end @functionName.domain.end
3232
)
3333

34-
(
34+
(_
3535
;;!! foo = ...;
3636
;;! ^^^-------
3737
(field_definition

queries/typescript.core.scm

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
) @_.domain
4040

4141
;; Define these here because these node types don't exist in javascript.
42-
(
42+
(_
4343
[
4444
;;!! class Foo { foo() {} }
4545
;;! ^^^^^^^^
@@ -80,7 +80,7 @@
8080
";"? @namedFunction.end @functionName.domain.end @name.domain.end
8181
)
8282

83-
(
83+
(_
8484
;;!! (public | private | protected) foo = ...;
8585
;;! -----------------------------------------
8686
(public_field_definition
@@ -92,7 +92,7 @@
9292
";"? @_.domain.end
9393
)
9494

95-
(
95+
(_
9696
;;!! (public | private | protected) foo: Bar = ...;
9797
;;! ----------------------------------------------
9898
(public_field_definition
@@ -340,14 +340,15 @@
340340
;;! ^^^^
341341
;;! xxxxxx
342342
;;! ------------
343-
(
343+
(_
344344
(property_signature
345345
name: (_) @collectionKey @type.leading.endOf
346346
type: (_
347347
":"
348348
(_) @type @collectionKey.trailing.startOf
349349
)
350350
) @_.domain.start
351+
.
351352
";"? @_.domain.end
352353
)
353354

@@ -372,11 +373,12 @@
372373
)
373374

374375
;; Statements with optional trailing `;`
375-
(
376+
(_
376377
[
377378
(property_signature)
378379
(public_field_definition)
379380
(abstract_method_signature)
380381
] @statement.start
382+
.
381383
";"? @statement.end
382384
)

0 commit comments

Comments
 (0)