From addf4e0192815e1ea1172e89f4e870159d91f94b Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 14:01:24 +0100 Subject: [PATCH 01/15] Working on unit testing scopes --- packages/cursorless-engine/package.json | 5 +- .../src/customCommandGrammar/lexer.test.ts | 2 +- .../src/test/scopes.test.ts} | 29 ++- .../src/test/sentenceSegmenter.test.ts | 2 +- .../src/test/subtoken.test.ts | 2 +- .../src/test/utils/TestEditor.ts | 170 ++++++++++++++++++ .../src/test/utils/createTestEngine.ts | 11 ++ .../src/test/utils/openNewEditor.ts | 21 +++ .../src/test/utils}/serializeScopeFixture.ts | 4 +- .../src/test/{ => utils}/unitTestSetup.ts | 2 +- .../tokenGraphemeSplitter.test.ts | 2 +- .../src/tokenizer/tokenizer.test.ts | 2 +- pnpm-lock.yaml | 10 ++ 13 files changed, 234 insertions(+), 28 deletions(-) rename packages/{cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts => cursorless-engine/src/test/scopes.test.ts} (90%) create mode 100644 packages/cursorless-engine/src/test/utils/TestEditor.ts create mode 100644 packages/cursorless-engine/src/test/utils/createTestEngine.ts create mode 100644 packages/cursorless-engine/src/test/utils/openNewEditor.ts rename packages/{cursorless-vscode-e2e/src/suite => cursorless-engine/src/test/utils}/serializeScopeFixture.ts (97%) rename packages/cursorless-engine/src/test/{ => utils}/unitTestSetup.ts (89%) diff --git a/packages/cursorless-engine/package.json b/packages/cursorless-engine/package.json index 688f700e83..d8d544fcc5 100644 --- a/packages/cursorless-engine/package.json +++ b/packages/cursorless-engine/package.json @@ -41,14 +41,17 @@ "zod": "^4.3.6" }, "devDependencies": { + "@types/chai": "^5.2.3", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.10", "@types/moo": "^0.5.10", "@types/nearley": "^2.11.5", "@types/sinon": "^21.0.0", + "chai": "^6.2.2", "js-yaml": "^4.1.1", "mocha": "^11.7.5", - "sinon": "^21.0.2" + "sinon": "^21.0.2", + "vscode-uri": "^3.1.0" } } diff --git a/packages/cursorless-engine/src/customCommandGrammar/lexer.test.ts b/packages/cursorless-engine/src/customCommandGrammar/lexer.test.ts index 59f559eb76..f015542362 100644 --- a/packages/cursorless-engine/src/customCommandGrammar/lexer.test.ts +++ b/packages/cursorless-engine/src/customCommandGrammar/lexer.test.ts @@ -1,5 +1,5 @@ import * as assert from "assert"; -import { unitTestSetup } from "../test/unitTestSetup"; +import { unitTestSetup } from "../test/utils/unitTestSetup"; import type { NearleyLexer, NearleyToken } from "./CommandLexer"; import { lexer } from "./lexer"; diff --git a/packages/cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts b/packages/cursorless-engine/src/test/scopes.test.ts similarity index 90% rename from packages/cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts rename to packages/cursorless-engine/src/test/scopes.test.ts index 053b0c2c0a..b27cbd6649 100644 --- a/packages/cursorless-vscode-e2e/src/suite/scopes.vscode.test.ts +++ b/packages/cursorless-engine/src/test/scopes.test.ts @@ -1,32 +1,29 @@ import type { - ScopeSupportFacet, - ScopeType, PlaintextScopeSupportFacet, ScopeRangeConfig, + ScopeSupportFacet, + ScopeType, } from "@cursorless/common"; import { asyncSafety, languageScopeSupport, + plaintextScopeSupportFacetInfos, scopeSupportFacetInfos, ScopeSupportFacetLevel, shouldUpdateFixtures, - plaintextScopeSupportFacetInfos, } from "@cursorless/common"; import { getScopeTestPathsRecursively } from "@cursorless/node-common"; -import { getCursorlessApi, openNewEditor } from "@cursorless/vscode-common"; import { assert } from "chai"; import { groupBy, uniq } from "lodash-es"; import { promises as fsp } from "node:fs"; -import { endToEndTestSetup } from "../endToEndTestSetup"; +import { createTestEngine } from "./utils/createTestEngine"; +import { openNewEditor } from "./utils/openNewEditor"; import { serializeIterationScopeFixture, serializeScopeFixture, -} from "./serializeScopeFixture"; -import { shouldSkipScopeTest } from "./shouldSkipTest"; +} from "./utils/serializeScopeFixture"; suite("Scope test cases", async function () { - endToEndTestSetup(this); - const testPaths = getScopeTestPathsRecursively(); if (!shouldUpdateFixtures()) { @@ -58,13 +55,7 @@ suite("Scope test cases", async function () { testPaths.forEach(({ path, name, languageId, facet }) => test( name, - asyncSafety(() => { - if (shouldSkipScopeTest(languageId)) { - this.ctx.skip(); - } - - return runTest(path, languageId, facet); - }), + asyncSafety(() => runTest(path, languageId, facet)), ), ); }); @@ -116,7 +107,6 @@ async function testLanguageSupport(languageId: string, testedFacets: string[]) { } async function runTest(file: string, languageId: string, facetId: string) { - const { ide, scopeProvider } = (await getCursorlessApi()).testHelpers!; const { scopeType, isIteration } = getFacetInfo(languageId, facetId); const fixture = (await fsp.readFile(file, "utf8")) .toString() @@ -130,9 +120,10 @@ async function runTest(file: string, languageId: string, facetId: string) { const code = fixture.slice(0, delimiterIndex! - 1); - await openNewEditor(code, { languageId }); + const { scopeProvider } = await createTestEngine(); + + const editor = await openNewEditor(code, languageId); - const editor = ide.activeTextEditor!; const updateFixture = shouldUpdateFixtures(); const [outputFixture, numScopes] = ((): [string, number] => { diff --git a/packages/cursorless-engine/src/test/sentenceSegmenter.test.ts b/packages/cursorless-engine/src/test/sentenceSegmenter.test.ts index 5032036e89..7998f7cdb7 100644 --- a/packages/cursorless-engine/src/test/sentenceSegmenter.test.ts +++ b/packages/cursorless-engine/src/test/sentenceSegmenter.test.ts @@ -1,7 +1,7 @@ import * as assert from "assert"; import { SentenceSegmenter } from "../processTargets/modifiers/scopeHandlers/SentenceScopeHandler/SentenceSegmenter"; import { sentenceSegmenterFixture } from "./fixtures/sentenceSegmeter.fixture"; -import { unitTestSetup } from "./unitTestSetup"; +import { unitTestSetup } from "./utils/unitTestSetup"; suite("Sentence segmenter", () => { unitTestSetup(); diff --git a/packages/cursorless-engine/src/test/subtoken.test.ts b/packages/cursorless-engine/src/test/subtoken.test.ts index 727c7ac2dd..ccc2d0d1cd 100644 --- a/packages/cursorless-engine/src/test/subtoken.test.ts +++ b/packages/cursorless-engine/src/test/subtoken.test.ts @@ -1,7 +1,7 @@ import * as assert from "assert"; import { WordTokenizer } from "../processTargets/modifiers/scopeHandlers/WordScopeHandler/WordTokenizer"; import { subtokenFixture } from "./fixtures/subtoken.fixture"; -import { unitTestSetup } from "./unitTestSetup"; +import { unitTestSetup } from "./utils/unitTestSetup"; suite("subtoken regex matcher", () => { unitTestSetup(); diff --git a/packages/cursorless-engine/src/test/utils/TestEditor.ts b/packages/cursorless-engine/src/test/utils/TestEditor.ts new file mode 100644 index 0000000000..1470e97631 --- /dev/null +++ b/packages/cursorless-engine/src/test/utils/TestEditor.ts @@ -0,0 +1,170 @@ +import type { + Edit, + EditableTextEditor, + GeneralizedRange, + InMemoryTextDocument, + OpenLinkOptions, + Range, + RevealLineAt, + Selection, + SetSelectionsOpts, + TextEditor, + TextEditorOptions, +} from "@cursorless/common"; + +export class TestEditor implements EditableTextEditor { + options: TextEditorOptions = { + tabSize: 4, + insertSpaces: true, + }; + + isActive = true; + + constructor( + public id: string, + public document: InMemoryTextDocument, + public visibleRanges: Range[], + public selections: Selection[], + ) {} + + isEqual(other: TextEditor): boolean { + return this.id === other.id; + } + + async setSelections( + _selections: Selection[], + _opts?: SetSelectionsOpts, + ): Promise { + throw Error("setSelections not implemented."); + } + + edit(_edits: Edit[]): Promise { + throw Error("edit not implemented."); + } + + async clipboardCopy(_ranges: Range[]): Promise { + throw Error("clipboardCopy not implemented."); + } + + async clipboardPaste(): Promise { + throw Error("clipboardPaste not implemented."); + } + + indentLine(_ranges: Range[]): Promise { + throw Error("indentLine not implemented."); + } + + outdentLine(_ranges: Range[]): Promise { + throw Error("outdentLine not implemented."); + } + + insertLineAfter(_ranges?: Range[]): Promise { + throw Error("insertLineAfter not implemented."); + } + + focus(): Promise { + throw new Error("focus not implemented."); + } + + revealRange(_range: Range): Promise { + return Promise.resolve(); + } + + revealLine(_lineNumber: number, _at: RevealLineAt): Promise { + throw new Error("revealLine not implemented."); + } + + openLink( + _range: Range, + _options?: OpenLinkOptions | undefined, + ): Promise { + throw new Error("openLink not implemented."); + } + + fold(_ranges?: Range[] | undefined): Promise { + throw new Error("fold not implemented."); + } + + unfold(_ranges?: Range[] | undefined): Promise { + throw new Error("unfold not implemented."); + } + + toggleBreakpoint(_ranges?: GeneralizedRange[]): Promise { + throw new Error("toggleBreakpoint not implemented."); + } + + toggleLineComment(_ranges?: Range[] | undefined): Promise { + throw new Error("toggleLineComment not implemented."); + } + + insertSnippet( + _snippet: string, + _ranges?: Range[] | undefined, + ): Promise { + throw new Error("insertSnippet not implemented."); + } + + rename(_range?: Range | undefined): Promise { + throw new Error("rename not implemented."); + } + + showReferences(_range?: Range | undefined): Promise { + throw new Error("showReferences not implemented."); + } + + quickFix(_range?: Range | undefined): Promise { + throw new Error("quickFix not implemented."); + } + + revealDefinition(_range?: Range | undefined): Promise { + throw new Error("revealDefinition not implemented."); + } + + revealTypeDefinition(_range?: Range | undefined): Promise { + throw new Error("revealTypeDefinition not implemented."); + } + + showHover(_range?: Range | undefined): Promise { + throw new Error("showHover not implemented."); + } + + showDebugHover(_range?: Range | undefined): Promise { + throw new Error("showDebugHover not implemented."); + } + + extractVariable(_range?: Range | undefined): Promise { + throw new Error("extractVariable not implemented."); + } + + editNewNotebookCellAbove(): Promise { + throw new Error("editNewNotebookCellAbove not implemented."); + } + + editNewNotebookCellBelow(): Promise { + throw new Error("editNewNotebookCellBelow not implemented."); + } + + public async gitAccept(_range?: Range): Promise { + throw Error("gitAccept not implemented"); + } + + public async gitRevert(_range?: Range): Promise { + throw Error("gitRevert not implemented"); + } + + public async gitStageFile(): Promise { + throw Error("gitStageFile not implemented"); + } + + public async gitUnstageFile(): Promise { + throw Error("gitUnstageFile not implemented"); + } + + public async gitStageRange(_range?: Range): Promise { + throw Error("gitStageRange not implemented"); + } + + public async gitUnstageRange(_range?: Range): Promise { + throw Error("gitUnstageRange not implemented"); + } +} diff --git a/packages/cursorless-engine/src/test/utils/createTestEngine.ts b/packages/cursorless-engine/src/test/utils/createTestEngine.ts new file mode 100644 index 0000000000..2cd3504ca0 --- /dev/null +++ b/packages/cursorless-engine/src/test/utils/createTestEngine.ts @@ -0,0 +1,11 @@ +import { createCursorlessEngine } from "../.."; +import { FakeIDE } from "@cursorless/common"; + +export async function createTestEngine() { + const ide = new FakeIDE(); + const { scopeProvider } = await createCursorlessEngine({ + ide, + }); + + return { ide, scopeProvider }; +} diff --git a/packages/cursorless-engine/src/test/utils/openNewEditor.ts b/packages/cursorless-engine/src/test/utils/openNewEditor.ts new file mode 100644 index 0000000000..46f644a631 --- /dev/null +++ b/packages/cursorless-engine/src/test/utils/openNewEditor.ts @@ -0,0 +1,21 @@ +import { + InMemoryTextDocument, + Selection, + type EditableTextEditor, +} from "@cursorless/common"; +import { URI } from "vscode-uri"; +import { TestEditor } from "./TestEditor"; + +let nextId = 0; + +export async function openNewEditor( + content: string, + languageId: string, +): Promise { + const id = String(nextId++); + const uri = URI.parse(`talon-js://${id}`); + const document = new InMemoryTextDocument(uri, languageId, content); + const visibleRanges = [document.range]; + const selections = [new Selection(0, 0, 0, 0)]; + return new TestEditor(id, document, visibleRanges, selections); +} diff --git a/packages/cursorless-vscode-e2e/src/suite/serializeScopeFixture.ts b/packages/cursorless-engine/src/test/utils/serializeScopeFixture.ts similarity index 97% rename from packages/cursorless-vscode-e2e/src/suite/serializeScopeFixture.ts rename to packages/cursorless-engine/src/test/utils/serializeScopeFixture.ts index 86b9a7b83d..18d918f810 100644 --- a/packages/cursorless-vscode-e2e/src/suite/serializeScopeFixture.ts +++ b/packages/cursorless-engine/src/test/utils/serializeScopeFixture.ts @@ -4,8 +4,8 @@ import type { ScopeRanges, TargetRanges, } from "@cursorless/common"; -import { serializeHeader } from "./serializeHeader"; -import { serializeTargetRange } from "./serializeTargetRange"; +import { serializeHeader } from "../../../../cursorless-vscode-e2e/src/suite/serializeHeader"; +import { serializeTargetRange } from "../../../../cursorless-vscode-e2e/src/suite/serializeTargetRange"; /** * These are special facets that are really only used as scopes for debugging. diff --git a/packages/cursorless-engine/src/test/unitTestSetup.ts b/packages/cursorless-engine/src/test/utils/unitTestSetup.ts similarity index 89% rename from packages/cursorless-engine/src/test/unitTestSetup.ts rename to packages/cursorless-engine/src/test/utils/unitTestSetup.ts index 64e3954bab..b8e66b61e2 100644 --- a/packages/cursorless-engine/src/test/unitTestSetup.ts +++ b/packages/cursorless-engine/src/test/utils/unitTestSetup.ts @@ -1,7 +1,7 @@ import { FakeIDE, SpyIDE } from "@cursorless/common"; import type { Context } from "mocha"; import * as sinon from "sinon"; -import { injectIde } from "../singletons/ide.singleton"; +import { injectIde } from "../../singletons/ide.singleton"; export function unitTestSetup(setupFake?: (fake: FakeIDE) => void) { let spy: SpyIDE | undefined; diff --git a/packages/cursorless-engine/src/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts b/packages/cursorless-engine/src/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts index d69e278b65..0b7896be97 100644 --- a/packages/cursorless-engine/src/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts +++ b/packages/cursorless-engine/src/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts @@ -1,6 +1,6 @@ import type { TokenHatSplittingMode } from "@cursorless/common"; import * as assert from "assert"; -import { unitTestSetup } from "../test/unitTestSetup"; +import { unitTestSetup } from "../test/utils/unitTestSetup"; import { TokenGraphemeSplitter, UNKNOWN } from "./tokenGraphemeSplitter"; /** diff --git a/packages/cursorless-engine/src/tokenizer/tokenizer.test.ts b/packages/cursorless-engine/src/tokenizer/tokenizer.test.ts index 68a90e0339..69753fb115 100644 --- a/packages/cursorless-engine/src/tokenizer/tokenizer.test.ts +++ b/packages/cursorless-engine/src/tokenizer/tokenizer.test.ts @@ -1,7 +1,7 @@ import * as assert from "assert"; import { flatten, range } from "lodash-es"; import { tokenize } from "."; -import { unitTestSetup } from "../test/unitTestSetup"; +import { unitTestSetup } from "../test/utils/unitTestSetup"; type TestCase = [string, string[]]; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70bb944e5c..ad501f988a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,6 +310,9 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: + '@types/chai': + specifier: ^5.2.3 + version: 5.2.3 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -328,6 +331,9 @@ importers: '@types/sinon': specifier: ^21.0.0 version: 21.0.0 + chai: + specifier: ^6.2.2 + version: 6.2.2 js-yaml: specifier: ^4.1.1 version: 4.1.1 @@ -337,6 +343,9 @@ importers: sinon: specifier: ^21.0.2 version: 21.0.2 + vscode-uri: + specifier: ^3.1.0 + version: 3.1.0 packages/cursorless-everywhere-talon: dependencies: @@ -10239,6 +10248,7 @@ packages: talon-snippets@1.3.0: resolution: {integrity: sha512-iFc1ePBQyaqZ73TL0lVgY+G8/DBfFTSiBRVdT2wT1CdPDips6usxSkBmXKGTDgHYJKstQx/NpXhIc0vXiAL4Kw==} + deprecated: 'Deprecated: use @cursorless/talon-tools instead.' tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} From fcbb3a334421d894a7d7c80efe2cb2c9187014ca Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 14:03:08 +0100 Subject: [PATCH 02/15] More files --- packages/cursorless-engine/src/test/utils/createTestEngine.ts | 3 ++- .../src/test/utils}/serializeHeader.ts | 0 .../cursorless-engine/src/test/utils/serializeScopeFixture.ts | 4 ++-- .../src/test/utils}/serializeTargetRange.ts | 0 4 files changed, 4 insertions(+), 3 deletions(-) rename packages/{cursorless-vscode-e2e/src/suite => cursorless-engine/src/test/utils}/serializeHeader.ts (100%) rename packages/{cursorless-vscode-e2e/src/suite => cursorless-engine/src/test/utils}/serializeTargetRange.ts (100%) diff --git a/packages/cursorless-engine/src/test/utils/createTestEngine.ts b/packages/cursorless-engine/src/test/utils/createTestEngine.ts index 2cd3504ca0..4b6399cced 100644 --- a/packages/cursorless-engine/src/test/utils/createTestEngine.ts +++ b/packages/cursorless-engine/src/test/utils/createTestEngine.ts @@ -3,9 +3,10 @@ import { FakeIDE } from "@cursorless/common"; export async function createTestEngine() { const ide = new FakeIDE(); + const { scopeProvider } = await createCursorlessEngine({ ide, }); - return { ide, scopeProvider }; + return { scopeProvider }; } diff --git a/packages/cursorless-vscode-e2e/src/suite/serializeHeader.ts b/packages/cursorless-engine/src/test/utils/serializeHeader.ts similarity index 100% rename from packages/cursorless-vscode-e2e/src/suite/serializeHeader.ts rename to packages/cursorless-engine/src/test/utils/serializeHeader.ts diff --git a/packages/cursorless-engine/src/test/utils/serializeScopeFixture.ts b/packages/cursorless-engine/src/test/utils/serializeScopeFixture.ts index 18d918f810..86b9a7b83d 100644 --- a/packages/cursorless-engine/src/test/utils/serializeScopeFixture.ts +++ b/packages/cursorless-engine/src/test/utils/serializeScopeFixture.ts @@ -4,8 +4,8 @@ import type { ScopeRanges, TargetRanges, } from "@cursorless/common"; -import { serializeHeader } from "../../../../cursorless-vscode-e2e/src/suite/serializeHeader"; -import { serializeTargetRange } from "../../../../cursorless-vscode-e2e/src/suite/serializeTargetRange"; +import { serializeHeader } from "./serializeHeader"; +import { serializeTargetRange } from "./serializeTargetRange"; /** * These are special facets that are really only used as scopes for debugging. diff --git a/packages/cursorless-vscode-e2e/src/suite/serializeTargetRange.ts b/packages/cursorless-engine/src/test/utils/serializeTargetRange.ts similarity index 100% rename from packages/cursorless-vscode-e2e/src/suite/serializeTargetRange.ts rename to packages/cursorless-engine/src/test/utils/serializeTargetRange.ts From 8ea5db8d0eac9c263abffcf1c69dd6a20871fc2b Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 14:27:59 +0100 Subject: [PATCH 03/15] Update all not implemented messages for consistency --- packages/common/src/ide/fake/FakeIDE.ts | 24 +++---- .../scopeHandlers/BaseScopeHandler.test.ts | 2 +- .../scopeHandlers/SortedScopeHandler.ts | 4 +- .../TreeSitterIterationScopeHandler.ts | 2 +- .../targets/NotebookCellDestination.ts | 4 +- .../src/test/utils/TestEditor.ts | 62 +++++++++--------- .../src/ide/TalonJsEditor.ts | 58 ++++++++--------- .../src/ide/TalonJsIDE.ts | 20 +++--- .../src/ide/TalonJsKeyValueStore.ts | 4 +- .../src/talonMock.ts | 4 +- .../neovim-common/src/ide/neovim/NeovimIDE.ts | 20 +++--- .../src/ide/neovim/NeovimTextEditorImpl.ts | 63 +++++++++---------- packages/test-harness/package.json | 1 + 13 files changed, 132 insertions(+), 136 deletions(-) diff --git a/packages/common/src/ide/fake/FakeIDE.ts b/packages/common/src/ide/fake/FakeIDE.ts index 9542d23f61..1847cc612f 100644 --- a/packages/common/src/ide/fake/FakeIDE.ts +++ b/packages/common/src/ide/fake/FakeIDE.ts @@ -71,41 +71,41 @@ export class FakeIDE implements IDE { } get activeTextEditor(): TextEditor | undefined { - throw Error("Not implemented"); + throw Error("activeTextEditor: not implemented"); } get activeEditableTextEditor(): EditableTextEditor | undefined { - throw Error("Not implemented"); + throw Error("activeEditableTextEditor: not implemented"); } get visibleTextEditors(): TextEditor[] { - throw Error("Not implemented"); + throw Error("visibleTextEditors: not implemented"); } get visibleNotebookEditors(): NotebookEditor[] { - throw Error("Not implemented"); + throw Error("visibleNotebookEditors: not implemented"); } public getEditableTextEditor(_editor: TextEditor): EditableTextEditor { - throw Error("Not implemented"); + throw Error("getEditableTextEditor: not implemented"); } public findInDocument(_query: string, _editor: TextEditor): Promise { - throw Error("Not implemented"); + throw Error("findInDocument: not implemented"); } public findInWorkspace(_query: string): Promise { - throw Error("Not implemented"); + throw Error("findInWorkspace: not implemented"); } public openTextDocument(_path: string): Promise { - throw Error("Not implemented"); + throw Error("openTextDocument: not implemented"); } public openUntitledTextDocument( _options: OpenUntitledTextDocumentOptions, ): Promise { - throw Error("Not implemented"); + throw Error("openUntitledTextDocument: not implemented"); } public setQuickPickReturnValue(value: string | undefined) { @@ -120,17 +120,17 @@ export class FakeIDE implements IDE { } public showInputBox(_options?: any): Promise { - throw Error("Not implemented"); + throw Error("showInputBox: not implemented"); } executeCommand(_command: string, ..._args: any[]): Promise { - throw new Error("Method not implemented."); + throw new Error("executeCommand: not implemented"); } public onDidChangeTextDocument( _listener: (event: TextDocumentChangeEvent) => void, ): Disposable { - throw Error("Not implemented"); + throw Error("onDidChangeTextDocument: not implemented"); } disposeOnExit(...disposables: Disposable[]): () => void { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.test.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.test.ts index d2f3f5ada6..113d063f96 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.test.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.test.ts @@ -13,7 +13,7 @@ class TestScopeHandler extends BaseScopeHandler { public scopeType = undefined; public get iterationScopeType(): CustomScopeType { - throw new Error("Method not implemented."); + throw new Error("iterationScopeType: not implemented"); } constructor( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SortedScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SortedScopeHandler.ts index 1941877233..87252e93de 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SortedScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SortedScopeHandler.ts @@ -59,7 +59,9 @@ export class SortedScopeHandler extends BaseScopeHandler { ), ), () => { - throw new Error("Not implemented"); + throw new Error( + "SortedScopeHandler: Iteration scope is not implemented", + ); }, ); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts index e3cfca770c..dd6efba28d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts @@ -20,7 +20,7 @@ export class TreeSitterIterationScopeHandler extends BaseTreeSitterScopeHandler // Doesn't have any iteration scope type itself; that would correspond to // something like "every every" public get iterationScopeType(): ScopeType { - throw Error("Not implemented"); + throw new Error("iterationScopeType: not implemented"); } constructor( diff --git a/packages/cursorless-engine/src/processTargets/targets/NotebookCellDestination.ts b/packages/cursorless-engine/src/processTargets/targets/NotebookCellDestination.ts index ba667d757b..6bfa97cce8 100644 --- a/packages/cursorless-engine/src/processTargets/targets/NotebookCellDestination.ts +++ b/packages/cursorless-engine/src/processTargets/targets/NotebookCellDestination.ts @@ -42,10 +42,10 @@ export class NotebookCellDestination implements Destination { } getEditNewActionType(): EditNewActionType { - throw new Error("Method not implemented."); + throw new Error("getEditNewActionType: not implemented"); } constructChangeEdit(_text: string): EditWithRangeUpdater { - throw new Error("Method not implemented."); + throw new Error("constructChangeEdit: not implemented"); } } diff --git a/packages/cursorless-engine/src/test/utils/TestEditor.ts b/packages/cursorless-engine/src/test/utils/TestEditor.ts index 1470e97631..4e13e55164 100644 --- a/packages/cursorless-engine/src/test/utils/TestEditor.ts +++ b/packages/cursorless-engine/src/test/utils/TestEditor.ts @@ -35,35 +35,35 @@ export class TestEditor implements EditableTextEditor { _selections: Selection[], _opts?: SetSelectionsOpts, ): Promise { - throw Error("setSelections not implemented."); + throw Error("setSelections: not implemented"); } edit(_edits: Edit[]): Promise { - throw Error("edit not implemented."); + throw Error("edit: not implemented"); } async clipboardCopy(_ranges: Range[]): Promise { - throw Error("clipboardCopy not implemented."); + throw Error("clipboardCopy: not implemented"); } async clipboardPaste(): Promise { - throw Error("clipboardPaste not implemented."); + throw Error("clipboardPaste: not implemented"); } indentLine(_ranges: Range[]): Promise { - throw Error("indentLine not implemented."); + throw Error("indentLine: not implemented"); } outdentLine(_ranges: Range[]): Promise { - throw Error("outdentLine not implemented."); + throw Error("outdentLine: not implemented"); } insertLineAfter(_ranges?: Range[]): Promise { - throw Error("insertLineAfter not implemented."); + throw Error("insertLineAfter: not implemented"); } focus(): Promise { - throw new Error("focus not implemented."); + throw new Error("focus: not implemented"); } revealRange(_range: Range): Promise { @@ -71,100 +71,100 @@ export class TestEditor implements EditableTextEditor { } revealLine(_lineNumber: number, _at: RevealLineAt): Promise { - throw new Error("revealLine not implemented."); + throw new Error("revealLine: not implemented"); } openLink( _range: Range, _options?: OpenLinkOptions | undefined, ): Promise { - throw new Error("openLink not implemented."); + throw new Error("openLink: not implemented"); } fold(_ranges?: Range[] | undefined): Promise { - throw new Error("fold not implemented."); + throw new Error("fold: not implemented"); } unfold(_ranges?: Range[] | undefined): Promise { - throw new Error("unfold not implemented."); + throw new Error("unfold: not implemented"); } toggleBreakpoint(_ranges?: GeneralizedRange[]): Promise { - throw new Error("toggleBreakpoint not implemented."); + throw new Error("toggleBreakpoint: not implemented"); } toggleLineComment(_ranges?: Range[] | undefined): Promise { - throw new Error("toggleLineComment not implemented."); + throw new Error("toggleLineComment: not implemented"); } insertSnippet( _snippet: string, _ranges?: Range[] | undefined, ): Promise { - throw new Error("insertSnippet not implemented."); + throw new Error("insertSnippet: not implemented"); } rename(_range?: Range | undefined): Promise { - throw new Error("rename not implemented."); + throw new Error("rename: not implemented"); } showReferences(_range?: Range | undefined): Promise { - throw new Error("showReferences not implemented."); + throw new Error("showReferences: not implemented"); } quickFix(_range?: Range | undefined): Promise { - throw new Error("quickFix not implemented."); + throw new Error("quickFix: not implemented"); } revealDefinition(_range?: Range | undefined): Promise { - throw new Error("revealDefinition not implemented."); + throw new Error("revealDefinition: not implemented"); } revealTypeDefinition(_range?: Range | undefined): Promise { - throw new Error("revealTypeDefinition not implemented."); + throw new Error("revealTypeDefinition: not implemented"); } showHover(_range?: Range | undefined): Promise { - throw new Error("showHover not implemented."); + throw new Error("showHover: not implemented"); } showDebugHover(_range?: Range | undefined): Promise { - throw new Error("showDebugHover not implemented."); + throw new Error("showDebugHover: not implemented"); } extractVariable(_range?: Range | undefined): Promise { - throw new Error("extractVariable not implemented."); + throw new Error("extractVariable: not implemented"); } editNewNotebookCellAbove(): Promise { - throw new Error("editNewNotebookCellAbove not implemented."); + throw new Error("editNewNotebookCellAbove: not implemented"); } editNewNotebookCellBelow(): Promise { - throw new Error("editNewNotebookCellBelow not implemented."); + throw new Error("editNewNotebookCellBelow: not implemented"); } public async gitAccept(_range?: Range): Promise { - throw Error("gitAccept not implemented"); + throw Error("gitAccept: not implemented"); } public async gitRevert(_range?: Range): Promise { - throw Error("gitRevert not implemented"); + throw Error("gitRevert: not implemented"); } public async gitStageFile(): Promise { - throw Error("gitStageFile not implemented"); + throw Error("gitStageFile: not implemented"); } public async gitUnstageFile(): Promise { - throw Error("gitUnstageFile not implemented"); + throw Error("gitUnstageFile: not implemented"); } public async gitStageRange(_range?: Range): Promise { - throw Error("gitStageRange not implemented"); + throw Error("gitStageRange: not implemented"); } public async gitUnstageRange(_range?: Range): Promise { - throw Error("gitUnstageRange not implemented"); + throw Error("gitUnstageRange: not implemented"); } } diff --git a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsEditor.ts b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsEditor.ts index c6592c0c65..6ab7bc3ac0 100644 --- a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsEditor.ts +++ b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsEditor.ts @@ -54,27 +54,27 @@ export class TalonJsEditor implements EditableTextEditor { } async clipboardCopy(_ranges: Range[]): Promise { - throw Error("clipboardCopy not implemented."); + throw Error("clipboardCopy: not implemented"); } async clipboardPaste(): Promise { - throw Error("clipboardPaste not implemented."); + throw Error("clipboardPaste: not implemented"); } indentLine(_ranges: Range[]): Promise { - throw Error("indentLine not implemented."); + throw Error("indentLine: not implemented"); } outdentLine(_ranges: Range[]): Promise { - throw Error("outdentLine not implemented."); + throw Error("outdentLine: not implemented"); } insertLineAfter(_ranges?: Range[]): Promise { - throw Error("insertLineAfter not implemented."); + throw Error("insertLineAfter: not implemented"); } focus(): Promise { - throw new Error("focus not implemented."); + throw new Error("focus: not implemented"); } revealRange(_range: Range): Promise { @@ -82,100 +82,100 @@ export class TalonJsEditor implements EditableTextEditor { } revealLine(_lineNumber: number, _at: RevealLineAt): Promise { - throw new Error("revealLine not implemented."); + throw new Error("revealLine: not implemented"); } openLink( _range: Range, _options?: OpenLinkOptions | undefined, ): Promise { - throw new Error("openLink not implemented."); + throw new Error("openLink: not implemented"); } fold(_ranges?: Range[] | undefined): Promise { - throw new Error("fold not implemented."); + throw new Error("fold: not implemented"); } unfold(_ranges?: Range[] | undefined): Promise { - throw new Error("unfold not implemented."); + throw new Error("unfold: not implemented"); } toggleBreakpoint(_ranges?: GeneralizedRange[]): Promise { - throw new Error("toggleBreakpoint not implemented."); + throw new Error("toggleBreakpoint: not implemented"); } toggleLineComment(_ranges?: Range[] | undefined): Promise { - throw new Error("toggleLineComment not implemented."); + throw new Error("toggleLineComment: not implemented"); } insertSnippet( _snippet: string, _ranges?: Range[] | undefined, ): Promise { - throw new Error("insertSnippet not implemented."); + throw new Error("insertSnippet: not implemented"); } rename(_range?: Range | undefined): Promise { - throw new Error("rename not implemented."); + throw new Error("rename: not implemented"); } showReferences(_range?: Range | undefined): Promise { - throw new Error("showReferences not implemented."); + throw new Error("showReferences: not implemented"); } quickFix(_range?: Range | undefined): Promise { - throw new Error("quickFix not implemented."); + throw new Error("quickFix: not implemented"); } revealDefinition(_range?: Range | undefined): Promise { - throw new Error("revealDefinition not implemented."); + throw new Error("revealDefinition: not implemented"); } revealTypeDefinition(_range?: Range | undefined): Promise { - throw new Error("revealTypeDefinition not implemented."); + throw new Error("revealTypeDefinition: not implemented"); } showHover(_range?: Range | undefined): Promise { - throw new Error("showHover not implemented."); + throw new Error("showHover: not implemented"); } showDebugHover(_range?: Range | undefined): Promise { - throw new Error("showDebugHover not implemented."); + throw new Error("showDebugHover: not implemented"); } extractVariable(_range?: Range | undefined): Promise { - throw new Error("extractVariable not implemented."); + throw new Error("extractVariable: not implemented"); } editNewNotebookCellAbove(): Promise { - throw new Error("editNewNotebookCellAbove not implemented."); + throw new Error("editNewNotebookCellAbove: not implemented"); } editNewNotebookCellBelow(): Promise { - throw new Error("editNewNotebookCellBelow not implemented."); + throw new Error("editNewNotebookCellBelow: not implemented"); } public async gitAccept(_range?: Range): Promise { - throw Error("gitAccept not implemented"); + throw Error("gitAccept: not implemented"); } public async gitRevert(_range?: Range): Promise { - throw Error("gitRevert not implemented"); + throw Error("gitRevert: not implemented"); } public async gitStageFile(): Promise { - throw Error("gitStageFile not implemented"); + throw Error("gitStageFile: not implemented"); } public async gitUnstageFile(): Promise { - throw Error("gitUnstageFile not implemented"); + throw Error("gitUnstageFile: not implemented"); } public async gitStageRange(_range?: Range): Promise { - throw Error("gitStageRange not implemented"); + throw Error("gitStageRange: not implemented"); } public async gitUnstageRange(_range?: Range): Promise { - throw Error("gitUnstageRange not implemented"); + throw Error("gitUnstageRange: not implemented"); } } diff --git a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsIDE.ts b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsIDE.ts index 64a1f56d7c..f98d104789 100644 --- a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsIDE.ts +++ b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsIDE.ts @@ -59,15 +59,15 @@ export class TalonJsIDE implements IDE { } get assetsRoot(): string { - throw new Error("assetsRoot not implemented."); + throw new Error("assetsRoot: not implemented"); } get cursorlessVersion(): string { - throw new Error("cursorlessVersion not implemented."); + throw new Error("cursorlessVersion: not implemented"); } get workspaceFolders(): readonly WorkspaceFolder[] | undefined { - throw new Error("workspaceFolders not implemented."); + throw new Error("workspaceFolders: not implemented"); } get activeTextEditor(): TextEditor | undefined { @@ -110,34 +110,34 @@ export class TalonJsIDE implements IDE { } findInWorkspace(_query: string): Promise { - throw new Error("findInWorkspace not implemented."); + throw new Error("findInWorkspace: not implemented"); } openTextDocument(_path: string): Promise { - throw new Error("openTextDocument not implemented."); + throw new Error("openTextDocument: not implemented"); } openUntitledTextDocument( _options?: OpenUntitledTextDocumentOptions | undefined, ): Promise { - throw new Error("openUntitledTextDocument not implemented."); + throw new Error("openUntitledTextDocument: not implemented"); } showInputBox( _options?: InputBoxOptions | undefined, ): Promise { - throw new Error("showInputBox not implemented."); + throw new Error("showInputBox: not implemented"); } showQuickPick( _items: readonly string[], _options?: QuickPickOptions | undefined, ): Promise { - throw new Error("showQuickPick not implemented."); + throw new Error("showQuickPick: not implemented"); } executeCommand(_command: string, ..._args: any[]): Promise { - throw new Error("executeCommand not implemented."); + throw new Error("executeCommand: not implemented"); } flashRanges(flashDescriptors: FlashDescriptor[]): Promise { @@ -149,7 +149,7 @@ export class TalonJsIDE implements IDE { _editor: TextEditor, _ranges: GeneralizedRange[], ): Promise { - throw new Error("setHighlightRanges not implemented."); + throw new Error("setHighlightRanges: not implemented"); } onDidChangeTextDocument( diff --git a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsKeyValueStore.ts b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsKeyValueStore.ts index c3a8fb8aaf..2df7c41577 100644 --- a/packages/cursorless-everywhere-talon-core/src/ide/TalonJsKeyValueStore.ts +++ b/packages/cursorless-everywhere-talon-core/src/ide/TalonJsKeyValueStore.ts @@ -6,13 +6,13 @@ import type { export class TalonJsKeyValueStore implements KeyValueStore { get(_key: K): KeyValueStoreData[K] { - throw new Error("state.get not implemented."); + throw new Error("state.get: not implemented."); } async set( _key: K, _value: KeyValueStoreData[K], ): Promise { - throw new Error("state.set not implemented."); + throw new Error("state.set: not implemented."); } } diff --git a/packages/cursorless-everywhere-talon-e2e/src/talonMock.ts b/packages/cursorless-everywhere-talon-e2e/src/talonMock.ts index 94a2c47595..7d9c97bd13 100644 --- a/packages/cursorless-everywhere-talon-e2e/src/talonMock.ts +++ b/packages/cursorless-everywhere-talon-e2e/src/talonMock.ts @@ -32,7 +32,7 @@ const actions: TalonActions = { }, edit: { find(_text?: string): void { - throw new Error("edit.find not implemented."); + throw new Error("edit.find: not implemented"); }, }, user: { @@ -62,7 +62,7 @@ const actions: TalonActions = { const settings: TalonSettings = { get(_name, _defaultValue) { - throw Error("settings.get not implemented."); + throw Error("settings.get: not implemented"); }, }; diff --git a/packages/neovim-common/src/ide/neovim/NeovimIDE.ts b/packages/neovim-common/src/ide/neovim/NeovimIDE.ts index 3160a7ef96..0dad021d80 100644 --- a/packages/neovim-common/src/ide/neovim/NeovimIDE.ts +++ b/packages/neovim-common/src/ide/neovim/NeovimIDE.ts @@ -89,7 +89,7 @@ export class NeovimIDE implements IDE { _items: readonly string[], _options?: QuickPickOptions, ): Promise { - throw Error("showQuickPick Not implemented"); + throw Error("showQuickPick: not implemented"); } async setHighlightRanges( @@ -97,7 +97,7 @@ export class NeovimIDE implements IDE { _editor: TextEditor, _ranges: GeneralizedRange[], ): Promise { - throw Error("setHighlightRanges Not implemented"); + throw Error("setHighlightRanges: not implemented"); } async flashRanges(_flashDescriptors: FlashDescriptor[]): Promise { @@ -118,12 +118,10 @@ export class NeovimIDE implements IDE { } get activeTextEditor(): TextEditor | undefined { - // throw Error("activeTextEditor Not implemented"); return this.getActiveTextEditor(); } get activeEditableTextEditor(): EditableTextEditor | undefined { - // throw Error("activeEditableTextEditor Not implemented"); return this.getActiveTextEditor(); } @@ -157,7 +155,6 @@ export class NeovimIDE implements IDE { get visibleTextEditors(): NeovimTextEditorImpl[] { return Array.from(this.editorMap.values()); - // throw Error("visibleTextEditors Not implemented"); } get visibleNotebookEditors(): NotebookEditor[] { @@ -166,39 +163,38 @@ export class NeovimIDE implements IDE { public getEditableTextEditor(editor: TextEditor): EditableTextEditor { return editor as EditableTextEditor; - // throw Error("getEditableTextEditor Not implemented"); } public async findInDocument( _query: string, _editor: TextEditor, ): Promise { - throw Error("findInDocument Not implemented"); + throw Error("findInDocument: not implemented"); } public async findInWorkspace(_query: string): Promise { - throw Error("findInWorkspace Not implemented"); + throw Error("findInWorkspace: not implemented"); } public async openTextDocument(_path: string): Promise { - throw Error("openTextDocument Not implemented"); + throw Error("openTextDocument: not implemented"); } public async openUntitledTextDocument( _options: OpenUntitledTextDocumentOptions, ): Promise { - throw Error("openUntitledTextDocument Not implemented"); + throw Error("openUntitledTextDocument: not implemented"); } public async showInputBox(_options?: any): Promise { - throw Error("TextDocumentChangeEvent Not implemented"); + throw Error("showInputBox: not implemented"); } public async executeCommand( _command: string, ..._args: any[] ): Promise { - throw new Error("executeCommand Method not implemented."); + throw new Error("executeCommand: not implemented"); } public onDidChangeTextDocument( diff --git a/packages/neovim-common/src/ide/neovim/NeovimTextEditorImpl.ts b/packages/neovim-common/src/ide/neovim/NeovimTextEditorImpl.ts index 3be47d4c77..f2953bda88 100644 --- a/packages/neovim-common/src/ide/neovim/NeovimTextEditorImpl.ts +++ b/packages/neovim-common/src/ide/neovim/NeovimTextEditorImpl.ts @@ -59,7 +59,6 @@ export class NeovimTextEditorImpl implements EditableTextEditor { get selections(): Selection[] { return this._selections as Selection[]; - // throw Error("get selections Not implemented"); } async setSelections(selections: Selection[]): Promise { @@ -74,11 +73,11 @@ export class NeovimTextEditorImpl implements EditableTextEditor { } get options(): TextEditorOptions { - throw Error("get options Not implemented"); + throw Error("options.get: not implemented"); } set options(options: TextEditorOptions) { - throw Error("set options Not implemented"); + throw Error("options.set: not implemented"); } get isActive(): boolean { @@ -89,50 +88,48 @@ export class NeovimTextEditorImpl implements EditableTextEditor { return this.id === other.id; } - public async revealRange(_range: Range): Promise { - // throw Error("revealRange Not implemented"); + public revealRange(_range: Range): Promise { + return Promise.resolve(); } public revealLine(_lineNumber: number, _at: RevealLineAt): Promise { - throw Error("revealLine Not implemented"); + throw Error("revealLine: not implemented"); } public async edit(edits: Edit[]): Promise { - //throw Error("edit Not implemented"); return await neovimEdit(this.client, this.neovimIDE, this.window, edits); } public focus(): Promise { return Promise.resolve(); - // throw Error("focus Not implemented"); } public editNewNotebookCellAbove(): Promise { - throw Error("editNewNotebookCellAbove Not implemented"); + throw Error("editNewNotebookCellAbove: not implemented"); } public editNewNotebookCellBelow(): Promise { - throw Error("editNewNotebookCellBelow Not implemented"); + throw Error("editNewNotebookCellBelow: not implemented"); } public openLink(_range: Range, _options?: OpenLinkOptions): Promise { - throw Error("openLink Not implemented"); + throw Error("openLink: not implemented"); } public fold(_ranges?: Range[]): Promise { - throw Error("fold Not implemented"); + throw Error("fold: not implemented"); } public unfold(_ranges?: Range[]): Promise { - throw Error("unfold Not implemented"); + throw Error("unfold: not implemented"); } public toggleBreakpoint(_ranges?: GeneralizedRange[]): Promise { - throw Error("toggleBreakpoint Not implemented"); + throw Error("toggleBreakpoint: not implemented"); } public async toggleLineComment(_ranges?: Range[]): Promise { - throw Error("toggleLineComment Not implemented"); + throw Error("toggleLineComment: not implemented"); } public async clipboardCopy(_ranges?: Range[]): Promise { @@ -144,74 +141,74 @@ export class NeovimTextEditorImpl implements EditableTextEditor { } public async indentLine(_ranges?: Range[]): Promise { - throw Error("indentLine Not implemented"); + throw Error("indentLine: not implemented"); } public async outdentLine(_ranges?: Range[]): Promise { - throw Error("outdentLine Not implemented"); + throw Error("outdentLine: not implemented"); } public async insertLineAfter(_ranges?: Range[]): Promise { - throw Error("insertLineAfter Not implemented"); + throw Error("insertLineAfter: not implemented"); } public insertSnippet(_snippet: string, _ranges?: Range[]): Promise { - throw Error("insertSnippet Not implemented"); + throw Error("insertSnippet: not implemented"); } public async rename(_range?: Range): Promise { - throw Error("rename Not implemented"); + throw Error("rename: not implemented"); } public async showReferences(_range?: Range): Promise { - throw Error("showReferences Not implemented"); + throw Error("showReferences: not implemented"); } public async quickFix(_range?: Range): Promise { - throw Error("quickFix Not implemented"); + throw Error("quickFix: not implemented"); } public async revealDefinition(_range?: Range): Promise { - throw Error("revealDefinition Not implemented"); + throw Error("revealDefinition: not implemented"); } public async revealTypeDefinition(_range?: Range): Promise { - throw Error("revealTypeDefinition Not implemented"); + throw Error("revealTypeDefinition: not implemented"); } public async showHover(_range?: Range): Promise { - throw Error("showHover Not implemented"); + throw Error("showHover: not implemented"); } public async showDebugHover(_range?: Range): Promise { - throw Error("showDebugHover Not implemented"); + throw Error("showDebugHover: not implemented"); } public async extractVariable(_range?: Range): Promise { - throw Error("extractVariable Not implemented"); + throw Error("extractVariable: not implemented"); } public async gitAccept(_range?: Range): Promise { - throw Error("gitAccept Not implemented"); + throw Error("gitAccept: not implemented"); } public async gitRevert(_range?: Range): Promise { - throw Error("gitRevert Not implemented"); + throw Error("gitRevert: not implemented"); } public async gitStageFile(): Promise { - throw Error("gitStageFile not implemented"); + throw Error("gitStageFile: not implemented"); } public async gitUnstageFile(): Promise { - throw Error("gitUnstageFile not implemented"); + throw Error("gitUnstageFile: not implemented"); } public async gitStageRange(_range?: Range): Promise { - throw Error("gitStageRange not implemented"); + throw Error("gitStageRange: not implemented"); } public async gitUnstageRange(_range?: Range): Promise { - throw Error("gitUnstageRange not implemented"); + throw Error("gitUnstageRange: not implemented"); } } diff --git a/packages/test-harness/package.json b/packages/test-harness/package.json index 75459a6877..f16ef8f87a 100644 --- a/packages/test-harness/package.json +++ b/packages/test-harness/package.json @@ -16,6 +16,7 @@ "test": "env CURSORLESS_MODE=test my-ts-node src/scripts/runVscodeTestsCI.ts", "test:neovim": "env CURSORLESS_MODE=test my-ts-node src/scripts/runNeovimTestsCI.ts", "test:talonJs": "env CURSORLESS_MODE=test my-ts-node src/scripts/runTalonJsTests.ts", + "test:unit": "env CURSORLESS_MODE=test my-ts-node src/scripts/runUnitTestsOnly.ts", "build:base": "esbuild --sourcemap --conditions=cursorless:bundler --bundle --external:vscode --external:./reporters/parallel-buffered --external:./worker.js --external:talon --format=cjs --platform=node", "build": "pnpm run build:runner:vscode && pnpm run build:runner:neovim && pnpm run build:tests && pnpm run build:unit && pnpm run build:talon && pnpm run build:talonJs", "build:runner:vscode": "pnpm run build:base ./src/runners/extensionTestsVscode.ts --outfile=dist/extensionTestsVscode.cjs", From 141b067b2d64baa7191cef1e1918089bf83e7490 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 14:37:58 +0100 Subject: [PATCH 04/15] Added test:unit:subset script --- packages/common/scripts/my-ts-node.js | 1 + packages/test-harness/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/common/scripts/my-ts-node.js b/packages/common/scripts/my-ts-node.js index b5fb4595d3..5f0b65bf39 100755 --- a/packages/common/scripts/my-ts-node.js +++ b/packages/common/scripts/my-ts-node.js @@ -88,6 +88,7 @@ async function main() { bundle: true, format: "cjs", outfile: outFile, + external: ["./reporters/parallel-buffered", "./worker.js"], }); const nodeProcess = runCommand( diff --git a/packages/test-harness/package.json b/packages/test-harness/package.json index f16ef8f87a..56918d70d6 100644 --- a/packages/test-harness/package.json +++ b/packages/test-harness/package.json @@ -17,6 +17,7 @@ "test:neovim": "env CURSORLESS_MODE=test my-ts-node src/scripts/runNeovimTestsCI.ts", "test:talonJs": "env CURSORLESS_MODE=test my-ts-node src/scripts/runTalonJsTests.ts", "test:unit": "env CURSORLESS_MODE=test my-ts-node src/scripts/runUnitTestsOnly.ts", + "test:unit:subset": "env CURSORLESS_MODE=test env CURSORLESS_RUN_TEST_SUBSET=true my-ts-node src/scripts/runUnitTestsOnly.ts", "build:base": "esbuild --sourcemap --conditions=cursorless:bundler --bundle --external:vscode --external:./reporters/parallel-buffered --external:./worker.js --external:talon --format=cjs --platform=node", "build": "pnpm run build:runner:vscode && pnpm run build:runner:neovim && pnpm run build:tests && pnpm run build:unit && pnpm run build:talon && pnpm run build:talonJs", "build:runner:vscode": "pnpm run build:base ./src/runners/extensionTestsVscode.ts --outfile=dist/extensionTestsVscode.cjs", From 624829571ec9c4da204a49858feaf7e66734a3a6 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 17:44:15 +0100 Subject: [PATCH 05/15] working on Tree sitter --- packages/common/src/ide/fake/FakeIDE.ts | 41 ++++--- packages/common/src/index.ts | 1 + packages/common/src/util/getErrorMessage.ts | 3 + packages/cursorless-engine/package.json | 3 +- .../src/customCommandGrammar/lexer.test.ts | 2 +- .../DisabledLanguageDefinitions.ts | 4 - .../src/languages/LanguageDefinitions.ts | 23 ++-- .../cursorless-engine/src/test/scopes.test.ts | 9 +- .../src/test/sentenceSegmenter.test.ts | 2 +- .../src/test/subtoken.test.ts | 2 +- .../src/test/utils/createTestEngine.ts | 12 -- .../src/test/utils/openNewEditor.ts | 21 ---- .../{test/utils => testUtil}/TestEditor.ts | 0 .../src/testUtil/TestFileSystem.ts | 43 +++++++ .../src/testUtil/TestTreeSitter.ts | 116 ++++++++++++++++++ .../src/testUtil/createTestEnvironment.ts | 78 ++++++++++++ .../utils => testUtil}/serializeHeader.ts | 0 .../serializeScopeFixture.ts | 0 .../serializeTargetRange.ts | 0 .../{test/utils => testUtil}/unitTestSetup.ts | 2 +- .../tokenGraphemeSplitter.test.ts | 2 +- .../src/tokenizer/tokenizer.test.ts | 2 +- packages/test-harness/package.json | 1 + pnpm-lock.yaml | 16 +++ 24 files changed, 308 insertions(+), 75 deletions(-) create mode 100644 packages/common/src/util/getErrorMessage.ts delete mode 100644 packages/cursorless-engine/src/test/utils/createTestEngine.ts delete mode 100644 packages/cursorless-engine/src/test/utils/openNewEditor.ts rename packages/cursorless-engine/src/{test/utils => testUtil}/TestEditor.ts (100%) create mode 100644 packages/cursorless-engine/src/testUtil/TestFileSystem.ts create mode 100644 packages/cursorless-engine/src/testUtil/TestTreeSitter.ts create mode 100644 packages/cursorless-engine/src/testUtil/createTestEnvironment.ts rename packages/cursorless-engine/src/{test/utils => testUtil}/serializeHeader.ts (100%) rename packages/cursorless-engine/src/{test/utils => testUtil}/serializeScopeFixture.ts (100%) rename packages/cursorless-engine/src/{test/utils => testUtil}/serializeTargetRange.ts (100%) rename packages/cursorless-engine/src/{test/utils => testUtil}/unitTestSetup.ts (89%) diff --git a/packages/common/src/ide/fake/FakeIDE.ts b/packages/common/src/ide/fake/FakeIDE.ts index 1847cc612f..6066869343 100644 --- a/packages/common/src/ide/fake/FakeIDE.ts +++ b/packages/common/src/ide/fake/FakeIDE.ts @@ -1,5 +1,11 @@ import { pull } from "lodash-es"; -import type { EditableTextEditor, NotebookEditor, TextEditor } from "../.."; +import type { + EditableTextEditor, + Messages, + NotebookEditor, + TextEditor, +} from "../.."; +import { Notifier } from "../.."; import type { GeneralizedRange } from "../../types/GeneralizedRange"; import type { TextDocument } from "../../types/TextDocument"; import type { TextDocumentChangeEvent } from "../types/Events"; @@ -24,11 +30,11 @@ import FakeKeyValueStore from "./FakeKeyValueStore"; import FakeMessages from "./FakeMessages"; export class FakeIDE implements IDE { - configuration: FakeConfiguration = new FakeConfiguration(); - messages: FakeMessages = new FakeMessages(); - keyValueStore: FakeKeyValueStore = new FakeKeyValueStore(); - clipboard: FakeClipboard = new FakeClipboard(); - capabilities: FakeCapabilities = new FakeCapabilities(); + configuration = new FakeConfiguration(); + keyValueStore = new FakeKeyValueStore(); + clipboard = new FakeClipboard(); + capabilities = new FakeCapabilities(); + messages: Messages; runMode: RunMode = "test"; cursorlessVersion: string = "0.0.0"; @@ -36,6 +42,12 @@ export class FakeIDE implements IDE { private disposables: Disposable[] = []; private assetsRoot_: string | undefined; private quickPickReturnValue: string | undefined = undefined; + private visibleTextEditors_: TextEditor[] = []; + private onOpenTextDocumentNotifier = new Notifier<[TextDocument]>(); + + constructor(messages: Messages = new FakeMessages()) { + this.messages = messages; + } async flashRanges(_flashDescriptors: FlashDescriptor[]): Promise { // empty @@ -49,7 +61,7 @@ export class FakeIDE implements IDE { // empty } - onDidOpenTextDocument: Event = dummyEvent; + onDidOpenTextDocument = this.onOpenTextDocumentNotifier.registerListener; onDidCloseTextDocument: Event = dummyEvent; onDidChangeActiveTextEditor: Event = dummyEvent; onDidChangeVisibleTextEditors: Event = dummyEvent; @@ -57,8 +69,13 @@ export class FakeIDE implements IDE { dummyEvent; onDidChangeTextEditorVisibleRanges: Event = dummyEvent; + onDidChangeTextDocument: Event = dummyEvent; - public mockAssetsRoot(_assetsRoot: string) { + triggerOpenTextDocument(document: TextDocument) { + this.onOpenTextDocumentNotifier.notifyListeners(document); + } + + mockAssetsRoot(_assetsRoot: string) { this.assetsRoot_ = _assetsRoot; } @@ -79,7 +96,7 @@ export class FakeIDE implements IDE { } get visibleTextEditors(): TextEditor[] { - throw Error("visibleTextEditors: not implemented"); + return this.visibleTextEditors_; } get visibleNotebookEditors(): NotebookEditor[] { @@ -127,12 +144,6 @@ export class FakeIDE implements IDE { throw new Error("executeCommand: not implemented"); } - public onDidChangeTextDocument( - _listener: (event: TextDocumentChangeEvent) => void, - ): Disposable { - throw Error("onDidChangeTextDocument: not implemented"); - } - disposeOnExit(...disposables: Disposable[]): () => void { this.disposables.push(...disposables); diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 49e9084636..b4677d3ec1 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -104,6 +104,7 @@ export * from "./util/DefaultMap"; export * from "./util/disposableFrom"; export * from "./util/ensureCommandShape"; export * from "./util/getEnvironmentVariableStrict"; +export * from "./util/getErrorMessage"; export * from "./util/itertools"; export * from "./util/Notifier"; export * from "./util/object"; diff --git a/packages/common/src/util/getErrorMessage.ts b/packages/common/src/util/getErrorMessage.ts new file mode 100644 index 0000000000..f60738ec5c --- /dev/null +++ b/packages/common/src/util/getErrorMessage.ts @@ -0,0 +1,3 @@ +export function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/packages/cursorless-engine/package.json b/packages/cursorless-engine/package.json index d8d544fcc5..0159f82145 100644 --- a/packages/cursorless-engine/package.json +++ b/packages/cursorless-engine/package.json @@ -52,6 +52,7 @@ "js-yaml": "^4.1.1", "mocha": "^11.7.5", "sinon": "^21.0.2", - "vscode-uri": "^3.1.0" + "vscode-uri": "^3.1.0", + "web-tree-sitter": "^0.26.6" } } diff --git a/packages/cursorless-engine/src/customCommandGrammar/lexer.test.ts b/packages/cursorless-engine/src/customCommandGrammar/lexer.test.ts index f015542362..307eebc680 100644 --- a/packages/cursorless-engine/src/customCommandGrammar/lexer.test.ts +++ b/packages/cursorless-engine/src/customCommandGrammar/lexer.test.ts @@ -1,5 +1,5 @@ import * as assert from "assert"; -import { unitTestSetup } from "../test/utils/unitTestSetup"; +import { unitTestSetup } from "../testUtil/unitTestSetup"; import type { NearleyLexer, NearleyToken } from "./CommandLexer"; import { lexer } from "./lexer"; diff --git a/packages/cursorless-engine/src/disabledComponents/DisabledLanguageDefinitions.ts b/packages/cursorless-engine/src/disabledComponents/DisabledLanguageDefinitions.ts index 8712704168..041f5134ed 100644 --- a/packages/cursorless-engine/src/disabledComponents/DisabledLanguageDefinitions.ts +++ b/packages/cursorless-engine/src/disabledComponents/DisabledLanguageDefinitions.ts @@ -7,10 +7,6 @@ export class DisabledLanguageDefinitions implements LanguageDefinitions { return { dispose: () => {} }; } - loadLanguage(_languageId: string): Promise { - return Promise.resolve(); - } - get(_languageId: string): LanguageDefinition | undefined { return undefined; } diff --git a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts index 5c500f81f8..c823c7fe03 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts @@ -5,8 +5,7 @@ import type { RawTreeSitterQueryProvider, TreeSitter, } from "@cursorless/common"; -import { Notifier, showError } from "@cursorless/common"; -import { toString } from "lodash-es"; +import { getErrorMessage, Notifier, showError } from "@cursorless/common"; import { LanguageDefinition } from "./LanguageDefinition"; import { treeSitterQueryCache } from "./TreeSitterQuery/TreeSitterQueryCache"; @@ -16,11 +15,9 @@ import { treeSitterQueryCache } from "./TreeSitterQuery/TreeSitterQueryCache"; */ const LANGUAGE_UNDEFINED = Symbol("LANGUAGE_UNDEFINED"); -export interface LanguageDefinitions { +export interface LanguageDefinitions extends Disposable { onDidChangeDefinition: (listener: Listener) => Disposable; - loadLanguage(languageId: string): Promise; - /** * Get a language definition for the given language id, if the language * has a new-style query definition, or return undefined if the language doesn't @@ -36,9 +33,7 @@ export interface LanguageDefinitions { * Keeps a map from language ids to {@link LanguageDefinition} instances, * constructing them as necessary */ -export class LanguageDefinitionsImpl - implements LanguageDefinitions, Disposable -{ +export class LanguageDefinitionsImpl implements LanguageDefinitions { private notifier: Notifier = new Notifier(); /** @@ -70,7 +65,13 @@ export class LanguageDefinitionsImpl if (isTesting) { treeSitterQueryCache.clear(); } - void this.loadLanguage(document.languageId); + this.loadLanguage(document.languageId).catch((err) => { + void showError( + this.ide.messages, + `Failed to load language definition: ${document.languageId}`, + getErrorMessage(err), + ); + }); }), ide.onDidChangeVisibleTextEditors((editors) => { @@ -112,7 +113,7 @@ export class LanguageDefinitionsImpl void showError( this.ide.messages, "Failed to load language definitions", - toString(err), + getErrorMessage(err), ); if (this.ide.runMode === "test") { throw err; @@ -120,7 +121,7 @@ export class LanguageDefinitionsImpl } } - public async loadLanguage(languageId: string): Promise { + private async loadLanguage(languageId: string): Promise { if (this.languageDefinitions.has(languageId)) { return; } diff --git a/packages/cursorless-engine/src/test/scopes.test.ts b/packages/cursorless-engine/src/test/scopes.test.ts index b27cbd6649..433eb225d6 100644 --- a/packages/cursorless-engine/src/test/scopes.test.ts +++ b/packages/cursorless-engine/src/test/scopes.test.ts @@ -16,12 +16,11 @@ import { getScopeTestPathsRecursively } from "@cursorless/node-common"; import { assert } from "chai"; import { groupBy, uniq } from "lodash-es"; import { promises as fsp } from "node:fs"; -import { createTestEngine } from "./utils/createTestEngine"; -import { openNewEditor } from "./utils/openNewEditor"; +import { createTestEnvironment } from "../testUtil/createTestEnvironment"; import { serializeIterationScopeFixture, serializeScopeFixture, -} from "./utils/serializeScopeFixture"; +} from "../testUtil/serializeScopeFixture"; suite("Scope test cases", async function () { const testPaths = getScopeTestPathsRecursively(); @@ -120,9 +119,9 @@ async function runTest(file: string, languageId: string, facetId: string) { const code = fixture.slice(0, delimiterIndex! - 1); - const { scopeProvider } = await createTestEngine(); + const { openNewEditor, scopeProvider } = await createTestEnvironment(); - const editor = await openNewEditor(code, languageId); + const editor = openNewEditor(code, languageId); const updateFixture = shouldUpdateFixtures(); diff --git a/packages/cursorless-engine/src/test/sentenceSegmenter.test.ts b/packages/cursorless-engine/src/test/sentenceSegmenter.test.ts index 7998f7cdb7..cb2fb0fd80 100644 --- a/packages/cursorless-engine/src/test/sentenceSegmenter.test.ts +++ b/packages/cursorless-engine/src/test/sentenceSegmenter.test.ts @@ -1,7 +1,7 @@ import * as assert from "assert"; import { SentenceSegmenter } from "../processTargets/modifiers/scopeHandlers/SentenceScopeHandler/SentenceSegmenter"; import { sentenceSegmenterFixture } from "./fixtures/sentenceSegmeter.fixture"; -import { unitTestSetup } from "./utils/unitTestSetup"; +import { unitTestSetup } from "../testUtil/unitTestSetup"; suite("Sentence segmenter", () => { unitTestSetup(); diff --git a/packages/cursorless-engine/src/test/subtoken.test.ts b/packages/cursorless-engine/src/test/subtoken.test.ts index ccc2d0d1cd..77409d0641 100644 --- a/packages/cursorless-engine/src/test/subtoken.test.ts +++ b/packages/cursorless-engine/src/test/subtoken.test.ts @@ -1,7 +1,7 @@ import * as assert from "assert"; import { WordTokenizer } from "../processTargets/modifiers/scopeHandlers/WordScopeHandler/WordTokenizer"; import { subtokenFixture } from "./fixtures/subtoken.fixture"; -import { unitTestSetup } from "./utils/unitTestSetup"; +import { unitTestSetup } from "../testUtil/unitTestSetup"; suite("subtoken regex matcher", () => { unitTestSetup(); diff --git a/packages/cursorless-engine/src/test/utils/createTestEngine.ts b/packages/cursorless-engine/src/test/utils/createTestEngine.ts deleted file mode 100644 index 4b6399cced..0000000000 --- a/packages/cursorless-engine/src/test/utils/createTestEngine.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createCursorlessEngine } from "../.."; -import { FakeIDE } from "@cursorless/common"; - -export async function createTestEngine() { - const ide = new FakeIDE(); - - const { scopeProvider } = await createCursorlessEngine({ - ide, - }); - - return { scopeProvider }; -} diff --git a/packages/cursorless-engine/src/test/utils/openNewEditor.ts b/packages/cursorless-engine/src/test/utils/openNewEditor.ts deleted file mode 100644 index 46f644a631..0000000000 --- a/packages/cursorless-engine/src/test/utils/openNewEditor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - InMemoryTextDocument, - Selection, - type EditableTextEditor, -} from "@cursorless/common"; -import { URI } from "vscode-uri"; -import { TestEditor } from "./TestEditor"; - -let nextId = 0; - -export async function openNewEditor( - content: string, - languageId: string, -): Promise { - const id = String(nextId++); - const uri = URI.parse(`talon-js://${id}`); - const document = new InMemoryTextDocument(uri, languageId, content); - const visibleRanges = [document.range]; - const selections = [new Selection(0, 0, 0, 0)]; - return new TestEditor(id, document, visibleRanges, selections); -} diff --git a/packages/cursorless-engine/src/test/utils/TestEditor.ts b/packages/cursorless-engine/src/testUtil/TestEditor.ts similarity index 100% rename from packages/cursorless-engine/src/test/utils/TestEditor.ts rename to packages/cursorless-engine/src/testUtil/TestEditor.ts diff --git a/packages/cursorless-engine/src/testUtil/TestFileSystem.ts b/packages/cursorless-engine/src/testUtil/TestFileSystem.ts new file mode 100644 index 0000000000..8014b2aec8 --- /dev/null +++ b/packages/cursorless-engine/src/testUtil/TestFileSystem.ts @@ -0,0 +1,43 @@ +import type { + Disposable, + FileSystem, + PathChangeListener, + RunMode, +} from "@cursorless/common"; +import { getCursorlessRepoRoot } from "@cursorless/node-common"; +import { join } from "node:path"; +import fs from "node:fs/promises"; + +export class TestFileSystem implements FileSystem { + public readonly cursorlessTalonStateJsonPath: string; + public readonly cursorlessCommandHistoryDirPath: string; + + constructor( + private readonly runMode: RunMode, + private readonly cursorlessDir: string, + ) { + this.cursorlessTalonStateJsonPath = join(this.cursorlessDir, "state.json"); + this.cursorlessCommandHistoryDirPath = join( + this.cursorlessDir, + "commandHistory", + ); + } + + public async initialize(): Promise {} + + public async readBundledFile(path: string): Promise { + const absolutePath = join(getCursorlessRepoRoot(), path); + try { + return fs.readFile(absolutePath, "utf-8"); + } catch (e) { + if (e instanceof Error && "code" in e && e.code === "ENOENT") { + return undefined; + } + throw e; + } + } + + public watchDir(_path: string, _onDidChange: PathChangeListener): Disposable { + throw new Error("watchDir: not implemented."); + } +} diff --git a/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts b/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts new file mode 100644 index 0000000000..9afeaebbda --- /dev/null +++ b/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts @@ -0,0 +1,116 @@ +import type { Range, TextDocument, TreeSitter } from "@cursorless/common"; +import * as path from "node:path"; +import type { Node, Query, Tree } from "web-tree-sitter"; +import { Language, Parser } from "web-tree-sitter"; + +const languageCache = new Map>(); +let initPromise: Promise | undefined; +// const webTreeSitterWasmPath = +// require.resolve("web-tree-sitter/web-tree-sitter.wasm"); + +function initTreeSitter() { + initPromise ??= Parser.init({ + locateFile(scriptName: string) { + console.log(`locateFile called with ${scriptName}`); // Debug log + return scriptName; + // return scriptName === "web-tree-sitter.wasm" + // ? webTreeSitterWasmPath + // : scriptName; + }, + }); + return initPromise; +} + +export class TestTreeSitter implements TreeSitter { + getNodeAtLocation(_document: TextDocument, _range: Range): Node { + throw new Error("getNodeAtLocation: not implemented."); + } + + getTree(_document: TextDocument): Tree { + throw new Error("getTree: not implemented."); + } + + async loadLanguage(languageId: string): Promise { + console.log(`loadLanguage called with languageId: ${languageId}`); // Debug log + if (!languageCache.has(languageId)) { + console.log("before"); + await initTreeSitter(); + console.log("after"); + const parserName = idToParser[languageId] ?? languageId; + const wasmFilePath = getWasmFilePath(parserName); + const promise = Language.load(wasmFilePath); + languageCache.set(languageId, promise); + } + + await languageCache.get(languageId); + + return true; + } + + createQuery(_languageId: string, _source: string): Query | undefined { + throw new Error("createQuery: not implemented."); + } +} + +function getWasmFilePath(parserName: string) { + const fileName = `${parserName}.wasm`; + return path.join( + __dirname, + "../../../../node_modules/@cursorless/tree-sitter-wasms/out", + fileName, + ); +} + +const idToParser: Record = { + // eslint-disable-next-line @typescript-eslint/naming-convention + "java-properties": "tree-sitter-properties", + // eslint-disable-next-line @typescript-eslint/naming-convention + "talon-list": "tree-sitter-talon", + agda: "tree-sitter-agda", + c: "tree-sitter-c", + clojure: "tree-sitter-clojure", + cpp: "tree-sitter-cpp", + csharp: "tree-sitter-c_sharp", + css: "tree-sitter-css", + dart: "tree-sitter-dart", + elixir: "tree-sitter-elixir", + elm: "tree-sitter-elm", + gdscript: "tree-sitter-gdscript", + gleam: "tree-sitter-gleam", + go: "tree-sitter-go", + haskell: "tree-sitter-haskell", + html: "tree-sitter-html", + java: "tree-sitter-java", + javascript: "tree-sitter-javascript", + javascriptreact: "tree-sitter-javascript", + json: "tree-sitter-json", + jsonc: "tree-sitter-json", + jsonl: "tree-sitter-json", + julia: "tree-sitter-julia", + kotlin: "tree-sitter-kotlin", + latex: "tree-sitter-latex", + lua: "tree-sitter-lua", + markdown: "tree-sitter-markdown", + nix: "tree-sitter-nix", + perl: "tree-sitter-perl", + php: "tree-sitter-php", + properties: "tree-sitter-properties", + python: "tree-sitter-python", + r: "tree-sitter-r", + ruby: "tree-sitter-ruby", + rust: "tree-sitter-rust", + scala: "tree-sitter-scala", + scm: "tree-sitter-query", + scss: "tree-sitter-scss", + shellscript: "tree-sitter-bash", + sparql: "tree-sitter-sparql", + starlark: "tree-sitter-python", + swift: "tree-sitter-swift", + talon: "tree-sitter-talon", + terraform: "tree-sitter-hcl", + typescript: "tree-sitter-typescript", + typescriptreact: "tree-sitter-tsx", + xml: "tree-sitter-xml", + yaml: "tree-sitter-yaml", + zig: "tree-sitter-zig", +}; diff --git a/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts b/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts new file mode 100644 index 0000000000..10f26e68ac --- /dev/null +++ b/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts @@ -0,0 +1,78 @@ +import { + FakeIDE, + InMemoryTextDocument, + Selection, + type EditableTextEditor, + type MessageId, + type Messages, + type MessageType, +} from "@cursorless/common"; +import { FileSystemRawTreeSitterQueryProvider } from "@cursorless/node-common"; +import { URI } from "vscode-uri"; +import { createCursorlessEngine } from ".."; +import { TestEditor } from "./TestEditor"; +import { TestFileSystem } from "./TestFileSystem"; +import { TestTreeSitter } from "./TestTreeSitter"; + +export async function createTestEnvironment() { + const ide = new FakeIDE(new TestMessages()); + const fileSystem = new TestFileSystem(ide.runMode, "testCursorlessDir"); + + const treeSitterQueryProvider = new FileSystemRawTreeSitterQueryProvider( + ide, + fileSystem, + ); + + const treeSitter = new TestTreeSitter(); + + const { scopeProvider } = await createCursorlessEngine({ + ide, + treeSitterQueryProvider, + treeSitter, + }); + + const openNewEditor = (content: string, languageId: string) => { + const editor = createNewEditor(content, languageId); + ide.triggerOpenTextDocument(editor.document); + return editor; + }; + + return { openNewEditor, scopeProvider }; +} + +let nextId = 0; + +function createNewEditor( + content: string, + languageId: string, +): EditableTextEditor { + const id = String(nextId++); + const uri = URI.parse(`talon-js://${id}`); + const document = new InMemoryTextDocument(uri, languageId, content); + const visibleRanges = [document.range]; + const selections = [new Selection(0, 0, 0, 0)]; + const editor = new TestEditor(id, document, visibleRanges, selections); + return editor; +} + +class TestMessages implements Messages { + showMessage( + type: MessageType, + _id: MessageId, + message: string, + ..._options: string[] + ): Promise { + switch (type) { + case "info": + console.log(message); + break; + case "warning": + console.log(`[warn] ${message}`); + break; + case "error": + console.log(`[error] ${message}`); + break; + } + return Promise.resolve(undefined); + } +} diff --git a/packages/cursorless-engine/src/test/utils/serializeHeader.ts b/packages/cursorless-engine/src/testUtil/serializeHeader.ts similarity index 100% rename from packages/cursorless-engine/src/test/utils/serializeHeader.ts rename to packages/cursorless-engine/src/testUtil/serializeHeader.ts diff --git a/packages/cursorless-engine/src/test/utils/serializeScopeFixture.ts b/packages/cursorless-engine/src/testUtil/serializeScopeFixture.ts similarity index 100% rename from packages/cursorless-engine/src/test/utils/serializeScopeFixture.ts rename to packages/cursorless-engine/src/testUtil/serializeScopeFixture.ts diff --git a/packages/cursorless-engine/src/test/utils/serializeTargetRange.ts b/packages/cursorless-engine/src/testUtil/serializeTargetRange.ts similarity index 100% rename from packages/cursorless-engine/src/test/utils/serializeTargetRange.ts rename to packages/cursorless-engine/src/testUtil/serializeTargetRange.ts diff --git a/packages/cursorless-engine/src/test/utils/unitTestSetup.ts b/packages/cursorless-engine/src/testUtil/unitTestSetup.ts similarity index 89% rename from packages/cursorless-engine/src/test/utils/unitTestSetup.ts rename to packages/cursorless-engine/src/testUtil/unitTestSetup.ts index b8e66b61e2..64e3954bab 100644 --- a/packages/cursorless-engine/src/test/utils/unitTestSetup.ts +++ b/packages/cursorless-engine/src/testUtil/unitTestSetup.ts @@ -1,7 +1,7 @@ import { FakeIDE, SpyIDE } from "@cursorless/common"; import type { Context } from "mocha"; import * as sinon from "sinon"; -import { injectIde } from "../../singletons/ide.singleton"; +import { injectIde } from "../singletons/ide.singleton"; export function unitTestSetup(setupFake?: (fake: FakeIDE) => void) { let spy: SpyIDE | undefined; diff --git a/packages/cursorless-engine/src/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts b/packages/cursorless-engine/src/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts index 0b7896be97..24e6d365e4 100644 --- a/packages/cursorless-engine/src/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts +++ b/packages/cursorless-engine/src/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts @@ -1,6 +1,6 @@ import type { TokenHatSplittingMode } from "@cursorless/common"; import * as assert from "assert"; -import { unitTestSetup } from "../test/utils/unitTestSetup"; +import { unitTestSetup } from "../testUtil/unitTestSetup"; import { TokenGraphemeSplitter, UNKNOWN } from "./tokenGraphemeSplitter"; /** diff --git a/packages/cursorless-engine/src/tokenizer/tokenizer.test.ts b/packages/cursorless-engine/src/tokenizer/tokenizer.test.ts index 69753fb115..cbd64c551a 100644 --- a/packages/cursorless-engine/src/tokenizer/tokenizer.test.ts +++ b/packages/cursorless-engine/src/tokenizer/tokenizer.test.ts @@ -1,7 +1,7 @@ import * as assert from "assert"; import { flatten, range } from "lodash-es"; import { tokenize } from "."; -import { unitTestSetup } from "../test/utils/unitTestSetup"; +import { unitTestSetup } from "../testUtil/unitTestSetup"; type TestCase = [string, string[]]; /** diff --git a/packages/test-harness/package.json b/packages/test-harness/package.json index 56918d70d6..cf332cc401 100644 --- a/packages/test-harness/package.json +++ b/packages/test-harness/package.json @@ -42,6 +42,7 @@ "tail": "^2.2.6" }, "devDependencies": { + "@cursorless/tree-sitter-wasms": "^0.7.0", "@types/cross-spawn": "^6.0.6", "@types/mocha": "^10.0.10", "@types/tail": "^2.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad501f988a..d20f7f25ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -346,6 +346,9 @@ importers: vscode-uri: specifier: ^3.1.0 version: 3.1.0 + web-tree-sitter: + specifier: ^0.26.6 + version: 0.26.6 packages/cursorless-everywhere-talon: dependencies: @@ -979,6 +982,9 @@ importers: specifier: ^2.2.6 version: 2.2.6 devDependencies: + '@cursorless/tree-sitter-wasms': + specifier: ^0.7.0 + version: 0.7.0 '@types/cross-spawn': specifier: ^6.0.6 version: 6.0.6 @@ -2048,6 +2054,9 @@ packages: peerDependencies: postcss: ^8.4 + '@cursorless/tree-sitter-wasms@0.7.0': + resolution: {integrity: sha512-yGmyFb75nmicYyXqKm0TMEydA8QF76kr/+dvBcu9eSN093CKijLYYgsg3xGU44K3lUI2Vjfec5OJ4tyaYssahA==} + '@dabh/diagnostics@2.0.8': resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} @@ -10763,6 +10772,9 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-tree-sitter@0.26.6: + resolution: {integrity: sha512-fSPR7VBW/fZQdUSp/bXTDLT+i/9dwtbnqgEBMzowrM4U3DzeCwDbY3MKo0584uQxID4m/1xpLflrlT/rLIRPew==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -12327,6 +12339,8 @@ snapshots: dependencies: postcss: 8.5.8 + '@cursorless/tree-sitter-wasms@0.7.0': {} + '@dabh/diagnostics@2.0.8': dependencies: '@so-ric/colorspace': 1.1.6 @@ -23360,6 +23374,8 @@ snapshots: web-namespaces@2.0.1: {} + web-tree-sitter@0.26.6: {} + webidl-conversions@7.0.0: {} webpack-bundle-analyzer@4.10.2: From e713290b5542809ec7f69e12bed7203861321398 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 18:56:26 +0100 Subject: [PATCH 06/15] Implemented Tree sitter properly --- packages/common/src/ide/fake/FakeIDE.ts | 11 +-- .../src/api/CursorlessEngineApi.ts | 2 + .../cursorless-engine/src/cursorlessEngine.ts | 1 + .../DisabledLanguageDefinitions.ts | 4 + .../src/languages/LanguageDefinitions.ts | 6 +- .../cursorless-engine/src/test/scopes.test.ts | 2 +- .../src/testUtil/TestTreeSitter.ts | 81 +++++++++++++------ .../src/testUtil/createTestEnvironment.ts | 6 +- 8 files changed, 73 insertions(+), 40 deletions(-) diff --git a/packages/common/src/ide/fake/FakeIDE.ts b/packages/common/src/ide/fake/FakeIDE.ts index 6066869343..e95d87ab24 100644 --- a/packages/common/src/ide/fake/FakeIDE.ts +++ b/packages/common/src/ide/fake/FakeIDE.ts @@ -5,7 +5,6 @@ import type { NotebookEditor, TextEditor, } from "../.."; -import { Notifier } from "../.."; import type { GeneralizedRange } from "../../types/GeneralizedRange"; import type { TextDocument } from "../../types/TextDocument"; import type { TextDocumentChangeEvent } from "../types/Events"; @@ -42,8 +41,6 @@ export class FakeIDE implements IDE { private disposables: Disposable[] = []; private assetsRoot_: string | undefined; private quickPickReturnValue: string | undefined = undefined; - private visibleTextEditors_: TextEditor[] = []; - private onOpenTextDocumentNotifier = new Notifier<[TextDocument]>(); constructor(messages: Messages = new FakeMessages()) { this.messages = messages; @@ -61,7 +58,7 @@ export class FakeIDE implements IDE { // empty } - onDidOpenTextDocument = this.onOpenTextDocumentNotifier.registerListener; + onDidOpenTextDocument: Event = dummyEvent; onDidCloseTextDocument: Event = dummyEvent; onDidChangeActiveTextEditor: Event = dummyEvent; onDidChangeVisibleTextEditors: Event = dummyEvent; @@ -71,10 +68,6 @@ export class FakeIDE implements IDE { dummyEvent; onDidChangeTextDocument: Event = dummyEvent; - triggerOpenTextDocument(document: TextDocument) { - this.onOpenTextDocumentNotifier.notifyListeners(document); - } - mockAssetsRoot(_assetsRoot: string) { this.assetsRoot_ = _assetsRoot; } @@ -96,7 +89,7 @@ export class FakeIDE implements IDE { } get visibleTextEditors(): TextEditor[] { - return this.visibleTextEditors_; + return []; } get visibleNotebookEditors(): NotebookEditor[] { diff --git a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts index 928d1e8202..aab3b72b5d 100644 --- a/packages/cursorless-engine/src/api/CursorlessEngineApi.ts +++ b/packages/cursorless-engine/src/api/CursorlessEngineApi.ts @@ -13,9 +13,11 @@ import type { } from "@cursorless/common"; import type { CommandRunner } from "../CommandRunner"; import type { StoredTargetMap } from "../core/StoredTargets"; +import type { LanguageDefinitions } from "../languages/LanguageDefinitions"; export interface CursorlessEngine { commandApi: CommandApi; + languageDefinitions: LanguageDefinitions; scopeProvider: ScopeProvider; customSpokenFormGenerator: CustomSpokenFormGenerator; storedTargets: StoredTargetMap; diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 67360ed471..b60a57f364 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -114,6 +114,7 @@ export async function createCursorlessEngine({ }; return { + languageDefinitions, commandApi: { runCommand(command: Command) { return runCommandClosure(command); diff --git a/packages/cursorless-engine/src/disabledComponents/DisabledLanguageDefinitions.ts b/packages/cursorless-engine/src/disabledComponents/DisabledLanguageDefinitions.ts index 041f5134ed..8712704168 100644 --- a/packages/cursorless-engine/src/disabledComponents/DisabledLanguageDefinitions.ts +++ b/packages/cursorless-engine/src/disabledComponents/DisabledLanguageDefinitions.ts @@ -7,6 +7,10 @@ export class DisabledLanguageDefinitions implements LanguageDefinitions { return { dispose: () => {} }; } + loadLanguage(_languageId: string): Promise { + return Promise.resolve(); + } + get(_languageId: string): LanguageDefinition | undefined { return undefined; } diff --git a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts index c823c7fe03..87c864f3d6 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinitions.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinitions.ts @@ -18,6 +18,8 @@ const LANGUAGE_UNDEFINED = Symbol("LANGUAGE_UNDEFINED"); export interface LanguageDefinitions extends Disposable { onDidChangeDefinition: (listener: Listener) => Disposable; + loadLanguage(languageId: string): Promise; + /** * Get a language definition for the given language id, if the language * has a new-style query definition, or return undefined if the language doesn't @@ -84,7 +86,7 @@ export class LanguageDefinitionsImpl implements LanguageDefinitions { ); } - public static async create( + static async create( ide: IDE, treeSitter: TreeSitter, treeSitterQueryProvider: RawTreeSitterQueryProvider, @@ -121,7 +123,7 @@ export class LanguageDefinitionsImpl implements LanguageDefinitions { } } - private async loadLanguage(languageId: string): Promise { + async loadLanguage(languageId: string): Promise { if (this.languageDefinitions.has(languageId)) { return; } diff --git a/packages/cursorless-engine/src/test/scopes.test.ts b/packages/cursorless-engine/src/test/scopes.test.ts index 433eb225d6..c5435af313 100644 --- a/packages/cursorless-engine/src/test/scopes.test.ts +++ b/packages/cursorless-engine/src/test/scopes.test.ts @@ -121,7 +121,7 @@ async function runTest(file: string, languageId: string, facetId: string) { const { openNewEditor, scopeProvider } = await createTestEnvironment(); - const editor = openNewEditor(code, languageId); + const editor = await openNewEditor(code, languageId); const updateFixture = shouldUpdateFixtures(); diff --git a/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts b/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts index 9afeaebbda..4c2456c9ac 100644 --- a/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts +++ b/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts @@ -1,23 +1,39 @@ import type { Range, TextDocument, TreeSitter } from "@cursorless/common"; +import { createRequire } from "node:module"; import * as path from "node:path"; -import type { Node, Query, Tree } from "web-tree-sitter"; -import { Language, Parser } from "web-tree-sitter"; +import type { + Node, + Tree, + Parser as TreeSitterParser, + Language as TreeSitterLanguage, + Query as TreeSitterQuery, +} from "web-tree-sitter"; -const languageCache = new Map>(); +// Force the CommonJS entrypoint because the ESM one crashes in this test +// runtime before we get a chance to initialize tree-sitter. +const moduleRequire = createRequire(__filename); + +const { + Language, + Parser, + Query, +}: { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + Parser: typeof import("web-tree-sitter").Parser; + Language: typeof TreeSitterLanguage; + Query: typeof TreeSitterQuery; +} = moduleRequire("web-tree-sitter"); + +interface Language { + language: TreeSitterLanguage; + parser: TreeSitterParser; +} + +const languageCache = new Map(); let initPromise: Promise | undefined; -// const webTreeSitterWasmPath = -// require.resolve("web-tree-sitter/web-tree-sitter.wasm"); function initTreeSitter() { - initPromise ??= Parser.init({ - locateFile(scriptName: string) { - console.log(`locateFile called with ${scriptName}`); // Debug log - return scriptName; - // return scriptName === "web-tree-sitter.wasm" - // ? webTreeSitterWasmPath - // : scriptName; - }, - }); + initPromise ??= Parser.init(); return initPromise; } @@ -26,29 +42,44 @@ export class TestTreeSitter implements TreeSitter { throw new Error("getNodeAtLocation: not implemented."); } - getTree(_document: TextDocument): Tree { - throw new Error("getTree: not implemented."); + getTree(document: TextDocument): Tree { + const language = languageCache.get(document.languageId); + + if (language == null) { + throw new Error(`Language not loaded: ${document.languageId}`); + } + + const tree = language.parser.parse(document.getText()); + + if (tree == null) { + throw new Error( + `Failed to parse document with language ${document.languageId}`, + ); + } + + return tree; } async loadLanguage(languageId: string): Promise { - console.log(`loadLanguage called with languageId: ${languageId}`); // Debug log if (!languageCache.has(languageId)) { - console.log("before"); await initTreeSitter(); - console.log("after"); const parserName = idToParser[languageId] ?? languageId; const wasmFilePath = getWasmFilePath(parserName); - const promise = Language.load(wasmFilePath); - languageCache.set(languageId, promise); + const language = await Language.load(wasmFilePath); + const parser = new Parser(); + parser.setLanguage(language); + languageCache.set(languageId, { language, parser }); } - await languageCache.get(languageId); - return true; } - createQuery(_languageId: string, _source: string): Query | undefined { - throw new Error("createQuery: not implemented."); + createQuery(languageId: string, source: string): TreeSitterQuery | undefined { + const language = languageCache.get(languageId); + if (language == null) { + return undefined; + } + return new Query(language.language, source); } } diff --git a/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts b/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts index 10f26e68ac..9d0b2ca2ad 100644 --- a/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts +++ b/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts @@ -25,15 +25,15 @@ export async function createTestEnvironment() { const treeSitter = new TestTreeSitter(); - const { scopeProvider } = await createCursorlessEngine({ + const { languageDefinitions, scopeProvider } = await createCursorlessEngine({ ide, treeSitterQueryProvider, treeSitter, }); - const openNewEditor = (content: string, languageId: string) => { + const openNewEditor = async (content: string, languageId: string) => { const editor = createNewEditor(content, languageId); - ide.triggerOpenTextDocument(editor.document); + await languageDefinitions.loadLanguage(languageId); return editor; }; From b39813bb359df4387522f8dd60c61cf6b4178207 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 19:21:12 +0100 Subject: [PATCH 07/15] Fix bug with reading un existing files --- .../src/languages/LanguageDefinition.ts | 2 +- .../cursorless-engine/src/test/scopes.test.ts | 38 +++++++++++++------ .../src/testUtil/TestFileSystem.ts | 2 +- .../src/testUtil/TestTreeSitter.ts | 6 ++- .../src/testUtil/createTestEnvironment.ts | 11 +++++- 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/cursorless-engine/src/languages/LanguageDefinition.ts b/packages/cursorless-engine/src/languages/LanguageDefinition.ts index e6c84b4d23..99da7defb7 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinition.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinition.ts @@ -122,7 +122,7 @@ async function readQueryFileAndImports( ide: IDE, provider: RawTreeSitterQueryProvider, languageQueryName: string, -) { +): Promise { // Seed the map with the query file itself const rawQueryStrings: Record = { [languageQueryName]: null, diff --git a/packages/cursorless-engine/src/test/scopes.test.ts b/packages/cursorless-engine/src/test/scopes.test.ts index c5435af313..3259e78e56 100644 --- a/packages/cursorless-engine/src/test/scopes.test.ts +++ b/packages/cursorless-engine/src/test/scopes.test.ts @@ -16,7 +16,10 @@ import { getScopeTestPathsRecursively } from "@cursorless/node-common"; import { assert } from "chai"; import { groupBy, uniq } from "lodash-es"; import { promises as fsp } from "node:fs"; -import { createTestEnvironment } from "../testUtil/createTestEnvironment"; +import { + createTestEnvironment, + type TestEnvironment, +} from "../testUtil/createTestEnvironment"; import { serializeIterationScopeFixture, serializeScopeFixture, @@ -24,6 +27,13 @@ import { suite("Scope test cases", async function () { const testPaths = getScopeTestPathsRecursively(); + let testEnvironment: TestEnvironment; + + suiteSetup( + asyncSafety(async () => { + testEnvironment = await createTestEnvironment(); + }), + ); if (!shouldUpdateFixtures()) { const languages = groupBy(testPaths, (test) => test.languageId); @@ -54,7 +64,7 @@ suite("Scope test cases", async function () { testPaths.forEach(({ path, name, languageId, facet }) => test( name, - asyncSafety(() => runTest(path, languageId, facet)), + asyncSafety(() => runTest(testEnvironment, path, languageId, facet)), ), ); }); @@ -105,7 +115,12 @@ async function testLanguageSupport(languageId: string, testedFacets: string[]) { } } -async function runTest(file: string, languageId: string, facetId: string) { +async function runTest( + testEnvironment: TestEnvironment, + file: string, + languageId: string, + facetId: string, +) { const { scopeType, isIteration } = getFacetInfo(languageId, facetId); const fixture = (await fsp.readFile(file, "utf8")) .toString() @@ -119,9 +134,7 @@ async function runTest(file: string, languageId: string, facetId: string) { const code = fixture.slice(0, delimiterIndex! - 1); - const { openNewEditor, scopeProvider } = await createTestEnvironment(); - - const editor = await openNewEditor(code, languageId); + const editor = await testEnvironment.openNewEditor(code, languageId); const updateFixture = shouldUpdateFixtures(); @@ -132,13 +145,11 @@ async function runTest(file: string, languageId: string, facetId: string) { }; if (isIteration) { - const iterationScopes = scopeProvider.provideIterationScopeRanges( - editor, - { + const iterationScopes = + testEnvironment.scopeProvider.provideIterationScopeRanges(editor, { ...config, includeNestedTargets: false, - }, - ); + }); if (!updateFixture) { assert.isFalse( @@ -155,7 +166,10 @@ async function runTest(file: string, languageId: string, facetId: string) { ]; } - const scopes = scopeProvider.provideScopeRanges(editor, config); + const scopes = testEnvironment.scopeProvider.provideScopeRanges( + editor, + config, + ); if (!updateFixture) { assert.isFalse( diff --git a/packages/cursorless-engine/src/testUtil/TestFileSystem.ts b/packages/cursorless-engine/src/testUtil/TestFileSystem.ts index 8014b2aec8..bd26e44940 100644 --- a/packages/cursorless-engine/src/testUtil/TestFileSystem.ts +++ b/packages/cursorless-engine/src/testUtil/TestFileSystem.ts @@ -28,7 +28,7 @@ export class TestFileSystem implements FileSystem { public async readBundledFile(path: string): Promise { const absolutePath = join(getCursorlessRepoRoot(), path); try { - return fs.readFile(absolutePath, "utf-8"); + return await fs.readFile(absolutePath, "utf-8"); } catch (e) { if (e instanceof Error && "code" in e && e.code === "ENOENT") { return undefined; diff --git a/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts b/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts index 4c2456c9ac..661e22f8bd 100644 --- a/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts +++ b/packages/cursorless-engine/src/testUtil/TestTreeSitter.ts @@ -61,9 +61,13 @@ export class TestTreeSitter implements TreeSitter { } async loadLanguage(languageId: string): Promise { + if (idToParser[languageId] == null) { + return false; + } + if (!languageCache.has(languageId)) { await initTreeSitter(); - const parserName = idToParser[languageId] ?? languageId; + const parserName = idToParser[languageId]; const wasmFilePath = getWasmFilePath(parserName); const language = await Language.load(wasmFilePath); const parser = new Parser(); diff --git a/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts b/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts index 9d0b2ca2ad..dadd0e7ef2 100644 --- a/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts +++ b/packages/cursorless-engine/src/testUtil/createTestEnvironment.ts @@ -6,6 +6,7 @@ import { type MessageId, type Messages, type MessageType, + type ScopeProvider, } from "@cursorless/common"; import { FileSystemRawTreeSitterQueryProvider } from "@cursorless/node-common"; import { URI } from "vscode-uri"; @@ -14,7 +15,15 @@ import { TestEditor } from "./TestEditor"; import { TestFileSystem } from "./TestFileSystem"; import { TestTreeSitter } from "./TestTreeSitter"; -export async function createTestEnvironment() { +export interface TestEnvironment { + openNewEditor: ( + content: string, + languageId: string, + ) => Promise; + scopeProvider: ScopeProvider; +} + +export async function createTestEnvironment(): Promise { const ide = new FakeIDE(new TestMessages()); const fileSystem = new TestFileSystem(ide.runMode, "testCursorlessDir"); From 8d921648911915ff25d8210415bfdeeac711268b Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 19:31:31 +0100 Subject: [PATCH 08/15] Small fixes --- packages/common/package.json | 3 +- packages/common/src/ide/fake/FakeIDE.ts | 2 +- pnpm-lock.yaml | 7 +- typings/treeSitter.d.ts | 1047 +---------------------- 4 files changed, 9 insertions(+), 1050 deletions(-) diff --git a/packages/common/package.json b/packages/common/package.json index b0af122433..2afd5dde83 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -38,6 +38,7 @@ "cross-spawn": "^7.0.6", "fast-check": "^4.5.3", "js-yaml": "^4.1.1", - "mocha": "^11.7.5" + "mocha": "^11.7.5", + "web-tree-sitter": "^0.26.6" } } diff --git a/packages/common/src/ide/fake/FakeIDE.ts b/packages/common/src/ide/fake/FakeIDE.ts index e95d87ab24..f8f41a21b5 100644 --- a/packages/common/src/ide/fake/FakeIDE.ts +++ b/packages/common/src/ide/fake/FakeIDE.ts @@ -93,7 +93,7 @@ export class FakeIDE implements IDE { } get visibleNotebookEditors(): NotebookEditor[] { - throw Error("visibleNotebookEditors: not implemented"); + return []; } public getEditableTextEditor(_editor: TextEditor): EditableTextEditor { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d20f7f25ea..1595c87ead 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,7 +125,7 @@ importers: version: 30.2.0 ts-jest: specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.12.0)(ts-node@10.9.2(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.12.0))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -248,6 +248,9 @@ importers: mocha: specifier: ^11.7.5 version: 11.7.5 + web-tree-sitter: + specifier: ^0.26.6 + version: 0.26.6 packages/cursorless-cheatsheet: dependencies: @@ -22981,7 +22984,7 @@ snapshots: ts-easing@0.2.0: {} - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.12.0)(ts-node@10.9.2(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.12.0))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 diff --git a/typings/treeSitter.d.ts b/typings/treeSitter.d.ts index 27a0289789..7c42b98d83 100644 --- a/typings/treeSitter.d.ts +++ b/typings/treeSitter.d.ts @@ -1,1047 +1,2 @@ -// From https://github.com/tree-sitter/tree-sitter/blob/a380e1a259667c4d78c30a81bd8005c72577629a/lib/binding_web/web-tree-sitter.d.ts -// License https://github.com/tree-sitter/tree-sitter/blob/a380e1a259667c4d78c30a81bd8005c72577629a/LICENSE - +// Necessary because of faulty type in web-tree-sitter type EmscriptenModule = unknown; - -declare module "web-tree-sitter" { - /** - * A position in a multi-line text document, in terms of rows and columns. - * - * Rows and columns are zero-based. - */ - export interface Point { - /** The zero-based row number. */ - row: number; - /** The zero-based column number. */ - column: number; - } - /** - * A range of positions in a multi-line text document, both in terms of bytes - * and of rows and columns. - */ - export interface Range { - /** The start position of the range. */ - startPosition: Point; - /** The end position of the range. */ - endPosition: Point; - /** The start index of the range. */ - startIndex: number; - /** The end index of the range. */ - endIndex: number; - } - /** - * A summary of a change to a text document. - */ - export interface Edit { - /** The start position of the change. */ - startPosition: Point; - /** The end position of the change before the edit. */ - oldEndPosition: Point; - /** The end position of the change after the edit. */ - newEndPosition: Point; - /** The start index of the change. */ - startIndex: number; - /** The end index of the change before the edit. */ - oldEndIndex: number; - /** The end index of the change after the edit. */ - newEndIndex: number; - } - /** - * A callback for parsing that takes an index and point, and should return a string. - */ - export type ParseCallback = ( - index: number, - position: Point, - ) => string | undefined; - /** - * A callback that receives the parse state during parsing. - */ - export type ProgressCallback = (progress: ParseState) => boolean; - /** - * A callback for logging messages. - * - * If `isLex` is `true`, the message is from the lexer, otherwise it's from the parser. - */ - export type LogCallback = (message: string, isLex: boolean) => void; - /** - * Options for parsing - * - * The `includedRanges` property is an array of {@link Range} objects that - * represent the ranges of text that the parser should include when parsing. - * - * The `progressCallback` property is a function that is called periodically - * during parsing to check whether parsing should be cancelled. - * - * See {@link Parser#parse} for more information. - */ - export interface ParseOptions { - /** - * An array of {@link Range} objects that - * represent the ranges of text that the parser should include when parsing. - * - * This sets the ranges of text that the parser should include when parsing. - * By default, the parser will always include entire documents. This - * function allows you to parse only a *portion* of a document but - * still return a syntax tree whose ranges match up with the document - * as a whole. You can also pass multiple disjoint ranges. - * If `ranges` is empty, then the entire document will be parsed. - * Otherwise, the given ranges must be ordered from earliest to latest - * in the document, and they must not overlap. That is, the following - * must hold for all `i` < `length - 1`: - * ```text - * ranges[i].end_byte <= ranges[i + 1].start_byte - * ``` - */ - includedRanges?: Range[]; - /** - * A function that is called periodically during parsing to check - * whether parsing should be cancelled. If the progress callback returns - * `true`, then parsing will be cancelled. You can also use this to instrument - * parsing and check where the parser is at in the document. The progress callback - * takes a single argument, which is a {@link ParseState} representing the current - * state of the parser. - */ - progressCallback?: (state: ParseState) => void; - } - /** - * A stateful object that is passed into the progress callback {@link ParseOptions#progressCallback} - * to provide the current state of the parser. - */ - export interface ParseState { - /** The byte offset in the document that the parser is at. */ - currentOffset: number; - /** Indicates whether the parser has encountered an error during parsing. */ - hasError: boolean; - } - /** - * The latest ABI version that is supported by the current version of the - * library. - * - * When Languages are generated by the Tree-sitter CLI, they are - * assigned an ABI version number that corresponds to the current CLI version. - * The Tree-sitter library is generally backwards-compatible with languages - * generated using older CLI versions, but is not forwards-compatible. - */ - export let LANGUAGE_VERSION: number; - /** - * The earliest ABI version that is supported by the current version of the - * library. - */ - export let MIN_COMPATIBLE_VERSION: number; - /** - * A stateful object that is used to produce a {@link Tree} based on some - * source code. - */ - export class Parser { - /** The parser's current language. */ - language: Language | null; - /** - * This must always be called before creating a Parser. - * - * You can optionally pass in options to configure the WASM module, the most common - * one being `locateFile` to help the module find the `.wasm` file. - */ - static init(moduleOptions?: EmscriptenModule): Promise; - /** - * Create a new parser. - */ - constructor(); - /** Delete the parser, freeing its resources. */ - delete(): void; - /** - * Set the language that the parser should use for parsing. - * - * If the language was not successfully assigned, an error will be thrown. - * This happens if the language was generated with an incompatible - * version of the Tree-sitter CLI. Check the language's version using - * {@link Language#version} and compare it to this library's - * {@link LANGUAGE_VERSION} and {@link MIN_COMPATIBLE_VERSION} constants. - */ - setLanguage(language: Language | null): this; - /** - * Parse a slice of UTF8 text. - * - * @param callback - The UTF8-encoded text to parse or a callback function. - * - * @param oldTree - A previous syntax tree parsed from the same document. If the text of the - * document has changed since `oldTree` was created, then you must edit `oldTree` to match - * the new text using {@link Tree#edit}. - * - * @param options - Options for parsing the text. - * This can be used to set the included ranges, or a progress callback. - * - * @returns A {@link Tree} if parsing succeeded, or `null` if: - * - The parser has not yet had a language assigned with {@link Parser#setLanguage}. - * - The progress callback returned true. - */ - parse( - callback: string | ParseCallback, - oldTree?: Tree | null, - options?: ParseOptions, - ): Tree | null; - /** - * Instruct the parser to start the next parse from the beginning. - * - * If the parser previously failed because of a timeout, cancellation, - * or callback, then by default, it will resume where it left off on the - * next call to {@link Parser#parse} or other parsing functions. - * If you don't want to resume, and instead intend to use this parser to - * parse some other document, you must call `reset` first. - */ - reset(): void; - /** Get the ranges of text that the parser will include when parsing. */ - getIncludedRanges(): Range[]; - /** - * @deprecated since version 0.25.0, prefer passing a progress callback to {@link Parser#parse} - * - * Get the duration in microseconds that parsing is allowed to take. - * - * This is set via {@link Parser#setTimeoutMicros}. - */ - getTimeoutMicros(): number; - /** - * @deprecated since version 0.25.0, prefer passing a progress callback to {@link Parser#parse} - * - * Set the maximum duration in microseconds that parsing should be allowed - * to take before halting. - * - * If parsing takes longer than this, it will halt early, returning `null`. - * See {@link Parser#parse} for more information. - */ - setTimeoutMicros(timeout: number): void; - /** Set the logging callback that a parser should use during parsing. */ - setLogger(callback: LogCallback | boolean | null): this; - /** Get the parser's current logger. */ - getLogger(): LogCallback | null; - } - class LanguageMetadata { - readonly major_version: number; - readonly minor_version: number; - readonly patch_version: number; - } - /** - * An opaque object that defines how to parse a particular language. - * The code for each `Language` is generated by the Tree-sitter CLI. - */ - export class Language { - /** - * A list of all node types in the language. The index of each type in this - * array is its node type id. - */ - types: string[]; - /** - * A list of all field names in the language. The index of each field name in - * this array is its field id. - */ - fields: (string | null)[]; - /** - * Gets the name of the language. - */ - get name(): string | null; - /** - * @deprecated since version 0.25.0, use {@link Language#abiVersion} instead - * Gets the version of the language. - */ - get version(): number; - /** - * Gets the ABI version of the language. - */ - get abiVersion(): number; - /** - * Get the metadata for this language. This information is generated by the - * CLI, and relies on the language author providing the correct metadata in - * the language's `tree-sitter.json` file. - */ - get metadata(): LanguageMetadata | null; - /** - * Gets the number of fields in the language. - */ - get fieldCount(): number; - /** - * Gets the number of states in the language. - */ - get stateCount(): number; - /** - * Get the field id for a field name. - */ - fieldIdForName(fieldName: string): number | null; - /** - * Get the field name for a field id. - */ - fieldNameForId(fieldId: number): string | null; - /** - * Get the node type id for a node type name. - */ - idForNodeType(type: string, named: boolean): number | null; - /** - * Gets the number of node types in the language. - */ - get nodeTypeCount(): number; - /** - * Get the node type name for a node type id. - */ - nodeTypeForId(typeId: number): string | null; - /** - * Check if a node type is named. - * - * @see {@link https://tree-sitter.github.io/tree-sitter/using-parsers/2-basic-parsing.html#named-vs-anonymous-nodes} - */ - nodeTypeIsNamed(typeId: number): boolean; - /** - * Check if a node type is visible. - */ - nodeTypeIsVisible(typeId: number): boolean; - /** - * Get the supertypes ids of this language. - * - * @see {@link https://tree-sitter.github.io/tree-sitter/using-parsers/6-static-node-types.html?highlight=supertype#supertype-nodes} - */ - get supertypes(): number[]; - /** - * Get the subtype ids for a given supertype node id. - */ - subtypes(supertype: number): number[]; - /** - * Get the next state id for a given state id and node type id. - */ - nextState(stateId: number, typeId: number): number; - /** - * Create a new lookahead iterator for this language and parse state. - * - * This returns `null` if state is invalid for this language. - * - * Iterating {@link LookaheadIterator} will yield valid symbols in the given - * parse state. Newly created lookahead iterators will return the `ERROR` - * symbol from {@link LookaheadIterator#currentType}. - * - * Lookahead iterators can be useful for generating suggestions and improving - * syntax error diagnostics. To get symbols valid in an `ERROR` node, use the - * lookahead iterator on its first leaf node state. For `MISSING` nodes, a - * lookahead iterator created on the previous non-extra leaf node may be - * appropriate. - */ - lookaheadIterator(stateId: number): LookaheadIterator | null; - /** - * @deprecated since version 0.25.0, call `new` on a {@link Query} instead - * - * Create a new query from a string containing one or more S-expression - * patterns. - * - * The query is associated with a particular language, and can only be run - * on syntax nodes parsed with that language. References to Queries can be - * shared between multiple threads. - * - * @link {@see https://tree-sitter.github.io/tree-sitter/using-parsers/queries} - */ - query(source: string): Query; - /** - * Load a language from a WebAssembly module. - * The module can be provided as a path to a file or as a buffer. - */ - static load(input: string | Uint8Array): Promise; - } - /** A tree that represents the syntactic structure of a source code file. */ - export class Tree { - /** The language that was used to parse the syntax tree. */ - language: Language; - /** Create a shallow copy of the syntax tree. This is very fast. */ - copy(): Tree; - /** Delete the syntax tree, freeing its resources. */ - delete(): void; - /** Get the root node of the syntax tree. */ - get rootNode(): Node; - /** - * Get the root node of the syntax tree, but with its position shifted - * forward by the given offset. - */ - rootNodeWithOffset(offsetBytes: number, offsetExtent: Point): Node; - /** - * Edit the syntax tree to keep it in sync with source code that has been - * edited. - * - * You must describe the edit both in terms of byte offsets and in terms of - * row/column coordinates. - */ - edit(edit: Edit): void; - /** Create a new {@link TreeCursor} starting from the root of the tree. */ - walk(): TreeCursor; - /** - * Compare this old edited syntax tree to a new syntax tree representing - * the same document, returning a sequence of ranges whose syntactic - * structure has changed. - * - * For this to work correctly, this syntax tree must have been edited such - * that its ranges match up to the new tree. Generally, you'll want to - * call this method right after calling one of the [`Parser::parse`] - * functions. Call it on the old tree that was passed to parse, and - * pass the new tree that was returned from `parse`. - */ - getChangedRanges(other: Tree): Range[]; - /** Get the included ranges that were used to parse the syntax tree. */ - getIncludedRanges(): Range[]; - } - /** A single node within a syntax {@link Tree}. */ - export class Node { - /** - * The numeric id for this node that is unique. - * - * Within a given syntax tree, no two nodes have the same id. However: - * - * * If a new tree is created based on an older tree, and a node from the old tree is reused in - * the process, then that node will have the same id in both trees. - * - * * A node not marked as having changes does not guarantee it was reused. - * - * * If a node is marked as having changed in the old tree, it will not be reused. - */ - id: number; - /** The byte index where this node starts. */ - startIndex: number; - /** The position where this node starts. */ - startPosition: Point; - /** The tree that this node belongs to. */ - tree: Tree; - /** Get this node's type as a numerical id. */ - get typeId(): number; - /** - * Get the node's type as a numerical id as it appears in the grammar, - * ignoring aliases. - */ - get grammarId(): number; - /** Get this node's type as a string. */ - get type(): string; - /** - * Get this node's symbol name as it appears in the grammar, ignoring - * aliases as a string. - */ - get grammarType(): string; - /** - * Check if this node is *named*. - * - * Named nodes correspond to named rules in the grammar, whereas - * *anonymous* nodes correspond to string literals in the grammar. - */ - get isNamed(): boolean; - /** - * Check if this node is *extra*. - * - * Extra nodes represent things like comments, which are not required - * by the grammar, but can appear anywhere. - */ - get isExtra(): boolean; - /** - * Check if this node represents a syntax error. - * - * Syntax errors represent parts of the code that could not be incorporated - * into a valid syntax tree. - */ - get isError(): boolean; - /** - * Check if this node is *missing*. - * - * Missing nodes are inserted by the parser in order to recover from - * certain kinds of syntax errors. - */ - get isMissing(): boolean; - /** Check if this node has been edited. */ - get hasChanges(): boolean; - /** - * Check if this node represents a syntax error or contains any syntax - * errors anywhere within it. - */ - get hasError(): boolean; - /** Get the byte index where this node ends. */ - get endIndex(): number; - /** Get the position where this node ends. */ - get endPosition(): Point; - /** Get the string content of this node. */ - get text(): string; - /** Get this node's parse state. */ - get parseState(): number; - /** Get the parse state after this node. */ - get nextParseState(): number; - /** Check if this node is equal to another node. */ - equals(other: Node): boolean; - /** - * Get the node's child at the given index, where zero represents the first child. - * - * This method is fairly fast, but its cost is technically log(n), so if - * you might be iterating over a long list of children, you should use - * {@link Node#children} instead. - */ - child(index: number): Node | null; - /** - * Get this node's *named* child at the given index. - * - * See also {@link Node#isNamed}. - * This method is fairly fast, but its cost is technically log(n), so if - * you might be iterating over a long list of children, you should use - * {@link Node#namedChildren} instead. - */ - namedChild(index: number): Node | null; - /** - * Get this node's child with the given numerical field id. - * - * See also {@link Node#childForFieldName}. You can - * convert a field name to an id using {@link Language#fieldIdForName}. - */ - childForFieldId(fieldId: number): Node | null; - /** - * Get the first child with the given field name. - * - * If multiple children may have the same field name, access them using - * {@link Node#childrenForFieldName}. - */ - childForFieldName(fieldName: string): Node | null; - /** Get the field name of this node's child at the given index. */ - fieldNameForChild(index: number): string | null; - /** Get the field name of this node's named child at the given index. */ - fieldNameForNamedChild(index: number): string | null; - /** - * Get an array of this node's children with a given field name. - * - * See also {@link Node#children}. - */ - childrenForFieldName(fieldName: string): Node[]; - /** - * Get an array of this node's children with a given field id. - * - * See also {@link Node#childrenForFieldName}. - */ - childrenForFieldId(fieldId: number): Node[]; - /** Get the node's first child that contains or starts after the given byte offset. */ - firstChildForIndex(index: number): Node | null; - /** Get the node's first named child that contains or starts after the given byte offset. */ - firstNamedChildForIndex(index: number): Node | null; - /** Get this node's number of children. */ - get childCount(): number; - /** - * Get this node's number of *named* children. - * - * See also {@link Node#isNamed}. - */ - get namedChildCount(): number; - /** Get this node's first child. */ - get firstChild(): Node | null; - /** - * Get this node's first named child. - * - * See also {@link Node#isNamed}. - */ - get firstNamedChild(): Node | null; - /** Get this node's last child. */ - get lastChild(): Node | null; - /** - * Get this node's last named child. - * - * See also {@link Node#isNamed}. - */ - get lastNamedChild(): Node | null; - /** - * Iterate over this node's children. - * - * If you're walking the tree recursively, you may want to use the - * {@link TreeCursor} APIs directly instead. - */ - get children(): Node[]; - /** - * Iterate over this node's named children. - * - * See also {@link Node#children}. - */ - get namedChildren(): Node[]; - /** - * Get the descendants of this node that are the given type, or in the given types array. - * - * The types array should contain node type strings, which can be retrieved from {@link Language#types}. - * - * Additionally, a `startPosition` and `endPosition` can be passed in to restrict the search to a byte range. - */ - descendantsOfType( - types: string | string[], - startPosition?: Point, - endPosition?: Point, - ): Node[]; - /** Get this node's next sibling. */ - get nextSibling(): Node | null; - /** Get this node's previous sibling. */ - get previousSibling(): Node | null; - /** - * Get this node's next *named* sibling. - * - * See also {@link Node#isNamed}. - */ - get nextNamedSibling(): Node | null; - /** - * Get this node's previous *named* sibling. - * - * See also {@link Node#isNamed}. - */ - get previousNamedSibling(): Node | null; - /** Get the node's number of descendants, including one for the node itself. */ - get descendantCount(): number; - /** - * Get this node's immediate parent. - * Prefer {@link Node#childWithDescendant} for iterating over this node's ancestors. - */ - get parent(): Node | null; - /** - * Get the node that contains `descendant`. - * - * Note that this can return `descendant` itself. - */ - childWithDescendant(descendant: Node): Node | null; - /** Get the smallest node within this node that spans the given byte range. */ - descendantForIndex(start: number, end?: number): Node | null; - /** Get the smallest named node within this node that spans the given byte range. */ - namedDescendantForIndex(start: number, end?: number): Node | null; - /** Get the smallest node within this node that spans the given point range. */ - descendantForPosition(start: Point, end?: Point): Node | null; - /** Get the smallest named node within this node that spans the given point range. */ - namedDescendantForPosition(start: Point, end?: Point): Node | null; - /** - * Create a new {@link TreeCursor} starting from this node. - * - * Note that the given node is considered the root of the cursor, - * and the cursor cannot walk outside this node. - */ - walk(): TreeCursor; - /** - * Edit this node to keep it in-sync with source code that has been edited. - * - * This function is only rarely needed. When you edit a syntax tree with - * the {@link Tree#edit} method, all of the nodes that you retrieve from - * the tree afterward will already reflect the edit. You only need to - * use {@link Node#edit} when you have a specific {@link Node} instance that - * you want to keep and continue to use after an edit. - */ - edit(edit: Edit): void; - /** Get the S-expression representation of this node. */ - toString(): string; - } - /** A stateful object for walking a syntax {@link Tree} efficiently. */ - export class TreeCursor { - /** Creates a deep copy of the tree cursor. This allocates new memory. */ - copy(): TreeCursor; - /** Delete the tree cursor, freeing its resources. */ - delete(): void; - /** Get the tree cursor's current {@link Node}. */ - get currentNode(): Node; - /** - * Get the numerical field id of this tree cursor's current node. - * - * See also {@link TreeCursor#currentFieldName}. - */ - get currentFieldId(): number; - /** Get the field name of this tree cursor's current node. */ - get currentFieldName(): string | null; - /** - * Get the depth of the cursor's current node relative to the original - * node that the cursor was constructed with. - */ - get currentDepth(): number; - /** - * Get the index of the cursor's current node out of all of the - * descendants of the original node that the cursor was constructed with. - */ - get currentDescendantIndex(): number; - /** Get the type of the cursor's current node. */ - get nodeType(): string; - /** Get the type id of the cursor's current node. */ - get nodeTypeId(): number; - /** Get the state id of the cursor's current node. */ - get nodeStateId(): number; - /** Get the id of the cursor's current node. */ - get nodeId(): number; - /** - * Check if the cursor's current node is *named*. - * - * Named nodes correspond to named rules in the grammar, whereas - * *anonymous* nodes correspond to string literals in the grammar. - */ - get nodeIsNamed(): boolean; - /** - * Check if the cursor's current node is *missing*. - * - * Missing nodes are inserted by the parser in order to recover from - * certain kinds of syntax errors. - */ - get nodeIsMissing(): boolean; - /** Get the string content of the cursor's current node. */ - get nodeText(): string; - /** Get the start position of the cursor's current node. */ - get startPosition(): Point; - /** Get the end position of the cursor's current node. */ - get endPosition(): Point; - /** Get the start index of the cursor's current node. */ - get startIndex(): number; - /** Get the end index of the cursor's current node. */ - get endIndex(): number; - /** - * Move this cursor to the first child of its current node. - * - * This returns `true` if the cursor successfully moved, and returns - * `false` if there were no children. - */ - gotoFirstChild(): boolean; - /** - * Move this cursor to the last child of its current node. - * - * This returns `true` if the cursor successfully moved, and returns - * `false` if there were no children. - * - * Note that this function may be slower than - * {@link TreeCursor#gotoFirstChild} because it needs to - * iterate through all the children to compute the child's position. - */ - gotoLastChild(): boolean; - /** - * Move this cursor to the parent of its current node. - * - * This returns `true` if the cursor successfully moved, and returns - * `false` if there was no parent node (the cursor was already on the - * root node). - * - * Note that the node the cursor was constructed with is considered the root - * of the cursor, and the cursor cannot walk outside this node. - */ - gotoParent(): boolean; - /** - * Move this cursor to the next sibling of its current node. - * - * This returns `true` if the cursor successfully moved, and returns - * `false` if there was no next sibling node. - * - * Note that the node the cursor was constructed with is considered the root - * of the cursor, and the cursor cannot walk outside this node. - */ - gotoNextSibling(): boolean; - /** - * Move this cursor to the previous sibling of its current node. - * - * This returns `true` if the cursor successfully moved, and returns - * `false` if there was no previous sibling node. - * - * Note that this function may be slower than - * {@link TreeCursor#gotoNextSibling} due to how node - * positions are stored. In the worst case, this will need to iterate - * through all the children up to the previous sibling node to recalculate - * its position. Also note that the node the cursor was constructed with is - * considered the root of the cursor, and the cursor cannot walk outside this node. - */ - gotoPreviousSibling(): boolean; - /** - * Move the cursor to the node that is the nth descendant of - * the original node that the cursor was constructed with, where - * zero represents the original node itself. - */ - gotoDescendant(goalDescendantIndex: number): void; - /** - * Move this cursor to the first child of its current node that contains or - * starts after the given byte offset. - * - * This returns `true` if the cursor successfully moved to a child node, and returns - * `false` if no such child was found. - */ - gotoFirstChildForIndex(goalIndex: number): boolean; - /** - * Move this cursor to the first child of its current node that contains or - * starts after the given byte offset. - * - * This returns the index of the child node if one was found, and returns - * `null` if no such child was found. - */ - gotoFirstChildForPosition(goalPosition: Point): boolean; - /** - * Re-initialize this tree cursor to start at the original node that the - * cursor was constructed with. - */ - reset(node: Node): void; - /** - * Re-initialize a tree cursor to the same position as another cursor. - * - * Unlike {@link TreeCursor#reset}, this will not lose parent - * information and allows reusing already created cursors. - */ - resetTo(cursor: TreeCursor): void; - } - /** - * Options for query execution - */ - export interface QueryOptions { - /** The start position of the range to query */ - startPosition?: Point; - /** The end position of the range to query */ - endPosition?: Point; - /** The start index of the range to query */ - startIndex?: number; - /** The end index of the range to query */ - endIndex?: number; - /** - * The maximum number of in-progress matches for this query. - * The limit must be > 0 and <= 65536. - */ - matchLimit?: number; - /** - * The maximum start depth for a query cursor. - * - * This prevents cursors from exploring children nodes at a certain depth. - * Note if a pattern includes many children, then they will still be - * checked. - * - * The zero max start depth value can be used as a special behavior and - * it helps to destructure a subtree by staying on a node and using - * captures for interested parts. Note that the zero max start depth - * only limit a search depth for a pattern's root node but other nodes - * that are parts of the pattern may be searched at any depth what - * defined by the pattern structure. - * - * Set to `null` to remove the maximum start depth. - */ - maxStartDepth?: number; - /** - * The maximum duration in microseconds that query execution should be allowed to - * take before halting. - * - * If query execution takes longer than this, it will halt early, returning an empty array. - */ - timeoutMicros?: number; - /** - * A function that will be called periodically during the execution of the query to check - * if query execution should be cancelled. You can also use this to instrument query execution - * and check where the query is at in the document. The progress callback takes a single argument, - * which is a {@link QueryState} representing the current state of the query. - */ - progressCallback?: (state: QueryState) => void; - } - /** - * A stateful object that is passed into the progress callback {@link QueryOptions#progressCallback} - * to provide the current state of the query. - */ - export interface QueryState { - /** The byte offset in the document that the query is at. */ - currentOffset: number; - } - /** A record of key-value pairs associated with a particular pattern in a {@link Query}. */ - export type QueryProperties = Record; - /** - * A predicate that contains an operator and list of operands. - */ - export interface QueryPredicate { - /** The operator of the predicate, like `match?`, `eq?`, `set!`, etc. */ - operator: string; - /** The operands of the predicate, which are either captures or strings. */ - operands: PredicateStep[]; - } - /** - * A particular {@link Node} that has been captured with a particular name within a - * {@link Query}. - */ - export interface QueryCapture { - /** The index of the pattern that matched. */ - patternIndex: number; - /** The name of the capture */ - name: string; - /** The captured node */ - node: Node; - /** The properties for predicates declared with the operator `set!`. */ - setProperties?: QueryProperties; - /** The properties for predicates declared with the operator `is?`. */ - assertedProperties?: QueryProperties; - /** The properties for predicates declared with the operator `is-not?`. */ - refutedProperties?: QueryProperties; - } - /** A match of a {@link Query} to a particular set of {@link Node}s. */ - export interface QueryMatch { - /** @deprecated since version 0.25.0, use `patternIndex` instead. */ - pattern: number; - /** The index of the pattern that matched. */ - patternIndex: number; - /** The captures associated with the match. */ - captures: QueryCapture[]; - /** The properties for predicates declared with the operator `set!`. */ - setProperties?: QueryProperties; - /** The properties for predicates declared with the operator `is?`. */ - assertedProperties?: QueryProperties; - /** The properties for predicates declared with the operator `is-not?`. */ - refutedProperties?: QueryProperties; - } - /** A quantifier for captures */ - export const CaptureQuantifier: { - readonly Zero: 0; - readonly ZeroOrOne: 1; - readonly ZeroOrMore: 2; - readonly One: 3; - readonly OneOrMore: 4; - }; - /** A quantifier for captures */ - export type CaptureQuantifier = - (typeof CaptureQuantifier)[keyof typeof CaptureQuantifier]; - /** - * Predicates are represented as a single array of steps. There are two - * types of steps, which correspond to the two legal values for - * the `type` field: - * - * - `CapturePredicateStep` - Steps with this type represent names - * of captures. - * - * - `StringPredicateStep` - Steps with this type represent literal - * strings. - */ - export type PredicateStep = CapturePredicateStep | StringPredicateStep; - /** - * A step in a predicate that refers to a capture. - * - * The `name` field is the name of the capture. - */ - interface CapturePredicateStep { - type: "capture"; - name: string; - } - /** - * A step in a predicate that refers to a string. - * - * The `value` field is the string value. - */ - interface StringPredicateStep { - type: "string"; - value: string; - } - export class Query { - /** The names of the captures used in the query. */ - readonly captureNames: string[]; - /** The quantifiers of the captures used in the query. */ - readonly captureQuantifiers: CaptureQuantifier[][]; - /** - * The other user-defined predicates associated with the given index. - * - * This includes predicates with operators other than: - * - `match?` - * - `eq?` and `not-eq?` - * - `any-of?` and `not-any-of?` - * - `is?` and `is-not?` - * - `set!` - */ - readonly predicates: QueryPredicate[][]; - /** The properties for predicates with the operator `set!`. */ - readonly setProperties: QueryProperties[]; - /** The properties for predicates with the operator `is?`. */ - readonly assertedProperties: QueryProperties[]; - /** The properties for predicates with the operator `is-not?`. */ - readonly refutedProperties: QueryProperties[]; - /** The maximum number of in-progress matches for this cursor. */ - matchLimit?: number; - /** - * Create a new query from a string containing one or more S-expression - * patterns. - * - * The query is associated with a particular language, and can only be run - * on syntax nodes parsed with that language. References to Queries can be - * shared between multiple threads. - * - * @link {@see https://tree-sitter.github.io/tree-sitter/using-parsers/queries} - */ - constructor(language: Language, source: string); - /** Delete the query, freeing its resources. */ - delete(): void; - /** - * Iterate over all of the matches in the order that they were found. - * - * Each match contains the index of the pattern that matched, and a list of - * captures. Because multiple patterns can match the same set of nodes, - * one match may contain captures that appear *before* some of the - * captures from a previous match. - * - * @param node - The node to execute the query on. - * - * @param options - Options for query execution. - */ - matches(node: Node, options?: QueryOptions): QueryMatch[]; - /** - * Iterate over all of the individual captures in the order that they - * appear. - * - * This is useful if you don't care about which pattern matched, and just - * want a single, ordered sequence of captures. - * - * @param node - The node to execute the query on. - * - * @param options - Options for query execution. - */ - captures(node: Node, options?: QueryOptions): QueryCapture[]; - /** Get the predicates for a given pattern. */ - predicatesForPattern(patternIndex: number): QueryPredicate[]; - /** - * Disable a certain capture within a query. - * - * This prevents the capture from being returned in matches, and also - * avoids any resource usage associated with recording the capture. - */ - disableCapture(captureName: string): void; - /** - * Disable a certain pattern within a query. - * - * This prevents the pattern from matching, and also avoids any resource - * usage associated with the pattern. This throws an error if the pattern - * index is out of bounds. - */ - disablePattern(patternIndex: number): void; - /** - * Check if, on its last execution, this cursor exceeded its maximum number - * of in-progress matches. - */ - didExceedMatchLimit(): boolean; - /** Get the byte offset where the given pattern starts in the query's source. */ - startIndexForPattern(patternIndex: number): number; - /** Get the byte offset where the given pattern ends in the query's source. */ - endIndexForPattern(patternIndex: number): number; - /** Get the number of patterns in the query. */ - patternCount(): number; - /** Get the index for a given capture name. */ - captureIndexForName(captureName: string): number; - /** Check if a given pattern within a query has a single root node. */ - isPatternRooted(patternIndex: number): boolean; - /** Check if a given pattern within a query has a single root node. */ - isPatternNonLocal(patternIndex: number): boolean; - /** - * Check if a given step in a query is 'definite'. - * - * A query step is 'definite' if its parent pattern will be guaranteed to - * match successfully once it reaches the step. - */ - isPatternGuaranteedAtStep(byteIndex: number): boolean; - } - export class LookaheadIterator implements Iterable { - /** Get the current symbol of the lookahead iterator. */ - get currentTypeId(): number; - /** Get the current symbol name of the lookahead iterator. */ - get currentType(): string; - /** Delete the lookahead iterator, freeing its resources. */ - delete(): void; - /** - * Reset the lookahead iterator. - * - * This returns `true` if the language was set successfully and `false` - * otherwise. - */ - reset(language: Language, stateId: number): boolean; - /** - * Reset the lookahead iterator to another state. - * - * This returns `true` if the iterator was reset to the given state and - * `false` otherwise. - */ - resetState(stateId: number): boolean; - /** - * Returns an iterator that iterates over the symbols of the lookahead iterator. - * - * The iterator will yield the current symbol name as a string for each step - * until there are no more symbols to iterate over. - */ - [Symbol.iterator](): Iterator; - } - - export {}; -} - -//# sourceMappingURL=web-tree-sitter.d.ts.map From 65b1a7e123ec6a2923d41ffeed90ac3336749936 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 20:18:47 +0100 Subject: [PATCH 09/15] Update test workflow --- .github/workflows/test.yml | 15 ++++++-- .vscode/launch.json | 35 ------------------- package.json | 2 +- .../neovim-test-infrastructure.md | 6 ++-- .../src/docs/contributing/tests.md | 11 ++++-- packages/test-harness/package.json | 6 ++-- packages/test-harness/src/runAllTests.ts | 12 +++---- .../src/runners/extensionTestsNeovim.ts | 2 +- .../src/runners/extensionTestsVscode.ts | 2 +- .../src/scripts/runNeovimTestsCI.ts | 2 -- .../src/scripts/runVscodeTestsCI.ts | 2 -- 11 files changed, 36 insertions(+), 59 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e87cb85c3..80e6ba1b78 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,14 +62,25 @@ jobs: - name: Build run: pnpm --color --filter '!cursorless-org' --filter '!cursorless-org-*' build - - name: Run tests (Linux) + - name: Lint + run: pnpm --color lint + + - name: Run unit tests (Linux) run: xvfb-run -a pnpm --color test if: runner.os == 'Linux' - - name: Run tests (Win,Mac) + - name: Run unit tests (Win,Mac) run: pnpm --color test if: runner.os != 'Linux' + - name: Run VSCode tests (Linux) + run: xvfb-run -a pnpm -F @cursorless/test-harness test:vscode + if: runner.os == 'Linux' + + - name: Run VSCode tests (Win,Mac) + run: pnpm -F @cursorless/test-harness test:vscode + if: runner.os != 'Linux' + - name: Run Talon-JS tests (Linux) run: xvfb-run -a pnpm -F @cursorless/test-harness test:talonJs if: runner.os == 'Linux' && matrix.app_version == 'stable' diff --git a/.vscode/launch.json b/.vscode/launch.json index a7a87c13da..5c80886a82 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -202,41 +202,6 @@ ] }, - // Unit tests launch configs - { - "name": "Unit tests: Test", - "type": "node", - "request": "launch", - "program": "${workspaceFolder}/packages/test-harness/dist/runUnitTestsOnly.cjs", - "env": { - "CURSORLESS_MODE": "test", - "CURSORLESS_REPO_ROOT": "${workspaceFolder}" - }, - "outFiles": ["${workspaceFolder}/**/out/**/*.js"], - "preLaunchTask": "VSCode: Build extension and tests", - "resolveSourceMapLocations": [ - "${workspaceFolder}/**", - "!**/node_modules/**" - ] - }, - { - "name": "Unit tests: Update test fixtures", - "type": "node", - "request": "launch", - "program": "${workspaceFolder}/packages/test-harness/dist/runUnitTestsOnly.cjs", - "env": { - "CURSORLESS_MODE": "test", - "CURSORLESS_TEST_UPDATE_FIXTURES": "true", - "CURSORLESS_REPO_ROOT": "${workspaceFolder}" - }, - "outFiles": ["${workspaceFolder}/**/out/**/*.js"], - "preLaunchTask": "VSCode: Build extension and tests", - "resolveSourceMapLocations": [ - "${workspaceFolder}/**", - "!**/node_modules/**" - ] - }, - // Docusaurus launch configs { "name": "Docusaurus: Run", diff --git a/package.json b/package.json index 847066cb79..e20ccf5f74 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "meta-updater:base": "pnpm --filter=@cursorless/meta-updater build && meta-updater", "preinstall": "npx only-allow pnpm", "test-compile": "tsc --build", - "test": "pnpm compile && pnpm lint && pnpm -F '!test-harness' test && pnpm -F test-harness test", + "test": "pnpm -r test", "generate-grammar": "pnpm -r generate-grammar", "transform-recorded-tests": "./packages/common/scripts/my-ts-node.js packages/cursorless-engine/src/scripts/transformRecordedTests/index.ts", "watch": "pnpm run -w --parallel '/^watch:.*/'", diff --git a/packages/cursorless-org-docs/src/docs/contributing/architecture/neovim-test-infrastructure.md b/packages/cursorless-org-docs/src/docs/contributing/architecture/neovim-test-infrastructure.md index 213e54cf56..c528ae2a9f 100644 --- a/packages/cursorless-org-docs/src/docs/contributing/architecture/neovim-test-infrastructure.md +++ b/packages/cursorless-org-docs/src/docs/contributing/architecture/neovim-test-infrastructure.md @@ -174,13 +174,13 @@ export default function entry(plugin: NvimPlugin) { export async function run(plugin: NvimPlugin): Promise { ... - await runAllTests(TestType.neovim, TestType.unit); + await runAllTests(TestType.neovim); console.log(`==== TESTS FINISHED: code: ${code}`); ``` This ends up calling `runAllTests()` which calls `runTestsInDir()` from `packages/test-harness/src/runAllTests.ts`. -This ends up using the [Mocha API](https://mochajs.org/) to execute tests which names end with `neovim.test.cjs` (Cursorless tests for neovim) and `test.cjs` (Cursorless unit tests): +This ends up using the [Mocha API](https://mochajs.org/) to execute tests whose names end with `neovim.test.cjs`: ```ts async function runTestsInDir( @@ -222,8 +222,6 @@ This ends up calling the default function from `package/test-harness/src/scripts ```ts (async () => { - // Note that we run all extension tests, including unit tests, in neovim, even though - // unit tests could be run separately. await launchNeovimAndRunTests(); })(); ``` diff --git a/packages/cursorless-org-docs/src/docs/contributing/tests.md b/packages/cursorless-org-docs/src/docs/contributing/tests.md index 571445a763..f137fd3dec 100644 --- a/packages/cursorless-org-docs/src/docs/contributing/tests.md +++ b/packages/cursorless-org-docs/src/docs/contributing/tests.md @@ -10,9 +10,16 @@ Our tests fall broadly into three categories: We run the above tests in various contexts, both locally and in CI. The contexts are: -- **VSCode**: Today, many of our tests must run within a VSCode context. For some of our tests, this is desirable, because they are designed to test that our code works in VSCode. However, many of our tests (such as scope tests and recorded tests) are not really VSCode-specific, but we haven't yet built the machinery to run them in a more isolated context, which would be much faster. +- **VSCode**: Today, many of our tests must run within a VSCode context. For some of our tests, this is desirable, because they are designed to test that our code works in VSCode. However, many of our tests (such as scope tests and recorded tests) are not really VSCode-specific, but we haven't yet built the machinery to run them in a more isolated context, which would be much faster. These tests are run separately from the default unit test pass. - **Unit tests**: Many of our tests can run in a neutral context, without requiring an actual IDE with editors, etc. Most of these are unit tests in the traditional sense of the word, testing the logic of a small unit of code, such as a function. - **Talon**: For each of our recorded tests, we test that saying the spoken form of the command in Talon results in the command payload that we expect. Note that these tests can only be run locally today. -- **Neovim**: We run a subset of our recorded tests within Neovim to ensure that the given subset of Cursorless works within Neovim. We also have a few lua unit tests that must be run in Neovim. These test the lua functions that Cursorless needs in order to interact with Neovim. To learn more about our Neovim test infrastructure, see [Neovim test infrastructure](./architecture/neovim-test-infrastructure.md). +- **Neovim**: We run a subset of our recorded tests within Neovim to ensure that the given subset of Cursorless works within Neovim. We also have a few lua unit tests that must be run in Neovim. These test the lua functions that Cursorless needs in order to interact with Neovim. Neovim tests no longer include the general unit test suite. To learn more about our Neovim test infrastructure, see [Neovim test infrastructure](./architecture/neovim-test-infrastructure.md). You can get an overview of the various test contexts that exist locally by looking at our VSCode launch configs, which include not only our VSCode tests, but all of our tests. + +## Common commands + +- `pnpm test` runs the default `test` script for each workspace package. In practice, this is the fast unit-oriented test pass. +- `pnpm -F @cursorless/test-harness test:vscode` runs the VSCode test harness. +- `pnpm -F @cursorless/test-harness test:neovim` runs the Neovim test harness. +- `pnpm -F @cursorless/test-harness test:talonJs` runs the Talon-JS test harness. diff --git a/packages/test-harness/package.json b/packages/test-harness/package.json index cf332cc401..f91d967a96 100644 --- a/packages/test-harness/package.json +++ b/packages/test-harness/package.json @@ -13,11 +13,11 @@ } }, "scripts": { - "test": "env CURSORLESS_MODE=test my-ts-node src/scripts/runVscodeTestsCI.ts", + "test": "env CURSORLESS_MODE=test my-ts-node src/scripts/runUnitTestsOnly.ts", + "test:subset": "env CURSORLESS_MODE=test env CURSORLESS_RUN_TEST_SUBSET=true my-ts-node src/scripts/runUnitTestsOnly.ts", + "test:vscode": "env CURSORLESS_MODE=test my-ts-node src/scripts/runVscodeTestsCI.ts", "test:neovim": "env CURSORLESS_MODE=test my-ts-node src/scripts/runNeovimTestsCI.ts", "test:talonJs": "env CURSORLESS_MODE=test my-ts-node src/scripts/runTalonJsTests.ts", - "test:unit": "env CURSORLESS_MODE=test my-ts-node src/scripts/runUnitTestsOnly.ts", - "test:unit:subset": "env CURSORLESS_MODE=test env CURSORLESS_RUN_TEST_SUBSET=true my-ts-node src/scripts/runUnitTestsOnly.ts", "build:base": "esbuild --sourcemap --conditions=cursorless:bundler --bundle --external:vscode --external:./reporters/parallel-buffered --external:./worker.js --external:talon --format=cjs --platform=node", "build": "pnpm run build:runner:vscode && pnpm run build:runner:neovim && pnpm run build:tests && pnpm run build:unit && pnpm run build:talon && pnpm run build:talonJs", "build:runner:vscode": "pnpm run build:base ./src/runners/extensionTestsVscode.ts --outfile=dist/extensionTestsVscode.cjs", diff --git a/packages/test-harness/src/runAllTests.ts b/packages/test-harness/src/runAllTests.ts index 82b31ccf3b..dba53163a6 100644 --- a/packages/test-harness/src/runAllTests.ts +++ b/packages/test-harness/src/runAllTests.ts @@ -29,28 +29,28 @@ export enum TestType { neovim, } -export function runAllTests(...types: TestType[]): Promise { +export function runAllTests(type: TestType): Promise { return runTestsInDir( path.join(getCursorlessRepoRoot(), "packages"), (files) => files.filter((f) => { if (f.endsWith("neovim.test.cjs")) { - return types.includes(TestType.neovim); + return type === TestType.neovim; } if (f.endsWith("vscode.test.cjs")) { - return types.includes(TestType.vscode); + return type === TestType.vscode; } if (f.endsWith("talon.test.cjs")) { - return types.includes(TestType.talon); + return type === TestType.talon; } if (f.endsWith("talonjs.test.cjs")) { - return types.includes(TestType.talonJs); + return type === TestType.talonJs; } - return types.includes(TestType.unit); + return type === TestType.unit; }), ); } diff --git a/packages/test-harness/src/runners/extensionTestsNeovim.ts b/packages/test-harness/src/runners/extensionTestsNeovim.ts index 7d0f6e105c..58c95a5698 100644 --- a/packages/test-harness/src/runners/extensionTestsNeovim.ts +++ b/packages/test-harness/src/runners/extensionTestsNeovim.ts @@ -21,7 +21,7 @@ export async function run(plugin: NvimPlugin): Promise { let code = 0; // NOTE: the parsing of the logs below is only done on CI in order to detect success/failure try { - await runAllTests(TestType.neovim, TestType.unit); + await runAllTests(TestType.neovim); console.log(`==== TESTS FINISHED: code: ${code}`); } catch (error) { console.log(`==== TESTS ERROR:`); diff --git a/packages/test-harness/src/runners/extensionTestsVscode.ts b/packages/test-harness/src/runners/extensionTestsVscode.ts index 090de48a9b..d1e8b89298 100644 --- a/packages/test-harness/src/runners/extensionTestsVscode.ts +++ b/packages/test-harness/src/runners/extensionTestsVscode.ts @@ -8,5 +8,5 @@ import { TestType, runAllTests } from "../runAllTests"; * @returns A promise that resolves when tests have finished running */ export function run(): Promise { - return runAllTests(TestType.vscode, TestType.unit); + return runAllTests(TestType.vscode); } diff --git a/packages/test-harness/src/scripts/runNeovimTestsCI.ts b/packages/test-harness/src/scripts/runNeovimTestsCI.ts index 03533a929d..51f859f8c1 100644 --- a/packages/test-harness/src/scripts/runNeovimTestsCI.ts +++ b/packages/test-harness/src/scripts/runNeovimTestsCI.ts @@ -5,7 +5,5 @@ import { launchNeovimAndRunTests } from "../launchNeovimAndRunTests"; void (async () => { - // Note that we run all extension tests, including unit tests, in neovim, even though - // unit tests could be run separately. await launchNeovimAndRunTests(); })(); diff --git a/packages/test-harness/src/scripts/runVscodeTestsCI.ts b/packages/test-harness/src/scripts/runVscodeTestsCI.ts index f4656dcd0c..53142d53c6 100644 --- a/packages/test-harness/src/scripts/runVscodeTestsCI.ts +++ b/packages/test-harness/src/scripts/runVscodeTestsCI.ts @@ -7,8 +7,6 @@ import * as path from "node:path"; import { launchVscodeAndRunTests } from "../launchVscodeAndRunTests"; void (async () => { - // Note that we run all extension tests, including unit tests, in VSCode, even though - // unit tests could be run separately. const extensionTestsPath = path.resolve( getCursorlessRepoRoot(), "packages/test-harness/dist/extensionTestsVscode.cjs", From 8c00c31e0f4a3a96c3cbd85db9053f59116600c4 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 20:21:23 +0100 Subject: [PATCH 10/15] update contributing docs --- .../cursorless-org-docs/src/docs/contributing/CONTRIBUTING.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cursorless-org-docs/src/docs/contributing/CONTRIBUTING.mdx b/packages/cursorless-org-docs/src/docs/contributing/CONTRIBUTING.mdx index 138dddc06e..86e4676526 100644 --- a/packages/cursorless-org-docs/src/docs/contributing/CONTRIBUTING.mdx +++ b/packages/cursorless-org-docs/src/docs/contributing/CONTRIBUTING.mdx @@ -150,7 +150,7 @@ Run the `workbench.action.debug.selectandstart` command and then select See [test-case-recorder.md](./test-case-recorder.md). -It is also possible to write manual tests. When doing so, we have a convention that any test that must be run within a VSCode context (eg because it imports `"vscode"`), should be placed in a file with the suffix `.vscode.test.ts`. All other tests should end with just `.test.ts`. This allows us to run non-VSCode tests locally outside of VSCode using the `Run unit tests` launch config. These tests run much faster than the full VSCode test suite. +It is also possible to write manual tests. When doing so, we have a convention that any test that must be run within a VSCode context (eg because it imports `"vscode"`), should be placed in a file with the suffix `.vscode.test.ts`. All other tests should end with just `.test.ts`. This allows us to run non-VSCode tests locally outside of VSCode using `pnpm test`. The non-VSCode tests run much faster than the full VSCode test suite. ## Parse tree support From 8e55076d57884dbb6a4c79b52b34dd627ef9c954 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 20:30:28 +0100 Subject: [PATCH 11/15] Update test docs --- packages/cursorless-org-docs/src/docs/contributing/tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cursorless-org-docs/src/docs/contributing/tests.md b/packages/cursorless-org-docs/src/docs/contributing/tests.md index f137fd3dec..323e64af02 100644 --- a/packages/cursorless-org-docs/src/docs/contributing/tests.md +++ b/packages/cursorless-org-docs/src/docs/contributing/tests.md @@ -13,7 +13,7 @@ We run the above tests in various contexts, both locally and in CI. The contexts - **VSCode**: Today, many of our tests must run within a VSCode context. For some of our tests, this is desirable, because they are designed to test that our code works in VSCode. However, many of our tests (such as scope tests and recorded tests) are not really VSCode-specific, but we haven't yet built the machinery to run them in a more isolated context, which would be much faster. These tests are run separately from the default unit test pass. - **Unit tests**: Many of our tests can run in a neutral context, without requiring an actual IDE with editors, etc. Most of these are unit tests in the traditional sense of the word, testing the logic of a small unit of code, such as a function. - **Talon**: For each of our recorded tests, we test that saying the spoken form of the command in Talon results in the command payload that we expect. Note that these tests can only be run locally today. -- **Neovim**: We run a subset of our recorded tests within Neovim to ensure that the given subset of Cursorless works within Neovim. We also have a few lua unit tests that must be run in Neovim. These test the lua functions that Cursorless needs in order to interact with Neovim. Neovim tests no longer include the general unit test suite. To learn more about our Neovim test infrastructure, see [Neovim test infrastructure](./architecture/neovim-test-infrastructure.md). +- **Neovim**: We run a subset of our recorded tests within Neovim to ensure that the given subset of Cursorless works within Neovim. We also have a few lua unit tests that must be run in Neovim. These test the lua functions that Cursorless needs in order to interact with Neovim. To learn more about our Neovim test infrastructure, see [Neovim test infrastructure](./architecture/neovim-test-infrastructure.md). You can get an overview of the various test contexts that exist locally by looking at our VSCode launch configs, which include not only our VSCode tests, but all of our tests. From 203ce25660f3d80fab42f040ce6226c7c4a68cde Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 20:52:05 +0100 Subject: [PATCH 12/15] Better error logging --- packages/test-harness/src/scripts/runUnitTestsOnly.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/test-harness/src/scripts/runUnitTestsOnly.ts b/packages/test-harness/src/scripts/runUnitTestsOnly.ts index 0df79f4109..b5756bd29e 100644 --- a/packages/test-harness/src/scripts/runUnitTestsOnly.ts +++ b/packages/test-harness/src/scripts/runUnitTestsOnly.ts @@ -3,4 +3,7 @@ */ import { TestType, runAllTests } from "../runAllTests"; -void runAllTests(TestType.unit); +runAllTests(TestType.unit).catch((error) => { + console.error(error); + process.exit(1); +}); From 0d9591026057f4668dc86dff6c9266f49865ce78 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 21:09:41 +0100 Subject: [PATCH 13/15] more logging --- packages/test-harness/src/scripts/runUnitTestsOnly.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/test-harness/src/scripts/runUnitTestsOnly.ts b/packages/test-harness/src/scripts/runUnitTestsOnly.ts index b5756bd29e..3842fe9737 100644 --- a/packages/test-harness/src/scripts/runUnitTestsOnly.ts +++ b/packages/test-harness/src/scripts/runUnitTestsOnly.ts @@ -5,5 +5,8 @@ import { TestType, runAllTests } from "../runAllTests"; runAllTests(TestType.unit).catch((error) => { console.error(error); + if (error.stack) { + console.error(error.stack); + } process.exit(1); }); From 8b71fa317009459d09a79fb59255466a3f3a6fa3 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 21:18:53 +0100 Subject: [PATCH 14/15] restore --- packages/test-harness/src/scripts/runUnitTestsOnly.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/test-harness/src/scripts/runUnitTestsOnly.ts b/packages/test-harness/src/scripts/runUnitTestsOnly.ts index 3842fe9737..0df79f4109 100644 --- a/packages/test-harness/src/scripts/runUnitTestsOnly.ts +++ b/packages/test-harness/src/scripts/runUnitTestsOnly.ts @@ -3,10 +3,4 @@ */ import { TestType, runAllTests } from "../runAllTests"; -runAllTests(TestType.unit).catch((error) => { - console.error(error); - if (error.stack) { - console.error(error.stack); - } - process.exit(1); -}); +void runAllTests(TestType.unit); From 14ec07daf7301fff975233c6a55b2a22f071dd78 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 11 Mar 2026 21:23:30 +0100 Subject: [PATCH 15/15] add missing independency --- packages/test-harness/package.json | 3 ++- pnpm-lock.yaml | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/test-harness/package.json b/packages/test-harness/package.json index f91d967a96..840dec35b9 100644 --- a/packages/test-harness/package.json +++ b/packages/test-harness/package.json @@ -48,6 +48,7 @@ "@types/tail": "^2.2.3", "@vscode/test-electron": "^2.5.2", "cross-spawn": "^7.0.6", - "mocha": "^11.7.5" + "mocha": "^11.7.5", + "web-tree-sitter": "^0.26.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1595c87ead..14b0c020ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,7 +125,7 @@ importers: version: 30.2.0 ts-jest: specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.12.0))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.12.0)(ts-node@10.9.2(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1006,6 +1006,9 @@ importers: mocha: specifier: ^11.7.5 version: 11.7.5 + web-tree-sitter: + specifier: ^0.26.6 + version: 0.26.6 packages/vscode-common: dependencies: @@ -22984,7 +22987,7 @@ snapshots: ts-easing@0.2.0: {} - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.12.0))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.12.0)(ts-node@10.9.2(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0