From 5ea6108f291c966992421c22645bcf1a0fe589cd Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 10 Nov 2022 09:58:56 +0100 Subject: [PATCH 1/2] Updated If statement insertion delimiter (#1125) * Updated if statement insertion delimiter * Updated test --- src/processTargets/targets/ScopeTypeTarget.ts | 2 +- .../fixtures/recorded/targets/scopeType/pourIfState.yml | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/processTargets/targets/ScopeTypeTarget.ts b/src/processTargets/targets/ScopeTypeTarget.ts index 7c3f56a6dd..0b488e63c5 100644 --- a/src/processTargets/targets/ScopeTypeTarget.ts +++ b/src/processTargets/targets/ScopeTypeTarget.ts @@ -154,7 +154,6 @@ function getDelimiter(scopeType: SimpleScopeTypeType): string { switch (scopeType) { case "class": case "namedFunction": - case "ifStatement": case "section": case "sectionLevelOne": case "sectionLevelTwo": @@ -166,6 +165,7 @@ function getDelimiter(scopeType: SimpleScopeTypeType): string { case "anonymousFunction": case "statement": + case "ifStatement": case "comment": case "xmlElement": case "collectionItem": diff --git a/src/test/suite/fixtures/recorded/targets/scopeType/pourIfState.yml b/src/test/suite/fixtures/recorded/targets/scopeType/pourIfState.yml index 59ec8f79e1..ca22b124c9 100644 --- a/src/test/suite/fixtures/recorded/targets/scopeType/pourIfState.yml +++ b/src/test/suite/fixtures/recorded/targets/scopeType/pourIfState.yml @@ -19,12 +19,11 @@ initialState: active: {line: 1, character: 4} marks: {} finalState: - documentContents: |+ + documentContents: | if (false) { } - selections: - - anchor: {line: 4, character: 0} - active: {line: 4, character: 0} + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: ifStatement}}]}] From 7a1e8150a69145d404960a303a67efefcf47893b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 10 Nov 2022 16:55:44 +0000 Subject: [PATCH 2/2] Move `ide` out of `graph` and into its own module (#1102) * Move out of and into its own module * Empty commit * Remove unused import * PR feedback * Fix CI --- .eslintrc.json | 3 + .vscode/launch.json | 32 ++++-- docs-site/package.json | 2 +- docs-site/yarn.lock | 8 +- package.json | 16 ++- src/actions/CommandAction.ts | 2 +- src/actions/Fold.ts | 2 +- src/actions/MakeshiftActions.ts | 2 +- src/actions/Paste.ts | 2 +- src/actions/Scroll.ts | 2 +- src/apps/cursorless-vscode-e2e/.eslintrc.json | 16 +++ .../cursorless-vscode-e2e}/asyncSafety.ts | 2 +- .../endToEndTestSetup.ts} | 25 ++++- .../cursorless-vscode-e2e}/getFixturePaths.ts | 2 +- .../mockPrePhraseGetVersion.ts | 2 +- .../cursorless-vscode-e2e}/notebook.ts | 0 .../cursorless-vscode-e2e}/openNewEditor.ts | 2 +- .../cursorless-vscode-e2e}/runCommand.ts | 4 +- .../shouldUpdateFixtures.ts | 0 .../suite/.eslintrc.json | 16 +++ .../suite/backwardCompatibility.test.ts | 10 +- .../suite/breakpoints.test.ts | 16 +-- .../suite/containingTokenTwice.test.ts | 10 +- .../suite/crossCellsSetSelection.test.ts | 15 +-- .../suite/editNewCell.test.ts | 18 ++-- .../cursorless-vscode-e2e}/suite/fold.test.ts | 6 +- .../suite/followLink.test.ts | 12 +-- .../suite/groupByDocument.test.ts | 20 ++-- .../suite/intraCellSetSelection.test.ts | 14 +-- .../suite/prePhraseSnapshot.test.ts | 14 +-- .../suite/recorded.test.ts | 101 +++++++++++------- .../suite/scroll.test.ts | 6 +- .../suite/toggleDecorations.test.ts | 8 +- .../cursorless-vscode/getMatchesInRange.ts | 48 +++++++++ src/core/Cheatsheet.ts | 14 +-- src/core/Debug.ts | 14 +-- src/core/Decorations.ts | 9 +- src/core/FontMeasurements.ts | 22 +++- src/core/HatAllocator.ts | 8 +- src/core/HatTokenMap.ts | 25 +---- src/core/IndividualHatMap.ts | 12 +-- src/core/Snippets.ts | 29 ++--- src/core/StatusBarItem.ts | 3 +- src/core/commandRunner/CommandRunner.ts | 7 +- .../canonicalizeAndValidateCommand.ts | 10 +- src/core/editStyles.ts | 5 +- src/core/tokenizerConfiguration.ts | 36 ------- .../getOffsetsForEmptyRangeInsert.ts | 5 +- .../getOffsetsForNonEmptyRangeInsert.ts | 5 +- src/extension.ts | 63 +++++++---- src/ide/ide.types.ts | 85 --------------- src/ide/vscode/VscodeClipboard.ts | 12 +++ src/ide/vscode/VscodeConfiguration.ts | 18 ++-- src/ide/vscode/VscodeGlobalState.ts | 7 +- src/ide/vscode/VscodeIDE.ts | 45 ++++++-- src/ide/vscode/VscodeMessages.ts | 2 +- src/ide/{ => vscode}/activeTextEditor.ts | 0 src/languages/getNodeMatcher.ts | 2 +- src/languages/getTextFragmentExtractor.ts | 2 +- src/languages/index.ts | 5 +- src/languages/markdown.ts | 2 +- src/libs/common/.eslintrc.json | 25 +++++ src/{ => libs}/common/commandIds.ts | 0 src/libs/common/ide/.eslintrc.json | 25 +++++ src/libs/common/ide/fake/.eslintrc.json | 25 +++++ src/libs/common/ide/fake/FakeClipboard.ts | 13 +++ src/libs/common/ide/fake/FakeConfiguration.ts | 70 ++++++++++++ .../common/ide/fake}/FakeGlobalState.ts | 7 +- .../ide => libs/common/ide/fake}/FakeIDE.ts | 51 +++++---- .../common/ide/fake}/FakeMessages.ts | 2 +- src/libs/common/ide/spy/.eslintrc.json | 25 +++++ .../spies => libs/common/ide/spy}/SpyIDE.ts | 44 ++++---- .../common/ide/spy}/SpyMessages.ts | 2 +- src/libs/common/ide/types/.eslintrc.json | 25 +++++ src/libs/common/ide/types/Clipboard.ts | 17 +++ src/libs/common/ide/types/Configuration.ts | 61 +++++++++++ src/libs/common/ide/types/Messages.ts | 23 ++++ src/libs/common/ide/types/Paths.ts | 62 +++++++++++ src/libs/common/ide/types/State.ts | 28 +++++ src/libs/common/ide/types/ide.types.ts | 47 ++++++++ src/libs/common/index.ts | 19 ++++ src/libs/common/testUtil/.eslintrc.json | 26 +++++ .../common}/testUtil/extractTargetedMarks.ts | 14 +-- .../common/testUtil}/runTestSubset.ts | 2 +- src/{ => libs/common}/testUtil/serialize.ts | 0 src/libs/common/util/.eslintrc.json | 26 +++++ src/{ => libs/common}/util/Notifier.ts | 2 +- src/libs/common/util/index.ts | 2 + src/{ => libs/common}/util/sleep.ts | 0 src/libs/common/util/splitKey.ts | 16 +++ src/{ => libs/common}/util/timeUtils.ts | 0 .../common/util}/walkSync.ts | 0 src/libs/cursorless-engine/.eslintrc.json | 21 ++++ .../languages/.eslintrc.json | 21 ++++ .../cursorless-engine}/languages/constants.ts | 0 .../scopeHandlers/.eslintrc.json | 21 ++++ .../WordScopeHandler/.eslintrc.json | 21 ++++ .../WordScopeHandler}/WordTokenizer.ts | 4 +- .../singletons/.eslintrc.json | 21 ++++ .../singletons/ide.singleton.ts | 30 ++++++ .../tokenGraphemeSplitter.singleton.ts | 18 ++++ .../cursorless-engine/test/.eslintrc.json | 21 ++++ .../test/fixtures/subtoken.fixture.ts | 87 +++++++++++++++ .../cursorless-engine/test}/subtoken.test.ts | 10 +- .../cursorless-engine/test/unitTestSetup.ts | 27 +++++ .../tokenGraphemeSplitter/.eslintrc.json | 21 ++++ .../tokenGraphemeSplitter/index.ts | 1 + .../tokenGraphemeSplitter.test.ts | 37 ++----- .../tokenGraphemeSplitter.ts} | 19 ++-- .../tokenizer/.eslintrc.json | 21 ++++ src/libs/cursorless-engine/tokenizer/index.ts | 1 + .../tokenizer}/tokenizer.test.ts | 26 +++-- .../cursorless-engine/tokenizer}/tokenizer.ts | 12 ++- .../tokenizer}/tokenizer.types.ts | 0 .../cursorless-engine/util/.eslintrc.json | 21 ++++ .../cursorless-engine}/util/regex.ts | 48 --------- src/libs/vscode-common/.eslintrc.json | 16 +++ .../vscode-common}/getExtensionApi.ts | 36 +++++-- src/libs/vscode-common/index.ts | 9 ++ src/libs/vscode-common/notebook.ts | 20 ++++ .../vscode-common/testUtil/.eslintrc.json | 16 +++ .../vscode-common}/testUtil/takeSnapshot.ts | 24 +++-- .../vscode-common}/testUtil/testConstants.ts | 0 .../vscode-common}/toPlainObject.ts | 8 +- .../scopeHandlers/CharacterScopeHandler.ts | 4 +- .../scopeHandlers/IdentifierScopeHandler.ts | 4 +- .../scopeHandlers/TokenScopeHandler.ts | 5 +- .../scopeHandlers/WordScopeHandler.ts | 2 +- .../findSurroundingPairTextBased.ts | 2 +- .../TokenInsertionRemovalBehavior.ts | 2 +- src/{test => scripts}/initLaunchSandbox.ts | 2 +- src/scripts/transformRecordedTests/index.ts | 2 +- .../transformRecordedTests/moveFile.ts | 2 +- .../transformRecordedTests/transformFile.ts | 4 +- .../transformations/identity.ts | 2 +- .../transformations/reorderFields.ts | 2 +- .../transformations/upgrade.ts | 2 +- .../transformations/upgradeFromVersion0.ts | 2 +- src/scripts/transformRecordedTests/types.ts | 2 +- .../upgradeThatMarks.ts | 4 +- ...{runTest.ts => launchVscodeAndRunTests.ts} | 15 ++- src/test/runners/README.md | 3 + src/test/runners/all.ts | 15 +++ src/test/runners/endToEndOnly.ts | 17 +++ src/test/runners/unitTestsOnly.ts | 11 ++ src/test/scripts/runTestsCI.ts | 19 ++++ src/test/scripts/runUnitTestsOnly.ts | 3 + src/test/suite/extension.test.ts | 15 --- src/test/suite/fakes/ide/FakeConfiguration.ts | 36 ------- .../fixtures/recorded/actions/pasteCap.yml | 1 - .../index.ts => util/runAllTestsInDir.ts} | 6 +- src/testUtil/TestCase.ts | 70 +++--------- src/testUtil/TestCaseFixture.ts | 49 +++++++++ src/testUtil/TestCaseRecorder.ts | 61 +++++++---- src/testUtil/cleanUpTestCaseCommand.ts | 2 +- src/testUtil/fromPlainObject.ts | 4 +- src/typings/Types.ts | 43 +------- src/util/Clipboard.ts | 12 --- src/util/addDecorationsToEditor.ts | 6 +- src/util/getTokensInRange.ts | 2 +- src/util/graphFactories.ts | 4 - src/util/makeGraph.ts | 13 +-- src/util/notebook.ts | 20 +--- src/util/notebookLegacy.ts | 5 +- src/util/setSelectionsAndFocusEditor.ts | 5 +- tsconfig.json | 6 +- yarn.lock | 14 ++- 167 files changed, 1846 insertions(+), 836 deletions(-) create mode 100644 src/apps/cursorless-vscode-e2e/.eslintrc.json rename src/{test/util => apps/cursorless-vscode-e2e}/asyncSafety.ts (96%) rename src/{test/suite/standardSuiteSetup.ts => apps/cursorless-vscode-e2e/endToEndTestSetup.ts} (74%) rename src/{test/util => apps/cursorless-vscode-e2e}/getFixturePaths.ts (89%) rename src/{test => apps/cursorless-vscode-e2e}/mockPrePhraseGetVersion.ts (84%) rename src/{test/util => apps/cursorless-vscode-e2e}/notebook.ts (100%) rename src/{test => apps/cursorless-vscode-e2e}/openNewEditor.ts (97%) rename src/{client-e2e-test => apps/cursorless-vscode-e2e}/runCommand.ts (56%) rename src/{test/suite => apps/cursorless-vscode-e2e}/shouldUpdateFixtures.ts (100%) create mode 100644 src/apps/cursorless-vscode-e2e/suite/.eslintrc.json rename src/{test => apps/cursorless-vscode-e2e}/suite/backwardCompatibility.test.ts (71%) rename src/{test => apps/cursorless-vscode-e2e}/suite/breakpoints.test.ts (89%) rename src/{test => apps/cursorless-vscode-e2e}/suite/containingTokenTwice.test.ts (80%) rename src/{test => apps/cursorless-vscode-e2e}/suite/crossCellsSetSelection.test.ts (69%) rename src/{test => apps/cursorless-vscode-e2e}/suite/editNewCell.test.ts (71%) rename src/{test => apps/cursorless-vscode-e2e}/suite/fold.test.ts (88%) rename src/{test => apps/cursorless-vscode-e2e}/suite/followLink.test.ts (81%) rename src/{test => apps/cursorless-vscode-e2e}/suite/groupByDocument.test.ts (73%) rename src/{test => apps/cursorless-vscode-e2e}/suite/intraCellSetSelection.test.ts (69%) rename src/{test => apps/cursorless-vscode-e2e}/suite/prePhraseSnapshot.test.ts (87%) rename src/{test => apps/cursorless-vscode-e2e}/suite/recorded.test.ts (75%) rename src/{test => apps/cursorless-vscode-e2e}/suite/scroll.test.ts (88%) rename src/{test => apps/cursorless-vscode-e2e}/suite/toggleDecorations.test.ts (81%) create mode 100644 src/apps/cursorless-vscode/getMatchesInRange.ts delete mode 100644 src/core/tokenizerConfiguration.ts delete mode 100644 src/ide/ide.types.ts create mode 100644 src/ide/vscode/VscodeClipboard.ts rename src/ide/{ => vscode}/activeTextEditor.ts (100%) create mode 100644 src/libs/common/.eslintrc.json rename src/{ => libs}/common/commandIds.ts (100%) create mode 100644 src/libs/common/ide/.eslintrc.json create mode 100644 src/libs/common/ide/fake/.eslintrc.json create mode 100644 src/libs/common/ide/fake/FakeClipboard.ts create mode 100644 src/libs/common/ide/fake/FakeConfiguration.ts rename src/{test/suite/fakes/ide => libs/common/ide/fake}/FakeGlobalState.ts (79%) rename src/{test/suite/fakes/ide => libs/common/ide/fake}/FakeIDE.ts (51%) rename src/{test/suite/fakes/ide => libs/common/ide/fake}/FakeMessages.ts (77%) create mode 100644 src/libs/common/ide/spy/.eslintrc.json rename src/{ide/spies => libs/common/ide/spy}/SpyIDE.ts (59%) rename src/{ide/spies => libs/common/ide/spy}/SpyMessages.ts (92%) create mode 100644 src/libs/common/ide/types/.eslintrc.json create mode 100644 src/libs/common/ide/types/Clipboard.ts create mode 100644 src/libs/common/ide/types/Configuration.ts create mode 100644 src/libs/common/ide/types/Messages.ts create mode 100644 src/libs/common/ide/types/Paths.ts create mode 100644 src/libs/common/ide/types/State.ts create mode 100644 src/libs/common/ide/types/ide.types.ts create mode 100644 src/libs/common/index.ts create mode 100644 src/libs/common/testUtil/.eslintrc.json rename src/{ => libs/common}/testUtil/extractTargetedMarks.ts (74%) rename src/{test/suite => libs/common/testUtil}/runTestSubset.ts (91%) rename src/{ => libs/common}/testUtil/serialize.ts (100%) create mode 100644 src/libs/common/util/.eslintrc.json rename src/{ => libs/common}/util/Notifier.ts (94%) create mode 100644 src/libs/common/util/index.ts rename src/{ => libs/common}/util/sleep.ts (100%) create mode 100644 src/libs/common/util/splitKey.ts rename src/{ => libs/common}/util/timeUtils.ts (100%) rename src/{testUtil => libs/common/util}/walkSync.ts (100%) create mode 100644 src/libs/cursorless-engine/.eslintrc.json create mode 100644 src/libs/cursorless-engine/languages/.eslintrc.json rename src/{ => libs/cursorless-engine}/languages/constants.ts (100%) create mode 100644 src/libs/cursorless-engine/scopeHandlers/.eslintrc.json create mode 100644 src/libs/cursorless-engine/scopeHandlers/WordScopeHandler/.eslintrc.json rename src/{processTargets/modifiers/scopeHandlers => libs/cursorless-engine/scopeHandlers/WordScopeHandler}/WordTokenizer.ts (88%) create mode 100644 src/libs/cursorless-engine/singletons/.eslintrc.json create mode 100644 src/libs/cursorless-engine/singletons/ide.singleton.ts create mode 100644 src/libs/cursorless-engine/singletons/tokenGraphemeSplitter.singleton.ts create mode 100644 src/libs/cursorless-engine/test/.eslintrc.json create mode 100644 src/libs/cursorless-engine/test/fixtures/subtoken.fixture.ts rename src/{test/suite => libs/cursorless-engine/test}/subtoken.test.ts (54%) create mode 100644 src/libs/cursorless-engine/test/unitTestSetup.ts create mode 100644 src/libs/cursorless-engine/tokenGraphemeSplitter/.eslintrc.json create mode 100644 src/libs/cursorless-engine/tokenGraphemeSplitter/index.ts rename src/{test/suite => libs/cursorless-engine/tokenGraphemeSplitter}/tokenGraphemeSplitter.test.ts (89%) rename src/{core/TokenGraphemeSplitter.ts => libs/cursorless-engine/tokenGraphemeSplitter/tokenGraphemeSplitter.ts} (93%) create mode 100644 src/libs/cursorless-engine/tokenizer/.eslintrc.json create mode 100644 src/libs/cursorless-engine/tokenizer/index.ts rename src/{test/suite => libs/cursorless-engine/tokenizer}/tokenizer.test.ts (90%) rename src/{core => libs/cursorless-engine/tokenizer}/tokenizer.ts (85%) rename src/{core => libs/cursorless-engine/tokenizer}/tokenizer.types.ts (100%) create mode 100644 src/libs/cursorless-engine/util/.eslintrc.json rename src/{ => libs/cursorless-engine}/util/regex.ts (54%) create mode 100644 src/libs/vscode-common/.eslintrc.json rename src/{util => libs/vscode-common}/getExtensionApi.ts (58%) create mode 100644 src/libs/vscode-common/index.ts create mode 100644 src/libs/vscode-common/notebook.ts create mode 100644 src/libs/vscode-common/testUtil/.eslintrc.json rename src/{ => libs/vscode-common}/testUtil/takeSnapshot.ts (77%) rename src/{ => libs/vscode-common}/testUtil/testConstants.ts (100%) rename src/{testUtil => libs/vscode-common}/toPlainObject.ts (93%) rename src/{test => scripts}/initLaunchSandbox.ts (95%) rename src/test/{runTest.ts => launchVscodeAndRunTests.ts} (83%) create mode 100644 src/test/runners/README.md create mode 100644 src/test/runners/all.ts create mode 100644 src/test/runners/endToEndOnly.ts create mode 100644 src/test/runners/unitTestsOnly.ts create mode 100644 src/test/scripts/runTestsCI.ts create mode 100644 src/test/scripts/runUnitTestsOnly.ts delete mode 100644 src/test/suite/extension.test.ts delete mode 100644 src/test/suite/fakes/ide/FakeConfiguration.ts rename src/test/{suite/index.ts => util/runAllTestsInDir.ts} (83%) create mode 100644 src/testUtil/TestCaseFixture.ts delete mode 100644 src/util/Clipboard.ts diff --git a/.eslintrc.json b/.eslintrc.json index dc55c32aa1..53b73aa434 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,6 +12,9 @@ }, "plugins": ["@typescript-eslint"], "rules": { + // Note: you must disable the base rule as it can report incorrect errors + "no-restricted-imports": "off", + "@typescript-eslint/no-restricted-imports": "error", "@typescript-eslint/consistent-type-assertions": [ "error", { diff --git a/.vscode/launch.json b/.vscode/launch.json index adaa3eadb1..d3ea85c3b1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,9 +31,25 @@ "args": [ "--extensions-dir=${workspaceFolder}/.vscode-sandbox/extensions", "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + "--extensionTestsPath=${workspaceFolder}/out/test/runners/all" ], - "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "${defaultBuildTask}", + "resolveSourceMapLocations": [ + "${workspaceFolder}/**", + "!${workspaceFolder}/.vscode-sandbox/**", + "!**/node_modules/**" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Unit tests only", + "program": "${workspaceFolder}/out/test/scripts/runUnitTestsOnly", + "env": { + "CURSORLESS_TEST": "true" + }, + "outFiles": ["${workspaceFolder}/out/**/*.js"], "preLaunchTask": "${defaultBuildTask}", "resolveSourceMapLocations": [ "${workspaceFolder}/**", @@ -52,9 +68,9 @@ "args": [ "--extensions-dir=${workspaceFolder}/.vscode-sandbox/extensions", "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + "--extensionTestsPath=${workspaceFolder}/out/test/runners/all" ], - "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], "preLaunchTask": "${defaultBuildTask}", "resolveSourceMapLocations": [ "${workspaceFolder}/**", @@ -73,9 +89,9 @@ "args": [ "--extensions-dir=${workspaceFolder}/.vscode-sandbox/extensions", "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + "--extensionTestsPath=${workspaceFolder}/out/test/runners/all" ], - "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], "preLaunchTask": "${defaultBuildTask}", "resolveSourceMapLocations": [ "${workspaceFolder}/**", @@ -95,9 +111,9 @@ "args": [ "--extensions-dir=${workspaceFolder}/.vscode-sandbox/extensions", "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + "--extensionTestsPath=${workspaceFolder}/out/test/runners/all" ], - "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], "preLaunchTask": "${defaultBuildTask}", "resolveSourceMapLocations": [ "${workspaceFolder}/**", diff --git a/docs-site/package.json b/docs-site/package.json index 76947d2302..b633df40e6 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -43,6 +43,6 @@ "typedoc-plugin-mdn-links": "^1.0.4", "typedoc-plugin-missing-exports": "^0.22.6", "typedoc-plugin-rename-defaults": "^0.4.0", - "typescript": "^4.5.5" + "typescript": "4.6.3" } } diff --git a/docs-site/yarn.lock b/docs-site/yarn.lock index 2f75ed5583..6ac21caf3d 100644 --- a/docs-site/yarn.lock +++ b/docs-site/yarn.lock @@ -8257,10 +8257,10 @@ typedoc@^0.22.10: minimatch "^3.0.4" shiki "^0.9.12" -typescript@^4.5.5: - version "4.5.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz" - integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== +typescript@4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" + integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== ua-parser-js@^0.7.30: version "0.7.31" diff --git a/package.json b/package.json index c45665aa4d..465d9305bc 100644 --- a/package.json +++ b/package.json @@ -566,6 +566,10 @@ "url": "https://github.com/sponsors/pokey" }, "funding": "https://github.com/sponsors/pokey", + "_moduleAliases": { + "@cursorless/common": "./out/libs/common/index.js", + "@cursorless/vscode-common": "./out/libs/vscode-common/index.js" + }, "scripts": { "vscode:prepublish": "npm run -S esbuild-base -- --minify", "update-licenses": "npx npm-license-crawler --onlyDirectDependencies --csv third-party-licenses.csv", @@ -577,9 +581,9 @@ "watch": "tsc -watch -p ./", "pretest": "yarn run compile && yarn run lint && yarn run esbuild", "lint": "eslint src --ext ts", - "test": "env CURSORLESS_TEST=true node ./out/test/runTest.js", + "test": "env CURSORLESS_TEST=true node ./out/test/scripts/runTestsCI.js", "unused-exports": "ts-unused-exports tsconfig.json --showLineNumber", - "init-launch-sandbox": "node ./out/test/initLaunchSandbox.js", + "init-launch-sandbox": "node ./out/scripts/initLaunchSandbox.js", "prepare-for-extension-publish": "node ./out/scripts/prepareForExtensionPublish.js" }, "devDependencies": { @@ -602,12 +606,13 @@ "glob": "^7.1.7", "js-yaml": "^4.1.0", "mocha": "^8.1.3", + "module-alias": "^2.2.2", "npm-license-crawler": "^0.2.1", "prettier": "2.7.1", "semver": "^7.3.7", "sinon": "^11.1.1", - "ts-unused-exports": "8.0.0", - "typescript": "^4.5.5" + "ts-unused-exports": "^8.0.0", + "typescript": "4.6.3" }, "dependencies": { "@types/lodash": "^4.14.168", @@ -615,6 +620,7 @@ "immutability-helper": "^3.1.1", "itertools": "^1.7.1", "lodash": "^4.17.21", - "node-html-parser": "^5.3.3" + "node-html-parser": "^5.3.3", + "vscode-uri": "^3.0.6" } } diff --git a/src/actions/CommandAction.ts b/src/actions/CommandAction.ts index 066bb5669d..25458003fc 100644 --- a/src/actions/CommandAction.ts +++ b/src/actions/CommandAction.ts @@ -15,7 +15,7 @@ import { runOnTargetsForEachEditor, } from "../util/targetUtils"; import { Action, ActionReturnValue } from "./actions.types"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; export interface CommandOptions { command?: string; diff --git a/src/actions/Fold.ts b/src/actions/Fold.ts index a0b38b363e..e54032606c 100644 --- a/src/actions/Fold.ts +++ b/src/actions/Fold.ts @@ -4,7 +4,7 @@ import { Graph } from "../typings/Types"; import { focusEditor } from "../util/setSelectionsAndFocusEditor"; import { createThatMark, ensureSingleEditor } from "../util/targetUtils"; import { Action, ActionReturnValue } from "./actions.types"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; class FoldAction implements Action { constructor(private command: string) { diff --git a/src/actions/MakeshiftActions.ts b/src/actions/MakeshiftActions.ts index 2608b40c33..46ae210605 100644 --- a/src/actions/MakeshiftActions.ts +++ b/src/actions/MakeshiftActions.ts @@ -1,5 +1,5 @@ import { Target } from "../typings/target.types"; -import sleep from "../util/sleep"; +import sleep from "../libs/common/util/sleep"; import CommandAction from "./CommandAction"; abstract class MakeshiftAction extends CommandAction { diff --git a/src/actions/Paste.ts b/src/actions/Paste.ts index db2621ec40..4fba022341 100644 --- a/src/actions/Paste.ts +++ b/src/actions/Paste.ts @@ -11,7 +11,7 @@ import { } from "../util/setSelectionsAndFocusEditor"; import { ensureSingleEditor } from "../util/targetUtils"; import { ActionReturnValue } from "./actions.types"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; export class Paste { constructor(private graph: Graph) {} diff --git a/src/actions/Scroll.ts b/src/actions/Scroll.ts index d0e1679bd6..8eb447ec0b 100644 --- a/src/actions/Scroll.ts +++ b/src/actions/Scroll.ts @@ -4,7 +4,7 @@ import { Graph } from "../typings/Types"; import { groupBy } from "../util/itertools"; import { focusEditor } from "../util/setSelectionsAndFocusEditor"; import { Action, ActionReturnValue } from "./actions.types"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; class Scroll implements Action { constructor(private graph: Graph, private at: string) { diff --git a/src/apps/cursorless-vscode-e2e/.eslintrc.json b/src/apps/cursorless-vscode-e2e/.eslintrc.json new file mode 100644 index 0000000000..6294ccf06a --- /dev/null +++ b/src/apps/cursorless-vscode-e2e/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../*"], + "message": "Cursorless end-to-end tests shouldn't import from Cursorless extension", + "allowTypeImports": true + } + ] + } + ] + } +} diff --git a/src/test/util/asyncSafety.ts b/src/apps/cursorless-vscode-e2e/asyncSafety.ts similarity index 96% rename from src/test/util/asyncSafety.ts rename to src/apps/cursorless-vscode-e2e/asyncSafety.ts index 69a85af325..7e96a407a4 100644 --- a/src/test/util/asyncSafety.ts +++ b/src/apps/cursorless-vscode-e2e/asyncSafety.ts @@ -1,4 +1,4 @@ -import { Context, Done } from "mocha"; +import type { Context, Done } from "mocha"; /** * if an async returns after the method times out, diff --git a/src/test/suite/standardSuiteSetup.ts b/src/apps/cursorless-vscode-e2e/endToEndTestSetup.ts similarity index 74% rename from src/test/suite/standardSuiteSetup.ts rename to src/apps/cursorless-vscode-e2e/endToEndTestSetup.ts index d24dc56309..ef0ed9e5dc 100644 --- a/src/test/suite/standardSuiteSetup.ts +++ b/src/apps/cursorless-vscode-e2e/endToEndTestSetup.ts @@ -1,6 +1,7 @@ +import { IDE, sleep, SpyIDE } from "@cursorless/common"; +import { getCursorlessApi } from "@cursorless/vscode-common"; import { Context } from "mocha"; import * as sinon from "sinon"; -import sleep from "../../util/sleep"; import shouldUpdateFixtures from "./shouldUpdateFixtures"; /** @@ -16,19 +17,33 @@ let retryCount = -1; */ let previousTestTitle = ""; -export function standardSuiteSetup(suite: Mocha.Suite) { +export function endToEndTestSetup(suite: Mocha.Suite) { suite.timeout("100s"); suite.retries(5); - teardown(() => { - sinon.restore(); - }); + let ide: IDE | undefined; + let injectIde: ((ide: IDE) => void) | undefined; + let spy: SpyIDE | undefined; setup(async function (this: Context) { const title = this.test!.fullTitle(); retryCount = title === previousTestTitle ? retryCount + 1 : 0; previousTestTitle = title; + ({ ide, injectIde } = (await getCursorlessApi()).testHelpers!); + spy = new SpyIDE(ide!); + injectIde!(spy); + }); + + teardown(() => { + sinon.restore(); + injectIde!(ide!); }); + + return { + getSpy() { + return spy; + }, + }; } /** diff --git a/src/test/util/getFixturePaths.ts b/src/apps/cursorless-vscode-e2e/getFixturePaths.ts similarity index 89% rename from src/test/util/getFixturePaths.ts rename to src/apps/cursorless-vscode-e2e/getFixturePaths.ts index cee66b9ada..9628463983 100644 --- a/src/test/util/getFixturePaths.ts +++ b/src/apps/cursorless-vscode-e2e/getFixturePaths.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { walkFilesSync } from "../../testUtil/walkSync"; +import { walkFilesSync } from "@cursorless/common"; export function getFixturesPath() { return path.join(__dirname, "../../../src/test/suite/fixtures"); diff --git a/src/test/mockPrePhraseGetVersion.ts b/src/apps/cursorless-vscode-e2e/mockPrePhraseGetVersion.ts similarity index 84% rename from src/test/mockPrePhraseGetVersion.ts rename to src/apps/cursorless-vscode-e2e/mockPrePhraseGetVersion.ts index dbe90ddf72..184a7f5a97 100644 --- a/src/test/mockPrePhraseGetVersion.ts +++ b/src/apps/cursorless-vscode-e2e/mockPrePhraseGetVersion.ts @@ -1,5 +1,5 @@ import * as sinon from "sinon"; -import { Graph } from "../typings/Types"; +import type { Graph } from "../../typings/Types"; export function mockPrePhraseGetVersion( graph: Graph, diff --git a/src/test/util/notebook.ts b/src/apps/cursorless-vscode-e2e/notebook.ts similarity index 100% rename from src/test/util/notebook.ts rename to src/apps/cursorless-vscode-e2e/notebook.ts diff --git a/src/test/openNewEditor.ts b/src/apps/cursorless-vscode-e2e/openNewEditor.ts similarity index 97% rename from src/test/openNewEditor.ts rename to src/apps/cursorless-vscode-e2e/openNewEditor.ts index 279fe1a622..b6c4f95cc3 100644 --- a/src/test/openNewEditor.ts +++ b/src/apps/cursorless-vscode-e2e/openNewEditor.ts @@ -1,5 +1,5 @@ +import { getParseTreeApi } from "@cursorless/vscode-common"; import * as vscode from "vscode"; -import { getParseTreeApi } from "../util/getExtensionApi"; export async function openNewEditor( content: string, diff --git a/src/client-e2e-test/runCommand.ts b/src/apps/cursorless-vscode-e2e/runCommand.ts similarity index 56% rename from src/client-e2e-test/runCommand.ts rename to src/apps/cursorless-vscode-e2e/runCommand.ts index 4994ce5a14..b7606c4339 100644 --- a/src/client-e2e-test/runCommand.ts +++ b/src/apps/cursorless-vscode-e2e/runCommand.ts @@ -1,6 +1,6 @@ +import { CURSORLESS_COMMAND_ID } from "@cursorless/common"; import * as vscode from "vscode"; -import { CURSORLESS_COMMAND_ID } from "../common/commandIds"; -import type { Command } from "../core/commandRunner/command.types"; +import type { Command } from "../../core/commandRunner/command.types"; export function runCursorlessCommand(command: Command) { return vscode.commands.executeCommand(CURSORLESS_COMMAND_ID, command); diff --git a/src/test/suite/shouldUpdateFixtures.ts b/src/apps/cursorless-vscode-e2e/shouldUpdateFixtures.ts similarity index 100% rename from src/test/suite/shouldUpdateFixtures.ts rename to src/apps/cursorless-vscode-e2e/shouldUpdateFixtures.ts diff --git a/src/apps/cursorless-vscode-e2e/suite/.eslintrc.json b/src/apps/cursorless-vscode-e2e/suite/.eslintrc.json new file mode 100644 index 0000000000..02a40c3677 --- /dev/null +++ b/src/apps/cursorless-vscode-e2e/suite/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Cursorless end-to-end tests shouldn't import from Cursorless extension", + "allowTypeImports": true + } + ] + } + ] + } +} diff --git a/src/test/suite/backwardCompatibility.test.ts b/src/apps/cursorless-vscode-e2e/suite/backwardCompatibility.test.ts similarity index 71% rename from src/test/suite/backwardCompatibility.test.ts rename to src/apps/cursorless-vscode-e2e/suite/backwardCompatibility.test.ts index b4d88cc2e6..6c79a7bf3c 100644 --- a/src/test/suite/backwardCompatibility.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/backwardCompatibility.test.ts @@ -1,18 +1,18 @@ +import { CURSORLESS_COMMAND_ID } from "@cursorless/common"; +import { getCursorlessApi } from "@cursorless/vscode-common"; import * as assert from "assert"; import * as vscode from "vscode"; -import { CURSORLESS_COMMAND_ID } from "../../common/commandIds"; -import { getCursorlessApi } from "../../util/getExtensionApi"; import { openNewEditor } from "../openNewEditor"; -import { standardSuiteSetup } from "./standardSuiteSetup"; +import { endToEndTestSetup } from "../endToEndTestSetup"; suite("Backward compatibility", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("Backward compatibility", runTest); }); async function runTest() { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; const editor = await openNewEditor(""); diff --git a/src/test/suite/breakpoints.test.ts b/src/apps/cursorless-vscode-e2e/suite/breakpoints.test.ts similarity index 89% rename from src/test/suite/breakpoints.test.ts rename to src/apps/cursorless-vscode-e2e/suite/breakpoints.test.ts index 1b9a6fb9c1..0f3d1bbb16 100644 --- a/src/test/suite/breakpoints.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/breakpoints.test.ts @@ -1,12 +1,12 @@ +import { getCursorlessApi } from "@cursorless/vscode-common"; import * as assert from "assert"; import * as vscode from "vscode"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; -import { getCursorlessApi } from "../../util/getExtensionApi"; import { openNewEditor } from "../openNewEditor"; -import { standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import { endToEndTestSetup } from "../endToEndTestSetup"; suite("breakpoints", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); setup(() => { removeBreakpoints(); @@ -23,7 +23,7 @@ suite("breakpoints", async function () { }); async function breakpointHarpAdd() { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; await openNewEditor(" hello"); await graph.hatTokenMap.addDecorations(); @@ -50,7 +50,7 @@ async function breakpointHarpAdd() { } async function breakpointTokenHarpAdd() { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; await openNewEditor(" hello"); await graph.hatTokenMap.addDecorations(); @@ -78,7 +78,7 @@ async function breakpointTokenHarpAdd() { } async function breakpointHarpRemove() { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; const editor = await openNewEditor(" hello"); await graph.hatTokenMap.addDecorations(); @@ -109,7 +109,7 @@ async function breakpointHarpRemove() { } async function breakpointTokenHarpRemove() { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; const editor = await openNewEditor(" hello"); await graph.hatTokenMap.addDecorations(); diff --git a/src/test/suite/containingTokenTwice.test.ts b/src/apps/cursorless-vscode-e2e/suite/containingTokenTwice.test.ts similarity index 80% rename from src/test/suite/containingTokenTwice.test.ts rename to src/apps/cursorless-vscode-e2e/suite/containingTokenTwice.test.ts index 94551d9cee..37852a15ab 100644 --- a/src/test/suite/containingTokenTwice.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/containingTokenTwice.test.ts @@ -1,21 +1,21 @@ +import { getCursorlessApi } from "@cursorless/vscode-common"; import { assert } from "chai"; import * as vscode from "vscode"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; -import { getCursorlessApi } from "../../util/getExtensionApi"; import { openNewEditor } from "../openNewEditor"; -import { standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import { endToEndTestSetup } from "../endToEndTestSetup"; // Check that we don't run afoul of stateful regex craziness // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#finding_successive_matches // When this fails, the regex that checks if something is an identifier will start at the wrong place the second time it is called suite("Take token twice", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("Take token twice", runTest); }); async function runTest() { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; const editor = await openNewEditor("a)"); await graph.hatTokenMap.addDecorations(); diff --git a/src/test/suite/crossCellsSetSelection.test.ts b/src/apps/cursorless-vscode-e2e/suite/crossCellsSetSelection.test.ts similarity index 69% rename from src/test/suite/crossCellsSetSelection.test.ts rename to src/apps/cursorless-vscode-e2e/suite/crossCellsSetSelection.test.ts index 462a4734e6..b6368ee054 100644 --- a/src/test/suite/crossCellsSetSelection.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/crossCellsSetSelection.test.ts @@ -1,19 +1,19 @@ +import { getCursorlessApi } from "@cursorless/vscode-common"; import * as assert from "assert"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; -import { getActiveTextEditor } from "../../ide/activeTextEditor"; -import { getCursorlessApi } from "../../util/getExtensionApi"; +import { window } from "vscode"; import { openNewNotebookEditor } from "../openNewEditor"; -import { sleepWithBackoff, standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import { sleepWithBackoff, endToEndTestSetup } from "../endToEndTestSetup"; // Check that setSelection is able to focus the correct cell suite("Cross-cell set selection", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("Cross-cell set selection", runTest); }); async function runTest() { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; await openNewNotebookEditor(['"hello"', '"world"']); @@ -38,7 +38,8 @@ async function runTest() { ], }); - const editor = getActiveTextEditor(); + // eslint-disable-next-line no-restricted-properties + const editor = window.activeTextEditor; if (editor == null) { assert(false, "No editor was focused"); diff --git a/src/test/suite/editNewCell.test.ts b/src/apps/cursorless-vscode-e2e/suite/editNewCell.test.ts similarity index 71% rename from src/test/suite/editNewCell.test.ts rename to src/apps/cursorless-vscode-e2e/suite/editNewCell.test.ts index d3f40518c7..4f69941337 100644 --- a/src/test/suite/editNewCell.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/editNewCell.test.ts @@ -1,15 +1,14 @@ +import { getCellIndex, getCursorlessApi } from "@cursorless/vscode-common"; import * as assert from "assert"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; -import { getActiveTextEditor } from "../../ide/activeTextEditor"; -import { getCursorlessApi } from "../../util/getExtensionApi"; -import { getCellIndex } from "../../util/notebook"; +import { window } from "vscode"; +import { getPlainNotebookContents } from "../notebook"; import { openNewNotebookEditor } from "../openNewEditor"; -import { getPlainNotebookContents } from "../util/notebook"; -import { sleepWithBackoff, standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import { sleepWithBackoff, endToEndTestSetup } from "../endToEndTestSetup"; // Check that setSelection is able to focus the correct cell suite("Edit new cell", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("drink cell", () => runTest("drink cell", "editNewLineBefore", 0, ["", "hello"])); @@ -23,7 +22,7 @@ async function runTest( expectedActiveCellIndex: number, expectedNotebookContents: string[], ) { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; const notebook = await openNewNotebookEditor(["hello"]); // FIXME: There seems to be some timing issue when you create a notebook @@ -52,7 +51,8 @@ async function runTest( const activeCelIndex = getCellIndex( notebook, - getActiveTextEditor()!.document, + // eslint-disable-next-line no-restricted-properties + window.activeTextEditor!.document, ); assert.equal(activeCelIndex, expectedActiveCellIndex); diff --git a/src/test/suite/fold.test.ts b/src/apps/cursorless-vscode-e2e/suite/fold.test.ts similarity index 88% rename from src/test/suite/fold.test.ts rename to src/apps/cursorless-vscode-e2e/suite/fold.test.ts index 548ba1f847..d352ea0913 100644 --- a/src/test/suite/fold.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/fold.test.ts @@ -1,11 +1,11 @@ import * as assert from "assert"; import * as vscode from "vscode"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; import { openNewEditor } from "../openNewEditor"; -import { standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import { endToEndTestSetup } from "../endToEndTestSetup"; suite("fold", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("fold made", foldMade); test("unfold made", unfoldMade); diff --git a/src/test/suite/followLink.test.ts b/src/apps/cursorless-vscode-e2e/suite/followLink.test.ts similarity index 81% rename from src/test/suite/followLink.test.ts rename to src/apps/cursorless-vscode-e2e/suite/followLink.test.ts index 13d8cbf46a..5910ef50b5 100644 --- a/src/test/suite/followLink.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/followLink.test.ts @@ -1,14 +1,13 @@ import * as assert from "assert"; import * as os from "os"; import * as vscode from "vscode"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; -import { getActiveTextEditor } from "../../ide/activeTextEditor"; +import { getFixturePath } from "../getFixturePaths"; import { openNewEditor } from "../openNewEditor"; -import { getFixturePath } from "../util/getFixturePaths"; -import { standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import { endToEndTestSetup } from "../endToEndTestSetup"; suite("followLink", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("follow definition", followDefinition); test("follow link", followLink); @@ -62,7 +61,8 @@ async function followLink() { ], }); - const editor = getActiveTextEditor(); + // eslint-disable-next-line no-restricted-properties + const editor = vscode.window.activeTextEditor; assert.equal(editor?.document?.uri?.scheme, "file"); assert.equal(editor?.document.getText().trimEnd(), "hello world"); } diff --git a/src/test/suite/groupByDocument.test.ts b/src/apps/cursorless-vscode-e2e/suite/groupByDocument.test.ts similarity index 73% rename from src/test/suite/groupByDocument.test.ts rename to src/apps/cursorless-vscode-e2e/suite/groupByDocument.test.ts index 6171929b39..e857c84694 100644 --- a/src/test/suite/groupByDocument.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/groupByDocument.test.ts @@ -1,18 +1,18 @@ +import { getCursorlessApi } from "@cursorless/vscode-common"; import * as assert from "assert"; import * as vscode from "vscode"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; -import HatTokenMap from "../../core/HatTokenMap"; -import { getCursorlessApi } from "../../util/getExtensionApi"; -import { standardSuiteSetup } from "./standardSuiteSetup"; +import { splitKey } from "@cursorless/common"; +import { runCursorlessCommand } from "../runCommand"; +import { endToEndTestSetup } from "../endToEndTestSetup"; suite("Group by document", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("Group by document", runTest); }); async function runTest() { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; await vscode.commands.executeCommand("workbench.action.closeAllEditors"); @@ -37,12 +37,8 @@ async function runTest() { .getEntries() .find(([, token]) => token.editor === editor2 && token.text === "world"); - const { hatStyle: hatStyle1, character: char1 } = HatTokenMap.splitKey( - hat1![0], - ); - const { hatStyle: hatStyle2, character: char2 } = HatTokenMap.splitKey( - hat2![0], - ); + const { hatStyle: hatStyle1, character: char1 } = splitKey(hat1![0]); + const { hatStyle: hatStyle2, character: char2 } = splitKey(hat2![0]); await runCursorlessCommand({ version: 1, diff --git a/src/test/suite/intraCellSetSelection.test.ts b/src/apps/cursorless-vscode-e2e/suite/intraCellSetSelection.test.ts similarity index 69% rename from src/test/suite/intraCellSetSelection.test.ts rename to src/apps/cursorless-vscode-e2e/suite/intraCellSetSelection.test.ts index bda3d983da..2aca92e2c7 100644 --- a/src/test/suite/intraCellSetSelection.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/intraCellSetSelection.test.ts @@ -1,19 +1,19 @@ +import { getCursorlessApi } from "@cursorless/vscode-common"; import * as assert from "assert"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; -import { getActiveTextEditor } from "../../ide/activeTextEditor"; -import { getCursorlessApi } from "../../util/getExtensionApi"; +import { window } from "vscode"; import { openNewNotebookEditor } from "../openNewEditor"; -import { sleepWithBackoff, standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import { sleepWithBackoff, endToEndTestSetup } from "../endToEndTestSetup"; // Check that setSelection is able to focus the correct cell suite("Within cell set selection", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("Within cell set selection", runTest); }); async function runTest() { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; await openNewNotebookEditor(['"hello world"']); @@ -38,7 +38,7 @@ async function runTest() { ], }); - const editor = getActiveTextEditor(); + const editor = window.activeTextEditor; // eslint-disable-line no-restricted-properties if (editor == null) { assert(false, "No editor was focused"); diff --git a/src/test/suite/prePhraseSnapshot.test.ts b/src/apps/cursorless-vscode-e2e/suite/prePhraseSnapshot.test.ts similarity index 87% rename from src/test/suite/prePhraseSnapshot.test.ts rename to src/apps/cursorless-vscode-e2e/suite/prePhraseSnapshot.test.ts index 1e182b1a4a..ab2379a147 100644 --- a/src/test/suite/prePhraseSnapshot.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/prePhraseSnapshot.test.ts @@ -1,11 +1,13 @@ +import { + getCursorlessApi, + selectionToPlainObject, +} from "@cursorless/vscode-common"; import * as assert from "assert"; import * as vscode from "vscode"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; -import { selectionToPlainObject } from "../../testUtil/toPlainObject"; -import { getCursorlessApi } from "../../util/getExtensionApi"; import { mockPrePhraseGetVersion } from "../mockPrePhraseGetVersion"; import { openNewEditor } from "../openNewEditor"; -import { standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import { endToEndTestSetup } from "../endToEndTestSetup"; /** * The selections we expect when the pre-phrase snapshot is used @@ -18,7 +20,7 @@ const snapshotExpectedSelections = [new vscode.Selection(0, 6, 0, 11)]; const noSnapshotExpectedSelections = [new vscode.Selection(1, 6, 1, 11)]; suite("Pre-phrase snapshots", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("Pre-phrase snapshot; single phrase", () => runTest(true, false, snapshotExpectedSelections)); @@ -36,7 +38,7 @@ async function runTest( multiplePhrases: boolean, expectedSelections: vscode.Selection[], ) { - const graph = (await getCursorlessApi()).graph!; + const { graph } = (await getCursorlessApi()).testHelpers!; const editor = await openNewEditor("Hello world testing whatever"); diff --git a/src/test/suite/recorded.test.ts b/src/apps/cursorless-vscode-e2e/suite/recorded.test.ts similarity index 75% rename from src/test/suite/recorded.test.ts rename to src/apps/cursorless-vscode-e2e/suite/recorded.test.ts index 3979e5f4f9..c39a627216 100644 --- a/src/test/suite/recorded.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/recorded.test.ts @@ -1,36 +1,34 @@ -import { assert } from "chai"; -import { promises as fsp } from "fs"; -import * as yaml from "js-yaml"; -import * as vscode from "vscode"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; -import HatTokenMap from "../../core/HatTokenMap"; -import { ReadOnlyHatMap } from "../../core/IndividualHatMap"; -import { injectSpyIde } from "../../ide/spies/SpyIDE"; -import { extractTargetedMarks } from "../../testUtil/extractTargetedMarks"; -import { plainObjectToTarget } from "../../testUtil/fromPlainObject"; -import serialize from "../../testUtil/serialize"; -import { - ExcludableSnapshotField, - takeSnapshot, -} from "../../testUtil/takeSnapshot"; -import { TestCaseFixture } from "../../testUtil/TestCase"; -import { DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST } from "../../testUtil/testConstants"; import { + getCursorlessApi, marksToPlainObject, PositionPlainObject, rangeToPlainObject, SelectionPlainObject, SerializedMarks, testDecorationsToPlainObject, -} from "../../testUtil/toPlainObject"; -import { Clipboard } from "../../util/Clipboard"; -import { getCursorlessApi } from "../../util/getExtensionApi"; + DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST, + ExcludableSnapshotField, + takeSnapshot, +} from "@cursorless/vscode-common"; +import { + serialize, + splitKey, + extractTargetedMarks, + FakeIDE, +} from "@cursorless/common"; +import { assert } from "chai"; +import { promises as fsp } from "fs"; +import * as yaml from "js-yaml"; +import * as vscode from "vscode"; +import type { ReadOnlyHatMap } from "../../../core/IndividualHatMap"; +import type { SpyIDE } from "@cursorless/common"; +import type { TestCaseFixture } from "../../../testUtil/TestCaseFixture"; +import asyncSafety from "../asyncSafety"; +import { getFixturePath, getRecordedTestPaths } from "../getFixturePaths"; import { openNewEditor } from "../openNewEditor"; -import asyncSafety from "../util/asyncSafety"; -import { getRecordedTestPaths } from "../util/getFixturePaths"; -import { injectFakeIde } from "./fakes/ide/FakeIDE"; -import shouldUpdateFixtures from "./shouldUpdateFixtures"; -import { sleepWithBackoff, standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import shouldUpdateFixtures from "../shouldUpdateFixtures"; +import { sleepWithBackoff, endToEndTestSetup } from "../endToEndTestSetup"; function createPosition(position: PositionPlainObject) { return new vscode.Position(position.line, position.character); @@ -43,22 +41,24 @@ function createSelection(selection: SelectionPlainObject): vscode.Selection { } suite("recorded test cases", async function () { - standardSuiteSetup(this); + const { getSpy } = endToEndTestSetup(this); suiteSetup(async () => { // Necessary because opening a notebook opens the panel for some reason await vscode.commands.executeCommand("workbench.action.closePanel"); + const { ide: fake } = (await getCursorlessApi()).testHelpers!; + setupFake(fake!); }); getRecordedTestPaths().forEach((path) => test( path.split(".")[0], - asyncSafety(() => runTest(path)), + asyncSafety(() => runTest(path, getSpy()!)), ), ); }); -async function runTest(file: string) { +async function runTest(file: string, spyIde: SpyIDE) { const buffer = await fsp.readFile(file); const fixture = yaml.load(buffer.toString()) as TestCaseFixture; const excludeFields: ExcludableSnapshotField[] = []; @@ -68,7 +68,8 @@ async function runTest(file: string) { const usePrePhraseSnapshot = false; const cursorlessApi = await getCursorlessApi(); - const graph = cursorlessApi.graph!; + const { graph, plainObjectToTarget } = cursorlessApi.testHelpers!; + graph.editStyles.testDecorations = []; const editor = await openNewEditor( @@ -100,13 +101,9 @@ async function runTest(file: string) { } if (fixture.initialState.clipboard) { - Clipboard.writeText(fixture.initialState.clipboard); + vscode.env.clipboard.writeText(fixture.initialState.clipboard); // FIXME https://github.com/cursorless-dev/cursorless/issues/559 - // let mockClipboard = fixture.initialState.clipboard; - // sinon.replace(Clipboard, "readText", async () => mockClipboard); - // sinon.replace(Clipboard, "writeText", async (value: string) => { - // mockClipboard = value; - // }); + // spyIde.clipboard.writeText(fixture.initialState.clipboard); } await graph.hatTokenMap.addDecorations(); @@ -118,9 +115,6 @@ async function runTest(file: string) { // Assert that recorded decorations are present checkMarks(fixture.initialState.marks, readableHatMap); - const { dispose: disposeFakeIde } = injectFakeIde(graph); - const { spy: spyIde } = injectSpyIde(graph); - let returnValue: unknown; try { @@ -150,8 +144,6 @@ async function runTest(file: string) { return; } - disposeFakeIde(); - if (fixture.postCommandSleepTimeMs != null) { await sleepWithBackoff(fixture.postCommandSleepTimeMs); } @@ -185,7 +177,13 @@ async function runTest(file: string) { cursorlessApi.sourceMark, excludeFields, [], + vscode.window.activeTextEditor!, // eslint-disable-line no-restricted-properties + spyIde, marks, + undefined, + undefined, + // FIXME: Stop overriding the clipboard once we have #559 + vscode.env.clipboard, ); const actualDecorations = @@ -239,6 +237,27 @@ async function runTest(file: string) { } } +function setupFake(fakeIde: FakeIDE) { + fakeIde.configuration.mockConfigurationScope( + { languageId: "css" }, + { wordSeparators: ["_", "-"] }, + true, + ); + fakeIde.configuration.mockConfigurationScope( + { languageId: "scss" }, + { wordSeparators: ["_", "-"] }, + true, + ); + fakeIde.configuration.mockConfigurationScope( + { languageId: "shellscript" }, + { wordSeparators: ["_", "-"] }, + true, + ); + fakeIde.configuration.mockConfiguration("experimental", { + snippetsDir: getFixturePath("cursorless-snippets"), + }); +} + function checkMarks( marks: SerializedMarks | undefined, hatTokenMap: ReadOnlyHatMap, @@ -248,7 +267,7 @@ function checkMarks( } Object.entries(marks).forEach(([key, token]) => { - const { hatStyle, character } = HatTokenMap.splitKey(key); + const { hatStyle, character } = splitKey(key); const currentToken = hatTokenMap.getToken(hatStyle, character); assert(currentToken != null, `Mark "${hatStyle} ${character}" not found`); assert.deepStrictEqual(rangeToPlainObject(currentToken.range), token); diff --git a/src/test/suite/scroll.test.ts b/src/apps/cursorless-vscode-e2e/suite/scroll.test.ts similarity index 88% rename from src/test/suite/scroll.test.ts rename to src/apps/cursorless-vscode-e2e/suite/scroll.test.ts index d7a9d267ee..d24e07dd90 100644 --- a/src/test/suite/scroll.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/scroll.test.ts @@ -1,11 +1,11 @@ import * as assert from "assert"; import * as vscode from "vscode"; -import { runCursorlessCommand } from "../../client-e2e-test/runCommand"; import { openNewEditor } from "../openNewEditor"; -import { standardSuiteSetup } from "./standardSuiteSetup"; +import { runCursorlessCommand } from "../runCommand"; +import { endToEndTestSetup } from "../endToEndTestSetup"; suite("scroll", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("top whale", topWhale); test("bottom whale", bottomWhale); diff --git a/src/test/suite/toggleDecorations.test.ts b/src/apps/cursorless-vscode-e2e/suite/toggleDecorations.test.ts similarity index 81% rename from src/test/suite/toggleDecorations.test.ts rename to src/apps/cursorless-vscode-e2e/suite/toggleDecorations.test.ts index 25d4fffae7..9b6cd63472 100644 --- a/src/test/suite/toggleDecorations.test.ts +++ b/src/apps/cursorless-vscode-e2e/suite/toggleDecorations.test.ts @@ -1,17 +1,17 @@ +import { getCursorlessApi } from "@cursorless/vscode-common"; import * as assert from "assert"; import * as vscode from "vscode"; -import { getCursorlessApi } from "../../util/getExtensionApi"; import { openNewEditor } from "../openNewEditor"; -import { standardSuiteSetup } from "./standardSuiteSetup"; +import { endToEndTestSetup } from "../endToEndTestSetup"; suite("toggle decorations", async function () { - standardSuiteSetup(this); + endToEndTestSetup(this); test("toggle decorations", () => runTest()); }); async function runTest() { - const { hatTokenMap } = (await getCursorlessApi()).graph!; + const { hatTokenMap } = (await getCursorlessApi()).testHelpers!.graph; await openNewEditor("Hello world testing whatever"); diff --git a/src/apps/cursorless-vscode/getMatchesInRange.ts b/src/apps/cursorless-vscode/getMatchesInRange.ts new file mode 100644 index 0000000000..4b19c302b5 --- /dev/null +++ b/src/apps/cursorless-vscode/getMatchesInRange.ts @@ -0,0 +1,48 @@ +import { imap } from "itertools"; +import { Range, TextEditor } from "vscode"; +import { Direction } from "../../typings/targetDescriptor.types"; +import { matchAll } from "../../libs/cursorless-engine/util/regex"; + +export function getMatchesInRange( + regex: RegExp, + editor: TextEditor, + range: Range, +): Range[] { + const offset = editor.document.offsetAt(range.start); + const text = editor.document.getText(range); + + return matchAll( + text, + regex, + (match) => + new Range( + editor.document.positionAt(offset + match.index!), + editor.document.positionAt(offset + match.index! + match[0].length), + ), + ); +} + +export function generateMatchesInRange( + regex: RegExp, + editor: TextEditor, + range: Range, + direction: Direction, +): Iterable { + const offset = editor.document.offsetAt(range.start); + const text = editor.document.getText(range); + + const matchToRange = (match: RegExpMatchArray): Range => + new Range( + editor.document.positionAt(offset + match.index!), + editor.document.positionAt(offset + match.index! + match[0].length), + ); + + // Reset the regex to start at the beginning of string, in case the regex has + // been used before. + // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#finding_successive_matches + regex.lastIndex = 0; + + return direction === "forward" + ? imap(text.matchAll(regex), matchToRange) + : Array.from(text.matchAll(regex), matchToRange).reverse(); +} diff --git a/src/core/Cheatsheet.ts b/src/core/Cheatsheet.ts index eb98de5c0d..5457ba9aa0 100644 --- a/src/core/Cheatsheet.ts +++ b/src/core/Cheatsheet.ts @@ -5,6 +5,7 @@ import { Graph } from "../typings/Types"; import path = require("path"); import produce from "immer"; import { sortBy } from "lodash"; +import ide from "../libs/cursorless-engine/singletons/ide.singleton"; /** * The argument expected by the cheatsheet command. @@ -31,7 +32,7 @@ export default class Cheatsheet { private disposables: vscode.Disposable[] = []; constructor(private graph: Graph) { - graph.extensionContext.subscriptions.push(this); + ide().disposeOnExit(this); this.showCheatsheet = this.showCheatsheet.bind(this); this.updateDefaults = this.updateDefaults.bind(this); @@ -60,7 +61,7 @@ export default class Cheatsheet { } const cheatsheetPath = path.join( - this.graph.extensionContext.extensionPath, + ide().assetsRoot, "cursorless-nx", "dist", "apps", @@ -87,11 +88,12 @@ export default class Cheatsheet { * @param spokenFormInfo The new value to use for default spoken forms. */ private async updateDefaults(spokenFormInfo: CheatsheetInfo) { + const { runMode, assetsRoot, workspaceFolders } = ide(); + const workspacePath = - this.graph.extensionContext.extensionMode === - vscode.ExtensionMode.Development - ? this.graph.extensionContext.extensionPath - : vscode.workspace.workspaceFolders?.[0].uri.path ?? null; + runMode === "development" + ? assetsRoot + : workspaceFolders?.[0].uri.path ?? null; if (workspacePath == null) { throw new Error( diff --git a/src/core/Debug.ts b/src/core/Debug.ts index 7224e9300f..4716f771ae 100644 --- a/src/core/Debug.ts +++ b/src/core/Debug.ts @@ -1,14 +1,14 @@ import { Disposable, - ExtensionMode, Location, TextEditorSelectionChangeEvent, window, workspace, } from "vscode"; import { SyntaxNode, TreeCursor } from "web-tree-sitter"; +import ide from "../libs/cursorless-engine/singletons/ide.singleton"; import { Graph } from "../typings/Types"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; export default class Debug { private disposableConfiguration?: Disposable; @@ -16,23 +16,23 @@ export default class Debug { active: boolean; constructor(private graph: Graph) { - this.graph.extensionContext.subscriptions.push(this); + ide().disposeOnExit(this); this.evaluateSetting = this.evaluateSetting.bind(this); this.logBranchTypes = this.logBranchTypes.bind(this); this.active = true; - switch (this.graph.extensionContext.extensionMode) { + switch (ide().runMode) { // Development mode. Always enable. - case ExtensionMode.Development: + case "development": this.enableDebugLog(); break; // Test mode. Always disable. - case ExtensionMode.Test: + case "test": this.disableDebugLog(); break; // Production mode. Enable based on user setting. - case ExtensionMode.Production: + case "production": this.evaluateSetting(); this.disposableConfiguration = workspace.onDidChangeConfiguration( this.evaluateSetting, diff --git a/src/core/Decorations.ts b/src/core/Decorations.ts index 761ef165b1..08fe3cf770 100644 --- a/src/core/Decorations.ts +++ b/src/core/Decorations.ts @@ -40,10 +40,9 @@ export default class Decorations { hatStyleNames!: HatStyleName[]; private decorationChangeListeners: DecorationChangeListener[] = []; private disposables: vscode.Disposable[] = []; + private extensionContext!: vscode.ExtensionContext; constructor(private graph: Graph) { - graph.extensionContext.subscriptions.push(this); - this.recomputeDecorationStyles = this.recomputeDecorationStyles.bind(this); this.disposables.push( @@ -60,7 +59,9 @@ export default class Decorations { ); } - async init() { + async init(extensionContext: vscode.ExtensionContext) { + this.extensionContext = extensionContext; + extensionContext.subscriptions.push(this); await this.graph.fontMeasurements.calculate(); this.constructDecorations(this.graph.fontMeasurements); } @@ -272,7 +273,7 @@ export default class Decorations { hatVerticalOffsetEm: number, ) { const iconPath = join( - this.graph.extensionContext.extensionPath, + this.extensionContext.extensionPath, "images", "hats", `${shape}.svg`, diff --git a/src/core/FontMeasurements.ts b/src/core/FontMeasurements.ts index 177c6fa529..9141dbd3c1 100644 --- a/src/core/FontMeasurements.ts +++ b/src/core/FontMeasurements.ts @@ -18,16 +18,32 @@ export default class FontMeasurements { */ characterHeight!: number; + private extensionContext!: vscode.ExtensionContext; + private initialized = false; + constructor(private graph: Graph) {} + init(extensionContext: vscode.ExtensionContext) { + this.extensionContext = extensionContext; + this.initialized = true; + } + clearCache() { - this.graph.extensionContext.globalState.update("fontRatios", undefined); + if (!this.initialized) { + throw Error("Font measurements used before initialization"); + } + + this.extensionContext.globalState.update("fontRatios", undefined); } async calculate() { + if (!this.initialized) { + throw Error("Font measurements used before initialization"); + } + const fontFamily = getFontFamily(); let widthRatio, heightRatio; - const fontRatiosCache = this.graph.extensionContext.globalState.get<{ + const fontRatiosCache = this.extensionContext.globalState.get<{ widthRatio: number; heightRatio: number; fontFamily: string; @@ -35,7 +51,7 @@ export default class FontMeasurements { if (fontRatiosCache == null || fontRatiosCache.fontFamily !== fontFamily) { const fontRatios = await getFontRatios(); - this.graph.extensionContext.globalState.update("fontRatios", { + this.extensionContext.globalState.update("fontRatios", { ...fontRatios, fontFamily, }); diff --git a/src/core/HatAllocator.ts b/src/core/HatAllocator.ts index c0b53faef8..3995aef63e 100644 --- a/src/core/HatAllocator.ts +++ b/src/core/HatAllocator.ts @@ -1,5 +1,7 @@ import * as vscode from "vscode"; import { Disposable } from "vscode"; +import ide from "../libs/cursorless-engine/singletons/ide.singleton"; +import tokenGraphemeSplitter from "../libs/cursorless-engine/singletons/tokenGraphemeSplitter.singleton"; import { Graph } from "../typings/Types"; import { addDecorationsToEditors } from "../util/addDecorationsToEditor"; import { IndividualHatMap } from "./IndividualHatMap"; @@ -15,7 +17,7 @@ export class HatAllocator { private disposalFunctions: (() => void)[] = []; constructor(private graph: Graph, private context: Context) { - graph.extensionContext.subscriptions.push(this); + ide().disposeOnExit(this); this.isActive = vscode.workspace .getConfiguration("cursorless") @@ -57,7 +59,7 @@ export class HatAllocator { ), // Re-draw hats on grapheme splitting algorithm change in case they // changed their token hat splitting setting. - graph.tokenGraphemeSplitter.registerAlgorithmChangeListener( + tokenGraphemeSplitter().registerAlgorithmChangeListener( this.addDecorationsDebounced, ), ); @@ -76,7 +78,7 @@ export class HatAllocator { addDecorationsToEditors( activeMap, this.graph.decorations, - this.graph.tokenGraphemeSplitter, + tokenGraphemeSplitter(), ); } else { vscode.window.visibleTextEditors.forEach(this.clearEditorDecorations); diff --git a/src/core/HatTokenMap.ts b/src/core/HatTokenMap.ts index bf7a64dbe2..7eb85a872d 100644 --- a/src/core/HatTokenMap.ts +++ b/src/core/HatTokenMap.ts @@ -1,9 +1,9 @@ -import { HatStyleName } from "./hatStyles"; -import { Graph } from "../typings/Types"; -import { IndividualHatMap, ReadOnlyHatMap } from "./IndividualHatMap"; -import { HatAllocator } from "./HatAllocator"; import { hrtime } from "process"; +import ide from "../libs/cursorless-engine/singletons/ide.singleton"; +import type { Graph } from "../typings/Types"; import { abs } from "../util/bigint"; +import { HatAllocator } from "./HatAllocator"; +import { IndividualHatMap, ReadOnlyHatMap } from "./IndividualHatMap"; /** * Maximum age for the pre-phrase snapshot before we consider it to be stale @@ -32,7 +32,7 @@ export default class HatTokenMap { private hatAllocator: HatAllocator; constructor(private graph: Graph) { - graph.extensionContext.subscriptions.push(this); + ide().disposeOnExit(this); this.activeMap = new IndividualHatMap(graph); this.getActiveMap = this.getActiveMap.bind(this); @@ -50,21 +50,6 @@ export default class HatTokenMap { return this.hatAllocator.addDecorations(); } - static getKey(hatStyle: HatStyleName, character: string) { - return `${hatStyle}.${character}`; - } - - static splitKey(key: string) { - const [hatStyle, character] = key.split("."); - - return { - hatStyle: hatStyle as HatStyleName, - // If the character is `.` then it will appear as a zero length string - // due to the way the split on `.` works - character: character.length === 0 ? "." : character, - }; - } - private async getActiveMap() { // NB: We need to take a snapshot of the hat map before we make any // modifications if it is the beginning of the phrase diff --git a/src/core/IndividualHatMap.ts b/src/core/IndividualHatMap.ts index 028392a045..4ab9fe6d7b 100644 --- a/src/core/IndividualHatMap.ts +++ b/src/core/IndividualHatMap.ts @@ -1,7 +1,8 @@ +import { getKey } from "@cursorless/common"; import { TextDocument } from "vscode"; -import { HatStyleName } from "./hatStyles"; +import tokenGraphemeSplitter from "../libs/cursorless-engine/singletons/tokenGraphemeSplitter.singleton"; import { Graph, Token } from "../typings/Types"; -import HatTokenMap from "./HatTokenMap"; +import { HatStyleName } from "./hatStyles"; export interface ReadOnlyHatMap { getEntries(): [string, Token][]; @@ -55,16 +56,13 @@ export class IndividualHatMap implements ReadOnlyHatMap { } addToken(hatStyle: HatStyleName, character: string, token: Token) { - this.addTokenByKey(HatTokenMap.getKey(hatStyle, character), token); + this.addTokenByKey(getKey(hatStyle, character), token); } getToken(hatStyle: HatStyleName, character: string) { this.checkExpired(); return this.map[ - HatTokenMap.getKey( - hatStyle, - this.graph.tokenGraphemeSplitter.normalizeGrapheme(character), - ) + getKey(hatStyle, tokenGraphemeSplitter().normalizeGrapheme(character)) ]; } diff --git a/src/core/Snippets.ts b/src/core/Snippets.ts index 9ca837b12f..09bbc164ca 100644 --- a/src/core/Snippets.ts +++ b/src/core/Snippets.ts @@ -1,8 +1,8 @@ import { readFile, stat } from "fs/promises"; import { cloneDeep, max, merge } from "lodash"; import { join } from "path"; -import { window, workspace } from "vscode"; -import isTesting from "../testUtil/isTesting"; +import { window } from "vscode"; +import ide from "../libs/cursorless-engine/singletons/ide.singleton"; import { walkFiles } from "../testUtil/walkAsync"; import { Snippet, SnippetMap } from "../typings/snippet"; import { Graph } from "../typings/Types"; @@ -62,8 +62,8 @@ export class Snippets { SNIPPET_DIR_REFRESH_INTERVAL_MS, ); - graph.extensionContext.subscriptions.push( - workspace.onDidChangeConfiguration(() => { + ide().disposeOnExit( + ide().configuration.onDidChangeConfiguration(() => { if (this.updateUserSnippetsPath()) { this.updateUserSnippets(); } @@ -77,7 +77,7 @@ export class Snippets { } async init() { - const extensionPath = this.graph.extensionContext.extensionPath; + const extensionPath = ide().assetsRoot; const snippetsDir = join(extensionPath, "cursorless-snippets"); const snippetFiles = await getSnippetPaths(snippetsDir); this.coreSnippets = mergeStrict( @@ -97,22 +97,9 @@ export class Snippets { * @returns Boolean indicating whether path has changed */ private updateUserSnippetsPath(): boolean { - let newUserSnippetsDir: string | undefined; - - if (isTesting()) { - newUserSnippetsDir = join( - this.graph.extensionContext.extensionPath, - "src", - "test", - "suite", - "fixtures", - "cursorless-snippets", - ); - } else { - newUserSnippetsDir = workspace - .getConfiguration("cursorless.experimental") - .get("snippetsDir"); - } + const newUserSnippetsDir = ide().configuration.getOwnConfiguration( + "experimental.snippetsDir", + ); if (newUserSnippetsDir === this.userSnippetsDir) { return false; diff --git a/src/core/StatusBarItem.ts b/src/core/StatusBarItem.ts index 5980f16edc..68de344eba 100644 --- a/src/core/StatusBarItem.ts +++ b/src/core/StatusBarItem.ts @@ -1,11 +1,12 @@ import * as vscode from "vscode"; +import ide from "../libs/cursorless-engine/singletons/ide.singleton"; import { Graph } from "../typings/Types"; export default class StatusBarItem { private disposables: vscode.Disposable[] = []; constructor(private graph: Graph) { - graph.extensionContext.subscriptions.push(this); + ide().disposeOnExit(this); } init() { diff --git a/src/core/commandRunner/CommandRunner.ts b/src/core/commandRunner/CommandRunner.ts index 2bf2637184..26f62ef8a1 100644 --- a/src/core/commandRunner/CommandRunner.ts +++ b/src/core/commandRunner/CommandRunner.ts @@ -1,8 +1,9 @@ import * as vscode from "vscode"; import { ActionType } from "../../actions/actions.types"; -import { CURSORLESS_COMMAND_ID } from "../../common/commandIds"; +import { CURSORLESS_COMMAND_ID } from "../../libs/common/commandIds"; import { OutdatedExtensionError } from "../../errors"; -import { getActiveTextEditor } from "../../ide/activeTextEditor"; +import ide from "../../libs/cursorless-engine/singletons/ide.singleton"; +import { getActiveTextEditor } from "../../ide/vscode/activeTextEditor"; import processTargets from "../../processTargets"; import isTesting from "../../testUtil/isTesting"; import { Target } from "../../typings/target.types"; @@ -31,7 +32,7 @@ export default class CommandRunner { private thatMark: ThatMark, private sourceMark: ThatMark, ) { - graph.extensionContext.subscriptions.push(this); + ide().disposeOnExit(this); this.runCommandBackwardCompatible = this.runCommandBackwardCompatible.bind(this); diff --git a/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts b/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts index 215a3b2069..bca747a9e3 100644 --- a/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts +++ b/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts @@ -1,5 +1,6 @@ import { ActionType } from "../../actions/actions.types"; import { OutdatedExtensionError } from "../../errors"; +import ide from "../../libs/cursorless-engine/singletons/ide.singleton"; import { Modifier, PartialTargetDescriptor, @@ -126,19 +127,18 @@ export async function checkForOldInference( }); if (hasOldInference) { - const hideInferenceWarning = graph.ide.globalState.get( - "hideInferenceWarning", - ); + const { globalState, messages } = ide(); + const hideInferenceWarning = globalState.get("hideInferenceWarning"); if (!hideInferenceWarning) { - const pressed = await graph.ide.messages.showWarning( + const pressed = await messages.showWarning( "deprecatedPositionInference", 'The "past start of" / "past end of" form has changed behavior. For the old behavior, update cursorless-talon (https://www.cursorless.org/docs/user/updating/), and then you can now say "past start of its" / "past end of its". For example, "take air past end of its line". You may also consider using "head" / "tail" instead; see https://www.cursorless.org/docs/#head-and-tail', "Don't show again", ); if (pressed) { - graph.ide.globalState.set("hideInferenceWarning", true); + globalState.set("hideInferenceWarning", true); } } } diff --git a/src/core/editStyles.ts b/src/core/editStyles.ts index 396e42e288..b8da397529 100644 --- a/src/core/editStyles.ts +++ b/src/core/editStyles.ts @@ -9,10 +9,11 @@ import { window, workspace, } from "vscode"; +import ide from "../libs/cursorless-engine/singletons/ide.singleton"; import isTesting from "../testUtil/isTesting"; import { Target } from "../typings/target.types"; import { Graph, RangeWithEditor } from "../typings/Types"; -import sleep from "../util/sleep"; +import sleep from "../libs/common/util/sleep"; import { getContentRange, runForEachEditor, @@ -82,7 +83,7 @@ export class EditStyles implements Record { this[editStyleName] = new EditStyle(`${editStyleName}Background`); }); - graph.extensionContext.subscriptions.push(this); + ide().disposeOnExit(this); } async displayPendingEditDecorations( diff --git a/src/core/tokenizerConfiguration.ts b/src/core/tokenizerConfiguration.ts deleted file mode 100644 index 080dba0be0..0000000000 --- a/src/core/tokenizerConfiguration.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * TODO: This is just an ugly mock since that tokenizer doesn't have access to the graph/ide - * Remove this once https://github.com/cursorless-dev/cursorless/issues/785 is implemented - */ - -import * as vscode from "vscode"; - -const defaultGetWordSeparators = (languageId: string) => { - // FIXME: The reason this code will auto-reload on settings change is that we don't use fine-grained settings listener in `Decorations`: - // https://github.com/cursorless-dev/cursorless/blob/c914d477c9624c498a47c964088b34e484eac494/src/core/Decorations.ts#L58 - return vscode.workspace - .getConfiguration("cursorless", { languageId }) - .get("wordSeparators", ["_"]); -}; - -let getWordSeparators = defaultGetWordSeparators; - -export const tokenizerConfiguration = { - getWordSeparators: (languageId: string) => { - return getWordSeparators(languageId); - }, - // For testing purposes, we override the word separator in a few languages to - // make sure that overriding the word separator works. Note that in - // production, we tokenize all languages the same way by default. - mockWordSeparators: () => { - getWordSeparators = (languageId: string) => { - switch (languageId) { - case "css": - case "scss": - case "shellscript": - return ["-", "_"]; - } - return ["_"]; - }; - }, -}; diff --git a/src/core/updateSelections/getOffsetsForEmptyRangeInsert.ts b/src/core/updateSelections/getOffsetsForEmptyRangeInsert.ts index 5c92a3baee..61463ac09f 100644 --- a/src/core/updateSelections/getOffsetsForEmptyRangeInsert.ts +++ b/src/core/updateSelections/getOffsetsForEmptyRangeInsert.ts @@ -1,5 +1,8 @@ import { invariant } from "immutability-helper"; -import { leftAnchored, rightAnchored } from "../../util/regex"; +import { + leftAnchored, + rightAnchored, +} from "../../libs/cursorless-engine/util/regex"; import { ChangeEventInfo, FullRangeInfo, diff --git a/src/core/updateSelections/getOffsetsForNonEmptyRangeInsert.ts b/src/core/updateSelections/getOffsetsForNonEmptyRangeInsert.ts index d5f3c893ca..306de52b35 100644 --- a/src/core/updateSelections/getOffsetsForNonEmptyRangeInsert.ts +++ b/src/core/updateSelections/getOffsetsForNonEmptyRangeInsert.ts @@ -1,5 +1,8 @@ import { invariant } from "immutability-helper"; -import { leftAnchored, rightAnchored } from "../../util/regex"; +import { + leftAnchored, + rightAnchored, +} from "../../libs/cursorless-engine/util/regex"; import { ChangeEventInfo, FullRangeInfo, diff --git a/src/extension.ts b/src/extension.ts index 574f04e5a9..dc163b274a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,10 +1,19 @@ import * as vscode from "vscode"; import CommandRunner from "./core/commandRunner/CommandRunner"; import { ThatMark } from "./core/ThatMark"; -import { tokenizerConfiguration } from "./core/tokenizerConfiguration"; +import VscodeIDE from "./ide/vscode/VscodeIDE"; +import FakeIDE from "./libs/common/ide/fake/FakeIDE"; +import ide, { + injectIde, +} from "./libs/cursorless-engine/singletons/ide.singleton"; +import { + CursorlessApi, + getCommandServerApi, + getParseTreeApi, +} from "./libs/vscode-common/getExtensionApi"; +import { plainObjectToTarget } from "./testUtil/fromPlainObject"; import isTesting from "./testUtil/isTesting"; import { Graph } from "./typings/Types"; -import { getCommandServerApi, getParseTreeApi } from "./util/getExtensionApi"; import graphFactories from "./util/graphFactories"; import makeGraph, { FactoryMap } from "./util/makeGraph"; @@ -16,22 +25,32 @@ import makeGraph, { FactoryMap } from "./util/makeGraph"; * use to record test cases. * - Creates an entrypoint for running commands {@link CommandRunner}. */ -export async function activate(context: vscode.ExtensionContext) { +export async function activate( + context: vscode.ExtensionContext, +): Promise { const { getNodeAtLocation } = await getParseTreeApi(); const commandServerApi = await getCommandServerApi(); - const graph = makeGraph( - { - ...graphFactories, - extensionContext: () => context, - commandServerApi: () => commandServerApi, - getNodeAtLocation: () => getNodeAtLocation, - } as FactoryMap, - ["ide"], - ); + if (isTesting()) { + // FIXME: At some point we'll probably want to support partial mocking + // rather than mocking away everything that we can + const fake = new FakeIDE(); + fake.mockAssetsRoot(context.extensionPath); + injectIde(fake); + } else { + injectIde(new VscodeIDE(context)); + } + + const graph = makeGraph({ + ...graphFactories, + extensionContext: () => context, + commandServerApi: () => commandServerApi, + getNodeAtLocation: () => getNodeAtLocation, + } as FactoryMap); graph.debug.init(); graph.snippets.init(); - await graph.decorations.init(); + graph.fontMeasurements.init(context); + await graph.decorations.init(context); graph.hatTokenMap.init(); graph.testCaseRecorder.init(); graph.cheatsheet.init(); @@ -43,15 +62,21 @@ export async function activate(context: vscode.ExtensionContext) { // TODO: Do this using the graph once we migrate its dependencies onto the graph new CommandRunner(graph, thatMark, sourceMark); - // TODO: Remove this once tokenizer has access to graph - if (isTesting()) { - tokenizerConfiguration.mockWordSeparators(); - } - return { thatMark, sourceMark, - graph: isTesting() ? graph : undefined, + testHelpers: isTesting() + ? { + graph, + ide: ide() as FakeIDE, + injectIde, + + // FIXME: Remove this once we have a better way to get this function + // accessible from our tests + plainObjectToTarget, + } + : undefined, + experimental: { registerThirdPartySnippets: graph.snippets.registerThirdPartySnippets, }, diff --git a/src/ide/ide.types.ts b/src/ide/ide.types.ts deleted file mode 100644 index 8eaec6796f..0000000000 --- a/src/ide/ide.types.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { TokenHatSplittingMode } from "../typings/Types"; -import type { Listener } from "../util/Notifier"; - -export interface IDE { - configuration: Configuration; - messages: Messages; - globalState: State; - - /** - * Register disposables to be disposed of on IDE exit. - * - * @param disposables A list of {@link Disposable}s to dispose when the IDE is exited. - * @returns A function that can be called to deregister the disposables - */ - disposeOnExit(...disposables: Disposable[]): () => void; -} - -export interface CursorlessConfiguration { - tokenHatSplittingMode: TokenHatSplittingMode; -} -export type CursorlessConfigKey = keyof CursorlessConfiguration; - -export interface Configuration { - getOwnConfiguration( - key: T, - ): CursorlessConfiguration[T] | undefined; - onDidChangeConfiguration(listener: Listener): Disposable; -} - -export type MessageId = string; - -export interface Messages { - /** - * Displays a warning message {@link message} to the user along with possible - * {@link options} for them to select. - * - * @param id Each code site where we issue a warning should have a unique, - * human readable id for testability, eg "deprecatedPositionInference". This - * allows us to write tests without tying ourself to the specific wording of - * the warning message provided in {@link message}. - * @param message The message to display to the user - * @param options A list of options to display to the user. The selected - * option will be returned by this function - * @returns The option selected by the user, or `undefined` if no option was - * selected - */ - showWarning( - id: MessageId, - message: string, - ...options: string[] - ): Promise; -} - -export interface Disposable { - dispose(): void; -} - -/** - * A mapping from allowable state keys to their default values - */ -export const STATE_KEYS = { hideInferenceWarning: false }; -export type StateType = typeof STATE_KEYS; -export type StateKey = keyof StateType; - -/** - * A state represents a storage utility. It can store and retrieve - * values. - */ -export interface State { - /** - * Return a value. - * - * @param key A string. - * @return The stored value or the defaultValue. - */ - get(key: StateKey): StateType[StateKey]; - - /** - * Store a value. The value must be JSON-stringifyable. - * - * @param key A string. - * @param value A value. MUST not contain cyclic references. - */ - set(key: StateKey, value: StateType[StateKey]): Thenable; -} diff --git a/src/ide/vscode/VscodeClipboard.ts b/src/ide/vscode/VscodeClipboard.ts new file mode 100644 index 0000000000..0e806483df --- /dev/null +++ b/src/ide/vscode/VscodeClipboard.ts @@ -0,0 +1,12 @@ +import * as vscode from "vscode"; +import { Clipboard } from "../../libs/common/ide/types/Clipboard"; + +export default class VscodeClipboard implements Clipboard { + readText(): Thenable { + return vscode.env.clipboard.readText(); + } + + writeText(value: string): Thenable { + return vscode.env.clipboard.writeText(value); + } +} diff --git a/src/ide/vscode/VscodeConfiguration.ts b/src/ide/vscode/VscodeConfiguration.ts index 9b40244c86..472a48aab5 100644 --- a/src/ide/vscode/VscodeConfiguration.ts +++ b/src/ide/vscode/VscodeConfiguration.ts @@ -1,10 +1,11 @@ import * as vscode from "vscode"; -import { Notifier } from "../../util/Notifier"; import { Configuration, - CursorlessConfigKey, + ConfigurationScope, CursorlessConfiguration, -} from "../ide.types"; +} from "../../libs/common/ide/types/Configuration"; +import { GetFieldType, Paths } from "../../libs/common/ide/types/Paths"; +import { Notifier } from "../../libs/common/util/Notifier"; import type VscodeIDE from "./VscodeIDE"; export default class VscodeConfiguration implements Configuration { @@ -18,12 +19,13 @@ export default class VscodeConfiguration implements Configuration { ); } - getOwnConfiguration( - key: T, - ): CursorlessConfiguration[T] | undefined { + getOwnConfiguration>( + path: Path, + scope?: ConfigurationScope, + ): GetFieldType { return vscode.workspace - .getConfiguration("cursorless") - .get(key); + .getConfiguration("cursorless", scope) + .get>(path)!; } onDidChangeConfiguration = this.notifier.registerListener; diff --git a/src/ide/vscode/VscodeGlobalState.ts b/src/ide/vscode/VscodeGlobalState.ts index 6a4308d866..4624b28c28 100644 --- a/src/ide/vscode/VscodeGlobalState.ts +++ b/src/ide/vscode/VscodeGlobalState.ts @@ -1,5 +1,10 @@ import { ExtensionContext } from "vscode"; -import { State, StateKey, StateType, STATE_KEYS } from "../ide.types"; +import { + State, + StateKey, + StateType, + STATE_KEYS, +} from "../../libs/common/ide/types/State"; export default class VscodeGlobalState implements State { constructor(private extensionContext: ExtensionContext) { diff --git a/src/ide/vscode/VscodeIDE.ts b/src/ide/vscode/VscodeIDE.ts index 18f7f93816..0768181d39 100644 --- a/src/ide/vscode/VscodeIDE.ts +++ b/src/ide/vscode/VscodeIDE.ts @@ -1,25 +1,54 @@ import { pull } from "lodash"; -import { Graph } from "../../typings/Types"; -import { Disposable, IDE } from "../ide.types"; +import { + ExtensionContext, + ExtensionMode, + workspace, + WorkspaceFolder, +} from "vscode"; +import { + Disposable, + IDE, + RunMode, +} from "../../libs/common/ide/types/ide.types"; +import VscodeClipboard from "./VscodeClipboard"; import VscodeConfiguration from "./VscodeConfiguration"; -import VscodeMessages from "./VscodeMessages"; import VscodeGlobalState from "./VscodeGlobalState"; +import VscodeMessages from "./VscodeMessages"; + +const EXTENSION_MODE_MAP: Record = { + [ExtensionMode.Development]: "development", + [ExtensionMode.Production]: "production", + [ExtensionMode.Test]: "test", +}; export default class VscodeIDE implements IDE { configuration: VscodeConfiguration; globalState: VscodeGlobalState; messages: VscodeMessages; + clipboard: VscodeClipboard; - constructor(private graph: Graph) { + constructor(private extensionContext: ExtensionContext) { this.configuration = new VscodeConfiguration(this); - this.globalState = new VscodeGlobalState(graph.extensionContext); + this.globalState = new VscodeGlobalState(extensionContext); this.messages = new VscodeMessages(); + this.clipboard = new VscodeClipboard(); + } + + get assetsRoot(): string { + return this.extensionContext.extensionPath; + } + + get runMode(): RunMode { + return EXTENSION_MODE_MAP[this.extensionContext.extensionMode]; + } + + get workspaceFolders(): readonly WorkspaceFolder[] | undefined { + return workspace.workspaceFolders; } disposeOnExit(...disposables: Disposable[]): () => void { - this.graph.extensionContext.subscriptions.push(...disposables); + this.extensionContext.subscriptions.push(...disposables); - return () => - pull(this.graph.extensionContext.subscriptions, ...disposables); + return () => pull(this.extensionContext.subscriptions, ...disposables); } } diff --git a/src/ide/vscode/VscodeMessages.ts b/src/ide/vscode/VscodeMessages.ts index 3a73845733..4872a25e8a 100644 --- a/src/ide/vscode/VscodeMessages.ts +++ b/src/ide/vscode/VscodeMessages.ts @@ -1,5 +1,5 @@ import { window } from "vscode"; -import { MessageId, Messages } from "../ide.types"; +import { MessageId, Messages } from "../../libs/common/ide/types/Messages"; export default class VscodeMessages implements Messages { async showWarning( diff --git a/src/ide/activeTextEditor.ts b/src/ide/vscode/activeTextEditor.ts similarity index 100% rename from src/ide/activeTextEditor.ts rename to src/ide/vscode/activeTextEditor.ts diff --git a/src/languages/getNodeMatcher.ts b/src/languages/getNodeMatcher.ts index 1668bca35d..fe1a13b90f 100644 --- a/src/languages/getNodeMatcher.ts +++ b/src/languages/getNodeMatcher.ts @@ -24,7 +24,7 @@ import latex from "./latex"; import { patternMatchers as ruby } from "./ruby"; import rust from "./rust"; import { UnsupportedLanguageError } from "../errors"; -import { SupportedLanguageId } from "./constants"; +import { SupportedLanguageId } from "../libs/cursorless-engine/languages/constants"; export function getNodeMatcher( languageId: string, diff --git a/src/languages/getTextFragmentExtractor.ts b/src/languages/getTextFragmentExtractor.ts index 214f4fb5e2..bdcee7c9e0 100644 --- a/src/languages/getTextFragmentExtractor.ts +++ b/src/languages/getTextFragmentExtractor.ts @@ -8,7 +8,7 @@ import { stringTextFragmentExtractor as typescriptStringTextFragmentExtractor } import { stringTextFragmentExtractor as scssStringTextFragmentExtractor } from "./scss"; import { UnsupportedLanguageError } from "../errors"; import { Range } from "vscode"; -import { SupportedLanguageId } from "./constants"; +import { SupportedLanguageId } from "../libs/cursorless-engine/languages/constants"; import { getNodeInternalRange, getNodeRange } from "../util/nodeSelectors"; import { getNodeMatcher } from "./getNodeMatcher"; import { notSupported } from "../util/nodeMatchers"; diff --git a/src/languages/index.ts b/src/languages/index.ts index eb3b766e4c..d33d3d3fde 100644 --- a/src/languages/index.ts +++ b/src/languages/index.ts @@ -1,4 +1,7 @@ -import { SupportedLanguageId, supportedLanguageIds } from "./constants"; +import { + SupportedLanguageId, + supportedLanguageIds, +} from "../libs/cursorless-engine/languages/constants"; export function isLanguageSupported( languageId: string, diff --git a/src/languages/markdown.ts b/src/languages/markdown.ts index 37fb01f0d5..b8fcb3cf0c 100644 --- a/src/languages/markdown.ts +++ b/src/languages/markdown.ts @@ -13,7 +13,7 @@ import { getNodeRange, selectWithLeadingDelimiter, } from "../util/nodeSelectors"; -import { getMatchesInRange } from "../util/regex"; +import { getMatchesInRange } from "../apps/cursorless-vscode/getMatchesInRange"; import { isReversed, selectionFromRange, diff --git a/src/libs/common/.eslintrc.json b/src/libs/common/.eslintrc.json new file mode 100644 index 0000000000..1444e1b536 --- /dev/null +++ b/src/libs/common/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../*"], + "message": "Common shouldn't have any dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Common shouldn't have any dependencies" + }, + { + "name": "@cursorless/vscode-common", + "message": "Common shouldn't have any dependencies" + } + ] + } + ] + } +} diff --git a/src/common/commandIds.ts b/src/libs/common/commandIds.ts similarity index 100% rename from src/common/commandIds.ts rename to src/libs/common/commandIds.ts diff --git a/src/libs/common/ide/.eslintrc.json b/src/libs/common/ide/.eslintrc.json new file mode 100644 index 0000000000..90c68065b1 --- /dev/null +++ b/src/libs/common/ide/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Common shouldn't have any dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Common shouldn't have any dependencies" + }, + { + "name": "@cursorless/vscode-common", + "message": "Common shouldn't have any dependencies" + } + ] + } + ] + } +} diff --git a/src/libs/common/ide/fake/.eslintrc.json b/src/libs/common/ide/fake/.eslintrc.json new file mode 100644 index 0000000000..e992a85235 --- /dev/null +++ b/src/libs/common/ide/fake/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../../*"], + "message": "Common shouldn't depend on Cursorless extension" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Common shouldn't depend on vscode" + }, + { + "name": "@cursorless/vscode-common", + "message": "Common shouldn't have any dependencies" + } + ] + } + ] + } +} diff --git a/src/libs/common/ide/fake/FakeClipboard.ts b/src/libs/common/ide/fake/FakeClipboard.ts new file mode 100644 index 0000000000..94926124e7 --- /dev/null +++ b/src/libs/common/ide/fake/FakeClipboard.ts @@ -0,0 +1,13 @@ +import type { Clipboard } from "../types/Clipboard"; + +export default class FakeClipboard implements Clipboard { + private clipboardContents: string = ""; + + async readText(): Promise { + return this.clipboardContents; + } + + async writeText(value: string): Promise { + this.clipboardContents = value; + } +} diff --git a/src/libs/common/ide/fake/FakeConfiguration.ts b/src/libs/common/ide/fake/FakeConfiguration.ts new file mode 100644 index 0000000000..a879cfb208 --- /dev/null +++ b/src/libs/common/ide/fake/FakeConfiguration.ts @@ -0,0 +1,70 @@ +import { get } from "lodash"; +import { Notifier } from "../../util/Notifier"; +import { + Configuration, + ConfigurationScope, + CONFIGURATION_DEFAULTS, + CursorlessConfigKey, + CursorlessConfiguration, +} from "../types/Configuration"; +import { GetFieldType, Paths } from "../types/Paths"; + +interface ConfigurationScopeValues { + scope: ConfigurationScope; + values: Partial; +} + +export default class FakeConfiguration implements Configuration { + private notifier = new Notifier(); + private mocks: CursorlessConfiguration = { + ...CONFIGURATION_DEFAULTS, + }; + private scopes: ConfigurationScopeValues[] = []; + + constructor() { + this.onDidChangeConfiguration = this.onDidChangeConfiguration.bind(this); + } + + getOwnConfiguration>( + path: Path, + scope?: ConfigurationScope, + ): GetFieldType { + if (scope != null) { + for (const { scope: candidateScope, values } of this.scopes) { + if (scopeMatches(candidateScope, scope)) { + return get(values, path) ?? get(this.mocks, path); + } + } + } + + return get(this.mocks, path); + } + + onDidChangeConfiguration = this.notifier.registerListener; + + mockConfiguration( + key: T, + value: CursorlessConfiguration[T], + ): void { + this.mocks[key] = value; + this.notifier.notifyListeners(); + } + + mockConfigurationScope( + scope: ConfigurationScope, + values: Partial, + noNotification: boolean = false, + ): void { + this.scopes.push({ scope, values }); + if (!noNotification) { + this.notifier.notifyListeners(); + } + } +} + +function scopeMatches( + candidateScope: ConfigurationScope, + scope: ConfigurationScope, +): boolean { + return candidateScope.languageId === scope.languageId; +} diff --git a/src/test/suite/fakes/ide/FakeGlobalState.ts b/src/libs/common/ide/fake/FakeGlobalState.ts similarity index 79% rename from src/test/suite/fakes/ide/FakeGlobalState.ts rename to src/libs/common/ide/fake/FakeGlobalState.ts index f8ac2ff233..07bc7ca302 100644 --- a/src/test/suite/fakes/ide/FakeGlobalState.ts +++ b/src/libs/common/ide/fake/FakeGlobalState.ts @@ -1,9 +1,4 @@ -import { - State, - StateKey, - StateType, - STATE_KEYS, -} from "../../../../ide/ide.types"; +import { State, StateKey, StateType, STATE_KEYS } from "../types/State"; export default class FakeGlobalState implements State { private readonly data: Map = new Map(); diff --git a/src/test/suite/fakes/ide/FakeIDE.ts b/src/libs/common/ide/fake/FakeIDE.ts similarity index 51% rename from src/test/suite/fakes/ide/FakeIDE.ts rename to src/libs/common/ide/fake/FakeIDE.ts index 2cf9596c58..f490552122 100644 --- a/src/test/suite/fakes/ide/FakeIDE.ts +++ b/src/libs/common/ide/fake/FakeIDE.ts @@ -1,6 +1,11 @@ import { pull } from "lodash"; -import { Disposable, IDE } from "../../../../ide/ide.types"; -import { Graph } from "../../../../typings/Types"; +import type { + Disposable, + IDE, + RunMode, + WorkspaceFolder, +} from "../types/ide.types"; +import FakeClipboard from "./FakeClipboard"; import FakeConfiguration from "./FakeConfiguration"; import FakeGlobalState from "./FakeGlobalState"; import FakeMessages from "./FakeMessages"; @@ -9,14 +14,33 @@ export default class FakeIDE implements IDE { configuration: FakeConfiguration; messages: FakeMessages; globalState: FakeGlobalState; + clipboard: FakeClipboard; private disposables: Disposable[] = []; - constructor(graph: Graph) { - this.configuration = new FakeConfiguration(graph); + constructor() { + this.configuration = new FakeConfiguration(); this.messages = new FakeMessages(); this.globalState = new FakeGlobalState(); + this.clipboard = new FakeClipboard(); } + private assetsRoot_: string | undefined; + + mockAssetsRoot(_assetsRoot: string) { + this.assetsRoot_ = _assetsRoot; + } + + get assetsRoot(): string { + if (this.assetsRoot_ == null) { + throw Error("Field `assetsRoot` has not yet been mocked"); + } + + return this.assetsRoot_; + } + + runMode: RunMode = "test"; + workspaceFolders: readonly WorkspaceFolder[] | undefined = undefined; + disposeOnExit(...disposables: Disposable[]): () => void { this.disposables.push(...disposables); @@ -27,22 +51,3 @@ export default class FakeIDE implements IDE { this.disposables.forEach((disposable) => disposable.dispose()); } } - -export interface FakeInfo extends Disposable { - fake: FakeIDE; -} - -export function injectFakeIde(graph: Graph): FakeInfo { - const original = graph.ide; - const fake = new FakeIDE(graph); - - graph.ide = fake; - - return { - fake, - - dispose() { - graph.ide = original; - }, - }; -} diff --git a/src/test/suite/fakes/ide/FakeMessages.ts b/src/libs/common/ide/fake/FakeMessages.ts similarity index 77% rename from src/test/suite/fakes/ide/FakeMessages.ts rename to src/libs/common/ide/fake/FakeMessages.ts index 7ca5d9e937..e7dfbae429 100644 --- a/src/test/suite/fakes/ide/FakeMessages.ts +++ b/src/libs/common/ide/fake/FakeMessages.ts @@ -1,4 +1,4 @@ -import { Messages } from "../../../../ide/ide.types"; +import type { Messages } from "../types/Messages"; export default class FakeMessages implements Messages { async showWarning( diff --git a/src/libs/common/ide/spy/.eslintrc.json b/src/libs/common/ide/spy/.eslintrc.json new file mode 100644 index 0000000000..e992a85235 --- /dev/null +++ b/src/libs/common/ide/spy/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../../*"], + "message": "Common shouldn't depend on Cursorless extension" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Common shouldn't depend on vscode" + }, + { + "name": "@cursorless/vscode-common", + "message": "Common shouldn't have any dependencies" + } + ] + } + ] + } +} diff --git a/src/ide/spies/SpyIDE.ts b/src/libs/common/ide/spy/SpyIDE.ts similarity index 59% rename from src/ide/spies/SpyIDE.ts rename to src/libs/common/ide/spy/SpyIDE.ts index 135c5676c3..50536ca664 100644 --- a/src/ide/spies/SpyIDE.ts +++ b/src/libs/common/ide/spy/SpyIDE.ts @@ -1,7 +1,14 @@ import { pickBy, values } from "lodash"; -import { Graph } from "../../typings/Types"; -import { Configuration, Disposable, IDE, State } from "../ide.types"; +import type { + Disposable, + IDE, + RunMode, + WorkspaceFolder, +} from "../types/ide.types"; +import type { Configuration } from "../types/Configuration"; +import type { State } from "../types/State"; import SpyMessages, { Message } from "./SpyMessages"; +import type { Clipboard } from "../types/Clipboard"; export interface SpyIDERecordedValues { messages?: Message[]; @@ -10,14 +17,28 @@ export interface SpyIDERecordedValues { export default class SpyIDE implements IDE { configuration: Configuration; globalState: State; + clipboard: Clipboard; messages: SpyMessages; constructor(private original: IDE) { this.configuration = original.configuration; this.globalState = original.globalState; + this.clipboard = original.clipboard; this.messages = new SpyMessages(original.messages); } + public get assetsRoot(): string { + return this.original.assetsRoot; + } + + public get runMode(): RunMode { + return this.original.runMode; + } + + public get workspaceFolders(): readonly WorkspaceFolder[] | undefined { + return this.original.workspaceFolders; + } + disposeOnExit(...disposables: Disposable[]): () => void { return this.original.disposeOnExit(...disposables); } @@ -32,22 +53,3 @@ export default class SpyIDE implements IDE { : pickBy(ret, (value) => value != null); } } - -export interface SpyInfo extends Disposable { - spy: SpyIDE; -} - -export function injectSpyIde(graph: Graph): SpyInfo { - const original = graph.ide; - const spy = new SpyIDE(original); - - graph.ide = spy; - - return { - spy, - - dispose() { - graph.ide = original; - }, - }; -} diff --git a/src/ide/spies/SpyMessages.ts b/src/libs/common/ide/spy/SpyMessages.ts similarity index 92% rename from src/ide/spies/SpyMessages.ts rename to src/libs/common/ide/spy/SpyMessages.ts index 81bb61bbe0..bd4016fd30 100644 --- a/src/ide/spies/SpyMessages.ts +++ b/src/libs/common/ide/spy/SpyMessages.ts @@ -1,4 +1,4 @@ -import { MessageId, Messages } from "../ide.types"; +import type { MessageId, Messages } from "../types/Messages"; type MessageType = "info" | "warning" | "error"; diff --git a/src/libs/common/ide/types/.eslintrc.json b/src/libs/common/ide/types/.eslintrc.json new file mode 100644 index 0000000000..6b97c90d3b --- /dev/null +++ b/src/libs/common/ide/types/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../../*"], + "message": "Common shouldn't have any dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Common shouldn't have any dependencies" + }, + { + "name": "@cursorless/vscode-common", + "message": "Common shouldn't have any dependencies" + } + ] + } + ] + } +} diff --git a/src/libs/common/ide/types/Clipboard.ts b/src/libs/common/ide/types/Clipboard.ts new file mode 100644 index 0000000000..1590c5e6cb --- /dev/null +++ b/src/libs/common/ide/types/Clipboard.ts @@ -0,0 +1,17 @@ +/** + * The clipboard provides read and write access to the system's clipboard. + */ + +export interface Clipboard { + /** + * Read the current clipboard contents as text. + * @returns A thenable that resolves to a string. + */ + readText(): Thenable; + + /** + * Writes text into the clipboard. + * @returns A thenable that resolves when writing happened. + */ + writeText(value: string): Thenable; +} diff --git a/src/libs/common/ide/types/Configuration.ts b/src/libs/common/ide/types/Configuration.ts new file mode 100644 index 0000000000..f622a150a8 --- /dev/null +++ b/src/libs/common/ide/types/Configuration.ts @@ -0,0 +1,61 @@ +import { Listener } from "@cursorless/common"; +import { Disposable } from "./ide.types"; +import { GetFieldType, Paths } from "./Paths"; + +export type CursorlessConfiguration = { + tokenHatSplittingMode: TokenHatSplittingMode; + wordSeparators: string[]; + experimental: { snippetsDir: string | undefined }; +}; + +export type CursorlessConfigKey = keyof CursorlessConfiguration; +export type ConfigurationScope = { languageId: string }; + +export const CONFIGURATION_DEFAULTS: CursorlessConfiguration = { + tokenHatSplittingMode: { + preserveCase: false, + lettersToPreserve: [], + symbolsToPreserve: [], + }, + wordSeparators: ["_"], + experimental: { snippetsDir: undefined }, +}; + +export interface Configuration { + /** + * Returns a Cursorless configuration value. Dots are accepted in + * {@link path}, and are interpreted as child access, eg + * `experimental.snippetsDir`. + * + * @param path A configuration key or path. Dots are interpreted as child + * access + * @param scope An optional scope specifier, indicating eg language id + */ + getOwnConfiguration>( + path: Path, + scope?: ConfigurationScope, + ): GetFieldType; + + onDidChangeConfiguration(listener: Listener): Disposable; +} + +export interface TokenHatSplittingMode { + /** + * Whether to distinguished between uppercase and lower case letters for hat + */ + preserveCase: boolean; + + /** + * A list of characters whose accents should not be stripped. This can be + * used, for example, if you would like to strip all accents except for those + * of a few characters, which you can add to this string. + */ + lettersToPreserve: string[]; + + /** + * A list of symbols that shouldn't be normalized by the token hat splitter. + * Add any extra symbols here that you have added to your + * capture. + */ + symbolsToPreserve: string[]; +} diff --git a/src/libs/common/ide/types/Messages.ts b/src/libs/common/ide/types/Messages.ts new file mode 100644 index 0000000000..c514ae8607 --- /dev/null +++ b/src/libs/common/ide/types/Messages.ts @@ -0,0 +1,23 @@ +export type MessageId = string; + +export interface Messages { + /** + * Displays a warning message {@link message} to the user along with possible + * {@link options} for them to select. + * + * @param id Each code site where we issue a warning should have a unique, + * human readable id for testability, eg "deprecatedPositionInference". This + * allows us to write tests without tying ourself to the specific wording of + * the warning message provided in {@link message}. + * @param message The message to display to the user + * @param options A list of options to display to the user. The selected + * option will be returned by this function + * @returns The option selected by the user, or `undefined` if no option was + * selected + */ + showWarning( + id: MessageId, + message: string, + ...options: string[] + ): Promise; +} diff --git a/src/libs/common/ide/types/Paths.ts b/src/libs/common/ide/types/Paths.ts new file mode 100644 index 0000000000..7ac23c974b --- /dev/null +++ b/src/libs/common/ide/types/Paths.ts @@ -0,0 +1,62 @@ +// From https://javascript.plainenglish.io/advanced-typescript-type-level-nested-object-paths-7f3d8901f29a + +type Primitive = string | number | symbol; + +type GenericObject = Record; + +type Join< + L extends Primitive | undefined, + R extends Primitive | undefined, +> = L extends string | number + ? R extends string | number + ? `${L}.${R}` + : L + : R extends string | number + ? R + : undefined; + +type Union< + L extends unknown | undefined, + R extends unknown | undefined, +> = L extends undefined + ? R extends undefined + ? undefined + : R + : R extends undefined + ? L + : L | R; + +/** + * Get all the possible paths of an object + * @example + * type Keys = Paths<{ a: { b: { c: string } }> + * // 'a' | 'a.b' | 'a.b.c' + */ +export type Paths< + T extends GenericObject, + Prev extends Primitive | undefined = undefined, + Path extends Primitive | undefined = undefined, +> = { + [K in keyof T]: T[K] extends GenericObject + ? Paths, Join> + : Union, Join>; +}[keyof T]; + +/** + * Get the type of the element specified by the path + * @example + * type TypeOfAB = GetFieldType<{ a: { b: { c: string } }, 'a.b'> + * // { c: string } + */ +export type GetFieldType< + T extends GenericObject, + Path extends string, // Or, if you prefer, NestedPaths +> = { + [K in Path]: K extends keyof T + ? T[K] + : K extends `${infer P}.${infer S}` + ? T[P] extends GenericObject + ? GetFieldType + : never + : never; +}[Path]; diff --git a/src/libs/common/ide/types/State.ts b/src/libs/common/ide/types/State.ts new file mode 100644 index 0000000000..6fc0ec0cbf --- /dev/null +++ b/src/libs/common/ide/types/State.ts @@ -0,0 +1,28 @@ +/** + * A mapping from allowable state keys to their default values + */ +export const STATE_KEYS = { hideInferenceWarning: false }; +export type StateType = typeof STATE_KEYS; +export type StateKey = keyof StateType; + +/** + * A state represents a storage utility. It can store and retrieve + * values. + */ +export interface State { + /** + * Return a value. + * + * @param key A string. + * @return The stored value or the defaultValue. + */ + get(key: StateKey): StateType[StateKey]; + + /** + * Store a value. The value must be JSON-stringifyable. + * + * @param key A string. + * @param value A value. MUST not contain cyclic references. + */ + set(key: StateKey, value: StateType[StateKey]): Thenable; +} diff --git a/src/libs/common/ide/types/ide.types.ts b/src/libs/common/ide/types/ide.types.ts new file mode 100644 index 0000000000..3ad5d0f2a1 --- /dev/null +++ b/src/libs/common/ide/types/ide.types.ts @@ -0,0 +1,47 @@ +import { URI } from "vscode-uri"; +import { Clipboard } from "./Clipboard"; +import { Configuration } from "./Configuration"; +import { Messages } from "./Messages"; +import { State } from "./State"; + +export type RunMode = "production" | "development" | "test"; + +export interface IDE { + configuration: Configuration; + messages: Messages; + globalState: State; + clipboard: Clipboard; + + /** + * Register disposables to be disposed of on IDE exit. + * + * @param disposables A list of {@link Disposable}s to dispose when the IDE is exited. + * @returns A function that can be called to deregister the disposables + */ + disposeOnExit(...disposables: Disposable[]): () => void; + + /** + * The root directory of this shipped code. Can be used to access bundled + * assets. + */ + assetsRoot: string; + + /** + * Whether we are running in development, test, or production + */ + runMode: RunMode; + + /** + * A list of workspace folders for the currently active workspace + */ + workspaceFolders: readonly WorkspaceFolder[] | undefined; +} + +export interface WorkspaceFolder { + uri: URI; + name: string; +} + +export interface Disposable { + dispose(): void; +} diff --git a/src/libs/common/index.ts b/src/libs/common/index.ts new file mode 100644 index 0000000000..3777a0b357 --- /dev/null +++ b/src/libs/common/index.ts @@ -0,0 +1,19 @@ +export * from "./commandIds"; +export { + extractTargetedMarks, + extractTargetKeys, +} from "./testUtil/extractTargetedMarks"; +export { default as FakeIDE } from "./ide/fake/FakeIDE"; +export { + runTestSubset, + TEST_SUBSET_GREP_STRING, +} from "./testUtil/runTestSubset"; +export { default as serialize } from "./testUtil/serialize"; +export { default as SpyIDE } from "./ide/spy/SpyIDE"; +export * from "./util"; +export { getKey, splitKey } from "./util/splitKey"; +export { hrtimeBigintToSeconds } from "./util/timeUtils"; +export { walkFilesSync } from "./util/walkSync"; +export { Listener, Notifier } from "./util/Notifier"; +export { TokenHatSplittingMode } from "./ide/types/Configuration"; +export * from "./ide/types/ide.types"; diff --git a/src/libs/common/testUtil/.eslintrc.json b/src/libs/common/testUtil/.eslintrc.json new file mode 100644 index 0000000000..c42f9f433a --- /dev/null +++ b/src/libs/common/testUtil/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Common shouldn't have any dependencies", + "allowTypeImports": true + } + ], + "paths": [ + { + "name": "vscode", + "message": "Common shouldn't have any dependencies" + }, + { + "name": "@cursorless/vscode-common", + "message": "Common shouldn't have any dependencies" + } + ] + } + ] + } +} diff --git a/src/testUtil/extractTargetedMarks.ts b/src/libs/common/testUtil/extractTargetedMarks.ts similarity index 74% rename from src/testUtil/extractTargetedMarks.ts rename to src/libs/common/testUtil/extractTargetedMarks.ts index ff59ad933e..004209354d 100644 --- a/src/testUtil/extractTargetedMarks.ts +++ b/src/libs/common/testUtil/extractTargetedMarks.ts @@ -1,17 +1,17 @@ -import { ReadOnlyHatMap } from "../core/IndividualHatMap"; -import HatTokenMap from "../core/HatTokenMap"; -import { Token } from "../typings/Types"; -import { +import { getKey, splitKey } from "@cursorless/common"; +import type { ReadOnlyHatMap } from "../../../core/IndividualHatMap"; +import type { PrimitiveTargetDescriptor, TargetDescriptor, -} from "../typings/targetDescriptor.types"; +} from "../../../typings/targetDescriptor.types"; +import type { Token } from "../../../typings/Types"; function extractPrimitiveTargetKeys(...targets: PrimitiveTargetDescriptor[]) { const keys: string[] = []; targets.forEach((target) => { if (target.mark.type === "decoratedSymbol") { const { character, symbolColor } = target.mark; - keys.push(HatTokenMap.getKey(symbolColor, character)); + keys.push(getKey(symbolColor, character)); } }); return keys; @@ -40,7 +40,7 @@ export function extractTargetedMarks( const targetedMarks: { [decoratedCharacter: string]: Token } = {}; targetKeys.forEach((key) => { - const { hatStyle, character } = HatTokenMap.splitKey(key); + const { hatStyle, character } = splitKey(key); targetedMarks[key] = hatTokenMap.getToken(hatStyle, character); }); diff --git a/src/test/suite/runTestSubset.ts b/src/libs/common/testUtil/runTestSubset.ts similarity index 91% rename from src/test/suite/runTestSubset.ts rename to src/libs/common/testUtil/runTestSubset.ts index 8efac20a31..feb220b211 100644 --- a/src/test/suite/runTestSubset.ts +++ b/src/libs/common/testUtil/runTestSubset.ts @@ -4,7 +4,7 @@ * configuration. * See https://mochajs.org/#-grep-regexp-g-regexp for supported syntax */ -export const TEST_SUBSET_GREP_STRING = "markdown"; +export const TEST_SUBSET_GREP_STRING = "snippets"; /** * Determine whether we should run just the subset of the tests specified by diff --git a/src/testUtil/serialize.ts b/src/libs/common/testUtil/serialize.ts similarity index 100% rename from src/testUtil/serialize.ts rename to src/libs/common/testUtil/serialize.ts diff --git a/src/libs/common/util/.eslintrc.json b/src/libs/common/util/.eslintrc.json new file mode 100644 index 0000000000..c42f9f433a --- /dev/null +++ b/src/libs/common/util/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Common shouldn't have any dependencies", + "allowTypeImports": true + } + ], + "paths": [ + { + "name": "vscode", + "message": "Common shouldn't have any dependencies" + }, + { + "name": "@cursorless/vscode-common", + "message": "Common shouldn't have any dependencies" + } + ] + } + ] + } +} diff --git a/src/util/Notifier.ts b/src/libs/common/util/Notifier.ts similarity index 94% rename from src/util/Notifier.ts rename to src/libs/common/util/Notifier.ts index 795c4bb323..3d5aaf0962 100644 --- a/src/util/Notifier.ts +++ b/src/libs/common/util/Notifier.ts @@ -1,5 +1,5 @@ import { pull } from "lodash"; -import { Disposable } from "../ide/ide.types"; +import type { Disposable } from "../ide/types/ide.types"; type Arr = readonly unknown[]; diff --git a/src/libs/common/util/index.ts b/src/libs/common/util/index.ts new file mode 100644 index 0000000000..3e90593166 --- /dev/null +++ b/src/libs/common/util/index.ts @@ -0,0 +1,2 @@ +export * from "./sleep"; +export { default as sleep } from "./sleep"; diff --git a/src/util/sleep.ts b/src/libs/common/util/sleep.ts similarity index 100% rename from src/util/sleep.ts rename to src/libs/common/util/sleep.ts diff --git a/src/libs/common/util/splitKey.ts b/src/libs/common/util/splitKey.ts new file mode 100644 index 0000000000..e87e15a852 --- /dev/null +++ b/src/libs/common/util/splitKey.ts @@ -0,0 +1,16 @@ +import type { HatStyleName } from "../../../core/hatStyles"; + +export function splitKey(key: string) { + const [hatStyle, character] = key.split("."); + + return { + hatStyle: hatStyle as HatStyleName, + // If the character is `.` then it will appear as a zero length string + // due to the way the split on `.` works + character: character.length === 0 ? "." : character, + }; +} + +export function getKey(hatStyle: HatStyleName, character: string) { + return `${hatStyle}.${character}`; +} diff --git a/src/util/timeUtils.ts b/src/libs/common/util/timeUtils.ts similarity index 100% rename from src/util/timeUtils.ts rename to src/libs/common/util/timeUtils.ts diff --git a/src/testUtil/walkSync.ts b/src/libs/common/util/walkSync.ts similarity index 100% rename from src/testUtil/walkSync.ts rename to src/libs/common/util/walkSync.ts diff --git a/src/libs/cursorless-engine/.eslintrc.json b/src/libs/cursorless-engine/.eslintrc.json new file mode 100644 index 0000000000..898fcdd9a6 --- /dev/null +++ b/src/libs/cursorless-engine/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../*"], + "message": "Cursorless engine has restricted dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Cursorless engine shouldn't depend on vscode" + } + ] + } + ] + } +} diff --git a/src/libs/cursorless-engine/languages/.eslintrc.json b/src/libs/cursorless-engine/languages/.eslintrc.json new file mode 100644 index 0000000000..d6a37848f8 --- /dev/null +++ b/src/libs/cursorless-engine/languages/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Cursorless engine has restricted dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Cursorless engine shouldn't depend on vscode" + } + ] + } + ] + } +} diff --git a/src/languages/constants.ts b/src/libs/cursorless-engine/languages/constants.ts similarity index 100% rename from src/languages/constants.ts rename to src/libs/cursorless-engine/languages/constants.ts diff --git a/src/libs/cursorless-engine/scopeHandlers/.eslintrc.json b/src/libs/cursorless-engine/scopeHandlers/.eslintrc.json new file mode 100644 index 0000000000..d6a37848f8 --- /dev/null +++ b/src/libs/cursorless-engine/scopeHandlers/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Cursorless engine has restricted dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Cursorless engine shouldn't depend on vscode" + } + ] + } + ] + } +} diff --git a/src/libs/cursorless-engine/scopeHandlers/WordScopeHandler/.eslintrc.json b/src/libs/cursorless-engine/scopeHandlers/WordScopeHandler/.eslintrc.json new file mode 100644 index 0000000000..f48fc4c1eb --- /dev/null +++ b/src/libs/cursorless-engine/scopeHandlers/WordScopeHandler/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../../*"], + "message": "Cursorless engine has restricted dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Cursorless engine shouldn't depend on vscode" + } + ] + } + ] + } +} diff --git a/src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts b/src/libs/cursorless-engine/scopeHandlers/WordScopeHandler/WordTokenizer.ts similarity index 88% rename from src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts rename to src/libs/cursorless-engine/scopeHandlers/WordScopeHandler/WordTokenizer.ts index 2c1b8944fc..d59b1ed5db 100644 --- a/src/processTargets/modifiers/scopeHandlers/WordTokenizer.ts +++ b/src/libs/cursorless-engine/scopeHandlers/WordScopeHandler/WordTokenizer.ts @@ -1,5 +1,5 @@ -import { getMatcher } from "../../../core/tokenizer"; -import { matchText } from "../../../util/regex"; +import { getMatcher } from "../../tokenizer"; +import { matchText } from "../../util/regex"; const CAMEL_REGEX = /\p{Lu}?\p{Ll}+|\p{Lu}+(?!\p{Ll})|\p{N}+/gu; diff --git a/src/libs/cursorless-engine/singletons/.eslintrc.json b/src/libs/cursorless-engine/singletons/.eslintrc.json new file mode 100644 index 0000000000..d6a37848f8 --- /dev/null +++ b/src/libs/cursorless-engine/singletons/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Cursorless engine has restricted dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Cursorless engine shouldn't depend on vscode" + } + ] + } + ] + } +} diff --git a/src/libs/cursorless-engine/singletons/ide.singleton.ts b/src/libs/cursorless-engine/singletons/ide.singleton.ts new file mode 100644 index 0000000000..a7be9c01a7 --- /dev/null +++ b/src/libs/cursorless-engine/singletons/ide.singleton.ts @@ -0,0 +1,30 @@ +import { IDE } from "@cursorless/common"; + +/** + * This is the `ide` singleton + */ +let ide_: IDE | undefined; + +/** + * Injects an {@link IDE} object that can be used to interact with the IDE. + * This function should only be called from a select few places, eg extension + * activation or when mocking a test. + * @param ide The ide to inject + */ +export function injectIde(ide: IDE | undefined) { + ide_ = ide; +} + +/** + * Gets the singleton used to interact with the IDE. + * @throws Error if the IDE hasn't been injected yet. Can avoid this by + * constructing your objects lazily + * @returns The IDE object + */ +export default function ide(): IDE { + if (ide_ == null) { + throw Error("Tried to access ide before it was injected"); + } + + return ide_; +} diff --git a/src/libs/cursorless-engine/singletons/tokenGraphemeSplitter.singleton.ts b/src/libs/cursorless-engine/singletons/tokenGraphemeSplitter.singleton.ts new file mode 100644 index 0000000000..b0ffc0f5a3 --- /dev/null +++ b/src/libs/cursorless-engine/singletons/tokenGraphemeSplitter.singleton.ts @@ -0,0 +1,18 @@ +import { TokenGraphemeSplitter } from "../tokenGraphemeSplitter/tokenGraphemeSplitter"; + +/** + * Returns the token grapheme splitter singleton, constructing it if it doesn't exist. + * @returns The token grapheme splitter singleton + */ + +export default function tokenGraphemeSplitter(): TokenGraphemeSplitter { + if (tokenGraphemeSplitter_ == null) { + tokenGraphemeSplitter_ = new TokenGraphemeSplitter(); + } + + return tokenGraphemeSplitter_; +} +/** + * This is the token grapheme splitter singleton + */ +let tokenGraphemeSplitter_: TokenGraphemeSplitter | undefined; diff --git a/src/libs/cursorless-engine/test/.eslintrc.json b/src/libs/cursorless-engine/test/.eslintrc.json new file mode 100644 index 0000000000..d6a37848f8 --- /dev/null +++ b/src/libs/cursorless-engine/test/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Cursorless engine has restricted dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Cursorless engine shouldn't depend on vscode" + } + ] + } + ] + } +} diff --git a/src/libs/cursorless-engine/test/fixtures/subtoken.fixture.ts b/src/libs/cursorless-engine/test/fixtures/subtoken.fixture.ts new file mode 100644 index 0000000000..7916fa3baf --- /dev/null +++ b/src/libs/cursorless-engine/test/fixtures/subtoken.fixture.ts @@ -0,0 +1,87 @@ +interface Fixture { + input: string; + expectedOutput: string[]; +} + +export const subtokenFixture: Fixture[] = [ + { + input: "QuickBrownFox", + expectedOutput: ["Quick", "Brown", "Fox"], + }, + { + input: "quickBrownFox", + expectedOutput: ["quick", "Brown", "Fox"], + }, + { + input: "quick_brown_fox", + expectedOutput: ["quick", "brown", "fox"], + }, + { + input: "QUICK_BROWN_FOX", + expectedOutput: ["QUICK", "BROWN", "FOX"], + }, + { + input: "quick-brown-fox", + expectedOutput: ["quick", "brown", "fox"], + }, + { + input: "QUICK-BROWN-FOX", + expectedOutput: ["QUICK", "BROWN", "FOX"], + }, + { + input: "quick.brown.fox", + expectedOutput: ["quick", "brown", "fox"], + }, + { + input: "QUICK.BROWN.FOX", + expectedOutput: ["QUICK", "BROWN", "FOX"], + }, + { + input: "../quick/brown/.fox", + expectedOutput: ["quick", "brown", "fox"], + }, + { + input: "../QUICK/BROWN/.FOX", + expectedOutput: ["QUICK", "BROWN", "FOX"], + }, + { + input: "quick::brown::fox", + expectedOutput: ["quick", "brown", "fox"], + }, + { + input: "QUICK::BROWN::FOX", + expectedOutput: ["QUICK", "BROWN", "FOX"], + }, + { + input: "APIClientFactory", + expectedOutput: ["API", "Client", "Factory"], + }, + { + input: "MockAPIClientFactory", + expectedOutput: ["Mock", "API", "Client", "Factory"], + }, + { + input: "mockAPIClientFactory", + expectedOutput: ["mock", "API", "Client", "Factory"], + }, + { + input: "mockAPIClient123FactoryV1", + expectedOutput: ["mock", "API", "Client", "123", "Factory", "V", "1"], + }, + { + input: "mock_api_client_123_factory_v1", + expectedOutput: ["mock", "api", "client", "123", "factory", "v1"], + }, + { + input: "v1", + expectedOutput: ["v", "1"], + }, + { + input: "aaBbÄä", + expectedOutput: ["aa", "Bb", "Ää"], + }, + { + input: "_quickBrownFox_", + expectedOutput: ["quick", "Brown", "Fox"], + }, +]; diff --git a/src/test/suite/subtoken.test.ts b/src/libs/cursorless-engine/test/subtoken.test.ts similarity index 54% rename from src/test/suite/subtoken.test.ts rename to src/libs/cursorless-engine/test/subtoken.test.ts index 5eb301df81..0b4414c3aa 100644 --- a/src/test/suite/subtoken.test.ts +++ b/src/libs/cursorless-engine/test/subtoken.test.ts @@ -1,13 +1,17 @@ import * as assert from "assert"; -import WordTokenizer from "../../processTargets/modifiers/scopeHandlers/WordTokenizer"; +import WordTokenizer from "../scopeHandlers/WordScopeHandler/WordTokenizer"; import { subtokenFixture } from "./fixtures/subtoken.fixture"; +import { unitTestSetup } from "./unitTestSetup"; suite("subtoken regex matcher", () => { - const wordTokenizer = new WordTokenizer("anyLang"); + unitTestSetup(); + subtokenFixture.forEach(({ input, expectedOutput }) => { test(input, () => { assert.deepStrictEqual( - wordTokenizer.splitIdentifier(input).map(({ text }) => text), + new WordTokenizer("anyLang") + .splitIdentifier(input) + .map(({ text }) => text), expectedOutput, ); }); diff --git a/src/libs/cursorless-engine/test/unitTestSetup.ts b/src/libs/cursorless-engine/test/unitTestSetup.ts new file mode 100644 index 0000000000..64e3954bab --- /dev/null +++ b/src/libs/cursorless-engine/test/unitTestSetup.ts @@ -0,0 +1,27 @@ +import { FakeIDE, SpyIDE } from "@cursorless/common"; +import type { Context } from "mocha"; +import * as sinon from "sinon"; +import { injectIde } from "../singletons/ide.singleton"; + +export function unitTestSetup(setupFake?: (fake: FakeIDE) => void) { + let spy: SpyIDE | undefined; + let fake: FakeIDE | undefined; + + setup(async function (this: Context) { + fake = new FakeIDE(); + setupFake?.(fake); + spy = new SpyIDE(fake); + injectIde(spy); + }); + + teardown(() => { + sinon.restore(); + injectIde(undefined); + }); + + return { + getSpy() { + return spy; + }, + }; +} diff --git a/src/libs/cursorless-engine/tokenGraphemeSplitter/.eslintrc.json b/src/libs/cursorless-engine/tokenGraphemeSplitter/.eslintrc.json new file mode 100644 index 0000000000..d6a37848f8 --- /dev/null +++ b/src/libs/cursorless-engine/tokenGraphemeSplitter/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Cursorless engine has restricted dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Cursorless engine shouldn't depend on vscode" + } + ] + } + ] + } +} diff --git a/src/libs/cursorless-engine/tokenGraphemeSplitter/index.ts b/src/libs/cursorless-engine/tokenGraphemeSplitter/index.ts new file mode 100644 index 0000000000..904ab7ef01 --- /dev/null +++ b/src/libs/cursorless-engine/tokenGraphemeSplitter/index.ts @@ -0,0 +1 @@ +export * from "./tokenGraphemeSplitter"; diff --git a/src/test/suite/tokenGraphemeSplitter.test.ts b/src/libs/cursorless-engine/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts similarity index 89% rename from src/test/suite/tokenGraphemeSplitter.test.ts rename to src/libs/cursorless-engine/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts index 4ade265693..7e4950b222 100644 --- a/src/test/suite/tokenGraphemeSplitter.test.ts +++ b/src/libs/cursorless-engine/tokenGraphemeSplitter/tokenGraphemeSplitter.test.ts @@ -1,11 +1,7 @@ +import { TokenHatSplittingMode } from "@cursorless/common"; import * as assert from "assert"; -import { - TokenGraphemeSplitter, - UNKNOWN, -} from "../../core/TokenGraphemeSplitter"; -import { Graph, TokenHatSplittingMode } from "../../typings/Types"; -import makeGraph, { FactoryMap } from "../../util/makeGraph"; -import FakeIDE from "./fakes/ide/FakeIDE"; +import { unitTestSetup } from "../test/unitTestSetup"; +import { TokenGraphemeSplitter, UNKNOWN } from "./tokenGraphemeSplitter"; /** * Compact representation of a grapheme to make the tests easier to read. @@ -277,31 +273,19 @@ const tests: SplittingModeTestCases[] = [ }, ]; -const graph = makeGraph({ - tokenGraphemeSplitter: (graph: Graph) => new TokenGraphemeSplitter(graph), - ide: (graph: Graph) => new FakeIDE(graph), -} as unknown as FactoryMap); - const tokenHatSplittingDefaults: TokenHatSplittingMode = { preserveCase: false, lettersToPreserve: [], symbolsToPreserve: [], }; -const ide = graph.ide as FakeIDE; - -ide.configuration.mockConfiguration( - "tokenHatSplittingMode", - tokenHatSplittingDefaults, -); - -const tokenGraphemeSplitter = graph.tokenGraphemeSplitter; - tests.forEach(({ tokenHatSplittingMode, extraTestCases }) => { suite(`getTokenGraphemes(${JSON.stringify(tokenHatSplittingMode)})`, () => { - ide.configuration.mockConfiguration("tokenHatSplittingMode", { - ...tokenHatSplittingDefaults, - ...tokenHatSplittingMode, + unitTestSetup(({ configuration }) => { + configuration.mockConfiguration("tokenHatSplittingMode", { + ...tokenHatSplittingDefaults, + ...tokenHatSplittingMode, + }); }); const testCases = [...commonTestCases, ...extraTestCases]; @@ -315,11 +299,12 @@ tests.forEach(({ tokenHatSplittingMode, extraTestCases }) => { }), ); - const actualOutput = tokenGraphemeSplitter.getTokenGraphemes(input); - const displayOutput = expectedOutput.map(({ text }) => text).join(", "); test(`${input} -> ${displayOutput}`, () => { + const actualOutput = new TokenGraphemeSplitter().getTokenGraphemes( + input, + ); assert.deepStrictEqual(actualOutput, expectedOutput); }); }); diff --git a/src/core/TokenGraphemeSplitter.ts b/src/libs/cursorless-engine/tokenGraphemeSplitter/tokenGraphemeSplitter.ts similarity index 93% rename from src/core/TokenGraphemeSplitter.ts rename to src/libs/cursorless-engine/tokenGraphemeSplitter/tokenGraphemeSplitter.ts index 86542aa6fc..aea23e3840 100644 --- a/src/core/TokenGraphemeSplitter.ts +++ b/src/libs/cursorless-engine/tokenGraphemeSplitter/tokenGraphemeSplitter.ts @@ -1,7 +1,10 @@ import { deburr, escapeRegExp } from "lodash"; -import { Disposable } from "../ide/ide.types"; -import { Graph, TokenHatSplittingMode } from "../typings/Types"; -import { Notifier } from "../util/Notifier"; +import ide from "../singletons/ide.singleton"; +import { + TokenHatSplittingMode, + Disposable, + Notifier, +} from "@cursorless/common"; import { matchAll } from "../util/regex"; /** @@ -75,8 +78,8 @@ export class TokenGraphemeSplitter { private algorithmChangeNotifier = new Notifier(); private tokenHatSplittingMode!: TokenHatSplittingMode; - constructor(private graph: Graph) { - graph.ide.disposeOnExit(this); + constructor() { + ide().disposeOnExit(this); this.updateTokenHatSplittingMode = this.updateTokenHatSplittingMode.bind(this); @@ -87,7 +90,7 @@ export class TokenGraphemeSplitter { this.disposables.push( // Notify listeners in case the user changed their token hat splitting // setting. - this.graph.ide.configuration.onDidChangeConfiguration( + ide().configuration.onDidChangeConfiguration( this.updateTokenHatSplittingMode, ), ); @@ -95,9 +98,7 @@ export class TokenGraphemeSplitter { private updateTokenHatSplittingMode() { const { lettersToPreserve, symbolsToPreserve, ...rest } = - this.graph.ide.configuration.getOwnConfiguration( - "tokenHatSplittingMode", - )!; + ide().configuration.getOwnConfiguration("tokenHatSplittingMode"); this.tokenHatSplittingMode = { lettersToPreserve: lettersToPreserve.map((grapheme) => diff --git a/src/libs/cursorless-engine/tokenizer/.eslintrc.json b/src/libs/cursorless-engine/tokenizer/.eslintrc.json new file mode 100644 index 0000000000..d6a37848f8 --- /dev/null +++ b/src/libs/cursorless-engine/tokenizer/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Cursorless engine has restricted dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Cursorless engine shouldn't depend on vscode" + } + ] + } + ] + } +} diff --git a/src/libs/cursorless-engine/tokenizer/index.ts b/src/libs/cursorless-engine/tokenizer/index.ts new file mode 100644 index 0000000000..816278dd59 --- /dev/null +++ b/src/libs/cursorless-engine/tokenizer/index.ts @@ -0,0 +1 @@ +export * from "./tokenizer"; diff --git a/src/test/suite/tokenizer.test.ts b/src/libs/cursorless-engine/tokenizer/tokenizer.test.ts similarity index 90% rename from src/test/suite/tokenizer.test.ts rename to src/libs/cursorless-engine/tokenizer/tokenizer.test.ts index 2e5c27f2ed..98cefa0d19 100644 --- a/src/test/suite/tokenizer.test.ts +++ b/src/libs/cursorless-engine/tokenizer/tokenizer.test.ts @@ -1,8 +1,8 @@ import * as assert from "assert"; import { flatten, range } from "lodash"; -import { tokenize } from "../../core/tokenizer"; -import { tokenizerConfiguration } from "../../core/tokenizerConfiguration"; -import { LanguageId } from "../../languages/constants"; +import { tokenize } from "."; +import { LanguageId } from "../languages/constants"; +import { unitTestSetup } from "../test/unitTestSetup"; type TestCase = [string, string[]]; /** @@ -12,6 +12,8 @@ type TestCase = [string, string[]]; * overriding works. */ interface LanguageTokenizerTests { + wordSeparators: string[]; + /** Language-specific test cases to run in addition to the global tests for this language */ additionalTests: TestCase[]; @@ -84,6 +86,7 @@ const globalTests: TestCase[] = [ ]; const cssDialectTokenizerTests: LanguageTokenizerTests = { + wordSeparators: ["-", "_"], additionalTests: [ ["min-height", ["min-height"]], ["-webkit-font-smoothing", ["-webkit-font-smoothing"]], @@ -95,6 +98,7 @@ const cssDialectTokenizerTests: LanguageTokenizerTests = { }; const shellScriptDialectTokenizerTests: LanguageTokenizerTests = { + wordSeparators: ["-", "_"], additionalTests: [ ["--commit-hooks", ["--commit-hooks"]], [ @@ -115,9 +119,19 @@ const languageTokenizerTests: Partial< }; suite("tokenizer", () => { - // TODO: Remove this once tokenizer has access to graph - suiteSetup(() => { - tokenizerConfiguration.mockWordSeparators(); + unitTestSetup((fake) => { + Object.entries(languageTokenizerTests).forEach( + ([languageId, { wordSeparators }]) => { + fake.configuration.mockConfigurationScope( + { + languageId, + }, + { + wordSeparators, + }, + ); + }, + ); }); globalTests.forEach(([input, expectedOutput]) => { diff --git a/src/core/tokenizer.ts b/src/libs/cursorless-engine/tokenizer/tokenizer.ts similarity index 85% rename from src/core/tokenizer.ts rename to src/libs/cursorless-engine/tokenizer/tokenizer.ts index 318967b5d1..736c352ac4 100644 --- a/src/core/tokenizer.ts +++ b/src/libs/cursorless-engine/tokenizer/tokenizer.ts @@ -1,7 +1,7 @@ import { escapeRegExp } from "lodash"; +import ide from "../singletons/ide.singleton"; import { matchAll } from "../util/regex"; import { LanguageTokenizerComponents } from "./tokenizer.types"; -import { tokenizerConfiguration } from "./tokenizerConfiguration"; const REPEATABLE_SYMBOLS = [ "-", @@ -94,7 +94,15 @@ function generateMatcher( const matchers = new Map(); export function getMatcher(languageId: string): Matcher { - const wordSeparators = tokenizerConfiguration.getWordSeparators(languageId); + // FIXME: The reason this code will auto-reload on settings change is that we don't use fine-grained settings listener in `Decorations`: + // https://github.com/cursorless-dev/cursorless/blob/c914d477c9624c498a47c964088b34e484eac494/src/core/Decorations.ts#L58 + const wordSeparators = ide().configuration.getOwnConfiguration( + "wordSeparators", + { + languageId, + }, + ); + const key = wordSeparators.join("\u0000"); if (!matchers.has(key)) { diff --git a/src/core/tokenizer.types.ts b/src/libs/cursorless-engine/tokenizer/tokenizer.types.ts similarity index 100% rename from src/core/tokenizer.types.ts rename to src/libs/cursorless-engine/tokenizer/tokenizer.types.ts diff --git a/src/libs/cursorless-engine/util/.eslintrc.json b/src/libs/cursorless-engine/util/.eslintrc.json new file mode 100644 index 0000000000..d6a37848f8 --- /dev/null +++ b/src/libs/cursorless-engine/util/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "Cursorless engine has restricted dependencies" + } + ], + "paths": [ + { + "name": "vscode", + "message": "Cursorless engine shouldn't depend on vscode" + } + ] + } + ] + } +} diff --git a/src/util/regex.ts b/src/libs/cursorless-engine/util/regex.ts similarity index 54% rename from src/util/regex.ts rename to src/libs/cursorless-engine/util/regex.ts index 2085225a16..8a71adb77c 100644 --- a/src/util/regex.ts +++ b/src/libs/cursorless-engine/util/regex.ts @@ -1,7 +1,3 @@ -import { imap } from "itertools"; -import { Range, TextEditor } from "vscode"; -import { Direction } from "../typings/targetDescriptor.types"; - function _rightAnchored(regex: RegExp) { const { source, flags } = regex; @@ -65,47 +61,3 @@ export function matchText(text: string, regex: RegExp): MatchedText[] { text: match[0], })); } - -export function getMatchesInRange( - regex: RegExp, - editor: TextEditor, - range: Range, -): Range[] { - const offset = editor.document.offsetAt(range.start); - const text = editor.document.getText(range); - - return matchAll( - text, - regex, - (match) => - new Range( - editor.document.positionAt(offset + match.index!), - editor.document.positionAt(offset + match.index! + match[0].length), - ), - ); -} - -export function generateMatchesInRange( - regex: RegExp, - editor: TextEditor, - range: Range, - direction: Direction, -): Iterable { - const offset = editor.document.offsetAt(range.start); - const text = editor.document.getText(range); - - const matchToRange = (match: RegExpMatchArray): Range => - new Range( - editor.document.positionAt(offset + match.index!), - editor.document.positionAt(offset + match.index! + match[0].length), - ); - - // Reset the regex to start at the beginning of string, in case the regex has - // been used before. - // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#finding_successive_matches - regex.lastIndex = 0; - - return direction === "forward" - ? imap(text.matchAll(regex), matchToRange) - : Array.from(text.matchAll(regex), matchToRange).reverse(); -} diff --git a/src/libs/vscode-common/.eslintrc.json b/src/libs/vscode-common/.eslintrc.json new file mode 100644 index 0000000000..9b1e4d27d8 --- /dev/null +++ b/src/libs/vscode-common/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../*"], + "message": "VSCode common shouldn't import from Cursorless extension", + "allowTypeImports": true + } + ] + } + ] + } +} diff --git a/src/util/getExtensionApi.ts b/src/libs/vscode-common/getExtensionApi.ts similarity index 58% rename from src/util/getExtensionApi.ts rename to src/libs/vscode-common/getExtensionApi.ts index f2aea6d43e..f774cc3912 100644 --- a/src/util/getExtensionApi.ts +++ b/src/libs/vscode-common/getExtensionApi.ts @@ -1,16 +1,38 @@ import * as vscode from "vscode"; -import { ThatMark } from "../core/ThatMark"; -import { SyntaxNode } from "web-tree-sitter"; -import { Graph } from "../typings/Types"; +import type { SyntaxNode } from "web-tree-sitter"; +import type { ThatMark } from "../../core/ThatMark"; +import type { SnippetMap } from "../../typings/snippet"; +import type { Target } from "../../typings/target.types"; +import type { Graph } from "../../typings/Types"; +import type FakeIDE from "../common/ide/fake/FakeIDE"; +import type { IDE } from "../common/ide/types/ide.types"; +import type { TargetPlainObject } from "./toPlainObject"; + +interface TestHelpers { + graph: Graph; + ide: FakeIDE; + injectIde: (ide: IDE) => void; + + // FIXME: Remove this once we have a better way to get this function + // accessible from our tests + plainObjectToTarget( + editor: vscode.TextEditor, + plainObject: TargetPlainObject, + ): Target; +} export interface CursorlessApi { thatMark: ThatMark; sourceMark: ThatMark; - /** - * The dependency injection graph object used by cursorless. Only exposed during testing - */ - graph?: Graph; + testHelpers: TestHelpers | undefined; + + experimental: { + registerThirdPartySnippets: ( + extensionId: string, + snippets: SnippetMap, + ) => void; + }; } export interface ParseTreeApi { diff --git a/src/libs/vscode-common/index.ts b/src/libs/vscode-common/index.ts new file mode 100644 index 0000000000..a88399b58a --- /dev/null +++ b/src/libs/vscode-common/index.ts @@ -0,0 +1,9 @@ +export * from "./getExtensionApi"; +export * from "./notebook"; +export * from "./toPlainObject"; +export * from "./testUtil/testConstants"; +export { + takeSnapshot, + TestCaseSnapshot, + ExcludableSnapshotField, +} from "./testUtil/takeSnapshot"; diff --git a/src/libs/vscode-common/notebook.ts b/src/libs/vscode-common/notebook.ts new file mode 100644 index 0000000000..9ec45e8316 --- /dev/null +++ b/src/libs/vscode-common/notebook.ts @@ -0,0 +1,20 @@ +import type { NotebookDocument, TextDocument } from "vscode"; + +/** + * Returns the index of the cell corresponding to the given document in the + * notebook. Assumes that the given notebook contains the given cell + * @param notebookDocument The notebook document containing the cell + * @param document The document corresponding to the given cell + * @returns The index of the cell in the notebook + */ + +export function getCellIndex( + notebookDocument: NotebookDocument, + document: TextDocument, +) { + return notebookDocument + .getCells() + .findIndex( + (cell) => cell.document.uri.toString() === document.uri.toString(), + ); +} diff --git a/src/libs/vscode-common/testUtil/.eslintrc.json b/src/libs/vscode-common/testUtil/.eslintrc.json new file mode 100644 index 0000000000..f256e7e963 --- /dev/null +++ b/src/libs/vscode-common/testUtil/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "rules": { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["../../*"], + "message": "VSCode common shouldn't import from Cursorless extension", + "allowTypeImports": true + } + ] + } + ] + } +} diff --git a/src/testUtil/takeSnapshot.ts b/src/libs/vscode-common/testUtil/takeSnapshot.ts similarity index 77% rename from src/testUtil/takeSnapshot.ts rename to src/libs/vscode-common/testUtil/takeSnapshot.ts index 054bc6b0af..7f02284ca2 100644 --- a/src/testUtil/takeSnapshot.ts +++ b/src/libs/vscode-common/testUtil/takeSnapshot.ts @@ -1,6 +1,5 @@ -import { ThatMark } from "../core/ThatMark"; -import { Clipboard } from "../util/Clipboard"; -import { hrtimeBigintToSeconds } from "../util/timeUtils"; +import type { ThatMark } from "../../../core/ThatMark"; +import { hrtimeBigintToSeconds } from "@cursorless/common"; import { RangePlainObject, rangeToPlainObject, @@ -9,8 +8,10 @@ import { SerializedMarks, TargetPlainObject, targetToPlainObject, -} from "./toPlainObject"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +} from "../toPlainObject"; +import { TextEditor } from "vscode"; +import type { IDE } from "../../common/ide/types/ide.types"; +import type { Clipboard } from "../../common/ide/types/Clipboard"; export type ExtraSnapshotField = keyof TestCaseSnapshot; export type ExcludableSnapshotField = keyof TestCaseSnapshot; @@ -42,15 +43,16 @@ export async function takeSnapshot( sourceMark: ThatMark | undefined, excludeFields: ExcludableSnapshotField[] = [], extraFields: ExtraSnapshotField[] = [], + editor: TextEditor, + ide: IDE, marks?: SerializedMarks, extraContext?: ExtraContext, metadata?: unknown, + clipboard?: Clipboard, ) { - const activeEditor = getActiveTextEditor()!; - const snapshot: TestCaseSnapshot = { - documentContents: activeEditor.document.getText(), - selections: activeEditor.selections.map(selectionToPlainObject), + documentContents: editor.document.getText(), + selections: editor.selections.map(selectionToPlainObject), }; if (marks != null) { @@ -62,11 +64,11 @@ export async function takeSnapshot( } if (!excludeFields.includes("clipboard")) { - snapshot.clipboard = await Clipboard.readText(); + snapshot.clipboard = await (clipboard ?? ide.clipboard).readText(); } if (!excludeFields.includes("visibleRanges")) { - snapshot.visibleRanges = activeEditor.visibleRanges.map(rangeToPlainObject); + snapshot.visibleRanges = editor.visibleRanges.map(rangeToPlainObject); } if ( diff --git a/src/testUtil/testConstants.ts b/src/libs/vscode-common/testUtil/testConstants.ts similarity index 100% rename from src/testUtil/testConstants.ts rename to src/libs/vscode-common/testUtil/testConstants.ts diff --git a/src/testUtil/toPlainObject.ts b/src/libs/vscode-common/toPlainObject.ts similarity index 93% rename from src/testUtil/toPlainObject.ts rename to src/libs/vscode-common/toPlainObject.ts index f99e4dc140..85a72bd393 100644 --- a/src/testUtil/toPlainObject.ts +++ b/src/libs/vscode-common/toPlainObject.ts @@ -1,7 +1,7 @@ -import { Position, Range, Selection } from "vscode"; -import { TestDecoration } from "../core/editStyles"; -import type { Target } from "../typings/target.types"; -import { Token } from "../typings/Types"; +import type { Position, Range, Selection } from "vscode"; +import type { TestDecoration } from "../../core/editStyles"; +import type { Target } from "../../typings/target.types"; +import type { Token } from "../../typings/Types"; export type PositionPlainObject = { line: number; diff --git a/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts index f0db89bd14..66aaa25796 100644 --- a/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts @@ -1,8 +1,8 @@ import { imap } from "itertools"; import { NestedScopeHandler } from "."; -import { GRAPHEME_SPLIT_REGEX } from "../../../core/TokenGraphemeSplitter"; +import { GRAPHEME_SPLIT_REGEX } from "../../../libs/cursorless-engine/tokenGraphemeSplitter"; import { Direction } from "../../../typings/targetDescriptor.types"; -import { generateMatchesInRange } from "../../../util/regex"; +import { generateMatchesInRange } from "../../../apps/cursorless-vscode/getMatchesInRange"; import { PlainTarget } from "../../targets"; import type { TargetScope } from "./scope.types"; diff --git a/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts index 4267aab7b8..c886a9ba45 100644 --- a/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts @@ -1,8 +1,8 @@ import { imap } from "itertools"; import { NestedScopeHandler } from "."; -import { getMatcher } from "../../../core/tokenizer"; +import { getMatcher } from "../../../libs/cursorless-engine/tokenizer"; import { Direction } from "../../../typings/targetDescriptor.types"; -import { generateMatchesInRange } from "../../../util/regex"; +import { generateMatchesInRange } from "../../../apps/cursorless-vscode/getMatchesInRange"; import { TokenTarget } from "../../targets"; import type { TargetScope } from "./scope.types"; diff --git a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index cc0c898a2b..df3a9b711d 100644 --- a/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -1,11 +1,12 @@ import { imap } from "itertools"; import { NestedScopeHandler } from "."; -import { getMatcher } from "../../../core/tokenizer"; +import { getMatcher } from "../../../libs/cursorless-engine/tokenizer"; import type { Direction, ScopeType, } from "../../../typings/targetDescriptor.types"; -import { generateMatchesInRange, testRegex } from "../../../util/regex"; +import { testRegex } from "../../../libs/cursorless-engine/util/regex"; +import { generateMatchesInRange } from "../../../apps/cursorless-vscode/getMatchesInRange"; import { TokenTarget } from "../../targets"; import type { TargetScope } from "./scope.types"; diff --git a/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts b/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts index 9305465cea..8e2d7ae7a6 100644 --- a/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts +++ b/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts @@ -3,7 +3,7 @@ import { NestedScopeHandler } from "."; import { Direction } from "../../../typings/targetDescriptor.types"; import { SubTokenWordTarget } from "../../targets"; import type { TargetScope } from "./scope.types"; -import WordTokenizer from "./WordTokenizer"; +import WordTokenizer from "../../../libs/cursorless-engine/scopeHandlers/WordScopeHandler/WordTokenizer"; export default class WordScopeHandler extends NestedScopeHandler { public readonly scopeType = { type: "word" } as const; diff --git a/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts b/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts index 02a66b323e..70bd746853 100644 --- a/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts +++ b/src/processTargets/modifiers/surroundingPair/findSurroundingPairTextBased.ts @@ -6,7 +6,7 @@ import { SurroundingPairScopeType, } from "../../../typings/targetDescriptor.types"; import { getDocumentRange } from "../../../util/rangeUtils"; -import { matchAll } from "../../../util/regex"; +import { matchAll } from "../../../libs/cursorless-engine/util/regex"; import { extractSelectionFromSurroundingPairOffsets } from "./extractSelectionFromSurroundingPairOffsets"; import { findSurroundingPairCore } from "./findSurroundingPairCore"; import { getIndividualDelimiters } from "./getIndividualDelimiters"; diff --git a/src/processTargets/targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts b/src/processTargets/targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts index 2c58c6c64e..e303596a88 100644 --- a/src/processTargets/targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts +++ b/src/processTargets/targetUtil/insertionRemovalBehaviors/TokenInsertionRemovalBehavior.ts @@ -1,5 +1,5 @@ import { Range, TextDocument, TextEditor } from "vscode"; -import { tokenize } from "../../../core/tokenizer"; +import { tokenize } from "../../../libs/cursorless-engine/tokenizer"; import type { Target } from "../../../typings/target.types"; import { expandToFullLine, makeEmptyRange } from "../../../util/rangeUtils"; import { PlainTarget } from "../../targets"; diff --git a/src/test/initLaunchSandbox.ts b/src/scripts/initLaunchSandbox.ts similarity index 95% rename from src/test/initLaunchSandbox.ts rename to src/scripts/initLaunchSandbox.ts index 1a25746cf1..057d41c67e 100644 --- a/src/test/initLaunchSandbox.ts +++ b/src/scripts/initLaunchSandbox.ts @@ -4,7 +4,7 @@ */ import * as path from "path"; import * as cp from "child_process"; -import { extensionDependencies } from "./extensionDependencies"; +import { extensionDependencies } from "../test/extensionDependencies"; import { mkdir } from "fs/promises"; const extraExtensions = ["pokey.command-server", "pokey.talon"]; diff --git a/src/scripts/transformRecordedTests/index.ts b/src/scripts/transformRecordedTests/index.ts index 37ca94e87e..ee2823f4d0 100644 --- a/src/scripts/transformRecordedTests/index.ts +++ b/src/scripts/transformRecordedTests/index.ts @@ -1,4 +1,4 @@ -import { getRecordedTestPaths } from "../../test/util/getFixturePaths"; +import { getRecordedTestPaths } from "../../apps/cursorless-vscode-e2e/getFixturePaths"; import { identity } from "./transformations/identity"; import { upgrade } from "./transformations/upgrade"; import { transformFile } from "./transformFile"; diff --git a/src/scripts/transformRecordedTests/moveFile.ts b/src/scripts/transformRecordedTests/moveFile.ts index d4b2352f65..d5f67ab94e 100644 --- a/src/scripts/transformRecordedTests/moveFile.ts +++ b/src/scripts/transformRecordedTests/moveFile.ts @@ -1,7 +1,7 @@ import { promises as fsp } from "fs"; import * as path from "path"; import * as yaml from "js-yaml"; -import { TestCaseFixture } from "../../testUtil/TestCase"; +import { TestCaseFixture } from "../../testUtil/TestCaseFixture"; import { mkdir, rename } from "fs/promises"; /** diff --git a/src/scripts/transformRecordedTests/transformFile.ts b/src/scripts/transformRecordedTests/transformFile.ts index abf08e2901..208ae285c9 100644 --- a/src/scripts/transformRecordedTests/transformFile.ts +++ b/src/scripts/transformRecordedTests/transformFile.ts @@ -1,7 +1,7 @@ import { promises as fsp } from "fs"; import * as yaml from "js-yaml"; -import { TestCaseFixture } from "../../testUtil/TestCase"; -import serialize from "../../testUtil/serialize"; +import { TestCaseFixture } from "../../testUtil/TestCaseFixture"; +import serialize from "../../libs/common/testUtil/serialize"; import { FixtureTransformation } from "./types"; export async function transformFile( diff --git a/src/scripts/transformRecordedTests/transformations/identity.ts b/src/scripts/transformRecordedTests/transformations/identity.ts index fdae966fcf..587a85e95c 100644 --- a/src/scripts/transformRecordedTests/transformations/identity.ts +++ b/src/scripts/transformRecordedTests/transformations/identity.ts @@ -1,4 +1,4 @@ -import { TestCaseFixture } from "../../../testUtil/TestCase"; +import { TestCaseFixture } from "../../../testUtil/TestCaseFixture"; export function identity(fixture: TestCaseFixture) { return fixture; diff --git a/src/scripts/transformRecordedTests/transformations/reorderFields.ts b/src/scripts/transformRecordedTests/transformations/reorderFields.ts index ae31011a8c..4c6555b362 100644 --- a/src/scripts/transformRecordedTests/transformations/reorderFields.ts +++ b/src/scripts/transformRecordedTests/transformations/reorderFields.ts @@ -1,4 +1,4 @@ -import { TestCaseFixture } from "../../../testUtil/TestCase"; +import { TestCaseFixture } from "../../../testUtil/TestCaseFixture"; export function reorderFields(fixture: TestCaseFixture) { return { diff --git a/src/scripts/transformRecordedTests/transformations/upgrade.ts b/src/scripts/transformRecordedTests/transformations/upgrade.ts index 0003492798..2ea2cfbfc0 100644 --- a/src/scripts/transformRecordedTests/transformations/upgrade.ts +++ b/src/scripts/transformRecordedTests/transformations/upgrade.ts @@ -1,7 +1,7 @@ import { flow } from "lodash"; import { canonicalizeAndValidateCommand } from "../../../core/commandVersionUpgrades/canonicalizeAndValidateCommand"; import { cleanUpTestCaseCommand } from "../../../testUtil/cleanUpTestCaseCommand"; -import { TestCaseFixture } from "../../../testUtil/TestCase"; +import { TestCaseFixture } from "../../../testUtil/TestCaseFixture"; import { reorderFields } from "./reorderFields"; export const upgrade = flow(upgradeCommand, reorderFields); diff --git a/src/scripts/transformRecordedTests/transformations/upgradeFromVersion0.ts b/src/scripts/transformRecordedTests/transformations/upgradeFromVersion0.ts index cddb81af6e..34213fe154 100644 --- a/src/scripts/transformRecordedTests/transformations/upgradeFromVersion0.ts +++ b/src/scripts/transformRecordedTests/transformations/upgradeFromVersion0.ts @@ -1,4 +1,4 @@ -import { TestCaseFixture } from "../../../testUtil/TestCase"; +import { TestCaseFixture } from "../../../testUtil/TestCaseFixture"; import { transformPartialPrimitiveTargets } from "../../../util/getPrimitiveTargets"; import { PartialPrimitiveTargetDescriptor } from "../../../typings/targetDescriptor.types"; diff --git a/src/scripts/transformRecordedTests/types.ts b/src/scripts/transformRecordedTests/types.ts index 8936b502e3..454d97ebdb 100644 --- a/src/scripts/transformRecordedTests/types.ts +++ b/src/scripts/transformRecordedTests/types.ts @@ -1,4 +1,4 @@ -import { TestCaseFixture } from "../../testUtil/TestCase"; +import { TestCaseFixture } from "../../testUtil/TestCaseFixture"; export type FixtureTransformation = ( originalFixture: TestCaseFixture, diff --git a/src/scripts/transformRecordedTests/upgradeThatMarks.ts b/src/scripts/transformRecordedTests/upgradeThatMarks.ts index 440e24ce04..839cf32682 100644 --- a/src/scripts/transformRecordedTests/upgradeThatMarks.ts +++ b/src/scripts/transformRecordedTests/upgradeThatMarks.ts @@ -1,8 +1,8 @@ -import { TestCaseFixture } from "../../testUtil/TestCase"; +import { TestCaseFixture } from "../../testUtil/TestCaseFixture"; import { SelectionPlainObject, TargetPlainObject, -} from "../../testUtil/toPlainObject"; +} from "../../libs/vscode-common/toPlainObject"; import { FixtureTransformation } from "./types"; // FIXME: Remove this before merging the PR diff --git a/src/test/runTest.ts b/src/test/launchVscodeAndRunTests.ts similarity index 83% rename from src/test/runTest.ts rename to src/test/launchVscodeAndRunTests.ts index 63e285452a..2931cddbbd 100644 --- a/src/test/runTest.ts +++ b/src/test/launchVscodeAndRunTests.ts @@ -1,6 +1,5 @@ import * as cp from "child_process"; import * as path from "path"; - import { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath, @@ -9,16 +8,18 @@ import { import { env } from "process"; import { extensionDependencies } from "./extensionDependencies"; -async function main() { +/** + * Downloads and launches VSCode, instructing it to run the test runner + * specified in {@link extensionTestsPath}. + * @param extensionTestsPath The path to test runner, passed to + * `--extensionTestsPath` + */ +export async function launchVscodeAndRunTests(extensionTestsPath: string) { try { // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` const extensionDevelopmentPath = path.resolve(__dirname, "../../"); - // The path to test runner - // Passed to --extensionTestsPath - const extensionTestsPath = path.resolve(__dirname, "./suite/index"); - // NB: We include the exact version here instead of in `test.yml` so that // we don't have to update the branch protection rules every time we bump // the legacy VSCode version. @@ -54,5 +55,3 @@ async function main() { process.exit(1); } } - -main(); diff --git a/src/test/runners/README.md b/src/test/runners/README.md new file mode 100644 index 0000000000..906ae33785 --- /dev/null +++ b/src/test/runners/README.md @@ -0,0 +1,3 @@ +# Test runners + +This directory contains files that export a `run` function that will run all, or a specific subset, of tests. These runners (once compiled to js) can be passed to `code --extensionTestsPath` to run tests. diff --git a/src/test/runners/all.ts b/src/test/runners/all.ts new file mode 100644 index 0000000000..de5de46c1b --- /dev/null +++ b/src/test/runners/all.ts @@ -0,0 +1,15 @@ +// Ensures that the aliases such as @cursorless/common that we define in +// package.json are active +import "module-alias/register"; +import * as path from "path"; +import { runAllTestsInDir } from "../util/runAllTestsInDir"; + +/** + * Runs all tests. This function should only be called via the + * --extensionDevelopmentPath VSCode mechanism, as it includes tests that can + * only run in VSCode + * @returns A promise that resolves when tests have finished running + */ +export function run(): Promise { + return runAllTestsInDir(path.resolve(__dirname, "../..")); +} diff --git a/src/test/runners/endToEndOnly.ts b/src/test/runners/endToEndOnly.ts new file mode 100644 index 0000000000..c83397228c --- /dev/null +++ b/src/test/runners/endToEndOnly.ts @@ -0,0 +1,17 @@ +// Ensures that the aliases such as @cursorless/common that we define in +// package.json are active +import "module-alias/register"; +import * as path from "path"; +import { runAllTestsInDir } from "../util/runAllTestsInDir"; + +/** + * Runs end-to-end VSCode tests. This function should only be called via the + * --extensionDevelopmentPath VSCode mechanism, as it includes tests that can + * only run in VSCode + * @returns A promise that resolves when tests have finished running + */ +export function run(): Promise { + return runAllTestsInDir( + path.resolve(__dirname, "../../apps/cursorless-vscode-e2e"), + ); +} diff --git a/src/test/runners/unitTestsOnly.ts b/src/test/runners/unitTestsOnly.ts new file mode 100644 index 0000000000..a07373b839 --- /dev/null +++ b/src/test/runners/unitTestsOnly.ts @@ -0,0 +1,11 @@ +// Ensures that the aliases such as @cursorless/common that we define in +// package.json are active +import "module-alias/register"; +import * as path from "path"; +import { runAllTestsInDir } from "../util/runAllTestsInDir"; + +export function run(): Promise { + return runAllTestsInDir( + path.resolve(__dirname, "../../libs/cursorless-engine"), + ); +} diff --git a/src/test/scripts/runTestsCI.ts b/src/test/scripts/runTestsCI.ts new file mode 100644 index 0000000000..f5c5dfe04c --- /dev/null +++ b/src/test/scripts/runTestsCI.ts @@ -0,0 +1,19 @@ +/** + * This file can be run from node to run tests in CI + */ + +import * as path from "path"; +import { launchVscodeAndRunTests } from "../launchVscodeAndRunTests"; + +async function main() { + // Note that we run all tests, including unit tests, in VSCode, even though + // unit tests could be run separately. If we wanted to run unit tests + // separately, we could instead use `../runners/endToEndOnly` instead of + // `../runners/all` and then just call `await runUnitTests()` beforehand to + // run the unit tests directly, instead of as part of VSCode runner. + const extensionTestsPath = path.resolve(__dirname, "../runners/all"); + + await launchVscodeAndRunTests(extensionTestsPath); +} + +main(); diff --git a/src/test/scripts/runUnitTestsOnly.ts b/src/test/scripts/runUnitTestsOnly.ts new file mode 100644 index 0000000000..f42085c5c8 --- /dev/null +++ b/src/test/scripts/runUnitTestsOnly.ts @@ -0,0 +1,3 @@ +import { run } from "../runners/unitTestsOnly"; + +run(); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts deleted file mode 100644 index 2f671d3c72..0000000000 --- a/src/test/suite/extension.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as assert from "assert"; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from "vscode"; -// import * as myExtension from '../../extension'; - -suite("Extension Test Suite", () => { - vscode.window.showInformationMessage("Start all tests."); - - test("Sample test", () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); -}); diff --git a/src/test/suite/fakes/ide/FakeConfiguration.ts b/src/test/suite/fakes/ide/FakeConfiguration.ts deleted file mode 100644 index 30c62491ec..0000000000 --- a/src/test/suite/fakes/ide/FakeConfiguration.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { - Configuration, - CursorlessConfigKey, - CursorlessConfiguration, -} from "../../../../ide/ide.types"; -import { Graph } from "../../../../typings/Types"; -import { Notifier } from "../../../../util/Notifier"; - -export default class FakeConfiguration implements Configuration { - private notifier = new Notifier(); - private mocks: Partial = {}; - - constructor(private graph: Graph) { - this.onDidChangeConfiguration = this.onDidChangeConfiguration.bind(this); - } - - getOwnConfiguration( - key: T, - ): CursorlessConfiguration[T] | undefined { - return this.mocks[key]; - } - - onDidChangeConfiguration = this.notifier.registerListener; - - mockConfiguration( - key: T, - value: CursorlessConfiguration[T], - ): void { - this.mocks[key] = value; - this.notifier.notifyListeners(); - } - - resetMocks(): void { - this.mocks = {}; - } -} diff --git a/src/test/suite/fixtures/recorded/actions/pasteCap.yml b/src/test/suite/fixtures/recorded/actions/pasteCap.yml index d87e16db10..786d6557c8 100644 --- a/src/test/suite/fixtures/recorded/actions/pasteCap.yml +++ b/src/test/suite/fixtures/recorded/actions/pasteCap.yml @@ -25,7 +25,6 @@ finalState: selections: - anchor: {line: 0, character: 0} active: {line: 0, character: 0} - clipboard: value thatMark: - type: UntypedTarget contentRange: diff --git a/src/test/suite/index.ts b/src/test/util/runAllTestsInDir.ts similarity index 83% rename from src/test/suite/index.ts rename to src/test/util/runAllTestsInDir.ts index 5939c8e50d..238d59fe67 100644 --- a/src/test/suite/index.ts +++ b/src/test/util/runAllTestsInDir.ts @@ -1,9 +1,9 @@ import * as glob from "glob"; import * as Mocha from "mocha"; import * as path from "path"; -import { runTestSubset, TEST_SUBSET_GREP_STRING } from "./runTestSubset"; +import { runTestSubset, TEST_SUBSET_GREP_STRING } from "@cursorless/common"; -export function run(): Promise { +export function runAllTestsInDir(testsRoot: string): Promise { // Create the mocha test const mocha = new Mocha({ ui: "tdd", @@ -11,8 +11,6 @@ export function run(): Promise { grep: runTestSubset() ? TEST_SUBSET_GREP_STRING : undefined, // Only run a subset of tests }); - const testsRoot = path.resolve(__dirname, ".."); - return new Promise((c, e) => { glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { if (err) { diff --git a/src/testUtil/TestCase.ts b/src/testUtil/TestCase.ts index 1e8fb139b2..feca3cba70 100644 --- a/src/testUtil/TestCase.ts +++ b/src/testUtil/TestCase.ts @@ -1,32 +1,35 @@ import { pick } from "lodash"; import { ActionType } from "../actions/actions.types"; -import { CommandLatest } from "../core/commandRunner/command.types"; import { TestDecoration } from "../core/editStyles"; import { ReadOnlyHatMap } from "../core/IndividualHatMap"; import { ThatMark } from "../core/ThatMark"; -import SpyIDE, { SpyIDERecordedValues } from "../ide/spies/SpyIDE"; +import SpyIDE, { SpyIDERecordedValues } from "../libs/common/ide/spy/SpyIDE"; import { TargetDescriptor } from "../typings/targetDescriptor.types"; import { Token } from "../typings/Types"; import { cleanUpTestCaseCommand } from "./cleanUpTestCaseCommand"; import { extractTargetedMarks, extractTargetKeys, -} from "./extractTargetedMarks"; -import serialize from "./serialize"; +} from "../libs/common/testUtil/extractTargetedMarks"; +import serialize from "../libs/common/testUtil/serialize"; import { ExtraSnapshotField, takeSnapshot, TestCaseSnapshot, -} from "./takeSnapshot"; +} from "../libs/vscode-common/testUtil/takeSnapshot"; import { marksToPlainObject, - PositionPlainObject, SerializedMarks, testDecorationsToPlainObject, -} from "./toPlainObject"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; - -export type TestCaseCommand = CommandLatest; +} from "../libs/vscode-common/toPlainObject"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; +import type { + PlainTestDecoration, + ThrownError, + TestCaseCommand, + TestCaseFixture, +} from "./TestCaseFixture"; +import ide from "../libs/cursorless-engine/singletons/ide.singleton"; export type TestCaseContext = { thatMark: ThatMark; @@ -36,49 +39,6 @@ export type TestCaseContext = { hatTokenMap: ReadOnlyHatMap; }; -interface PlainTestDecoration { - name: string; - type: "token" | "line"; - start: PositionPlainObject; - end: PositionPlainObject; -} - -export type ThrownError = { - name: string; -}; - -export type TestCaseFixture = { - languageId: string; - postEditorOpenSleepTimeMs?: number; - postCommandSleepTimeMs?: number; - command: TestCaseCommand; - - /** - * A list of marks to check in the case of navigation map test otherwise undefined - */ - marksToCheck?: string[]; - - initialState: TestCaseSnapshot; - /** - * Expected decorations in the test case, for example highlighting deletions in red. - */ - decorations?: PlainTestDecoration[]; - ide?: SpyIDERecordedValues; - /** The final state after a command is issued. Undefined if we are testing a non-match(error) case. */ - finalState?: TestCaseSnapshot; - /** Used to assert if an error has been thrown. */ - thrownError?: ThrownError; - - /** - * The return value of the command. Will be undefined when we have recorded an - * error test case. - */ - returnValue?: unknown; - - /** Inferred full targets added for context; not currently used in testing */ - fullTargets: TargetDescriptor[]; -}; - export class TestCase { private languageId: string; private fullTargets: TargetDescriptor[]; @@ -218,6 +178,8 @@ export class TestCase { this.context.sourceMark, excludeFields, this.extraSnapshotFields, + getActiveTextEditor()!, + ide(), this.getMarks(), { startTimestamp: this.startTimestamp }, ); @@ -231,6 +193,8 @@ export class TestCase { this.context.sourceMark, excludeFields, this.extraSnapshotFields, + getActiveTextEditor()!, + ide(), this.isHatTokenMapTest ? this.getMarks() : undefined, { startTimestamp: this.startTimestamp }, ); diff --git a/src/testUtil/TestCaseFixture.ts b/src/testUtil/TestCaseFixture.ts new file mode 100644 index 0000000000..623fbf28e8 --- /dev/null +++ b/src/testUtil/TestCaseFixture.ts @@ -0,0 +1,49 @@ +import type { CommandLatest } from "../core/commandRunner/command.types"; +import type { SpyIDERecordedValues } from "../libs/common/ide/spy/SpyIDE"; +import type { TargetDescriptor } from "../typings/targetDescriptor.types"; +import type { TestCaseSnapshot } from "../libs/vscode-common/testUtil/takeSnapshot"; +import type { PositionPlainObject } from "../libs/vscode-common/toPlainObject"; + +export type TestCaseCommand = CommandLatest; +export interface PlainTestDecoration { + name: string; + type: "token" | "line"; + start: PositionPlainObject; + end: PositionPlainObject; +} + +export type ThrownError = { + name: string; +}; + +export type TestCaseFixture = { + languageId: string; + postEditorOpenSleepTimeMs?: number; + postCommandSleepTimeMs?: number; + command: TestCaseCommand; + + /** + * A list of marks to check in the case of navigation map test otherwise undefined + */ + marksToCheck?: string[]; + + initialState: TestCaseSnapshot; + /** + * Expected decorations in the test case, for example highlighting deletions in red. + */ + decorations?: PlainTestDecoration[]; + ide?: SpyIDERecordedValues; + /** The final state after a command is issued. Undefined if we are testing a non-match(error) case. */ + finalState?: TestCaseSnapshot; + /** Used to assert if an error has been thrown. */ + thrownError?: ThrownError; + + /** + * The return value of the command. Will be undefined when we have recorded an + * error test case. + */ + returnValue?: unknown; + + /** Inferred full targets added for context; not currently used in testing */ + fullTargets: TargetDescriptor[]; +}; diff --git a/src/testUtil/TestCaseRecorder.ts b/src/testUtil/TestCaseRecorder.ts index 76e2845658..323d6b65e0 100644 --- a/src/testUtil/TestCaseRecorder.ts +++ b/src/testUtil/TestCaseRecorder.ts @@ -4,20 +4,31 @@ import { invariant } from "immutability-helper"; import { merge } from "lodash"; import * as path from "path"; import * as vscode from "vscode"; -import HatTokenMap from "../core/HatTokenMap"; -import { injectSpyIde, SpyInfo } from "../ide/spies/SpyIDE"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; +import { extractTargetedMarks } from "../libs/common/testUtil/extractTargetedMarks"; +import serialize from "../libs/common/testUtil/serialize"; +import SpyIDE from "../libs/common/ide/spy/SpyIDE"; +import sleep from "../libs/common/util/sleep"; +import { getKey } from "@cursorless/common"; +import { walkDirsSync } from "../libs/common/util/walkSync"; +import ide, { + injectIde, +} from "../libs/cursorless-engine/singletons/ide.singleton"; +import { IDE } from "../libs/common/ide/types/ide.types"; +import { + ExtraSnapshotField, + takeSnapshot, +} from "../libs/vscode-common/testUtil/takeSnapshot"; +import { DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST } from "../libs/vscode-common/testUtil/testConstants"; +import { + marksToPlainObject, + SerializedMarks, +} from "../libs/vscode-common/toPlainObject"; import { DecoratedSymbolMark } from "../typings/targetDescriptor.types"; import { Graph } from "../typings/Types"; import { getDocumentRange } from "../util/rangeUtils"; -import sleep from "../util/sleep"; -import { extractTargetedMarks } from "./extractTargetedMarks"; -import serialize from "./serialize"; -import { ExtraSnapshotField, takeSnapshot } from "./takeSnapshot"; -import { TestCase, TestCaseCommand, TestCaseContext } from "./TestCase"; -import { DEFAULT_TEXT_EDITOR_OPTIONS_FOR_TEST } from "./testConstants"; -import { marksToPlainObject, SerializedMarks } from "./toPlainObject"; -import { walkDirsSync } from "./walkSync"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +import { TestCase, TestCaseContext } from "./TestCase"; +import { TestCaseCommand } from "./TestCaseFixture"; const CALIBRATION_DISPLAY_BACKGROUND_COLOR = "#230026"; const CALIBRATION_DISPLAY_DURATION_MS = 50; @@ -90,15 +101,18 @@ export class TestCaseRecorder { backgroundColor: CALIBRATION_DISPLAY_BACKGROUND_COLOR, }); private captureFinalThatMark: boolean = false; - private spyInfo: SpyInfo | undefined; + private spyIde: SpyIDE | undefined; + private originalIde: IDE | undefined; constructor(private graph: Graph) { - graph.extensionContext.subscriptions.push(this); + ide().disposeOnExit(this); + + const { runMode, assetsRoot, workspaceFolders } = ide(); this.workspacePath = - graph.extensionContext.extensionMode === vscode.ExtensionMode.Development - ? graph.extensionContext.extensionPath - : vscode.workspace.workspaceFolders?.[0].uri.path ?? null; + runMode === "development" + ? assetsRoot + : workspaceFolders?.[0].uri.path ?? null; this.workspaceName = this.workspacePath ? path.basename(this.workspacePath) @@ -155,7 +169,7 @@ export class TestCaseRecorder { let marks: SerializedMarks | undefined; if (targetedMarks.length !== 0) { const keys = targetedMarks.map(({ character, symbolColor }) => - HatTokenMap.getKey(symbolColor, character), + getKey(symbolColor, character), ); const readableHatMap = await this.graph.hatTokenMap.getReadableMap( usePrePhraseSnapshot, @@ -172,6 +186,8 @@ export class TestCaseRecorder { undefined, ["clipboard"], this.active ? this.extraSnapshotFields : undefined, + getActiveTextEditor()!, + ide(), marks, this.active ? { startTimestamp: this.startTimestamp } : undefined, metadata, @@ -303,12 +319,14 @@ export class TestCaseRecorder { await this.finishTestCase(); } else { // Otherwise, we are starting a new test case - this.spyInfo = injectSpyIde(this.graph); + this.originalIde = ide(); + this.spyIde = new SpyIDE(this.originalIde); + injectIde(this.spyIde!); this.testCase = new TestCase( command, context, - this.spyInfo.spy, + this.spyIde, this.isHatTokenMapTest, this.isDecorationsTest, this.startTimestamp!, @@ -447,8 +465,9 @@ export class TestCaseRecorder { } finallyHook() { - this.spyInfo?.dispose(); - this.spyInfo = undefined; + injectIde(this.originalIde!); + this.spyIde = undefined; + this.originalIde = undefined; const editor = getActiveTextEditor()!; editor.options = this.originalTextEditorOptions; diff --git a/src/testUtil/cleanUpTestCaseCommand.ts b/src/testUtil/cleanUpTestCaseCommand.ts index c7a526a5c9..61b7ff5b10 100644 --- a/src/testUtil/cleanUpTestCaseCommand.ts +++ b/src/testUtil/cleanUpTestCaseCommand.ts @@ -1,4 +1,4 @@ -import { TestCaseCommand } from "./TestCase"; +import { TestCaseCommand } from "./TestCaseFixture"; export function cleanUpTestCaseCommand( command: TestCaseCommand, diff --git a/src/testUtil/fromPlainObject.ts b/src/testUtil/fromPlainObject.ts index 9ebd78460e..69d0640443 100644 --- a/src/testUtil/fromPlainObject.ts +++ b/src/testUtil/fromPlainObject.ts @@ -1,12 +1,12 @@ import { Position, Range, Selection, TextEditor } from "vscode"; import { UntypedTarget } from "../processTargets/targets"; import type { Target } from "../typings/target.types"; -import { +import type { PositionPlainObject, RangePlainObject, SelectionPlainObject, TargetPlainObject, -} from "./toPlainObject"; +} from "../libs/vscode-common/toPlainObject"; /** * Given a plain object describing a target, constructs a `Target` object. diff --git a/src/typings/Types.ts b/src/typings/Types.ts index afd6f96651..17178b0adc 100644 --- a/src/typings/Types.ts +++ b/src/typings/Types.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode"; -import { ExtensionContext, Location } from "vscode"; +import { Location } from "vscode"; import { SyntaxNode } from "web-tree-sitter"; import { ActionRecord } from "../actions/actions.types"; import Cheatsheet from "../core/Cheatsheet"; @@ -11,12 +11,10 @@ import HatTokenMap from "../core/HatTokenMap"; import { ReadOnlyHatMap } from "../core/IndividualHatMap"; import { Snippets } from "../core/Snippets"; import StatusBarItem from "../core/StatusBarItem"; -import { TokenGraphemeSplitter } from "../core/TokenGraphemeSplitter"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import { IDE } from "../ide/ide.types"; +import { CommandServerApi } from "../libs/vscode-common/getExtensionApi"; import { ModifierStage } from "../processTargets/PipelineStages.types"; import { TestCaseRecorder } from "../testUtil/TestCaseRecorder"; -import { CommandServerApi } from "../util/getExtensionApi"; import { Target } from "./target.types"; import { FullRangeInfo } from "./updateSelections"; @@ -108,11 +106,6 @@ export interface Graph { */ readonly hatTokenMap: HatTokenMap; - /** - * The extension context passed in during extension activation - */ - readonly extensionContext: ExtensionContext; - /** * Keeps a merged list of all user-contributed, core, and * extension-contributed cursorless snippets @@ -164,17 +157,6 @@ export interface Graph { * Creates a VSCode status bar item */ readonly statusBarItem: StatusBarItem; - - /** - * Used to split a token into a graphemes that can be used for a hat placement - */ - readonly tokenGraphemeSplitter: TokenGraphemeSplitter; - - /** - * Used to interact with the ide - * NB: We make ide mutable to allow spying - */ - ide: IDE; } export type NodeMatcherValue = { @@ -235,24 +217,3 @@ export type TextFormatterName = | "pascalCase" | "snakeCase" | "upperSnakeCase"; - -export interface TokenHatSplittingMode { - /** - * Whether to distinguished between uppercase and lower case letters for hat - */ - preserveCase: boolean; - - /** - * A list of characters whose accents should not be stripped. This can be - * used, for example, if you would like to strip all accents except for those - * of a few characters, which you can add to this string. - */ - lettersToPreserve: string[]; - - /** - * A list of symbols that shouldn't be normalized by the token hat splitter. - * Add any extra symbols here that you have added to your - * capture. - */ - symbolsToPreserve: string[]; -} diff --git a/src/util/Clipboard.ts b/src/util/Clipboard.ts deleted file mode 100644 index f2d13d1b97..0000000000 --- a/src/util/Clipboard.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as vscode from "vscode"; - -/** - * A mockable layer over the vscode clipboard - * - * For unknown reasons it's not possible to mock the clipboard directly. - * Use this instead of vscode.env.clipboard so it can be mocked in testing. - **/ -export class Clipboard { - static readText = vscode.env.clipboard.readText; - static writeText = vscode.env.clipboard.writeText; -} diff --git a/src/util/addDecorationsToEditor.ts b/src/util/addDecorationsToEditor.ts index b98444e57e..a536ee2b9b 100644 --- a/src/util/addDecorationsToEditor.ts +++ b/src/util/addDecorationsToEditor.ts @@ -3,13 +3,13 @@ import * as vscode from "vscode"; import Decorations from "../core/Decorations"; import { HatStyleName } from "../core/hatStyles"; import { IndividualHatMap } from "../core/IndividualHatMap"; -import { TokenGraphemeSplitter } from "../core/TokenGraphemeSplitter"; -import { getMatcher } from "../core/tokenizer"; +import { TokenGraphemeSplitter } from "../libs/cursorless-engine/tokenGraphemeSplitter"; +import { getMatcher } from "../libs/cursorless-engine/tokenizer"; import { Token } from "../typings/Types"; import { getDisplayLineMap } from "./getDisplayLineMap"; import { getTokenComparator } from "./getTokenComparator"; import { getTokensInRange } from "./getTokensInRange"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; export function addDecorationsToEditors( hatTokenMap: IndividualHatMap, diff --git a/src/util/getTokensInRange.ts b/src/util/getTokensInRange.ts index d7d11ccbac..b59f5dd0e8 100644 --- a/src/util/getTokensInRange.ts +++ b/src/util/getTokensInRange.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode"; -import { tokenize } from "../core/tokenizer"; +import { tokenize } from "../libs/cursorless-engine/tokenizer"; import { RangeOffsets } from "../typings/updateSelections"; export interface PartialToken { diff --git a/src/util/graphFactories.ts b/src/util/graphFactories.ts index 24069a44e8..f669efeace 100644 --- a/src/util/graphFactories.ts +++ b/src/util/graphFactories.ts @@ -7,9 +7,7 @@ import FontMeasurements from "../core/FontMeasurements"; import HatTokenMap from "../core/HatTokenMap"; import { Snippets } from "../core/Snippets"; import StatusBarItem from "../core/StatusBarItem"; -import { TokenGraphemeSplitter } from "../core/TokenGraphemeSplitter"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; -import VscodeIDE from "../ide/vscode/VscodeIDE"; import { TestCaseRecorder } from "../testUtil/TestCaseRecorder"; import { Graph } from "../typings/Types"; import { FactoryMap } from "./makeGraph"; @@ -30,8 +28,6 @@ const graphConstructors: Partial> = { testCaseRecorder: TestCaseRecorder, cheatsheet: Cheatsheet, statusBarItem: StatusBarItem, - tokenGraphemeSplitter: TokenGraphemeSplitter, - ide: VscodeIDE, }; const graphFactories: Partial> = Object.fromEntries( diff --git a/src/util/makeGraph.ts b/src/util/makeGraph.ts index 6bde8244a6..145a896bc6 100644 --- a/src/util/makeGraph.ts +++ b/src/util/makeGraph.ts @@ -1,5 +1,4 @@ import isTesting from "../testUtil/isTesting"; -import { ExtractMutable } from "./typeUtils"; export type FactoryMap = { [P in keyof T]: (t: T) => T[P]; @@ -39,16 +38,12 @@ function makeGetter( export default function makeGraph( factoryMap: FactoryMap, - writableKeys: ExtractMutable[] = [], ) { const components: Partial = {}; const graph: Partial = {}; const lockedKeys: (keyof GraphType)[] = []; Object.keys(factoryMap).forEach((key: keyof GraphType | PropertyKey) => { - const isMutable = ( - writableKeys as (keyof GraphType | PropertyKey)[] - ).includes(key); Object.defineProperty(graph, key, { get: makeGetter( graph as GraphType, @@ -58,14 +53,8 @@ export default function makeGraph( key as keyof GraphType, ), - set: isMutable - ? (value) => { - components[key as keyof GraphType] = value; - } - : undefined, - // NB: We allow mutable keys for spying - configurable: isMutable || isTesting(), + configurable: isTesting(), }); }); diff --git a/src/util/notebook.ts b/src/util/notebook.ts index 1cea4754f2..ddb0563682 100644 --- a/src/util/notebook.ts +++ b/src/util/notebook.ts @@ -1,4 +1,4 @@ -import { NotebookCell, NotebookDocument, TextDocument, window } from "vscode"; +import { NotebookCell, TextDocument, window } from "vscode"; import { getNotebookFromCellDocumentLegacy, isVscodeLegacyNotebookVersion, @@ -36,21 +36,3 @@ export function getNotebookFromCellDocument(document: TextDocument) { return notebookEditor; } - -/** - * Returns the index of the cell corresponding to the given document in the - * notebook. Assumes that the given notebook contains the given cell - * @param notebookDocument The notebook document containing the cell - * @param document The document corresponding to the given cell - * @returns The index of the cell in the notebook - */ -export function getCellIndex( - notebookDocument: NotebookDocument, - document: TextDocument, -) { - return notebookDocument - .getCells() - .findIndex( - (cell) => cell.document.uri.toString() === document.uri.toString(), - ); -} diff --git a/src/util/notebookLegacy.ts b/src/util/notebookLegacy.ts index f329f6925a..bc7e8f02ba 100644 --- a/src/util/notebookLegacy.ts +++ b/src/util/notebookLegacy.ts @@ -7,8 +7,9 @@ import { TextEditor, version, } from "vscode"; -import { getCellIndex, getNotebookFromCellDocument } from "./notebook"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +import { getNotebookFromCellDocument } from "./notebook"; +import { getCellIndex } from "../libs/vscode-common/notebook"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; export function isVscodeLegacyNotebookVersion() { return semver.lt(version, "1.68.0"); diff --git a/src/util/setSelectionsAndFocusEditor.ts b/src/util/setSelectionsAndFocusEditor.ts index 87e6ed3ae7..34cb15f2d2 100644 --- a/src/util/setSelectionsAndFocusEditor.ts +++ b/src/util/setSelectionsAndFocusEditor.ts @@ -8,13 +8,14 @@ import { ViewColumn, window, } from "vscode"; -import { getCellIndex, getNotebookFromCellDocument } from "./notebook"; +import { getNotebookFromCellDocument } from "./notebook"; +import { getCellIndex } from "../libs/vscode-common/notebook"; import { focusNotebookCellLegacy, isVscodeLegacyNotebookVersion, } from "./notebookLegacy"; import uniqDeep from "./uniqDeep"; -import { getActiveTextEditor } from "../ide/activeTextEditor"; +import { getActiveTextEditor } from "../ide/vscode/activeTextEditor"; const columnFocusCommands = { [ViewColumn.One]: "workbench.action.focusFirstEditorGroup", diff --git a/tsconfig.json b/tsconfig.json index 51fac6ee69..236da3e949 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,11 +6,15 @@ "lib": ["es2020"], "sourceMap": true, "rootDir": "src", - "strict": true /* enable all strict type-checking options */ + "strict": true /* enable all strict type-checking options */, /* Additional Checks */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ + "paths": { + "@cursorless/common": ["./src/libs/common/index.ts"], + "@cursorless/vscode-common": ["./src/libs/vscode-common/index.ts"] + } }, "exclude": [ "cursorless-nx", diff --git a/yarn.lock b/yarn.lock index bdf446c4ef..781dd757fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1376,6 +1376,11 @@ mocha@^8.1.3: yargs-parser "20.2.4" yargs-unparser "2.0.0" +module-alias@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" + integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -1873,7 +1878,7 @@ treeify@^1.0.1, treeify@^1.1.0: resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== -ts-unused-exports@8.0.0: +ts-unused-exports@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/ts-unused-exports/-/ts-unused-exports-8.0.0.tgz#6dd15ff26286e0b7e5663cda3b98c77ea6f3ffe7" integrity sha512-gylHFyJqC80PSb4zy35KTckykEW1vmKjnOHjBeX9iKBo4b/SzqQIcXXbYSuif4YMgNm6ewFF62VM1C9z0bGZPw== @@ -1920,7 +1925,7 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^4.5.5: +typescript@4.6.3: version "4.6.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== @@ -1963,6 +1968,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +vscode-uri@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.6.tgz#5e6e2e1a4170543af30151b561a41f71db1d6f91" + integrity sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ== + which@2.0.2, which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"