Skip to content

Commit f640f98

Browse files
committed
Query hot reloading during development
1 parent c10b417 commit f640f98

File tree

5 files changed

+98
-19
lines changed

5 files changed

+98
-19
lines changed

Diff for: packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts

+33-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { Disposable } from "@cursorless/common";
1+
import { Disposable, showError } from "@cursorless/common";
22
import { pull } from "lodash";
33
import {
44
IterationScopeChangeEventCallback,
55
IterationScopeRangeConfig,
66
ScopeChangeEventCallback,
77
ScopeRangeConfig,
8+
ScopeRanges,
89
} from "..";
910
import { Debouncer } from "../core/Debouncer";
11+
import { LanguageDefinitions } from "../languages/LanguageDefinitions";
1012
import { ide } from "../singletons/ide.singleton";
1113
import { ScopeRangeProvider } from "./ScopeRangeProvider";
1214

@@ -19,7 +21,10 @@ export class ScopeRangeWatcher {
1921
private debouncer = new Debouncer(() => this.onChange());
2022
private listeners: (() => void)[] = [];
2123

22-
constructor(private scopeRangeProvider: ScopeRangeProvider) {
24+
constructor(
25+
languageDefinitions: LanguageDefinitions,
26+
private scopeRangeProvider: ScopeRangeProvider,
27+
) {
2328
this.disposables.push(
2429
// An Event which fires when the array of visible editors has changed.
2530
ide().onDidChangeVisibleTextEditors(this.debouncer.run),
@@ -32,6 +37,7 @@ export class ScopeRangeWatcher {
3237
// dirty-state changes.
3338
ide().onDidChangeTextDocument(this.debouncer.run),
3439
ide().onDidChangeTextEditorVisibleRanges(this.debouncer.run),
40+
languageDefinitions.onDidChangeDefinition(this.debouncer.run),
3541
this.debouncer,
3642
);
3743

@@ -54,10 +60,31 @@ export class ScopeRangeWatcher {
5460
): Disposable {
5561
const fn = () => {
5662
ide().visibleTextEditors.forEach((editor) => {
57-
callback(
58-
editor,
59-
this.scopeRangeProvider.provideScopeRanges(editor, config),
60-
);
63+
let scopeRanges: ScopeRanges[];
64+
try {
65+
scopeRanges = this.scopeRangeProvider.provideScopeRanges(
66+
editor,
67+
config,
68+
);
69+
} catch (err) {
70+
showError(
71+
ide().messages,
72+
"ScopeRangeWatcher.provide",
73+
(err as Error).message,
74+
);
75+
// If there was a problem getting scopes for an editor, we show an
76+
// error and clear any scopes we might have shown last time. This is
77+
// especially important during development, but also seems like the
78+
// robust thing to do generally.
79+
scopeRanges = [];
80+
81+
if (ide().runMode === "test") {
82+
// Fail hard if we're in test mode; otherwise recover
83+
throw err;
84+
}
85+
}
86+
87+
callback(editor, scopeRanges);
6188
});
6289
};
6390

Diff for: packages/cursorless-engine/src/cursorlessEngine.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export function createCursorlessEngine(
3535
const debug = new Debug(treeSitter);
3636

3737
const rangeUpdater = new RangeUpdater();
38-
ide.disposeOnExit(rangeUpdater);
3938

4039
const snippets = new Snippets();
4140
snippets.init();
@@ -52,7 +51,9 @@ export function createCursorlessEngine(
5251

5352
const testCaseRecorder = new TestCaseRecorder(hatTokenMap, storedTargets);
5453

55-
const languageDefinitions = new LanguageDefinitions(treeSitter);
54+
const languageDefinitions = new LanguageDefinitions(fileSystem, treeSitter);
55+
56+
ide.disposeOnExit(rangeUpdater, languageDefinitions, hatTokenMap, debug);
5657

5758
return {
5859
commandApi: {
@@ -108,7 +109,10 @@ function createScopeProvider(
108109
),
109110
);
110111

111-
const rangeWatcher = new ScopeRangeWatcher(rangeProvider);
112+
const rangeWatcher = new ScopeRangeWatcher(
113+
languageDefinitions,
114+
rangeProvider,
115+
);
112116
const supportChecker = new ScopeSupportChecker(scopeHandlerFactory);
113117

114118
return {

Diff for: packages/cursorless-engine/src/languages/LanguageDefinition.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,10 @@ export class LanguageDefinition {
3535
*/
3636
static create(
3737
treeSitter: TreeSitter,
38+
queryDir: string,
3839
languageId: string,
3940
): LanguageDefinition | undefined {
40-
const languageQueryPath = join(
41-
ide().assetsRoot,
42-
"queries",
43-
`${languageId}.scm`,
44-
);
41+
const languageQueryPath = join(queryDir, `${languageId}.scm`);
4542

4643
if (!existsSync(languageQueryPath)) {
4744
return undefined;

Diff for: packages/cursorless-engine/src/languages/LanguageDefinitions.ts

+43-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
import { Range, TextDocument } from "@cursorless/common";
1+
import {
2+
Disposable,
3+
FileSystem,
4+
Notifier,
5+
Range,
6+
TextDocument,
7+
getCursorlessRepoRoot,
8+
} from "@cursorless/common";
9+
import { join } from "path";
210
import { SyntaxNode } from "web-tree-sitter";
311
import { TreeSitter } from "..";
12+
import { ide } from "../singletons/ide.singleton";
413
import { LanguageDefinition } from "./LanguageDefinition";
514

615
/**
@@ -14,6 +23,8 @@ const LANGUAGE_UNDEFINED = Symbol("LANGUAGE_UNDEFINED");
1423
* constructing them as necessary
1524
*/
1625
export class LanguageDefinitions {
26+
private notifier: Notifier = new Notifier();
27+
1728
/**
1829
* Maps from language id to {@link LanguageDefinition} or
1930
* {@link LANGUAGE_UNDEFINED} if language doesn't have new-style definitions.
@@ -28,8 +39,31 @@ export class LanguageDefinitions {
2839
string,
2940
LanguageDefinition | typeof LANGUAGE_UNDEFINED
3041
> = new Map();
42+
private queryDir: string;
43+
private disposables: Disposable[] = [];
44+
45+
constructor(
46+
fileSystem: FileSystem,
47+
private treeSitter: TreeSitter,
48+
) {
49+
// Use the repo root as the root for development mode, so that we can
50+
// we can make hot-reloading work for the queries
51+
this.queryDir = join(
52+
ide().runMode === "development"
53+
? getCursorlessRepoRoot()
54+
: ide().assetsRoot,
55+
"queries",
56+
);
3157

32-
constructor(private treeSitter: TreeSitter) {}
58+
if (ide().runMode === "development") {
59+
this.disposables.push(
60+
fileSystem.watchDir(this.queryDir, () => {
61+
this.languageDefinitions.clear();
62+
this.notifier.notifyListeners();
63+
}),
64+
);
65+
}
66+
}
3367

3468
/**
3569
* Get a language definition for the given language id, if the language
@@ -44,7 +78,7 @@ export class LanguageDefinitions {
4478

4579
if (definition == null) {
4680
definition =
47-
LanguageDefinition.create(this.treeSitter, languageId) ??
81+
LanguageDefinition.create(this.treeSitter, this.queryDir, languageId) ??
4882
LANGUAGE_UNDEFINED;
4983

5084
this.languageDefinitions.set(languageId, definition);
@@ -59,4 +93,10 @@ export class LanguageDefinitions {
5993
public getNodeAtLocation(document: TextDocument, range: Range): SyntaxNode {
6094
return this.treeSitter.getNodeAtLocation(document, range);
6195
}
96+
97+
onDidChangeDefinition = this.notifier.registerListener;
98+
99+
dispose() {
100+
this.disposables.forEach((disposable) => disposable.dispose());
101+
}
62102
}

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

+13-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Position,
44
TextDocument,
55
TextEditor,
6+
showError,
67
} from "@cursorless/common";
78
import { uniqWith } from "lodash";
89
import { TreeSitterQuery } from "../../../../languages/TreeSitterQuery";
@@ -15,6 +16,7 @@ import {
1516
ScopeIteratorRequirements,
1617
} from "../scopeHandler.types";
1718
import { mergeAdjacentBy } from "./mergeAdjacentBy";
19+
import { ide } from "../../../../singletons/ide.singleton";
1820

1921
/** Base scope handler to use for both tree-sitter scopes and their iteration scopes */
2022
export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {
@@ -65,9 +67,18 @@ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler {
6567
targets.length > 1 &&
6668
!equivalentScopes.every((scope) => scope.allowMultiple)
6769
) {
68-
throw Error(
69-
"Please use #allow-multiple! predicate in your query to allow multiple matches for this scope type",
70+
const message =
71+
"Please use #allow-multiple! predicate in your query to allow multiple matches for this scope type";
72+
73+
showError(
74+
ide().messages,
75+
"BaseTreeSitterScopeHandler.allow-multiple",
76+
message,
7077
);
78+
79+
if (ide().runMode === "test") {
80+
throw Error(message);
81+
}
7182
}
7283

7384
return targets;

0 commit comments

Comments
 (0)