Skip to content

Commit c69a57a

Browse files
Added Tree sitter query cache (#2711)
Uses the cache added in #2700 to also optimize all tree sitter queries, not just surrounding pairs. ## 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: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 1bffae5 commit c69a57a

File tree

8 files changed

+93
-102
lines changed

8 files changed

+93
-102
lines changed

packages/cursorless-engine/src/disabledComponents/DisabledLanguageDefinitions.ts

-4
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ export class DisabledLanguageDefinitions implements LanguageDefinitions {
1616
return undefined;
1717
}
1818

19-
clearCache(): void {
20-
// Do nothing
21-
}
22-
2319
getNodeAtLocation(
2420
_document: TextDocument,
2521
_range: Range,

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

+10-33
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type {
33
ScopeType,
44
SimpleScopeType,
55
SimpleScopeTypeType,
6-
StringRecord,
76
TreeSitter,
87
} from "@cursorless/common";
98
import {
@@ -13,7 +12,6 @@ import {
1312
type TextDocument,
1413
} from "@cursorless/common";
1514
import { TreeSitterScopeHandler } from "../processTargets/modifiers/scopeHandlers";
16-
import { LanguageDefinitionCache } from "./LanguageDefinitionCache";
1715
import { TreeSitterQuery } from "./TreeSitterQuery";
1816
import type { QueryCapture } from "./TreeSitterQuery/QueryCapture";
1917
import { validateQueryCaptures } from "./TreeSitterQuery/validateQueryCaptures";
@@ -23,18 +21,14 @@ import { validateQueryCaptures } from "./TreeSitterQuery/validateQueryCaptures";
2321
* tree-sitter query used to extract scopes for the given language
2422
*/
2523
export class LanguageDefinition {
26-
private cache: LanguageDefinitionCache;
27-
2824
private constructor(
2925
/**
3026
* The tree-sitter query used to extract scopes for the given language.
3127
* Note that this query contains patterns for all scope types that the
3228
* language supports using new-style tree-sitter queries
3329
*/
3430
private query: TreeSitterQuery,
35-
) {
36-
this.cache = new LanguageDefinitionCache();
37-
}
31+
) {}
3832

3933
/**
4034
* Construct a language definition for the given language id, if the language
@@ -88,41 +82,24 @@ export class LanguageDefinition {
8882
}
8983

9084
/**
91-
* This is a low-level function that just returns a list of captures of the given
92-
* capture name in the document. We use this in our surrounding pair code.
85+
* This is a low-level function that just returns a map of all captures in the
86+
* document. We use this in our surrounding pair code.
9387
*
9488
* @param document The document to search
9589
* @param captureName The name of a capture to search for
96-
* @returns A list of captures of the given capture name in the document
97-
*/
98-
getCaptures(
99-
document: TextDocument,
100-
captureName: SimpleScopeTypeType,
101-
): QueryCapture[] {
102-
if (!this.cache.isValid(document)) {
103-
this.cache.update(document, this.getCapturesMap(document));
104-
}
105-
106-
return this.cache.get(captureName);
107-
}
108-
109-
clearCache(): void {
110-
this.cache = new LanguageDefinitionCache();
111-
}
112-
113-
/**
114-
* This is a low level function that returns a map of all captures in the document.
90+
* @returns A map of captures in the document
11591
*/
116-
private getCapturesMap(document: TextDocument): StringRecord<QueryCapture[]> {
92+
getCapturesMap(document: TextDocument) {
11793
const matches = this.query.matches(document);
118-
const result: StringRecord<QueryCapture[]> = {};
94+
const result: Partial<Record<SimpleScopeTypeType, QueryCapture[]>> = {};
11995

12096
for (const match of matches) {
12197
for (const capture of match.captures) {
122-
if (result[capture.name] == null) {
123-
result[capture.name] = [];
98+
const name = capture.name as SimpleScopeTypeType;
99+
if (result[name] == null) {
100+
result[name] = [];
124101
}
125-
result[capture.name]!.push(capture);
102+
result[name]!.push(capture);
126103
}
127104
}
128105

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

-29
This file was deleted.

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

+8-19
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { toString } from "lodash-es";
1111
import type { SyntaxNode } from "web-tree-sitter";
1212
import { LanguageDefinition } from "./LanguageDefinition";
13+
import { treeSitterQueryCache } from "./TreeSitterQuery/treeSitterQueryCache";
1314

1415
/**
1516
* Sentinel value to indicate that a language doesn't have
@@ -32,17 +33,6 @@ export interface LanguageDefinitions {
3233
*/
3334
get(languageId: string): LanguageDefinition | undefined;
3435

35-
/**
36-
* Clear the cache of all language definitions. This is run at the start of a command.
37-
* This isn't strict necessary for normal user operations since whenever the user
38-
* makes a change to the document the document version is updated. When
39-
* running our test though we keep closing and reopening an untitled document.
40-
* That test document will have the same uri and version unfortunately. Also
41-
* to be completely sure there isn't some extension doing similar trickery
42-
* it's just good hygiene to clear the cache before every command.
43-
*/
44-
clearCache(): void;
45-
4636
/**
4737
* @deprecated Only for use in legacy containing scope stage
4838
*/
@@ -82,7 +72,13 @@ export class LanguageDefinitionsImpl
8272
private treeSitter: TreeSitter,
8373
private treeSitterQueryProvider: RawTreeSitterQueryProvider,
8474
) {
75+
const isTesting = ide.runMode === "test";
76+
8577
ide.onDidOpenTextDocument((document) => {
78+
// During testing we open untitled documents that all have the same uri and version which breaks our cache
79+
if (isTesting) {
80+
treeSitterQueryCache.clear();
81+
}
8682
void this.loadLanguage(document.languageId);
8783
});
8884
ide.onDidChangeVisibleTextEditors((editors) => {
@@ -150,6 +146,7 @@ export class LanguageDefinitionsImpl
150146
private async reloadLanguageDefinitions(): Promise<void> {
151147
this.languageDefinitions.clear();
152148
await this.loadAllLanguages();
149+
treeSitterQueryCache.clear();
153150
this.notifier.notifyListeners();
154151
}
155152

@@ -166,14 +163,6 @@ export class LanguageDefinitionsImpl
166163
return definition === LANGUAGE_UNDEFINED ? undefined : definition;
167164
}
168165

169-
clearCache(): void {
170-
for (const definition of this.languageDefinitions.values()) {
171-
if (definition !== LANGUAGE_UNDEFINED) {
172-
definition.clearCache();
173-
}
174-
}
175-
}
176-
177166
public getNodeAtLocation(document: TextDocument, range: Range): SyntaxNode {
178167
return this.treeSitter.getNodeAtLocation(document, range);
179168
}

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

+13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { isContainedInErrorNode } from "./isContainedInErrorNode";
1414
import { parsePredicates } from "./parsePredicates";
1515
import { predicateToString } from "./predicateToString";
1616
import { rewriteStartOfEndOf } from "./rewriteStartOfEndOf";
17+
import { treeSitterQueryCache } from "./treeSitterQueryCache";
1718

1819
/**
1920
* Wrapper around a tree-sitter query that provides a more convenient API, and
@@ -70,6 +71,18 @@ export class TreeSitterQuery {
7071
document: TextDocument,
7172
start?: Position,
7273
end?: Position,
74+
): QueryMatch[] {
75+
if (!treeSitterQueryCache.isValid(document, start, end)) {
76+
const matches = this.getAllMatches(document, start, end);
77+
treeSitterQueryCache.update(document, start, end, matches);
78+
}
79+
return treeSitterQueryCache.get();
80+
}
81+
82+
private getAllMatches(
83+
document: TextDocument,
84+
start?: Position,
85+
end?: Position,
7386
): QueryMatch[] {
7487
return this.query
7588
.matches(this.treeSitter.getTree(document).rootNode, {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Position, TextDocument } from "@cursorless/common";
2+
import type { QueryMatch } from "./QueryCapture";
3+
4+
export class Cache {
5+
private documentUri: string = "";
6+
private documentVersion: number = -1;
7+
private documentLanguageId: string = "";
8+
private startPosition: Position | undefined;
9+
private endPosition: Position | undefined;
10+
private matches: QueryMatch[] = [];
11+
12+
clear() {
13+
this.documentUri = "";
14+
this.documentVersion = -1;
15+
this.documentLanguageId = "";
16+
this.startPosition = undefined;
17+
this.endPosition = undefined;
18+
this.matches = [];
19+
}
20+
21+
isValid(
22+
document: TextDocument,
23+
startPosition: Position | undefined,
24+
endPosition: Position | undefined,
25+
) {
26+
return (
27+
this.documentUri === document.uri.toString() &&
28+
this.documentVersion === document.version &&
29+
this.documentLanguageId === document.languageId &&
30+
this.startPosition === startPosition &&
31+
this.endPosition === endPosition
32+
);
33+
}
34+
35+
update(
36+
document: TextDocument,
37+
startPosition: Position | undefined,
38+
endPosition: Position | undefined,
39+
matches: QueryMatch[],
40+
) {
41+
this.documentUri = document.uri.toString();
42+
this.documentVersion = document.version;
43+
this.documentLanguageId = document.languageId;
44+
this.startPosition = startPosition;
45+
this.endPosition = endPosition;
46+
this.matches = matches;
47+
}
48+
49+
get(): QueryMatch[] {
50+
return this.matches;
51+
}
52+
}
53+
54+
export const treeSitterQueryCache = new Cache();

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts

+8-15
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
matchAllIterator,
3-
Range,
4-
type SimpleScopeTypeType,
5-
type TextDocument,
6-
} from "@cursorless/common";
1+
import { matchAllIterator, Range, type TextDocument } from "@cursorless/common";
72
import type { LanguageDefinition } from "../../../../languages/LanguageDefinition";
83
import type { QueryCapture } from "../../../../languages/TreeSitterQuery/QueryCapture";
94
import { getDelimiterRegex } from "./getDelimiterRegex";
@@ -28,12 +23,12 @@ export function getDelimiterOccurrences(
2823
return [];
2924
}
3025

26+
const capturesMap = languageDefinition?.getCapturesMap(document) ?? {};
3127
const disqualifyDelimiters = new OneWayRangeFinder(
32-
getSortedCaptures(languageDefinition, document, "disqualifyDelimiter"),
28+
getSortedCaptures(capturesMap.disqualifyDelimiter),
3329
);
34-
// We need a tree for text fragments since they can be nested
3530
const textFragments = new OneWayNestedRangeFinder(
36-
getSortedCaptures(languageDefinition, document, "textFragment"),
31+
getSortedCaptures(capturesMap.textFragment),
3732
);
3833

3934
const delimiterTextToDelimiterInfoMap = Object.fromEntries(
@@ -74,12 +69,10 @@ export function getDelimiterOccurrences(
7469
return results;
7570
}
7671

77-
function getSortedCaptures(
78-
languageDefinition: LanguageDefinition | undefined,
79-
document: TextDocument,
80-
captureName: SimpleScopeTypeType,
81-
): QueryCapture[] {
82-
const items = languageDefinition?.getCaptures(document, captureName) ?? [];
72+
function getSortedCaptures(items?: QueryCapture[]): QueryCapture[] {
73+
if (items == null) {
74+
return [];
75+
}
8376
items.sort((a, b) => a.range.start.compareTo(b.range.start));
8477
return items;
8578
}

packages/cursorless-engine/src/runCommand.ts

-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ export async function runCommand(
7171
commandRunner = decorator.wrapCommandRunner(readableHatMap, commandRunner);
7272
}
7373

74-
languageDefinitions.clearCache();
75-
7674
const response = await commandRunner.run(commandComplete);
7775

7876
return await unwrapLegacyCommandResponse(command, response);

0 commit comments

Comments
 (0)