diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts index c91b1df70f..6a4a7971b5 100644 --- a/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeRangeWatcher.ts @@ -1,12 +1,14 @@ -import { Disposable } from "@cursorless/common"; +import { Disposable, showError } from "@cursorless/common"; import { pull } from "lodash"; import { IterationScopeChangeEventCallback, IterationScopeRangeConfig, ScopeChangeEventCallback, ScopeRangeConfig, + ScopeRanges, } from ".."; import { Debouncer } from "../core/Debouncer"; +import { LanguageDefinitions } from "../languages/LanguageDefinitions"; import { ide } from "../singletons/ide.singleton"; import { ScopeRangeProvider } from "./ScopeRangeProvider"; @@ -19,7 +21,10 @@ export class ScopeRangeWatcher { private debouncer = new Debouncer(() => this.onChange()); private listeners: (() => void)[] = []; - constructor(private scopeRangeProvider: ScopeRangeProvider) { + constructor( + languageDefinitions: LanguageDefinitions, + private scopeRangeProvider: ScopeRangeProvider, + ) { this.disposables.push( // An Event which fires when the array of visible editors has changed. ide().onDidChangeVisibleTextEditors(this.debouncer.run), @@ -32,6 +37,7 @@ export class ScopeRangeWatcher { // dirty-state changes. ide().onDidChangeTextDocument(this.debouncer.run), ide().onDidChangeTextEditorVisibleRanges(this.debouncer.run), + languageDefinitions.onDidChangeDefinition(this.debouncer.run), this.debouncer, ); @@ -54,10 +60,31 @@ export class ScopeRangeWatcher { ): Disposable { const fn = () => { ide().visibleTextEditors.forEach((editor) => { - callback( - editor, - this.scopeRangeProvider.provideScopeRanges(editor, config), - ); + let scopeRanges: ScopeRanges[]; + try { + scopeRanges = this.scopeRangeProvider.provideScopeRanges( + editor, + config, + ); + } catch (err) { + showError( + ide().messages, + "ScopeRangeWatcher.provide", + (err as Error).message, + ); + // If there was a problem getting scopes for an editor, we show an + // error and clear any scopes we might have shown last time. This is + // especially important during development, but also seems like the + // robust thing to do generally. + scopeRanges = []; + + if (ide().runMode === "test") { + // Fail hard if we're in test mode; otherwise recover + throw err; + } + } + + callback(editor, scopeRanges); }); }; diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index ef501b18be..ab0c96b495 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -35,7 +35,6 @@ export function createCursorlessEngine( const debug = new Debug(treeSitter); const rangeUpdater = new RangeUpdater(); - ide.disposeOnExit(rangeUpdater); const snippets = new Snippets(); snippets.init(); @@ -52,7 +51,9 @@ export function createCursorlessEngine( const testCaseRecorder = new TestCaseRecorder(hatTokenMap, storedTargets); - const languageDefinitions = new LanguageDefinitions(treeSitter); + const languageDefinitions = new LanguageDefinitions(fileSystem, treeSitter); + + ide.disposeOnExit(rangeUpdater, languageDefinitions, hatTokenMap, debug); return { commandApi: { @@ -108,7 +109,10 @@ function createScopeProvider( ), ); - const rangeWatcher = new ScopeRangeWatcher(rangeProvider); + const rangeWatcher = new ScopeRangeWatcher( + languageDefinitions, + rangeProvider, + ); const supportChecker = new ScopeSupportChecker(scopeHandlerFactory); return { diff --git a/packages/cursorless-engine/src/languages/LanguageDefinition.ts b/packages/cursorless-engine/src/languages/LanguageDefinition.ts index 0d220e0fd4..3576b94124 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinition.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinition.ts @@ -35,13 +35,10 @@ export class LanguageDefinition { */ static create( treeSitter: TreeSitter, + queryDir: string, languageId: string, ): LanguageDefinition | undefined { - const languageQueryPath = join( - ide().assetsRoot, - "queries", - `${languageId}.scm`, - ); + const languageQueryPath = join(queryDir, `${languageId}.scm`); if (!existsSync(languageQueryPath)) { return undefined; diff --git a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts index 639907e2f1..4632f3473c 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts @@ -1,6 +1,15 @@ -import { Range, TextDocument } from "@cursorless/common"; +import { + Disposable, + FileSystem, + Notifier, + Range, + TextDocument, + getCursorlessRepoRoot, +} from "@cursorless/common"; +import { join } from "path"; import { SyntaxNode } from "web-tree-sitter"; import { TreeSitter } from ".."; +import { ide } from "../singletons/ide.singleton"; import { LanguageDefinition } from "./LanguageDefinition"; /** @@ -14,6 +23,8 @@ const LANGUAGE_UNDEFINED = Symbol("LANGUAGE_UNDEFINED"); * constructing them as necessary */ export class LanguageDefinitions { + private notifier: Notifier = new Notifier(); + /** * Maps from language id to {@link LanguageDefinition} or * {@link LANGUAGE_UNDEFINED} if language doesn't have new-style definitions. @@ -28,8 +39,31 @@ export class LanguageDefinitions { string, LanguageDefinition | typeof LANGUAGE_UNDEFINED > = new Map(); + private queryDir: string; + private disposables: Disposable[] = []; + + constructor( + fileSystem: FileSystem, + private treeSitter: TreeSitter, + ) { + // Use the repo root as the root for development mode, so that we can + // we can make hot-reloading work for the queries + this.queryDir = join( + ide().runMode === "development" + ? getCursorlessRepoRoot() + : ide().assetsRoot, + "queries", + ); - constructor(private treeSitter: TreeSitter) {} + if (ide().runMode === "development") { + this.disposables.push( + fileSystem.watchDir(this.queryDir, () => { + this.languageDefinitions.clear(); + this.notifier.notifyListeners(); + }), + ); + } + } /** * Get a language definition for the given language id, if the language @@ -44,7 +78,7 @@ export class LanguageDefinitions { if (definition == null) { definition = - LanguageDefinition.create(this.treeSitter, languageId) ?? + LanguageDefinition.create(this.treeSitter, this.queryDir, languageId) ?? LANGUAGE_UNDEFINED; this.languageDefinitions.set(languageId, definition); @@ -59,4 +93,10 @@ export class LanguageDefinitions { public getNodeAtLocation(document: TextDocument, range: Range): SyntaxNode { return this.treeSitter.getNodeAtLocation(document, range); } + + onDidChangeDefinition = this.notifier.registerListener; + + dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts index a03fe61217..3011807a4e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts @@ -3,6 +3,7 @@ import { Position, TextDocument, TextEditor, + showError, } from "@cursorless/common"; import { uniqWith } from "lodash"; import { TreeSitterQuery } from "../../../../languages/TreeSitterQuery"; @@ -15,6 +16,7 @@ import { ScopeIteratorRequirements, } from "../scopeHandler.types"; import { mergeAdjacentBy } from "./mergeAdjacentBy"; +import { ide } from "../../../../singletons/ide.singleton"; /** Base scope handler to use for both tree-sitter scopes and their iteration scopes */ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler { @@ -65,9 +67,18 @@ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler { targets.length > 1 && !equivalentScopes.every((scope) => scope.allowMultiple) ) { - throw Error( - "Please use #allow-multiple! predicate in your query to allow multiple matches for this scope type", + const message = + "Please use #allow-multiple! predicate in your query to allow multiple matches for this scope type"; + + showError( + ide().messages, + "BaseTreeSitterScopeHandler.allow-multiple", + message, ); + + if (ide().runMode === "test") { + throw Error(message); + } } return targets;