Skip to content

Commit bc50059

Browse files
Added Tree sitter provider (#2488)
## 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: Pokey Rule <755842+pokey@users.noreply.github.com>
1 parent 4d68be9 commit bc50059

File tree

12 files changed

+237
-82
lines changed

12 files changed

+237
-82
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Disposable } from "@cursorless/common";
2+
3+
/**
4+
* Provides raw tree-sitter queries. These are usually read from `.scm` files
5+
* on the filesystem, but this class abstracts away the details of how the
6+
* queries are stored.
7+
*/
8+
export interface RawTreeSitterQueryProvider {
9+
/**
10+
* Listen for changes to queries. For now, this is only used during
11+
* development, when we want to hot-reload queries.
12+
*/
13+
onChanges(listener: () => void): Disposable;
14+
15+
/**
16+
* Return the raw text of the tree-sitter query of the given name. The query
17+
* name is the name of one of the `.scm` files in our monorepo.
18+
*/
19+
readQuery(name: string): Promise<string | undefined>;
20+
}

packages/common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export * from "./ide/types/QuickPickOptions";
3131
export * from "./ide/types/events.types";
3232
export * from "./ide/types/Paths";
3333
export * from "./ide/types/CommandHistoryStorage";
34+
export * from "./ide/types/RawTreeSitterQueryProvider";
3435
export * from "./ide/types/FileSystem.types";
3536
export * from "./types/RangeExpansionBehavior";
3637
export * from "./types/InputBoxOptions";

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
IDE,
66
ScopeProvider,
77
ensureCommandShape,
8-
type FileSystem,
8+
type RawTreeSitterQueryProvider,
99
} from "@cursorless/common";
1010
import { KeyboardTargetUpdater } from "./KeyboardTargetUpdater";
1111
import {
@@ -19,10 +19,15 @@ import { StoredTargetMap } from "./core/StoredTargets";
1919
import { RangeUpdater } from "./core/updateSelections/RangeUpdater";
2020
import { DisabledCommandServerApi } from "./disabledComponents/DisabledCommandServerApi";
2121
import { DisabledHatTokenMap } from "./disabledComponents/DisabledHatTokenMap";
22+
import { DisabledLanguageDefinitions } from "./disabledComponents/DisabledLanguageDefinitions";
2223
import { DisabledSnippets } from "./disabledComponents/DisabledSnippets";
2324
import { DisabledTalonSpokenForms } from "./disabledComponents/DisabledTalonSpokenForms";
25+
import { DisabledTreeSitter } from "./disabledComponents/DisabledTreeSitter";
2426
import { CustomSpokenFormGeneratorImpl } from "./generateSpokenForm/CustomSpokenFormGeneratorImpl";
25-
import { LanguageDefinitions } from "./languages/LanguageDefinitions";
27+
import {
28+
LanguageDefinitionsImpl,
29+
type LanguageDefinitions,
30+
} from "./languages/LanguageDefinitions";
2631
import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl";
2732
import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers";
2833
import { runCommand } from "./runCommand";
@@ -39,8 +44,8 @@ import { TreeSitter } from "./typings/TreeSitter";
3944
interface Props {
4045
ide: IDE;
4146
hats?: Hats;
42-
treeSitter: TreeSitter;
43-
fileSystem: FileSystem;
47+
treeSitterQueryProvider?: RawTreeSitterQueryProvider;
48+
treeSitter?: TreeSitter;
4449
commandServerApi?: CommandServerApi;
4550
talonSpokenForms?: TalonSpokenForms;
4651
snippets?: Snippets;
@@ -49,8 +54,8 @@ interface Props {
4954
export async function createCursorlessEngine({
5055
ide,
5156
hats,
52-
treeSitter,
53-
fileSystem,
57+
treeSitterQueryProvider,
58+
treeSitter = new DisabledTreeSitter(),
5459
commandServerApi = new DisabledCommandServerApi(),
5560
talonSpokenForms = new DisabledTalonSpokenForms(),
5661
snippets = new DisabledSnippets(),
@@ -71,8 +76,13 @@ export async function createCursorlessEngine({
7176
: new DisabledHatTokenMap();
7277
void hatTokenMap.allocateHats();
7378

74-
const languageDefinitions = new LanguageDefinitions(fileSystem, treeSitter);
75-
await languageDefinitions.init();
79+
const languageDefinitions = treeSitterQueryProvider
80+
? await LanguageDefinitionsImpl.create(
81+
ide,
82+
treeSitter,
83+
treeSitterQueryProvider,
84+
)
85+
: new DisabledLanguageDefinitions();
7686

7787
ide.disposeOnExit(
7888
rangeUpdater,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { TextDocument, Range, Listener } from "@cursorless/common";
2+
import type { SyntaxNode } from "web-tree-sitter";
3+
import type { LanguageDefinition } from "../languages/LanguageDefinition";
4+
import type { LanguageDefinitions } from "../languages/LanguageDefinitions";
5+
6+
export class DisabledLanguageDefinitions implements LanguageDefinitions {
7+
onDidChangeDefinition(_listener: Listener) {
8+
return { dispose: () => {} };
9+
}
10+
11+
loadLanguage(_languageId: string): Promise<void> {
12+
return Promise.resolve();
13+
}
14+
15+
get(_languageId: string): LanguageDefinition | undefined {
16+
return undefined;
17+
}
18+
19+
getNodeAtLocation(
20+
_document: TextDocument,
21+
_range: Range,
22+
): SyntaxNode | undefined {
23+
return undefined;
24+
}
25+
26+
dispose(): void {
27+
// Do nothing
28+
}
29+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { TextDocument, Range } from "@cursorless/common";
2+
import type { SyntaxNode, Tree, Language } from "web-tree-sitter";
3+
import type { TreeSitter } from "../typings/TreeSitter";
4+
5+
export class DisabledTreeSitter implements TreeSitter {
6+
getTree(_document: TextDocument): Tree {
7+
throw new Error("Tree sitter not provided");
8+
}
9+
10+
loadLanguage(_languageId: string): Promise<boolean> {
11+
return Promise.resolve(false);
12+
}
13+
14+
getLanguage(_languageId: string): Language | undefined {
15+
throw new Error("Tree sitter not provided");
16+
}
17+
18+
getNodeAtLocation(_document: TextDocument, _range: Range): SyntaxNode {
19+
throw new Error("Tree sitter not provided");
20+
}
21+
}

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

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import {
2-
FileSystem,
32
ScopeType,
43
SimpleScopeType,
54
showError,
5+
type IDE,
6+
type RawTreeSitterQueryProvider,
67
} from "@cursorless/common";
7-
import { basename, dirname, join } from "pathe";
8+
import { dirname, join } from "pathe";
89
import { TreeSitterScopeHandler } from "../processTargets/modifiers/scopeHandlers";
9-
import { ide } from "../singletons/ide.singleton";
1010
import { TreeSitter } from "../typings/TreeSitter";
1111
import { matchAll } from "../util/regex";
1212
import { TreeSitterQuery } from "./TreeSitterQuery";
@@ -36,16 +36,15 @@ export class LanguageDefinition {
3636
* id doesn't have a new-style query definition
3737
*/
3838
static async create(
39+
ide: IDE,
40+
treeSitterQueryProvider: RawTreeSitterQueryProvider,
3941
treeSitter: TreeSitter,
40-
fileSystem: FileSystem,
41-
queryDir: string,
4242
languageId: string,
4343
): Promise<LanguageDefinition | undefined> {
44-
const languageQueryPath = join(queryDir, `${languageId}.scm`);
45-
4644
const rawLanguageQueryString = await readQueryFileAndImports(
47-
fileSystem,
48-
languageQueryPath,
45+
ide,
46+
treeSitterQueryProvider,
47+
`${languageId}.scm`,
4948
);
5049

5150
if (rawLanguageQueryString == null) {
@@ -91,43 +90,42 @@ export class LanguageDefinition {
9190
* @returns The text of the query file, with all imports inlined
9291
*/
9392
async function readQueryFileAndImports(
94-
fileSystem: FileSystem,
95-
languageQueryPath: string,
93+
ide: IDE,
94+
provider: RawTreeSitterQueryProvider,
95+
languageQueryName: string,
9696
) {
9797
// Seed the map with the query file itself
9898
const rawQueryStrings: Record<string, string | null> = {
99-
[languageQueryPath]: null,
99+
[languageQueryName]: null,
100100
};
101101

102-
const doValidation = ide().runMode !== "production";
102+
const doValidation = ide.runMode !== "production";
103103

104104
// Keep reading imports until we've read all the imports. Every time we
105105
// encounter an import in a query file, we add it to the map with a value
106106
// of null, so that it will be read on the next iteration
107107
while (Object.values(rawQueryStrings).some((v) => v == null)) {
108-
for (const [queryPath, rawQueryString] of Object.entries(rawQueryStrings)) {
108+
for (const [queryName, rawQueryString] of Object.entries(rawQueryStrings)) {
109109
if (rawQueryString != null) {
110110
continue;
111111
}
112112

113-
const fileName = basename(queryPath);
114-
115-
let rawQuery = await fileSystem.readBundledFile(queryPath);
113+
let rawQuery = await provider.readQuery(queryName);
116114

117115
if (rawQuery == null) {
118-
if (queryPath === languageQueryPath) {
116+
if (queryName === languageQueryName) {
119117
// If this is the main query file, then we know that this language
120118
// just isn't defined using new-style queries
121119
return undefined;
122120
}
123121

124122
showError(
125-
ide().messages,
123+
ide.messages,
126124
"LanguageDefinition.readQueryFileAndImports.queryNotFound",
127-
`Could not find imported query file ${queryPath}`,
125+
`Could not find imported query file ${queryName}`,
128126
);
129127

130-
if (ide().runMode === "test") {
128+
if (ide.runMode === "test") {
131129
throw new Error("Invalid import statement");
132130
}
133131

@@ -136,10 +134,10 @@ async function readQueryFileAndImports(
136134
}
137135

138136
if (doValidation) {
139-
validateQueryCaptures(fileName, rawQuery);
137+
validateQueryCaptures(queryName, rawQuery);
140138
}
141139

142-
rawQueryStrings[queryPath] = rawQuery;
140+
rawQueryStrings[queryName] = rawQuery;
143141
matchAll(
144142
rawQuery,
145143
// Matches lines like:
@@ -154,10 +152,10 @@ async function readQueryFileAndImports(
154152
const relativeImportPath = match[1];
155153

156154
if (doValidation) {
157-
validateImportSyntax(fileName, relativeImportPath, match[0]);
155+
validateImportSyntax(ide, queryName, relativeImportPath, match[0]);
158156
}
159157

160-
const importQueryPath = join(dirname(queryPath), relativeImportPath);
158+
const importQueryPath = join(dirname(queryName), relativeImportPath);
161159
rawQueryStrings[importQueryPath] =
162160
rawQueryStrings[importQueryPath] ?? null;
163161
},
@@ -169,6 +167,7 @@ async function readQueryFileAndImports(
169167
}
170168

171169
function validateImportSyntax(
170+
ide: IDE,
172171
file: string,
173172
relativeImportPath: string,
174173
actual: string,
@@ -177,12 +176,12 @@ function validateImportSyntax(
177176

178177
if (actual !== canonicalSyntax) {
179178
showError(
180-
ide().messages,
179+
ide.messages,
181180
"LanguageDefinition.readQueryFileAndImports.malformedImport",
182181
`Malformed import statement in ${file}: "${actual}". Import statements must be of the form "${canonicalSyntax}"`,
183182
);
184183

185-
if (ide().runMode === "test") {
184+
if (ide.runMode === "test") {
186185
throw new Error("Invalid import statement");
187186
}
188187
}

0 commit comments

Comments
 (0)