From 2d38438e15724f13cb2896f39b56edbaa6eb97ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Geis?= Date: Tue, 4 May 2021 22:09:29 +0200 Subject: [PATCH] Rewrite Dance for v0.5.0. --- .eslintrc.js | 21 +- .github/workflows/main.yml | 11 + .vscode/launch.json | 44 +- .vscode/settings.json | 31 +- .vscode/tasks.json | 25 +- .vscodeignore | 1 + LICENSE | 2 +- README.md | 364 +- assets/dance.afdesign | Bin 0 -> 50185 bytes assets/dance.png | Bin 0 -> 14536 bytes assets/dance.svg | 39 + commands/README.md | 199 - commands/commands.yaml | 687 --- commands/generate.ts | 269 - commands/index.ts | 3111 ----------- meta.ts | 692 +++ package.build.ts | 651 +++ package.json | 4862 ++++++++++------- package.ts | 391 -- recipes/README.md | 98 + recipes/evil-dance.md | 90 + src/api/clipboard.ts | 16 + src/api/context.ts | 526 ++ src/api/edit/index.ts | 791 +++ src/api/edit/linewise.ts | 339 ++ src/api/errors.ts | 214 + src/api/functional.ts | 416 ++ src/api/history.ts | 17 + src/api/index.ts | 71 + src/api/keybindings/built-in.build.ts | 11 + src/api/keybindings/built-in.ts | 1117 ++++ src/api/keybindings/index.ts | 7 + .../api/keybindings/layout-azerty.ts | 194 +- src/api/lines.ts | 180 + src/api/menu.ts | 173 + src/api/modes.ts | 38 + src/api/positions.ts | 167 + src/api/prompt.ts | 426 ++ src/api/registers.ts | 53 + src/api/run.ts | 464 ++ src/api/search/index.ts | 386 ++ src/api/search/lines.ts | 54 + src/api/search/move-to.ts | 100 + src/api/search/move.ts | 601 ++ src/api/search/pairs.ts | 177 + src/api/search/range.ts | 634 +++ src/api/search/word.ts | 130 + src/api/selections.ts | 1892 +++++++ src/commands/README.build.ts | 118 + src/commands/README.md | 1208 ++++ src/commands/changes.ts | 205 - src/commands/count.ts | 13 - src/commands/dev.ts | 31 + src/commands/edit.ts | 405 ++ src/commands/goto.ts | 272 - src/commands/history.ts | 276 +- src/commands/index.ts | 635 +-- src/commands/insert.ts | 208 - src/commands/keybindings.ts | 33 + src/commands/load-all.build.ts | 199 + src/commands/load-all.ts | 1289 +++++ src/commands/macros.ts | 49 - src/commands/mark.ts | 201 - src/commands/menus.ts | 55 - src/commands/misc.ts | 309 +- src/commands/modes.ts | 79 +- src/commands/move.ts | 135 - src/commands/pipe.ts | 355 -- src/commands/rotate.ts | 91 - src/commands/search.ts | 572 +- src/commands/seek.ts | 462 ++ src/commands/select.ts | 1192 ++-- src/commands/selectObject.ts | 897 --- src/commands/selections.rotate.ts | 62 + src/commands/selections.ts | 1154 ++-- src/commands/view.ts | 29 + src/commands/yankPaste.ts | 326 -- src/extension.ts | 46 +- src/registers.ts | 147 - src/state/document.ts | 186 - src/state/editor.ts | 584 -- src/state/editors.ts | 590 ++ src/state/extension.ts | 741 +-- src/state/modes.ts | 695 +++ src/state/recorder.ts | 923 ++++ src/state/registers.ts | 604 ++ src/state/status-bar.ts | 92 + src/utils/assert.ts | 3 - src/utils/charset.ts | 5 +- src/utils/disposables.ts | 231 + src/utils/misc.ts | 59 + src/utils/prompt.ts | 131 - src/utils/regexp.ts | 1705 ++++++ src/utils/savedSelection.ts | 66 - src/utils/selectionHelper.ts | 538 -- src/utils/settings-validator.ts | 71 + src/utils/tracked-selection.ts | 331 ++ test/README.md | 329 +- test/suite/api.test.build.ts | 92 + test/suite/api.test.ts | 1353 +++++ test/suite/build-utils.ts | 24 + test/suite/commands.build.ts | 275 + test/suite/commands.test.ts | 343 -- test/suite/commands/edit-deindent.md | 38 + test/suite/commands/edit-deindent.test.ts | 72 + test/suite/commands/edit-indent.md | 130 + test/suite/commands/edit-indent.test.ts | 152 + test/suite/commands/edit-join.md | 71 + test/suite/commands/edit-join.test.ts | 118 + test/suite/commands/edit-paste-before.md | 27 + test/suite/commands/edit-paste-before.test.ts | 57 + test/suite/commands/edit-paste.md | 60 + test/suite/commands/edit-paste.test.ts | 109 + test/suite/commands/indent | 56 - test/suite/commands/indent.caret | 68 - test/suite/commands/join | 14 - test/suite/commands/join.1 | 17 - test/suite/commands/move.lineend | 66 - test/suite/commands/objects | 132 - test/suite/commands/objects.paragraph | 99 - test/suite/commands/objects.sentence | 214 - test/suite/commands/objects.sentence.caret | 5 - test/suite/commands/objects.word.caret | 9 - test/suite/commands/paste | 23 - test/suite/commands/search | 179 - test/suite/commands/search-next.md | 159 + test/suite/commands/search-next.test.ts | 259 + test/suite/commands/search.md | 412 ++ test/suite/commands/search.next | 83 - test/suite/commands/search.test.ts | 604 ++ test/suite/commands/seek-enclosing.md | 94 + test/suite/commands/seek-enclosing.test.ts | 120 + test/suite/commands/seek-object-between.md | 234 + .../commands/seek-object-between.test.ts | 384 ++ test/suite/commands/seek-object-charset.md | 27 + .../commands/seek-object-charset.test.ts | 56 + test/suite/commands/seek-object-paragraph.md | 197 + .../commands/seek-object-paragraph.test.ts | 269 + test/suite/commands/seek-object-quoted.md | 31 + .../suite/commands/seek-object-quoted.test.ts | 56 + test/suite/commands/seek-object-sentence.md | 464 ++ .../commands/seek-object-sentence.test.ts | 561 ++ test/suite/commands/seek-word-edge.md | 283 + test/suite/commands/seek-word-edge.test.ts | 397 ++ test/suite/commands/seek-word-end.md | 36 + test/suite/commands/seek-word-end.test.ts | 73 + test/suite/commands/seek-word.md | 338 ++ test/suite/commands/seek-word.test.ts | 551 ++ test/suite/commands/seek.md | 213 + test/suite/commands/seek.test.ts | 353 ++ test/suite/commands/select | 232 - test/suite/commands/select-lateral.md | 258 + test/suite/commands/select-lateral.test.ts | 426 ++ test/suite/commands/select-line-end.md | 70 + test/suite/commands/select-line-end.test.ts | 128 + test/suite/commands/select-lines.md | 350 ++ test/suite/commands/select-lines.test.ts | 517 ++ test/suite/commands/select.copy | 80 - test/suite/commands/select.enclosing | 58 - test/suite/commands/select.line.caret | 39 - test/suite/commands/select.lineend.empty | 54 - test/suite/commands/select.to | 83 - test/suite/commands/select.word | 89 - test/suite/commands/select.word.1 | 22 - test/suite/commands/select.word.2 | 25 - test/suite/commands/select.word.caret | 14 - test/suite/commands/select.word.edge | 130 - test/suite/commands/selections-copy.md | 152 + test/suite/commands/selections-copy.test.ts | 221 + test/suite/commands/selections-trim.md | 143 + test/suite/commands/selections-trim.test.ts | 193 + test/suite/commands/trim | 18 - test/suite/index.ts | 102 +- test/suite/utils.test.ts | 34 + test/suite/utils.ts | 519 ++ tsconfig.json | 8 +- yarn.lock | 1643 +++--- 177 files changed, 38404 insertions(+), 16980 deletions(-) create mode 100644 assets/dance.afdesign create mode 100644 assets/dance.png create mode 100644 assets/dance.svg delete mode 100644 commands/README.md delete mode 100644 commands/commands.yaml delete mode 100644 commands/generate.ts delete mode 100644 commands/index.ts create mode 100644 meta.ts create mode 100644 package.build.ts delete mode 100644 package.ts create mode 100644 recipes/README.md create mode 100644 recipes/evil-dance.md create mode 100644 src/api/clipboard.ts create mode 100644 src/api/context.ts create mode 100644 src/api/edit/index.ts create mode 100644 src/api/edit/linewise.ts create mode 100644 src/api/errors.ts create mode 100644 src/api/functional.ts create mode 100644 src/api/history.ts create mode 100644 src/api/index.ts create mode 100644 src/api/keybindings/built-in.build.ts create mode 100644 src/api/keybindings/built-in.ts create mode 100644 src/api/keybindings/index.ts rename layouts/azerty.json => src/api/keybindings/layout-azerty.ts (56%) create mode 100644 src/api/lines.ts create mode 100644 src/api/menu.ts create mode 100644 src/api/modes.ts create mode 100644 src/api/positions.ts create mode 100644 src/api/prompt.ts create mode 100644 src/api/registers.ts create mode 100644 src/api/run.ts create mode 100644 src/api/search/index.ts create mode 100644 src/api/search/lines.ts create mode 100644 src/api/search/move-to.ts create mode 100644 src/api/search/move.ts create mode 100644 src/api/search/pairs.ts create mode 100644 src/api/search/range.ts create mode 100644 src/api/search/word.ts create mode 100644 src/api/selections.ts create mode 100644 src/commands/README.build.ts create mode 100644 src/commands/README.md delete mode 100644 src/commands/changes.ts delete mode 100644 src/commands/count.ts create mode 100644 src/commands/dev.ts create mode 100644 src/commands/edit.ts delete mode 100644 src/commands/goto.ts delete mode 100644 src/commands/insert.ts create mode 100644 src/commands/keybindings.ts create mode 100644 src/commands/load-all.build.ts create mode 100644 src/commands/load-all.ts delete mode 100644 src/commands/macros.ts delete mode 100644 src/commands/mark.ts delete mode 100644 src/commands/menus.ts delete mode 100644 src/commands/move.ts delete mode 100644 src/commands/pipe.ts delete mode 100644 src/commands/rotate.ts create mode 100644 src/commands/seek.ts delete mode 100644 src/commands/selectObject.ts create mode 100644 src/commands/selections.rotate.ts create mode 100644 src/commands/view.ts delete mode 100644 src/commands/yankPaste.ts delete mode 100644 src/registers.ts delete mode 100644 src/state/document.ts delete mode 100644 src/state/editor.ts create mode 100644 src/state/editors.ts create mode 100644 src/state/modes.ts create mode 100644 src/state/recorder.ts create mode 100644 src/state/registers.ts create mode 100644 src/state/status-bar.ts delete mode 100644 src/utils/assert.ts create mode 100644 src/utils/disposables.ts create mode 100644 src/utils/misc.ts delete mode 100644 src/utils/prompt.ts create mode 100644 src/utils/regexp.ts delete mode 100644 src/utils/savedSelection.ts delete mode 100644 src/utils/selectionHelper.ts create mode 100644 src/utils/settings-validator.ts create mode 100644 src/utils/tracked-selection.ts create mode 100644 test/suite/api.test.build.ts create mode 100644 test/suite/api.test.ts create mode 100644 test/suite/build-utils.ts create mode 100644 test/suite/commands.build.ts delete mode 100644 test/suite/commands.test.ts create mode 100644 test/suite/commands/edit-deindent.md create mode 100644 test/suite/commands/edit-deindent.test.ts create mode 100644 test/suite/commands/edit-indent.md create mode 100644 test/suite/commands/edit-indent.test.ts create mode 100644 test/suite/commands/edit-join.md create mode 100644 test/suite/commands/edit-join.test.ts create mode 100644 test/suite/commands/edit-paste-before.md create mode 100644 test/suite/commands/edit-paste-before.test.ts create mode 100644 test/suite/commands/edit-paste.md create mode 100644 test/suite/commands/edit-paste.test.ts delete mode 100644 test/suite/commands/indent delete mode 100644 test/suite/commands/indent.caret delete mode 100644 test/suite/commands/join delete mode 100644 test/suite/commands/join.1 delete mode 100644 test/suite/commands/move.lineend delete mode 100644 test/suite/commands/objects delete mode 100644 test/suite/commands/objects.paragraph delete mode 100644 test/suite/commands/objects.sentence delete mode 100644 test/suite/commands/objects.sentence.caret delete mode 100644 test/suite/commands/objects.word.caret delete mode 100644 test/suite/commands/paste delete mode 100644 test/suite/commands/search create mode 100644 test/suite/commands/search-next.md create mode 100644 test/suite/commands/search-next.test.ts create mode 100644 test/suite/commands/search.md delete mode 100644 test/suite/commands/search.next create mode 100644 test/suite/commands/search.test.ts create mode 100644 test/suite/commands/seek-enclosing.md create mode 100644 test/suite/commands/seek-enclosing.test.ts create mode 100644 test/suite/commands/seek-object-between.md create mode 100644 test/suite/commands/seek-object-between.test.ts create mode 100644 test/suite/commands/seek-object-charset.md create mode 100644 test/suite/commands/seek-object-charset.test.ts create mode 100644 test/suite/commands/seek-object-paragraph.md create mode 100644 test/suite/commands/seek-object-paragraph.test.ts create mode 100644 test/suite/commands/seek-object-quoted.md create mode 100644 test/suite/commands/seek-object-quoted.test.ts create mode 100644 test/suite/commands/seek-object-sentence.md create mode 100644 test/suite/commands/seek-object-sentence.test.ts create mode 100644 test/suite/commands/seek-word-edge.md create mode 100644 test/suite/commands/seek-word-edge.test.ts create mode 100644 test/suite/commands/seek-word-end.md create mode 100644 test/suite/commands/seek-word-end.test.ts create mode 100644 test/suite/commands/seek-word.md create mode 100644 test/suite/commands/seek-word.test.ts create mode 100644 test/suite/commands/seek.md create mode 100644 test/suite/commands/seek.test.ts delete mode 100644 test/suite/commands/select create mode 100644 test/suite/commands/select-lateral.md create mode 100644 test/suite/commands/select-lateral.test.ts create mode 100644 test/suite/commands/select-line-end.md create mode 100644 test/suite/commands/select-line-end.test.ts create mode 100644 test/suite/commands/select-lines.md create mode 100644 test/suite/commands/select-lines.test.ts delete mode 100644 test/suite/commands/select.copy delete mode 100644 test/suite/commands/select.enclosing delete mode 100644 test/suite/commands/select.line.caret delete mode 100644 test/suite/commands/select.lineend.empty delete mode 100644 test/suite/commands/select.to delete mode 100644 test/suite/commands/select.word delete mode 100644 test/suite/commands/select.word.1 delete mode 100644 test/suite/commands/select.word.2 delete mode 100644 test/suite/commands/select.word.caret delete mode 100644 test/suite/commands/select.word.edge create mode 100644 test/suite/commands/selections-copy.md create mode 100644 test/suite/commands/selections-copy.test.ts create mode 100644 test/suite/commands/selections-trim.md create mode 100644 test/suite/commands/selections-trim.test.ts delete mode 100644 test/suite/commands/trim create mode 100644 test/suite/utils.test.ts create mode 100644 test/suite/utils.ts diff --git a/.eslintrc.js b/.eslintrc.js index 7a9a743..9b811bd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,12 @@ module.exports = { "max-len": "off", }, }, + { + files: ["test/suite/commands/*.ts"], + rules: { + "no-useless-escape": "off", + }, + } ], rules: { "indent": ["error", 2, { @@ -25,6 +31,7 @@ module.exports = { "VariableDeclarator": "first", "flatTernaryExpressions": true, "offsetTernaryExpressions": true, + "ignoredNodes": ["TemplateLiteral *"], }], "curly": ["error", "all"], "dot-location": ["error", "property"], @@ -41,7 +48,7 @@ module.exports = { { code: 100, comments: 80, - ignorePattern: "^ *(\\*|//) ([sS]ee )?http\\S+\\)?.?$|^ *// =+( [^=]+ =+)?$", + ignorePattern: "^ *(\\*|//) ([sS]ee )?http\\S+\\)?.?$|^ *// =+( [^=]+ =+)?$|\|$", }, ], "multiline-ternary": ["error", "always-multiline"], @@ -50,21 +57,19 @@ module.exports = { "no-unexpected-multiline": "error", "no-unneeded-ternary": "error", "object-curly-spacing": ["error", "always"], - "operator-linebreak": ["error", "before"], + "operator-linebreak": ["error", "before", { overrides: { "=": "after" } }], "object-shorthand": "error", "quotes": ["error", "double", { avoidEscape: true, allowTemplateLiterals: true }], "semi": ["error", "always"], "sort-imports": [ "error", { + ignoreCase: true, ignoreDeclarationSort: true, memberSyntaxSortOrder: ["none", "all", "single", "multiple"], }, ], - "space-before-function-paren": [ - "error", - { anonymous: "always", named: "never", asyncArrow: "always" }, - ], + "space-before-function-paren": "off", "space-before-blocks": "error", "space-infix-ops": "error", "unicode-bom": "error", @@ -73,5 +78,9 @@ module.exports = { "no-case-declarations": "off", "no-cond-assign": "off", "@typescript-eslint/explicit-member-accessibility": ["error"], + "@typescript-eslint/space-before-function-paren": [ + "error", + { anonymous: "always", named: "never", asyncArrow: "always" }, + ], }, }; diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 63519c4..520161a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,3 +17,14 @@ jobs: - name: Check formatting and lints run: yarn run check + + - name: Ensure auto-generated files are up-to-date + run: yarn run ts-node ./meta.ts --ensure-up-to-date --check + + - name: Check tests + run: yarn run test + + - uses: butlerlogic/action-autotag@ade8d2e19bfcd1e6a91272e2849b4bf4c37a67f1 + with: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + tag_prefix: v diff --git a/.vscode/launch.json b/.vscode/launch.json index 31a3276..59779fb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,25 +2,55 @@ "version": "0.2.0", "configurations": [ { - "name": "Run Extension", + "name": "Launch extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceFolder}"], "outFiles": ["${workspaceFolder}/out/**/*.js"], - "preLaunchTask": "npm: watch" }, { - "name": "Run Extension Tests", + "name": "Run all tests", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ + "--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite" + "--extensionTestsPath=${workspaceFolder}/out/test/suite", ], "outFiles": ["${workspaceFolder}/out/**/*.js"], - "preLaunchTask": "npm: watch" - } - ] + }, + { + "name": "Run tests in this file", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite", + ], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "env": { + "CURRENT_FILE": "${relativeFile}", + }, + }, + { + "name": "Run test on this line", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite", + ], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "env": { + "CURRENT_FILE": "${relativeFile}", + "CURRENT_LINE": "${lineNumber}", + }, + }, + ], } diff --git a/.vscode/settings.json b/.vscode/settings.json index 7424905..2b0d766 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,31 @@ { - "files.associations": { - "test/suite/commands/*": "text" - }, "search.exclude": { "out": true }, - "[typescript]": { - "editor.rulers": [80, 100] + "editor.rulers": [80, 100], + "typescript.tsdk": "node_modules/typescript/lib", + + // Disable italic on doc comments since they contain a lot of Markdown: + "editor.tokenColorCustomizations": { + "textMateRules": [ + { + "scope": "comment.block.documentation", + "settings": { + "fontStyle": "", + }, + }, + ], + }, + + "dance.menus": { + "dancedev": { + "items": { + "b": { + "text": "toggle character selection behavior", + "command": "dance.dev.setSelectionBehavior", + "args": [{ "mode": "normal" }], + }, + }, + }, }, - "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 34edf97..819e4c0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,9 +1,11 @@ { "version": "2.0.0", "tasks": [ + // These tasks run automatically when loading Dance in VS Code. Make sure to + // enable them (Command palette > Manage automatic tasks in folder > Allow). { "type": "npm", - "script": "watch", + "script": "compile:watch", "problemMatcher": "$tsc-watch", "isBackground": true, "presentation": { @@ -12,7 +14,22 @@ "group": { "kind": "build", "isDefault": true - } - } - ] + }, + "runOptions": { + "runOn": "folderOpen", + }, + }, + { + "type": "npm", + "script": "generate:watch", + "problemMatcher": [], + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "runOptions": { + "runOn": "folderOpen", + }, + }, + ], } diff --git a/.vscodeignore b/.vscodeignore index 1166f14..37de047 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -9,3 +9,4 @@ test/** **/*.map **/*.ts **/*.lock +out/src/build.js diff --git a/LICENSE b/LICENSE index 9b1fb63..3155ccf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 Grégoire Geis +Copyright 2020-2021 Grégoire Geis Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. diff --git a/README.md b/README.md index 70f13e4..6252a2a 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,30 @@ [Kakoune]-inspired key bindings for [Visual Studio Code][vsc]. +## Important note + +The next release of Dance (available in this branch) is a complete rewrite from +the previous release. It adds many features (to list a few, custom modes, better +commands, and a scripting API) and has many QOL improvements (better status bar +buttons, better [character mode](#selection-behaviors), better history, support +for multiple editors for one document, more tests, and better internal +architecture). + +While this will improve the quality of Dance over time, in the short term this +will make it much less stable due to bugs. If you encounter a bug, please file +an issue (or directly submit a PR) containing [test cases](./test) for the +command. + +Thanks for bearing with me! + ## Huh? -Dance provides [Kakoune]-inspired commands and key bindings for [Visual Studio Code][vsc]. +Dance provides [Kakoune]-inspired commands and key bindings for +[Visual Studio Code][vsc], as well as support for custom modes and scripting. -These key bindings are (mostly) compatible with [Kakoune]'s, but are meant to be an addition -to [Visual Studio Code][vsc], rather than an emulation layer on top of it. +Added key bindings are (mostly) compatible with [Kakoune]'s, but are meant to be +an addition to [Visual Studio Code][vsc], rather than an emulation layer on top +of it. #### Why [VS Code][vsc], and not [Kakoune] directly? @@ -17,250 +35,174 @@ to [Visual Studio Code][vsc], rather than an emulation layer on top of it. #### Why [Kakoune]'s key bindings, and not [Vim]'s? -- Whether you prefer Vim, Emacs or Kakoune key bindings is a matter of preference. I, for one, - prefer [Kakoune's](https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc). +- Whether you prefer Vim, Emacs or Kakoune key bindings is a matter of + preference. I, for one, prefer + [Kakoune's](https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc). - Vim's key bindings are [already available to VS Code users][vscodevim]. #### Why is it merely 'inspired' by [Kakoune]? -- Unlike [VSCodeVim] which attempts to emulate Vim, Dance's only goal is to provide - VS Code-native [commands][vsccommands] and [key bindings][vsckeybindings] that are inspired by [Kakoune]. - - Some features are provided to mimic Kakoune's behavior (e.g. treating positions as coordonates - of characters, rather than carets between characters like VS Code), but are optional. -- Kakoune, Vim and VS Code are all fully-fledged text editors; therefore, they have overlapping - features. For instance, where [VSCodeVim] provides its own multi-cursor and command engines - to feel more familiar to existing Vim users, Dance leaves multi-cursor mode and editor - commands to VS Code entirely. +- Unlike [VSCodeVim] which attempts to emulate Vim, Dance's only goal is to + provide VS Code-native [commands][vsccommands] and + [key bindings][vsckeybindings] that are inspired by [Kakoune]. + - Some features are provided to mimic Kakoune's behavior (e.g. treating + positions as coordonates of characters, rather than carets between + characters like VS Code), but are optional. +- Kakoune, Vim and VS Code are all fully-fledged text editors; therefore, they + have overlapping features. For instance, where [VSCodeVim] provides its own + multi-cursor and command engines to feel more familiar to existing Vim users, + Dance leaves multi-cursor mode and editor commands to VS Code entirely. ## User Guide -For most [commands], the usage is the same as in [Kakoune]. However, the following changes have been made: +For most [commands], the usage is the same as in [Kakoune]. However, the +following changes have been made: + +### Custom modes + +All modes are custom. By default, the `normal` and `insert` modes are defined, +and many [Kakoune]-inspired keybindings are available. More modes can be +created, though. These modes are configured with `dance.modes`. ### Selection behaviors -Dance by default uses caret-based selections just like VSCode. This means a selection is anchored between two carets -(i.e. positions between characters), and may be empty. +Dance by default uses caret-based selections just like VS Code. This means a +selection is anchored between two carets (i.e. positions between characters), +and may be empty. -If you prefer character-based selections like Kakoune, please set `"dance.selectionBehavior": "character"` in your -settings. This will make Dance treat selections as inclusive ranges between two characters, and implies that each -selection will contain at least one character. (This behavior is recommended for Kakoune-users who have already -developed muscle memory, e.g. hitting `;d` to delete one character.) +If you prefer character-based selections like Kakoune, please set +`"selectionBehavior": "character"` in the configuration of the mode in which you +wish to use character-based selections. This mode is designed to work with +block-style cursors, so your configuration would typically look like: + +```json +"dance.modes": { + "insert": { + // ... + }, + "normal": { + "cursorStyle": "block", + "selectionBehavior": "character", + // ... + } +}, +``` + +If this is enabled, Dance will internally treat selections as inclusive ranges +between two characters and imply that each selection contains at least one +character. + +### Scripting + +Most keybindings exposed by Dance are actually implemented by running several +Dance commands in a row. For instance, `dance.modes.set.normal` is actually a +wrapper around `dance.modes.set` with the argument `{ input: "normal" }`. +Commands that take an input, like `dance.modes.set`, will prompt a user for a +value if no argument is given. + +Additionally to having commands with many settings, Dance also exposes the +`dance.run` command, which runs JavaScript code. That code has access to the +[Dance API][API], and can perform operations with more control than Dance +commands. Where Dance commands in the `dance.selections` namespace operate the +same way on all selections at once, `dance.run` can be used to individually +manipulate selections. + +Finally, the [Dance API][API] is exported by Dance. Other VS Code extensions +can specify that they depend on Dance (with the [`extensionDependencies` +property](https://code.visualstudio.com/api/references/extension-manifest#fields)), +and then access the API by calling [`activate`]( +https://code.visualstudio.com/api/references/vscode-api#Extension.activate): + +```js +const { api } = await vscode.extensions.getExtension("gregoire.dance").activate(); +``` ### Pipes -- Pipes no longer accept shell commands, but instead accept 'expressions', those being: +Pipes no longer accept shell commands, but instead accept "expressions", those +being: +- `#`: Pipes each selection into a shell command (the shell is + taken from the `terminal.external.exec` value). +- `/[/[/]`: A RegExp literal, as + [defined in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions). + Do note the addition of a `replacement`, for commands that add or replace + text. +- ``: A JavaScript expression in which the following variables + are available: + - `$`: Text of the current selection. + - `$$`: Array of the text of all the selections. + - `i`: Index of the current selection. + - `n`: Number of selections in `$$`. - - `#`: Pipes each selection into a shell command (the shell is taken from the `terminal.external.exec` value). - - `/[/[/]`: A RegExp literal, as [defined in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions). Do note the addition of a `replacement`, for commands that add or replace text. - - ``: A JavaScript expression in which the following variables are available: - - - `$`: Text of the current selection. - - `$$`: Array of the text of all the selections. - - `i`: Index of the current selection. - - Depending on the result of the expression, it will be inserted differently: - - - `string`: Inserted directly. - - `number`: Inserted in its string representation. - - `boolean`: Inserted as `true` or `false`. - - `null`: Inserted as `null`. - - `undefined`: Inserted as an empty string. - - `object`: Inserted as JSON. - - Any other type: Leads to an error. + Depending on the result of the expression, it will be inserted differently: + - `string`: Inserted directly. + - `number`: Inserted in its string representation. + - `boolean`: Inserted as `true` or `false`. + - `null`: Inserted as `null`. + - `undefined`: Inserted as an empty string. + - `object`: Inserted as JSON. + - Any other type: Leads to an error. #### Examples - `/(\d+),(\d+)/$1.$2/g` replaces `12,34` into `12.34`. -- `i + 1` replaces `1,1,1,1,1` into `1,2,3,4,5`, assuming that each selection is on a different digit. +- `i + 1` replaces `1,1,1,1,1` into `1,2,3,4,5`, assuming that each selection is + on a different digit. ### Miscellaneous changes -A few changes were made from Kakoune, mostly out of personal preference, and to make the -extension integrate better in VS Code. If you disagree with any of these changes, -you're welcome to open an issue to discuss it, or to add an option for it by submitting a PR. +A few changes were made from Kakoune, mostly out of personal preference, and to +make the extension integrate better with VS Code. -- The cursor is not a block, but a line: Dance focuses on selections, and using a line instead of - a block makes it obvious whether zero or one characters are selected. Besides, the line-shaped - cursor is the default in VS Code. -- Changing the mode will also change the `editor.lineNumbers` configuration value to `on` in `insert` - mode, and `relative` in normal mode. - The default yank register `"` maps to the system clipboard. -- There are some additional features not documented here but mentioned in [issues] and/or in - the configuration of the plugin. TODO: document them here. +- When using the default configuration (that is to say, these settings can be + modified): + - The cursor is not a block, but a line: Dance focuses on selections, and + using a line instead of a block makes it obvious whether zero or one + characters are selected. Besides, the line-shaped cursor is the default in + VS Code. + - Changing the mode will also change the `editor.lineNumbers` configuration + value to `on` in `insert` mode, and `relative` in normal mode. ### Troubleshooting -Dance uses the built-in VS Code key bindings, and therefore does not override the `type` command. -**However**, it sometimes needs access to the `type` command, in dialogs and register selection, -for instance. Consequently, it is not compatible with extensions that always override the `type` -command, such as [VSCodeVim]; these extensions must therefore be disabled. - -## Progress - -This project is still a WIP. It has gotten better over the years, but may have annoying bugs -and lack some features, especially for Kakoune users. Despite this, several users use Dance -daily. - -In the following list, if a command is implemented, then its extending equivalent -(activated while pressing `Shift`) then likely is implemented as well. - -Most (but not all) commands defined in [`commands`][commands] are implemented. - -- [x] Basic movements: - - [x] Arrows, hjkl. - - [x] Move to character, move until character. - - [x] Move to next word, move to previous word. -- [x] Insert mode: - - [x] Enter insert mode with `a`, `i`, `o`, and their `Alt` / `Shift` equivalents. - - [x] Exit insert mode with `Escape`. -- [x] Basic selections: - - [x] Search in selections. - - [x] Split in selections. - - [x] Split selections by lines. - - [x] Extend selections by taking lines. - - [x] Trim selections. -- [x] Pipes. -- [x] Object selection. -- [x] Yanking: - - [x] Yank. - - [x] Paste. -- [x] Rotate: - - [x] Rotate selections only. - - [x] Rotate selections content only. - - [x] Rotate selections and content. -- [x] Changes: - - [x] Join. - - [x] Replace. - - [x] Delete. - - [x] Indent. - - [x] Dedent. - - [x] Change case. -- [x] Search. -- [ ] History: - - [x] Undo / redo. - - [ ] Forward / backward. - - [x] Repeat command. - - [ ] Repeat insertion. -- [x] Macros. -- [ ] Registers. +- Dance uses the built-in VS Code key bindings, and therefore does not override + the `type` command. **However**, it sometimes needs access to the `type` + command, in dialogs and register selection, for instance. Consequently, it is + not compatible with extensions that always override the `type` command, such + as [VSCodeVim]; these extensions must therefore be disabled. +- If you're on Linux and your keybindings don't work as expected (for instance, + `swapescape` is not respected), take a look at the [VS Code guide for + troubleshooting Linux keybindings]( + https://github.com/Microsoft/vscode/wiki/Keybinding-Issues#troubleshoot-linux-keybindings). + TL;DR: adding `"keyboard.dispatch": "keyCode"` to your VS Code settings will + likely fix it. ## Contributing -### Plugins +### Bugs -Dance was designed to nicely interopate with other extensions: it does not override -the `type` command, and allows any extension to execute its commands. -It should therefore be possible to create other extensions that work with Dance. If -you'd like to add new features to Dance directly, please file an issue. +There are unfortunately still bugs lurking around. If you find one, please +ensure that it has not been reported yet and submit a [test](./test/README.md) +that does not pass and can be used to reliably reproduce the bug. -### Bugs and features +### Features -There are unfortunately still bugs lurking around features missing. If you'd like to -fix bugs or add features, please look at the [issues] and file one if no other issue -matches your request. This will ensure that no two people work on the same feature -at the same time, and will be a good place to ask for help in case you want -to tackle this yourself. +If you'd like to add or improve a feature, please make sure that no similar +feature has been requested in the [issues] and file a new issue for it. This +will ensure that no two people work on the same feature at the same time, and +will be a good place to ask for help in case you want to tackle this yourself. +Since some features are not general enough, it may be requested of you to make a +plugin that uses the Dance API or to simply use scripts in the meantime. -When contributing, please be mindful of the existing coding conventions and naming. +When contributing, please be mindful of the existing coding conventions and +naming. -Your PR will be rebased on top of `master` in order to keep a clean commit history. -Please avoid unnecessary commits (`git commit --amend` is your friend). +Your PR will be rebased on top of `master` in order to keep a clean commit +history. Please avoid unnecessary commits (`git commit --amend` is your friend). -### Tests - -We recently started adding tests to Dance. Most tests are in `test/suite/commands`, -as plain text files that are separated into several sections. - -Tests can be run and debugged in VS Code in the run menu, under "Run Extension Tests". - -#### Sections - -Each section has a name, which is any string that has no whitespace. - -Except for the first section (implicitly named `0` or `root`), each section -is associated with some transition that consists of several Dance commands to run. - -For instance, let's look at the following code: - -``` -... - -//== 0 > 1 -//= dance.select.line -... - -//== 1 > 2 -//= dance.select.line.extend -... - -//== 1 > 3 -//= dance.select.line -//= dance.select.line.extend -... -``` - -It defines three sections: - -- `1`, which is reached after executing `dance.select.line` from section `0`. -- `2`, which is reached after executing `dance.select.line.extend` from section `1`. -- `3`, which is reached after executing `dance.select.line` and then `dance.select.line.extend` from section `1`. - -As you can see, several sections can depend on the same parent section. Do note that -sections must be defined in order; that is, a section `a` cannot depend on a section `b` -if section `b` is defined after `a`. - -#### Section content - -Each section has content (the `...` in the example above). That content is plain text to which -one or more selections must be added using a `{...}` / `|{...}` syntax, where `...` is a number. - -`{0}` represents the anchor of the 1st selection, and `|{2}` represents the active position of the 3rd selection. - -Selections can be given in any order, but must be complete; that is, if a selection `3` is given, then the -selections `0`, `1`, and `2` must be defined at some point too. The anchor can be omitted, and will default to -the active position. - -#### Tests generation - -For each transition, a test will be generated making sure that executing the corresponding commands -will lead to some document with selections at some locations. - -Let's look at the following code: - -``` -{0}f|{0}oo - -//== 0 > 1 -//= dance.right -f{0}o|{0}o - -//== 1 > 2 -//= dance.delete.yank -f{0}o|{0} -``` - -The first generated test asserts that calling `dance.right` in the document `foo` where `f` is the main selection -leads to a document `foo` with the first `o` selected. - -The second generated test asserts that calling `dance.delete.yank` in the document `foo` where the first `o` is -the main selection leads to a document `fo` with `o` selected. - -#### Other features - -- Command test files ending with `.caret` will we run with `selectionBehavior == "caret"`. Otherwise, - `selectionBehavior == "character"` is used. -- Comments can be added by having lines start with "// ". -- When arguments must be passed to the command, JSON can be used, e.g. - ``` - //= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "selectToEnd"}]} - ``` -- When a command awaits a key press, it can be added **after** the command, e.g. - ``` - //= dance.select.to.included - //= type:c - ``` - -[commands]: ./commands +[api]: ./src/api +[commands]: ./src/commands [issues]: https://github.com/71/dance/issues [vim]: https://www.vim.org [kakoune]: https://github.com/mawww/kakoune diff --git a/assets/dance.afdesign b/assets/dance.afdesign new file mode 100644 index 0000000000000000000000000000000000000000..216b7363f133d6218907fab99fa71310d6f7dd77 GIT binary patch literal 50185 zcmeFXRa6{Jv<8X<2|=wxNGo1f?E;>55e8t8Jyq}AUFhqyZiK={O6p< z`*NS}Uei@IwWhnKwtjne!2!xrZ{SejTs%G0X%$^;44L8nqo4oVqy2CHKeKRfO=b4x z|Hf#r3jubid$@XG!FpPGdncwQ&KJ1%81LcRr71kM-kR%bBQn({Bo0yIzmodGm2ah> zaX!my(TpMH$CXmr<;xGpr8LK9&yZ|1T}SY^%ZQGj{K~~n`?h53(stW!DacVVcV!?= z#-(6tYXk1*}7!Ipo&ri$OL`qbZt zQKN>bEM_O_#(#?lB?iCdAV#Q$Zs0h^vz&W9suyY~TeJ94aYU>$CyCe0^H@*uWe$BU z=SPrv`|9j?Xvl36-=|}Z1GAE@e9fh0rOMQNSsGI)|1HrQ&R zP=kG|V*rllfkVyB$;0h2uIaw zO2zr>AcYKO?lh;TMnJCAbl|eM0f5#|-fvNVCIo1%Q33{0DeS7i-ArwK9~XKIh1{xR zJD_|=@#@_eQ1Nc3UoPTQ>JEZ=hd01pCz9Tc=svMHvQ_h%W={QZ%Dkv;h)a=wi!8B7 z>i{p3V)V1t`aW0!>s9ZROlT?gHm6yj0=+t!*7kdP%W36Mx)jFX!YRjyg5ww#9EK+O zG6Iu;vxXBLEqiS1Ml$x|Bc?|zL~VOsX25&E=Yty5(zak9P+;Gk#ZPMCJAn)v8A2tJ z*13ztTo>Pb$9|DuOWWJ4jsI5I@1clGo+_h#9p#7-XZF#?a2j-W7)SEpG(~&5QCmNF zL6WLLbQ}%cs3lZ6c>pac8>?xX4s?mGqV|4^;iCyMFaIo-qWDVe7h&-FfmU8;yn>~& zb`0TbLPi8kMhR7!&-%<3=78EkjYlvQ6Vp%%4ugj@mh6CAAypi-#;UWZ8VU-TbPs>u z)R&sh@>RCB@Ul-+aVJmw*)J#E!LM{S<}*aw7vp@IJ)!59U?L@pXol!7RDA!E_(#!M zySdBvEN*-eWphE;pZLP)*%Hga=`4Qqn(3E)Y{3+0{3L#P?@5BF9O#Pr-r_g+)$l9% zF@Y6#f`mX66h^rUnhD)*bg_Wz>NLL9g`AFx9KcyldwK=rvmA;8HKjC?FSSks=2RNa zb)nVRa>ieuL1gTG+9c1DtX~O!h=hpPcn!Qj;PRl916|^OQvt?&UYM%=C9<-uoi(<) zXTvDWyf*Xi$7TRP!|{Fl_n@AS)o+ghnQJ{^Ac?)1DeSEw$Hbj$2J5TW(?CtS^3o4{ zLSti|;c|vnP{%^UEg;?KDCT$%$`X3Vw*#egwSNQ>Tjp*6#C`4p+HO!%MiFfFM^Myu z`t37FDE27k!E#8IV1ugqXdXbE=15lxcU?`Pvt;X*yarL60;G4TY%f4mxBE77x4Y{q zSHj@=3urr)w>=$;GyUNZ5Nmv`WzU66dw?Z*A065oiy?D~U5_HnKuBfTvU1{6+3;=g zy)(*WfAOoVXA-{+lpCf1%8v&sEwh=njo!oInRf$X)>WbIEVC7hjb zfz<&Z*#3QH@EnK}HdY>ur`J0IQhDE{0(*Ht@J8I@N2rx zolTba68n=bPUy)T&V-SI#!-__#{+ov6WKi%NeAGrkh+CP`^5#0hd;G#0$jx`rT6b2VK@AM(d``0uuZ!?Kenf&jz z{_@xH&VvW(*t8fk)02G9YW+(`D%QUvKegR$s2D93gZbH+|^ z{jXkA%34Hzh5Hy%_*%KggNLoUN&e0v<`S^klEp+PYn?9ZEmu^<*5*2x>8DfJ>yIT< ztksX8R={D+4IRegYQ`_w=1ny|0VQua;BG60%DvTG3@b%BLZDQRp|%8@OO3)UC?T=N z5-%Wir>CH$VkQ%_R#|Jf0Sx`5OF;jAdO9h=9pHm)vWX8off~XV?>F)*hN^SHEZJ_T z!B)!NmzoCn{`s##KS~|)I#ZdGH0jWxf;2t>)+x{p4xx;T*t!y+MOg*8GvBHLJvt1z zKb7UtW%#cphlv(ol-k^er^^-*1*4L=Y(Djl=G>0fh0J{@dHcdLP~a6;ZOyG*9eKM^ zxB~_rhby&rfUHTz}P_vA2%8eCJ);t9QFMc3YK8P>QwcEKW z_=vsb#hC+e58?kVUWM?gMn8dYynG$q^@o}rm5WB8pHzmLfevup396w_J^iU>OC}9W zu{B#Fg=O8y-IRqn5NaKG1^~edMO4Xi_-s^RKCI*Iz2*I|ag(M7f`*s=revKD(FQr*KJ%(k39lt52Mj;V3lZR$=|9RQz0~ojc0g zq|VIws(t-aC%+sA@a1LMwckdmgI~$l*JKII?g%)o z#Kp7bZ?w&CWRQAT_RJnOw=O^9;tAn|zOnpeMCK5Xw!+wAec_(q=H3g=?8Xs`7zEnBqY95srK;@) zK#7P>E~>F!%;W6wI1-v1MV%T%<)ZQZQ@q)#EJ=7jGz)+2H&A;xJ2{=gU~26|Q(Lb+ zZt?7978E31sGP1;AQFZ`Rr;6e4Fg7qA2lyLeHDgsWzn(uAg*z(t|rwZR}T(N=>gD^ z{k5%#m!~BK;N=;e)!u@Of;i*Yv1Gr$*M65LU-MZb?=E3uFGIy_^o>li;wzmJfY900 zg}IXDTS)OHBA!0H<1if!l498R+AmC-V|^%go7m2aTnhL?wKOPLaMYocfAvMGD45J6 zb%1(-bMjw9xQ1^LaJox-dzbTOn7$z;`n40t*pq%3+(mPkkzxs9^+xsNayt z3#ao!sJt|2%i8iJj)+H$!bCLCCCDyq*}t zM7nvB;Ak{=G&~Jc4vT4-h=M6Zg)*?Z9KA=`V5-dxVGUWfHDKjU@ zK5A3ggC9GwW9BZkOSM>^yU%ry+W?DGAngg{19YqcyDyMg9t&Jw5=gSc07be&N|fTm zl_YkA7}$X<;k`j%Q{PPKE1L`6m_^zmb>^h&KU0VeEZ04iIK8;cDBO7}rCJmU^Ng`S z4so?8e3gPI4z$s_)I}4R&b`8VX%1c$yE)2AXLo8dMXtZ652lEwDeGL=$JH^5z2-^u zi2dBFE0nKp$^v5Hj8MpldXxmGJ@#RRQ~t2lf$}J(Jj%bl9?*PqtzvO_#WTWpsk(*Q zqRlv6^nvyQ?2sUfCsf|4c2g!=%OAX+Zj@KjN3R~CcJF=0{{)P7(JTRD-JHY^mZ9@M z*wN!DWCCX6u%s%$>>~iG>v3r8JXONsCNLvdOt?9^X>TAU?XUSA)ayXU z9YXEq!Zf4FD#+LSMu9CPmc}~k*9&NA*Gu_yW0zP93Sd8i;#1S9ov@Uass-uo-7IEg zu-PiLdt)uOWG3eas9?NarAnC2congkxP_ErF9AsW5F$$f(h?k$A2otOM7$rTTLCF< zA$F2bLBH}g1~q<#N387~-K>Y_`5$Hb5J6ul>t!s%v6wvK?U;UzvZA(W8Ass@W7lnf z>?heudFSx=Wgm+9)a$(lv5S`g`XYD$g8EWpM>U%#8>jqCi5R(TsQ!fz_cpqx{0WfVr@R|BL0T%aM3vsBx(2B$kH2l+gJ4;Atb=RyMgNf! z^yrHxKlgi|j#5( zsK+Bx?sGj+JtM4!RIhVAnf}^%@D0ZUSZzyWyr=bS-uWD3_0gsAgC0%bT3c9-F}}(Q zG7Z~xWY^;O)s$y9|F^R!7yr64wB1RM1UtbX47x#-K&SpApO_d;G_2HG5yl zRBmpDy=8(&QT@UO{*7c_nv1l9#HDT?P0Vx=kwOq#S>5wFk?))eQMvL5++!YVFRmmj z-Mu7oE@eB8Xx`B1E4kXuzWgW#U0kLxBtil)KI};L=0(dm_GW!Q3)}x_5GiZIreR6f zfGI_(NYj2$4lh-m1vYf0|D64gSakUomta(u5636lgb}1n0yim6ZA+cg%n!>bAW#wzaMx8B4uCwwr4!V^E$ygEGYs->Y%}?tS zVeO-Jk;JUjE=VNfxq7H?KcibtONK3$At1kmK|yQYQh?qW0Kck(L7jSi4Z>OlR96A2 zrSxJkz|ojVU5U(CWeP}U?Q_$8=D|qm0hCkLWC1mf;{k(RHkyAvp559A3jQ-I7ZPXH~sW)3IFErRS7bUN1h^mk`7pTE9H5Qt?8 z@;v{*roIBEex{m)Ich<3b_ViTfGm0&V+p{lxn_CfMP$!z2WZFzK5=LmP63e07~=fC zW~Q+pD!F_&mE55kDw`gSTod>{COBrGA{m$Fg`@&19aehR)74+?qvk zTlbbdM`q6(eZpXyLsv8uHMqD!`gly zKG_;mbF{S0N1?$4!VOsJ?>lvGq!si7g~=e_+XSd=Y| zQB}>C=dd+-5qoEf`AaaW`~-!9f0o)V(+;kEcCpV2vs+$YrfAGeQfC@KJnIo%)Rl9$ zi@nU9_x)MfMt)#eP%tl@r_S^laX6`&fzb1|c7EYYW=B)|ijvg~8Cry-q#Isu`TE3C zxlpBTMHNNNKseu4o@N;rRtA4df$T>VY#LeL^Zh)>6=E_8rzY?5dWz=dLs~qv(2z}{ z6Rs1kh-rSt$e%>$iIjX*n4f-aGkgvoUdnH!s9Z)&7@1cklW_5`b%^bFoy{Km>yL~7 z>OCC1!JpDHJ3cu(2Ia3fCUKRkHtHILq=PD)Lcwz`)MW2!;fdXz)m?e)R7&OS?MhB< zcDB#Bdj@`1dH|D+`cMGhi@=H)w2)f0&sFavM`{0;*)k)3yXs2PE0SUy&%J)gvJeUPcorG};YHrl#J9r6@GsbH4Ix=4DBmFJ#{b#%LH+M_*iz ziVI%={Izyw`_`#zP+B9{X!}PSl<(w!)3{6+oN=aLSwS>HlGR#`i@)K3g4-&{reI8b|;$=CqIo1Nb zgM)+Cks+xU?+kjWiEng8W?$ za!u&My#C@ueKr(~cuGlVB^ca9WBlpIigTB zIym#`AdiomWqj#4nqs{Jgs%5e07^{Z&OG3^8)AY0Ym}buwy7!gNo7Qzqx;-7k`9fH8Cn_2DC*^OGxK`V}AuD=hL9^WXCB&ZUB98c%>k zYk43C{RKi_z`WuK*z!38E$L(XKvEuWX7l}yN@Dar!0+y5hc^1-4+syh!O0zJhW=tE zzqC~-b_cOpN(Cq*1;z+aASTuwP*7RG2-^W*PW8cH1%OQdEys1AlGRHn9c-M7@z*`n zoB{DVNR`PEo^2A0?Hw0%4JCYYh1d97;StaA-t$j`Da9eeeBbqO;&Uh&jWDe)$-P^ zs!sA*;sK<&9-AxvT+>(8_X32Mk%Grr!Pn!$Nfyx78D%ZO3af@Q1_TXWIS? zjh=R+gR}^&E*HfZ7Yv0I7VVZOc*x1I3y?a>eu;9xVEDNQ3NJ&u@=IgOtyCt^ly~sJ zQG8cSn>Nykfd%EgNE?-?+_W*;u8W6M3*l45XHIesxn>7+|-jE1P}k z9Q-P3#HaAhOUo-%i%8)aR@asflRgKSjEV71K@6*aax0MH2GIUlC;f{z6mZtX#BFCx z^e=VbwmR@L>IHHL|3EjkOz9@KmHylSkT*^%P|Ao&4z~eFMnNcm6#+FGh!D&N(jwY! zuYl2yJSTfV%=q{L?A->sjiZD^)x-rb%>P?!kWr&)z$|bk5-@86#nn+MJ_ESlr$1x< z&cp!}w)$KgVgL!BdcK@sTd;gE44M=)t^&My08a@!6!^9?P}ZpcuPXveLFflvOl93( ztpH}y05F2)?aF z?A3l7DNWBDPXl}LNQ7zudmRdoV1*8cDJ*L+52s+niP_us#2eCn*=YJ!Jkp=hLG%ip z`y@ozYC4%4Us^Neo|T<@St)PWi)6(I_ni+FWBr`;+LK&G6f(Lt`#DNs&l5B#=SY;NkOJbxsQ?k~g=^VzRZjliAH;kCz28@;@`epZaO37+=@ zp(t2Hm1j}Ywfkc_wM1sQ33)~{FGv3U%`1J;am)S7X2G;+oXg2==Yn+j?)xX2iPg1AQMm}cW0 z;BbB&m4rnVc*KpYLaR66+jEp@*dL}Qh^9a#ouWhBj%X>@If19N zs@~txss?Jw{BOxT#_1^~!~SnG=`p|Vb7M_;4_3IF4K@CNG>H)v^ohC47*jCkzPs?| zv6d`Z6ehGpj=R>X=VpVysJZi>hjirH$IDCzph$&$6Jz_;r4cj@Xoy`^$>0G1CeLY{3q-1#M40hWu{pE zPf&M9zX=1Rkki4@?UP4g#GXZz)ZV$u>($5aXz$ELt``PZh>6X7ODz_#pkg3wgT3EV z%yfFpl0sM<rs_l-#gFICi_b!vk`Ait^1i1v%{3U8}X~V2Z@#q z)ZZj@$9H~4?BSwwE7M8m&9-mTKoa^{CN@K3y{%Q2gPO#dr*Ymlpiu`NL>Mw+?#)Ot z3WwItk=$tKezs3dUOkwfq~+7>P=qu2^!r(f*An3c62m;w9Ni)>0_JGnT&IYx_Ej8X z;&mpW_X`jaK=o9mFePE?YsZ|Sp4pjrRQ%vFiTM18ra9dqO_@YtV_1;yU@ZH7yMhmg z&+AX)>((Z*^F*wqPF~5a+@3h*W-;l#TGg6NIz;vCee2InB))s z8ox@Ax23v;mJgOzF9s^~5HF~GP@2yUdp&6V$M%yj+@%LOwTQ3=$E6~@i9yP*_zfTnHcu-f-nFz{RY2gyFk#-G_&>VJ>s9OJ{RBM?}JYw6v z9gpmJ9lIa=+Zb*Vg;y$`IxFLb{306eG6b!Q>nua!vK3?%d<0k` zLicfIFmgLwM(XhuD2pV!v;_lk-wb?*yGnQ%7yaKS4!0%QNNrGeslybPs&DI0Bn;(BuaQyL^F} zbgz+{W>BXw=Gd1~c?wBVK9QShBj9 ztjobaYVvk=x92qCjxj(foj=QS7t>N2j|p?$x68}d#kC2%x~cZ|#4bYWjrPIfdunRl zc6x@ePQp2-t{0j|Bjb(RPc}CosI;JD|aIY>$>tONyLE<5HokuQc`l{wzNY{O^X%|;k;9RWy zhwU!(`+IzP2m08I;euv%1Sa^HTp?NQSsHhAM%Gu4xz!6+$y7}vgcCctqani+r``E) zOxqiFPRf%yp()(^JEX7PKaElD5w+qrfG&l8UHCRgiG>R3XiiTkQr!BKhlUGhE;TL( z6Z1(trk2;mO2Dtd&z7%C;p(*lqc%jr;XgUjy(aCj?a*6KqNkiMgK<}nir?8T)t43K z$_082s_)-L_Jv&$Iite=+T?0UV*6<>^k#d~z(Vlc*SZ03vDRdjR8dX7)zht!_4rGm zAtB~bZRpdY-N-BRWw}AKjC3135c6p-`o@^U`!w0=!?|Pe>Wfch&1W}HV}H!Qr2oOU ziG{qg!i~W@XBG@imeG!mGcI6&AM2{Y63Pnv;lznEtIDp$()!bK3Rhf7SSD=(ze-2x z=!Jx+?Gw)QXO1fUbuL|I`TO>VqLGczAC<{tAp+kOv5EdDVIt6Ti7G^NLY4NJ1u!b& zsJW>~`b&1(mH3Ilwk9-&^cacO=>}JfYmS6-SzLZyoHZCZ24n9i?Q4CuSZ9T4Zhvv{ z$k*D1oIfY~Ab)&Kt-1I%a?-lJH8mPVG~u@!g9^)!8*17j(|o)_L~(3vEr#~emTR^z zrFEYXCct9>8`eUPAJwC>biJ@HZpjS$xN;gau6c@HN#VF@8vdrpTm3W`!#{*1-za#3 z+KtBlar2YpR4LXTPjiBpqZG|m+m+H9dFSGMt>7V9vppd`ubCDbruZdUkCuTaJCw%R zVj>g6?*-poKB@!whxSbuxz??V zjqRZPRuw190iuJ!)mp??7VClDddG3bpZ=2LL~7~KX&R85J7r2`HpGRB8No{zU%!GL z-rTem$1D}=arPNAcvLh0z@FvI*U!FyB{0|3rClA8r@=~iP%h);y{AKV7*-zn_AucVWt#JNv$x=CNY6qJL)Da}R5@R%Te@TQ>#W${(=RQPzq<95o+LSYgb02vsjMo5$kL}s z-=6D=JX7)to7vE&aP~443b1sAc}U1s3SK1)1Q(P2p{KxE)p=b;M2t&7V3S#ToYenK z?dZl{U2hj-!ONHF9fZ7~g55X@QMtr{ZReSJ{6W1`ffV_yi@)DJ-!0K%&4ohIn@1Yg zUEy~`j@}$jtt7FdhwXB-r9guzfr;?yse7M{NP)t4*>QGo_-6rW{c-p5p?V7r`jR;f zHFzUEGbIJzo6_4i7)KO@*bLpW-lfn9mA=(a%ptQ}aA%|A#;zRjXCVk13_4{2mKQ$x zwJsR`I~-E1Nm)n_c7B^@L6MoR!dYnL{qu8E86bQ#6ILl)C$;|X7Px;|(yl{qI#Xjr zUs#k2D>{`6$Qu8ci6|QW%9>MmA+3`+bWtvwlK|g-iL(tMD8KKJXjvWd!k|TKCjl5L zm>4LYCfh*xefU*3mY#pjN|hA;EGHo~zx4X#!M|a>p`>$2%obKX5j&Lp8OH{Ki@*n~ z&f`Oj$~kFI(hxAb_{+np6;?Yos;-skn=Q9c>XVT^(d-jnj!0${1{tsO^zLWilo$R& z8WMHo-AuqNA5NX6K7Zpcx{B2R$-9{uLtMI^77wrfqQgC82)+yF zZ`GGVd@SECH!DO%TMbv6*|i@-U47Ya%o0n;6!;KN>&a$op@r(o$C4OrRfW>I*5ds7 z)uAgh#T+N@cLd?)4M6z^*qOgC?<=@h1ry%XfhBDf||!TA*le2Xyvu}V41*w^na%& z|KCnk!vF8@c>n7W{`>m=f9D)0{%7!iCozY5mpBnnVRKZoi+w0?A6>0DK3aJFTT$E2 z+4A45+?jB2+uyZNqrY-p8kS{+MeHR`nV41IOjE9Ht z^Ft|%3lfqXzRrf}9+rb{*G5CmUbbhNrBv0Pp#1#0W0mm6!(`j8;Ko?{v1UB?5RujY z-~4~$!LCuiTP{9Vk{E^WW~c?(@Q&uFCbM-LxLy>!e^w|_IpB64q{dW4CWhY6y)8t+ zi{9VvPWC%Zb0I}Dk$CtbU31i;pR1C+2=*aE$O(WXZMXUmMatX;R8EW)P? ztu#A7^>Cpd4Z9mlz<5&xz9|CJg8!JlvPsV%vl%$LW3v7Fj<~$q+?s3{MVa|-9@NjZ zDww#ve&SAcW@2dA?kJG8W~9_s0`{2VLcfdO_MK|?K57=k=NU>z3?UDRX>eX??jmR) z=_B%PK|jn-l;Uq?Gg40~2sJe{%pX1!FHU|}>~(e!<)L~T2r3o*#@_mgD6$H+NXIaB zYXDK?%CLK90l41Y-u@uboy=AEalfFYD1q2&ydeI6pN>syj{jn-5y)23th1EJ)Ty|D z$r_c0#nP~(vT#Hm#RLJ@cYy1I$cFu~`ffEx5sSbpO3c5}`_Jdy$smSGZ3{~XR^u(W zB5=5Sp(qqYMrA&t1PP{H-Qrmjx50X44;Dm)p*k8J?cHY9*1rLrMhaeX9oyrEcJ#bc zGxbTyt<~{S+B8Dl+rSMSrM70U#}XHMTEcc~9r$wI`)J|BJOw86tU=740l&>ql-){Q z{`jJCQW6)&26Qzx?{Q`DVSrhZ7Y__oj~QRJVBA}}Xd zQ`}6c&2lOsR>ZbpDba$6LVP#f>iKgRw+*Bgk;{FNK18!oL%Zzi`0<`54Z$+tCLsR0 zj>0icm`3bf!_%9&iLkhJ-{>2E&J;MTgl*p!E_A)bZQntmL^IMzi!m6|z)#rXm0{uz79yH}1t5rw)S+3t0SpvEEb{T2&Srp?7}$_BL%2)w;G) zTksTwRGArO0M(Sk7sWS6zn^{}Sj7*(b}WS=S@eLJr{;E(Cl9FUS*o7xD9G6rrBGrT%%rN#AW-oon7HZcVEZgb z3>j)->`I8N9=@+{)_*uQ3{U~Kcfu%!X*w{cnsq?69|SN}R67g5t_`H;egAPDB#^j` zxGm(uUbJdN(bjg}uq_nqLKJC~B)m&x#=>Q@_0I;d#P2twf6vm6{!~y1l`9aNgkO)y zS#u)Pun$g=iXN`Rdhf;Iu$#AVz|}t27USJ8E_GIf2Rry4#SlmtrC{pW=}9(R1J;BV zgD?MB5LKJDxMaK1@!5p^_iWMgo@nIoH7~s+ZkZyQ2NeR|FUrcw2N)pq;$)9k1Toh} zOEHgKeWrqBcX`3{_M__CCP{MS^~#uaqJN4tN0ExaNia#7Fk2sSp<_KBAXs4r{}WFq zO0IXZ>Cv)K^5Dn0Z3nZXS;U2Ys|;@4NTH(u@Yl7w+{Xzr@Z#Q&W4YoLDBPaMo=~D* znx?oGh2P)vAqygxj0u}N@_^$yALMvp)PKC?Xy4D*C~BA?;w0g=os_N!v{~F1_i;8G zkwc8#Fz}N6Q;?VU;_~ zE38ari@5sl=S6ix!zJ#+%>riPOs{)ACx!QBVVYRu^+sSwl)=Cg793DgkE%OdTqt?3 zkA3Nie>!I9w~SUG*X#)gAr)^X=R6hNWF*2E=HCjr+ziJWeZn?`NhWA4m3`;&s3z@^ zxkTlFY{m95dVEw}N!WoX5>(&Vc&)a6*L~M@9ds73q4p#2azyYs3Am7eK36tOK5iP= zX75n)^X$6ElEXC+^8GY;Wj0p+3HhTu^ZIerP?B7~-Q2bo%J_kuFb<2Q&E4ydH~OG@ zclD)^g&oX!&XmKmnVhw#+>c4y`ntNhqc3+GphclXwYKY7qYdiqNE977@x0e-7+ynOGzC|AL{QdPlK zp5{JN+(F#rd!M`DB-JMJn*mAArB+$kY3QbJQ9C@0RQVh`17|F}Z=XCig6=TRU<@_$ z|NB(|8mFZyyi@Vp*mL3uw&HJ>z(H11_lFGIAATJVt+kCj0q>hb9Ju6)+wC?h9-~O(1n04X9v)cuGwS}w z&xX#<=kKskwbtamJv1eLps3b=FS$7uREn`Roc>OTB1BxOY*vk1>+|IQ!}{ll|q zf&09!4@TyRmUGibW|JlBHz}z}9sxWQ8BjLdyn1@}z0%(^(j~MD7L)2@7X7zYK6V0G zDKHg0Z7nJ)(sN#_-?0{4^ZHdG0li29&h9$*#Lh&IDjT|eMwv_Q`3HtXkzwNdNH z6(*E1o}jIC*R}SW*D%Ea3_h)S|Mui)`}>VD;HJVX=wc_$4C*)qf?l~6hnvSgG+Utb z{6rFyKO6rkJKZC1m6v~&su4egj^VKTdEh1EA`Ye{`wRY~y}jG}fwkkP@6cx8H8cV` zDMN|>ED}KP)mcZARAtB%ida3-^G_R=DvuZIwx`KOe7;{#e(Jtrgl=&{x8@;_6_Ce2 zu$}rrytfksO+lw^d#C;vac0L)ui_sBtWbK6=}AcVYUlTBDK{-{bl#CAvCLHUipGgY*+^W43Ckrau^h;1UA(W)eU&8y zK23j(d30KCJa|oL>ib7ce1}=hZwKZpB`*Ith<=zRbpU^gID-yuhRHvDCx^G9u>R-9 zy`9@AjOZ?H-hga~mYS~R$l&LzJrMM>Qv*{zND_~??av6OIw!b-yXTftN>2N9jk3o=3e;hG^=6 z`CeiI$j5-{*TJO7ndtL+z**2iRm-~nEqlxAWdQ_uf&jPwWJ&oHN_h`2!EZiZ5JC<| zTLAJ2dWSP+Qx@d(reI#KIfGl<{;U)fzi@H+S?RzwvgK6k#U?1J-ikO?PW`LTZt1|u zIM?=luIXWNLY&82-nD7&X#dK}N_3+Z7qQbZGw3Cx_3^L*vX@d5xRXLE=<>Y8J0*IB z{4RYc{bdLiO%7t>Q^Ma$ESUgotkYKMhG}rx?GlchB%-McHj(1Z6ZNk~6=6X68?)b> zmNm1R%HoHNQc!B0owZQClOkLHV?G&)_fqa&-GE7l4@N&1*-IMeAq_Y#pnTca3*HtC z-hSRr6gg!-TBusND){4e{W``*$8{-wh6m{=Bte<6+4QBpeye>h&v;~gMFNOd-U*HX_oKB9%( z&5vA{4RfmOCO%8n8=qcoenVc^UH!&Zf_L;fHJI@(%a2h z58mb}Y!V_$3 zkmyTG^NVmLX8TJ5D{DzTc~_cVt%SjbLqHL@9!8DUW2qQ~W`P$|z(Wr3V7qaV`04q| z#%d_NO9&Q=(Gz*#oW9nmz1g&^$~R?5Bc_{UEbd)8igq;Ow;V7lvGSh&8vR6=;ACLN z?OpSjW6i{soR<$(u?43I+g}Jh(JQ2;jF5>gARqCL>OJzpP{D%BYU{;;o>|b1oNM4V zDG&(5D-h_m1a$jTHjEJ+_DEJL%!9;mTE5pcWMOPKQ}KeQdKyAu&Ra=)T!`9Z7S{=w zpOritnz3y{bY4}ezvz5*aUy-H%J?Wg9oafmzFw2uB}GOFEmIFWqE z*)?Eg$~fp|9(bC99#5IxG`Dn}eC{}HA5(inMC*oZzEz53$Uel9KTO#1F;RM~cJMZ% z?tr_Qt?%RYZ9@un&lyX~_hHs zFwA<#jU{$H4S9L|4S{x^th9jche%xmk3I$OeR4S?upEDNP9GA3Yi6j0mejf(ylV5? zHRD1rQXqMZ#sr_cx!V0V4;x0}i7lpV>TB~CtCQ_+U9`@5LZ_CZoc2JRB<3Y|k#`|V z!)yV#oUEmf`^H;EXl#@>>)k<5v%3>nQ_ts763?u_?#6Rg5@?q<$B)DLXL^h!tX8nu z7dw>3KAT}7dG2z}!f9GDH9K7SBB!}zXMW`E8i;Fpq$znc8$F9bj=Af^=Fx^QOd~&8 z2XSX}H!k?@OOakz{gjrLR-`q=qrk$OHx$G58vNG^ zU>WQ&)@*g;g&1enf;8g51p-GA-b(fo#2D4cZxNHxvE>GI345y9wmVd{{!6PpsHVd3 zPMu)do?X-D%16*P>4uQQ)9newbH%wu%==`y31qC)rX=7rE|tg$uR@lhYP~UI5IsEK zM=F)!kR|KdXtIFJanJTR>|==e-sXF)(}Hc0BRJd}yj_@YbL>f`;H?UlNp5PA2s+?j zu2idJFScEfCl806H~I8%ImQVn$Q z869{yai%gC{oII7vaa{xc`(S#nxB}RbW$;_RY8r*)G$ptUPL}pn?S!Nx;7*jWA4wi zPT0C&iW)g_2Wune@1WK`)CSz0dhDd=b)R=Xod)b;2(G$~rgdKzwSewEf$qdKUt3{H ze}};v7Kc%EHu;nr`9|GWqzM{qSxv+7{K{Ik@(d!8*6G+I_0;h{coualS9rd#A%^d4 zbCZDtipR})yY!o>JWEqkQ+G}GD(gj9j>4V{Zc($}7vY{l9(JpBaX;uumvg$d6VG~3jQIxIL^w~t$JcyC8&L-BTjW4)L1MrSJ73Lw%E52%4x7AbH^ z5uzyP)G+mmP8Vl-vmzHhcAdzx)vA!O~ zTix~bJimj%Q;%=Q(s*2ZtWs%tZN$DxD2EO!Q)+aRwD^)e5&pU}m?NY0#9y;As$5HL z2n`z94SUq~l(m+tb=h4U;%@!>OT4L!^CV4+o6_0;``Ls8rYmiwpjP?>66JRIlfIv z{uS;alpiV|J~4!!U35&gDQx3yAi=Y3a`&gT%v~E3*9sOw#?p*EMza1P5?mN0NO~TD zK?1BG83KV__jpctB8OuzZiohc@=UC%L1U!7oo?jFk)j!Im%6-9nPkydBZCAvGTM}` z=*tv$XJXJ!=f@0;1=C^$aLF##J3@Dm04RTa;n~sJDLrsu>gfyuM6*`6ZV>|4YQVMN z`c3sGv7l-xD@=rc2-zBSQF>ziVO>eAqVXNHlk+L@z#)~r=k`2+EzWz3e7kAN7m-TF zghsu+B4~zMI*KvacDs6Z#h`IrX>AxDc!qwFm)8h2;4d2c;AK!Z6a zlxPhLWeR36Y*O>EI4pjdZjL$cl?Rcfz)AA+W<@A`TS4L^It~Lb^RE}Qx7Hd52g557R&K%JecvIU?{GY zjBfF+>HlEgti%9^E+M1@C$208yON~K6^y?fw5nuxN|IZygp8Np%%TNEZAydH# zsZhqt|A)PA|7Y_5n^^C3CRMw;Z9Go_Htmdvsov&flE zjyWW3nVASVREm%xIhW&o`FtPu@An^Y`-z_}*LA($ulMWqJY8*W!j_Ma0npPn-4k$h zKFA$QxQp_faIj7s+S`|$hp&9E$F}QH46yJ9Yp4v0ZAz~cXV3aXEj03T?knVRR-{I_ z5ICj{F8A_PwkeUVRmDvz8XfoYVEJ(j79N}H&J-1wt-p2px=4xk0|wE*D=m|LB&j_g zt7;iA2R#g=a(18gp|mipPvuozY0qrvyI&WP4T_h_g2=y0S9`9Z45flB%n}vLnU@gf zOCz-mq3$Hi&{}=t)xNLKok|)xr!eSy{Wf-)5$&RO4Rc9AFLcZ$>hDB+@4=+d44@)h zbrfhy^ruIFGIKLPY_nKo3AI^%ADWCrTaGhC3mX4nQd;G13IC8wFNgd`2-Q*zhQHsN zi5ms$-Yy4qK9y`i!;c5eD%eS#{IK%7xV&kdF~74tzvG#%>D#paP{i#00pR-YZqBrY z0>~fJHnBbz)ak7@v&Ol~m}hsq7xcl^3?vBzlD;Z463$DO+h(V!cypY%V!Yvxa+owK z5h`oZS%7<=Tfg#?80HaRyj7peEA}_79DmqNwu}meu$1QZr?CW@_Pee=&C6CAI^8XJ zHp)&o-B`F-{$%BmS*-z2ZA^u{&5F`(rw*^Gssyfa_G|`r5zgAvStxUx*AtoizBzTp z3lnXmjZEe1sM|8yk7`N%Zc8r*ZgstZ2DGbG%7Y7hKVyfo z^|Is1mR!lW?>j@lH22YqKPi;p2DFX^uiqG>DqpKAm7gAyP|t-<+|3c14G-3ZlM6KQ z_0&^_wBs!Pm0K8_vF)^aY?#6;Iru|>UaD6=v5^Nkc`gbld0Hyssg>&Zpz$;RPQLtR z-|qL=r}CL)p~*hesajGdU*1mER{S%|s0Q+4G4OWu6E(az=tHBxx(uEjROe#h>#|xr z2_^Wd_$_!Eog-)r#V@k_RwDULJwLMoxw)ehPJvy+eY%7*j^>#`Mn5n-F1ukmiiFS< zk0oKv2Odo0NAOsQyPxfw!!VWpMR&S0P(v|#n^TtTXvFu2BEgnCl6Ck|D z;k#}~FfJMt$&4)BUS6#-0x6A-*!of~VXPS?u`rG@Iy1>Z@2fTd71>Yj@AQaEYne2e zuQhb8oE4B99l}-9$cyQ%YMUL0Ub{DZZ>^9kGyWLnZxI7V~=)PmWs+bZ#KEksii;X1tlG^Cftc91BY*m1G(h&9H>}?Z@NjB7ni$gD2 z#pU!m4u%U}!!GmOGp1uAZy~!luBnXSL$(N>S#Z993A#8^NlB@A7&C=Oc9wK=>Mk=v zj|aK*DYPvWa81Zb4Z(j#dKbJRVq)Uq770a%zNk z=LzNKf~iMr@I|m+;`CI@Ud=x?vN@3BeaR~aQqkvU|F0HcrKO-2bDZJjk|aChS@JAN z#4x3^voiptg6D<5&!{l=(qZ;}+i5^XtR;m3dPG-#W+mkP%gzxpekVY5Um9+kN zy-sp?$a!yQbSUW|>@Me2;hZ5WrmA@T6Z=L1j9o#OLAq~Q4egmoXb&EOxR0M$;V`v` z=rh|zvRs*gd!VlIsPp%1M%N%tR>q-`3KK3Mw6rV~q@NZm6d`EQ0@+t}ckEu3>VVTr zpAr^C42O>Ht!rdbrpf)NEju5@tuc)NN<||js=$t}5N! zA^IgsfqxwpCgjYuzCY|tPn1}a9~X>C#$%Nh+ZpVO7RfJ+g=rjL=3R7P=`efQW6|MA zIx^NT9EC0lyf~!pEN#_|e~JiUTX=GhOUl}ooso-DY;>9mj^^&Av`b!hIQS5AKE}#K z!S~DSTOU*2h^kMb-T~;I+d&EvOBH4&9FmQ)l^#*VY%{f28&5UclVz$q}Q4Dfk zK?@y~_VmHTSthyZfj;)zoP~REIy#t20WZUj=CUKc$=*#3i^>%`l>dYJF($ycs{_dp z?Z4*k`xG}_lp)bC*0KidiF9DOL|uCJj;U+B+y5=-E%Y28KOJ{aKgw1%M%Z#@{SM_$ z*$67XUI-%VihFej&d1F-87S|lJ$5gsZZE4mRk>iaUnvyKM#$%Jw%lC;hr=Lzw$dzB z_yaA8WrZ8YdYvK|+5d7Y}1m!IZ|`!GH~5%~QQQoHWnn zmxT_aHZKdZ!U}?{JtRilx8AR}t*a5yf4TFUT&xprmX=W-y)|>|i|Njhk!LH{I#7(e zpJ%i@1NN|>w?#Gx=r*R`6gShQY3*zH&N#s1>2ENeHR3R4IqD=BfFyJ2ENLC=^sHbi zhLUqB9j+nqNa?lU|DXL9naEUw3VvRqM&3~SMYM8A5Xl@X>bLWIQ)?fiZWoUvS-Iq9mdUxOsrLQ#y&lXFwbSK5>#fSd1b)KB?7<-w%+j@{AsydxCbqb1a@)V0fMr%z5d8ApOhDbLvSH# z&s$_R_y<(nz@4;ySS0vDME-*o>1l@uM>1x$NnwtvTW@1nrNbw`-GiMvbRk6whg zjK~M%eVFm4L-40VM}6dJ6imHZB&vD+%4NbYa#`(LqpACPO|}N;XlC-xq2$EX@u&#z zQvpH@&gF+IdILuGFP+P;E4`A_KH|p_cbL48K`t6@&ab*0)pXS8#Db9 zq7d=m45h_yqZhy}IR7z3z5z)5}yh!to6cl$uix zt>K71D4z6)e&~GXzw&6vN+R=qBnjQhv;q4MNb~xvG;gQ(yJe%K;elzl-v2;<;hjSo zk-Fl)mRJS4TAw3L9i0+ydUGjBPiMz|9>7ZOMX%5KMsFajL($R)cFT3e0az64H5O~_ z9!YrJ69T8DfRA8tHarS4t_p%ZH<(+_dttiSUoXw~9Aj;-b2U8TqPmq)Cf+|c&@K%V}WQcsEy#d3YZGnxe8R%rn2Q)EoX2sWS4zh?{ z&!k^nK>jJ3%c>LUgd4r~jPfumRqU?52Vh*wRppU$Z-DiCtZ7}zMF`dxj`XFwG$D==8=&&AtGB?vTthVKcv!@*Ly}+6D))*bs_^6i^KZLU_Yu;X; z@&S-=BmJ#ubD93x9QD!tK&dhVH5Ma}h+UsJ_wBspD%ya4G&n%)>yXP#8&VtpuI~X} zh!5`VUBWmSm?{7b zQ`zuo?YFUepuKM7Scb{ay=R&#pXsJ#7Q3K;0wxxibXw5$Y1{N;Kim4y90yAxp7*9# zU}h|>Qc!RP{uH?=ez=nE#&n)`dOZF<<9M*Xn(RwK#`=O{oPTbvN|6~L2J=O9R(&dFM(Fq(5t)3CVnvo3ao4*>?pDrqQYZ~S4EW`tx3K7q4!K|2RGLNmxAF> zBbb_>yO7qHuwj3Wv)-3j;qa0Wi3x-AD~#9@Z36AdR-gQ#?hdJbt}L=h>>^|KQ0Jb4 z(EC0EO#t1|K6($pAh$Pf#1sC4b_Wkv-t^WiUGggK~$)N8%5bV(hIq5~TaM@XL*%`>co*tnZ^Opi})91uqaGn%-(W8ofT6VM{EA!xn-q5bH;lwqF z^_bpXMa3k>;5%ZR96o_QFQ1)B%CfF0di5iRucdc}<65bY@nHmX<>H_zh`WmQ-Hc;U zcrGtjKjfoFgJxWkFjip9-snsJ=!aJZThn5@i@Bt3ui9k#e|c@jZC! ztJU@`luCFW4oaG%K`0Cw5dO4`JFP7vOg!D z37$o7DL;S$@)h{@`{P*xkjj$AK@|y){!wTVaU#51RB4nKJ!c1v-KkI+=@!%-aX~Ff zj$ns`R%7i@TtLtBOtpD)5w3aXbBM}Zh5E*S5)Tlry5iY8m+y|UzieMW@*k@1B}Kwy zTM2iVr6hmB8nRm-kI9jBQmYUsL#%%VG!hma0>2jypuVWp0rXBIW<33({J=|%s8qV%3maR9)n zLsQvLKaM}FFxh9^95yRC`iI%EOYP!!r6n?_B>$^IX^jOZD`6K_!b_ZH+M^ZUygj|n z`R+RElnhbo-fnLy&}YzHd;a;T8UVFs>%$}bZaDR*nY{VrKYKk$IhGTc7+JaoQga^L zUVl7q@|NCh`RmYE($g-vezD*(dySZ`fC1Y{kMDJEQ?GxG($TC%pda1Y+8Ev0e)WMX zS@};eKqj3by@ktCz?RsneZ{C54Y-6U$yPC4`DLxMZ$t6MZ`eUN$FIjwHkO&x7^NBD z4I6hPeNPJiY0)0#j=i{Yv|Hq3rM9o~7SnHZ{@gdjdxAgke$16B>O?m>A<)2t+^oS* zxSNKVLzsUzCEX*Lh=&_#DHg^GcFsZlkBuOHZAznn6k|f+{QurCh*qyEZ6{ou7&Vj| z7vv6J!3UWREH1+4;$do~(EB_&9ch`!%Z)vCuH&)Il^^^=7?Guio^|+Oq(9W*ebmvc z+(G%2>F$hIoV}&r#xwuub>2{W6^D3TB_+B=!)f?Z`SK^D`x>Y8pIaK&jU_5=e|!-y z9ro_?UNmTTOW#t~@}?(MknacOf)#kK-T=r|G9p?Dh!p$MN zF8&)@(p#7Dp1N`h|Bd<`Ev%^|8HAbAmGj3ZRjzT%`;w6aj6@`-rLyw~*^qrUzs%lSO3P>RrGDc~$n~E7V3a^O8=& z0iLsQl+6B4tVhpF@5>#onEufDi^$5^hH<}50=l^OS7|^2yRt+$eVjL+Gx2-OUa`?S z|9;Z#{868pFRKraMSSCHrXEqxzYmD(MhnQdZoFp9QE(A<25dJxtchW(yh+WdP2wK3 zT>C!%srXz#5W)VWW6hA(n7YQQVlRd$gtu1T*UcnO=R*)z5RVqZv_BeDKWaXaFl9X5 zIc!t!HaZw}@Z;swc71wX$Ffx1iyI-|AF2L%C)V`aT5&K`J>Z;9%e^rKfWVs{0pO{h=x(Lh5d7x6<@ z=I^cHG7*>O~O)-tVpMt(J@*ywib@YGiaNf zg}3v(doqlrfs{x$OxSuV29>5Vm|fopNVZ=^@vO47HxiY~w5}XcGu-79Us#fRVhVn0 zvb~T?_LQnl0&wcVG z=UMqJZ$!jJOKik~79}v57ckzvDeg&nx=lF@2Xha&KdAz~pFA zc0N}!sVxMvBNP<#3?*qN4L2+HRg^1uApH40mSK{B{#XL0UyhfaB)KXz@sCyHXcCmUVH*ffp{|QtLCudE|dZXB!QunjNtKe@; z70!U)BW zC^|ivPC@?0m91%o@@~uzR~3Jb+L&uTXV;Uqe-{o#O8e0Zr|$)~x|GJ^YZC`&qE;kV zQD;Av{=(0}oCkxkuN^$H^Dqt${VuakN-4dxfMRKi4<-n9c@UN#qF>=MJ8>~&+*R*u zVgyQVz$i$_VKf~6FI%eyCK?p2blGxaVB=oPuzVi5dE-rUICsim=kLc`r_hIX&m+v} zb>vF;XBXpZ^L>Vn$$|kOd+P8VoC$b;;LW>!ck<|{dz)N_Y>7UvEkiN5@*j8<@6$;e zm$AQ>TT_rd;*qRK==kv&B|b5hBCnWXfRbhxtqe8FnabtKjWEL%e&49ha6FhEy^x|# zBsC1ica>8D>}u})`dqg;1VBm6=S0t8kNx+5teh4}y5k{kyE81UVq99FjxVC|oxpeq z1P<_H)6)28Alu2$9z%SX3l2K7b0NyKq&PAdvnu{?HI1Q#bJ3vqMNjU(;(QyiB5O{M zI57Xxh*q+UrT(L9gcEdffK2>Bf2+J&8eGVy81%Tr!7`~`ngvw6$$F42hjBc z_Bn>JmKp(tS`{Fve72)(^Hu!qF3bcn!yX!3Fb(!i#|e`J6eQdS41ELBr=KE}*)d9b z_R-`!L-s(&%<6Oo(10HQ z^@^i;CguhgNfYML>mPT*KIUJ)IdWaZ{DhV9ccwg^P!VCSc-|zs=+Sj#vvD-G0zMuu zFZ)F{9&Axk3=^%=zb%WEU=WZRahyT5R`N7u#R(z9p<8$i9kngfd~O#&v(ePClaEKB zOG~cU8gA)T!>;6!7fCyRNOF;XJ6)o*eFoFsKIbcqK~!vRcJD+;pmKWLSaBCeC37y& zKx?U%fW|uGsbQSrdeTPIT6lBK?AONd7gw#ojw5E066xeih-Eif_ z-d@X(!d=VAcw58~>bATHi@{o$0Y7s*HlI9XHUGj*e0u|@vC)-crd_ymC9UJT$)0?& z<&S{vJdWy>pq9dWUg*R?$l8{x0Q_6ps&$;Q69gS@>;e^;_;%gRi|i!qe@?r^W!7P> zK08TlCWRg7ffm_lX)*$r7(lhV1+}xHgPNRP6Qam15Q9Fu7KjR`w@qGm0F8<&dDeAH z!{tdyw|zcOYD0P$5dmRM3AR?;)WDkV@}N0{^U|JivB>ka$Kgh&q|r?KX_V{*Le$`R z5dA1QMQVQN4(5Qp6HYMgj(D;nnk$IKmN<|lHbgMrf@coCEpRH{85BFOX|IEGO7c8= zPkd758X>Y!yC{^i9v^8H$?~U`4cRMB-4%~}m1=25z5O9=*Ob;O2;8Xs*#XS78S;8L zL*5xor=WWkS-H9cGUQkm!{toLM7Us@ zI=B$kd`9D`xv;e;T-`w(B%y*i32S@y7T*X$+Q`3|_32$$sd{kvbPF$bmlriYg?S>pE?@S>{pLRD8%1aU=v<9UphKzi?wHljrsAE~G+l)~@rg zHLA-0$X(Mkm8<8T@)QK}Q&nxnmD&gNWc3-gKTNm zxZAOSc275sqJ3Ku%Y^EZU@w*W%VUJIU}5+SfY}^HSFn+s48A;m$rR29bp#dNr_(f~ zKk4DYNRy@$*#A873M^}SS`O*4H$ZBGmG-A|&|bc0upz7)BWNV^6Ob2213Vz}z^Ox?S*QID2L9Mki5$b+#AAEiv!P>2y@7#=i2Loy=qE_!cc+Ja+`Y|YAqF{7RR5Rs2 z6CYmzsDg96P20h`H1{1+%)zFtzuMp4(cuW@c#$Ban1f1yt+J*&PG17;Eef`_<}k!d zzCcAjXtV59;KPbfu>lq5HAT{}%bUa2Ow*_B?~Vn0paW{k#$volk;HUkcKsuy~Pwx*ZYh1+1aydqCt!rsiJ^3L5Tr$awB_>IxRC(2Gii= zF$GOY#T*>0t(E~B7Qw2-yL4$@Eb)+G3l(LN91h90=dV1Y z{*>9I<_W+KNLxMhY5Nivw%=i7;WLKlIgfb1 zOq|L_I26Z{M{!&rzFm#5oUzFGzyOYM#Wt6$w`efBt1Cjd^p0 z{;C9)6C!l?X7#W;CVVpPX>7oO%r6un07dVk8I|If$QNN3FDp#o6b4N}I(x7}qGyVH zK6m$4h^C%#MfpQVVPWFe6=K_SFJSD$1S=DVkFQ)tI#3?2ybwJFXQ*?2Z$T15HJda7E}el@?ZzggKM zOFo}BqMZ|Y+OR&_Wjzft{UH*&^!l#f|J4H6>e*@3zzf~ke(w98hkd$R zie(xnukkw`09?}D3VCD$?GMIjEYL1@t0m?J1{A?@i|=LcGiTe^_U!uYMF2$xkUjvv z2%tCr|Ih!&r!fHWI>5O<5oECVa;r>-LMH=nU3?e=_|6n=u+xk`JC%?l{aJp{DypCP zR%0$Ra>WE(M25{48W3N!6!64XlQzzYvnxDDYaRK?Z(i+?PL6)M^QDF-5{4kVnPT1G6zUSE3jjlHd`9KrH)c9rwv#fk_YgOBuA=0#h!JL63Nw%FTyLb-f6 zA+p%VEqzd$c>Q*Xg>AoZx1EUwuaI5gPc<$1G%UtC+*yDSUvY4B+;!oz@vsWWN25#~ z+a`9~5;t&8ia$>vIcP)X2AbcM{pUmsl`qL~izqOzCfN7;yBz$z*Xx<7-8OgKv_@V; zOcSTdww32ginL=E@i?nWm)n+afx}9~Fh$!yPtV(Lu5@AP%0dNsn`x`s0@3>__=_5UTpp;WPg7rDK<#yX<2C5Zu25l6~hkuMF(l6 zxjD66yohcQRQh;JSe7$SiVKixJJKIa9k=jL9jE-3aoJcC}2>$wS{633gY)l*`H zt_Fg_WHIDNTUYAXr%*(TPE^5!gEnLZeNc|w#S9a+Has-J!B)P8<`1PpLHCv1M~D6r zeF_&6)|Cb!EnDYD`j+mUQGj4b4GW!tAen@z1fZ zGi{!Yz-}c3IZY8WMKWkZ4c)8sfcl)W8r)#T_gj{s0UySJnM3l1h5fP~QTF;{n1jP= zj51Dff-nbfjT5#d45+0)!APbN!KN-aPhma_RH5H&%F{eSA3z9Q{bS|zkce}o1{Gp+ zmNKW#C`f-w#RT=$Dr7Pkcx^Filf8a~w92FOYXDaFvfyV{D%vm)Gw6uAl`j&j98&A$ zxn+S@F>$K_c?Di|H$dH-^iY#+J?%j`;5I%O-tmtPKQLCGb27XJHVa(st4r7k%OF83 zA8HpO5~fZ}d=qC}HTcD%TLoO`xxFmnx$&Vaj&l*eYw518rT=`ce0K%gynf7AcSapr zb@QA@tvz#{Dtq5Ql`jQ+b@GGj52+R8<+tl^mr(_?djq6lL+lQVsfP&ph5YNbl z_*z)LL-jut8v$=6m|X?k6&3MFuetjN9jUKvyrLD}2b~eX`#VK?Y@x@qkUvTwoyWgz zwl3v2u~vDt9z7wQ$rjW&DEzqFVmRa?4vzsFQ}2oM9pfB6pYl#9hD!7N-YIxLv39)L z@v6fcMPtu7h4B+JW2XyYlC@og$BESy*YjLJ4-4Px!#6p9-2u9$lbMsJs(nJ4k4Xd! zJpue9m=V)gP*}I1mXhR(H|Ci8C?Ly9SgGdP`(*Q9*GpKh{J z5oh(HMF7k%^LA?H+u)x2MpLnU_W&mza2E==Q-vEda{6m7YcJf|)~Odgw7>aLs<{wB zU2bpfpiET^%-Mo}6btVs6T;LOKZQq7=!fNbr7Poq3YKz# z5R2Tf2GRR9%(8wJ^&4G>bOSWT>PYqS{8#(C*&a;kpd_EMO~WZfr)$N62lc^+-4hKs zRmSAUAGrP?!1|7HE7~PwJ|%Yd0#2rXOF^Hryd*KjQRAdv6;bGLs8!13kJI z7!F^h`gIVVDvl;fhYPx63)GDhN1A^NG*^7u`&OT+y$}Kgbh6fE4U6nWK(lwN>?Xy~ zNVepC#xu%-?V^*A-MK`+O($@&yvtURJ4uL27*x%>Ox~E^{?cp$m(kmbo^cvgobsBC zpfB}*?3SlZ$a_re#x#`WJ?irKeP&PP5z|b{Q`H2$>A$rkFpe^C2dSI3Zhf%sQR1v4 zChaTxkBxI>f+Yf2Z`PrKy1yfGGO`3O%}Vl01)UyNu>>8-<;=YsOu}j2t4aQk*ZQp{ zyoYc%(n>5>or4o2Ej2A+tw5xcqEQA=M^Ewm-=B*AY%X~|rDYe^Jl%Nc>j&G!Ch5}N z0n?khakm_+V__IZ0u4LM0_tahd(w|nCeqczy%DH)ssQe(HQo<7SrLtKd;^`&5+<19@@Q6*b=i5nz zfLY!sYVvd679dUD?IPxZrXPA5-Qyfw6G4vMNYfSdJ53dShg7X*otDA=@NOPM2NW6L zPGK?By&fpY0BgINT})^CK<=fYEGEP`;>G@@30l?QPLF^qh~y&O+p5)%(5V>Bu{utM z^onNcG$?bxb_eCM+QDNjI0LaC0|*S~<*w0p8A>TJt$y#ZsK#)QYsfRlRerkv^SmWJ z0{huQmGF8@|BOKtiJ6CyPuuGXbBIp+h04zjy3vm!Uc+a3-K=`NVx4~laokvjQfCpM znZ=T(k=9po*hSm!8?9opDGsv#SBUlVGGpAsL0dx3A;$fe704zmfRJYS{C<3XsU$~>iyQX{sbv~hb<6@>ew zWp}RtuglU{M>(_cR}eq}U=fBHwIOuLoV5FH%(E(YOkW{kBDfw;Lt1rqlUmD~?){d_Z_sK>XH>`cGxny<|j> z#Aq5O;n{Kf=;R?B_P=ul0Lz|`#(8hZ3vr0lRl{*{v9dpgdbslH7rzn>?U!RmQ2N@3 z*{S7$y zGqW;@?mIf;VI~#uFHFufDN+H`e?81}r+{ByIRz2y0E+F1`rX36CI0Qd^4#|>8+^7p z8*@DDbcZ6}Xy?r}&x5kk_#hi=&NA{c7m`bXe*VM@$NuYn@)f}sqB^mk4(PsIzxm3j z#mnbbQv-xI*csq~W_vm5&YMXkfyEnx;_B!Qfr6G`4Cm$Uqlcj#)&MuEsY<6r!Q!7r35Himu3)NPBHrDuV#Ww}GDGnW6x=U$DlXf< zEiB_#QESzwo45aWlIM{(>(4md<@t%u6+Oatyw5mvBLaST#BqQ48XN)Dwi zS+Fvbz^YhhJ2HW+-`fwx&JPsGz_EC)D0yh)CP|$OGl$B?_`_Kh{h-*)nc4YgsXDV2 ztxU-~wQlj&ZKnHn#dfmQmg}H)`;ej&%ctYQevHFh{BYRm-*>)UvC>oi(Upfs7oo3qW?j8T5#sZf?^Mpmw5^ z?l{8Ie%WVXjQi@4nP%G4FT>6B|9Sv7#{x=gq2~RYA%< zRqxXVS%2+O9X8jkL>B=6U$lxiW1E2BEnT@I!0|w{k%&!M9ubQRUQ|;T63~S zJv=#cC(HCQIo7w>dddxhnK-DkXZLy?r{5xZ=zmih2VYhF9DC4R1`LMpSCQjA{`!oc zM`;?fnmQC@xxCYK?Ntvp!4(Uks#_UCC6+lCJ$hdI8&`;+GsXuQ%#;3CB zn{M~|M{msl`JuWpsH*C-neyiCRT@cucny%x0PD#JnFWPm;$C6ohjJ5y1-H=$EgOZ8 zwxqxPtutd}a&O|bYc?b5lg6Orwqw{gI&=~HiAjY&MHsbKtUod?kR`@SEJ5)(1kd2F zgPX1_IRAkS+N9j7*vz*&ckS=B9_yY=-8UHn>rNg*K!%f4P59RO$}zpQy6!K{ud@+e zyM@T!oBYMurSrQ|_|QA+PFq0zBzfcI*rivwi&hJi?P%~2qrtN(MP=sw(RB@>NXx`CsM$juFYw5 zPcy(jq)@Di#t30bSQ%#~_A@m4w00xdMu4sLBYx^&t10n3zvJEj*T>2FPS$-*mLbpU zPHuTYoUZs?#N%b~H=MQ-)~l8=VTEXv3?`$JZW&4RcXtTPN>jg3v|#k=`ifL$PJzoN zE=hRRDaoK(WCE5+$LUFD|9(qHtt^Q4P~nf4NEjje9R}LL_5ng}DAd*?&fu59Sc2@Y zFhIh!`QhDBz%Astn-+8vhJKY>2=y*vjjwS|B}_Ph_d~lg_)`mA7l(2Qh7Z9aXPWu3 zhn&==PV-3PUj&p!M1)yBo1grjq;ZYKg1#~kFS!Qk{Olo1qGY<#LO4Ys0ah9ClAx`` znl$@dgY9~*W2U>%#*aZ`VY`Us(1Ubj(2;XZ4uJ)r-zCbrTtctrh#V$@?!x(L15ir6 z&t}x>yuaI01<3djJ%^M*Azn)V^y4sDJ;Q3&A5WNxLcp^NC&d+t>uAd@i|VwPp~KkU z3*n!CNm&_o-xz8PUz;-z>rIjVdBRJ`8#wk6vPB@z>Yx2#)X!IMC!^!=$R|ZU)mJ@} zCE2dGMop^|z2U8y$QDz58Y|@WX=6|!7NxjacmB?Ti?Y>BL3Ri+XlOfuZJl!im!9gA zSy_^4Uy~Y&+DaUc-^n$Fl;gH5{Q%{|ZlYM!yTfw83w@njN=D%sKkJ>6-0Xz9dhObFq5Mx{YUqj&!xmOwGCkiB8A*(aUBUb!<2T_VREgKNkx$L2~=pyym)q@p(q>4-Scuhl=U=PT#I z%)tnziahYA@+Eh;O^7QFbGSPA3a9b=bA_P~N%pJ9yJd-Q{IA|6q*b;Hp5GX8jlOJZ zSRz7Vr6Y$Xj%CysNd@)c{@ZPUB!5k34*|Z%DqdN?a)_Xxp9i{GLc)9BAgB3R9IT86wN1@#VXtvq!;wjgd+rdo2RFbeI$lM82EDxodg zp|HA?Ox1k6HgniQ`%@%m2p{>*-%Gb&e3jGkr;riky|1tI;Rk6 z;ag4W;B}m>sHghPU#v<;$js_iBA!fVuVjORgQXoKyD}UmL)=G-vN;_sh&ZAU#ch0; zm!jCpgby5!mT@u%c2&3sAJ%erSFyd?eL#5?k+WJRr{J;$grr_ks|7`Yn6QEZ$&CzT z4At*ldv=nKl49Gux1TSPRmxx|Fyx!ak(#~qB@U1Aw}q)bBSYziC}YV($F3oVs42zk zMKDOgu~0^)X)B zfwqA7weJQvlW8_+OWp-}n+_q0>BMdPhKEn6Ym0N9V1mHxzmH*#sPMp-YELmsd>ctq zjMTkvw^zhj_8Ppp1d)Lhl&iMq)PX4ssnW`r8XoDd8@DkA%x#r-MgzL_YIK*V-3^Gj zmsCMuCK57nro3}0$R%7k@1=Oc`d~V;3ss$Xe2tZ(z}ho|>GARPh#!9|D15WX70~i2 zD)T+S*#96#x~nQDMkY4f#R#RabJSkF5KT7P8baX1)cIxE>4^P)GVDZ*gU)zC zc90=jB&ZcOs5)@0?=nJ4-UI=(23$cw2~7Fed^c(3*Ylh)8Ls+v`gjU5e-{!p_(5`QztNmp{WQnQ@H`50Isoulex~^ z>td^kE}*M-?M-k*QiIZwKNGaGz-pk2V}5yXm4wpZG$@dy8Wcu|!e03hQU#F_?SRql zuPo@oXf%T5qr5C?mc0vjdho1HGcWCVIbI?Pqc)=<86B+qG5P+KBLbRp!{b8vYNVta0GJuK5wz4!)JYNz53q-^C9NS;H6=-7aAe@g@Ke7D;+!tzo~c{ zJs-&mMiF~;C>QJ4hdTr*FrKN(YD)h_=YCq47w&h4=fMLbR)8Wju2vO3a0E2>DS*ZX z07|a9B)pG=$e=gY8fYItmBGe%^LIl-sazEKzR#7$c(=61-;Y!q<^T-ow^B^k&0YQR z4O3ndrxqSt1jKqNJu)~bMw73ub+?uli_Ft>DsV9Ppk=L>o7E3|;;SK_Ij%(RcP|Tk z8)Dp!7iL6b492H;5=*lhG;NJy24< zDona-0wNtFM~%A9{qEQO4_uG0J>U`SWXF!zf^As;h3rn1WQsP|o& zo4yfiO1qVN!&PIvum*3Ah6CngqZThj@K6UirEJ6CLe1@(7X_cVAXTSLf| zj5>oae`o_b71dvQGkz09xJo^r5YwY8nFZ@*@7(UouQb*c_{>eVQqQ)tA!h2dU(>h~ z-}5PRqnn)iSS;r2Yh)Ve8ERkeA5UT&3*>HNd!OjK| zlE9n{Rj{tGDKD*esjxTQD385qVrboNn2G|`=-vOJlaiXc;+rQYDY(rV#y|Rcjhj`e z=l;S^227;2aOS}Jku#Jpplen^Rdb+Xep|ViEJt1Zs{c#$(Z6rb1KQ)SA(`hf8T2LD z-m|KmJYB1K1h9$UVdjENa}8spzOrGoH|a2guu=~?{;09D6sA*O@n;ubz3aeeb{rVE zeFCWKzMcnCoY7aJ@ifIBlFel=71H~u%1Arxn9~PkJ*=te%5oujy0% z*#D0Ou!dRZ<#X9`3BTEP&INtYNCjkEagU{qXpK%9>iW7L3*vw;$2*-eJ<#@o>fvu| z>8iu;p8d10ol#i{rAif-^K}kK5gRj`#H@X|80Qo8{XjTyRSaP_v(jlhfE`x5{td-o zt0KN*K<2IiFNkF|du_vfvQ#rz#w|1^;&NguFz#C#WSA2d=WdS7;Wj}*;;l~t$nu?- zB_<;@D9`8hIv$6`u~lG}a9=K_*I3&@=}XM;hQrxW)JTaCHWjE}3B|kBoq~sVr*;{l z7O%{$yO-_C93Dc-UNCIc{c3t@)<CTYK77>WZ&Zkqz{<6%xK zYn^lii_@!7`rWON-3&~NEq;P54{sjn!2NNrDV2e{=6|$H(Ue~9zxg%!ur|6u-E1c} zTcPydbZ_9xX1FgzDcK9f18Hh+*9$@tpt2`3op0^U-3dh{cdwT)#*Bm zNn^UmCu%QkWYj`(C^sCX^bliBEtR9X%2n%TsZt7;L9mt(v(VZGTAu^WR?*A?Fx|fG z)F}vF>k%EKcSzOu6YCV5th;)pvbX!8{a$8&ZeN7eS8FYH>*(B@&jKvTJU>BC_YH0P z-=z1yJF>a=FF4su2L^EWySY@R>#q3^;Lbb+ePFJ->P-noxuW|_B(^_cYleazbkhYN z+_KVG1(1iY%OuRfPosZZc|4MKrKT%VO?Rkn9)u#Ra1H~S;v}0st3N+3(WmNO+tz2Q6>tjkq z$dKv7xBbinE^(S$+<9GPw7&HmzP+ck+gdiJA(~-AcpN71bN?%NvXYJWz|Og+MS%GG zX2sU^YnMKIeWRF0mK7Tn0mp_NJL?qcA!tI_XOzW?z-H{1{e*L4=X$xOEWuStTdB|M z*LXm${7|vtWlrv@#xQu5uVEI(25yr9 zmMqtF&J^hRw&AKFj$wcM;9k<*)GTj6LZ7$jPrA4b@NhtCl3pR@&96@{E~lfx^}Ki( z=q9ox*4^=oEw*8)?$Y5MGPEmbKR~q2p8w~~b_WfP?S0Ad-0YflLn|}U#B{fAw_ftx z785y-__VA3cT?XC?)f;SiM+S|56_mZsILXdtz>mfIQq0P*nP*#Qo^wN-1Of9C$hZ? z@GYQV5`UL4pZ2eW=*fK%MgZw(C>o1+B(h_4>j#}I{B+{q49y%L$br4P@`i`n@H^$F zcjIUrD^?_he|%grIN@ub4!+H(w$fjBShlJnd%Wkwjq#yKX!8kV=#?B)n>nOA_<{rj zh?)9g@*?Dq6oX#&KCEywh|5dPE%={!fQ;y*b&#Ha<7z-kwhmT5d*WPd+I#p2P{W^! z7QX9EL@u^W-SoYAv*DKs$gI#S3x%=Xo^f+BNZuW(gZDo)On_!g3wk{HcV!+{L;jaz zrD2~j{L7!KzZnNbnh{9jWyI2Sx8Wpzvis`({nQfHZxu5kqOm}a{l3Tcc-GHgn_c?v zLFr2Wc@BD_a^;FF|4(qX#KsmewCGZI?a-0sn?mfG@X2XJ`93bG=N8T@40R zp1Yk&XOxRv+!dI5!+dCgwyCc_iwbASCQgQ-@zgd825M)cFP~=P-Zfa;jskrIvluSE z3Jz5|0b7KwqZ@$5Pg zY)KpBdfeVDl@C$_imq6JsVy?{gbVASSm$5Z=@2+SXFue7@Ef)_M*J2D0szz|Q>G5< zTRolix<#ej<)7PF9nxKW|0rZE6c7boqj2?AVq~{N=odZBu;C^y$}ck{;rwgG&ecD3T|58Ulej(8d2%X8 zQJzvE*ZbA=xj$-En$3##(_ppaFNj zR)gME&CFhpc-a({wQU)&-THrILi_-!G`YXY21M zx>p(z&AN4~n3!IC?Ru-&{Sip%`FPfMshG`KJyxLtZa+8kdj6YBMnnFSL4@pxo?f?? z0M86J7qVS2njyEvy}rD3yZJ0`c8qQ(b8KY<9hxcE-F^))E3!O_!9+#6#tHvK>g-@S>kEce(*&_$We`;&q7OxfwPDv|js{Z8cWV1)LL+q-d4E zx>hOsxPD%$b>K7Yq3Z&3bZ+`-rk=bxfD-0{7j8r~m}=#SuS1|>pkep6A`b?`@K zF$aoVy{sM@XQo{Rf89cNa%5`p>r;j-%lG_e9aI=%Nz0D`L~4uLC8b!kRosznivQS! z@BN!i70iqLMK!qSqdn9QfP1}eHzd8=R+Hu4%ahY)%N|IRXM}n*ks0mJQR}bw`Ex}5 zzkx9R{3yyhDhDxzANE)I$4gkPozsc6z2%0_em+$Ot3EfoHgK-Db}|S2njmYEz2DE$ zSQ6ZqR!E(iU4R+Uervq^)oEYwCcq$V`bNnx_8kP!`KHE~ZZgq7QW2Zwb+?*&6~N(t z3!`&#BhLfY0Qy?neU;ur6~Ico{q5R@x#yb%kVjFSSFf@Smd!)*1@Ce{EjT*S@7>IN z=eV%f6Am=Odlw_vt|kmR4wA21%L;{W0o{776xM-iFWd9YU{BU}U%I(f=dynFRvdHe z8xhE9+4UW1t~84e6dZTTUdgJL0QklMc)79Goz_>#0eSsg_!yW(ax4_`&~dWT$PeJ8 z`Tins$NO|Y#Zm*J<=p(b-Htg`je%`+Lv(xGG0CuVaFBKOx?ySXfB=pqm9f7$VcWPZ z`k;<{Ff4^%Uj_d2U3DE^mXa9Lb2az|@T*@=tOBeE`s4wTC)vK5E3xxWKxP%qU?fbO zG0}Hi29;o3)VKMmTUymr56wL8d-V(F5HrIGyM6J&MdYYoK`oK&)PKIP|CJq^{Nz19E0Wyrr*1qfn+wqKD z=iz5{!Tp$!ehEVMTTmTT{^_$>j5Ek$vbcr$ea{TaP`_sF;MP`!JGlSGqbw<}GVb7^ ztQ!yA03(TavRB4gk%66&9O3PzOUA+`Nz4#XCrDucB z8uY{@^d3Yrsrc$p&R9lKGycs>4+ntdgEjh%EEv!LKD7ZG3FN4cl=FjH?20d~- zcChCcl56t@xlFD_A)@p(HEY}^p8)u!SHfnh`_j^q-c{}}Uzp>y^lbV%78~RVXadMb zgekv>7{2l7ndaNERPi^EH2WDX;%S_$I~6~El@aQD+?`NNu}wqKPsD8XIZ`yuQMFS( zv=koLcyW-fn_)VUP)+k{$b1OjAek=yYZu;`ADrd4n%?>K^j}P8zz+(EJ%ejB(@mE5 zf;}ozXrh3PS6fumFG`V6XG0y@39gKjNist}@2*_*_*y;aa){^&n8;pGE_#-1JtDNy zs@iNpN>^2-UEFo|cLMV(A*G_Na6Y$#0QA#~J&vzDZj;yCjqsDWY&X)Ud1O; zejy%S5zP*Z&ALT9QQ&Ug%FQu~^>5)IQCt|$#HlxrxHQi}?U$b=DsKm*wejQ}MAg4E zZ|TTen3d4{^r&Jv`8lZ8-k9RDMtRUBB;~RUgJlxhI9F= znjE2+Vy8LdS2P0X1Kk4j^p>+M%VLoO%Cthhged+QO^sFU)F<(2&1xZ&rtJm{_xUo& zPSot5@~} zCOnVshKi%oaImyyULW)h8#RPnbMjtZ?1wdO)iL@ieRbd*oXCNtr0^!q!iwf3oF3SY&VTb;*%{`6kQ6VUzk?Mm+ae(aBcJ z$~oYrc99xGFLbqaKX@-}$?g7Ki0?WP6?K`=$5A-H<|yh#iq9BIWtK03&c=_LBf7it zPjOA!;|RI{^I<<6-O@xgOh^PC=To8`KFvey$uf)Ln`9T@H){c;|2vMVELd1rg1*6) zJ~DF!(paWiyDF~O7KOZnrq+2Ja+B#ObW8KDY2IF6(hv9fp=xw@j-;{sJ?MQpU}rn+ zqvKgG$0fBef1i?FhmHtNQQ2|aLPwo08*rR+b#)~lat+A(BmftS)LJo`{SzfoI^CDZ z7e@kbHOJQML!4QsQtI5>8q35sU)*mD4s|55B{fp!?Ve@qU(eugG_K;=jpc@qc&)dO zmm`5+^lo=l-z6DyIx9@l!tayF?S;w%fqU$1l+7JYIA)#>Uo&@V2D~O`T}K6_Y+Y*+ zfhrdw^g^5TZ;#i@tMyvtoC8M2VaRsg6$j^;mZJXcnT8(y8uQEbRzOKU>zs_YSG!lm zzCT%=7lcM|p95M(K)~`F3h21=kaL-eu+yJY$N6}SyFNR#*^n2NFs?A^oyJw#{gE}& zWWMjp8_LZ7Ig%*MF|NqRSphLPp5Xk$rGU1h`Kl<&v?Ad$7`W^b%m8`vlN!@+?hzP} z;=JWLktgK%jzfr^*^i>DjuU3Bu$l7?)P;lnc8erl?z}oi5TyVsSfbO zp5Sn3X+Rpk69b4v%6G-nxRYAld9U#ypX*XKL+FHqFTmRAz&AOiU}0t#Jc$f%nEt-S4G+=pL#ZU;&#VDU)tpFJ!XLY2{bfJTj#Ep0hPM`ae2u0@Qro% z#QiOsMPwY>5j@i8ozK)Ts3q#AAY;Az{6~oA1Blnk*UN3LKAI}B{Ye^8;nR%J(9mr4 zRKd&9IEnew8ub)@t9#MR-h_3`W>E0_Q8i}?Q+o)fNi@yOsoI$L1VFh_dPE{g>lun` z3mx{VY=^};`8;J>_Wxd_f`fvB{A_HNO#pKulMZ)4px#-&^v+$s>}MAn?&2W(Vj51{=t3wr9FL4F~=}8IQ*o7g=U9gw^k^OdbE;k#&Aq#r3%XIjN`i8RO@E z{Zk9ET^aF$gM@c?Ves?Q6^*O46)-ki`SZ5?6B;;FTK(MXWztP}y(vNj+X-1Cc0UaE z^Xue*9=$|Lk#t+Doe{e~>14AvL-{Gxj83g}$0zM}O7)~a#>qjj|I2&Vw!w=G9_tRu^g)vCk9zkQC2%zc|FAvG}oin-_ zp+jyB3yw=h9)&tg_G;@Tndi%F=8~-ONyRnED+ftFz3GSEt=LKOuM#_`!@p)C<`z}t zqT$CR704aTEcQ5nj^A+4XT+Dvy>wBJ$Shv-H+>j0h$~L4R(xQ0PNOfl{mJ*%yCy8S zj^ddxZueO5EprT>1{Kl4ncZI3B%1ysp3ZQxTQ5~9T$8DO9o160@JO5L*%31eP-?Z9 zM}d-$MlX;3E8)MkQOjmwe_Jq*IdUH$daQ0MQgV6{k;Z=J{87H;OC7AK<=G!IdsJ5% z)Hv5DKz*^}`>JDgIj_Zhvf!l+3tU3SqH$zL*~j%l#!QxYBhHFq%kr?4qREH+;@to___rG*v9`K`HkZMzwr@A_z&*+2Uxm|q!6 z_4YAhab`(Jo68_o2@&M!44qPXI+(C#Dg{an>4jWheYj0_{56iooq-T5m0qjL`|I8Zs0QMKI@@l{?QWH>hq zktImfIR+~(x>q`cPqUkLFrrP!Z(U>fa%gq(rEa7J6a1*;yA-uIQ(RIiY^29w%4X2d zWAN;d1VPc_FtYr<{fPx6Cq&&OdwsS{XNC2rj$NqF{M9!2e*E~MahU|VT|(5tJfvW<D&;xzS{3vq-=jhMhk@;&+g6iNhc2)Y- zDr4@YIaAuNCSy9V=MUQ)2beHu;X|-m_GH7Lu3LGfwaETiW#k_@8JUar&F@t1xBk*9 zq%J{G$F}pLvgIcmQsTcHUGuXy4Q$r61uCKfX^W=1Z^Cc4t^YX>uC<4t%05nUVtQjY z*DICE)*v#PS+ZM?4DHmf#UM4u0ZATV^(3%>W2wyHBMFWJE9x_g+6U|+L|QjIA?O$0 z2fB80P+#4l#; zd_Mcr&>yZtTX}BP>;)=p9{8K*SSgu1?^WS7ERTu7hYbpBib&b19P3k`X>%M*@9ph< zr2>Z$j-fs03iQ`XcnRA=Og)h}r=@GF`q*^Cd0YaM5?6?AYJXDcQF6YU&Bs@uL+((>=^okU_=~CM zN&QILGIO>+1-GSq3u<) zV1;3V&A=`H7JeiXSS5t@HUUx=?jhL|W*Hx}rDK>2X83XxbfdL%)#=NA6f=`&_9GB$ zj@3z){9N@^&Pivj%UsKD8(`~lGJ2X+@Q@H)-{F>umPaU1Njx`hftAhiMoc0Ym9skb z*4m05<_3Df$%?tSl^-BWG*5*WUqGg^`$obO27f=Zr$r?|Mer-7rz$q=I?9~rc1G7@S;BXqNAi50@P;(NxIFl-dI3UPlS z$S98yl6KL|cXHLJH1!M;`?jNViT+(Tc;`Q^k}i zo#f~~jl5tZRRG77$NypArtg$a#l&ZIp>|y70$^@=0yvk4E&SsP{#{2j1tcExrlsgt z#9ughYBZFc<>)mfXmNkQ8J*7ez;6)za~48lFm*G|^kd0r?}DZ(eoQb;kaSe}NTc$Y zM_!+}+d&4pxkY*A_IOFLmKl!^fR|s10bIcsfic^47ofQ*ekn|*7_UpnaPZle_|}da zQ>H5a|6FEj@-4d zKrC-Es(y_)PIG+My)w$&o4nT#6?3P(u~qCC{qpTUB!_GpIdmpR#7@f`rp`%{8Va09UrUG2pQea>N7~n0^`)KjXOqt2ey*VcVco5P3BehR5DWAT zhfV$sEDeCwMKu6MNS%Q=z-)F2kYEH+}EjUJB5NKnA&`=E4ayg@|R;@%0BaZ?j@>?&7&S$*UCY2rg<@h zM@o@;s*BuVzli+)u+n{5|Jk=C*z{s%_8wACqR2>5b$Ef);|E8v(RLpb1CjYEU;$X8 znL5SP@@WGa6+W=D>U zjI5;ym>hox9)XikQc_x~rB;lmK4q{>4U2u&LLGpxr2V}q+MeRvXA%k9g zStOySGI{Mkk2YaNvCVe0*wdN1#vmMrJGPMBEJIQVVXfac>>G51YhI}wL~q0{Z~5~) zJ{9TV;Na7*JY6h*U9lqxaB5i#_MgrXi2x-<4jT%FFsqypopt>DK}ALFXKqsSL6uff zteI%ZitpfiY>Zf-{~+TLne5|5kd)1K!NyH#aeu8p@npsm=7ZKa2b-Z8rSq!AxcXaY zYF>=7L6r$4B*!#>Qm5(Wk+x82BiwE%XU@Ft%`b^vz;yB2m*6?}=%XhPOj?+2M2F*M zVsfx?;%crYGS+Xi@0*w}zaN1nu=yj{{SDgJO6`BR<|^;uUFj>0*2-5ODO=M+EK5V0 zTw$J72?2u!Nu+Eb|8bvJLw#z>@~7j4+WHbE-d7iAnL>bfcjqa=B@_V^an2RqpA)S~ zf#`BC$EvaWXNoF#IGDETi+JNmB)Q!fPpq+L+;IS`$SzjaNM9TIxC~^tOuBJ$Yx>w} z_778n{l$@-Yny}rtOiA|Y2KCURNA1Eayp%-+&w}mJm2Q0Jr8vGE!qGRj80Ke(Peq} z=V;-3gcyTh=aX;|;#EFsV`{0kqYzeWO?6(T8jpo1*-xazk=G518n$Pmn^Zt?#149W zb@d#?^IWZ#Jr6fnA>1hcB8}xdKfU|ZD2@BGu01Zmg97LTjBzM}X)p1~oXX~GOWB0x+l?E|=!`$}5ON0B3<@~cA#HnNb8@-0TiV|`?M_&qcoX_ocIG!D>T%XYZ zTJ<%lLKgEfua(ilq=Zdsr{UgBHl6sqNy2QIHg6XZ+90(q1()aDg*F*moHKms9;_$` zW+%3zK$xwVpQEI4-L3Y_+q<_6WfPU)LpS_G!d#@?x!VkAQ?FE(YR)Zd1aJZ5O*wM+ z%~PIFVK+A3))=EUert!rVA_|jO>Q;_nw45DSM?dl|>v;lcBk|5=G*>r$7{4lG$~+se<*nV+Bkv#@Y|;5B7-@{`~oeR3bj3N!Sh5(UF439HO61Ww5RW-RK8QS6cV z0RnLq`p`h&x$%XFt|Jbs8t3tlLZQth0lO9drN!hE&m)}Dhzu_c<B`ti=`xIH@ioeOsG3zJbA&@F#JT!aGR*Jjj6Po zZerK|tP%p;dyBSBbw?)OO`*rxp*^K&50Bk|c@(xe-q@&NNm(}vs=1%DgTL_pd^GHz z4NQlHgiOEgjZ9P+gj|L*xtz)qdYuMmG&l1IH8#Q<68!9-&6;uOneT?%EA5aP_?^no z!~8z(fZ?dc6LKC0rsRZTN|{SorgvalW%!hZsJG^<(s;c(th+Gfu06`weL!p~&~Ye7 zwlf3(m0zp_(#DwM@5J}Nwor{?35_s^5d(oBEQHisuG!YU4eTvH+O@d|q88{gAzDMd z^}^MY%^Lc7%+3z`-I*90Yubfw=N!I|-}>g8=@qr?I~BU7JT3PqXymIn zk@|T4%r*NVmtM&Lw&IgBD9$|hJ+4vv{-bYn0fO6(snP@ekABQKpe&cGG5feWIIOz> zxwX5y(BQt(d+i4dj{bC7{Rl5v9m>rGFbC$%yB`yY|JOHHg>&e|MW<3Gj2^?(z9yj` zLP+edzNP#-#&1AZVCOJVlO_Zrw)g=q*!gdj`MRIK@_^L=_@VwC#$0a2DJ|`{e7&a1 z)^DkD zEwF(VKW~O@MTmKGAGsVIbk~%PD5rl!Zz#k+N3uWHik!ft9A?p*%r~(>00KgyH8q@F z3$lW|o^n84E#?I5oQ+}@u8wz~&1s{%o&&QQPOxvO6OSE5*45dp)@+mL%Qem)Z~Oda z?zvXau)1>esT-LXp>AHJNe@Q%y_{E zssvz)#&3d-TVGmAbDYOq0=lPn zRD+Z|N$vIa*3(#+miv{dy=*LHXMl`r>HV_uU<`|1>vRLaPBWlA6EbV@JORAB{TD=r zWDb)7!+G2Dl~fBkR&iz0x8%U)yG@J2^!(f&XnSca32ubpB4ep|OB!ysXr4R#EzLQ3 zr8NFjXrcy_&^z7XkDrK~AJ|1j45H>r6FLk{0`{Y-6j`NmfCtMde50gzmvEnqKYqF-$~lm7H2sh=4kw6!(9vq8fOStllr zyaK#)ok?JuUvl?Qmq50gQpb1hHG}@@uSMjZQORD@&ek(S^RpzJ7!Y$2s)s>9UmmrE=GIX(YNiz zbj&sxFgj0CnR4u5{Ql;>jCI1^>M-n;a~b4*4!g=ovurU7*_N^&Wn7Td4Y+208I5Po zHeWbQJiqKmSTX-B)*CYPt+O*^3h45nR3wG`BYd)aPgJU{3gV3b zwkkWOo~6BSzNv=u=nWUP_v*>vbPMw+3-lVzu7G*rD&1Y0?fxeQhlNB2zl9|v_^@LA zo=Z&@v*hDfPJt&h0XHg}1)#Ej9e6Zh5OPLXO2Fw-%Q7Q35p)wcICsIHxm#QrVF3%U z#+jeoMsqsL8_98=2gT3w-^Qb#51;E~u>OvjRk$<9$H6lg6jI0g=oa5vY zVq@E$iHjd&3B_Gmrc2949igB+189<5PPtU>r(b}v(U-XQnoKdL;mk#~RT?(qjT*0> zjtb9Cwm{R#g+94ZWNVILG(w$4qDQ?TW*tdRA0-6E+G0P-(m|eiYpnLCH~0WKmnyL2 z3x;jJ`$+~CK}XsIkr@M4fSgt?_c4)^n1zUg!}ll%+0ukT@VD1E1?Z6&S8H9Xj?^V@ zwIg3F6>YR%ajvm%*2sm=Mvv~lP+d0l(-T@RQiW^D^~&T4k6tQJ{>_KR99NpP9y4{~ zCu%xsefR&-10beD3}BP%K4m9=Zp58L1@<5uO0Zb-+42&}U zfQxO&JOGPIs5EcK@6rpJeswgqBV|{mAPmNVvhKeZy0@5Ef=u-?`G9v9-JGaME*psO z;Vq#{R^2#x(KOLBIRsJH*48d<$QD;<0WupM|A2rJ7Dw>J&E7R2s8vaOZ=b>~EEWz^ z&Aew4YFakSUHJhxf!*QifOVrbT3F$dQYHRvj_<#yw-Sd>4u6zt(I`2tNbs=zuGwJR zcs&LJ8NaeP>C0SbcE?^lo_|4t)Etok#<=y4Q{#Dmw4}$U0klU7z=po`1eQxa z2bd*yKUPnX`DWSmA&!uvz^XJzA?1WGDmE@|g~T@?Gk);K4I`o+5e@5|ZYw z8;?FR)4aQv*S8-VD;B)oDxsRdu~;u3-CebRvicR(&)tIBy5$srsDt8F!Z99c8Av6{ z2&@5Zdw?V7a9aA=We5$TQ;SDdkfW0dQ3Y9J(b%UMB@F+ots1&}V{iy7)!i1L>1iGr z&T(09QNvk_$B5&MiCggteTw!ho+jS0G6hw+ISO8t@h}r(3s;$=k~OYEr2F7EAaU|u z%gR;bkxlWqDvrFs{QaAEEv2G}7N!6O3|W99gmzu<>{s#Z>t;ixaB^gG=>Y2?_7f(d@^>kiJ#Z*ykHCCMX~->0-E1& z>xEGJ{f!uWlE*v`N2c=fFXSjNhe1YOyyN({ASEe@Zj@uUx%a=Sm!C7tP{jE5d8KT5 zSZs(uHVgcw1KFVtuiVWITFo1xUMV`ok20baT2`}8ijl$Ty8hy87nPA0?*Hvw%M{@* zzJLEdSnC1&gAtTOZu=a50c~abiZizz$+6yJP9MB@ozAq1AL@wTgz%utBJqWoV|)xQ z&9XUCMw)u$T3K0qHA^B2ZA&upBgNB2ga6JY4t813rx~Xm9Ls31GSZd({yLEJm!;6dT-2(79XMRlB02P)hk>MUEi?$A+NTPfps_L3hgtQ3^ zG&Qc$AA}^tpua}OmLQK;vO?H7w2P5B{SbK`4tYY_LYWMFDH1@7&@5iu-XE6PEc7{4 zp)A;}205})fX$$uI_FURg8Q9h(C0ml7heSOCG2rLa#V|Y2H`P3~gmwujrAWc8b{37hstvu6t7v_*4O%;Tx(XH&y15B*9t zm$fGADDLUdpNS))<5$%*V&=b>25 zS|}v5S7L?^n6sEn7GKDMMik|6W~oQ<0SDQ~4UyXq5CZkr0)Bu6t%=8=iLy;aq?0C( zGd05C$(8v;h{xLCN`>cLb`iP`($c<}j-c}wgJ-_f5H00+bsm;fk1JVfP|m4IqAueg zPbA`0G~WD8JCG3(6QQCnv~pC2}rdu-H9CC$vRv<`R1{)_^}tTmR-Ua7VK+BXp7p!t^XTyy#AbN}02UP*6EupXz!&-de*E9A c!L5ss{wGtPb+z~&5CN~3P%Y(ZMavKW2V_r^@Bjb+ literal 0 HcmV?d00001 diff --git a/assets/dance.png b/assets/dance.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca6bb3fca603160afc69f9b6033e7fad99534af GIT binary patch literal 14536 zcmch;cQ~Bg);Nq1(R)b*y^KonX{aXAs@!We^da5IuTyq9z2Rw?vJI-We@= z@BD7hbKY~_^PTtozQ4Yo>vGL~&)R$Ky~$H2gOWuvXCo>CMm@AVx%*w_= zf_bN*nVHGPQi2&GsLG@2BnPv$dExB>d*!XBZQ*TeA!^AiC5bQY2?h|@!(7dnJniiq z5MWOU=D+ZQf#=(=xtW>%8scgz!K|mM!6fJC0%H>766E4xmc(Zgcd>+nwd9}wV>0k2 z!EEj7>ICNI_VDoF^5Exkbg|;*6%`fb=HcV!<9h~-c!uzDa5eLM=73lwIl;KKUHP~P!OYLY18Cx{mi&cK|9^)4 zQySpEcBtxT32c!6uWA1Ry_A#FaB+m&*a0wvmZB`vO9eS0UQr>TXM9|L69b4249vBG zd&!%*0;BkN_;{c32tMQC)8^v=^9zCbctAXSV4lA>^w((M1X-H7n*D!`zdZ*`;+7U* zR~uJ5*k5mdp9>i~*T3KXezmi?Jq%zwGY2aPX3uAqFu0kUoh!4XyqSXqOq~1n4tV*erGz;E=FtVPgI>ln zjDW2TvH^79KV*-A!4~Tl+n{_$hBTyEJ}4(P)Id)UJ8bgt;+;1_)MQa_3?7q3PCm9` zkY(sLeiQoH6vP%P)P#RU@P(=rM@AF7lA@H#ULXzel1ZkIv-)7m(c|5We#JX2C4+Lt z0M{o4IG2*EvYtZ|yWvD2dy(}Bwt6sK8lmQJ6GKJv-hjnuvER0dEi`N$U++P1H>FN5 zo;u!O>9Y0*k6^KQOG|}Qqw&u_aDsPKB@7mYKTxn<^Xe1ebhS=$Y^rEGe+f^>O{7jt zT2d<5S2~kiQQoZV^VKk~^`rq~UY_RC46E=&C&@gA9WR=NBK0*{{Iim!6m?ywQglX# z^jKx$=p8fS) zP=vOMF-5;R+zMX-V|PC>GJ`^Eh)Z?mtcQxlaWKs$)@ zd)Y*1#dPCR0Z`E$y=xpxl+|+0+d2qvrQtcf&_`fkaIxI}V|wOExnp23VZ4-=(e_N+ z^0Tji>LjdyPt>p;z4v*Io5YVJp~Y$iG?sRn7v|CR*^GdX!I_Qg6S8^ zxA*V__+H6HS+|g-xYMs;es+*$E@bT}&mcd0oz>b^^OExM+}p=HDU8!p85gL*$sIbT z$8|muI<>Qi;|ZUwuO|{4h|KC3edgE~kIBR2|8IRjt=)z0G~kbx8H@es%TJxqn5yAV z%}<#W=-1}|_^W1X)?zGQP?i7l@cQPkK)=&eO}pwap`dED$yCin>Vzx*M?3gn^2DEf zn^AXgx}fTCsqK{e=akQa)kpA}fj-^T2@3nEGJ$F-A=T>1zR|Dt7n?tS{!D}*?~)7V zc;i&KZ%uah+uh{bl-e8e^V?31-o1ObCY^+kP_JNkouj)R4o_FJnmVenC$dSMs6jN3 z3Pibq)614Bg-TNMy|V15{CTQPgj5eU)Cvy1rAP>hi(gKIc!k5lV*XII)VpoeyeuTh z55gJ6`MptJVUhAW#Poq4W3Rp!>1QJQsq3G8$Z3K8^2r*%jtmGb-Me@1pw{s(4y8FT zvN1Gx8m9*noWS2C8LP7_G7D+P=`_RTkEGr;fYS$v?%cWKkuu>J5)#6n_TudeoC*iy zh&J@LIDS^c8doHIrfuLz?|WlZPGEKr=uTt&?S=x!`xFfSNSQ=gsT-# zrW~95o@z(_p$fpqj0eT6Zn73$VuX1t#=>x~x<$DX%($7DU_^rvMh!tW0NbmE*zi@KGg$C?} zs=3Ty3%a9|8yq{*#ulb?-3&gZ&-AP0IUW_9jn+1eAWs_Rf<^D4jgLq&WURKQ>--UG z*&$F~zw^Te?mBdwk$!U4DMajJpfQe;^xgvkvc#msO>Gxa|Cz;-1k6p)qNst+u0>h7 zIEl8U#n#>J#*^8T%k!~jsC^288!uD!e$8WG?u8?38?aK zuPAQJg2+N?X=&HU4JhUNHk00_C&+OxlV_u2olNq09cEEpgH#}0-^(sKN8kCDJ0zFw zZ*T}C&&~$@C5gJRWsdNr>&{|7_P2a{ur7b3;iTgS>(}S;@w%I#p*`l+8Q5KdqDPw@ z{Tbm;d}eqIQBLI4_nWjSxavGx&6HwGd9f)}2*ni9ZX*m1!_gaotPWMB6$hc_djb~8 z27T(Tl+JeXF-Mnch*y1v^HqIjoVy=F{5K5e4E)a53vN~kxV|w_wK{}rV~pBkP8?Z1 z+6U{6t$W^YxO@?lwCSl&7URQuPbYcao@a^Ci--Q)(^yb4zu9@ItEd)G0sqG9^9@KQ z@_gMYk9qv+J$?)H+mD11n8(aUr3Ra=fvja@#^OgD zt9qu_O3F=BiHvy?ARr727piuUu!Mmi8|+u5%E-FHRE$oX+GpYl+@FqB8kh;m^()Pr zMWrUq$hAa62Yv6&%I@oV)WN`%GP(MuwstwjG;22^@?hj#@{TdAZ}-Bh$Wdh_n`dZ+ zDNc&(z+&ZVE^0Tqk%kJ(Nn&|se)_Au?a7?~jsLfWcXSz&OOgC)1W#EiUuiUAX>5Zt zEV?qLDyyWG>AXmb9czwbf31JDvl4T1R23h( z{gzlwLkv`GitL4d^2RaE&+J0P^X+O9MvcYW4zz`C?M?(iDYU#jGT^1wG$ssAb3p_*86CZ$+87q|A4OcY_8@psuW5p5SZSc_L?amqq@W;c1uQC+ z_?8ajH%UpxR#Cj=mqnso$kwLa%|jKXY^YWfo z)L*240Nd}F~ec~?UTs?9;gVT)rt&EyxyVHBV3+3lf<{=R)OUuUxSYjAO7}A*X ziQgVm>M6>w<4xlw_v+|npF>`PDcSP#-c|}ccMO5bqw!1|po~&SRn{Kno$a*OzLu8g zDVI13?gzl-6h$Xo1E0YnO)sGjB%cgb`w<*NsaiDmy;nngK~d3r`7SewV&I0eF_rGo zh9K$ZCB^5@x8JKghEf)*W2L$^R~ah3Nr|FiKpK)R8IIgVVC4zl)fsQ??o$?^hQ*3P zS{}(#9@e!WA9N_GjK8lWm(GmZ;0g3LJU_R0Jhe1;=UVDoibR^>Qov(l?U$Y%pV9bGj$VJ%F7 znVkc%-}&@P@9WYm5DpthsGY)Sa--+v)7T&$M#$k62IY%Rf?pc5_dw$15f)wbS7=tA z+982{DBS%5*h~qDi3u^FDS83%7$rM53S3ntOM1HcDy)~9444<@k6cz!+qn z|1L0S+G9@o9Vjd|o0fUn@w7%l&vLYp^W>)-$%NaO@mRvvbNL0icySQkz1mkj>%Ww_ zx5%~8W)oGg8sD@1oHotyNBhq!NF;Ta!s7>MYZ$!OvdcvQ^O>>9{CJea(bj2=r_sYMr^e^`&P2o!JVxAdrOYs&?AAc8H@N9_JSQ3g6-w*XQ92_if zI)8mJIo34F`hD#f1Z!WNPE5ca#7!!?RG6v}Lag+4&s}YM+*WlMnraz<@6~ zicrf_d3UkMm)PJgp&n3J8xFNW!gsmUK3pHc zJZGmHLTXpm`8;0d^iUEzkJWS!E-*m*al%`21whc_Z5(5x*_@wTq9&ff!I#b&{8zDq z-(QdOLXG<34kgp z>3E|9TRYK&BdtmfC)!sSP$7nN-37HcSX*1+(-+ZK3(g3 zv2nkflDyn)_djkWLGE^x`J3kem;&_A%xIe@e$ybm^wR<6(jD2tTNlmjf!M@{mZW} z>4fZ(-RJyHjG3Q4dIX$Lw$36r+_2VRs~+k-9x}C5`A6~DR-IE8h~YD-e8wA>3ol&e z(vV%GGWSb`ww~}e2^3QA`et8J8Z7uO7*%?>3`(4&A-3yQ)z)7pzrS_yl*-uVrt_Sb z!e2e?>3?23Yj+8!V}0}-segZ0E4|jNWFX@%0aZ*4PMJ2OpJ<=KOIal|p(xdM-hVd` zm*JFo&_@=y`UBb~K{Te+4HJ=Z4WFC(LynYpwn}ObnPGu#VP7g}x$`mXS=;~WP5p~Apdvvk%bFUckS)?_fifrmxF-*Ck$ ze7`Xi*+!WkB9O3K#iu=c=cHgosqT9EDaW(rg~H>%`0>ye@XhyO*^;;J3|2m=KJrM{ z2)icBp;2iTcdXmwD22=YN)8S0&L12%`yaBuJ!Z1P1C1^>{zBJgA9ZrNf%>f;sHJ(EybUM`%kv76wtS?)>L;pkW10wkPl zv%nX@Sf$xk%t`yJ+m->VaJQ{jhdc%+d#qjPd+k_Qh5+Xf8kzExQvAr3@wmX;ccr^` zgdVY$yMo(|+-W?4(`rZ`;Rk%SM~Cl6Jz(w8AVod<<;*SZg*6UAgZWJwWs}$h!b&-~ z**WIN6BfhU$Q*3*A)s-?P$6@eW9NmU`1R?MqvXYAwbl2)$6A<9veBQDl&sk?%|M+y zJ7VL(D;jUgANjMMWCrt9#TA!@FHx!IIVY_Itzy~~8QnR^I<`bNr7(;F4r90EtRNm9 zzcP*~9NfFT?AyM;EmcbKwH@4(4kHOlh^(rac6`ZO1pZF)L|EAw9HS7dI^xe4rvQr~=%H{N3UqvOk#(><@=k>8w`$!?Xah^l;2lZ=1{UVoKtbmV|kA3wR zV|2{>vW=W_=VHbJ?UO^|G){TD&yB_|+TRD)E`?8TT0<|5^j((E3K6*`J+G48e9V#r)9Zz(x^nZLRaWB79yuwMu zv%#t6oHZh1$(`iQ2Tv9jmZ_B&L-R=e$|EgL5%`fM%NWAov`xEUxDs$*%?nN8JN4)| zu?zclX>Dh_7u^XXB_9 zJ%8-gk%|9I@SV@0GKr22;jE&;z5b!rGORwQ~FJN9T-B=qnbe_HJ`b|Xw z?>m?9ev58=BjGF)_C|d#gZ*q^TS@#|a`B9(v$YuA*|Hqst7p=?mrJMGr-28bR)+g# zpNJLH0{7q=otLHyNJ*SsX4$qU*y=&wof?PYbDU3$9v zAtvw8%~HXzzi1y-!S#%P#f2&RoYb^8@tOVlljt_AWiojOAIhL=LGGa#O2Px(s`sxa z;s6c?R*D`(qJRsVJHxAwj7XP*HgWH(*7x5vTMX6ke($o4OilLU6AYv8ULQG-0;AL6-a2{ZFo`*Ba|^KFT_`9Ae^@Go|DCUn6eraJ2j`XC=M zZC}mJC{*Py^qE4EL=Vj_sv^z;`R2@kR82eh`z>Mi9wGHZjr1E#IUj$OUWj^(=+^xt zq}KXMM+{mf9}XZDkU1=Jsgc8kBAji zoILG4JWCrpWq}n~v*mToDQfs|?0R-zdkiCQF6MRwZAVLqEJkA1%-c#+2P1XLF=m}d zKM!8WApMt|14a#OTfFuU+)J|=g>i5Rrc4cOyB)km=DJ=jaSAQjP@2zj9f~&Xsiv0k zt$fmB^i1+QX?;Z#@{B`d%3ZQ)cw239bhA!^1a(vqGjnrzd?=y`+=rg<-Jj|GvWp4N zyJwrK^~ZHI{K$D?3-1?UkcWlG;xok-(*FPYsCkv2^(w>1Rm^any3u=e0=ajoRx{H* zHJoS@x#NH7-u;46nA{y~0Xer?ge3>joRT#V<5E-^H!#usB;2kW*d+34=In5P1$aT* zP1|Va)geVzz&qi37$o&N(1bF4GO_8L!jB+}<_22UBtbf)BVHWiRT$$e?{jwYz@q$2 zLmXl9C-p(Q-kIE(xT#_fF$a)Y2*`Gz)af!=buJ`<&UZ_nE+vhZpYuE!wvs%f^@^5k zYDE=U2&!FcygYPP>$F`ThQ3z$1CqJsCC#Pqqs7KyT#aoy66x<_pct+y6S1MVH{$=G zlt_h;0F^?m)pVv$J0j@z@InVwSg(0QOs$cd7cc*#c>k2@**TWF2(iv=x$q}(LWlM_ zI@j4NaC|xa!-sxfEsM!5$$6xIe2sh?)Oc44-r5$Uw^-2iZ7!5?EN%LA=?Htc@u=E}S@v_~4Pac zc$VzCpyF%j8SNQD;a&K1zt&_bTF@(Pq05{5pqn7e3g%oaQd8Sa*q&_ubHWXeekody;JjcY}{RHX=reu>FaylN$eds!J}aZ!;$DNBDbu zQ^G%8ijN|zuJcG+?NCl25cMOaTu~EZ*pCzkb{f*p;-(T*9Bc-gNQ4+HOrNJQ65B1U zy8|mK9PdLs3ZMDhlOqkSq;bVJUsRCs8s7MsW7d=q)=9r<03rxK8j!m272B+6?;4R= zKKRZOGp;~m{J4>j_|ES6{JiN{oc>Eph0Dr{MS3TO$Emo4j%Cf)-t0fQP{MP*)ib^a z{Y%yh2-+7?TE4L^8IpeI41Ve@*a}DZ^NLdEu_=MC!Y)TyB8$ZNDI0^OyYiH^cL^DPHWXzx15h zAdHYF@%!w{ikIH(@(a9`pqR^Iqq`>*>`a6k$J2;G!AI>0S>7ZVIFY>~aDicJ_0P)| zcKyQ41b4bCi!)rjbo{?r=36ICFMf|O8@%WBYQ7F#L#*pAZc_R_;j0H&>At6Nf`%af zM**8(uSEmG-!lYM7;&gYC!T4q=;JXw{c`F3!%*6+MI6Hf;=0$h>ioR2ir}%K2CI>; z>d>B7L|ej|fsGsXs&Q0c0{>G-XS)wiR1H94h4%nY4_At3u{keUuP%9$mub8QG^td` z)__zp4H31V51}@0fX_2fN^7$;@ua_iDav1kd#FV&) zE5wS8bn}>g+O|}!y|bTMoRzTwxjcOJbf3?Yb4`X#Qh7F3Nu-!QI>n7W zjQv&qr7n5dy%8S_Vz>5e*428}K)G_P&mQby-NMC6^JZ}ay?aSEM_O2%u0312Sk!nn zk9S`vdfm%A7aya4t>Eez9C+a{HF`X0Tkc=k6APRy-wzxmU04&nBk1wpHuL>PF6o*- zJ&Pi!-jZXl#3Ha!HRbUki=y~kj55}^@9fP43uk`e~0||@LeZB8)M0V{z%AJ zikD+=x81;}^bun^ z)q? z91_JJIf|8>27;X37ERfWDqC~iEp(#ZH^zAOhc1MMmNjU-?y2*+mX-kfnfk9KsNWgA z&PJ~0)ltZA=RHZgZ^0#}*6?}v7GJ>jvdZ+kt}Kzc4IoaV zI;a&p+w62k5PK>iGY+{sZ#upx8-^%U)*etqk(}v(NC~xN^Ninru{rY?uuf(dHh29@8MgeHBI3OnZ8Dy8XB28$ur&kO2WL ze3`I8|0G(#E`5ANyRz8DY~l*8F3Zl4+XgfLzyAFBlE zaLP&UiAINx-KN1F;p#ln!o=2i6O_pB(&>X2sRU)`X0CeEW5@&A0aBwy2yMdGAuJ)@ z&Wh|DM@$J?aiwgP%`eXO@G(I~rR~;Jqd>h<_w5Ir`^ElrySX3K_^S7{5=|h&l~uFT z8IG>H9ODjMk@u(8iV7xxk(Ce!?R1t5HagW`9J=tcUbo?-uiH&P~)eijbS2u1=NHJXYplM(__fZT@Od?=^Rk8pMciuCHN!Wb+0OZo5;`z6+v zLOUNhX2mc5j|T7TmqA}^Wr1toQ2~JigY?S^ormHv(6&}7)WX@jt*4vw^ca~I_fPb_ zBi{eS5gEn6APczd1!yEcwseX_{R*@_3+Y2Kpt;cqPEMvNLEq`Q~^UG0ms9E(CEp5HB^H6b&<}UoIT#fVf z%!CjP+PV7=AI?m6YIg!SGlVkG?pLjOTW$fxv^ahG zS4*nVB<*aNE7v~H+KUR9s3vQ^>d@hCiIJ#o)gNchejCVF-FB5QFxJwIU38U1m1S1P zS(X=}bt1oBq->U&=i-`9+Uv+JyPf5uEwBw<2Hs_N-vcjtOkAh^l77>0}}m5B7j0oBpalr znEgsPCq2;U)U=9EV3Wkn`*&lW*W=1WWhgzSqw9?= z-ip>}pBtb0*H;T9C3b5j>i$g%!;QsGW5ym6i=7cL;+Bn)q!K4J7RSH^DETk&;ZHfl znR=Et&hE##%O@(b**--=>88sq_$|F8;I%CKF;A}no3h&bLKUmu)sh3Z+Utqbzwh@C z+9x8+k5STP(8kH_<{P9}e`&K7wKlCraRdxV{GM&lNUY)Amf-Ig>rk6V&keP)+eSNL zGE3+RY^LrIuN|!w_9(EOWRU42FQ2i~CIsz)<>chl`_n`ZW@*G$!hosLt`_?xC|`VmiCwyB3aWjEsCTcvx6E~J_!v`S7$G~i33HhWF3_m!hko;(IB5*$f!!6=)q z9Zed2x8^dw^)$i`kHOZfyI%e-RoZ&xRCH}AC5s`uPn?5ku+KCEq0FhW3tHt zVNam&!=~SD!hHVLeBm!An5)Jr#5!*j4xO&tK3vb~2@HdXsZ)R4GtNsX8AiM;@tr z{B(MX4Vcv)7f;b^aJ*rU&mKD@(^${9LwK?g?h$mpi%Y;J1^?W|z^?EWRX|yN15`>i zBerH1Hb?i{Xi#PgHBb_RhdQ2J~>x zhEFk)=jB|@m;1hlwYWeIqwhxH)M0fnxduYHPvez6|PA)1n^Ki6o)Z3z=YE#%C&UD5jM)EPc@5E}~op~;#{ zuUok|FN{~o0Mg8+tQp9VxF8U7aHY5{e`uAi!-nFF%cV1e z<s`iwg!07 zB`k?j&J4ewe|jA@s#4B@y4rHQi2!6?GT4;1l)Cl;hv)8c;`1=}Xw`n`_~j>vxkkD= zw!r9>wn;hqNrPYFm@qeBB@sP_+;=(} znF~Xc?WRh-p?b{S=#t3j@5~;1L()x;=%X4lLi9^*?Y62)=VDcChp6W)H?OU@IsH=V zEV}&er0wW|{d6PV9DV+0e4}`<3X$XP>3JG+YaFJd?+_XWYGzN1t zW%k67)E<~rR7S#=sBG4!FqI}!C8Yd!JiEoNmtaZ8iHsMXv4G!$y1CjB=5H*s%U^PB zHsb~S4B=nmKx}Dg6d`;lBL~`=c&6^7T*}&m^1YO7)h%V`5Oh^DuCcd`5*(d8XdX>g zP03F0DtcAWv)N>Eb9GWXtR`kJZo=I0M{sT&4tN9*Bj3HwlNayE{mf2;CAi5x3faFN zsrr!l(M<}oFtM1Dr}F#UT@C8!gcmkpoPX5ym8!QLvF2Io$3wz@JX9SX|FD6(`40DF zJ?CaYSkgl7LxV{KJoq~z>`GqwtsaeM-Atbv{r&qtI_8w6j~Flrl2edB6lFRqfXw|D zQx3stD9wzUP#0jMMG-DPER^A`(f@^Lh4WY@+m2)reMA1g}7X{T#m$-*oF?9^=tt2{m81*rMil9Kt zTLhw1sEjf-=A^(s%L4qZYYUq8u&w81SQvWu^w#tO8l#kUA9>LrQg&n(>^A!X0-C>AhQoS;7%&WF95zB1 zotNEq54KLheAkI@)s`_etn1WEgQ!Kli5XWF_VQ5VmrFvg%f1m^`XTCB#j=wA9nyeZTdgYJ>;D^=6-TabaK_XlxXVJG1e4L(|}m>q~CMxFEQYrlu#xm zA+SD7F^g1AmLMrpzt}hA*iU`18b}l6%}AaM26})F??`WNBOXd|>A$9a!8*^@Li>*G zL|jXNtD_HrRzfuYsa_6cQ_(o+h6ZT?Re2`{`5ThdiOi_TNOZfT7{*9B7CrKG;TL_S zpw$T_nZ5vO;7$Lbj&*>WvTHM-$5XRDJI|&7-JAoOeK!A;lqX)Lu5&giF}v)?*f`%K z4SskJNsYO#nkry(wAan(KcANbJumom`2;Fn>`y!&_B3P7i{e5MMs}f3jjj)c$_8DP zoGkZu5%3WB4-3>d%z%9s!=BdncyZ<#$&@2>AN=vc$>{Ett5MSL_98E^9do z5&J<5<)pxM8M~~F$K}v18@+XvJjDSKpJe+GQP62QLZ#((q`DO!6rJIW4rUa_)s^pFcK@$LQcI>$Xo&fOqD|27M0)L5)mzgPFJ$C)4`m zpgJO-uxn}NMBFoQ(NR>4-XzK2#8;9ns2@6akgcvHLd(U0U04Svz@kb&RfXG4d?PY6 z%p;6vLim4|V;?GXA&>nfgN!#fJtuoO&5|8oVx8LcL^buxI#A(}+yerR^K_u^$?!YG zy!#QA^dqu&tFaBeuIEvH_c@BXuqcl0x(NS(rP!b>u--a}znmwmp1Roe}32lbTx5pIvsx z?I4V$h0N-~nfsuvwg{9whzKY$@xgi~0Z-PfRk&#{s=49nc*+Y*A%+QDyWe5YAI*Df ziUcH9TI%niAZ%oK;%}M-a*f;v+}QGHmciPhV-TQ9voe_JKMXjgD;j86}F_GpxAe-L#vfkgYeO#-w&;JCG0dZR3D$}QL|I$!r(%Vv#1*h|Le;b zZYauAr|YbN22e`;O)zLdAR-feU_u2-DivouQTpQSC*{%FZ9C**NqKQ0o>kDgidY?J zJlkz2k=p11l^fLoT_qW)C+-pe0Y4?&F*U^I(d}C(K5$fXH8X25@{)Mk)>#2H%kqYfVy4aZH`B04e zkm8P+sj2D6?(S}z&2awJV2m%CrDVHdr)lfs_YMa$z)&R|Snf$BJGF{>v z@X$WPs7@JDdq5PgG=?~+ zg<5p0{@HGXgr1_;&_poQ&jblow!iO_D}XxT=uCe8TmHbwo}Kos>7Ey*4Ea&$|(`@et>)hR1fr~ zYH(ddyXP(3+Ia#z{^%UNPt-rj@Cr~H7Km{Lr{nf*w&vR)g^n*S)C#KMo}MDjaUVVP zHLf@UEc#OgKm6UNUy;(!Q{6aOlWq=wHB~dyV3S$|7wYfJ|7tJxTcH0-jeSVJoe4jk zLn^#JMb&Yt#sXf_Z`a?_(vk`w&9_N)0~+)NtG^!muJ}czT$S0R`tw#Nq^qqK0X_YZ t#!~|JlV2!z|F8WX;U5bbW8W~o`SITNp_}NF+aDmlR8W&IlQn(we*l9mbnyTH literal 0 HcmV?d00001 diff --git a/assets/dance.svg b/assets/dance.svg new file mode 100644 index 0000000..7319efa --- /dev/null +++ b/assets/dance.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/commands/README.md b/commands/README.md deleted file mode 100644 index e5c6d9a..0000000 --- a/commands/README.md +++ /dev/null @@ -1,199 +0,0 @@ - -Commands -======== - - - -Commands are defined in [`commands.yaml`](./commands.yaml), and then exported -to VS Code-compatible [commands](https://code.visualstudio.com/api/extension-guides/command) -and [key bindings](https://code.visualstudio.com/docs/getstarted/keybindings). - -They are implemented in [`src/commands`](../src/commands). - -| ID | Title | Description | Key bindings | -| -- | ----- | ----------- | ------------ | -| `dance.toggle` | Toggle | Toggles Dance key bindings. | | -| `dance.set.normal` | Set mode to Normal | Set Dance mode to Normal. | `Escape` (`dance.mode == 'insert'`) | -| `dance.set.insert` | Set mode to Insert | Set Dance mode to Insert. | | -| `dance.tmp.normal` | Temporary normal mode | Switches to normal mode temporarily. | `Ctrl+V` (`dance.mode == 'insert'`) | -| `dance.tmp.insert` | Temporary insert mode | Switches to insert mode temporarily. | `Ctrl+V` (`dance.mode == 'normal'`) | -| `dance.insert.before` | Insert before | Start insert before the current selections. | `I` (`dance.mode == 'normal'`) | -| `dance.insert.after` | Insert after | Start insert after the current selections. | `A` (`dance.mode == 'normal'`) | -| `dance.insert.lineStart` | Insert at line start | Start insert at line start of each selection. | `Shift+I` (`dance.mode == 'normal'`) | -| `dance.insert.lineEnd` | Insert at line end | Start insert at line end of each selection. | `Shift+A` (`dance.mode == 'normal'`) | -| `dance.insert.newLine.below` | Insert new line below | Create new line and start insert below. | `O` (`dance.mode == 'normal'`) | -| `dance.insert.newLine.above` | Insert new line above | Create new line and start insert above. | `Shift+O` (`dance.mode == 'normal'`) | -| `dance.newLine.below` | Add new line below | Add a new line below, without entering insert mode. | `Alt+O` (`dance.mode == 'normal'`) | -| `dance.newLine.above` | Add new line above | Add a new line above, without entering insert mode. | `Shift+Alt+O` (`dance.mode == 'normal'`) | -| `dance.repeat.insert` | Repeat last insert-mode change | Repeat last insert-mode change. | `.` (`dance.mode == 'normal'`) | -| `dance.repeat.objectOrSelectTo` | Repeat last object select / character find | Repeat last object select / character find. | `Alt+.` (`dance.mode == 'normal'`) | -| `dance.left` | Move left | Move left. | `Left` (`dance.mode == 'normal'`), `H` (`dance.mode == 'normal'`) | -| `dance.right` | Move right | Move right. | `Right` (`dance.mode == 'normal'`), `L` (`dance.mode == 'normal'`) | -| `dance.up` | Move up | Move up. | `Up` (`dance.mode == 'normal'`), `K` (`dance.mode == 'normal'`) | -| `dance.down` | Move down | Move down. | `Down` (`dance.mode == 'normal'`), `J` (`dance.mode == 'normal'`) | -| `dance.up.page` | Scroll one page up | Scroll one page up. | `Ctrl+B` (`dance.mode == 'normal'`), `Ctrl+B` (`dance.mode == 'insert'`) | -| `dance.down.page` | Scroll one page down | Scroll one page down. | `Ctrl+F` (`dance.mode == 'normal'`), `Ctrl+F` (`dance.mode == 'insert'`) | -| `dance.up.halfPage` | Scroll half a page up | Scroll half a page up. | `Ctrl+U` (`dance.mode == 'normal'`), `Ctrl+U` (`dance.mode == 'insert'`) | -| `dance.down.halfPage` | Scroll half a page down | Scroll half a page down. | `Ctrl+D` (`dance.mode == 'normal'`), `Ctrl+D` (`dance.mode == 'insert'`) | -| `dance.select.to.included` | Select to | Select to the next character pressed, including it. | `F` (`dance.mode == 'normal'`) | -| `dance.select.to.excluded` | Select until | Select until the next character pressed, excluding it. | `T` (`dance.mode == 'normal'`) | -| `dance.select.buffer` | Select whole buffer | Select whole buffer. | `Shift+5` (`dance.mode == 'normal'`) | -| `dance.select.line` | Select line | Select line on which the end of each selection lies (or next line when end lies on an end-of-line). | `X` (`dance.mode == 'normal'`) | -| `dance.select.toLineBegin` | Select to line beginning | Select to line beginning. | `Alt+H` (`dance.mode == 'normal'`), `Home` (`dance.mode == 'normal'`) | -| `dance.select.toLineEnd` | Select to line end | Select to line end. | `Alt+L` (`dance.mode == 'normal'`), `End` (`dance.mode == 'normal'`) | -| `dance.select.enclosing` | Select enclosing characters | Select enclosing characters. | `M` (`dance.mode == 'normal'`) | -| `dance.expandLines` | Extend lines | Extend selections to contain full lines (including end-of-lines). | `Alt+X` (`dance.mode == 'normal'`) | -| `dance.trimLines` | Trim lines | Trim selections to only contain full lines (from start to line break). | `Shift+Alt+X` (`dance.mode == 'normal'`) | -| `dance.trimSelections` | Trim selections | Trim whitespace at beginning and end of selections. | `Shift+-` (`dance.mode == 'normal'`) | -| `dance.select.word` | Select to next word start | Select the word and following whitespaces on the right of the end of each selection. | `W` (`dance.mode == 'normal'`) | -| `dance.select.word.previous` | Select to previous word start | Select preceding whitespaces and the word on the left of the end of each selection. | `B` (`dance.mode == 'normal'`) | -| `dance.select.word.end` | Select to next word end | Select preceding whitespaces and the word on the right of the end of each selection. | `E` (`dance.mode == 'normal'`) | -| `dance.select.word.alt` | Select to next non-whitespace word start | Select the non-whitespace word and following whitespaces on the right of the end of each selection. | `Alt+W` (`dance.mode == 'normal'`) | -| `dance.select.word.alt.previous` | Select to previous non-whitespace word start | Select preceding whitespaces and the non-whitespace word on the left of the end of each selection. | `Alt+B` (`dance.mode == 'normal'`) | -| `dance.select.word.alt.end` | Select to next non-whitespace word end | Select preceding whitespaces and the non-whitespace word on the right of the end of each selection. | `Alt+E` (`dance.mode == 'normal'`) | -| `dance.select` | Select | Select within current selections according to a RegExp. | `S` (`dance.mode == 'normal'`) | -| `dance.split` | Split | Split within current selections according to a RegExp. | `Shift+S` (`dance.mode == 'normal'`) | -| `dance.split.lines` | Split lines | Split selections into lines. | `Alt+S` (`dance.mode == 'normal'`) | -| `dance.select.firstLast` | Select first and last characters | Select first and last characters of each selection. | `Shift+Alt+S` (`dance.mode == 'normal'`) | -| `dance.select.copy` | Copy selection to next line | Copy selection to next line. | `Shift+C` (`dance.mode == 'normal'`) | -| `dance.select.copy.backwards` | Copy selection to previous line | Copy selection to previous line. | `Shift+Alt+C` (`dance.mode == 'normal'`) | -| `dance.selections.reduce` | Reduce selections | Reduce selections to their cursor. | `;` (`dance.mode == 'normal'`) | -| `dance.selections.flip` | Flip selections | Flip the direction of each selection. | `Alt+;` (`dance.mode == 'normal'`) | -| `dance.selections.forward` | Forward selections | Ensure selections are in forward direction (the active cursor is after the anchor). | `Shift+Alt+;` (`dance.mode == 'normal'`) | -| `dance.selections.backward` | Backward selections | Ensure selections are in backward direction (the active cursor is before the anchor). | | -| `dance.selections.clear` | Clear selections | Clear selections (except main) | `Space` (`dance.mode == 'normal'`) | -| `dance.selections.clearMain` | Clear main selection | Clear main selection. | `Alt+Space` (`dance.mode == 'normal'`) | -| `dance.selections.keepMatching` | Keep matching selections | Keep selections that match a RegExp. | `Alt+K` (`dance.mode == 'normal'`) | -| `dance.selections.clearMatching` | Clear matching selections | Clear selections that match a RegExp. | `Shift+Alt+K` (`dance.mode == 'normal'`) | -| `dance.selections.merge` | Merge contiguous selections | Merge contiguous selections together, including across lines. | `Shift+Alt+-` (`dance.mode == 'normal'`) | -| `dance.selections.align` | Align selections | Align selections, aligning the cursor of each selection by inserting spaces before the first character of each selection. | `Shift+7` (`dance.mode == 'normal'`) | -| `dance.selections.align.copy` | Copy indentation | Copy the indentation of the main selection (or the count one if a count is given) to all other ones. | `Shift+Alt+7` (`dance.mode == 'normal'`) | -| `dance.delete.yank` | Yank and delete | Yank and delete selections. | `D` (`dance.mode == 'normal'`) | -| `dance.delete.insert.yank` | Yank, delete and insert | Yank, delete and enter insert mode. | `C` (`dance.mode == 'normal'`) | -| `dance.delete.noYank` | Delete without yank | Delete selections without yanking. | `Alt+D` (`dance.mode == 'normal'`) | -| `dance.delete.insert.noYank` | Delete and insert without yank | Delete selections without yanking and enter insert mode. | `Alt+C` (`dance.mode == 'normal'`) | -| `dance.yank` | Yank | Yank selections. | `Y` (`dance.mode == 'normal'`) | -| `dance.paste.after` | Paste after | Paste after the end of each selection. | `P` (`dance.mode == 'normal'`) | -| `dance.paste.before` | Paste before | Paste before the start of each selection. | `Shift+P` (`dance.mode == 'normal'`) | -| `dance.paste.select.after` | Paste after and select | Paste after the end of each selection and select pasted text. | `Alt+P` (`dance.mode == 'normal'`) | -| `dance.paste.select.before` | Paste before and select | Paste before the start of each selection and select pasted text. | `Shift+Alt+P` (`dance.mode == 'normal'`) | -| `dance.paste.replace` | Replace | Replace selections with yanked text. | `Shift+R` (`dance.mode == 'normal'`) | -| `dance.paste.replace.every` | Replace with every | Replace selections with every yanked text. | `Shift+Alt+R` (`dance.mode == 'normal'`) | -| `dance.replace.characters` | Replace character | Replace each selected character with the next entered one. | `R` (`dance.mode == 'normal'`) | -| `dance.join` | Join lines | Join selected lines. | `Alt+J` (`dance.mode == 'normal'`) | -| `dance.join.select` | Join lines and select spaces | Join selected lines and select spaces inserted in place of line breaks. | `Shift+Alt+J` (`dance.mode == 'normal'`) | -| `dance.indent` | Indent | Indent selected lines. | `Shift+.` (`dance.mode == 'normal'`) | -| `dance.indent.withEmpty` | Indent (including empty) | Indent selected lines (including empty lines). | `Shift+Alt+.` (`dance.mode == 'normal'`) | -| `dance.deindent` | Deindent | Deindent selected lines. | `Shift+Alt+,` (`dance.mode == 'normal'`) | -| `dance.deindent.further` | Deindent (including incomplete indent) | Deindent selected lines (and remove additional incomplete indent). | `Shift+,` (`dance.mode == 'normal'`) | -| `dance.toLowerCase` | Transform to lowercase | Transform to lowercase. | ``` (`dance.mode == 'normal'`) | -| `dance.toUpperCase` | Transform to uppercase | Transform to uppercase. | `Shift+`` (`dance.mode == 'normal'`) | -| `dance.swapCase` | Swap case | Swap case. | `Alt+`` (`dance.mode == 'normal'`) | -| `dance.pipe.filter` | Filter through pipe | Pipe each selection to a program, and keeps it if the program returns 0. | `Shift+4` (`dance.mode == 'normal'`) | -| `dance.pipe.replace` | Pipe and replace | Pipe each selection to a command, and replaces it with its output. | `Shift+\` (`dance.mode == 'normal'`) | -| `dance.pipe.ignore` | Pipe | Pipe each selection to a command, ignoring their results. | `Shift+Alt+\` (`dance.mode == 'normal'`) | -| `dance.pipe.append` | Pipe and append | Pipe each selection to a command, appending the output after the selection. | `Shift+1` (`dance.mode == 'normal'`) | -| `dance.pipe.prepend` | Pipe and prepend | Pipe each selection to a command, prepending the output before the selection. | `Shift+Alt+1` (`dance.mode == 'normal'`) | -| `dance.history.undo` | Undo | Undo. | `U` (`dance.mode == 'normal'`) | -| `dance.history.backward` | Move backward in history | Move backward in history. | `Alt+U` (`dance.mode == 'normal'`) | -| `dance.history.redo` | Redo | Redo. | `Shift+U` (`dance.mode == 'normal'`) | -| `dance.history.forward` | Move forward in history | Move forward in history. | `Shift+Alt+U` (`dance.mode == 'normal'`) | -| `dance.history.repeat` | Repeat last change | Repeat last change. | | -| `dance.history.repeat.selection` | Repeat last selection change | Repeat last selection change. | | -| `dance.history.repeat.edit` | Repeat last edit change | Repeat last edit change. | | -| `dance.macros.record.start` | Start recording macro | Start recording macro. | `Shift+Q` (`dance.mode == 'normal' && !dance.recordingMacro`) | -| `dance.macros.record.stop` | Stop recording macro | Stop recording macro. | `Escape` (`dance.mode == 'normal'`) | -| `dance.macros.play` | Play macro | Play macro. | `Q` (`dance.mode == 'normal'`) | -| `dance.rotate` | Rotate | Rotate each selection clockwise. | `Shift+9` (`dance.mode == 'normal'`) | -| `dance.rotate.backwards` | Rotate backwards | Rotate each selection counter-clockwise. | `Shift+0` (`dance.mode == 'normal'`) | -| `dance.rotate.content` | Rotate selection content | Rotate each selection (as well as its content) clockwise. | `Shift+Alt+9` (`dance.mode == 'normal'`) | -| `dance.rotate.content.backwards` | Rotate selection content backwards | Rotate each selection (as well as its content) counter-clockwise. | `Shift+Alt+0` (`dance.mode == 'normal'`) | -| `dance.rotate.contentOnly` | Rotate content only | Rotate each selection content clockwise, without changing selections. | | -| `dance.rotate.contentOnly.backwards` | Rotate content only backwards | Rotate each selection content counter-clockwise, without changing selections. | | -| `dance.search` | Search | Search for the given input string. | `/` (`dance.mode == 'normal'`) | -| `dance.search.backwards` | Search backwards | Search for the given input string before the current selections. | `Alt+/` (`dance.mode == 'normal'`) | -| `dance.search.selection.smart` | Search current selections (smart) | Search current selections (smart). | `Shift+8` (`dance.mode == 'normal'`) | -| `dance.search.selection` | Search current selections | Search current selections. | `Shift+Alt+8` (`dance.mode == 'normal'`) | -| `dance.search.next` | Select next match | Select next match after the main selection. | `N` (`dance.mode == 'normal'`) | -| `dance.search.next.add` | Add next match | Add a new selection with the next match after the main selection. | `Shift+N` (`dance.mode == 'normal'`) | -| `dance.search.previous` | Select previous match | Select previous match before the main selection. | `Alt+N` (`dance.mode == 'normal'`) | -| `dance.search.previous.add` | Add previous match | Add a new selection with the previous match before the main selection. | `Shift+Alt+N` (`dance.mode == 'normal'`) | -| | Select whole object | Select whole object. | `Alt+A` (`dance.mode == 'normal'`), `Alt+A` (`dance.mode == 'insert'`) | -| | Select inner object | Select inner object. | `Alt+I` (`dance.mode == 'normal'`), `Alt+I` (`dance.mode == 'insert'`) | -| | Select to the whole object start | Select to the whole object start. | `[` (`dance.mode == 'normal'`) | -| | Select to the whole object end | Select to the whole object end. | `]` (`dance.mode == 'normal'`) | -| `dance.objects.performSelection` | Perform selections specified in the arguments. | Perform selections specified in the arguments.. | | -| `dance.goto` | Go to... | Shows prompt to jump somewhere | `G` (`dance.mode == 'normal'`) | -| `dance.goto.lineStart` | Go to line start | Go to line start. | | -| `dance.goto.lineStart.nonBlank` | Go to non-blank line start | Go to first non-whitespace character of the line | | -| `dance.goto.lineEnd` | Go to line end | Go to line end. | | -| `dance.goto.firstLine` | Go to first line | Go to first line. | | -| `dance.goto.lastLine` | Go to last line | Go to last line. | | -| `dance.goto.lastCharacter` | Go to last character of the document | Go to last character of the document. | | -| `dance.goto.firstVisibleLine` | Go to first visible line | Go to first visible line. | | -| `dance.goto.middleVisibleLine` | Go to middle visible line | Go to middle visible line. | | -| `dance.goto.lastVisibleLine` | Go to last visible line | Go to last visible line. | | -| `dance.goto.selectedFile` | Open file under selection | Open file under selection. | | -| `dance.goto.lastModification` | Go to last buffer modification position | Go to last buffer modification position. | | -| `dance.openMenu` | Open quick-jump menu | Open quick-jump menu. | | -| `dance.registers.insert` | Insert value in register | Insert value in register. | `Ctrl+R` (`dance.mode == 'normal'`), `Ctrl+R` (`dance.mode == 'insert'`) | -| `dance.registers.select` | Select register for next command | Select register for next command. | `Shift+\'` (`dance.mode == 'normal'`) | -| `dance.marks.saveSelections` | Save selections | Save selections. | `Shift+Z` (`dance.mode == 'normal'`) | -| `dance.marks.restoreSelections` | Restore selections | Restore selections. | `Z` (`dance.mode == 'normal'`) | -| `dance.marks.combineSelections.fromCurrent` | Combine current selections with ones from register | Combine current selections with ones from register. | `Shift+Alt+Z` (`dance.mode == 'normal'`) | -| `dance.marks.combineSelections.fromRegister` | Combine register selections with current ones | Combine register selections with current ones. | `Alt+Z` (`dance.mode == 'normal'`) | -| `dance.cancel` | Cancel operation | Cancels waiting for input from the user | `Escape` (`dance.mode == 'awaiting'`) | -| `dance.run` | Run code | Runs JavaScript code passed in a 'code' argument | | -| | Open Command Palette | Open the built-in Command Palette in VSCode | `Shift+;` (`dance.mode == 'normal'`) | -| `dance.left.extend` | Move left (extend) | Move left (extend). | `Shift+Left` (`dance.mode == 'normal'`), `Shift+H` (`dance.mode == 'normal'`) | -| `dance.right.extend` | Move right (extend) | Move right (extend). | `Shift+Right` (`dance.mode == 'normal'`), `Shift+L` (`dance.mode == 'normal'`) | -| `dance.up.extend` | Move up (extend) | Move up (extend). | `Shift+Up` (`dance.mode == 'normal'`), `Shift+K` (`dance.mode == 'normal'`) | -| `dance.down.extend` | Move down (extend) | Move down (extend). | `Shift+Down` (`dance.mode == 'normal'`), `Shift+J` (`dance.mode == 'normal'`) | -| `dance.select.to.included.extend` | Extend to | Extend to the next character pressed, including it. | `Shift+F` (`dance.mode == 'normal'`) | -| `dance.select.to.excluded.extend` | Extend until | Extend with until the next character pressed, excluding it. | `Shift+T` (`dance.mode == 'normal'`) | -| `dance.select.line.extend` | Extend with line | Extend with line on which the end of each selection lies (or next line when end lies on an end-of-line). | `Shift+X` (`dance.mode == 'normal'`) | -| `dance.select.toLineBegin.extend` | Extend to line beginning | Extend to line beginning. | `Shift+Alt+H` (`dance.mode == 'normal'`), `Shift+Home` (`dance.mode == 'normal'`) | -| `dance.select.toLineEnd.extend` | Extend to line end | Extend to line end. | `Shift+Alt+L` (`dance.mode == 'normal'`), `Shift+End` (`dance.mode == 'normal'`) | -| `dance.select.enclosing.extend` | Extend with enclosing characters | Extend with enclosing characters. | `Shift+M` (`dance.mode == 'normal'`) | -| `dance.select.word.extend` | Extend to next word start | Extend with the word and following whitespaces on the right of the end of each selection. | `Shift+W` (`dance.mode == 'normal'`) | -| `dance.select.word.previous.extend` | Extend to previous word start | Extend with preceding whitespaces and the word on the left of the end of each selection. | `Shift+B` (`dance.mode == 'normal'`) | -| `dance.select.word.end.extend` | Extend to next word end | Extend with preceding whitespaces and the word on the right of the end of each selection. | `Shift+E` (`dance.mode == 'normal'`) | -| `dance.select.word.alt.extend` | Extend to next non-whitespace word start | Extend with the non-whitespace word and following whitespaces on the right of the end of each selection. | `Shift+Alt+W` (`dance.mode == 'normal'`) | -| `dance.select.word.alt.previous.extend` | Extend to previous non-whitespace word start | Extend with preceding whitespaces and the non-whitespace word on the left of the end of each selection. | `Shift+Alt+B` (`dance.mode == 'normal'`) | -| `dance.select.word.alt.end.extend` | Extend to next non-whitespace word end | Extend with preceding whitespaces and the non-whitespace word on the right of the end of each selection. | `Shift+Alt+E` (`dance.mode == 'normal'`) | -| `dance.search.extend` | Search (extend) | Search for the given input string (extend). | `Shift+/` (`dance.mode == 'normal'`) | -| `dance.search.backwards.extend` | Search backwards (extend) | Search for the given input string before the current selections (extend). | `Shift+Alt+/` (`dance.mode == 'normal'`) | -| | Extend to the whole object start | Extend to the whole object start. | `Shift+[` (`dance.mode == 'normal'`) | -| | Extend to the whole object end | Extend to the whole object end. | `Shift+]` (`dance.mode == 'normal'`) | -| `dance.goto.extend` | Go to... (extend) | Shows prompt to jump somewhere | `Shift+G` (`dance.mode == 'normal'`) | -| `dance.goto.lineStart.extend` | Go to line start (extend) | Go to line start (extend). | | -| `dance.goto.lineStart.nonBlank.extend` | Go to non-blank line start (extend) | Go to first non-whitespace character of the line | | -| `dance.goto.lineEnd.extend` | Go to line end (extend) | Go to line end (extend). | | -| `dance.goto.firstLine.extend` | Go to first line (extend) | Go to first line (extend). | | -| `dance.goto.lastLine.extend` | Go to last line (extend) | Go to last line (extend). | | -| `dance.goto.lastCharacter.extend` | Go to last character of the document (extend) | Go to last character of the document (extend). | | -| `dance.goto.firstVisibleLine.extend` | Go to first visible line (extend) | Go to first visible line (extend). | | -| `dance.goto.middleVisibleLine.extend` | Go to middle visible line (extend) | Go to middle visible line (extend). | | -| `dance.goto.lastVisibleLine.extend` | Go to last visible line (extend) | Go to last visible line (extend). | | -| `dance.goto.lastModification.extend` | Go to last buffer modification position (extend) | Go to last buffer modification position (extend). | | -| `dance.select.to.included.backwards` | Select to (backwards) | Select to the next character pressed, including it. (backwards) | `Alt+F` (`dance.mode == 'normal'`) | -| `dance.select.to.excluded.backwards` | Select until (backwards) | Select until the next character pressed, excluding it. (backwards) | `Alt+T` (`dance.mode == 'normal'`) | -| `dance.select.enclosing.backwards` | Select enclosing characters (backwards) | Select enclosing characters. (backwards) | `Alt+M` (`dance.mode == 'normal'`) | -| `dance.select.to.included.extend.backwards` | Extend to (backwards) | Extend to the next character pressed, including it. (backwards) | `Alt+Shift+F` (`dance.mode == 'normal'`) | -| `dance.select.to.excluded.extend.backwards` | Extend until (backwards) | Extend with until the next character pressed, excluding it. (backwards) | `Alt+Shift+T` (`dance.mode == 'normal'`) | -| `dance.select.enclosing.extend.backwards` | Extend with enclosing characters (backwards) | Extend with enclosing characters. (backwards) | `Alt+Shift+M` (`dance.mode == 'normal'`) | -| | Select to the inner object start | Select to the inner object start. | `Alt+[` (`dance.mode == 'normal'`) | -| | Select to the inner object end | Select to the inner object end. | `Alt+]` (`dance.mode == 'normal'`) | -| | Extend to the inner object start | Extend to the inner object start. | `Alt+Shift+[` (`dance.mode == 'normal'`) | -| | Extend to the inner object end | Extend to the inner object end. | `Alt+Shift+]` (`dance.mode == 'normal'`) | -| `dance.count.0` | Count 0 | Adds 0 to the current counter for the next operation. | `0` (`dance.mode == 'normal'`) | -| `dance.count.1` | Count 1 | Adds 1 to the current counter for the next operation. | `1` (`dance.mode == 'normal'`) | -| `dance.count.2` | Count 2 | Adds 2 to the current counter for the next operation. | `2` (`dance.mode == 'normal'`) | -| `dance.count.3` | Count 3 | Adds 3 to the current counter for the next operation. | `3` (`dance.mode == 'normal'`) | -| `dance.count.4` | Count 4 | Adds 4 to the current counter for the next operation. | `4` (`dance.mode == 'normal'`) | -| `dance.count.5` | Count 5 | Adds 5 to the current counter for the next operation. | `5` (`dance.mode == 'normal'`) | -| `dance.count.6` | Count 6 | Adds 6 to the current counter for the next operation. | `6` (`dance.mode == 'normal'`) | -| `dance.count.7` | Count 7 | Adds 7 to the current counter for the next operation. | `7` (`dance.mode == 'normal'`) | -| `dance.count.8` | Count 8 | Adds 8 to the current counter for the next operation. | `8` (`dance.mode == 'normal'`) | -| `dance.count.9` | Count 9 | Adds 9 to the current counter for the next operation. | `9` (`dance.mode == 'normal'`) | - diff --git a/commands/commands.yaml b/commands/commands.yaml deleted file mode 100644 index 647dc80..0000000 --- a/commands/commands.yaml +++ /dev/null @@ -1,687 +0,0 @@ -# Most commands defined below come from Kakoune: -# - https://github.com/mawww/kakoune/blob/master/src/normal.cc -# - https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc - -# Meta (extension.ts & modes.ts) -# ================================================================================================ - -toggle: - title: Toggle - descr: Toggles Dance key bindings. - -set.normal: - title: Set mode to Normal - descr: Set Dance mode to Normal. - keys: escape (insert) - -set.insert: - title: Set mode to Insert - descr: Set Dance mode to Insert. - -tmp.normal: - title: Temporary normal mode - descr: Switches to normal mode temporarily. - keys: c-v (insert) - -tmp.insert: - title: Temporary insert mode - descr: Switches to insert mode temporarily. - keys: c-v (normal) - -# Insert (insert.ts) -# ================================================================================================ - -insert.before: - title: Insert before - descr: Start insert before the current selections. - keys: i (normal) - -insert.after: - title: Insert after - descr: Start insert after the current selections. - keys: a (normal) - -insert.lineStart: - title: Insert at line start - descr: Start insert at line start of each selection. - keys: s-i (normal) - -insert.lineEnd: - title: Insert at line end - descr: Start insert at line end of each selection. - keys: s-a (normal) - -insert.newLine.below: - title: Insert new line below - descr: Create new line and start insert below. - keys: o (normal) - -insert.newLine.above: - title: Insert new line above - descr: Create new line and start insert above. - keys: s-o (normal) - -newLine.below: - title: Add new line below - descr: Add a new line below, without entering insert mode. - keys: a-o (normal) - -newLine.above: - title: Add new line above - descr: Add a new line above, without entering insert mode. - keys: s-a-o (normal) - -repeat.insert: - title: Repeat last insert-mode change - keys: . (normal) - -repeat.objectOrSelectTo: - title: Repeat last object select / character find - keys: a-. (normal) - -# Move around (move.ts) -# ================================================================================================ - -left: - title: Move left - keys: left (normal), h (normal) - add: extend -right: - title: Move right - keys: right (normal), l (normal) - add: extend -up: - title: Move up - keys: up (normal), k (normal) - add: extend -down: - title: Move down - keys: down (normal), j (normal) - add: extend - -up.page: - title: Scroll one page up - keys: c-b (normal), c-b (insert) - -down.page: - title: Scroll one page down - keys: c-f (normal), c-f (insert) - -up.halfPage: - title: Scroll half a page up - keys: c-u (normal), c-u (insert) - -down.halfPage: - title: Scroll half a page down - keys: c-d (normal), c-d (insert) - -# Select to / until / buffer / line (move.ts) -# ================================================================================================ - -select.to.included: - title: Select to - descr: Select to the next character pressed, including it. - keys: f (normal) - add: extend back - -select.to.excluded: - title: Select until - descr: Select until the next character pressed, excluding it. - keys: t (normal) - add: extend back - -select.buffer: - title: Select whole buffer - keys: s-5 (normal) - -select.line: - title: Select line - descr: Select line on which the end of each selection lies (or next line when end lies on an end-of-line). - keys: x (normal) - add: extend - -select.toLineBegin: - title: Select to line beginning - keys: a-h (normal), home (normal) - add: extend - -select.toLineEnd: - title: Select to line end - keys: a-l (normal), end (normal) - add: extend - -select.enclosing: - title: Select enclosing characters - keys: m (normal) - add: extend back - -expandLines: - title: Extend lines - descr: Extend selections to contain full lines (including end-of-lines). - keys: a-x (normal) - -trimLines: - title: Trim lines - descr: Trim selections to only contain full lines (from start to line break). - keys: s-a-x (normal) - -trimSelections: - title: Trim selections - descr: Trim whitespace at beginning and end of selections. - keys: s-- (normal) - -# Select word (move.ts) -# ================================================================================================ - -select.word: - title: Select to next word start - descr: Select the word and following whitespaces on the right of the end of each selection. - keys: w (normal) - add: extend - -select.word.previous: - title: Select to previous word start - descr: Select preceding whitespaces and the word on the left of the end of each selection. - keys: b (normal) - add: extend - -select.word.end: - title: Select to next word end - descr: Select preceding whitespaces and the word on the right of the end of each selection. - keys: e (normal) - add: extend - -select.word.alt: - title: Select to next non-whitespace word start - descr: Select the non-whitespace word and following whitespaces on the right of the end of each selection. - keys: a-w (normal) - add: extend - -select.word.alt.previous: - title: Select to previous non-whitespace word start - descr: Select preceding whitespaces and the non-whitespace word on the left of the end of each selection. - keys: a-b (normal) - add: extend - -select.word.alt.end: - title: Select to next non-whitespace word end - descr: Select preceding whitespaces and the non-whitespace word on the right of the end of each selection. - keys: a-e (normal) - add: extend - -# Select within current selections (multiple.ts) -# ================================================================================================ - -select: - title: Select - descr: Select within current selections according to a RegExp. - keys: s (normal) - -split: - title: Split - descr: Split within current selections according to a RegExp. - keys: s-s (normal) - -split.lines: - title: Split lines - descr: Split selections into lines. - keys: a-s (normal) - -select.firstLast: - title: Select first and last characters - descr: Select first and last characters of each selection. - keys: s-a-s (normal) - -select.copy: - title: Copy selection to next line - keys: s-c (normal) - -select.copy.backwards: - title: Copy selection to previous line - keys: s-a-c (normal) - -# Switch current selections and modify selections (select.ts) -# ================================================================================================ - -selections.reduce: - title: Reduce selections - descr: Reduce selections to their cursor. - keys: ; (normal) - -selections.flip: - title: Flip selections - descr: Flip the direction of each selection. - keys: a-; (normal) - -selections.forward: - title: Forward selections - descr: Ensure selections are in forward direction (the active cursor is after the anchor). - keys: s-a-; (normal) - -selections.backward: - title: Backward selections - descr: Ensure selections are in backward direction (the active cursor is before the anchor). - -selections.clear: - title: Clear selections - descr: Clear selections (except main) - keys: space (normal) - -selections.clearMain: - title: Clear main selection - keys: a-space (normal) - -selections.keepMatching: - title: Keep matching selections - descr: Keep selections that match a RegExp. - keys: a-k (normal) - -selections.clearMatching: - title: Clear matching selections - descr: Clear selections that match a RegExp. - keys: s-a-k (normal) - -selections.merge: - title: Merge contiguous selections - descr: Merge contiguous selections together, including across lines. - keys: s-a-- (normal) - -selections.align: - title: Align selections - descr: Align selections, aligning the cursor of each selection by inserting spaces before the first character of each selection. - keys: "s-7 (normal)" - -selections.align.copy: - title: Copy indentation - descr: Copy the indentation of the main selection (or the count one if a count is given) to all other ones. - keys: "s-a-7 (normal)" - -# Yank & Paste (yankPaste.ts) -# ================================================================================================ - -delete.yank: - title: Yank and delete - descr: Yank and delete selections. - keys: d (normal) - -delete.insert.yank: - title: Yank, delete and insert - descr: Yank, delete and enter insert mode. - keys: c (normal) - -delete.noYank: - title: Delete without yank - descr: Delete selections without yanking. - keys: a-d (normal) - -delete.insert.noYank: - title: Delete and insert without yank - descr: Delete selections without yanking and enter insert mode. - keys: a-c (normal) - -yank: - title: Yank - descr: Yank selections. - keys: y (normal) - -paste.after: - title: Paste after - descr: Paste after the end of each selection. - keys: p (normal) - -paste.before: - title: Paste before - descr: Paste before the start of each selection. - keys: s-p (normal) - -paste.select.after: - title: Paste after and select - descr: Paste after the end of each selection and select pasted text. - keys: a-p (normal) - -paste.select.before: - title: Paste before and select - descr: Paste before the start of each selection and select pasted text. - keys: s-a-p (normal) - -paste.replace: - title: Replace - descr: Replace selections with yanked text. - keys: s-r (normal) - -paste.replace.every: - title: Replace with every - descr: Replace selections with every yanked text. - keys: s-a-r (normal) - -# Changes (changes.ts) -# ================================================================================================ - -replace.characters: - title: Replace character - descr: Replace each selected character with the next entered one. - keys: r (normal) - -join: - title: Join lines - descr: Join selected lines. - keys: a-j (normal) - -join.select: - title: Join lines and select spaces - descr: Join selected lines and select spaces inserted in place of line breaks. - keys: s-a-j (normal) - -indent: - title: Indent - descr: Indent selected lines. - keys: "s-. (normal)" - -indent.withEmpty: - title: Indent (including empty) - descr: Indent selected lines (including empty lines). - keys: "s-a-. (normal)" - -deindent: - title: Deindent - descr: Deindent selected lines. - keys: "s-a-, (normal)" - -deindent.further: - title: Deindent (including incomplete indent) - descr: Deindent selected lines (and remove additional incomplete indent). - keys: "s-, (normal)" - -toLowerCase: - title: Transform to lowercase - keys: "` (normal)" - -toUpperCase: - title: Transform to uppercase - keys: "s-` (normal)" - -swapCase: - title: Swap case - keys: "a-` (normal)" - -# Pipes (pipe.ts) -# ================================================================================================ - -pipe.filter: - title: Filter through pipe - descr: Pipe each selection to a program, and keeps it if the program returns 0. - keys: s-4 (normal) - -pipe.replace: - title: Pipe and replace - descr: Pipe each selection to a command, and replaces it with its output. - keys: s-\ (normal) - -pipe.ignore: - title: Pipe - descr: Pipe each selection to a command, ignoring their results. - keys: s-a-\ (normal) - -pipe.append: - title: Pipe and append - descr: Pipe each selection to a command, appending the output after the selection. - keys: s-1 (normal) - -pipe.prepend: - title: Pipe and prepend - descr: Pipe each selection to a command, prepending the output before the selection. - keys: s-a-1 (normal) - -# History (history.ts) -# ================================================================================================ - -history.undo: - title: Undo - keys: u (normal) - -history.backward: - title: Move backward in history - keys: a-u (normal) - -history.redo: - title: Redo - keys: s-u (normal) - -history.forward: - title: Move forward in history - keys: s-a-u (normal) - -history.repeat: - title: Repeat last change - -history.repeat.selection: - title: Repeat last selection change - -history.repeat.edit: - title: Repeat last edit change - -# Macros (macros.ts) -# ================================================================================================ - -macros.record.start: - title: Start recording macro - keys: s-q (normal -macro) - -macros.record.stop: - title: Stop recording macro - keys: escape (normal) - # Note: this command executes even if a macro recording is not in progess, - # so that will noop instead of defaulting to deselecting in VSCode. - -macros.play: - title: Play macro - keys: q (normal) - -# Rotate (rotate.ts) -# ================================================================================================ - -rotate: - title: Rotate - descr: Rotate each selection clockwise. - keys: s-9 (normal) - -rotate.backwards: - title: Rotate backwards - descr: Rotate each selection counter-clockwise. - keys: s-0 (normal) - -rotate.content: - title: Rotate selection content - descr: Rotate each selection (as well as its content) clockwise. - keys: s-a-9 (normal) - -rotate.content.backwards: - title: Rotate selection content backwards - descr: Rotate each selection (as well as its content) counter-clockwise. - keys: s-a-0 (normal) - -rotate.contentOnly: - title: Rotate content only - descr: Rotate each selection content clockwise, without changing selections. - -rotate.contentOnly.backwards: - title: Rotate content only backwards - descr: Rotate each selection content counter-clockwise, without changing selections. - -# Search (search.ts) -# ================================================================================================ - -search: - title: Search - descr: Search for the given input string. - keys: / (normal) - add: extend - -search.backwards: - title: Search backwards - descr: Search for the given input string before the current selections. - keys: a-/ (normal) - add: extend - -search.selection.smart: - title: Search current selections (smart) - keys: s-8 (normal) - -search.selection: - title: Search current selections - keys: s-a-8 (normal) - -search.next: - title: Select next match - descr: Select next match after the main selection. - keys: n (normal) - -search.next.add: - title: Add next match - descr: Add a new selection with the next match after the main selection. - keys: s-n (normal) - -search.previous: - title: Select previous match - descr: Select previous match before the main selection. - keys: a-n (normal) - -search.previous.add: - title: Add previous match - descr: Add a new selection with the previous match before the main selection. - keys: s-a-n (normal) - -# Objects (selectObject.ts) -# ================================================================================================ - -_objects.select: - title: Select whole object - keys: a-a (normal), a-a (insert) - command: dance.openMenu - args: { menu: "object", action: "select" } - -_objects.select.inner: - title: Select inner object - keys: a-i (normal), a-i (insert) - command: dance.openMenu - args: { menu: "object", action: "select", inner: true } - -_objects.selectToStart: - title: Select to the whole object start - keys: "[ (normal)" - add: inner, extend - command: dance.openMenu - args: { menu: "object", action: "selectToStart" } - -_objects.selectToEnd: - title: Select to the whole object end - keys: "] (normal)" - add: inner, extend - command: dance.openMenu - args: { menu: "object", action: "selectToEnd" } - -objects.performSelection: - title: Perform selections specified in the arguments. - -# Goto (goto.ts) -# ================================================================================================ - -goto: - title: Go to... - descr: Shows prompt to jump somewhere - keys: g (normal) - add: extend - -goto.lineStart: - title: Go to line start - add: extend - -goto.lineStart.nonBlank: - title: Go to non-blank line start - descr: Go to first non-whitespace character of the line - add: extend - -goto.lineEnd: - title: Go to line end - add: extend - -goto.firstLine: - title: Go to first line - add: extend - -goto.lastLine: - title: Go to last line - add: extend - -goto.lastCharacter: - title: Go to last character of the document - add: extend - -goto.firstVisibleLine: - title: Go to first visible line - add: extend - -goto.middleVisibleLine: - title: Go to middle visible line - add: extend - -goto.lastVisibleLine: - title: Go to last visible line - add: extend - -goto.selectedFile: - title: Open file under selection - -goto.lastModification: - title: Go to last buffer modification position - add: extend - -# Menus (menus.ts) -# ================================================================================================ - -openMenu: - title: Open quick-jump menu - -# Registers and marks (mark.ts) -# ================================================================================================ - -registers.insert: - title: Insert value in register - keys: c-r (normal), c-r (insert) - -registers.select: - title: Select register for next command - keys: s-\' (normal) - -marks.saveSelections: - title: Save selections - keys: s-z (normal) - -marks.restoreSelections: - title: Restore selections - keys: z (normal) - -marks.combineSelections.fromCurrent: - title: Combine current selections with ones from register - keys: s-a-z (normal) - -marks.combineSelections.fromRegister: - title: Combine register selections with current ones - keys: a-z (normal) - -# Misc. (misc.ts) - -cancel: - title: Cancel operation - descr: Cancels waiting for input from the user - keys: escape (awaiting) - -run: - title: Run code - descr: Runs JavaScript code passed in a 'code' argument - -_commandPalette: - title: Open Command Palette - descr: Open the built-in Command Palette in VSCode - keys: s-; (normal) - command: "workbench.action.showCommands" diff --git a/commands/generate.ts b/commands/generate.ts deleted file mode 100644 index 9b96276..0000000 --- a/commands/generate.ts +++ /dev/null @@ -1,269 +0,0 @@ -// @ts-ignore -import { CORE_SCHEMA, load } from "js-yaml"; -import { WriteStream, createWriteStream, readFileSync } from "fs"; - -// File setup -// =============================================================================================== - -const prefix = "dance"; -const header = "Auto-generated by commands/generate.ts. Do not edit manually."; - -const stream: WriteStream = createWriteStream("./commands/index.ts", "utf8"); -const doc: WriteStream = createWriteStream("./commands/README.md", "utf8"); - -stream.write(`// ${header} - -/** A provided command. */ -export interface ICommand { - readonly id: ID; - readonly title: string; - readonly description: string; - readonly keybindings: { - readonly key: string; - readonly when: string; - }[]; -} - -`); - -doc.write(` -Commands -======== - - - -Commands are defined in [\`commands.yaml\`](./commands.yaml), and then exported -to VS Code-compatible [commands](https://code.visualstudio.com/api/extension-guides/command) -and [key bindings](https://code.visualstudio.com/docs/getstarted/keybindings). - -They are implemented in [\`src/commands\`](../src/commands). - -| ID | Title | Description | Key bindings | -| -- | ----- | ----------- | ------------ | -`); - -// Data setup -// =============================================================================================== - -interface Entry { - title: string; - descr: string; - keys?: string; - add?: string; - - command?: string; - args?: any; -} - -const yaml: Record = load(readFileSync("./commands/commands.yaml", "utf8"), { - schema: CORE_SCHEMA, -}); - -const prefixKeys = (prefix: string, keys?: string) => keys?.replace(/(^|, )(.)/g, `$1${prefix}$2`); - -for (const id in yaml) { - const command = yaml[id]; - - if (command.descr == null) { - command.descr = command.title + "."; - } - - if (command.add && command.add.includes("extend")) { - let title = command.title.replace("Select to", "Extend to").replace("Select", "Extend with"); - let descr = command.descr.replace("Select to", "Extend to").replace("Select", "Extend with"); - - if (title === "Extend with until") { - title = "Extend until"; - } - if (title === command.title) { - title += " (extend)"; - } - if (descr === command.descr) { - descr = descr.replace(".", " (extend)."); - } - - yaml[id + ".extend"] = { - ...command, - - title, - descr, - keys: prefixKeys("s-", command.keys), - args: command.args ? { ...command.args, extend: true } : undefined, - }; - } -} - -for (const id in yaml) { - const command = yaml[id]; - - if (command.add && command.add.includes("back")) { - yaml[id + ".backwards"] = { - ...command, - title: `${command.title} (backwards)`, - descr: `${command.descr} (backwards)`, - keys: prefixKeys("a-", command.keys!), - args: command.args ? { ...command.args, backwards: true } : undefined, - }; - } -} - -for (const id in yaml) { - const command = yaml[id]; - - if (command.add && command.add.includes("inner")) { - yaml[id + ".inner"] = { - ...command, - title: command.title.replace("whole", "inner"), - descr: command.descr.replace("whole", "inner"), - keys: prefixKeys("a-", command.keys!), - args: command.args ? { ...command.args, inner: true } : undefined, - }; - } -} - -for (let i = 0; i < 10; i++) { - yaml[`count.${i}`] = { - title: `Count ${i}`, - descr: `Adds ${i} to the current counter for the next operation.`, - keys: `${i} (normal)`, - }; -} - -// Generate TypeScript and Markdown files -// =============================================================================================== - -const commands: string[] = []; - -const matches = (regex: RegExp, input: string) => { - const m: RegExpExecArray[] = []; - let match: RegExpExecArray | null; - - while ((match = regex.exec(input))) { - m.push(match); - } - - return m; -}; - -const parseWhen = (when: string) => - (({ - "enabled": `${prefix}.mode != 'disabled'`, - "normal": `${prefix}.mode == 'normal'`, - "insert": `${prefix}.mode == 'insert'`, - "awaiting": `${prefix}.mode == 'awaiting'`, - - "normal macro": `${prefix}.mode == 'normal' && ${prefix}.recordingMacro`, - "normal -macro": `${prefix}.mode == 'normal' && !${prefix}.recordingMacro`, - } as any)[when]); - -const parseKey = (key: string) => - key.replace("a-", "Alt+").replace("s-", "Shift+").replace("c-", "Ctrl+"); - -const writable = (id: string) => id.replace(/\.\w/g, (c) => c.substr(1).toUpperCase()); -const parseKeys = (key: string) => - matches(/([\S]+) \(([\w- ]+)\)/g, key).map((x) => ({ - key: parseKey(x[1]), - when: parseWhen(x[2]), - })); - -const additionalKeyBindings: string[] = []; - -for (const id in yaml) { - const command = yaml[id]; - const keys = parseKeys(command.keys || ""); - - const docKeys = keys - .map( - ({ key, when }) => `\`${key.replace(/(\+|^)[a-z]/g, (x) => x.toUpperCase())}\` (\`${when}\`)`, - ) - .join(", "); - - const docStringKeys = docKeys.length === 0 - ? "" - : `\n *\n * Default key${keys.length === 1 ? "" : "s"}: ${docKeys}.`; - - const docCommandName = command.command ? "" : `\`${prefix}.${id}\``; - doc.write(`| ${docCommandName} | ${command.title} | ${command.descr} | ${docKeys} |\n`); - - if (command.command) { - // This is an additional keybinding to an existing command, instead of a - // new command to be implemented. - for (const key of keys) { - additionalKeyBindings.push(` {`); - additionalKeyBindings.push( - ` key : "${key.key.replace("\\", "\\\\").replace('"', '\\"')}",`, - ); - additionalKeyBindings.push( - ` when : "editorTextFocus && ${key.when.replace(/"/g, '\\"')}",`, - ); - additionalKeyBindings.push(` command: ${JSON.stringify(command.command)},`); - if (command.args !== undefined) { - const args = JSON.stringify(command.args, undefined, " ") - .replace(/\n/g, "") - .replace(/}$/, " }"); - - additionalKeyBindings.push(` args : ${args},`); - } - additionalKeyBindings.push(` },`); - } - continue; - } - commands.push(id); - - stream.write(`/**\n * ${command.descr}${docStringKeys}\n */\n`); - stream.write(`export const ${writable(id)}: ICommand<"${prefix}.${id}"> = {\n`); - stream.write(` id : "${prefix}.${id}",\n`); - stream.write(` title : "${command.title}",\n`); - stream.write(` description: "${command.descr.replace('"', '\\"')}",\n`); - - if (command.keys) { - stream.write(` keybindings: [\n`); - - for (const key of keys) { - stream.write( - ` { key: "${key.key - .replace("\\", "\\\\") - .replace('"', '\\"')}", when: "editorTextFocus && ${key.when.replace(/"/g, '\\"')}" },\n`, - ); - } - - stream.write(` ],\n`); - } else { - stream.write(" keybindings: [],\n"); - } - - stream.write(`};\n\n`); -} - -// Write footers and close streams -// =============================================================================================== - -stream.write(` -/** - * All defined commands. - */ -export const commands = { -${commands.map((x) => ` /** ${yaml[x].descr} */\n ${writable(x)}`).join(",\n")}, -}; - -/** - * An enum which maps command names to command IDs. - */ -export const enum Command { -${commands - .map((x) => ` /** ${yaml[x].descr} */\n ${writable(x)} = "${prefix}.${x}"`) - .join(",\n")}, -} - -/** - * Additional key bindings. - */ -export const additionalKeyBindings = [ -${additionalKeyBindings.join("\n")} -]; -`); - -doc.write("\n"); - -stream.end(); -doc.end(); diff --git a/commands/index.ts b/commands/index.ts deleted file mode 100644 index 8de6ac5..0000000 --- a/commands/index.ts +++ /dev/null @@ -1,3111 +0,0 @@ -// Auto-generated by commands/generate.ts. Do not edit manually. - -/** A provided command. */ -export interface ICommand { - readonly id: ID; - readonly title: string; - readonly description: string; - readonly keybindings: { - readonly key: string; - readonly when: string; - }[]; -} - -/** - * Toggles Dance key bindings. - */ -export const toggle: ICommand<"dance.toggle"> = { - id : "dance.toggle", - title : "Toggle", - description: "Toggles Dance key bindings.", - keybindings: [], -}; - -/** - * Set Dance mode to Normal. - * - * Default key: `Escape` (`dance.mode == 'insert'`). - */ -export const setNormal: ICommand<"dance.set.normal"> = { - id : "dance.set.normal", - title : "Set mode to Normal", - description: "Set Dance mode to Normal.", - keybindings: [ - { key: "escape", when: "editorTextFocus && dance.mode == 'insert'" }, - ], -}; - -/** - * Set Dance mode to Insert. - */ -export const setInsert: ICommand<"dance.set.insert"> = { - id : "dance.set.insert", - title : "Set mode to Insert", - description: "Set Dance mode to Insert.", - keybindings: [], -}; - -/** - * Switches to normal mode temporarily. - * - * Default key: `Ctrl+V` (`dance.mode == 'insert'`). - */ -export const tmpNormal: ICommand<"dance.tmp.normal"> = { - id : "dance.tmp.normal", - title : "Temporary normal mode", - description: "Switches to normal mode temporarily.", - keybindings: [ - { key: "Ctrl+v", when: "editorTextFocus && dance.mode == 'insert'" }, - ], -}; - -/** - * Switches to insert mode temporarily. - * - * Default key: `Ctrl+V` (`dance.mode == 'normal'`). - */ -export const tmpInsert: ICommand<"dance.tmp.insert"> = { - id : "dance.tmp.insert", - title : "Temporary insert mode", - description: "Switches to insert mode temporarily.", - keybindings: [ - { key: "Ctrl+v", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Start insert before the current selections. - * - * Default key: `I` (`dance.mode == 'normal'`). - */ -export const insertBefore: ICommand<"dance.insert.before"> = { - id : "dance.insert.before", - title : "Insert before", - description: "Start insert before the current selections.", - keybindings: [ - { key: "i", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Start insert after the current selections. - * - * Default key: `A` (`dance.mode == 'normal'`). - */ -export const insertAfter: ICommand<"dance.insert.after"> = { - id : "dance.insert.after", - title : "Insert after", - description: "Start insert after the current selections.", - keybindings: [ - { key: "a", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Start insert at line start of each selection. - * - * Default key: `Shift+I` (`dance.mode == 'normal'`). - */ -export const insertLineStart: ICommand<"dance.insert.lineStart"> = { - id : "dance.insert.lineStart", - title : "Insert at line start", - description: "Start insert at line start of each selection.", - keybindings: [ - { key: "Shift+i", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Start insert at line end of each selection. - * - * Default key: `Shift+A` (`dance.mode == 'normal'`). - */ -export const insertLineEnd: ICommand<"dance.insert.lineEnd"> = { - id : "dance.insert.lineEnd", - title : "Insert at line end", - description: "Start insert at line end of each selection.", - keybindings: [ - { key: "Shift+a", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Create new line and start insert below. - * - * Default key: `O` (`dance.mode == 'normal'`). - */ -export const insertNewLineBelow: ICommand<"dance.insert.newLine.below"> = { - id : "dance.insert.newLine.below", - title : "Insert new line below", - description: "Create new line and start insert below.", - keybindings: [ - { key: "o", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Create new line and start insert above. - * - * Default key: `Shift+O` (`dance.mode == 'normal'`). - */ -export const insertNewLineAbove: ICommand<"dance.insert.newLine.above"> = { - id : "dance.insert.newLine.above", - title : "Insert new line above", - description: "Create new line and start insert above.", - keybindings: [ - { key: "Shift+o", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Add a new line below, without entering insert mode. - * - * Default key: `Alt+O` (`dance.mode == 'normal'`). - */ -export const newLineBelow: ICommand<"dance.newLine.below"> = { - id : "dance.newLine.below", - title : "Add new line below", - description: "Add a new line below, without entering insert mode.", - keybindings: [ - { key: "Alt+o", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Add a new line above, without entering insert mode. - * - * Default key: `Shift+Alt+O` (`dance.mode == 'normal'`). - */ -export const newLineAbove: ICommand<"dance.newLine.above"> = { - id : "dance.newLine.above", - title : "Add new line above", - description: "Add a new line above, without entering insert mode.", - keybindings: [ - { key: "Shift+Alt+o", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Repeat last insert-mode change. - * - * Default key: `.` (`dance.mode == 'normal'`). - */ -export const repeatInsert: ICommand<"dance.repeat.insert"> = { - id : "dance.repeat.insert", - title : "Repeat last insert-mode change", - description: "Repeat last insert-mode change.", - keybindings: [ - { key: ".", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Repeat last object select / character find. - * - * Default key: `Alt+.` (`dance.mode == 'normal'`). - */ -export const repeatObjectOrSelectTo: ICommand<"dance.repeat.objectOrSelectTo"> = { - id : "dance.repeat.objectOrSelectTo", - title : "Repeat last object select / character find", - description: "Repeat last object select / character find.", - keybindings: [ - { key: "Alt+.", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Move left. - * - * Default keys: `Left` (`dance.mode == 'normal'`), `H` (`dance.mode == 'normal'`). - */ -export const left: ICommand<"dance.left"> = { - id : "dance.left", - title : "Move left", - description: "Move left.", - keybindings: [ - { key: "left", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "h", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Move right. - * - * Default keys: `Right` (`dance.mode == 'normal'`), `L` (`dance.mode == 'normal'`). - */ -export const right: ICommand<"dance.right"> = { - id : "dance.right", - title : "Move right", - description: "Move right.", - keybindings: [ - { key: "right", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "l", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Move up. - * - * Default keys: `Up` (`dance.mode == 'normal'`), `K` (`dance.mode == 'normal'`). - */ -export const up: ICommand<"dance.up"> = { - id : "dance.up", - title : "Move up", - description: "Move up.", - keybindings: [ - { key: "up", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "k", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Move down. - * - * Default keys: `Down` (`dance.mode == 'normal'`), `J` (`dance.mode == 'normal'`). - */ -export const down: ICommand<"dance.down"> = { - id : "dance.down", - title : "Move down", - description: "Move down.", - keybindings: [ - { key: "down", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "j", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Scroll one page up. - * - * Default keys: `Ctrl+B` (`dance.mode == 'normal'`), `Ctrl+B` (`dance.mode == 'insert'`). - */ -export const upPage: ICommand<"dance.up.page"> = { - id : "dance.up.page", - title : "Scroll one page up", - description: "Scroll one page up.", - keybindings: [ - { key: "Ctrl+b", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Ctrl+b", when: "editorTextFocus && dance.mode == 'insert'" }, - ], -}; - -/** - * Scroll one page down. - * - * Default keys: `Ctrl+F` (`dance.mode == 'normal'`), `Ctrl+F` (`dance.mode == 'insert'`). - */ -export const downPage: ICommand<"dance.down.page"> = { - id : "dance.down.page", - title : "Scroll one page down", - description: "Scroll one page down.", - keybindings: [ - { key: "Ctrl+f", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Ctrl+f", when: "editorTextFocus && dance.mode == 'insert'" }, - ], -}; - -/** - * Scroll half a page up. - * - * Default keys: `Ctrl+U` (`dance.mode == 'normal'`), `Ctrl+U` (`dance.mode == 'insert'`). - */ -export const upHalfPage: ICommand<"dance.up.halfPage"> = { - id : "dance.up.halfPage", - title : "Scroll half a page up", - description: "Scroll half a page up.", - keybindings: [ - { key: "Ctrl+u", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Ctrl+u", when: "editorTextFocus && dance.mode == 'insert'" }, - ], -}; - -/** - * Scroll half a page down. - * - * Default keys: `Ctrl+D` (`dance.mode == 'normal'`), `Ctrl+D` (`dance.mode == 'insert'`). - */ -export const downHalfPage: ICommand<"dance.down.halfPage"> = { - id : "dance.down.halfPage", - title : "Scroll half a page down", - description: "Scroll half a page down.", - keybindings: [ - { key: "Ctrl+d", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Ctrl+d", when: "editorTextFocus && dance.mode == 'insert'" }, - ], -}; - -/** - * Select to the next character pressed, including it. - * - * Default key: `F` (`dance.mode == 'normal'`). - */ -export const selectToIncluded: ICommand<"dance.select.to.included"> = { - id : "dance.select.to.included", - title : "Select to", - description: "Select to the next character pressed, including it.", - keybindings: [ - { key: "f", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select until the next character pressed, excluding it. - * - * Default key: `T` (`dance.mode == 'normal'`). - */ -export const selectToExcluded: ICommand<"dance.select.to.excluded"> = { - id : "dance.select.to.excluded", - title : "Select until", - description: "Select until the next character pressed, excluding it.", - keybindings: [ - { key: "t", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select whole buffer. - * - * Default key: `Shift+5` (`dance.mode == 'normal'`). - */ -export const selectBuffer: ICommand<"dance.select.buffer"> = { - id : "dance.select.buffer", - title : "Select whole buffer", - description: "Select whole buffer.", - keybindings: [ - { key: "Shift+5", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select line on which the end of each selection lies (or next line when end lies on an end-of-line). - * - * Default key: `X` (`dance.mode == 'normal'`). - */ -export const selectLine: ICommand<"dance.select.line"> = { - id : "dance.select.line", - title : "Select line", - description: "Select line on which the end of each selection lies (or next line when end lies on an end-of-line).", - keybindings: [ - { key: "x", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select to line beginning. - * - * Default keys: `Alt+H` (`dance.mode == 'normal'`), `Home` (`dance.mode == 'normal'`). - */ -export const selectToLineBegin: ICommand<"dance.select.toLineBegin"> = { - id : "dance.select.toLineBegin", - title : "Select to line beginning", - description: "Select to line beginning.", - keybindings: [ - { key: "Alt+h", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "home", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select to line end. - * - * Default keys: `Alt+L` (`dance.mode == 'normal'`), `End` (`dance.mode == 'normal'`). - */ -export const selectToLineEnd: ICommand<"dance.select.toLineEnd"> = { - id : "dance.select.toLineEnd", - title : "Select to line end", - description: "Select to line end.", - keybindings: [ - { key: "Alt+l", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "end", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select enclosing characters. - * - * Default key: `M` (`dance.mode == 'normal'`). - */ -export const selectEnclosing: ICommand<"dance.select.enclosing"> = { - id : "dance.select.enclosing", - title : "Select enclosing characters", - description: "Select enclosing characters.", - keybindings: [ - { key: "m", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend selections to contain full lines (including end-of-lines). - * - * Default key: `Alt+X` (`dance.mode == 'normal'`). - */ -export const expandLines: ICommand<"dance.expandLines"> = { - id : "dance.expandLines", - title : "Extend lines", - description: "Extend selections to contain full lines (including end-of-lines).", - keybindings: [ - { key: "Alt+x", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Trim selections to only contain full lines (from start to line break). - * - * Default key: `Shift+Alt+X` (`dance.mode == 'normal'`). - */ -export const trimLines: ICommand<"dance.trimLines"> = { - id : "dance.trimLines", - title : "Trim lines", - description: "Trim selections to only contain full lines (from start to line break).", - keybindings: [ - { key: "Shift+Alt+x", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Trim whitespace at beginning and end of selections. - * - * Default key: `Shift+-` (`dance.mode == 'normal'`). - */ -export const trimSelections: ICommand<"dance.trimSelections"> = { - id : "dance.trimSelections", - title : "Trim selections", - description: "Trim whitespace at beginning and end of selections.", - keybindings: [ - { key: "Shift+-", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select the word and following whitespaces on the right of the end of each selection. - * - * Default key: `W` (`dance.mode == 'normal'`). - */ -export const selectWord: ICommand<"dance.select.word"> = { - id : "dance.select.word", - title : "Select to next word start", - description: "Select the word and following whitespaces on the right of the end of each selection.", - keybindings: [ - { key: "w", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select preceding whitespaces and the word on the left of the end of each selection. - * - * Default key: `B` (`dance.mode == 'normal'`). - */ -export const selectWordPrevious: ICommand<"dance.select.word.previous"> = { - id : "dance.select.word.previous", - title : "Select to previous word start", - description: "Select preceding whitespaces and the word on the left of the end of each selection.", - keybindings: [ - { key: "b", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select preceding whitespaces and the word on the right of the end of each selection. - * - * Default key: `E` (`dance.mode == 'normal'`). - */ -export const selectWordEnd: ICommand<"dance.select.word.end"> = { - id : "dance.select.word.end", - title : "Select to next word end", - description: "Select preceding whitespaces and the word on the right of the end of each selection.", - keybindings: [ - { key: "e", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select the non-whitespace word and following whitespaces on the right of the end of each selection. - * - * Default key: `Alt+W` (`dance.mode == 'normal'`). - */ -export const selectWordAlt: ICommand<"dance.select.word.alt"> = { - id : "dance.select.word.alt", - title : "Select to next non-whitespace word start", - description: "Select the non-whitespace word and following whitespaces on the right of the end of each selection.", - keybindings: [ - { key: "Alt+w", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select preceding whitespaces and the non-whitespace word on the left of the end of each selection. - * - * Default key: `Alt+B` (`dance.mode == 'normal'`). - */ -export const selectWordAltPrevious: ICommand<"dance.select.word.alt.previous"> = { - id : "dance.select.word.alt.previous", - title : "Select to previous non-whitespace word start", - description: "Select preceding whitespaces and the non-whitespace word on the left of the end of each selection.", - keybindings: [ - { key: "Alt+b", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select preceding whitespaces and the non-whitespace word on the right of the end of each selection. - * - * Default key: `Alt+E` (`dance.mode == 'normal'`). - */ -export const selectWordAltEnd: ICommand<"dance.select.word.alt.end"> = { - id : "dance.select.word.alt.end", - title : "Select to next non-whitespace word end", - description: "Select preceding whitespaces and the non-whitespace word on the right of the end of each selection.", - keybindings: [ - { key: "Alt+e", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select within current selections according to a RegExp. - * - * Default key: `S` (`dance.mode == 'normal'`). - */ -export const select: ICommand<"dance.select"> = { - id : "dance.select", - title : "Select", - description: "Select within current selections according to a RegExp.", - keybindings: [ - { key: "s", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Split within current selections according to a RegExp. - * - * Default key: `Shift+S` (`dance.mode == 'normal'`). - */ -export const split: ICommand<"dance.split"> = { - id : "dance.split", - title : "Split", - description: "Split within current selections according to a RegExp.", - keybindings: [ - { key: "Shift+s", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Split selections into lines. - * - * Default key: `Alt+S` (`dance.mode == 'normal'`). - */ -export const splitLines: ICommand<"dance.split.lines"> = { - id : "dance.split.lines", - title : "Split lines", - description: "Split selections into lines.", - keybindings: [ - { key: "Alt+s", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select first and last characters of each selection. - * - * Default key: `Shift+Alt+S` (`dance.mode == 'normal'`). - */ -export const selectFirstLast: ICommand<"dance.select.firstLast"> = { - id : "dance.select.firstLast", - title : "Select first and last characters", - description: "Select first and last characters of each selection.", - keybindings: [ - { key: "Shift+Alt+s", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Copy selection to next line. - * - * Default key: `Shift+C` (`dance.mode == 'normal'`). - */ -export const selectCopy: ICommand<"dance.select.copy"> = { - id : "dance.select.copy", - title : "Copy selection to next line", - description: "Copy selection to next line.", - keybindings: [ - { key: "Shift+c", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Copy selection to previous line. - * - * Default key: `Shift+Alt+C` (`dance.mode == 'normal'`). - */ -export const selectCopyBackwards: ICommand<"dance.select.copy.backwards"> = { - id : "dance.select.copy.backwards", - title : "Copy selection to previous line", - description: "Copy selection to previous line.", - keybindings: [ - { key: "Shift+Alt+c", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Reduce selections to their cursor. - * - * Default key: `;` (`dance.mode == 'normal'`). - */ -export const selectionsReduce: ICommand<"dance.selections.reduce"> = { - id : "dance.selections.reduce", - title : "Reduce selections", - description: "Reduce selections to their cursor.", - keybindings: [ - { key: ";", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Flip the direction of each selection. - * - * Default key: `Alt+;` (`dance.mode == 'normal'`). - */ -export const selectionsFlip: ICommand<"dance.selections.flip"> = { - id : "dance.selections.flip", - title : "Flip selections", - description: "Flip the direction of each selection.", - keybindings: [ - { key: "Alt+;", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Ensure selections are in forward direction (the active cursor is after the anchor). - * - * Default key: `Shift+Alt+;` (`dance.mode == 'normal'`). - */ -export const selectionsForward: ICommand<"dance.selections.forward"> = { - id : "dance.selections.forward", - title : "Forward selections", - description: "Ensure selections are in forward direction (the active cursor is after the anchor).", - keybindings: [ - { key: "Shift+Alt+;", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Ensure selections are in backward direction (the active cursor is before the anchor). - */ -export const selectionsBackward: ICommand<"dance.selections.backward"> = { - id : "dance.selections.backward", - title : "Backward selections", - description: "Ensure selections are in backward direction (the active cursor is before the anchor).", - keybindings: [], -}; - -/** - * Clear selections (except main) - * - * Default key: `Space` (`dance.mode == 'normal'`). - */ -export const selectionsClear: ICommand<"dance.selections.clear"> = { - id : "dance.selections.clear", - title : "Clear selections", - description: "Clear selections (except main)", - keybindings: [ - { key: "space", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Clear main selection. - * - * Default key: `Alt+Space` (`dance.mode == 'normal'`). - */ -export const selectionsClearMain: ICommand<"dance.selections.clearMain"> = { - id : "dance.selections.clearMain", - title : "Clear main selection", - description: "Clear main selection.", - keybindings: [ - { key: "Alt+space", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Keep selections that match a RegExp. - * - * Default key: `Alt+K` (`dance.mode == 'normal'`). - */ -export const selectionsKeepMatching: ICommand<"dance.selections.keepMatching"> = { - id : "dance.selections.keepMatching", - title : "Keep matching selections", - description: "Keep selections that match a RegExp.", - keybindings: [ - { key: "Alt+k", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Clear selections that match a RegExp. - * - * Default key: `Shift+Alt+K` (`dance.mode == 'normal'`). - */ -export const selectionsClearMatching: ICommand<"dance.selections.clearMatching"> = { - id : "dance.selections.clearMatching", - title : "Clear matching selections", - description: "Clear selections that match a RegExp.", - keybindings: [ - { key: "Shift+Alt+k", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Merge contiguous selections together, including across lines. - * - * Default key: `Shift+Alt+-` (`dance.mode == 'normal'`). - */ -export const selectionsMerge: ICommand<"dance.selections.merge"> = { - id : "dance.selections.merge", - title : "Merge contiguous selections", - description: "Merge contiguous selections together, including across lines.", - keybindings: [ - { key: "Shift+Alt+-", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Align selections, aligning the cursor of each selection by inserting spaces before the first character of each selection. - * - * Default key: `Shift+7` (`dance.mode == 'normal'`). - */ -export const selectionsAlign: ICommand<"dance.selections.align"> = { - id : "dance.selections.align", - title : "Align selections", - description: "Align selections, aligning the cursor of each selection by inserting spaces before the first character of each selection.", - keybindings: [ - { key: "Shift+7", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Copy the indentation of the main selection (or the count one if a count is given) to all other ones. - * - * Default key: `Shift+Alt+7` (`dance.mode == 'normal'`). - */ -export const selectionsAlignCopy: ICommand<"dance.selections.align.copy"> = { - id : "dance.selections.align.copy", - title : "Copy indentation", - description: "Copy the indentation of the main selection (or the count one if a count is given) to all other ones.", - keybindings: [ - { key: "Shift+Alt+7", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Yank and delete selections. - * - * Default key: `D` (`dance.mode == 'normal'`). - */ -export const deleteYank: ICommand<"dance.delete.yank"> = { - id : "dance.delete.yank", - title : "Yank and delete", - description: "Yank and delete selections.", - keybindings: [ - { key: "d", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Yank, delete and enter insert mode. - * - * Default key: `C` (`dance.mode == 'normal'`). - */ -export const deleteInsertYank: ICommand<"dance.delete.insert.yank"> = { - id : "dance.delete.insert.yank", - title : "Yank, delete and insert", - description: "Yank, delete and enter insert mode.", - keybindings: [ - { key: "c", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Delete selections without yanking. - * - * Default key: `Alt+D` (`dance.mode == 'normal'`). - */ -export const deleteNoYank: ICommand<"dance.delete.noYank"> = { - id : "dance.delete.noYank", - title : "Delete without yank", - description: "Delete selections without yanking.", - keybindings: [ - { key: "Alt+d", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Delete selections without yanking and enter insert mode. - * - * Default key: `Alt+C` (`dance.mode == 'normal'`). - */ -export const deleteInsertNoYank: ICommand<"dance.delete.insert.noYank"> = { - id : "dance.delete.insert.noYank", - title : "Delete and insert without yank", - description: "Delete selections without yanking and enter insert mode.", - keybindings: [ - { key: "Alt+c", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Yank selections. - * - * Default key: `Y` (`dance.mode == 'normal'`). - */ -export const yank: ICommand<"dance.yank"> = { - id : "dance.yank", - title : "Yank", - description: "Yank selections.", - keybindings: [ - { key: "y", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Paste after the end of each selection. - * - * Default key: `P` (`dance.mode == 'normal'`). - */ -export const pasteAfter: ICommand<"dance.paste.after"> = { - id : "dance.paste.after", - title : "Paste after", - description: "Paste after the end of each selection.", - keybindings: [ - { key: "p", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Paste before the start of each selection. - * - * Default key: `Shift+P` (`dance.mode == 'normal'`). - */ -export const pasteBefore: ICommand<"dance.paste.before"> = { - id : "dance.paste.before", - title : "Paste before", - description: "Paste before the start of each selection.", - keybindings: [ - { key: "Shift+p", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Paste after the end of each selection and select pasted text. - * - * Default key: `Alt+P` (`dance.mode == 'normal'`). - */ -export const pasteSelectAfter: ICommand<"dance.paste.select.after"> = { - id : "dance.paste.select.after", - title : "Paste after and select", - description: "Paste after the end of each selection and select pasted text.", - keybindings: [ - { key: "Alt+p", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Paste before the start of each selection and select pasted text. - * - * Default key: `Shift+Alt+P` (`dance.mode == 'normal'`). - */ -export const pasteSelectBefore: ICommand<"dance.paste.select.before"> = { - id : "dance.paste.select.before", - title : "Paste before and select", - description: "Paste before the start of each selection and select pasted text.", - keybindings: [ - { key: "Shift+Alt+p", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Replace selections with yanked text. - * - * Default key: `Shift+R` (`dance.mode == 'normal'`). - */ -export const pasteReplace: ICommand<"dance.paste.replace"> = { - id : "dance.paste.replace", - title : "Replace", - description: "Replace selections with yanked text.", - keybindings: [ - { key: "Shift+r", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Replace selections with every yanked text. - * - * Default key: `Shift+Alt+R` (`dance.mode == 'normal'`). - */ -export const pasteReplaceEvery: ICommand<"dance.paste.replace.every"> = { - id : "dance.paste.replace.every", - title : "Replace with every", - description: "Replace selections with every yanked text.", - keybindings: [ - { key: "Shift+Alt+r", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Replace each selected character with the next entered one. - * - * Default key: `R` (`dance.mode == 'normal'`). - */ -export const replaceCharacters: ICommand<"dance.replace.characters"> = { - id : "dance.replace.characters", - title : "Replace character", - description: "Replace each selected character with the next entered one.", - keybindings: [ - { key: "r", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Join selected lines. - * - * Default key: `Alt+J` (`dance.mode == 'normal'`). - */ -export const join: ICommand<"dance.join"> = { - id : "dance.join", - title : "Join lines", - description: "Join selected lines.", - keybindings: [ - { key: "Alt+j", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Join selected lines and select spaces inserted in place of line breaks. - * - * Default key: `Shift+Alt+J` (`dance.mode == 'normal'`). - */ -export const joinSelect: ICommand<"dance.join.select"> = { - id : "dance.join.select", - title : "Join lines and select spaces", - description: "Join selected lines and select spaces inserted in place of line breaks.", - keybindings: [ - { key: "Shift+Alt+j", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Indent selected lines. - * - * Default key: `Shift+.` (`dance.mode == 'normal'`). - */ -export const indent: ICommand<"dance.indent"> = { - id : "dance.indent", - title : "Indent", - description: "Indent selected lines.", - keybindings: [ - { key: "Shift+.", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Indent selected lines (including empty lines). - * - * Default key: `Shift+Alt+.` (`dance.mode == 'normal'`). - */ -export const indentWithEmpty: ICommand<"dance.indent.withEmpty"> = { - id : "dance.indent.withEmpty", - title : "Indent (including empty)", - description: "Indent selected lines (including empty lines).", - keybindings: [ - { key: "Shift+Alt+.", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Deindent selected lines. - * - * Default key: `Shift+Alt+,` (`dance.mode == 'normal'`). - */ -export const deindent: ICommand<"dance.deindent"> = { - id : "dance.deindent", - title : "Deindent", - description: "Deindent selected lines.", - keybindings: [ - { key: "Shift+Alt+,", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Deindent selected lines (and remove additional incomplete indent). - * - * Default key: `Shift+,` (`dance.mode == 'normal'`). - */ -export const deindentFurther: ICommand<"dance.deindent.further"> = { - id : "dance.deindent.further", - title : "Deindent (including incomplete indent)", - description: "Deindent selected lines (and remove additional incomplete indent).", - keybindings: [ - { key: "Shift+,", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Transform to lowercase. - * - * Default key: ``` (`dance.mode == 'normal'`). - */ -export const toLowerCase: ICommand<"dance.toLowerCase"> = { - id : "dance.toLowerCase", - title : "Transform to lowercase", - description: "Transform to lowercase.", - keybindings: [ - { key: "`", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Transform to uppercase. - * - * Default key: `Shift+`` (`dance.mode == 'normal'`). - */ -export const toUpperCase: ICommand<"dance.toUpperCase"> = { - id : "dance.toUpperCase", - title : "Transform to uppercase", - description: "Transform to uppercase.", - keybindings: [ - { key: "Shift+`", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Swap case. - * - * Default key: `Alt+`` (`dance.mode == 'normal'`). - */ -export const swapCase: ICommand<"dance.swapCase"> = { - id : "dance.swapCase", - title : "Swap case", - description: "Swap case.", - keybindings: [ - { key: "Alt+`", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Pipe each selection to a program, and keeps it if the program returns 0. - * - * Default key: `Shift+4` (`dance.mode == 'normal'`). - */ -export const pipeFilter: ICommand<"dance.pipe.filter"> = { - id : "dance.pipe.filter", - title : "Filter through pipe", - description: "Pipe each selection to a program, and keeps it if the program returns 0.", - keybindings: [ - { key: "Shift+4", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Pipe each selection to a command, and replaces it with its output. - * - * Default key: `Shift+\` (`dance.mode == 'normal'`). - */ -export const pipeReplace: ICommand<"dance.pipe.replace"> = { - id : "dance.pipe.replace", - title : "Pipe and replace", - description: "Pipe each selection to a command, and replaces it with its output.", - keybindings: [ - { key: "Shift+\\", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Pipe each selection to a command, ignoring their results. - * - * Default key: `Shift+Alt+\` (`dance.mode == 'normal'`). - */ -export const pipeIgnore: ICommand<"dance.pipe.ignore"> = { - id : "dance.pipe.ignore", - title : "Pipe", - description: "Pipe each selection to a command, ignoring their results.", - keybindings: [ - { key: "Shift+Alt+\\", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Pipe each selection to a command, appending the output after the selection. - * - * Default key: `Shift+1` (`dance.mode == 'normal'`). - */ -export const pipeAppend: ICommand<"dance.pipe.append"> = { - id : "dance.pipe.append", - title : "Pipe and append", - description: "Pipe each selection to a command, appending the output after the selection.", - keybindings: [ - { key: "Shift+1", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Pipe each selection to a command, prepending the output before the selection. - * - * Default key: `Shift+Alt+1` (`dance.mode == 'normal'`). - */ -export const pipePrepend: ICommand<"dance.pipe.prepend"> = { - id : "dance.pipe.prepend", - title : "Pipe and prepend", - description: "Pipe each selection to a command, prepending the output before the selection.", - keybindings: [ - { key: "Shift+Alt+1", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Undo. - * - * Default key: `U` (`dance.mode == 'normal'`). - */ -export const historyUndo: ICommand<"dance.history.undo"> = { - id : "dance.history.undo", - title : "Undo", - description: "Undo.", - keybindings: [ - { key: "u", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Move backward in history. - * - * Default key: `Alt+U` (`dance.mode == 'normal'`). - */ -export const historyBackward: ICommand<"dance.history.backward"> = { - id : "dance.history.backward", - title : "Move backward in history", - description: "Move backward in history.", - keybindings: [ - { key: "Alt+u", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Redo. - * - * Default key: `Shift+U` (`dance.mode == 'normal'`). - */ -export const historyRedo: ICommand<"dance.history.redo"> = { - id : "dance.history.redo", - title : "Redo", - description: "Redo.", - keybindings: [ - { key: "Shift+u", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Move forward in history. - * - * Default key: `Shift+Alt+U` (`dance.mode == 'normal'`). - */ -export const historyForward: ICommand<"dance.history.forward"> = { - id : "dance.history.forward", - title : "Move forward in history", - description: "Move forward in history.", - keybindings: [ - { key: "Shift+Alt+u", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Repeat last change. - */ -export const historyRepeat: ICommand<"dance.history.repeat"> = { - id : "dance.history.repeat", - title : "Repeat last change", - description: "Repeat last change.", - keybindings: [], -}; - -/** - * Repeat last selection change. - */ -export const historyRepeatSelection: ICommand<"dance.history.repeat.selection"> = { - id : "dance.history.repeat.selection", - title : "Repeat last selection change", - description: "Repeat last selection change.", - keybindings: [], -}; - -/** - * Repeat last edit change. - */ -export const historyRepeatEdit: ICommand<"dance.history.repeat.edit"> = { - id : "dance.history.repeat.edit", - title : "Repeat last edit change", - description: "Repeat last edit change.", - keybindings: [], -}; - -/** - * Start recording macro. - * - * Default key: `Shift+Q` (`dance.mode == 'normal' && !dance.recordingMacro`). - */ -export const macrosRecordStart: ICommand<"dance.macros.record.start"> = { - id : "dance.macros.record.start", - title : "Start recording macro", - description: "Start recording macro.", - keybindings: [ - { key: "Shift+q", when: "editorTextFocus && dance.mode == 'normal' && !dance.recordingMacro" }, - ], -}; - -/** - * Stop recording macro. - * - * Default key: `Escape` (`dance.mode == 'normal'`). - */ -export const macrosRecordStop: ICommand<"dance.macros.record.stop"> = { - id : "dance.macros.record.stop", - title : "Stop recording macro", - description: "Stop recording macro.", - keybindings: [ - { key: "escape", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Play macro. - * - * Default key: `Q` (`dance.mode == 'normal'`). - */ -export const macrosPlay: ICommand<"dance.macros.play"> = { - id : "dance.macros.play", - title : "Play macro", - description: "Play macro.", - keybindings: [ - { key: "q", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Rotate each selection clockwise. - * - * Default key: `Shift+9` (`dance.mode == 'normal'`). - */ -export const rotate: ICommand<"dance.rotate"> = { - id : "dance.rotate", - title : "Rotate", - description: "Rotate each selection clockwise.", - keybindings: [ - { key: "Shift+9", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Rotate each selection counter-clockwise. - * - * Default key: `Shift+0` (`dance.mode == 'normal'`). - */ -export const rotateBackwards: ICommand<"dance.rotate.backwards"> = { - id : "dance.rotate.backwards", - title : "Rotate backwards", - description: "Rotate each selection counter-clockwise.", - keybindings: [ - { key: "Shift+0", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Rotate each selection (as well as its content) clockwise. - * - * Default key: `Shift+Alt+9` (`dance.mode == 'normal'`). - */ -export const rotateContent: ICommand<"dance.rotate.content"> = { - id : "dance.rotate.content", - title : "Rotate selection content", - description: "Rotate each selection (as well as its content) clockwise.", - keybindings: [ - { key: "Shift+Alt+9", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Rotate each selection (as well as its content) counter-clockwise. - * - * Default key: `Shift+Alt+0` (`dance.mode == 'normal'`). - */ -export const rotateContentBackwards: ICommand<"dance.rotate.content.backwards"> = { - id : "dance.rotate.content.backwards", - title : "Rotate selection content backwards", - description: "Rotate each selection (as well as its content) counter-clockwise.", - keybindings: [ - { key: "Shift+Alt+0", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Rotate each selection content clockwise, without changing selections. - */ -export const rotateContentOnly: ICommand<"dance.rotate.contentOnly"> = { - id : "dance.rotate.contentOnly", - title : "Rotate content only", - description: "Rotate each selection content clockwise, without changing selections.", - keybindings: [], -}; - -/** - * Rotate each selection content counter-clockwise, without changing selections. - */ -export const rotateContentOnlyBackwards: ICommand<"dance.rotate.contentOnly.backwards"> = { - id : "dance.rotate.contentOnly.backwards", - title : "Rotate content only backwards", - description: "Rotate each selection content counter-clockwise, without changing selections.", - keybindings: [], -}; - -/** - * Search for the given input string. - * - * Default key: `/` (`dance.mode == 'normal'`). - */ -export const search: ICommand<"dance.search"> = { - id : "dance.search", - title : "Search", - description: "Search for the given input string.", - keybindings: [ - { key: "/", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Search for the given input string before the current selections. - * - * Default key: `Alt+/` (`dance.mode == 'normal'`). - */ -export const searchBackwards: ICommand<"dance.search.backwards"> = { - id : "dance.search.backwards", - title : "Search backwards", - description: "Search for the given input string before the current selections.", - keybindings: [ - { key: "Alt+/", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Search current selections (smart). - * - * Default key: `Shift+8` (`dance.mode == 'normal'`). - */ -export const searchSelectionSmart: ICommand<"dance.search.selection.smart"> = { - id : "dance.search.selection.smart", - title : "Search current selections (smart)", - description: "Search current selections (smart).", - keybindings: [ - { key: "Shift+8", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Search current selections. - * - * Default key: `Shift+Alt+8` (`dance.mode == 'normal'`). - */ -export const searchSelection: ICommand<"dance.search.selection"> = { - id : "dance.search.selection", - title : "Search current selections", - description: "Search current selections.", - keybindings: [ - { key: "Shift+Alt+8", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select next match after the main selection. - * - * Default key: `N` (`dance.mode == 'normal'`). - */ -export const searchNext: ICommand<"dance.search.next"> = { - id : "dance.search.next", - title : "Select next match", - description: "Select next match after the main selection.", - keybindings: [ - { key: "n", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Add a new selection with the next match after the main selection. - * - * Default key: `Shift+N` (`dance.mode == 'normal'`). - */ -export const searchNextAdd: ICommand<"dance.search.next.add"> = { - id : "dance.search.next.add", - title : "Add next match", - description: "Add a new selection with the next match after the main selection.", - keybindings: [ - { key: "Shift+n", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select previous match before the main selection. - * - * Default key: `Alt+N` (`dance.mode == 'normal'`). - */ -export const searchPrevious: ICommand<"dance.search.previous"> = { - id : "dance.search.previous", - title : "Select previous match", - description: "Select previous match before the main selection.", - keybindings: [ - { key: "Alt+n", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Add a new selection with the previous match before the main selection. - * - * Default key: `Shift+Alt+N` (`dance.mode == 'normal'`). - */ -export const searchPreviousAdd: ICommand<"dance.search.previous.add"> = { - id : "dance.search.previous.add", - title : "Add previous match", - description: "Add a new selection with the previous match before the main selection.", - keybindings: [ - { key: "Shift+Alt+n", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Perform selections specified in the arguments.. - */ -export const objectsPerformSelection: ICommand<"dance.objects.performSelection"> = { - id : "dance.objects.performSelection", - title : "Perform selections specified in the arguments.", - description: "Perform selections specified in the arguments..", - keybindings: [], -}; - -/** - * Shows prompt to jump somewhere - * - * Default key: `G` (`dance.mode == 'normal'`). - */ -export const goto: ICommand<"dance.goto"> = { - id : "dance.goto", - title : "Go to...", - description: "Shows prompt to jump somewhere", - keybindings: [ - { key: "g", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Go to line start. - */ -export const gotoLineStart: ICommand<"dance.goto.lineStart"> = { - id : "dance.goto.lineStart", - title : "Go to line start", - description: "Go to line start.", - keybindings: [], -}; - -/** - * Go to first non-whitespace character of the line - */ -export const gotoLineStartNonBlank: ICommand<"dance.goto.lineStart.nonBlank"> = { - id : "dance.goto.lineStart.nonBlank", - title : "Go to non-blank line start", - description: "Go to first non-whitespace character of the line", - keybindings: [], -}; - -/** - * Go to line end. - */ -export const gotoLineEnd: ICommand<"dance.goto.lineEnd"> = { - id : "dance.goto.lineEnd", - title : "Go to line end", - description: "Go to line end.", - keybindings: [], -}; - -/** - * Go to first line. - */ -export const gotoFirstLine: ICommand<"dance.goto.firstLine"> = { - id : "dance.goto.firstLine", - title : "Go to first line", - description: "Go to first line.", - keybindings: [], -}; - -/** - * Go to last line. - */ -export const gotoLastLine: ICommand<"dance.goto.lastLine"> = { - id : "dance.goto.lastLine", - title : "Go to last line", - description: "Go to last line.", - keybindings: [], -}; - -/** - * Go to last character of the document. - */ -export const gotoLastCharacter: ICommand<"dance.goto.lastCharacter"> = { - id : "dance.goto.lastCharacter", - title : "Go to last character of the document", - description: "Go to last character of the document.", - keybindings: [], -}; - -/** - * Go to first visible line. - */ -export const gotoFirstVisibleLine: ICommand<"dance.goto.firstVisibleLine"> = { - id : "dance.goto.firstVisibleLine", - title : "Go to first visible line", - description: "Go to first visible line.", - keybindings: [], -}; - -/** - * Go to middle visible line. - */ -export const gotoMiddleVisibleLine: ICommand<"dance.goto.middleVisibleLine"> = { - id : "dance.goto.middleVisibleLine", - title : "Go to middle visible line", - description: "Go to middle visible line.", - keybindings: [], -}; - -/** - * Go to last visible line. - */ -export const gotoLastVisibleLine: ICommand<"dance.goto.lastVisibleLine"> = { - id : "dance.goto.lastVisibleLine", - title : "Go to last visible line", - description: "Go to last visible line.", - keybindings: [], -}; - -/** - * Open file under selection. - */ -export const gotoSelectedFile: ICommand<"dance.goto.selectedFile"> = { - id : "dance.goto.selectedFile", - title : "Open file under selection", - description: "Open file under selection.", - keybindings: [], -}; - -/** - * Go to last buffer modification position. - */ -export const gotoLastModification: ICommand<"dance.goto.lastModification"> = { - id : "dance.goto.lastModification", - title : "Go to last buffer modification position", - description: "Go to last buffer modification position.", - keybindings: [], -}; - -/** - * Open quick-jump menu. - */ -export const openMenu: ICommand<"dance.openMenu"> = { - id : "dance.openMenu", - title : "Open quick-jump menu", - description: "Open quick-jump menu.", - keybindings: [], -}; - -/** - * Insert value in register. - * - * Default keys: `Ctrl+R` (`dance.mode == 'normal'`), `Ctrl+R` (`dance.mode == 'insert'`). - */ -export const registersInsert: ICommand<"dance.registers.insert"> = { - id : "dance.registers.insert", - title : "Insert value in register", - description: "Insert value in register.", - keybindings: [ - { key: "Ctrl+r", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Ctrl+r", when: "editorTextFocus && dance.mode == 'insert'" }, - ], -}; - -/** - * Select register for next command. - * - * Default key: `Shift+\'` (`dance.mode == 'normal'`). - */ -export const registersSelect: ICommand<"dance.registers.select"> = { - id : "dance.registers.select", - title : "Select register for next command", - description: "Select register for next command.", - keybindings: [ - { key: "Shift+\\'", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Save selections. - * - * Default key: `Shift+Z` (`dance.mode == 'normal'`). - */ -export const marksSaveSelections: ICommand<"dance.marks.saveSelections"> = { - id : "dance.marks.saveSelections", - title : "Save selections", - description: "Save selections.", - keybindings: [ - { key: "Shift+z", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Restore selections. - * - * Default key: `Z` (`dance.mode == 'normal'`). - */ -export const marksRestoreSelections: ICommand<"dance.marks.restoreSelections"> = { - id : "dance.marks.restoreSelections", - title : "Restore selections", - description: "Restore selections.", - keybindings: [ - { key: "z", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Combine current selections with ones from register. - * - * Default key: `Shift+Alt+Z` (`dance.mode == 'normal'`). - */ -export const marksCombineSelectionsFromCurrent: ICommand<"dance.marks.combineSelections.fromCurrent"> = { - id : "dance.marks.combineSelections.fromCurrent", - title : "Combine current selections with ones from register", - description: "Combine current selections with ones from register.", - keybindings: [ - { key: "Shift+Alt+z", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Combine register selections with current ones. - * - * Default key: `Alt+Z` (`dance.mode == 'normal'`). - */ -export const marksCombineSelectionsFromRegister: ICommand<"dance.marks.combineSelections.fromRegister"> = { - id : "dance.marks.combineSelections.fromRegister", - title : "Combine register selections with current ones", - description: "Combine register selections with current ones.", - keybindings: [ - { key: "Alt+z", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Cancels waiting for input from the user - * - * Default key: `Escape` (`dance.mode == 'awaiting'`). - */ -export const cancel: ICommand<"dance.cancel"> = { - id : "dance.cancel", - title : "Cancel operation", - description: "Cancels waiting for input from the user", - keybindings: [ - { key: "escape", when: "editorTextFocus && dance.mode == 'awaiting'" }, - ], -}; - -/** - * Runs JavaScript code passed in a 'code' argument - */ -export const run: ICommand<"dance.run"> = { - id : "dance.run", - title : "Run code", - description: "Runs JavaScript code passed in a 'code' argument", - keybindings: [], -}; - -/** - * Move left (extend). - * - * Default keys: `Shift+Left` (`dance.mode == 'normal'`), `Shift+H` (`dance.mode == 'normal'`). - */ -export const leftExtend: ICommand<"dance.left.extend"> = { - id : "dance.left.extend", - title : "Move left (extend)", - description: "Move left (extend).", - keybindings: [ - { key: "Shift+left", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Shift+h", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Move right (extend). - * - * Default keys: `Shift+Right` (`dance.mode == 'normal'`), `Shift+L` (`dance.mode == 'normal'`). - */ -export const rightExtend: ICommand<"dance.right.extend"> = { - id : "dance.right.extend", - title : "Move right (extend)", - description: "Move right (extend).", - keybindings: [ - { key: "Shift+right", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Shift+l", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Move up (extend). - * - * Default keys: `Shift+Up` (`dance.mode == 'normal'`), `Shift+K` (`dance.mode == 'normal'`). - */ -export const upExtend: ICommand<"dance.up.extend"> = { - id : "dance.up.extend", - title : "Move up (extend)", - description: "Move up (extend).", - keybindings: [ - { key: "Shift+up", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Shift+k", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Move down (extend). - * - * Default keys: `Shift+Down` (`dance.mode == 'normal'`), `Shift+J` (`dance.mode == 'normal'`). - */ -export const downExtend: ICommand<"dance.down.extend"> = { - id : "dance.down.extend", - title : "Move down (extend)", - description: "Move down (extend).", - keybindings: [ - { key: "Shift+down", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Shift+j", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend to the next character pressed, including it. - * - * Default key: `Shift+F` (`dance.mode == 'normal'`). - */ -export const selectToIncludedExtend: ICommand<"dance.select.to.included.extend"> = { - id : "dance.select.to.included.extend", - title : "Extend to", - description: "Extend to the next character pressed, including it.", - keybindings: [ - { key: "Shift+f", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with until the next character pressed, excluding it. - * - * Default key: `Shift+T` (`dance.mode == 'normal'`). - */ -export const selectToExcludedExtend: ICommand<"dance.select.to.excluded.extend"> = { - id : "dance.select.to.excluded.extend", - title : "Extend until", - description: "Extend with until the next character pressed, excluding it.", - keybindings: [ - { key: "Shift+t", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with line on which the end of each selection lies (or next line when end lies on an end-of-line). - * - * Default key: `Shift+X` (`dance.mode == 'normal'`). - */ -export const selectLineExtend: ICommand<"dance.select.line.extend"> = { - id : "dance.select.line.extend", - title : "Extend with line", - description: "Extend with line on which the end of each selection lies (or next line when end lies on an end-of-line).", - keybindings: [ - { key: "Shift+x", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend to line beginning. - * - * Default keys: `Shift+Alt+H` (`dance.mode == 'normal'`), `Shift+Home` (`dance.mode == 'normal'`). - */ -export const selectToLineBeginExtend: ICommand<"dance.select.toLineBegin.extend"> = { - id : "dance.select.toLineBegin.extend", - title : "Extend to line beginning", - description: "Extend to line beginning.", - keybindings: [ - { key: "Shift+Alt+h", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Shift+home", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend to line end. - * - * Default keys: `Shift+Alt+L` (`dance.mode == 'normal'`), `Shift+End` (`dance.mode == 'normal'`). - */ -export const selectToLineEndExtend: ICommand<"dance.select.toLineEnd.extend"> = { - id : "dance.select.toLineEnd.extend", - title : "Extend to line end", - description: "Extend to line end.", - keybindings: [ - { key: "Shift+Alt+l", when: "editorTextFocus && dance.mode == 'normal'" }, - { key: "Shift+end", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with enclosing characters. - * - * Default key: `Shift+M` (`dance.mode == 'normal'`). - */ -export const selectEnclosingExtend: ICommand<"dance.select.enclosing.extend"> = { - id : "dance.select.enclosing.extend", - title : "Extend with enclosing characters", - description: "Extend with enclosing characters.", - keybindings: [ - { key: "Shift+m", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with the word and following whitespaces on the right of the end of each selection. - * - * Default key: `Shift+W` (`dance.mode == 'normal'`). - */ -export const selectWordExtend: ICommand<"dance.select.word.extend"> = { - id : "dance.select.word.extend", - title : "Extend to next word start", - description: "Extend with the word and following whitespaces on the right of the end of each selection.", - keybindings: [ - { key: "Shift+w", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with preceding whitespaces and the word on the left of the end of each selection. - * - * Default key: `Shift+B` (`dance.mode == 'normal'`). - */ -export const selectWordPreviousExtend: ICommand<"dance.select.word.previous.extend"> = { - id : "dance.select.word.previous.extend", - title : "Extend to previous word start", - description: "Extend with preceding whitespaces and the word on the left of the end of each selection.", - keybindings: [ - { key: "Shift+b", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with preceding whitespaces and the word on the right of the end of each selection. - * - * Default key: `Shift+E` (`dance.mode == 'normal'`). - */ -export const selectWordEndExtend: ICommand<"dance.select.word.end.extend"> = { - id : "dance.select.word.end.extend", - title : "Extend to next word end", - description: "Extend with preceding whitespaces and the word on the right of the end of each selection.", - keybindings: [ - { key: "Shift+e", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with the non-whitespace word and following whitespaces on the right of the end of each selection. - * - * Default key: `Shift+Alt+W` (`dance.mode == 'normal'`). - */ -export const selectWordAltExtend: ICommand<"dance.select.word.alt.extend"> = { - id : "dance.select.word.alt.extend", - title : "Extend to next non-whitespace word start", - description: "Extend with the non-whitespace word and following whitespaces on the right of the end of each selection.", - keybindings: [ - { key: "Shift+Alt+w", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with preceding whitespaces and the non-whitespace word on the left of the end of each selection. - * - * Default key: `Shift+Alt+B` (`dance.mode == 'normal'`). - */ -export const selectWordAltPreviousExtend: ICommand<"dance.select.word.alt.previous.extend"> = { - id : "dance.select.word.alt.previous.extend", - title : "Extend to previous non-whitespace word start", - description: "Extend with preceding whitespaces and the non-whitespace word on the left of the end of each selection.", - keybindings: [ - { key: "Shift+Alt+b", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with preceding whitespaces and the non-whitespace word on the right of the end of each selection. - * - * Default key: `Shift+Alt+E` (`dance.mode == 'normal'`). - */ -export const selectWordAltEndExtend: ICommand<"dance.select.word.alt.end.extend"> = { - id : "dance.select.word.alt.end.extend", - title : "Extend to next non-whitespace word end", - description: "Extend with preceding whitespaces and the non-whitespace word on the right of the end of each selection.", - keybindings: [ - { key: "Shift+Alt+e", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Search for the given input string (extend). - * - * Default key: `Shift+/` (`dance.mode == 'normal'`). - */ -export const searchExtend: ICommand<"dance.search.extend"> = { - id : "dance.search.extend", - title : "Search (extend)", - description: "Search for the given input string (extend).", - keybindings: [ - { key: "Shift+/", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Search for the given input string before the current selections (extend). - * - * Default key: `Shift+Alt+/` (`dance.mode == 'normal'`). - */ -export const searchBackwardsExtend: ICommand<"dance.search.backwards.extend"> = { - id : "dance.search.backwards.extend", - title : "Search backwards (extend)", - description: "Search for the given input string before the current selections (extend).", - keybindings: [ - { key: "Shift+Alt+/", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Shows prompt to jump somewhere - * - * Default key: `Shift+G` (`dance.mode == 'normal'`). - */ -export const gotoExtend: ICommand<"dance.goto.extend"> = { - id : "dance.goto.extend", - title : "Go to... (extend)", - description: "Shows prompt to jump somewhere", - keybindings: [ - { key: "Shift+g", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Go to line start (extend). - */ -export const gotoLineStartExtend: ICommand<"dance.goto.lineStart.extend"> = { - id : "dance.goto.lineStart.extend", - title : "Go to line start (extend)", - description: "Go to line start (extend).", - keybindings: [], -}; - -/** - * Go to first non-whitespace character of the line - */ -export const gotoLineStartNonBlankExtend: ICommand<"dance.goto.lineStart.nonBlank.extend"> = { - id : "dance.goto.lineStart.nonBlank.extend", - title : "Go to non-blank line start (extend)", - description: "Go to first non-whitespace character of the line", - keybindings: [], -}; - -/** - * Go to line end (extend). - */ -export const gotoLineEndExtend: ICommand<"dance.goto.lineEnd.extend"> = { - id : "dance.goto.lineEnd.extend", - title : "Go to line end (extend)", - description: "Go to line end (extend).", - keybindings: [], -}; - -/** - * Go to first line (extend). - */ -export const gotoFirstLineExtend: ICommand<"dance.goto.firstLine.extend"> = { - id : "dance.goto.firstLine.extend", - title : "Go to first line (extend)", - description: "Go to first line (extend).", - keybindings: [], -}; - -/** - * Go to last line (extend). - */ -export const gotoLastLineExtend: ICommand<"dance.goto.lastLine.extend"> = { - id : "dance.goto.lastLine.extend", - title : "Go to last line (extend)", - description: "Go to last line (extend).", - keybindings: [], -}; - -/** - * Go to last character of the document (extend). - */ -export const gotoLastCharacterExtend: ICommand<"dance.goto.lastCharacter.extend"> = { - id : "dance.goto.lastCharacter.extend", - title : "Go to last character of the document (extend)", - description: "Go to last character of the document (extend).", - keybindings: [], -}; - -/** - * Go to first visible line (extend). - */ -export const gotoFirstVisibleLineExtend: ICommand<"dance.goto.firstVisibleLine.extend"> = { - id : "dance.goto.firstVisibleLine.extend", - title : "Go to first visible line (extend)", - description: "Go to first visible line (extend).", - keybindings: [], -}; - -/** - * Go to middle visible line (extend). - */ -export const gotoMiddleVisibleLineExtend: ICommand<"dance.goto.middleVisibleLine.extend"> = { - id : "dance.goto.middleVisibleLine.extend", - title : "Go to middle visible line (extend)", - description: "Go to middle visible line (extend).", - keybindings: [], -}; - -/** - * Go to last visible line (extend). - */ -export const gotoLastVisibleLineExtend: ICommand<"dance.goto.lastVisibleLine.extend"> = { - id : "dance.goto.lastVisibleLine.extend", - title : "Go to last visible line (extend)", - description: "Go to last visible line (extend).", - keybindings: [], -}; - -/** - * Go to last buffer modification position (extend). - */ -export const gotoLastModificationExtend: ICommand<"dance.goto.lastModification.extend"> = { - id : "dance.goto.lastModification.extend", - title : "Go to last buffer modification position (extend)", - description: "Go to last buffer modification position (extend).", - keybindings: [], -}; - -/** - * Select to the next character pressed, including it. (backwards) - * - * Default key: `Alt+F` (`dance.mode == 'normal'`). - */ -export const selectToIncludedBackwards: ICommand<"dance.select.to.included.backwards"> = { - id : "dance.select.to.included.backwards", - title : "Select to (backwards)", - description: "Select to the next character pressed, including it. (backwards)", - keybindings: [ - { key: "Alt+f", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select until the next character pressed, excluding it. (backwards) - * - * Default key: `Alt+T` (`dance.mode == 'normal'`). - */ -export const selectToExcludedBackwards: ICommand<"dance.select.to.excluded.backwards"> = { - id : "dance.select.to.excluded.backwards", - title : "Select until (backwards)", - description: "Select until the next character pressed, excluding it. (backwards)", - keybindings: [ - { key: "Alt+t", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Select enclosing characters. (backwards) - * - * Default key: `Alt+M` (`dance.mode == 'normal'`). - */ -export const selectEnclosingBackwards: ICommand<"dance.select.enclosing.backwards"> = { - id : "dance.select.enclosing.backwards", - title : "Select enclosing characters (backwards)", - description: "Select enclosing characters. (backwards)", - keybindings: [ - { key: "Alt+m", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend to the next character pressed, including it. (backwards) - * - * Default key: `Alt+Shift+F` (`dance.mode == 'normal'`). - */ -export const selectToIncludedExtendBackwards: ICommand<"dance.select.to.included.extend.backwards"> = { - id : "dance.select.to.included.extend.backwards", - title : "Extend to (backwards)", - description: "Extend to the next character pressed, including it. (backwards)", - keybindings: [ - { key: "Alt+Shift+f", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with until the next character pressed, excluding it. (backwards) - * - * Default key: `Alt+Shift+T` (`dance.mode == 'normal'`). - */ -export const selectToExcludedExtendBackwards: ICommand<"dance.select.to.excluded.extend.backwards"> = { - id : "dance.select.to.excluded.extend.backwards", - title : "Extend until (backwards)", - description: "Extend with until the next character pressed, excluding it. (backwards)", - keybindings: [ - { key: "Alt+Shift+t", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Extend with enclosing characters. (backwards) - * - * Default key: `Alt+Shift+M` (`dance.mode == 'normal'`). - */ -export const selectEnclosingExtendBackwards: ICommand<"dance.select.enclosing.extend.backwards"> = { - id : "dance.select.enclosing.extend.backwards", - title : "Extend with enclosing characters (backwards)", - description: "Extend with enclosing characters. (backwards)", - keybindings: [ - { key: "Alt+Shift+m", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 0 to the current counter for the next operation. - * - * Default key: `0` (`dance.mode == 'normal'`). - */ -export const count0: ICommand<"dance.count.0"> = { - id : "dance.count.0", - title : "Count 0", - description: "Adds 0 to the current counter for the next operation.", - keybindings: [ - { key: "0", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 1 to the current counter for the next operation. - * - * Default key: `1` (`dance.mode == 'normal'`). - */ -export const count1: ICommand<"dance.count.1"> = { - id : "dance.count.1", - title : "Count 1", - description: "Adds 1 to the current counter for the next operation.", - keybindings: [ - { key: "1", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 2 to the current counter for the next operation. - * - * Default key: `2` (`dance.mode == 'normal'`). - */ -export const count2: ICommand<"dance.count.2"> = { - id : "dance.count.2", - title : "Count 2", - description: "Adds 2 to the current counter for the next operation.", - keybindings: [ - { key: "2", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 3 to the current counter for the next operation. - * - * Default key: `3` (`dance.mode == 'normal'`). - */ -export const count3: ICommand<"dance.count.3"> = { - id : "dance.count.3", - title : "Count 3", - description: "Adds 3 to the current counter for the next operation.", - keybindings: [ - { key: "3", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 4 to the current counter for the next operation. - * - * Default key: `4` (`dance.mode == 'normal'`). - */ -export const count4: ICommand<"dance.count.4"> = { - id : "dance.count.4", - title : "Count 4", - description: "Adds 4 to the current counter for the next operation.", - keybindings: [ - { key: "4", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 5 to the current counter for the next operation. - * - * Default key: `5` (`dance.mode == 'normal'`). - */ -export const count5: ICommand<"dance.count.5"> = { - id : "dance.count.5", - title : "Count 5", - description: "Adds 5 to the current counter for the next operation.", - keybindings: [ - { key: "5", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 6 to the current counter for the next operation. - * - * Default key: `6` (`dance.mode == 'normal'`). - */ -export const count6: ICommand<"dance.count.6"> = { - id : "dance.count.6", - title : "Count 6", - description: "Adds 6 to the current counter for the next operation.", - keybindings: [ - { key: "6", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 7 to the current counter for the next operation. - * - * Default key: `7` (`dance.mode == 'normal'`). - */ -export const count7: ICommand<"dance.count.7"> = { - id : "dance.count.7", - title : "Count 7", - description: "Adds 7 to the current counter for the next operation.", - keybindings: [ - { key: "7", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 8 to the current counter for the next operation. - * - * Default key: `8` (`dance.mode == 'normal'`). - */ -export const count8: ICommand<"dance.count.8"> = { - id : "dance.count.8", - title : "Count 8", - description: "Adds 8 to the current counter for the next operation.", - keybindings: [ - { key: "8", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - -/** - * Adds 9 to the current counter for the next operation. - * - * Default key: `9` (`dance.mode == 'normal'`). - */ -export const count9: ICommand<"dance.count.9"> = { - id : "dance.count.9", - title : "Count 9", - description: "Adds 9 to the current counter for the next operation.", - keybindings: [ - { key: "9", when: "editorTextFocus && dance.mode == 'normal'" }, - ], -}; - - -/** - * All defined commands. - */ -export const commands = { - /** Toggles Dance key bindings. */ - toggle, - /** Set Dance mode to Normal. */ - setNormal, - /** Set Dance mode to Insert. */ - setInsert, - /** Switches to normal mode temporarily. */ - tmpNormal, - /** Switches to insert mode temporarily. */ - tmpInsert, - /** Start insert before the current selections. */ - insertBefore, - /** Start insert after the current selections. */ - insertAfter, - /** Start insert at line start of each selection. */ - insertLineStart, - /** Start insert at line end of each selection. */ - insertLineEnd, - /** Create new line and start insert below. */ - insertNewLineBelow, - /** Create new line and start insert above. */ - insertNewLineAbove, - /** Add a new line below, without entering insert mode. */ - newLineBelow, - /** Add a new line above, without entering insert mode. */ - newLineAbove, - /** Repeat last insert-mode change. */ - repeatInsert, - /** Repeat last object select / character find. */ - repeatObjectOrSelectTo, - /** Move left. */ - left, - /** Move right. */ - right, - /** Move up. */ - up, - /** Move down. */ - down, - /** Scroll one page up. */ - upPage, - /** Scroll one page down. */ - downPage, - /** Scroll half a page up. */ - upHalfPage, - /** Scroll half a page down. */ - downHalfPage, - /** Select to the next character pressed, including it. */ - selectToIncluded, - /** Select until the next character pressed, excluding it. */ - selectToExcluded, - /** Select whole buffer. */ - selectBuffer, - /** Select line on which the end of each selection lies (or next line when end lies on an end-of-line). */ - selectLine, - /** Select to line beginning. */ - selectToLineBegin, - /** Select to line end. */ - selectToLineEnd, - /** Select enclosing characters. */ - selectEnclosing, - /** Extend selections to contain full lines (including end-of-lines). */ - expandLines, - /** Trim selections to only contain full lines (from start to line break). */ - trimLines, - /** Trim whitespace at beginning and end of selections. */ - trimSelections, - /** Select the word and following whitespaces on the right of the end of each selection. */ - selectWord, - /** Select preceding whitespaces and the word on the left of the end of each selection. */ - selectWordPrevious, - /** Select preceding whitespaces and the word on the right of the end of each selection. */ - selectWordEnd, - /** Select the non-whitespace word and following whitespaces on the right of the end of each selection. */ - selectWordAlt, - /** Select preceding whitespaces and the non-whitespace word on the left of the end of each selection. */ - selectWordAltPrevious, - /** Select preceding whitespaces and the non-whitespace word on the right of the end of each selection. */ - selectWordAltEnd, - /** Select within current selections according to a RegExp. */ - select, - /** Split within current selections according to a RegExp. */ - split, - /** Split selections into lines. */ - splitLines, - /** Select first and last characters of each selection. */ - selectFirstLast, - /** Copy selection to next line. */ - selectCopy, - /** Copy selection to previous line. */ - selectCopyBackwards, - /** Reduce selections to their cursor. */ - selectionsReduce, - /** Flip the direction of each selection. */ - selectionsFlip, - /** Ensure selections are in forward direction (the active cursor is after the anchor). */ - selectionsForward, - /** Ensure selections are in backward direction (the active cursor is before the anchor). */ - selectionsBackward, - /** Clear selections (except main) */ - selectionsClear, - /** Clear main selection. */ - selectionsClearMain, - /** Keep selections that match a RegExp. */ - selectionsKeepMatching, - /** Clear selections that match a RegExp. */ - selectionsClearMatching, - /** Merge contiguous selections together, including across lines. */ - selectionsMerge, - /** Align selections, aligning the cursor of each selection by inserting spaces before the first character of each selection. */ - selectionsAlign, - /** Copy the indentation of the main selection (or the count one if a count is given) to all other ones. */ - selectionsAlignCopy, - /** Yank and delete selections. */ - deleteYank, - /** Yank, delete and enter insert mode. */ - deleteInsertYank, - /** Delete selections without yanking. */ - deleteNoYank, - /** Delete selections without yanking and enter insert mode. */ - deleteInsertNoYank, - /** Yank selections. */ - yank, - /** Paste after the end of each selection. */ - pasteAfter, - /** Paste before the start of each selection. */ - pasteBefore, - /** Paste after the end of each selection and select pasted text. */ - pasteSelectAfter, - /** Paste before the start of each selection and select pasted text. */ - pasteSelectBefore, - /** Replace selections with yanked text. */ - pasteReplace, - /** Replace selections with every yanked text. */ - pasteReplaceEvery, - /** Replace each selected character with the next entered one. */ - replaceCharacters, - /** Join selected lines. */ - join, - /** Join selected lines and select spaces inserted in place of line breaks. */ - joinSelect, - /** Indent selected lines. */ - indent, - /** Indent selected lines (including empty lines). */ - indentWithEmpty, - /** Deindent selected lines. */ - deindent, - /** Deindent selected lines (and remove additional incomplete indent). */ - deindentFurther, - /** Transform to lowercase. */ - toLowerCase, - /** Transform to uppercase. */ - toUpperCase, - /** Swap case. */ - swapCase, - /** Pipe each selection to a program, and keeps it if the program returns 0. */ - pipeFilter, - /** Pipe each selection to a command, and replaces it with its output. */ - pipeReplace, - /** Pipe each selection to a command, ignoring their results. */ - pipeIgnore, - /** Pipe each selection to a command, appending the output after the selection. */ - pipeAppend, - /** Pipe each selection to a command, prepending the output before the selection. */ - pipePrepend, - /** Undo. */ - historyUndo, - /** Move backward in history. */ - historyBackward, - /** Redo. */ - historyRedo, - /** Move forward in history. */ - historyForward, - /** Repeat last change. */ - historyRepeat, - /** Repeat last selection change. */ - historyRepeatSelection, - /** Repeat last edit change. */ - historyRepeatEdit, - /** Start recording macro. */ - macrosRecordStart, - /** Stop recording macro. */ - macrosRecordStop, - /** Play macro. */ - macrosPlay, - /** Rotate each selection clockwise. */ - rotate, - /** Rotate each selection counter-clockwise. */ - rotateBackwards, - /** Rotate each selection (as well as its content) clockwise. */ - rotateContent, - /** Rotate each selection (as well as its content) counter-clockwise. */ - rotateContentBackwards, - /** Rotate each selection content clockwise, without changing selections. */ - rotateContentOnly, - /** Rotate each selection content counter-clockwise, without changing selections. */ - rotateContentOnlyBackwards, - /** Search for the given input string. */ - search, - /** Search for the given input string before the current selections. */ - searchBackwards, - /** Search current selections (smart). */ - searchSelectionSmart, - /** Search current selections. */ - searchSelection, - /** Select next match after the main selection. */ - searchNext, - /** Add a new selection with the next match after the main selection. */ - searchNextAdd, - /** Select previous match before the main selection. */ - searchPrevious, - /** Add a new selection with the previous match before the main selection. */ - searchPreviousAdd, - /** Perform selections specified in the arguments.. */ - objectsPerformSelection, - /** Shows prompt to jump somewhere */ - goto, - /** Go to line start. */ - gotoLineStart, - /** Go to first non-whitespace character of the line */ - gotoLineStartNonBlank, - /** Go to line end. */ - gotoLineEnd, - /** Go to first line. */ - gotoFirstLine, - /** Go to last line. */ - gotoLastLine, - /** Go to last character of the document. */ - gotoLastCharacter, - /** Go to first visible line. */ - gotoFirstVisibleLine, - /** Go to middle visible line. */ - gotoMiddleVisibleLine, - /** Go to last visible line. */ - gotoLastVisibleLine, - /** Open file under selection. */ - gotoSelectedFile, - /** Go to last buffer modification position. */ - gotoLastModification, - /** Open quick-jump menu. */ - openMenu, - /** Insert value in register. */ - registersInsert, - /** Select register for next command. */ - registersSelect, - /** Save selections. */ - marksSaveSelections, - /** Restore selections. */ - marksRestoreSelections, - /** Combine current selections with ones from register. */ - marksCombineSelectionsFromCurrent, - /** Combine register selections with current ones. */ - marksCombineSelectionsFromRegister, - /** Cancels waiting for input from the user */ - cancel, - /** Runs JavaScript code passed in a 'code' argument */ - run, - /** Move left (extend). */ - leftExtend, - /** Move right (extend). */ - rightExtend, - /** Move up (extend). */ - upExtend, - /** Move down (extend). */ - downExtend, - /** Extend to the next character pressed, including it. */ - selectToIncludedExtend, - /** Extend with until the next character pressed, excluding it. */ - selectToExcludedExtend, - /** Extend with line on which the end of each selection lies (or next line when end lies on an end-of-line). */ - selectLineExtend, - /** Extend to line beginning. */ - selectToLineBeginExtend, - /** Extend to line end. */ - selectToLineEndExtend, - /** Extend with enclosing characters. */ - selectEnclosingExtend, - /** Extend with the word and following whitespaces on the right of the end of each selection. */ - selectWordExtend, - /** Extend with preceding whitespaces and the word on the left of the end of each selection. */ - selectWordPreviousExtend, - /** Extend with preceding whitespaces and the word on the right of the end of each selection. */ - selectWordEndExtend, - /** Extend with the non-whitespace word and following whitespaces on the right of the end of each selection. */ - selectWordAltExtend, - /** Extend with preceding whitespaces and the non-whitespace word on the left of the end of each selection. */ - selectWordAltPreviousExtend, - /** Extend with preceding whitespaces and the non-whitespace word on the right of the end of each selection. */ - selectWordAltEndExtend, - /** Search for the given input string (extend). */ - searchExtend, - /** Search for the given input string before the current selections (extend). */ - searchBackwardsExtend, - /** Shows prompt to jump somewhere */ - gotoExtend, - /** Go to line start (extend). */ - gotoLineStartExtend, - /** Go to first non-whitespace character of the line */ - gotoLineStartNonBlankExtend, - /** Go to line end (extend). */ - gotoLineEndExtend, - /** Go to first line (extend). */ - gotoFirstLineExtend, - /** Go to last line (extend). */ - gotoLastLineExtend, - /** Go to last character of the document (extend). */ - gotoLastCharacterExtend, - /** Go to first visible line (extend). */ - gotoFirstVisibleLineExtend, - /** Go to middle visible line (extend). */ - gotoMiddleVisibleLineExtend, - /** Go to last visible line (extend). */ - gotoLastVisibleLineExtend, - /** Go to last buffer modification position (extend). */ - gotoLastModificationExtend, - /** Select to the next character pressed, including it. (backwards) */ - selectToIncludedBackwards, - /** Select until the next character pressed, excluding it. (backwards) */ - selectToExcludedBackwards, - /** Select enclosing characters. (backwards) */ - selectEnclosingBackwards, - /** Extend to the next character pressed, including it. (backwards) */ - selectToIncludedExtendBackwards, - /** Extend with until the next character pressed, excluding it. (backwards) */ - selectToExcludedExtendBackwards, - /** Extend with enclosing characters. (backwards) */ - selectEnclosingExtendBackwards, - /** Adds 0 to the current counter for the next operation. */ - count0, - /** Adds 1 to the current counter for the next operation. */ - count1, - /** Adds 2 to the current counter for the next operation. */ - count2, - /** Adds 3 to the current counter for the next operation. */ - count3, - /** Adds 4 to the current counter for the next operation. */ - count4, - /** Adds 5 to the current counter for the next operation. */ - count5, - /** Adds 6 to the current counter for the next operation. */ - count6, - /** Adds 7 to the current counter for the next operation. */ - count7, - /** Adds 8 to the current counter for the next operation. */ - count8, - /** Adds 9 to the current counter for the next operation. */ - count9, -}; - -/** - * An enum which maps command names to command IDs. - */ -export const enum Command { - /** Toggles Dance key bindings. */ - toggle = "dance.toggle", - /** Set Dance mode to Normal. */ - setNormal = "dance.set.normal", - /** Set Dance mode to Insert. */ - setInsert = "dance.set.insert", - /** Switches to normal mode temporarily. */ - tmpNormal = "dance.tmp.normal", - /** Switches to insert mode temporarily. */ - tmpInsert = "dance.tmp.insert", - /** Start insert before the current selections. */ - insertBefore = "dance.insert.before", - /** Start insert after the current selections. */ - insertAfter = "dance.insert.after", - /** Start insert at line start of each selection. */ - insertLineStart = "dance.insert.lineStart", - /** Start insert at line end of each selection. */ - insertLineEnd = "dance.insert.lineEnd", - /** Create new line and start insert below. */ - insertNewLineBelow = "dance.insert.newLine.below", - /** Create new line and start insert above. */ - insertNewLineAbove = "dance.insert.newLine.above", - /** Add a new line below, without entering insert mode. */ - newLineBelow = "dance.newLine.below", - /** Add a new line above, without entering insert mode. */ - newLineAbove = "dance.newLine.above", - /** Repeat last insert-mode change. */ - repeatInsert = "dance.repeat.insert", - /** Repeat last object select / character find. */ - repeatObjectOrSelectTo = "dance.repeat.objectOrSelectTo", - /** Move left. */ - left = "dance.left", - /** Move right. */ - right = "dance.right", - /** Move up. */ - up = "dance.up", - /** Move down. */ - down = "dance.down", - /** Scroll one page up. */ - upPage = "dance.up.page", - /** Scroll one page down. */ - downPage = "dance.down.page", - /** Scroll half a page up. */ - upHalfPage = "dance.up.halfPage", - /** Scroll half a page down. */ - downHalfPage = "dance.down.halfPage", - /** Select to the next character pressed, including it. */ - selectToIncluded = "dance.select.to.included", - /** Select until the next character pressed, excluding it. */ - selectToExcluded = "dance.select.to.excluded", - /** Select whole buffer. */ - selectBuffer = "dance.select.buffer", - /** Select line on which the end of each selection lies (or next line when end lies on an end-of-line). */ - selectLine = "dance.select.line", - /** Select to line beginning. */ - selectToLineBegin = "dance.select.toLineBegin", - /** Select to line end. */ - selectToLineEnd = "dance.select.toLineEnd", - /** Select enclosing characters. */ - selectEnclosing = "dance.select.enclosing", - /** Extend selections to contain full lines (including end-of-lines). */ - expandLines = "dance.expandLines", - /** Trim selections to only contain full lines (from start to line break). */ - trimLines = "dance.trimLines", - /** Trim whitespace at beginning and end of selections. */ - trimSelections = "dance.trimSelections", - /** Select the word and following whitespaces on the right of the end of each selection. */ - selectWord = "dance.select.word", - /** Select preceding whitespaces and the word on the left of the end of each selection. */ - selectWordPrevious = "dance.select.word.previous", - /** Select preceding whitespaces and the word on the right of the end of each selection. */ - selectWordEnd = "dance.select.word.end", - /** Select the non-whitespace word and following whitespaces on the right of the end of each selection. */ - selectWordAlt = "dance.select.word.alt", - /** Select preceding whitespaces and the non-whitespace word on the left of the end of each selection. */ - selectWordAltPrevious = "dance.select.word.alt.previous", - /** Select preceding whitespaces and the non-whitespace word on the right of the end of each selection. */ - selectWordAltEnd = "dance.select.word.alt.end", - /** Select within current selections according to a RegExp. */ - select = "dance.select", - /** Split within current selections according to a RegExp. */ - split = "dance.split", - /** Split selections into lines. */ - splitLines = "dance.split.lines", - /** Select first and last characters of each selection. */ - selectFirstLast = "dance.select.firstLast", - /** Copy selection to next line. */ - selectCopy = "dance.select.copy", - /** Copy selection to previous line. */ - selectCopyBackwards = "dance.select.copy.backwards", - /** Reduce selections to their cursor. */ - selectionsReduce = "dance.selections.reduce", - /** Flip the direction of each selection. */ - selectionsFlip = "dance.selections.flip", - /** Ensure selections are in forward direction (the active cursor is after the anchor). */ - selectionsForward = "dance.selections.forward", - /** Ensure selections are in backward direction (the active cursor is before the anchor). */ - selectionsBackward = "dance.selections.backward", - /** Clear selections (except main) */ - selectionsClear = "dance.selections.clear", - /** Clear main selection. */ - selectionsClearMain = "dance.selections.clearMain", - /** Keep selections that match a RegExp. */ - selectionsKeepMatching = "dance.selections.keepMatching", - /** Clear selections that match a RegExp. */ - selectionsClearMatching = "dance.selections.clearMatching", - /** Merge contiguous selections together, including across lines. */ - selectionsMerge = "dance.selections.merge", - /** Align selections, aligning the cursor of each selection by inserting spaces before the first character of each selection. */ - selectionsAlign = "dance.selections.align", - /** Copy the indentation of the main selection (or the count one if a count is given) to all other ones. */ - selectionsAlignCopy = "dance.selections.align.copy", - /** Yank and delete selections. */ - deleteYank = "dance.delete.yank", - /** Yank, delete and enter insert mode. */ - deleteInsertYank = "dance.delete.insert.yank", - /** Delete selections without yanking. */ - deleteNoYank = "dance.delete.noYank", - /** Delete selections without yanking and enter insert mode. */ - deleteInsertNoYank = "dance.delete.insert.noYank", - /** Yank selections. */ - yank = "dance.yank", - /** Paste after the end of each selection. */ - pasteAfter = "dance.paste.after", - /** Paste before the start of each selection. */ - pasteBefore = "dance.paste.before", - /** Paste after the end of each selection and select pasted text. */ - pasteSelectAfter = "dance.paste.select.after", - /** Paste before the start of each selection and select pasted text. */ - pasteSelectBefore = "dance.paste.select.before", - /** Replace selections with yanked text. */ - pasteReplace = "dance.paste.replace", - /** Replace selections with every yanked text. */ - pasteReplaceEvery = "dance.paste.replace.every", - /** Replace each selected character with the next entered one. */ - replaceCharacters = "dance.replace.characters", - /** Join selected lines. */ - join = "dance.join", - /** Join selected lines and select spaces inserted in place of line breaks. */ - joinSelect = "dance.join.select", - /** Indent selected lines. */ - indent = "dance.indent", - /** Indent selected lines (including empty lines). */ - indentWithEmpty = "dance.indent.withEmpty", - /** Deindent selected lines. */ - deindent = "dance.deindent", - /** Deindent selected lines (and remove additional incomplete indent). */ - deindentFurther = "dance.deindent.further", - /** Transform to lowercase. */ - toLowerCase = "dance.toLowerCase", - /** Transform to uppercase. */ - toUpperCase = "dance.toUpperCase", - /** Swap case. */ - swapCase = "dance.swapCase", - /** Pipe each selection to a program, and keeps it if the program returns 0. */ - pipeFilter = "dance.pipe.filter", - /** Pipe each selection to a command, and replaces it with its output. */ - pipeReplace = "dance.pipe.replace", - /** Pipe each selection to a command, ignoring their results. */ - pipeIgnore = "dance.pipe.ignore", - /** Pipe each selection to a command, appending the output after the selection. */ - pipeAppend = "dance.pipe.append", - /** Pipe each selection to a command, prepending the output before the selection. */ - pipePrepend = "dance.pipe.prepend", - /** Undo. */ - historyUndo = "dance.history.undo", - /** Move backward in history. */ - historyBackward = "dance.history.backward", - /** Redo. */ - historyRedo = "dance.history.redo", - /** Move forward in history. */ - historyForward = "dance.history.forward", - /** Repeat last change. */ - historyRepeat = "dance.history.repeat", - /** Repeat last selection change. */ - historyRepeatSelection = "dance.history.repeat.selection", - /** Repeat last edit change. */ - historyRepeatEdit = "dance.history.repeat.edit", - /** Start recording macro. */ - macrosRecordStart = "dance.macros.record.start", - /** Stop recording macro. */ - macrosRecordStop = "dance.macros.record.stop", - /** Play macro. */ - macrosPlay = "dance.macros.play", - /** Rotate each selection clockwise. */ - rotate = "dance.rotate", - /** Rotate each selection counter-clockwise. */ - rotateBackwards = "dance.rotate.backwards", - /** Rotate each selection (as well as its content) clockwise. */ - rotateContent = "dance.rotate.content", - /** Rotate each selection (as well as its content) counter-clockwise. */ - rotateContentBackwards = "dance.rotate.content.backwards", - /** Rotate each selection content clockwise, without changing selections. */ - rotateContentOnly = "dance.rotate.contentOnly", - /** Rotate each selection content counter-clockwise, without changing selections. */ - rotateContentOnlyBackwards = "dance.rotate.contentOnly.backwards", - /** Search for the given input string. */ - search = "dance.search", - /** Search for the given input string before the current selections. */ - searchBackwards = "dance.search.backwards", - /** Search current selections (smart). */ - searchSelectionSmart = "dance.search.selection.smart", - /** Search current selections. */ - searchSelection = "dance.search.selection", - /** Select next match after the main selection. */ - searchNext = "dance.search.next", - /** Add a new selection with the next match after the main selection. */ - searchNextAdd = "dance.search.next.add", - /** Select previous match before the main selection. */ - searchPrevious = "dance.search.previous", - /** Add a new selection with the previous match before the main selection. */ - searchPreviousAdd = "dance.search.previous.add", - /** Perform selections specified in the arguments.. */ - objectsPerformSelection = "dance.objects.performSelection", - /** Shows prompt to jump somewhere */ - goto = "dance.goto", - /** Go to line start. */ - gotoLineStart = "dance.goto.lineStart", - /** Go to first non-whitespace character of the line */ - gotoLineStartNonBlank = "dance.goto.lineStart.nonBlank", - /** Go to line end. */ - gotoLineEnd = "dance.goto.lineEnd", - /** Go to first line. */ - gotoFirstLine = "dance.goto.firstLine", - /** Go to last line. */ - gotoLastLine = "dance.goto.lastLine", - /** Go to last character of the document. */ - gotoLastCharacter = "dance.goto.lastCharacter", - /** Go to first visible line. */ - gotoFirstVisibleLine = "dance.goto.firstVisibleLine", - /** Go to middle visible line. */ - gotoMiddleVisibleLine = "dance.goto.middleVisibleLine", - /** Go to last visible line. */ - gotoLastVisibleLine = "dance.goto.lastVisibleLine", - /** Open file under selection. */ - gotoSelectedFile = "dance.goto.selectedFile", - /** Go to last buffer modification position. */ - gotoLastModification = "dance.goto.lastModification", - /** Open quick-jump menu. */ - openMenu = "dance.openMenu", - /** Insert value in register. */ - registersInsert = "dance.registers.insert", - /** Select register for next command. */ - registersSelect = "dance.registers.select", - /** Save selections. */ - marksSaveSelections = "dance.marks.saveSelections", - /** Restore selections. */ - marksRestoreSelections = "dance.marks.restoreSelections", - /** Combine current selections with ones from register. */ - marksCombineSelectionsFromCurrent = "dance.marks.combineSelections.fromCurrent", - /** Combine register selections with current ones. */ - marksCombineSelectionsFromRegister = "dance.marks.combineSelections.fromRegister", - /** Cancels waiting for input from the user */ - cancel = "dance.cancel", - /** Runs JavaScript code passed in a 'code' argument */ - run = "dance.run", - /** Move left (extend). */ - leftExtend = "dance.left.extend", - /** Move right (extend). */ - rightExtend = "dance.right.extend", - /** Move up (extend). */ - upExtend = "dance.up.extend", - /** Move down (extend). */ - downExtend = "dance.down.extend", - /** Extend to the next character pressed, including it. */ - selectToIncludedExtend = "dance.select.to.included.extend", - /** Extend with until the next character pressed, excluding it. */ - selectToExcludedExtend = "dance.select.to.excluded.extend", - /** Extend with line on which the end of each selection lies (or next line when end lies on an end-of-line). */ - selectLineExtend = "dance.select.line.extend", - /** Extend to line beginning. */ - selectToLineBeginExtend = "dance.select.toLineBegin.extend", - /** Extend to line end. */ - selectToLineEndExtend = "dance.select.toLineEnd.extend", - /** Extend with enclosing characters. */ - selectEnclosingExtend = "dance.select.enclosing.extend", - /** Extend with the word and following whitespaces on the right of the end of each selection. */ - selectWordExtend = "dance.select.word.extend", - /** Extend with preceding whitespaces and the word on the left of the end of each selection. */ - selectWordPreviousExtend = "dance.select.word.previous.extend", - /** Extend with preceding whitespaces and the word on the right of the end of each selection. */ - selectWordEndExtend = "dance.select.word.end.extend", - /** Extend with the non-whitespace word and following whitespaces on the right of the end of each selection. */ - selectWordAltExtend = "dance.select.word.alt.extend", - /** Extend with preceding whitespaces and the non-whitespace word on the left of the end of each selection. */ - selectWordAltPreviousExtend = "dance.select.word.alt.previous.extend", - /** Extend with preceding whitespaces and the non-whitespace word on the right of the end of each selection. */ - selectWordAltEndExtend = "dance.select.word.alt.end.extend", - /** Search for the given input string (extend). */ - searchExtend = "dance.search.extend", - /** Search for the given input string before the current selections (extend). */ - searchBackwardsExtend = "dance.search.backwards.extend", - /** Shows prompt to jump somewhere */ - gotoExtend = "dance.goto.extend", - /** Go to line start (extend). */ - gotoLineStartExtend = "dance.goto.lineStart.extend", - /** Go to first non-whitespace character of the line */ - gotoLineStartNonBlankExtend = "dance.goto.lineStart.nonBlank.extend", - /** Go to line end (extend). */ - gotoLineEndExtend = "dance.goto.lineEnd.extend", - /** Go to first line (extend). */ - gotoFirstLineExtend = "dance.goto.firstLine.extend", - /** Go to last line (extend). */ - gotoLastLineExtend = "dance.goto.lastLine.extend", - /** Go to last character of the document (extend). */ - gotoLastCharacterExtend = "dance.goto.lastCharacter.extend", - /** Go to first visible line (extend). */ - gotoFirstVisibleLineExtend = "dance.goto.firstVisibleLine.extend", - /** Go to middle visible line (extend). */ - gotoMiddleVisibleLineExtend = "dance.goto.middleVisibleLine.extend", - /** Go to last visible line (extend). */ - gotoLastVisibleLineExtend = "dance.goto.lastVisibleLine.extend", - /** Go to last buffer modification position (extend). */ - gotoLastModificationExtend = "dance.goto.lastModification.extend", - /** Select to the next character pressed, including it. (backwards) */ - selectToIncludedBackwards = "dance.select.to.included.backwards", - /** Select until the next character pressed, excluding it. (backwards) */ - selectToExcludedBackwards = "dance.select.to.excluded.backwards", - /** Select enclosing characters. (backwards) */ - selectEnclosingBackwards = "dance.select.enclosing.backwards", - /** Extend to the next character pressed, including it. (backwards) */ - selectToIncludedExtendBackwards = "dance.select.to.included.extend.backwards", - /** Extend with until the next character pressed, excluding it. (backwards) */ - selectToExcludedExtendBackwards = "dance.select.to.excluded.extend.backwards", - /** Extend with enclosing characters. (backwards) */ - selectEnclosingExtendBackwards = "dance.select.enclosing.extend.backwards", - /** Adds 0 to the current counter for the next operation. */ - count0 = "dance.count.0", - /** Adds 1 to the current counter for the next operation. */ - count1 = "dance.count.1", - /** Adds 2 to the current counter for the next operation. */ - count2 = "dance.count.2", - /** Adds 3 to the current counter for the next operation. */ - count3 = "dance.count.3", - /** Adds 4 to the current counter for the next operation. */ - count4 = "dance.count.4", - /** Adds 5 to the current counter for the next operation. */ - count5 = "dance.count.5", - /** Adds 6 to the current counter for the next operation. */ - count6 = "dance.count.6", - /** Adds 7 to the current counter for the next operation. */ - count7 = "dance.count.7", - /** Adds 8 to the current counter for the next operation. */ - count8 = "dance.count.8", - /** Adds 9 to the current counter for the next operation. */ - count9 = "dance.count.9", -} - -/** - * Additional key bindings. - */ -export const additionalKeyBindings = [ - { - key : "Alt+a", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "select" }, - }, - { - key : "Alt+a", - when : "editorTextFocus && dance.mode == 'insert'", - command: "dance.openMenu", - args : { "menu": "object", "action": "select" }, - }, - { - key : "Alt+i", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "select", "inner": true }, - }, - { - key : "Alt+i", - when : "editorTextFocus && dance.mode == 'insert'", - command: "dance.openMenu", - args : { "menu": "object", "action": "select", "inner": true }, - }, - { - key : "[", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "selectToStart" }, - }, - { - key : "]", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "selectToEnd" }, - }, - { - key : "Shift+;", - when : "editorTextFocus && dance.mode == 'normal'", - command: "workbench.action.showCommands", - }, - { - key : "Shift+[", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "selectToStart", "extend": true }, - }, - { - key : "Shift+]", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "selectToEnd", "extend": true }, - }, - { - key : "Alt+[", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "selectToStart", "inner": true }, - }, - { - key : "Alt+]", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "selectToEnd", "inner": true }, - }, - { - key : "Alt+Shift+[", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "selectToStart", "extend": true, "inner": true }, - }, - { - key : "Alt+Shift+]", - when : "editorTextFocus && dance.mode == 'normal'", - command: "dance.openMenu", - args : { "menu": "object", "action": "selectToEnd", "extend": true, "inner": true }, - }, -]; diff --git a/meta.ts b/meta.ts new file mode 100644 index 0000000..85ac2ec --- /dev/null +++ b/meta.ts @@ -0,0 +1,692 @@ +import * as assert from "assert"; +import * as fs from "fs/promises"; +import * as G from "glob"; +import * as path from "path"; + +const verbose = process.argv.includes("--verbose"); + +const moduleCommentRe = + new RegExp(String.raw`\/\*\*\n` // start of doc comment + + String.raw`((?: \*(?:\n| .+\n))+?)` // #1: doc comment + + String.raw` \*\/\n` // end of doc comment + + String.raw`declare module \"(.+?)\"`, // #2: module name + "m"); + +const docCommentRe = + new RegExp(String.raw`^( *)` // #1: indentation + + String.raw`\/\*\*\n` // start of doc comment + + String.raw`((?:\1 \*(?:\n| .+\n))+?)` // #2: doc comment + + String.raw`\1 \*\/\n` // end of doc comment + + String.raw`\1export (?:async )?function (\w+)` // #3: function name + + String.raw`\((.*|\n[\s\S]+?^\1)\)` // #4: parameters + + String.raw`(?:: )?(.+)[;{]$` // #5: return type (optional) + + "|" // or + + String.raw`^ *export namespace (\w+) {\n` // #6: namespace (alternative) + + String.raw`^( +)`, // #7: namespace indentation + "gm"); + +function countNewLines(text: string) { + let count = 0; + + for (let i = 0; i < text.length; i++) { + if (text.charCodeAt(i) === 10 /* \n */) { + count++; + } + } + + return count; +} + +const keyMapping: Record = { + Command: "commands", + Commands: "commands", + Identifier: "identifier", + Identifiers: "identifier", + Keys: "keys", + Keybinding: "keys", + Keybindings: "keys", + Title: "title", +}; + +const valueConverter: Record string> = { + commands(commands) { + return commands + .replace(/^`+|`+$/g, "") + .replace(/MAX_INT/g, `${2 ** 31 - 1}`); // Max integer supported in JSON. + }, + identifier(identifier) { + return identifier.replace(/^`+|`+$/g, ""); + }, + keys(keys) { + return keys; + }, + title(title) { + return title; + }, + qualifiedIdentifier(qualifiedIdentifier) { + return qualifiedIdentifier; + }, + line() { + throw new Error("this should not be called"); + }, +}; + +function parseAdditional(qualificationPrefix: string, text: string, textStartLine: number) { + const lines = text.split("\n"), + additional: Builder.AdditionalCommand[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.length > 2 && line.startsWith("| ") && line.endsWith(" |")) { + const keys = line + .slice(2, line.length - 2) // Remove start and end |. + .split(" | ") // Split into keys. + .map((k) => keyMapping[k.trim()]); // Normalize keys. + + i++; + + if (/^\|[-| ]+\|$/.test(lines[i])) { + i++; + } + + while (i < lines.length) { + const line = lines[i]; + + if (!line.startsWith("| ") || !line.endsWith(" |")) { + break; + } + + i++; + + const obj: Builder.AdditionalCommand = { line: textStartLine + i }, + values = line.slice(2, line.length - 2).split(" | "); + + for (let j = 0; j < values.length; j++) { + const key = keys[j], + value = valueConverter[key](values[j].trim()); + + (obj as Record)[key] = value; + } + + if ("identifier" in obj) { + obj.qualifiedIdentifier = qualificationPrefix + obj.identifier; + } + + additional.push(obj); + } + } + } + + return additional; +} + +/** + * Parses all the doc comments of functions in the given string of TypeScript + * code. Examples will be parsed using the given function. + */ +function parseDocComments(code: string, modulePath: string) { + let moduleDoc: string, + moduleDocStartLine: number, + moduleName: string; + const moduleHeaderMatch = moduleCommentRe.exec(code); + + if (moduleHeaderMatch !== null) { + moduleDoc = moduleHeaderMatch[1].split("\n").map((line) => line.slice(3)).join("\n"); + moduleDocStartLine = code.slice(0, moduleHeaderMatch.index).split("\n").length + 2; + moduleName = moduleHeaderMatch[2].replace(/^\.\//, ""); + } else { + moduleDoc = ""; + moduleDocStartLine = 0; + moduleName = path.basename(modulePath, ".ts"); + } + + if (verbose) { + console.log("Parsing doc comments in module", moduleName); + } + + const modulePrefix = moduleName === "misc" ? "" : moduleName + "."; + + const functions: Builder.ParsedFunction[] = [], + namespaces: string[] = []; + let previousIndentation = 0; + + for (let match = docCommentRe.exec(code); match !== null; match = docCommentRe.exec(code)) { + const indentationString = match[1], + docCommentString = match[2], + functionName = match[3], + parametersString = match[4], + returnTypeString = match[5], + enteredNamespace = match[6], + enteredNamespaceIndentation = match[7], + startLine = countNewLines(code.slice(0, match.index)), + endLine = startLine + countNewLines(match[0]); + + if (enteredNamespace !== undefined) { + namespaces.push(enteredNamespace); + previousIndentation = enteredNamespaceIndentation.length; + + continue; + } + + const indentation = indentationString.length, + namespace = namespaces.length === 0 ? undefined : namespaces.join("."), + returnType = returnTypeString.trim(), + parameters = parametersString + .split(/,(?![^:]+?[}>])/g) + .map((p) => p.trim()) + .filter((p) => p.length > 0) + .map((p) => { + let match: RegExpExecArray | null; + + if (match = /^(\w+\??|.+[}\]]): *(.+)$/.exec(p)) { + return match.slice(1) as [string, string]; + } + if (match = /^(\w+) *= *(\d+|true|false)$/.exec(p)) { + const type = match[2] === "true" || match[2] === "false" + ? "Argument" + : "number"; + + return [match[1], `${type} = ${match[2]}`] as [string, string]; + } + if (match = /^(\w+) *= *(\w+)\.([\w.]+)$/.exec(p)) { + return [match[1], `${match[2]} = ${match[2]}.${match[3]}`] as [string, string]; + } + if (match = /^(\.\.\.\w+): *(.+)$/.exec(p)) { + return [match[1], match[2]] as [string, string]; + } + + throw new Error(`unrecognized parameter pattern ${p}`); + }), + docComment = docCommentString + .split("\n") + .map((line) => line.slice(indentation).replace(/^ \* ?/g, "")) + .join("\n"); + + if (previousIndentation > indentation) { + namespaces.pop(); + previousIndentation = indentation; + } + + for (const parameter of parameters) { + if (parameter[0].endsWith("?")) { + // Optional parameters. + parameter[0] = parameter[0].slice(0, parameter[0].length - 1); + parameter[1] += " | undefined"; + } else { + const match = /^(.+?)\s+=\s+(.+)$/.exec(parameter[1]); + + if (match !== null) { + // Optional parameters with default values. + parameter[1] = match[1] + " | undefined"; + } + } + } + + const splitDocComment = docComment.split(/\n### Example\n/gm), + properties: Record = {}, + doc = splitDocComment[0].replace(/^@(param \w+|\w+)(?:\n| ((?:.+\n)(?: {2}.+\n)*))/gm, + (_, k: string, v: string) => { + properties[k] = v?.replace(/\n {2}/g, " ").trim(); + return ""; + }), + summary = /((?:.+(?:\n|$))+)/.exec(doc)![0].trim().replace(/\.$/, ""), + examplesStrings = splitDocComment.slice(1), + nameWithDot = functionName.replace(/_/g, "."); + + let qualifiedName = modulePrefix; + + if (namespace !== undefined) { + qualifiedName += namespace + "."; + } + + if (nameWithDot === moduleName) { + qualifiedName = qualifiedName.replace(/\.$/, ""); + } else { + qualifiedName += nameWithDot; + } + + functions.push({ + namespace, + name: functionName, + nameWithDot, + qualifiedName, + + startLine, + endLine, + + doc, + properties, + summary, + examples: examplesStrings, + additional: parseAdditional(modulePrefix, splitDocComment[0], startLine), + + parameters, + returnType: returnType.length === 0 ? undefined : returnType, + }); + } + + docCommentRe.lastIndex = 0; + + return { + path: path.relative(path.dirname(__dirname), modulePath).replace(/\\/g, "/"), + name: moduleName, + doc: moduleDoc, + + additional: parseAdditional(modulePrefix, moduleDoc, moduleDocStartLine), + + functions, + functionNames: [...new Set(functions.map((f) => f.name))], + + get commands() { + return getCommands(this); + }, + get keybindings() { + return getKeybindings(this); + }, + } as Builder.ParsedModule; +} + +/** + * Mapping from character to corresponding VS Code keybinding. + */ +export const specialCharacterMapping = { + "~": "s-`", + "!": "s-1", + "@": "s-2", + "#": "s-3", + "$": "s-4", + "%": "s-5", + "^": "s-6", + "&": "s-7", + "*": "s-8", + "(": "s-9", + ")": "s-0", + "_": "s--", + "+": "s-=", + "{": "s-[", + "}": "s-]", + "|": "s-\\", + ":": "s-;", + '"': "s-'", + "<": "s-,", + ">": "s-.", + "?": "s-/", +}; + +/** + * RegExp for keys of `specialCharacterMapping`. + */ +export const specialCharacterRegExp = /[~!@#$%^&*()_+{}|:"<>?]/g; + +/** + * Async wrapper around the `glob` package. + */ +export function glob(pattern: string, ignore?: string) { + return new Promise((resolve, reject) => { + G(pattern, { ignore }, (err, matches) => err ? reject(err) : resolve(matches)); + }); +} + +/** + * A class used in .build.ts files. + */ +export class Builder { + private _apiModules?: Builder.ParsedModule[]; + private _commandModules?: Builder.ParsedModule[]; + + /** + * Returns all modules for API files. + */ + public async getApiModules() { + if (this._apiModules !== undefined) { + return this._apiModules; + } + + const apiFiles = await glob(`${__dirname}/src/api/**/*.ts`, /* ignore= */ "**/*.build.ts"), + apiModules = await Promise.all( + apiFiles.map((filepath) => + fs.readFile(filepath, "utf-8").then((code) => parseDocComments(code, filepath)))); + + return this._apiModules = apiModules.sort((a, b) => a.name.localeCompare(b.name)); + } + + /** + * Returns all modules for command files. + */ + public async getCommandModules() { + if (this._commandModules !== undefined) { + return this._commandModules; + } + + const commandsGlob = `${__dirname}/src/commands/**/*.ts`, + commandFiles = await glob(commandsGlob, /* ignore= */ "**/*.build.ts"), + allCommandModules = await Promise.all( + commandFiles.map((filepath) => + fs.readFile(filepath, "utf-8").then((code) => parseDocComments(code, filepath)))), + commandModules = allCommandModules.filter((m) => m.doc.length > 0); + + return this._commandModules = commandModules.sort((a, b) => a.name.localeCompare(b.name)); + } +} + +export namespace Builder { + export interface ParsedFunction { + readonly namespace?: string; + readonly name: string; + readonly nameWithDot: string; + readonly qualifiedName: string; + + readonly startLine: number; + readonly endLine: number; + + readonly doc: string; + readonly properties: Record; + readonly summary: string; + readonly examples: string[]; + readonly additional: AdditionalCommand[]; + + readonly parameters: readonly [name: string, type: string][]; + readonly returnType: string | undefined; + } + + export interface AdditionalCommand { + title?: string; + identifier?: string; + qualifiedIdentifier?: string; + keys?: string; + commands?: string; + line: number; + } + + export interface ParsedModule { + readonly path: string; + readonly name: string; + readonly doc: string; + + readonly additional: readonly AdditionalCommand[]; + readonly functions: readonly ParsedFunction[]; + readonly functionNames: readonly string[]; + + readonly commands: { + readonly id: string; + readonly title: string; + readonly when?: string; + }[]; + + readonly keybindings: { + readonly title?: string; + readonly key: string; + readonly when: string; + readonly command: string; + readonly args?: any; + }[]; + } +} + +/** + * Parses the short "`s-a-b` (mode)"-like syntax for defining keybindings into + * a format compatible with VS Code keybindings. + */ +export function parseKeys(keys: string) { + if (keys.length === 0) { + return []; + } + + return keys.split(/ *, (?=`)/g).map((keyString) => { + const match = /^(`+)(.+?)\1 \((.+?)\)$/.exec(keyString)!, + keybinding = match[2].trim().replace( + specialCharacterRegExp, (m) => (specialCharacterMapping as Record)[m]); + + // Reorder to match Ctrl+Shift+Alt+_ + let key = ""; + + if (keybinding.includes("c-")) { + key += "Ctrl+"; + } + + if (keybinding.includes("s-")) { + key += "Shift+"; + } + + if (keybinding.includes("a-")) { + key += "Alt+"; + } + + const remainingKeybinding = keybinding.replace(/[csa]-/g, ""), + whenClauses = ["editorTextFocus"]; + + for (const tag of match[3].split(", ")) { + switch (tag) { + case "normal": + case "insert": + case "input": + whenClauses.push(`dance.mode == '${tag}'`); + break; + + case "recording": + whenClauses.push("dance.isRecording"); + break; + + default: + throw new Error("unknown keybinding tag " + tag); + } + } + + key += remainingKeybinding[0].toUpperCase() + remainingKeybinding.slice(1); + + return { + key, + when: whenClauses.join(" && "), + }; + }); +} + +/** + * Returns all defined commands in the given module. + */ +function getCommands(module: Omit) { + // TODO: improve conditions + return [ + ...module.functions.map((f) => ({ + id: `dance.${f.qualifiedName}`, + title: f.summary, + when: "dance.mode == 'normal'", + })), + ...module.additional + .concat(...module.functions.flatMap((f) => f.additional)) + .filter((a) => a.identifier !== undefined && a.title !== undefined) + .map((a) => ({ + id: `dance.${a.qualifiedIdentifier}`, + title: a.title!, + when: "dance.mode == 'normal'", + })), + ].sort((a, b) => a.id.localeCompare(b.id)); +} + +/** + * Returns all defined keybindings in the given module. + */ +function getKeybindings(module: Omit) { + return [ + ...module.functions.flatMap((f) => parseKeys(f.properties.keys ?? "").map((key) => ({ + ...key, + title: f.summary, + command: `dance.${f.qualifiedName}`, + }))), + + ...module.additional + .concat(...module.functions.flatMap((f) => f.additional)) + .flatMap(({ title, keys, commands, qualifiedIdentifier }) => { + const parsedKeys = parseKeys(keys ?? ""); + + if (qualifiedIdentifier !== undefined) { + return parsedKeys.map((key) => ({ + ...key, + title, + command: `dance.${qualifiedIdentifier}`, + })); + } + + const parsedCommands = + JSON.parse("[" + commands!.replace(/(\w+):/g, "\"$1\":") + "]") as any[]; + + if (parsedCommands.length === 1) { + let [command]: [string] = parsedCommands[0]; + + if (command[0] === ".") { + command = "dance" + command; + } + + return parsedKeys.map((key) => ({ + ...key, + title, + command, + args: parsedCommands[0][1], + })); + } + + return parsedKeys.map((key) => ({ + ...key, + title, + command: "dance.run", + args: { + commands: parsedCommands, + }, + })); + }), + ].sort((a, b) => a.command.localeCompare(b.command)); +} + +/** + * Given a multiline string, returns the same string with all lines starting + * with an indentation `>= by` reduced by `by` spaces. + */ +export function unindent(by: number, string: string) { + return string.replace(new RegExp(`^ {${by}}`, "gm"), "").replace(/^ +$/gm, ""); +} + +/** + * Updates a .build.ts file. + */ +async function buildFile(fileName: string, builder: Builder) { + const relativeName = path.relative(__dirname, fileName), + relativeNameWithoutBuild = relativeName.replace(/build\.ts$/, ""), + modulePath = `./${relativeNameWithoutBuild}build`, + module: { build(builder: Builder): Promise } = require(modulePath), + generatedContent = await module.build(builder); + + if (typeof generatedContent === "string") { + const prefix = path.basename(relativeNameWithoutBuild), + outputName = (await fs.readdir(path.dirname(fileName))) + .find((path) => path.startsWith(prefix) && !path.endsWith(".build.ts"))!, + outputPath = path.join(path.dirname(fileName), outputName), + outputContent = await fs.readFile(outputPath, "utf-8"), + outputContentHeader = + /^[\s\S]+?\n.+Content below this line was auto-generated.+\n/m.exec(outputContent)![0]; + + await fs.writeFile(outputPath, outputContentHeader + generatedContent, "utf-8"); + } +} + +/** + * The main entry point of the script. + */ +async function main() { + let success = true; + + const ensureUpToDate = process.argv.includes("--ensure-up-to-date"), + check = process.argv.includes("--check"), + buildIndex = process.argv.indexOf("--build"), + build = buildIndex === -1 ? "**/*.build.ts" : process.argv[buildIndex + 1]; + + const contentsBefore: string[] = [], + fileNames = [ + `${__dirname}/package.json`, + `${__dirname}/src/commands/README.md`, + `${__dirname}/src/commands/index.ts`, + ]; + + if (ensureUpToDate) { + contentsBefore.push(...await Promise.all(fileNames.map((name) => fs.readFile(name, "utf-8")))); + } + + const builder = new Builder(), + filesToBuild = await glob(__dirname + "/" + build), + buildErrors: unknown[] = []; + + await Promise.all( + filesToBuild.map((path) => buildFile(path, builder).catch((e) => buildErrors.push(e)))); + + if (buildErrors.length > 0) { + console.error(buildErrors); + } + + if (ensureUpToDate) { + const contentsAfter = await Promise.all(fileNames.map((name) => fs.readFile(name, "utf-8"))); + + for (let i = 0; i < fileNames.length; i++) { + if (verbose) { + console.log("Checking file", fileNames[i], "for diffs..."); + } + + // The built-in "assert" module displays a multiline diff if the strings + // are different, so we use it instead of comparing manually. + assert.strictEqual(contentsBefore[i], contentsAfter[i]); + } + } + + if (check) { + const filesToCheck = await glob( + `${__dirname}/src/commands/**/*.ts`, /* ignore= */ "**/*.build.ts"), + contentsToCheck = await Promise.all(filesToCheck.map((f) => fs.readFile(f, "utf-8"))); + + for (let i = 0; i < filesToCheck.length; i++) { + const fileToCheck = filesToCheck[i], + contentToCheck = contentsToCheck[i]; + + if (contentToCheck.includes("editor.selections")) { + console.error("File", fileToCheck, "includes forbidden access to editor.selections."); + success = false; + } + } + } + + return success; +} + +if (require.main === module) { + main().then((success) => { + if (!process.argv.includes("--watch")) { + process.exit(success ? 0 : 1); + } + + import("chokidar").then((chokidar) => { + const watcher = chokidar.watch([ + "**/*.build.ts", + "src/api/*.ts", + "src/commands/*.ts", + "test/suite/commands/*.md", + ], { + ignored: "src/commands/load-all.ts", + }); + + let isGenerating = false; + + watcher.on("change", async (path) => { + if (isGenerating) { + return; + } + + console.log("Change detected at " + path + ", updating generated files..."); + isGenerating = true; + + try { + await main(); + } finally { + isGenerating = false; + } + }); + }); + }); +} diff --git a/package.build.ts b/package.build.ts new file mode 100644 index 0000000..40dce9d --- /dev/null +++ b/package.build.ts @@ -0,0 +1,651 @@ +import type { Builder } from "./meta"; + +// Shared values +// ============================================================================ + +const commandType = { + type: "array", + items: { + type: ["array", "object", "string"], + properties: { + command: { + type: "string", + }, + args: {}, + }, + required: ["command"], + }, +}; + +const builtinModesAreDeprecatedMessage = + "Built-in modes are deprecated. Use `#dance.modes#` instead."; + +const modeNamePattern = { + pattern: /^[a-zA-Z]\w*$/.source, + patternErrorMessage: "", +}; + +const colorPattern = { + pattern: /^(#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|#[a-fA-F0-9]{8}|\$([a-zA-Z]+(\.[a-zA-Z]+)+))$/.source, + patternErrorMessage: "Color should be an hex color or a '$' sign followed by a color identifier.", +}; + +const selectionDecorationType = { + type: "object", + properties: { + applyTo: { + enum: ["all", "main", "secondary"], + default: "all", + description: "The selections to apply this style to.", + enumDescriptions: [ + "Apply to all selections.", + "Apply to main selection only.", + "Apply to all selections except main selection.", + ], + }, + backgroundColor: { + type: "string", + ...colorPattern, + }, + borderColor: { + type: "string", + ...colorPattern, + }, + borderStyle: { + type: "string", + }, + borderWidth: { + type: "string", + }, + borderRadius: { + type: "string", + }, + isWholeLine: { + type: "boolean", + default: false, + }, + }, +}; + +// Package information +// ============================================================================ + +export const pkg = (modules: Builder.ParsedModule[]) => ({ + + // Common package.json properties. + // ========================================================================== + + name: "dance", + description: "Make those cursors dance with Kakoune-inspired keybindings.", + version: "0.5.0-rc", + license: "ISC", + + author: { + name: "Grégoire Geis", + email: "opensource@gregoirege.is", + }, + + repository: { + type: "git", + url: "https://github.com/71/dance.git", + }, + + main: "./out/src/extension.js", + + engines: { + vscode: "^1.44.0", + }, + + scripts: { + "check": "eslint .", + "format": "eslint . --fix", + "generate": "ts-node ./meta.ts", + "generate:watch": "ts-node ./meta.ts --watch", + "vscode:prepublish": "yarn run generate && yarn run compile", + "compile": "tsc -p ./", + "compile:watch": "tsc -watch -p ./", + "test": "yarn run compile && node ./out/test/run.js", + "package": "vsce package", + "publish": "vsce publish", + }, + + devDependencies: { + "@types/glob": "^7.1.1", + "@types/mocha": "^8.0.3", + "@types/node": "^14.6.0", + "@types/vscode": "^1.44.0", + "@typescript-eslint/eslint-plugin": "^4.18.0", + "@typescript-eslint/parser": "^4.18.0", + "chokidar": "^3.5.1", + "eslint": "^7.22.0", + "glob": "^7.1.6", + "mocha": "^8.1.1", + "source-map-support": "^0.5.19", + "ts-node": "^9.1.1", + "typescript": "^4.2.4", + "unexpected": "^12.0.0", + "vsce": "^1.87.0", + "vscode-test": "^1.5.2", + }, + + // VS Code-specific properties. + // ========================================================================== + + displayName: "Dance", + publisher: "gregoire", + categories: ["Keymaps", "Other"], + readme: "README.md", + icon: "assets/dance.png", + + activationEvents: ["*"], + extensionKind: ["ui", "workspace"], + + // Dance-specific properties. + // ========================================================================== + + // The two properties below can be set when distributing Dance to ensure it + // cannot execute arbitrary code (with `dance.run`) or system commands (with + // `dance.selections.{filter,pipe}`). + "dance.disableArbitraryCodeExecution": false, + "dance.disableArbitraryCommandExecution": false, + + contributes: { + + // Configuration. + // ======================================================================== + + configuration: { + type: "object", + title: "Dance", + properties: { + "dance.defaultMode": { + type: "string", + scope: "language-overridable", + default: "normal", + description: "Controls which mode is set by default when an editor is opened.", + ...modeNamePattern, + }, + "dance.modes": { + type: "object", + scope: "language-overridable", + additionalProperties: { + type: "object", + propertyNames: modeNamePattern, + properties: { + inheritFrom: { + type: ["string", "null"], + description: + "Controls how default configuration options are obtained for this mode. " + + "Specify a string to inherit from the mode with the given name, " + + "and null to inherit from the VS Code configuration.", + ...modeNamePattern, + }, + cursorStyle: { + enum: [ + "line", + "block", + "underline", + "line-thin", + "block-outline", + "underline-thin", + "inherit", + null, + ], + description: "Controls the cursor style.", + }, + lineHighlight: { + type: ["string", "null"], + markdownDescription: + "Controls the line highlighting applied to active lines. " + + "Can be an hex color, a [theme color](" + + "https://code.visualstudio.com/api/references/theme-color) or null.", + ...colorPattern, + }, + lineNumbers: { + enum: ["off", "on", "relative", "inherit", null], + description: "Controls the display of line numbers.", + enumDescriptions: [ + "No line numbers.", + "Absolute line numbers.", + "Relative line numbers.", + "Inherit from `editor.lineNumbers`.", + ], + }, + onEnterMode: { + ...commandType, + description: + "Controls what commands should be executed upon entering this mode.", + }, + onLeaveMode: { + ...commandType, + description: + "Controls what commands should be executed upon leaving this mode.", + }, + selectionBehavior: { + enum: ["caret", "character", null], + default: "caret", + description: "Controls how selections behave within VS Code.", + markdownEnumDescriptions: [ + "Selections are anchored to carets, which is the native VS Code behavior; " + + "that is, they are positioned *between* characters and can therefore be " + + "empty.", + "Selections are anchored to characters, like Kakoune; that is, they are " + + "positioned *on* characters, and therefore cannot be empty. " + + "Additionally, one-character selections will behave as if they were " + + "non-directional, like Kakoune.", + ], + }, + decorations: { + ...selectionDecorationType, + type: ["array", "object", "null"], + description: "The decorations to apply to selections.", + items: selectionDecorationType, + }, + }, + additionalProperties: false, + }, + default: { + insert: {}, + normal: { + lineNumbers: "relative", + decorations: { + applyTo: "main", + backgroundColor: "$editor.hoverHighlightBackground", + isWholeLine: true, + }, + onEnterMode: [ + [".selections.restore", { register: " ^", try: true }], + ], + onLeaveMode: [ + [".selections.save", { + register: " ^", + style: { + borderColor: "$editor.selectionBackground", + borderStyle: "solid", + borderWidth: "2px", + borderRadius: "1px", + }, + until: [ + ["mode-did-change", { include: "normal" }], + ["selections-did-change"], + ], + }], + ], + }, + }, + description: "Controls the different modes available in Dance.", + }, + + "dance.menus": { + type: "object", + scope: "language-overridable", + description: "Controls the different menus available in Dance.", + additionalProperties: { + type: "object", + properties: { + items: { + type: "object", + additionalProperties: { + type: "object", + properties: { + text: { + type: "string", + description: "Text shown in the menu.", + }, + command: { + type: "string", + description: "Command to execute on item selection.", + }, + args: { + type: "array", + description: "Arguments to the command to execute.", + }, + }, + required: ["command"], + }, + }, + }, + additionalProperties: false, + }, + default: { + "object": { + items: ((command = "dance.seek.object") => ({ + "b()": { + command, + args: [{ input: "\\((?#inner)\\)" }], + text: "parenthesis block", + }, + "B{}": { + command, + args: [{ input: "\\{(?#inner)\\}" }], + text: "braces block", + }, + "r[]": { + command, + args: [{ input: "\\[(?#inner)\\]" }], + text: "brackets block", + }, + "a<>": { + command, + args: [{ input: "<(?#inner)>" }], + text: "angle block", + }, + 'Q"': { + command, + args: [{ input: "(?#noescape)\"(?#inner)(?#noescape)\"" }], + text: "double quote string", + }, + "q'": { + command, + args: [{ input: "(?#noescape)'(?#inner)(?#noescape)'" }], + text: "single quote string", + }, + "g`": { + command, + args: [{ input: "(?#noescape)`(?#inner)(?#noescape)`" }], + text: "grave quote string", + }, + "w": { + command, + args: [{ input: "[\\p{L}]+(?[^\\S\\n]+)" }], + text: "word", + }, + "W": { + command, + args: [{ input: "[\\S]+(?[^\\S\\n]+)" }], + text: "WORD", + }, + "s": { + command, + args: [{ input: "(?#predefined=sentence)" }], + text: "sentence", + }, + "p": { + command, + args: [{ input: "(?#predefined=paragraph)" }], + text: "paragraph", + }, + " ": { + command, + args: [{ input: "(?[\\s]+)[^\\S\\n]+(?[\\s]+)" }], + text: "whitespaces", + }, + "i": { + command, + args: [{ input: "(?#predefined=indent)" }], + text: "indent", + }, + "n": { + command, + args: [{ input: "(?#singleline)-?[\\d_]+(\\.[0-9]+)?([eE]\\d+)?" }], + text: "number", + }, + "u": { + command, + args: [{ input: "(?#predefined=argument)" }], + text: "argument", + }, + "c": { + command, + text: "custom object desc", + }, + }))(), + }, + + "goto": { + items: { + "h": { + text: "to line start", + command: "dance.select.lineStart", + }, + "l": { + text: "to line end", + command: "dance.select.lineEnd", + }, + "i": { + text: "to non-blank line start", + command: "dance.select.lineStart", + args: [{ skipBlank: true }], + }, + "gk": { + text: "to first line", + command: "dance.select.lineStart", + args: [{ count: 0 }], + }, + "j": { + text: "to last line", + command: "dance.select.lastLine", + }, + "e": { + text: "to last char of last line", + command: "dance.select.lineEnd", + args: [{ count: 2 ** 31 - 1 }], + }, + "t": { + text: "to first displayed line", + command: "dance.select.firstVisibleLine", + }, + "c": { + text: "to middle displayed line", + command: "dance.select.middleVisibleLine", + }, + "b": { + text: "to last displayed line", + command: "dance.select.lastVisibleLine", + }, + "f": { + text: "to file whose name is selected", + command: "dance.selections.open", + }, + ".": { + text: "to last buffer modification position", + command: "dance.select.lastModification", + }, + }, + }, + + "view": { + items: { + // AFAIK, we can't implement these yet since VS Code only + // exposes vertical view ranges: + // - m, center cursor horizontally + // - h, scroll left + // - l, scroll right + "vc": { + text: "center cursor vertically", + command: "dance.view.line", + args: [{ at: "center" }], + }, + "t": { + text: "cursor on top", + command: "dance.view.line", + args: [{ at: "top" }], + }, + "b": { + text: "cursor on bottom", + command: "dance.view.line", + args: [{ at: "bottom" }], + }, + "j": { + text: "scroll down", + command: "editorScroll", + args: [{ to: "down", by: "line", revealCursor: true }], + }, + "k": { + text: "scroll up", + command: "editorScroll", + args: [{ to: "up", by: "line", revealCursor: true }], + }, + }, + }, + } as Record}>, + }, + + // Deprecated configuration: + "dance.enabled": { + type: "boolean", + default: true, + description: "Controls whether the Dance keybindings are enabled.", + deprecationMessage: "dance.enabled is deprecated; disable the Dance extension instead.", + }, + + "dance.normalMode.lineHighlight": { + type: ["string", "null"], + default: "editor.hoverHighlightBackground", + markdownDescription: + "Controls the line highlighting applied to active lines in normal mode. " + + "Can be an hex color, a [theme color](" + + "https://code.visualstudio.com/api/references/theme-color) or null.", + markdownDeprecationMessage: builtinModesAreDeprecatedMessage, + }, + "dance.insertMode.lineHighlight": { + type: ["string", "null"], + default: null, + markdownDescription: + "Controls the line highlighting applied to active lines in insert mode. " + + "Can be an hex color, a [theme color](" + + "https://code.visualstudio.com/api/references/theme-color) or null.", + markdownDeprecationMessage: builtinModesAreDeprecatedMessage, + }, + "dance.normalMode.lineNumbers": { + enum: ["off", "on", "relative", "inherit"], + default: "relative", + description: "Controls the display of line numbers in normal mode.", + enumDescriptions: [ + "No line numbers.", + "Absolute line numbers.", + "Relative line numbers.", + "Inherit from `editor.lineNumbers`.", + ], + markdownDeprecationMessage: builtinModesAreDeprecatedMessage, + }, + "dance.insertMode.lineNumbers": { + enum: ["off", "on", "relative", "inherit"], + default: "inherit", + description: "Controls the display of line numbers in insert mode.", + enumDescriptions: [ + "No line numbers.", + "Absolute line numbers.", + "Relative line numbers.", + "Inherit from `editor.lineNumbers`.", + ], + markdownDeprecationMessage: builtinModesAreDeprecatedMessage, + }, + "dance.normalMode.cursorStyle": { + enum: [ + "line", + "block", + "underline", + "line-thin", + "block-outline", + "underline-thin", + "inherit", + ], + default: "inherit", + description: "Controls the cursor style in normal mode.", + markdownDeprecationMessage: builtinModesAreDeprecatedMessage, + }, + "dance.insertMode.cursorStyle": { + enum: [ + "line", + "block", + "underline", + "line-thin", + "block-outline", + "underline-thin", + "inherit", + ], + default: "inherit", + description: "Controls the cursor style in insert mode.", + markdownDeprecationMessage: builtinModesAreDeprecatedMessage, + }, + "dance.insertMode.selectionStyle": { + type: "object", + default: { + borderColor: "$editor.selectionBackground", + borderStyle: "solid", + borderWidth: "2px", + borderRadius: "1px", + }, + description: "The style to apply to selections in insert mode.", + properties: (Object as any).fromEntries( + [ + "backgroundColor", + "borderColor", + "borderStyle", + "borderWidth", + "borderRadius", + ].map((x) => [x, { type: "string" }]), + ), + markdownDeprecationMessage: builtinModesAreDeprecatedMessage, + }, + "dance.selectionBehavior": { + enum: ["caret", "character"], + default: "caret", + description: "Controls how selections behave within VS Code.", + markdownEnumDescriptions: [ + "Selections are anchored to carets, which is the native VS Code behavior; that is, " + + "they are positioned *between* characters and can therefore be empty.", + "Selections are anchored to characters, like Kakoune; that is, they are positioned " + + "*on* characters, and therefore cannot be empty. Additionally, one-character " + + "selections will behave as if they were non-directional, like Kakoune.", + ], + markdownDeprecationMessage: builtinModesAreDeprecatedMessage, + }, + }, + }, + + // Commands. + // ======================================================================== + + commands: modules.flatMap((module) => module.commands.map((x) => ({ + command: x.id, + title: x.title, + category: "Dance", + }))), + + menus: { + commandPalette: modules.flatMap((module) => module.commands.map((x) => ({ + command: x.id, + when: x.when, + }))), + }, + + // Keybindings. + // ======================================================================== + + keybindings: (() => { + const keybindings = modules.flatMap((module) => module.keybindings), + alphanum = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"], + keysToAssign = new Set([...alphanum, ...alphanum.map((x) => `Shift+${x}`), ...",'"]); + + for (const keybinding of keybindings) { + keysToAssign.delete(keybinding.key); + } + + for (const keyToAssign of keysToAssign) { + keybindings.push({ + command: "dance.ignore", + key: keyToAssign, + when: "editorTextFocus && dance.mode == 'normal'", + }); + } + + return keybindings; + })(), + }, +}); + +// Save to package.json +// ============================================================================ + +export async function build(builder: Builder) { + const fs = await import("fs/promises"); + + await fs.writeFile( + `${__dirname}/package.json`, + JSON.stringify(pkg(await builder.getCommandModules()), undefined, 2) + "\n", + "utf-8", + ); +} diff --git a/package.json b/package.json index bff101a..0535221 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,8 @@ { "name": "dance", - "displayName": "Dance", "description": "Make those cursors dance with Kakoune-inspired keybindings.", - "version": "0.4.2", + "version": "0.5.0-rc", "license": "ISC", - "publisher": "gregoire", "author": { "name": "Grégoire Geis", "email": "opensource@gregoirege.is" @@ -13,11 +11,6 @@ "type": "git", "url": "https://github.com/71/dance.git" }, - "readme": "README.md", - "categories": [ - "Keymaps", - "Other" - ], "main": "./out/src/extension.js", "engines": { "vscode": "^1.44.0" @@ -25,44 +18,612 @@ "scripts": { "check": "eslint .", "format": "eslint . --fix", - "generate": "ts-node ./commands/generate.ts && ts-node package.ts", + "generate": "ts-node ./meta.ts", + "generate:watch": "ts-node ./meta.ts --watch", "vscode:prepublish": "yarn run generate && yarn run compile", "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", + "compile:watch": "tsc -watch -p ./", "test": "yarn run compile && node ./out/test/run.js", "package": "vsce package", "publish": "vsce publish" }, "devDependencies": { "@types/glob": "^7.1.1", - "@types/js-yaml": "^3.12.3", "@types/mocha": "^8.0.3", "@types/node": "^14.6.0", "@types/vscode": "^1.44.0", "@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/parser": "^4.18.0", + "chokidar": "^3.5.1", "eslint": "^7.22.0", "glob": "^7.1.6", - "js-yaml": "^3.13.0", "mocha": "^8.1.1", "source-map-support": "^0.5.19", "ts-node": "^9.1.1", - "typescript": "^4.2.3", + "typescript": "^4.2.4", + "unexpected": "^12.0.0", "vsce": "^1.87.0", - "vscode-test": "^1.3.0" + "vscode-test": "^1.5.2" }, + "displayName": "Dance", + "publisher": "gregoire", + "categories": [ + "Keymaps", + "Other" + ], + "readme": "README.md", + "icon": "assets/dance.png", "activationEvents": [ "*" ], + "extensionKind": [ + "ui", + "workspace" + ], + "dance.disableArbitraryCodeExecution": false, + "dance.disableArbitraryCommandExecution": false, "contributes": { "configuration": { "type": "object", "title": "Dance", "properties": { + "dance.defaultMode": { + "type": "string", + "scope": "language-overridable", + "default": "normal", + "description": "Controls which mode is set by default when an editor is opened.", + "pattern": "^[a-zA-Z]\\w*$", + "patternErrorMessage": "" + }, + "dance.modes": { + "type": "object", + "scope": "language-overridable", + "additionalProperties": { + "type": "object", + "propertyNames": { + "pattern": "^[a-zA-Z]\\w*$", + "patternErrorMessage": "" + }, + "properties": { + "inheritFrom": { + "type": [ + "string", + "null" + ], + "description": "Controls how default configuration options are obtained for this mode. Specify a string to inherit from the mode with the given name, and null to inherit from the VS Code configuration.", + "pattern": "^[a-zA-Z]\\w*$", + "patternErrorMessage": "" + }, + "cursorStyle": { + "enum": [ + "line", + "block", + "underline", + "line-thin", + "block-outline", + "underline-thin", + "inherit", + null + ], + "description": "Controls the cursor style." + }, + "lineHighlight": { + "type": [ + "string", + "null" + ], + "markdownDescription": "Controls the line highlighting applied to active lines. Can be an hex color, a [theme color](https://code.visualstudio.com/api/references/theme-color) or null.", + "pattern": "^(#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|#[a-fA-F0-9]{8}|\\$([a-zA-Z]+(\\.[a-zA-Z]+)+))$", + "patternErrorMessage": "Color should be an hex color or a '$' sign followed by a color identifier." + }, + "lineNumbers": { + "enum": [ + "off", + "on", + "relative", + "inherit", + null + ], + "description": "Controls the display of line numbers.", + "enumDescriptions": [ + "No line numbers.", + "Absolute line numbers.", + "Relative line numbers.", + "Inherit from `editor.lineNumbers`." + ] + }, + "onEnterMode": { + "type": "array", + "items": { + "type": [ + "array", + "object", + "string" + ], + "properties": { + "command": { + "type": "string" + }, + "args": {} + }, + "required": [ + "command" + ] + }, + "description": "Controls what commands should be executed upon entering this mode." + }, + "onLeaveMode": { + "type": "array", + "items": { + "type": [ + "array", + "object", + "string" + ], + "properties": { + "command": { + "type": "string" + }, + "args": {} + }, + "required": [ + "command" + ] + }, + "description": "Controls what commands should be executed upon leaving this mode." + }, + "selectionBehavior": { + "enum": [ + "caret", + "character", + null + ], + "default": "caret", + "description": "Controls how selections behave within VS Code.", + "markdownEnumDescriptions": [ + "Selections are anchored to carets, which is the native VS Code behavior; that is, they are positioned *between* characters and can therefore be empty.", + "Selections are anchored to characters, like Kakoune; that is, they are positioned *on* characters, and therefore cannot be empty. Additionally, one-character selections will behave as if they were non-directional, like Kakoune." + ] + }, + "decorations": { + "type": [ + "array", + "object", + "null" + ], + "properties": { + "applyTo": { + "enum": [ + "all", + "main", + "secondary" + ], + "default": "all", + "description": "The selections to apply this style to.", + "enumDescriptions": [ + "Apply to all selections.", + "Apply to main selection only.", + "Apply to all selections except main selection." + ] + }, + "backgroundColor": { + "type": "string", + "pattern": "^(#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|#[a-fA-F0-9]{8}|\\$([a-zA-Z]+(\\.[a-zA-Z]+)+))$", + "patternErrorMessage": "Color should be an hex color or a '$' sign followed by a color identifier." + }, + "borderColor": { + "type": "string", + "pattern": "^(#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|#[a-fA-F0-9]{8}|\\$([a-zA-Z]+(\\.[a-zA-Z]+)+))$", + "patternErrorMessage": "Color should be an hex color or a '$' sign followed by a color identifier." + }, + "borderStyle": { + "type": "string" + }, + "borderWidth": { + "type": "string" + }, + "borderRadius": { + "type": "string" + }, + "isWholeLine": { + "type": "boolean", + "default": false + } + }, + "description": "The decorations to apply to selections.", + "items": { + "type": "object", + "properties": { + "applyTo": { + "enum": [ + "all", + "main", + "secondary" + ], + "default": "all", + "description": "The selections to apply this style to.", + "enumDescriptions": [ + "Apply to all selections.", + "Apply to main selection only.", + "Apply to all selections except main selection." + ] + }, + "backgroundColor": { + "type": "string", + "pattern": "^(#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|#[a-fA-F0-9]{8}|\\$([a-zA-Z]+(\\.[a-zA-Z]+)+))$", + "patternErrorMessage": "Color should be an hex color or a '$' sign followed by a color identifier." + }, + "borderColor": { + "type": "string", + "pattern": "^(#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|#[a-fA-F0-9]{8}|\\$([a-zA-Z]+(\\.[a-zA-Z]+)+))$", + "patternErrorMessage": "Color should be an hex color or a '$' sign followed by a color identifier." + }, + "borderStyle": { + "type": "string" + }, + "borderWidth": { + "type": "string" + }, + "borderRadius": { + "type": "string" + }, + "isWholeLine": { + "type": "boolean", + "default": false + } + } + } + } + }, + "additionalProperties": false + }, + "default": { + "insert": {}, + "normal": { + "lineNumbers": "relative", + "decorations": { + "applyTo": "main", + "backgroundColor": "$editor.hoverHighlightBackground", + "isWholeLine": true + }, + "onEnterMode": [ + [ + ".selections.restore", + { + "register": " ^", + "try": true + } + ] + ], + "onLeaveMode": [ + [ + ".selections.save", + { + "register": " ^", + "style": { + "borderColor": "$editor.selectionBackground", + "borderStyle": "solid", + "borderWidth": "2px", + "borderRadius": "1px" + }, + "until": [ + [ + "mode-did-change", + { + "include": "normal" + } + ], + [ + "selections-did-change" + ] + ] + } + ] + ] + } + }, + "description": "Controls the different modes available in Dance." + }, + "dance.menus": { + "type": "object", + "scope": "language-overridable", + "description": "Controls the different menus available in Dance.", + "additionalProperties": { + "type": "object", + "properties": { + "items": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Text shown in the menu." + }, + "command": { + "type": "string", + "description": "Command to execute on item selection." + }, + "args": { + "type": "array", + "description": "Arguments to the command to execute." + } + }, + "required": [ + "command" + ] + } + } + }, + "additionalProperties": false + }, + "default": { + "object": { + "items": { + "b()": { + "command": "dance.seek.object", + "args": [ + { + "input": "\\((?#inner)\\)" + } + ], + "text": "parenthesis block" + }, + "B{}": { + "command": "dance.seek.object", + "args": [ + { + "input": "\\{(?#inner)\\}" + } + ], + "text": "braces block" + }, + "r[]": { + "command": "dance.seek.object", + "args": [ + { + "input": "\\[(?#inner)\\]" + } + ], + "text": "brackets block" + }, + "a<>": { + "command": "dance.seek.object", + "args": [ + { + "input": "<(?#inner)>" + } + ], + "text": "angle block" + }, + "Q\"": { + "command": "dance.seek.object", + "args": [ + { + "input": "(?#noescape)\"(?#inner)(?#noescape)\"" + } + ], + "text": "double quote string" + }, + "q'": { + "command": "dance.seek.object", + "args": [ + { + "input": "(?#noescape)'(?#inner)(?#noescape)'" + } + ], + "text": "single quote string" + }, + "g`": { + "command": "dance.seek.object", + "args": [ + { + "input": "(?#noescape)`(?#inner)(?#noescape)`" + } + ], + "text": "grave quote string" + }, + "w": { + "command": "dance.seek.object", + "args": [ + { + "input": "[\\p{L}]+(?[^\\S\\n]+)" + } + ], + "text": "word" + }, + "W": { + "command": "dance.seek.object", + "args": [ + { + "input": "[\\S]+(?[^\\S\\n]+)" + } + ], + "text": "WORD" + }, + "s": { + "command": "dance.seek.object", + "args": [ + { + "input": "(?#predefined=sentence)" + } + ], + "text": "sentence" + }, + "p": { + "command": "dance.seek.object", + "args": [ + { + "input": "(?#predefined=paragraph)" + } + ], + "text": "paragraph" + }, + " ": { + "command": "dance.seek.object", + "args": [ + { + "input": "(?[\\s]+)[^\\S\\n]+(?[\\s]+)" + } + ], + "text": "whitespaces" + }, + "i": { + "command": "dance.seek.object", + "args": [ + { + "input": "(?#predefined=indent)" + } + ], + "text": "indent" + }, + "n": { + "command": "dance.seek.object", + "args": [ + { + "input": "(?#singleline)-?[\\d_]+(\\.[0-9]+)?([eE]\\d+)?" + } + ], + "text": "number" + }, + "u": { + "command": "dance.seek.object", + "args": [ + { + "input": "(?#predefined=argument)" + } + ], + "text": "argument" + }, + "c": { + "command": "dance.seek.object", + "text": "custom object desc" + } + } + }, + "goto": { + "items": { + "h": { + "text": "to line start", + "command": "dance.select.lineStart" + }, + "l": { + "text": "to line end", + "command": "dance.select.lineEnd" + }, + "i": { + "text": "to non-blank line start", + "command": "dance.select.lineStart", + "args": [ + { + "skipBlank": true + } + ] + }, + "gk": { + "text": "to first line", + "command": "dance.select.lineStart", + "args": [ + { + "count": 0 + } + ] + }, + "j": { + "text": "to last line", + "command": "dance.select.lastLine" + }, + "e": { + "text": "to last char of last line", + "command": "dance.select.lineEnd", + "args": [ + { + "count": 2147483647 + } + ] + }, + "t": { + "text": "to first displayed line", + "command": "dance.select.firstVisibleLine" + }, + "c": { + "text": "to middle displayed line", + "command": "dance.select.middleVisibleLine" + }, + "b": { + "text": "to last displayed line", + "command": "dance.select.lastVisibleLine" + }, + "f": { + "text": "to file whose name is selected", + "command": "dance.selections.open" + }, + ".": { + "text": "to last buffer modification position", + "command": "dance.select.lastModification" + } + } + }, + "view": { + "items": { + "vc": { + "text": "center cursor vertically", + "command": "dance.view.line", + "args": [ + { + "at": "center" + } + ] + }, + "t": { + "text": "cursor on top", + "command": "dance.view.line", + "args": [ + { + "at": "top" + } + ] + }, + "b": { + "text": "cursor on bottom", + "command": "dance.view.line", + "args": [ + { + "at": "bottom" + } + ] + }, + "j": { + "text": "scroll down", + "command": "editorScroll", + "args": [ + { + "to": "down", + "by": "line", + "revealCursor": true + } + ] + }, + "k": { + "text": "scroll up", + "command": "editorScroll", + "args": [ + { + "to": "up", + "by": "line", + "revealCursor": true + } + ] + } + } + } + } + }, "dance.enabled": { "type": "boolean", "default": true, - "description": "Controls whether the Dance keybindings are enabled." + "description": "Controls whether the Dance keybindings are enabled.", + "deprecationMessage": "dance.enabled is deprecated; disable the Dance extension instead." }, "dance.normalMode.lineHighlight": { "type": [ @@ -70,7 +631,8 @@ "null" ], "default": "editor.hoverHighlightBackground", - "markdownDescription": "Controls the line highlighting applied to active lines in normal mode. Can be an hex color, a [theme color](https://code.visualstudio.com/api/references/theme-color) or null." + "markdownDescription": "Controls the line highlighting applied to active lines in normal mode. Can be an hex color, a [theme color](https://code.visualstudio.com/api/references/theme-color) or null.", + "markdownDeprecationMessage": "Built-in modes are deprecated. Use `#dance.modes#` instead." }, "dance.insertMode.lineHighlight": { "type": [ @@ -78,7 +640,8 @@ "null" ], "default": null, - "markdownDescription": "Controls the line highlighting applied to active lines in insert mode. Can be an hex color, a [theme color](https://code.visualstudio.com/api/references/theme-color) or null." + "markdownDescription": "Controls the line highlighting applied to active lines in insert mode. Can be an hex color, a [theme color](https://code.visualstudio.com/api/references/theme-color) or null.", + "markdownDeprecationMessage": "Built-in modes are deprecated. Use `#dance.modes#` instead." }, "dance.normalMode.lineNumbers": { "enum": [ @@ -94,7 +657,8 @@ "Absolute line numbers.", "Relative line numbers.", "Inherit from `editor.lineNumbers`." - ] + ], + "markdownDeprecationMessage": "Built-in modes are deprecated. Use `#dance.modes#` instead." }, "dance.insertMode.lineNumbers": { "enum": [ @@ -110,7 +674,8 @@ "Absolute line numbers.", "Relative line numbers.", "Inherit from `editor.lineNumbers`." - ] + ], + "markdownDeprecationMessage": "Built-in modes are deprecated. Use `#dance.modes#` instead." }, "dance.normalMode.cursorStyle": { "enum": [ @@ -123,7 +688,8 @@ "inherit" ], "default": "inherit", - "description": "Controls the cursor style in normal mode." + "description": "Controls the cursor style in normal mode.", + "markdownDeprecationMessage": "Built-in modes are deprecated. Use `#dance.modes#` instead." }, "dance.insertMode.cursorStyle": { "enum": [ @@ -136,7 +702,8 @@ "inherit" ], "default": "inherit", - "description": "Controls the cursor style in insert mode." + "description": "Controls the cursor style in insert mode.", + "markdownDeprecationMessage": "Built-in modes are deprecated. Use `#dance.modes#` instead." }, "dance.insertMode.selectionStyle": { "type": "object", @@ -163,7 +730,8 @@ "borderRadius": { "type": "string" } - } + }, + "markdownDeprecationMessage": "Built-in modes are deprecated. Use `#dance.modes#` instead." }, "dance.selectionBehavior": { "enum": [ @@ -175,1388 +743,1993 @@ "markdownEnumDescriptions": [ "Selections are anchored to carets, which is the native VS Code behavior; that is, they are positioned *between* characters and can therefore be empty.", "Selections are anchored to characters, like Kakoune; that is, they are positioned *on* characters, and therefore cannot be empty. Additionally, one-character selections will behave as if they were non-directional, like Kakoune." - ] - }, - "dance.menus": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "items": { - "type": "object", - "additionalProperties": { - "type": [ - "object", - "null" - ], - "properties": { - "text": { - "type": "string" - }, - "command": { - "type": "string" - }, - "args": { - "type": "array" - } - } - } - } - }, - "additionalProperties": false - }, - "default": { - "object": { - "items": { - "b()": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "parens" - } - ], - "text": "parenthesis block" - }, - "B{}": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "braces" - } - ], - "text": "braces block" - }, - "r[]": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "brackets" - } - ], - "text": "brackets block" - }, - "a<>": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "angleBrackets" - } - ], - "text": "angle block" - }, - "Q\"": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "doubleQuoteString" - } - ], - "text": "double quote string" - }, - "q'": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "singleQuoteString" - } - ], - "text": "single quote string" - }, - "g`": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "graveQuoteString" - } - ], - "text": "grave quote string" - }, - "w": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "word" - } - ], - "text": "word" - }, - "W": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "WORD" - } - ], - "text": "WORD" - }, - "s": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "sentence" - } - ], - "text": "sentence" - }, - "p": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "paragraph" - } - ], - "text": "paragraph" - }, - " ": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "whitespaces" - } - ], - "text": "whitespaces" - }, - "i": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "indent" - } - ], - "text": "indent" - }, - "n": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "number" - } - ], - "text": "number" - }, - "u": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "argument" - } - ], - "text": "argument" - }, - "c": { - "command": "dance.objects.performSelection", - "args": [ - { - "object": "custom" - } - ], - "text": "custom object desc" - } - } - }, - "goto": { - "items": { - "h": { - "text": "go to line start", - "command": "dance.goto.lineStart" - }, - "l": { - "text": "go to line end", - "command": "dance.goto.lineEnd" - }, - "i": { - "text": "go to non-blank line start", - "command": "dance.goto.lineStart.nonBlank" - }, - "g": { - "text": "go to first line", - "command": "dance.goto.firstLine" - }, - "k": { - "text": "go to first line", - "command": "dance.goto.firstLine" - }, - "j": { - "text": "go to last line", - "command": "dance.goto.lastLine" - }, - "e": { - "text": "go to last char of last line", - "command": "dance.goto.lastCharacter" - }, - "t": { - "text": "go to the first displayed line", - "command": "dance.goto.firstVisibleLine" - }, - "c": { - "text": "go to the middle displayed line", - "command": "dance.goto.middleVisibleLine" - }, - "b": { - "text": "go to the last displayed line", - "command": "dance.goto.lastVisibleLine" - }, - "f": { - "text": "go to file whose name is selected", - "command": "dance.goto.selectedFile" - }, - ".": { - "text": "go to last buffer modification position", - "command": "dance.goto.lastModification" - } - } - }, - "goto.extend": { - "items": { - "h": { - "text": "extend to line start", - "command": "dance.goto.lineStart.extend" - }, - "l": { - "text": "extend to line end", - "command": "dance.goto.lineEnd.extend" - }, - "i": { - "text": "extend to non-blank line start", - "command": "dance.goto.lineStart.nonBlank.extend" - }, - "g": { - "text": "extend to first line", - "command": "dance.goto.firstLine.extend" - }, - "k": { - "text": "extend to first line", - "command": "dance.goto.firstLine.extend" - }, - "j": { - "text": "extend to last line", - "command": "dance.goto.lastLine.extend" - }, - "e": { - "text": "extend to last char of last line", - "command": "dance.goto.lastCharacter.extend" - }, - "t": { - "text": "extend to the first displayed line", - "command": "dance.goto.firstVisibleLine.extend" - }, - "c": { - "text": "extend to the middle displayed line", - "command": "dance.goto.middleVisibleLine.extend" - }, - "b": { - "text": "extend to the last displayed line", - "command": "dance.goto.lastVisibleLine.extend" - }, - "f": { - "text": "extend to file whose name is selected", - "command": "dance.goto.selectedFile.extend" - }, - ".": { - "text": "extend to last buffer modification position", - "command": "dance.goto.lastModification.extend" - } - } - } - } + ], + "markdownDeprecationMessage": "Built-in modes are deprecated. Use `#dance.modes#` instead." } } }, "commands": [ { - "command": "dance.toggle", - "title": "Toggle", - "description": "Toggles Dance key bindings.", + "command": "dance.dev.setSelectionBehavior", + "title": "Set the selection behavior of the specified mode", "category": "Dance" }, { - "command": "dance.set.normal", - "title": "Set mode to Normal", - "description": "Set Dance mode to Normal.", - "category": "Dance" - }, - { - "command": "dance.set.insert", - "title": "Set mode to Insert", - "description": "Set Dance mode to Insert.", - "category": "Dance" - }, - { - "command": "dance.tmp.normal", - "title": "Temporary normal mode", - "description": "Switches to normal mode temporarily.", - "category": "Dance" - }, - { - "command": "dance.tmp.insert", - "title": "Temporary insert mode", - "description": "Switches to insert mode temporarily.", - "category": "Dance" - }, - { - "command": "dance.insert.before", - "title": "Insert before", - "description": "Start insert before the current selections.", - "category": "Dance" - }, - { - "command": "dance.insert.after", - "title": "Insert after", - "description": "Start insert after the current selections.", - "category": "Dance" - }, - { - "command": "dance.insert.lineStart", - "title": "Insert at line start", - "description": "Start insert at line start of each selection.", - "category": "Dance" - }, - { - "command": "dance.insert.lineEnd", - "title": "Insert at line end", - "description": "Start insert at line end of each selection.", - "category": "Dance" - }, - { - "command": "dance.insert.newLine.below", - "title": "Insert new line below", - "description": "Create new line and start insert below.", - "category": "Dance" - }, - { - "command": "dance.insert.newLine.above", - "title": "Insert new line above", - "description": "Create new line and start insert above.", - "category": "Dance" - }, - { - "command": "dance.newLine.below", - "title": "Add new line below", - "description": "Add a new line below, without entering insert mode.", - "category": "Dance" - }, - { - "command": "dance.newLine.above", - "title": "Add new line above", - "description": "Add a new line above, without entering insert mode.", - "category": "Dance" - }, - { - "command": "dance.repeat.insert", - "title": "Repeat last insert-mode change", - "description": "Repeat last insert-mode change.", - "category": "Dance" - }, - { - "command": "dance.repeat.objectOrSelectTo", - "title": "Repeat last object select / character find", - "description": "Repeat last object select / character find.", - "category": "Dance" - }, - { - "command": "dance.left", - "title": "Move left", - "description": "Move left.", - "category": "Dance" - }, - { - "command": "dance.right", - "title": "Move right", - "description": "Move right.", - "category": "Dance" - }, - { - "command": "dance.up", - "title": "Move up", - "description": "Move up.", - "category": "Dance" - }, - { - "command": "dance.down", - "title": "Move down", - "description": "Move down.", - "category": "Dance" - }, - { - "command": "dance.up.page", - "title": "Scroll one page up", - "description": "Scroll one page up.", - "category": "Dance" - }, - { - "command": "dance.down.page", - "title": "Scroll one page down", - "description": "Scroll one page down.", - "category": "Dance" - }, - { - "command": "dance.up.halfPage", - "title": "Scroll half a page up", - "description": "Scroll half a page up.", - "category": "Dance" - }, - { - "command": "dance.down.halfPage", - "title": "Scroll half a page down", - "description": "Scroll half a page down.", - "category": "Dance" - }, - { - "command": "dance.select.to.included", - "title": "Select to", - "description": "Select to the next character pressed, including it.", - "category": "Dance" - }, - { - "command": "dance.select.to.excluded", - "title": "Select until", - "description": "Select until the next character pressed, excluding it.", - "category": "Dance" - }, - { - "command": "dance.select.buffer", - "title": "Select whole buffer", - "description": "Select whole buffer.", - "category": "Dance" - }, - { - "command": "dance.select.line", - "title": "Select line", - "description": "Select line on which the end of each selection lies (or next line when end lies on an end-of-line).", - "category": "Dance" - }, - { - "command": "dance.select.toLineBegin", - "title": "Select to line beginning", - "description": "Select to line beginning.", - "category": "Dance" - }, - { - "command": "dance.select.toLineEnd", - "title": "Select to line end", - "description": "Select to line end.", - "category": "Dance" - }, - { - "command": "dance.select.enclosing", - "title": "Select enclosing characters", - "description": "Select enclosing characters.", - "category": "Dance" - }, - { - "command": "dance.expandLines", - "title": "Extend lines", - "description": "Extend selections to contain full lines (including end-of-lines).", - "category": "Dance" - }, - { - "command": "dance.trimLines", - "title": "Trim lines", - "description": "Trim selections to only contain full lines (from start to line break).", - "category": "Dance" - }, - { - "command": "dance.trimSelections", - "title": "Trim selections", - "description": "Trim whitespace at beginning and end of selections.", - "category": "Dance" - }, - { - "command": "dance.select.word", - "title": "Select to next word start", - "description": "Select the word and following whitespaces on the right of the end of each selection.", - "category": "Dance" - }, - { - "command": "dance.select.word.previous", - "title": "Select to previous word start", - "description": "Select preceding whitespaces and the word on the left of the end of each selection.", - "category": "Dance" - }, - { - "command": "dance.select.word.end", - "title": "Select to next word end", - "description": "Select preceding whitespaces and the word on the right of the end of each selection.", - "category": "Dance" - }, - { - "command": "dance.select.word.alt", - "title": "Select to next non-whitespace word start", - "description": "Select the non-whitespace word and following whitespaces on the right of the end of each selection.", - "category": "Dance" - }, - { - "command": "dance.select.word.alt.previous", - "title": "Select to previous non-whitespace word start", - "description": "Select preceding whitespaces and the non-whitespace word on the left of the end of each selection.", - "category": "Dance" - }, - { - "command": "dance.select.word.alt.end", - "title": "Select to next non-whitespace word end", - "description": "Select preceding whitespaces and the non-whitespace word on the right of the end of each selection.", - "category": "Dance" - }, - { - "command": "dance.select", - "title": "Select", - "description": "Select within current selections according to a RegExp.", - "category": "Dance" - }, - { - "command": "dance.split", - "title": "Split", - "description": "Split within current selections according to a RegExp.", - "category": "Dance" - }, - { - "command": "dance.split.lines", - "title": "Split lines", - "description": "Split selections into lines.", - "category": "Dance" - }, - { - "command": "dance.select.firstLast", - "title": "Select first and last characters", - "description": "Select first and last characters of each selection.", - "category": "Dance" - }, - { - "command": "dance.select.copy", - "title": "Copy selection to next line", - "description": "Copy selection to next line.", - "category": "Dance" - }, - { - "command": "dance.select.copy.backwards", - "title": "Copy selection to previous line", - "description": "Copy selection to previous line.", - "category": "Dance" - }, - { - "command": "dance.selections.reduce", - "title": "Reduce selections", - "description": "Reduce selections to their cursor.", - "category": "Dance" - }, - { - "command": "dance.selections.flip", - "title": "Flip selections", - "description": "Flip the direction of each selection.", - "category": "Dance" - }, - { - "command": "dance.selections.forward", - "title": "Forward selections", - "description": "Ensure selections are in forward direction (the active cursor is after the anchor).", - "category": "Dance" - }, - { - "command": "dance.selections.backward", - "title": "Backward selections", - "description": "Ensure selections are in backward direction (the active cursor is before the anchor).", - "category": "Dance" - }, - { - "command": "dance.selections.clear", - "title": "Clear selections", - "description": "Clear selections (except main)", - "category": "Dance" - }, - { - "command": "dance.selections.clearMain", - "title": "Clear main selection", - "description": "Clear main selection.", - "category": "Dance" - }, - { - "command": "dance.selections.keepMatching", - "title": "Keep matching selections", - "description": "Keep selections that match a RegExp.", - "category": "Dance" - }, - { - "command": "dance.selections.clearMatching", - "title": "Clear matching selections", - "description": "Clear selections that match a RegExp.", - "category": "Dance" - }, - { - "command": "dance.selections.merge", - "title": "Merge contiguous selections", - "description": "Merge contiguous selections together, including across lines.", - "category": "Dance" - }, - { - "command": "dance.selections.align", + "command": "dance.edit.align", "title": "Align selections", - "description": "Align selections, aligning the cursor of each selection by inserting spaces before the first character of each selection.", "category": "Dance" }, { - "command": "dance.selections.align.copy", - "title": "Copy indentation", - "description": "Copy the indentation of the main selection (or the count one if a count is given) to all other ones.", - "category": "Dance" - }, - { - "command": "dance.delete.yank", - "title": "Yank and delete", - "description": "Yank and delete selections.", - "category": "Dance" - }, - { - "command": "dance.delete.insert.yank", - "title": "Yank, delete and insert", - "description": "Yank, delete and enter insert mode.", - "category": "Dance" - }, - { - "command": "dance.delete.noYank", - "title": "Delete without yank", - "description": "Delete selections without yanking.", - "category": "Dance" - }, - { - "command": "dance.delete.insert.noYank", - "title": "Delete and insert without yank", - "description": "Delete selections without yanking and enter insert mode.", - "category": "Dance" - }, - { - "command": "dance.yank", - "title": "Yank", - "description": "Yank selections.", - "category": "Dance" - }, - { - "command": "dance.paste.after", - "title": "Paste after", - "description": "Paste after the end of each selection.", - "category": "Dance" - }, - { - "command": "dance.paste.before", - "title": "Paste before", - "description": "Paste before the start of each selection.", - "category": "Dance" - }, - { - "command": "dance.paste.select.after", - "title": "Paste after and select", - "description": "Paste after the end of each selection and select pasted text.", - "category": "Dance" - }, - { - "command": "dance.paste.select.before", - "title": "Paste before and select", - "description": "Paste before the start of each selection and select pasted text.", - "category": "Dance" - }, - { - "command": "dance.paste.replace", - "title": "Replace", - "description": "Replace selections with yanked text.", - "category": "Dance" - }, - { - "command": "dance.paste.replace.every", - "title": "Replace with every", - "description": "Replace selections with every yanked text.", - "category": "Dance" - }, - { - "command": "dance.replace.characters", - "title": "Replace character", - "description": "Replace each selected character with the next entered one.", - "category": "Dance" - }, - { - "command": "dance.join", - "title": "Join lines", - "description": "Join selected lines.", - "category": "Dance" - }, - { - "command": "dance.join.select", - "title": "Join lines and select spaces", - "description": "Join selected lines and select spaces inserted in place of line breaks.", - "category": "Dance" - }, - { - "command": "dance.indent", - "title": "Indent", - "description": "Indent selected lines.", - "category": "Dance" - }, - { - "command": "dance.indent.withEmpty", - "title": "Indent (including empty)", - "description": "Indent selected lines (including empty lines).", - "category": "Dance" - }, - { - "command": "dance.deindent", - "title": "Deindent", - "description": "Deindent selected lines.", - "category": "Dance" - }, - { - "command": "dance.deindent.further", - "title": "Deindent (including incomplete indent)", - "description": "Deindent selected lines (and remove additional incomplete indent).", - "category": "Dance" - }, - { - "command": "dance.toLowerCase", - "title": "Transform to lowercase", - "description": "Transform to lowercase.", - "category": "Dance" - }, - { - "command": "dance.toUpperCase", - "title": "Transform to uppercase", - "description": "Transform to uppercase.", - "category": "Dance" - }, - { - "command": "dance.swapCase", + "command": "dance.edit.case.swap", "title": "Swap case", - "description": "Swap case.", "category": "Dance" }, { - "command": "dance.pipe.filter", - "title": "Filter through pipe", - "description": "Pipe each selection to a program, and keeps it if the program returns 0.", + "command": "dance.edit.case.toLower", + "title": "Transform to lower case", "category": "Dance" }, { - "command": "dance.pipe.replace", - "title": "Pipe and replace", - "description": "Pipe each selection to a command, and replaces it with its output.", + "command": "dance.edit.case.toUpper", + "title": "Transform to upper case", "category": "Dance" }, { - "command": "dance.pipe.ignore", - "title": "Pipe", - "description": "Pipe each selection to a command, ignoring their results.", + "command": "dance.edit.copyIndentation", + "title": "Copy indentation", "category": "Dance" }, { - "command": "dance.pipe.append", - "title": "Pipe and append", - "description": "Pipe each selection to a command, appending the output after the selection.", + "command": "dance.edit.deindent", + "title": "Deindent selected lines", "category": "Dance" }, { - "command": "dance.pipe.prepend", - "title": "Pipe and prepend", - "description": "Pipe each selection to a command, prepending the output before the selection.", + "command": "dance.edit.deindent.withIncomplete", + "title": "Deindent selected lines (including incomplete indent)", "category": "Dance" }, { - "command": "dance.history.undo", - "title": "Undo", - "description": "Undo.", + "command": "dance.edit.delete", + "title": "Delete", "category": "Dance" }, { - "command": "dance.history.backward", - "title": "Move backward in history", - "description": "Move backward in history.", + "command": "dance.edit.delete-insert", + "title": "Delete and switch to Insert", + "category": "Dance" + }, + { + "command": "dance.edit.indent", + "title": "Indent selected lines", + "category": "Dance" + }, + { + "command": "dance.edit.indent.withEmpty", + "title": "Indent selected lines (including empty lines)", + "category": "Dance" + }, + { + "command": "dance.edit.insert", + "title": "Insert contents of register", + "category": "Dance" + }, + { + "command": "dance.edit.join", + "title": "Join lines", + "category": "Dance" + }, + { + "command": "dance.edit.join.select", + "title": "Join lines and select inserted separators", + "category": "Dance" + }, + { + "command": "dance.edit.newLine.above", + "title": "Insert new line above each selection", + "category": "Dance" + }, + { + "command": "dance.edit.newLine.above.insert", + "title": "Insert new line above and switch to insert", + "category": "Dance" + }, + { + "command": "dance.edit.newLine.below", + "title": "Insert new line below each selection", + "category": "Dance" + }, + { + "command": "dance.edit.newLine.below.insert", + "title": "Insert new line below and switch to insert", + "category": "Dance" + }, + { + "command": "dance.edit.paste.after", + "title": "Paste after", + "category": "Dance" + }, + { + "command": "dance.edit.paste.after.select", + "title": "Paste after and select", + "category": "Dance" + }, + { + "command": "dance.edit.paste.before", + "title": "Paste before", + "category": "Dance" + }, + { + "command": "dance.edit.paste.before.select", + "title": "Paste before and select", + "category": "Dance" + }, + { + "command": "dance.edit.replaceCharacters", + "title": "Replace characters", + "category": "Dance" + }, + { + "command": "dance.edit.selectRegister-insert", + "title": "Pick register and replace", + "category": "Dance" + }, + { + "command": "dance.edit.yank-delete", + "title": "Copy and delete", + "category": "Dance" + }, + { + "command": "dance.edit.yank-delete-insert", + "title": "Copy, delete and switch to Insert", + "category": "Dance" + }, + { + "command": "dance.edit.yank-replace", + "title": "Copy and replace", + "category": "Dance" + }, + { + "command": "dance.history.recording.play", + "title": "Replay recording", + "category": "Dance" + }, + { + "command": "dance.history.recording.start", + "title": "Start recording", + "category": "Dance" + }, + { + "command": "dance.history.recording.stop", + "title": "Stop recording", "category": "Dance" }, { "command": "dance.history.redo", "title": "Redo", - "description": "Redo.", "category": "Dance" }, { - "command": "dance.history.forward", - "title": "Move forward in history", - "description": "Move forward in history.", + "command": "dance.history.redo.selections", + "title": "Redo a change of selections", "category": "Dance" }, { "command": "dance.history.repeat", "title": "Repeat last change", - "description": "Repeat last change.", + "category": "Dance" + }, + { + "command": "dance.history.repeat.edit", + "title": "Repeat last edit without a command", + "category": "Dance" + }, + { + "command": "dance.history.repeat.seek", + "title": "Repeat last seek", "category": "Dance" }, { "command": "dance.history.repeat.selection", "title": "Repeat last selection change", - "description": "Repeat last selection change.", "category": "Dance" }, { - "command": "dance.history.repeat.edit", - "title": "Repeat last edit change", - "description": "Repeat last edit change.", + "command": "dance.history.undo", + "title": "Undo", "category": "Dance" }, { - "command": "dance.macros.record.start", - "title": "Start recording macro", - "description": "Start recording macro.", + "command": "dance.history.undo.selections", + "title": "Undo a change of selections", "category": "Dance" }, { - "command": "dance.macros.record.stop", - "title": "Stop recording macro", - "description": "Stop recording macro.", - "category": "Dance" - }, - { - "command": "dance.macros.play", - "title": "Play macro", - "description": "Play macro.", - "category": "Dance" - }, - { - "command": "dance.rotate", - "title": "Rotate", - "description": "Rotate each selection clockwise.", - "category": "Dance" - }, - { - "command": "dance.rotate.backwards", - "title": "Rotate backwards", - "description": "Rotate each selection counter-clockwise.", - "category": "Dance" - }, - { - "command": "dance.rotate.content", - "title": "Rotate selection content", - "description": "Rotate each selection (as well as its content) clockwise.", - "category": "Dance" - }, - { - "command": "dance.rotate.content.backwards", - "title": "Rotate selection content backwards", - "description": "Rotate each selection (as well as its content) counter-clockwise.", - "category": "Dance" - }, - { - "command": "dance.rotate.contentOnly", - "title": "Rotate content only", - "description": "Rotate each selection content clockwise, without changing selections.", - "category": "Dance" - }, - { - "command": "dance.rotate.contentOnly.backwards", - "title": "Rotate content only backwards", - "description": "Rotate each selection content counter-clockwise, without changing selections.", - "category": "Dance" - }, - { - "command": "dance.search", - "title": "Search", - "description": "Search for the given input string.", - "category": "Dance" - }, - { - "command": "dance.search.backwards", - "title": "Search backwards", - "description": "Search for the given input string before the current selections.", - "category": "Dance" - }, - { - "command": "dance.search.selection.smart", - "title": "Search current selections (smart)", - "description": "Search current selections (smart).", - "category": "Dance" - }, - { - "command": "dance.search.selection", - "title": "Search current selections", - "description": "Search current selections.", - "category": "Dance" - }, - { - "command": "dance.search.next", - "title": "Select next match", - "description": "Select next match after the main selection.", - "category": "Dance" - }, - { - "command": "dance.search.next.add", - "title": "Add next match", - "description": "Add a new selection with the next match after the main selection.", - "category": "Dance" - }, - { - "command": "dance.search.previous", - "title": "Select previous match", - "description": "Select previous match before the main selection.", - "category": "Dance" - }, - { - "command": "dance.search.previous.add", - "title": "Add previous match", - "description": "Add a new selection with the previous match before the main selection.", - "category": "Dance" - }, - { - "command": "dance.objects.performSelection", - "title": "Perform selections specified in the arguments.", - "description": "Perform selections specified in the arguments..", - "category": "Dance" - }, - { - "command": "dance.goto", - "title": "Go to...", - "description": "Shows prompt to jump somewhere", - "category": "Dance" - }, - { - "command": "dance.goto.lineStart", - "title": "Go to line start", - "description": "Go to line start.", - "category": "Dance" - }, - { - "command": "dance.goto.lineStart.nonBlank", - "title": "Go to non-blank line start", - "description": "Go to first non-whitespace character of the line", - "category": "Dance" - }, - { - "command": "dance.goto.lineEnd", - "title": "Go to line end", - "description": "Go to line end.", - "category": "Dance" - }, - { - "command": "dance.goto.firstLine", - "title": "Go to first line", - "description": "Go to first line.", - "category": "Dance" - }, - { - "command": "dance.goto.lastLine", - "title": "Go to last line", - "description": "Go to last line.", - "category": "Dance" - }, - { - "command": "dance.goto.lastCharacter", - "title": "Go to last character of the document", - "description": "Go to last character of the document.", - "category": "Dance" - }, - { - "command": "dance.goto.firstVisibleLine", - "title": "Go to first visible line", - "description": "Go to first visible line.", - "category": "Dance" - }, - { - "command": "dance.goto.middleVisibleLine", - "title": "Go to middle visible line", - "description": "Go to middle visible line.", - "category": "Dance" - }, - { - "command": "dance.goto.lastVisibleLine", - "title": "Go to last visible line", - "description": "Go to last visible line.", - "category": "Dance" - }, - { - "command": "dance.goto.selectedFile", - "title": "Open file under selection", - "description": "Open file under selection.", - "category": "Dance" - }, - { - "command": "dance.goto.lastModification", - "title": "Go to last buffer modification position", - "description": "Go to last buffer modification position.", - "category": "Dance" - }, - { - "command": "dance.openMenu", - "title": "Open quick-jump menu", - "description": "Open quick-jump menu.", - "category": "Dance" - }, - { - "command": "dance.registers.insert", - "title": "Insert value in register", - "description": "Insert value in register.", - "category": "Dance" - }, - { - "command": "dance.registers.select", - "title": "Select register for next command", - "description": "Select register for next command.", - "category": "Dance" - }, - { - "command": "dance.marks.saveSelections", - "title": "Save selections", - "description": "Save selections.", - "category": "Dance" - }, - { - "command": "dance.marks.restoreSelections", - "title": "Restore selections", - "description": "Restore selections.", - "category": "Dance" - }, - { - "command": "dance.marks.combineSelections.fromCurrent", - "title": "Combine current selections with ones from register", - "description": "Combine current selections with ones from register.", - "category": "Dance" - }, - { - "command": "dance.marks.combineSelections.fromRegister", - "title": "Combine register selections with current ones", - "description": "Combine register selections with current ones.", + "command": "dance.keybindings.setup", + "title": "Set up Dance keybindings", "category": "Dance" }, { "command": "dance.cancel", - "title": "Cancel operation", - "description": "Cancels waiting for input from the user", + "title": "Cancel Dance operation", + "category": "Dance" + }, + { + "command": "dance.ignore", + "title": "Ignore key", + "category": "Dance" + }, + { + "command": "dance.openMenu", + "title": "Open menu", "category": "Dance" }, { "command": "dance.run", "title": "Run code", - "description": "Runs JavaScript code passed in a 'code' argument", "category": "Dance" }, { - "command": "dance.left.extend", - "title": "Move left (extend)", - "description": "Move left (extend).", + "command": "dance.selectRegister", + "title": "Select register for next command", "category": "Dance" }, { - "command": "dance.right.extend", - "title": "Move right (extend)", - "description": "Move right (extend).", + "command": "dance.updateCount", + "title": "Update Dance count", "category": "Dance" }, { - "command": "dance.up.extend", - "title": "Move up (extend)", - "description": "Move up (extend).", + "command": "dance.modes.insert.after", + "title": "Insert after", "category": "Dance" }, { - "command": "dance.down.extend", - "title": "Move down (extend)", - "description": "Move down (extend).", + "command": "dance.modes.insert.before", + "title": "Insert before", "category": "Dance" }, { - "command": "dance.select.to.included.extend", - "title": "Extend to", - "description": "Extend to the next character pressed, including it.", + "command": "dance.modes.insert.lineEnd", + "title": "Insert at line end", "category": "Dance" }, { - "command": "dance.select.to.excluded.extend", - "title": "Extend until", - "description": "Extend with until the next character pressed, excluding it.", + "command": "dance.modes.insert.lineStart", + "title": "Insert at line start", "category": "Dance" }, { - "command": "dance.select.line.extend", - "title": "Extend with line", - "description": "Extend with line on which the end of each selection lies (or next line when end lies on an end-of-line).", + "command": "dance.modes.set", + "title": "Set Dance mode", "category": "Dance" }, { - "command": "dance.select.toLineBegin.extend", - "title": "Extend to line beginning", - "description": "Extend to line beginning.", + "command": "dance.modes.set.insert", + "title": "Set mode to Insert", "category": "Dance" }, { - "command": "dance.select.toLineEnd.extend", - "title": "Extend to line end", - "description": "Extend to line end.", + "command": "dance.modes.set.normal", + "title": "Set mode to Normal", "category": "Dance" }, { - "command": "dance.select.enclosing.extend", - "title": "Extend with enclosing characters", - "description": "Extend with enclosing characters.", + "command": "dance.modes.set.temporarily", + "title": "Set Dance mode temporarily", "category": "Dance" }, { - "command": "dance.select.word.extend", - "title": "Extend to next word start", - "description": "Extend with the word and following whitespaces on the right of the end of each selection.", + "command": "dance.modes.set.temporarily.insert", + "title": "Temporart Insert mode", "category": "Dance" }, { - "command": "dance.select.word.previous.extend", - "title": "Extend to previous word start", - "description": "Extend with preceding whitespaces and the word on the left of the end of each selection.", + "command": "dance.modes.set.temporarily.normal", + "title": "Temporary Normal mode", "category": "Dance" }, { - "command": "dance.select.word.end.extend", - "title": "Extend to next word end", - "description": "Extend with preceding whitespaces and the word on the right of the end of each selection.", + "command": "dance.search", + "title": "Search", "category": "Dance" }, { - "command": "dance.select.word.alt.extend", - "title": "Extend to next non-whitespace word start", - "description": "Extend with the non-whitespace word and following whitespaces on the right of the end of each selection.", + "command": "dance.search.backward", + "title": "Search backward", "category": "Dance" }, { - "command": "dance.select.word.alt.previous.extend", - "title": "Extend to previous non-whitespace word start", - "description": "Extend with preceding whitespaces and the non-whitespace word on the left of the end of each selection.", - "category": "Dance" - }, - { - "command": "dance.select.word.alt.end.extend", - "title": "Extend to next non-whitespace word end", - "description": "Extend with preceding whitespaces and the non-whitespace word on the right of the end of each selection.", + "command": "dance.search.backward.extend", + "title": "Search backward (extend)", "category": "Dance" }, { "command": "dance.search.extend", "title": "Search (extend)", - "description": "Search for the given input string (extend).", "category": "Dance" }, { - "command": "dance.search.backwards.extend", - "title": "Search backwards (extend)", - "description": "Search for the given input string before the current selections (extend).", + "command": "dance.search.next", + "title": "Select next match", "category": "Dance" }, { - "command": "dance.goto.extend", - "title": "Go to... (extend)", - "description": "Shows prompt to jump somewhere", + "command": "dance.search.next.add", + "title": "Add next match", "category": "Dance" }, { - "command": "dance.goto.lineStart.extend", - "title": "Go to line start (extend)", - "description": "Go to line start (extend).", + "command": "dance.search.previous", + "title": "Select previous match", "category": "Dance" }, { - "command": "dance.goto.lineStart.nonBlank.extend", - "title": "Go to non-blank line start (extend)", - "description": "Go to first non-whitespace character of the line", + "command": "dance.search.previous.add", + "title": "Add previous match", "category": "Dance" }, { - "command": "dance.goto.lineEnd.extend", - "title": "Go to line end (extend)", - "description": "Go to line end (extend).", + "command": "dance.search.selection", + "title": "Search current selection", "category": "Dance" }, { - "command": "dance.goto.firstLine.extend", - "title": "Go to first line (extend)", - "description": "Go to first line (extend).", + "command": "dance.search.selection.smart", + "title": "Search current selection (smart)", "category": "Dance" }, { - "command": "dance.goto.lastLine.extend", - "title": "Go to last line (extend)", - "description": "Go to last line (extend).", + "command": "dance.seek", + "title": "Select to character (excluded)", "category": "Dance" }, { - "command": "dance.goto.lastCharacter.extend", - "title": "Go to last character of the document (extend)", - "description": "Go to last character of the document (extend).", + "command": "dance.seek.askObject", + "title": "Select whole object", "category": "Dance" }, { - "command": "dance.goto.firstVisibleLine.extend", - "title": "Go to first visible line (extend)", - "description": "Go to first visible line (extend).", + "command": "dance.seek.askObject.end", + "title": "Select to whole object end", "category": "Dance" }, { - "command": "dance.goto.middleVisibleLine.extend", - "title": "Go to middle visible line (extend)", - "description": "Go to middle visible line (extend).", + "command": "dance.seek.askObject.end", + "title": "Extend to whole object end", "category": "Dance" }, { - "command": "dance.goto.lastVisibleLine.extend", - "title": "Go to last visible line (extend)", - "description": "Go to last visible line (extend).", + "command": "dance.seek.askObject.inner", + "title": "Select inner object", "category": "Dance" }, { - "command": "dance.goto.lastModification.extend", - "title": "Go to last buffer modification position (extend)", - "description": "Go to last buffer modification position (extend).", + "command": "dance.seek.askObject.inner.end", + "title": "Select to inner object end", "category": "Dance" }, { - "command": "dance.select.to.included.backwards", - "title": "Select to (backwards)", - "description": "Select to the next character pressed, including it. (backwards)", + "command": "dance.seek.askObject.inner.end.extend", + "title": "Extend to inner object end", "category": "Dance" }, { - "command": "dance.select.to.excluded.backwards", - "title": "Select until (backwards)", - "description": "Select until the next character pressed, excluding it. (backwards)", + "command": "dance.seek.askObject.inner.start", + "title": "Select to inner object start", "category": "Dance" }, { - "command": "dance.select.enclosing.backwards", - "title": "Select enclosing characters (backwards)", - "description": "Select enclosing characters. (backwards)", + "command": "dance.seek.askObject.inner.start.extend", + "title": "Extend to inner object start", "category": "Dance" }, { - "command": "dance.select.to.included.extend.backwards", - "title": "Extend to (backwards)", - "description": "Extend to the next character pressed, including it. (backwards)", + "command": "dance.seek.askObject.start", + "title": "Select to whole object start", "category": "Dance" }, { - "command": "dance.select.to.excluded.extend.backwards", - "title": "Extend until (backwards)", - "description": "Extend with until the next character pressed, excluding it. (backwards)", + "command": "dance.seek.askObject.start", + "title": "Extend to whole object start", "category": "Dance" }, { - "command": "dance.select.enclosing.extend.backwards", - "title": "Extend with enclosing characters (backwards)", - "description": "Extend with enclosing characters. (backwards)", + "command": "dance.seek.backward", + "title": "Select to character (excluded, backward)", "category": "Dance" }, { - "command": "dance.count.0", - "title": "Count 0", - "description": "Adds 0 to the current counter for the next operation.", + "command": "dance.seek.enclosing", + "title": "Select to next enclosing character", "category": "Dance" }, { - "command": "dance.count.1", - "title": "Count 1", - "description": "Adds 1 to the current counter for the next operation.", + "command": "dance.seek.enclosing.backward", + "title": "Select to previous enclosing character", "category": "Dance" }, { - "command": "dance.count.2", - "title": "Count 2", - "description": "Adds 2 to the current counter for the next operation.", + "command": "dance.seek.enclosing.extend", + "title": "Extend to next enclosing character", "category": "Dance" }, { - "command": "dance.count.3", - "title": "Count 3", - "description": "Adds 3 to the current counter for the next operation.", + "command": "dance.seek.enclosing.extend.backward", + "title": "Extend to previous enclosing character", "category": "Dance" }, { - "command": "dance.count.4", - "title": "Count 4", - "description": "Adds 4 to the current counter for the next operation.", + "command": "dance.seek.extend", + "title": "Extend to character (excluded)", "category": "Dance" }, { - "command": "dance.count.5", - "title": "Count 5", - "description": "Adds 5 to the current counter for the next operation.", + "command": "dance.seek.extend.backward", + "title": "Extend to character (excluded, backward)", "category": "Dance" }, { - "command": "dance.count.6", - "title": "Count 6", - "description": "Adds 6 to the current counter for the next operation.", + "command": "dance.seek.included", + "title": "Select to character (included)", "category": "Dance" }, { - "command": "dance.count.7", - "title": "Count 7", - "description": "Adds 7 to the current counter for the next operation.", + "command": "dance.seek.included.backward", + "title": "Select to character (included, backward)", "category": "Dance" }, { - "command": "dance.count.8", - "title": "Count 8", - "description": "Adds 8 to the current counter for the next operation.", + "command": "dance.seek.included.extend", + "title": "Extend to character (included)", "category": "Dance" }, { - "command": "dance.count.9", - "title": "Count 9", - "description": "Adds 9 to the current counter for the next operation.", + "command": "dance.seek.included.extend.backward", + "title": "Extend to character (included, backward)", + "category": "Dance" + }, + { + "command": "dance.seek.object", + "title": "Select object", + "category": "Dance" + }, + { + "command": "dance.seek.word", + "title": "Select to next word start", + "category": "Dance" + }, + { + "command": "dance.seek.word.backward", + "title": "Select to previous word start", + "category": "Dance" + }, + { + "command": "dance.seek.word.extend", + "title": "Extend to next word start", + "category": "Dance" + }, + { + "command": "dance.seek.word.extend.backward", + "title": "Extend to previous word start", + "category": "Dance" + }, + { + "command": "dance.seek.word.ws", + "title": "Select to next non-whitespace word start", + "category": "Dance" + }, + { + "command": "dance.seek.word.ws.backward", + "title": "Select to previous non-whitespace word start", + "category": "Dance" + }, + { + "command": "dance.seek.word.ws.extend", + "title": "Extend to next non-whitespace word start", + "category": "Dance" + }, + { + "command": "dance.seek.word.ws.extend.backward", + "title": "Extend to previous non-whitespace word start", + "category": "Dance" + }, + { + "command": "dance.seek.wordEnd", + "title": "Select to next word end", + "category": "Dance" + }, + { + "command": "dance.seek.wordEnd.extend", + "title": "Extend to next word end", + "category": "Dance" + }, + { + "command": "dance.seek.wordEnd.ws", + "title": "Select to next non-whitespace word end", + "category": "Dance" + }, + { + "command": "dance.seek.wordEnd.ws.extend", + "title": "Extend to next non-whitespace word end", + "category": "Dance" + }, + { + "command": "dance.select.buffer", + "title": "Select whole buffer", + "category": "Dance" + }, + { + "command": "dance.select.documentEnd.extend", + "title": "Extend to last character", + "category": "Dance" + }, + { + "command": "dance.select.documentEnd.jump", + "title": "Jump to last character", + "category": "Dance" + }, + { + "command": "dance.select.down.extend", + "title": "Extend down", + "category": "Dance" + }, + { + "command": "dance.select.down.jump", + "title": "Jump down", + "category": "Dance" + }, + { + "command": "dance.select.firstLine.extend", + "title": "Extend to first line", + "category": "Dance" + }, + { + "command": "dance.select.firstLine.jump", + "title": "Jump to first line", + "category": "Dance" + }, + { + "command": "dance.select.firstVisibleLine", + "title": "Select to first visible line", + "category": "Dance" + }, + { + "command": "dance.select.firstVisibleLine.extend", + "title": "Extend to first visible line", + "category": "Dance" + }, + { + "command": "dance.select.firstVisibleLine.jump", + "title": "Jump to first visible line", + "category": "Dance" + }, + { + "command": "dance.select.horizontally", + "title": "Select horizontally", + "category": "Dance" + }, + { + "command": "dance.select.lastLine", + "title": "Select to last line", + "category": "Dance" + }, + { + "command": "dance.select.lastLine.extend", + "title": "Extend to last line", + "category": "Dance" + }, + { + "command": "dance.select.lastLine.jump", + "title": "Jump to last line", + "category": "Dance" + }, + { + "command": "dance.select.lastModification", + "title": "Select to last modification", + "category": "Dance" + }, + { + "command": "dance.select.lastModification.extend", + "title": "Extend to last modification", + "category": "Dance" + }, + { + "command": "dance.select.lastModification.jump", + "title": "Jump to last modification", + "category": "Dance" + }, + { + "command": "dance.select.lastVisibleLine", + "title": "Select to last visible line", + "category": "Dance" + }, + { + "command": "dance.select.lastVisibleLine.extend", + "title": "Extend to last visible line", + "category": "Dance" + }, + { + "command": "dance.select.lastVisibleLine.jump", + "title": "Jump to last visible line", + "category": "Dance" + }, + { + "command": "dance.select.left.extend", + "title": "Extend left", + "category": "Dance" + }, + { + "command": "dance.select.left.jump", + "title": "Jump left", + "category": "Dance" + }, + { + "command": "dance.select.line.above", + "title": "Select line above", + "category": "Dance" + }, + { + "command": "dance.select.line.above.extend", + "title": "Extend to line above", + "category": "Dance" + }, + { + "command": "dance.select.line.below", + "title": "Select line below", + "category": "Dance" + }, + { + "command": "dance.select.line.below.extend", + "title": "Extend to line below", + "category": "Dance" + }, + { + "command": "dance.select.lineEnd", + "title": "Select to line end", + "category": "Dance" + }, + { + "command": "dance.select.lineEnd.extend", + "title": "Extend to line end", + "category": "Dance" + }, + { + "command": "dance.select.lineStart", + "title": "Select to line start", + "category": "Dance" + }, + { + "command": "dance.select.lineStart.extend", + "title": "Extend to line start", + "category": "Dance" + }, + { + "command": "dance.select.lineStart.jump", + "title": "Jump to line start", + "category": "Dance" + }, + { + "command": "dance.select.lineStart.skipBlank.extend", + "title": "Extend to line start (skip blank)", + "category": "Dance" + }, + { + "command": "dance.select.lineStart.skipBlank.jump", + "title": "Jump to line start (skip blank)", + "category": "Dance" + }, + { + "command": "dance.select.middleVisibleLine", + "title": "Select to middle visible line", + "category": "Dance" + }, + { + "command": "dance.select.middleVisibleLine.extend", + "title": "Extend to middle visible line", + "category": "Dance" + }, + { + "command": "dance.select.middleVisibleLine.jump", + "title": "Jump to middle visible line", + "category": "Dance" + }, + { + "command": "dance.select.right.extend", + "title": "Extend right", + "category": "Dance" + }, + { + "command": "dance.select.right.jump", + "title": "Jump right", + "category": "Dance" + }, + { + "command": "dance.select.to", + "title": "Select to", + "category": "Dance" + }, + { + "command": "dance.select.to.extend", + "title": "Extend to", + "category": "Dance" + }, + { + "command": "dance.select.to.jump", + "title": "Go to", + "category": "Dance" + }, + { + "command": "dance.select.up.extend", + "title": "Extend up", + "category": "Dance" + }, + { + "command": "dance.select.up.jump", + "title": "Jump up", + "category": "Dance" + }, + { + "command": "dance.select.vertically", + "title": "Select vertically", + "category": "Dance" + }, + { + "command": "dance.selections.changeDirection", + "title": "Change direction of selections", + "category": "Dance" + }, + { + "command": "dance.selections.clear.main", + "title": "Clear main selections", + "category": "Dance" + }, + { + "command": "dance.selections.clear.secondary", + "title": "Clear secondary selections", + "category": "Dance" + }, + { + "command": "dance.selections.copy", + "title": "Copy selections below", + "category": "Dance" + }, + { + "command": "dance.selections.copy.above", + "title": "Copy selections above", + "category": "Dance" + }, + { + "command": "dance.selections.expandToLines", + "title": "Expand to lines", + "category": "Dance" + }, + { + "command": "dance.selections.faceBackward", + "title": "Backward selections", + "category": "Dance" + }, + { + "command": "dance.selections.faceForward", + "title": "Forward selections", + "category": "Dance" + }, + { + "command": "dance.selections.filter", + "title": "Filter selections", + "category": "Dance" + }, + { + "command": "dance.selections.filter.regexp", + "title": "Keep matching selections", + "category": "Dance" + }, + { + "command": "dance.selections.filter.regexp.inverse", + "title": "Clear matching selections", + "category": "Dance" + }, + { + "command": "dance.selections.hideIndices", + "title": "Hide selection indices", + "category": "Dance" + }, + { + "command": "dance.selections.merge", + "title": "Merge contiguous selections", + "category": "Dance" + }, + { + "command": "dance.selections.open", + "title": "Open selected file", + "category": "Dance" + }, + { + "command": "dance.selections.pipe", + "title": "Pipe selections", + "category": "Dance" + }, + { + "command": "dance.selections.pipe.append", + "title": "Pipe and append", + "category": "Dance" + }, + { + "command": "dance.selections.pipe.prepend", + "title": "Pipe and prepend", + "category": "Dance" + }, + { + "command": "dance.selections.pipe.replace", + "title": "Pipe and replace", + "category": "Dance" + }, + { + "command": "dance.selections.reduce", + "title": "Reduce selections to their cursor", + "category": "Dance" + }, + { + "command": "dance.selections.reduce.edges", + "title": "Reduce selections to their ends", + "category": "Dance" + }, + { + "command": "dance.selections.restore", + "title": "Restore selections", + "category": "Dance" + }, + { + "command": "dance.selections.restore.withCurrent", + "title": "Combine register selections with current ones", + "category": "Dance" + }, + { + "command": "dance.selections.save", + "title": "Save selections", + "category": "Dance" + }, + { + "command": "dance.selections.saveText", + "title": "Copy selections text", + "category": "Dance" + }, + { + "command": "dance.selections.select", + "title": "Select within selections", + "category": "Dance" + }, + { + "command": "dance.selections.showIndices", + "title": "Show selection indices", + "category": "Dance" + }, + { + "command": "dance.selections.split", + "title": "Split selections", + "category": "Dance" + }, + { + "command": "dance.selections.splitLines", + "title": "Split selections at line boundaries", + "category": "Dance" + }, + { + "command": "dance.selections.toggleIndices", + "title": "Toggle selection indices", + "category": "Dance" + }, + { + "command": "dance.selections.trimLines", + "title": "Trim lines", + "category": "Dance" + }, + { + "command": "dance.selections.trimWhitespace", + "title": "Trim whitespace", + "category": "Dance" + }, + { + "command": "dance.selections.rotate.both", + "title": "Rotate selections clockwise", + "category": "Dance" + }, + { + "command": "dance.selections.rotate.both.reverse", + "title": "Rotate selections counter-clockwise", + "category": "Dance" + }, + { + "command": "dance.selections.rotate.contents", + "title": "Rotate selections clockwise (contents only)", + "category": "Dance" + }, + { + "command": "dance.selections.rotate.contents.reverse", + "title": "Rotate selections counter-clockwise (contents only)", + "category": "Dance" + }, + { + "command": "dance.selections.rotate.selections", + "title": "Rotate selections clockwise (selections only)", + "category": "Dance" + }, + { + "command": "dance.selections.rotate.selections.reverse", + "title": "Rotate selections counter-clockwise (selections only)", + "category": "Dance" + }, + { + "command": "dance.view.line", + "title": "Reveals a position based on the main cursor", "category": "Dance" } ], + "menus": { + "commandPalette": [ + { + "command": "dance.dev.setSelectionBehavior", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.align", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.case.swap", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.case.toLower", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.case.toUpper", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.copyIndentation", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.deindent", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.deindent.withIncomplete", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.delete", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.delete-insert", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.indent", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.indent.withEmpty", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.insert", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.join", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.join.select", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.newLine.above", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.newLine.above.insert", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.newLine.below", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.newLine.below.insert", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.paste.after", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.paste.after.select", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.paste.before", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.paste.before.select", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.replaceCharacters", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.selectRegister-insert", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.yank-delete", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.yank-delete-insert", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.edit.yank-replace", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.recording.play", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.recording.start", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.recording.stop", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.redo", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.redo.selections", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.repeat", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.repeat.edit", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.repeat.seek", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.repeat.selection", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.undo", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.history.undo.selections", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.keybindings.setup", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.cancel", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.ignore", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.openMenu", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.run", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selectRegister", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.updateCount", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.insert.after", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.insert.before", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.insert.lineEnd", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.insert.lineStart", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.set", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.set.insert", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.set.normal", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.set.temporarily", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.set.temporarily.insert", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.modes.set.temporarily.normal", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search.backward.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search.next", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search.next.add", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search.previous", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search.previous.add", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search.selection", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.search.selection.smart", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject.end", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject.end", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject.inner", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject.inner.end", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject.inner.end.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject.inner.start", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject.inner.start.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject.start", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.askObject.start", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.enclosing", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.enclosing.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.enclosing.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.enclosing.extend.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.extend.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.included", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.included.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.included.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.included.extend.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.object", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.word", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.word.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.word.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.word.extend.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.word.ws", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.word.ws.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.word.ws.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.word.ws.extend.backward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.wordEnd", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.wordEnd.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.wordEnd.ws", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.seek.wordEnd.ws.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.buffer", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.documentEnd.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.documentEnd.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.down.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.down.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.firstLine.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.firstLine.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.firstVisibleLine", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.firstVisibleLine.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.firstVisibleLine.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.horizontally", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lastLine", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lastLine.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lastLine.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lastModification", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lastModification.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lastModification.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lastVisibleLine", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lastVisibleLine.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lastVisibleLine.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.left.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.left.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.line.above", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.line.above.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.line.below", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.line.below.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lineEnd", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lineEnd.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lineStart", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lineStart.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lineStart.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lineStart.skipBlank.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.lineStart.skipBlank.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.middleVisibleLine", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.middleVisibleLine.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.middleVisibleLine.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.right.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.right.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.to", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.to.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.to.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.up.extend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.up.jump", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.select.vertically", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.changeDirection", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.clear.main", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.clear.secondary", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.copy", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.copy.above", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.expandToLines", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.faceBackward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.faceForward", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.filter", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.filter.regexp", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.filter.regexp.inverse", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.hideIndices", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.merge", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.open", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.pipe", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.pipe.append", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.pipe.prepend", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.pipe.replace", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.reduce", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.reduce.edges", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.restore", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.restore.withCurrent", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.save", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.saveText", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.select", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.showIndices", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.split", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.splitLines", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.toggleIndices", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.trimLines", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.trimWhitespace", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.rotate.both", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.rotate.both.reverse", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.rotate.contents", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.rotate.contents.reverse", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.rotate.selections", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.selections.rotate.selections.reverse", + "when": "dance.mode == 'normal'" + }, + { + "command": "dance.view.line", + "when": "dance.mode == 'normal'" + } + ] + }, "keybindings": [ { - "key": "Alt+a", + "key": "Shift+7", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "select" - } + "title": "Align selections", + "command": "dance.edit.align" }, { - "key": "Alt+a", + "key": "Alt+`", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Swap case", + "command": "dance.edit.case.swap" + }, + { + "key": "`", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Transform to lower case", + "command": "dance.edit.case.toLower" + }, + { + "key": "Shift+`", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Transform to upper case", + "command": "dance.edit.case.toUpper" + }, + { + "key": "Shift+Alt+7", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Copy indentation", + "command": "dance.edit.copyIndentation" + }, + { + "key": "Shift+Alt+,", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Deindent selected lines", + "command": "dance.edit.deindent" + }, + { + "key": "Shift+,", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Deindent selected lines (including incomplete indent)", + "command": "dance.edit.deindent.withIncomplete" + }, + { + "key": "Alt+D", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Delete", + "command": "dance.edit.delete" + }, + { + "key": "Alt+C", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Delete and switch to Insert", + "command": "dance.edit.delete-insert" + }, + { + "key": "Shift+.", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Indent selected lines", + "command": "dance.edit.indent" + }, + { + "key": "Shift+Alt+.", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Indent selected lines (including empty lines)", + "command": "dance.edit.indent.withEmpty" + }, + { + "key": "Shift+Alt+R", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Insert contents of register", + "command": "dance.edit.insert" + }, + { + "key": "Alt+J", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Join lines", + "command": "dance.edit.join" + }, + { + "key": "Shift+Alt+J", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Join lines and select inserted separators", + "command": "dance.edit.join.select" + }, + { + "key": "Shift+Alt+O", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Insert new line above each selection", + "command": "dance.edit.newLine.above" + }, + { + "key": "Shift+O", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Insert new line above and switch to insert", + "command": "dance.edit.newLine.above.insert" + }, + { + "key": "Alt+O", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Insert new line below each selection", + "command": "dance.edit.newLine.below" + }, + { + "key": "O", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Insert new line below and switch to insert", + "command": "dance.edit.newLine.below.insert" + }, + { + "key": "P", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Paste after", + "command": "dance.edit.paste.after" + }, + { + "key": "Alt+P", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Paste after and select", + "command": "dance.edit.paste.after.select" + }, + { + "key": "Shift+P", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Paste before", + "command": "dance.edit.paste.before" + }, + { + "key": "Shift+Alt+P", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Paste before and select", + "command": "dance.edit.paste.before.select" + }, + { + "key": "R", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Replace characters", + "command": "dance.edit.replaceCharacters" + }, + { + "key": "Ctrl+R", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Pick register and replace", + "command": "dance.edit.selectRegister-insert" + }, + { + "key": "Ctrl+R", "when": "editorTextFocus && dance.mode == 'insert'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "select" - } + "title": "Pick register and replace", + "command": "dance.edit.selectRegister-insert" }, { - "key": "Alt+i", + "key": "D", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "select", - "inner": true - } + "title": "Copy and delete", + "command": "dance.edit.yank-delete" }, { - "key": "Alt+i", - "when": "editorTextFocus && dance.mode == 'insert'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "select", - "inner": true - } - }, - { - "key": "[", + "key": "C", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", + "title": "Copy, delete and switch to Insert", + "command": "dance.edit.yank-delete-insert" + }, + { + "key": "Shift+R", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Copy and replace", + "command": "dance.edit.yank-replace" + }, + { + "key": "Q", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Replay recording", + "command": "dance.history.recording.play" + }, + { + "key": "Shift+Q", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Start recording", + "command": "dance.history.recording.start" + }, + { + "key": "Escape", + "when": "editorTextFocus && dance.mode == 'normal' && dance.isRecording", + "title": "Stop recording", + "command": "dance.history.recording.stop" + }, + { + "key": "Shift+U", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Redo", + "command": "dance.history.redo" + }, + { + "key": "Shift+Alt+U", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Redo a change of selections", + "command": "dance.history.redo.selections" + }, + { + "key": ".", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Repeat last edit without a command", + "command": "dance.history.repeat.edit" + }, + { + "key": "Alt+.", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Repeat last seek", + "command": "dance.history.repeat.seek" + }, + { + "key": "U", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Undo", + "command": "dance.history.undo" + }, + { + "key": "Alt+U", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Undo a change of selections", + "command": "dance.history.undo.selections" + }, + { + "key": "Escape", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Cancel Dance operation", + "command": "dance.cancel" + }, + { + "key": "Escape", + "when": "editorTextFocus && dance.mode == 'input'", + "title": "Cancel Dance operation", + "command": "dance.cancel" + }, + { + "key": "Shift+'", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select register for next command", + "command": "dance.selectRegister" + }, + { + "key": "0", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add the digit 0 to the counter", + "command": "dance.updateCount", "args": { - "menu": "object", - "action": "selectToStart" + "addDigits": 0 } }, { - "key": "]", + "key": "1", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", + "title": "Add the digit 1 to the counter", + "command": "dance.updateCount", "args": { - "menu": "object", - "action": "selectToEnd" + "addDigits": 1 + } + }, + { + "key": "2", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add the digit 2 to the counter", + "command": "dance.updateCount", + "args": { + "addDigits": 2 + } + }, + { + "key": "3", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add the digit 3 to the counter", + "command": "dance.updateCount", + "args": { + "addDigits": 3 + } + }, + { + "key": "4", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add the digit 4 to the counter", + "command": "dance.updateCount", + "args": { + "addDigits": 4 + } + }, + { + "key": "5", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add the digit 5 to the counter", + "command": "dance.updateCount", + "args": { + "addDigits": 5 + } + }, + { + "key": "6", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add the digit 6 to the counter", + "command": "dance.updateCount", + "args": { + "addDigits": 6 + } + }, + { + "key": "7", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add the digit 7 to the counter", + "command": "dance.updateCount", + "args": { + "addDigits": 7 + } + }, + { + "key": "8", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add the digit 8 to the counter", + "command": "dance.updateCount", + "args": { + "addDigits": 8 + } + }, + { + "key": "9", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add the digit 9 to the counter", + "command": "dance.updateCount", + "args": { + "addDigits": 9 } }, { @@ -1565,899 +2738,814 @@ "command": "workbench.action.showCommands" }, { - "key": "Shift+[", + "key": "A", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "selectToStart", - "extend": true - } + "title": "Insert after", + "command": "dance.modes.insert.after" + }, + { + "key": "I", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Insert before", + "command": "dance.modes.insert.before" + }, + { + "key": "Shift+A", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Insert at line end", + "command": "dance.modes.insert.lineEnd" + }, + { + "key": "Shift+I", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Insert at line start", + "command": "dance.modes.insert.lineStart" + }, + { + "key": "Escape", + "when": "editorTextFocus && dance.mode == 'insert'", + "title": "Set mode to Normal", + "command": "dance.modes.set.normal" + }, + { + "key": "Ctrl+V", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Temporart Insert mode", + "command": "dance.modes.set.temporarily.insert" + }, + { + "key": "Ctrl+V", + "when": "editorTextFocus && dance.mode == 'insert'", + "title": "Temporary Normal mode", + "command": "dance.modes.set.temporarily.normal" + }, + { + "key": "/", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Search", + "command": "dance.search" + }, + { + "key": "Alt+/", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Search backward", + "command": "dance.search.backward" + }, + { + "key": "Shift+Alt+/", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Search backward (extend)", + "command": "dance.search.backward.extend" + }, + { + "key": "Shift+/", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Search (extend)", + "command": "dance.search.extend" + }, + { + "key": "N", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select next match", + "command": "dance.search.next" + }, + { + "key": "Shift+N", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add next match", + "command": "dance.search.next.add" + }, + { + "key": "Alt+N", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select previous match", + "command": "dance.search.previous" + }, + { + "key": "Shift+Alt+N", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Add previous match", + "command": "dance.search.previous.add" + }, + { + "key": "Shift+Alt+8", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Search current selection", + "command": "dance.search.selection" + }, + { + "key": "Shift+8", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Search current selection (smart)", + "command": "dance.search.selection.smart" + }, + { + "key": "T", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to character (excluded)", + "command": "dance.seek" + }, + { + "key": "Alt+A", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select whole object", + "command": "dance.seek.askObject" + }, + { + "key": "Alt+A", + "when": "editorTextFocus && dance.mode == 'insert'", + "title": "Select whole object", + "command": "dance.seek.askObject" + }, + { + "key": "]", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to whole object end", + "command": "dance.seek.askObject.end" }, { "key": "Shift+]", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "selectToEnd", - "extend": true - } + "title": "Extend to whole object end", + "command": "dance.seek.askObject.end" }, { - "key": "Alt+[", + "key": "Alt+I", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "selectToStart", - "inner": true - } + "title": "Select inner object", + "command": "dance.seek.askObject.inner" + }, + { + "key": "Alt+I", + "when": "editorTextFocus && dance.mode == 'insert'", + "title": "Select inner object", + "command": "dance.seek.askObject.inner" }, { "key": "Alt+]", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "selectToEnd", - "inner": true - } + "title": "Select to inner object end", + "command": "dance.seek.askObject.inner.end" }, { - "key": "Alt+Shift+[", + "key": "Shift+Alt+]", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "selectToStart", - "extend": true, - "inner": true - } + "title": "Extend to inner object end", + "command": "dance.seek.askObject.inner.end.extend" }, { - "key": "Alt+Shift+]", + "key": "Alt+[", "when": "editorTextFocus && dance.mode == 'normal'", - "command": "dance.openMenu", - "args": { - "menu": "object", - "action": "selectToEnd", - "extend": true, - "inner": true - } + "title": "Select to inner object start", + "command": "dance.seek.askObject.inner.start" }, { - "command": "dance.set.normal", - "key": "escape", - "when": "editorTextFocus && dance.mode == 'insert'" + "key": "Shift+Alt+[", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to inner object start", + "command": "dance.seek.askObject.inner.start.extend" }, { - "command": "dance.tmp.normal", - "key": "Ctrl+v", - "when": "editorTextFocus && dance.mode == 'insert'" + "key": "[", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to whole object start", + "command": "dance.seek.askObject.start" }, { - "command": "dance.tmp.insert", - "key": "Ctrl+v", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+[", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to whole object start", + "command": "dance.seek.askObject.start" }, { - "command": "dance.insert.before", - "key": "i", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Alt+T", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to character (excluded, backward)", + "command": "dance.seek.backward" }, { - "command": "dance.insert.after", - "key": "a", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "M", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to next enclosing character", + "command": "dance.seek.enclosing" }, { - "command": "dance.insert.lineStart", - "key": "Shift+i", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Alt+M", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to previous enclosing character", + "command": "dance.seek.enclosing.backward" }, { - "command": "dance.insert.lineEnd", - "key": "Shift+a", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+M", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to next enclosing character", + "command": "dance.seek.enclosing.extend" }, { - "command": "dance.insert.newLine.below", - "key": "o", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+M", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to previous enclosing character", + "command": "dance.seek.enclosing.extend.backward" }, { - "command": "dance.insert.newLine.above", - "key": "Shift+o", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+T", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to character (excluded)", + "command": "dance.seek.extend" }, { - "command": "dance.newLine.below", - "key": "Alt+o", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+T", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to character (excluded, backward)", + "command": "dance.seek.extend.backward" }, { - "command": "dance.newLine.above", - "key": "Shift+Alt+o", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "F", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to character (included)", + "command": "dance.seek.included" }, { - "command": "dance.repeat.insert", - "key": ".", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Alt+F", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to character (included, backward)", + "command": "dance.seek.included.backward" }, { - "command": "dance.repeat.objectOrSelectTo", - "key": "Alt+.", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+F", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to character (included)", + "command": "dance.seek.included.extend" }, { - "command": "dance.left", - "key": "left", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+F", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to character (included, backward)", + "command": "dance.seek.included.extend.backward" }, { - "command": "dance.left", - "key": "h", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "W", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to next word start", + "command": "dance.seek.word" }, { - "command": "dance.right", - "key": "right", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "B", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to previous word start", + "command": "dance.seek.word.backward" }, { - "command": "dance.right", - "key": "l", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+W", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to next word start", + "command": "dance.seek.word.extend" }, { - "command": "dance.up", - "key": "up", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+B", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to previous word start", + "command": "dance.seek.word.extend.backward" }, { - "command": "dance.up", - "key": "k", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Alt+W", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to next non-whitespace word start", + "command": "dance.seek.word.ws" }, { - "command": "dance.down", - "key": "down", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Alt+B", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to previous non-whitespace word start", + "command": "dance.seek.word.ws.backward" }, { - "command": "dance.down", - "key": "j", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+W", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to next non-whitespace word start", + "command": "dance.seek.word.ws.extend" }, { - "command": "dance.up.page", - "key": "Ctrl+b", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+B", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to previous non-whitespace word start", + "command": "dance.seek.word.ws.extend.backward" }, { - "command": "dance.up.page", - "key": "Ctrl+b", - "when": "editorTextFocus && dance.mode == 'insert'" + "key": "E", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to next word end", + "command": "dance.seek.wordEnd" }, { - "command": "dance.down.page", - "key": "Ctrl+f", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+E", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to next word end", + "command": "dance.seek.wordEnd.extend" }, { - "command": "dance.down.page", - "key": "Ctrl+f", - "when": "editorTextFocus && dance.mode == 'insert'" + "key": "Alt+E", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to next non-whitespace word end", + "command": "dance.seek.wordEnd.ws" }, { - "command": "dance.up.halfPage", - "key": "Ctrl+u", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+E", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to next non-whitespace word end", + "command": "dance.seek.wordEnd.ws.extend" }, { - "command": "dance.up.halfPage", - "key": "Ctrl+u", - "when": "editorTextFocus && dance.mode == 'insert'" - }, - { - "command": "dance.down.halfPage", - "key": "Ctrl+d", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.down.halfPage", - "key": "Ctrl+d", - "when": "editorTextFocus && dance.mode == 'insert'" - }, - { - "command": "dance.select.to.included", - "key": "f", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.to.excluded", - "key": "t", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.buffer", "key": "Shift+5", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select whole buffer", + "command": "dance.select.buffer" }, { - "command": "dance.select.line", - "key": "x", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+J", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend down", + "command": "dance.select.down.extend" }, { - "command": "dance.select.toLineBegin", - "key": "Alt+h", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Down", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend down", + "command": "dance.select.down.extend" }, { - "command": "dance.select.toLineBegin", - "key": "home", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "J", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Jump down", + "command": "dance.select.down.jump" }, { - "command": "dance.select.toLineEnd", - "key": "Alt+l", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Down", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Jump down", + "command": "dance.select.down.jump" }, { - "command": "dance.select.toLineEnd", - "key": "end", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+H", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend left", + "command": "dance.select.left.extend" }, { - "command": "dance.select.enclosing", - "key": "m", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Left", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend left", + "command": "dance.select.left.extend" }, { - "command": "dance.expandLines", - "key": "Alt+x", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "H", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Jump left", + "command": "dance.select.left.jump" }, { - "command": "dance.trimLines", - "key": "Shift+Alt+x", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Left", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Jump left", + "command": "dance.select.left.jump" }, { - "command": "dance.trimSelections", - "key": "Shift+-", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "X", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select line below", + "command": "dance.select.line.below" }, { - "command": "dance.select.word", - "key": "w", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+X", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to line below", + "command": "dance.select.line.below.extend" }, { - "command": "dance.select.word.previous", - "key": "b", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Alt+L", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to line end", + "command": "dance.select.lineEnd" }, { - "command": "dance.select.word.end", - "key": "e", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "End", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to line end", + "command": "dance.select.lineEnd" }, { - "command": "dance.select.word.alt", - "key": "Alt+w", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+L", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to line end", + "command": "dance.select.lineEnd.extend" }, { - "command": "dance.select.word.alt.previous", - "key": "Alt+b", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+End", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to line end", + "command": "dance.select.lineEnd.extend" }, { - "command": "dance.select.word.alt.end", - "key": "Alt+e", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Alt+H", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to line start", + "command": "dance.select.lineStart" }, { - "command": "dance.select", - "key": "s", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Home", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select to line start", + "command": "dance.select.lineStart" }, { - "command": "dance.split", - "key": "Shift+s", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+H", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to line start", + "command": "dance.select.lineStart.extend" }, { - "command": "dance.split.lines", - "key": "Alt+s", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Home", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to line start", + "command": "dance.select.lineStart.extend" }, { - "command": "dance.select.firstLast", - "key": "Shift+Alt+s", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+L", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend right", + "command": "dance.select.right.extend" }, { - "command": "dance.select.copy", - "key": "Shift+c", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Right", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend right", + "command": "dance.select.right.extend" }, { - "command": "dance.select.copy.backwards", - "key": "Shift+Alt+c", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "L", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Jump right", + "command": "dance.select.right.jump" }, { - "command": "dance.selections.reduce", - "key": ";", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Right", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Jump right", + "command": "dance.select.right.jump" + }, + { + "key": "Shift+G", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend to", + "command": "dance.select.to.extend" + }, + { + "key": "G", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Go to", + "command": "dance.select.to.jump" + }, + { + "key": "Shift+K", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend up", + "command": "dance.select.up.extend" + }, + { + "key": "Shift+Up", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Extend up", + "command": "dance.select.up.extend" + }, + { + "key": "K", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Jump up", + "command": "dance.select.up.jump" + }, + { + "key": "Up", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Jump up", + "command": "dance.select.up.jump" + }, + { + "key": "Ctrl+F", + "when": "editorTextFocus && dance.mode == 'normal'", + "command": "dance.select.vertically", + "args": { + "direction": 1, + "by": "page" + } + }, + { + "key": "Ctrl+F", + "when": "editorTextFocus && dance.mode == 'insert'", + "command": "dance.select.vertically", + "args": { + "direction": 1, + "by": "page" + } + }, + { + "key": "Ctrl+D", + "when": "editorTextFocus && dance.mode == 'normal'", + "command": "dance.select.vertically", + "args": { + "direction": 1, + "by": "halfPage" + } + }, + { + "key": "Ctrl+D", + "when": "editorTextFocus && dance.mode == 'insert'", + "command": "dance.select.vertically", + "args": { + "direction": 1, + "by": "halfPage" + } + }, + { + "key": "Ctrl+B", + "when": "editorTextFocus && dance.mode == 'normal'", + "command": "dance.select.vertically", + "args": { + "direction": -1, + "by": "page" + } + }, + { + "key": "Ctrl+B", + "when": "editorTextFocus && dance.mode == 'insert'", + "command": "dance.select.vertically", + "args": { + "direction": -1, + "by": "page" + } + }, + { + "key": "Ctrl+U", + "when": "editorTextFocus && dance.mode == 'normal'", + "command": "dance.select.vertically", + "args": { + "direction": -1, + "by": "halfPage" + } + }, + { + "key": "Ctrl+U", + "when": "editorTextFocus && dance.mode == 'insert'", + "command": "dance.select.vertically", + "args": { + "direction": -1, + "by": "halfPage" + } }, { - "command": "dance.selections.flip", "key": "Alt+;", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Change direction of selections", + "command": "dance.selections.changeDirection" + }, + { + "key": "Alt+Space", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Clear main selections", + "command": "dance.selections.clear.main" + }, + { + "key": "Space", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Clear secondary selections", + "command": "dance.selections.clear.secondary" + }, + { + "key": "Shift+C", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Copy selections below", + "command": "dance.selections.copy" + }, + { + "key": "Shift+Alt+C", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Copy selections above", + "command": "dance.selections.copy.above" + }, + { + "key": "Alt+X", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Expand to lines", + "command": "dance.selections.expandToLines" }, { - "command": "dance.selections.forward", "key": "Shift+Alt+;", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Forward selections", + "command": "dance.selections.faceForward" }, { - "command": "dance.selections.clear", - "key": "space", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.selections.clearMain", - "key": "Alt+space", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.selections.keepMatching", - "key": "Alt+k", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.selections.clearMatching", - "key": "Shift+Alt+k", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.selections.merge", - "key": "Shift+Alt+-", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.selections.align", - "key": "Shift+7", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.selections.align.copy", - "key": "Shift+Alt+7", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.delete.yank", - "key": "d", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.delete.insert.yank", - "key": "c", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.delete.noYank", - "key": "Alt+d", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.delete.insert.noYank", - "key": "Alt+c", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.yank", - "key": "y", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.paste.after", - "key": "p", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.paste.before", - "key": "Shift+p", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.paste.select.after", - "key": "Alt+p", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.paste.select.before", - "key": "Shift+Alt+p", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.paste.replace", - "key": "Shift+r", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.paste.replace.every", - "key": "Shift+Alt+r", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.replace.characters", - "key": "r", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.join", - "key": "Alt+j", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.join.select", - "key": "Shift+Alt+j", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.indent", - "key": "Shift+.", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.indent.withEmpty", - "key": "Shift+Alt+.", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.deindent", - "key": "Shift+Alt+,", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.deindent.further", - "key": "Shift+,", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.toLowerCase", - "key": "`", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.toUpperCase", - "key": "Shift+`", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.swapCase", - "key": "Alt+`", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.pipe.filter", "key": "Shift+4", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Filter selections", + "command": "dance.selections.filter" }, { - "command": "dance.pipe.replace", - "key": "Shift+\\", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Alt+K", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Keep matching selections", + "command": "dance.selections.filter.regexp" + }, + { + "key": "Shift+Alt+K", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Clear matching selections", + "command": "dance.selections.filter.regexp.inverse" + }, + { + "key": "Shift+Alt+-", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Merge contiguous selections", + "command": "dance.selections.merge" }, { - "command": "dance.pipe.ignore", "key": "Shift+Alt+\\", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Pipe selections", + "command": "dance.selections.pipe" }, { - "command": "dance.pipe.append", "key": "Shift+1", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Pipe and append", + "command": "dance.selections.pipe.append" }, { - "command": "dance.pipe.prepend", "key": "Shift+Alt+1", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Pipe and prepend", + "command": "dance.selections.pipe.prepend" }, { - "command": "dance.history.undo", - "key": "u", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+\\", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Pipe and replace", + "command": "dance.selections.pipe.replace" }, { - "command": "dance.history.backward", - "key": "Alt+u", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": ";", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Reduce selections to their cursor", + "command": "dance.selections.reduce" }, { - "command": "dance.history.redo", - "key": "Shift+u", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+S", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Reduce selections to their ends", + "command": "dance.selections.reduce.edges" }, { - "command": "dance.history.forward", - "key": "Shift+Alt+u", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Z", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Restore selections", + "command": "dance.selections.restore" }, { - "command": "dance.macros.record.start", - "key": "Shift+q", - "when": "editorTextFocus && dance.mode == 'normal' && !dance.recordingMacro" + "key": "Alt+Z", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Combine register selections with current ones", + "command": "dance.selections.restore.withCurrent" }, { - "command": "dance.macros.record.stop", - "key": "escape", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Alt+Z", + "when": "editorTextFocus && dance.mode == 'normal'", + "command": "dance.selections.restore.withCurrent", + "args": { + "reverse": true + } }, { - "command": "dance.macros.play", - "key": "q", - "when": "editorTextFocus && dance.mode == 'normal'" + "key": "Shift+Z", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Save selections", + "command": "dance.selections.save" + }, + { + "key": "Y", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Copy selections text", + "command": "dance.selections.saveText" + }, + { + "key": "S", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Select within selections", + "command": "dance.selections.select" + }, + { + "key": "Shift+S", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Split selections", + "command": "dance.selections.split" + }, + { + "key": "Alt+S", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Split selections at line boundaries", + "command": "dance.selections.splitLines" + }, + { + "key": "Enter", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Toggle selection indices", + "command": "dance.selections.toggleIndices" + }, + { + "key": "Shift+Alt+X", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Trim lines", + "command": "dance.selections.trimLines" + }, + { + "key": "Shift+-", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Trim whitespace", + "command": "dance.selections.trimWhitespace" }, { - "command": "dance.rotate", "key": "Shift+9", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Rotate selections clockwise", + "command": "dance.selections.rotate.both" }, { - "command": "dance.rotate.backwards", "key": "Shift+0", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Rotate selections counter-clockwise", + "command": "dance.selections.rotate.both.reverse" }, { - "command": "dance.rotate.content", "key": "Shift+Alt+9", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Rotate selections clockwise (selections only)", + "command": "dance.selections.rotate.selections" }, { - "command": "dance.rotate.content.backwards", "key": "Shift+Alt+0", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Rotate selections counter-clockwise (selections only)", + "command": "dance.selections.rotate.selections.reverse" + }, + { + "key": "V", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Show view menu", + "command": "dance.openMenu", + "args": { + "input": "view" + } + }, + { + "key": "Shift+V", + "when": "editorTextFocus && dance.mode == 'normal'", + "title": "Show view menu (locked)", + "command": "dance.openMenu", + "args": { + "input": "view", + "locked": true + } + }, + { + "command": "dance.ignore", + "key": "Shift+D", "when": "editorTextFocus && dance.mode == 'normal'" }, { - "command": "dance.search", - "key": "/", + "command": "dance.ignore", + "key": "Shift+Y", "when": "editorTextFocus && dance.mode == 'normal'" }, { - "command": "dance.search.backwards", - "key": "Alt+/", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.search.selection.smart", - "key": "Shift+8", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.search.selection", - "key": "Shift+Alt+8", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.search.next", - "key": "n", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.search.next.add", - "key": "Shift+n", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.search.previous", - "key": "Alt+n", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.search.previous.add", - "key": "Shift+Alt+n", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.goto", - "key": "g", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.registers.insert", - "key": "Ctrl+r", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.registers.insert", - "key": "Ctrl+r", - "when": "editorTextFocus && dance.mode == 'insert'" - }, - { - "command": "dance.registers.select", - "key": "Shift+\\'", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.marks.saveSelections", - "key": "Shift+z", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.marks.restoreSelections", - "key": "z", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.marks.combineSelections.fromCurrent", - "key": "Shift+Alt+z", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.marks.combineSelections.fromRegister", - "key": "Alt+z", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.cancel", - "key": "escape", - "when": "editorTextFocus && dance.mode == 'awaiting'" - }, - { - "command": "dance.left.extend", - "key": "Shift+left", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.left.extend", - "key": "Shift+h", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.right.extend", - "key": "Shift+right", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.right.extend", - "key": "Shift+l", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.up.extend", - "key": "Shift+up", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.up.extend", - "key": "Shift+k", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.down.extend", - "key": "Shift+down", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.down.extend", - "key": "Shift+j", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.to.included.extend", - "key": "Shift+f", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.to.excluded.extend", - "key": "Shift+t", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.line.extend", - "key": "Shift+x", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.toLineBegin.extend", - "key": "Shift+Alt+h", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.toLineBegin.extend", - "key": "Shift+home", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.toLineEnd.extend", - "key": "Shift+Alt+l", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.toLineEnd.extend", - "key": "Shift+end", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.enclosing.extend", - "key": "Shift+m", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.word.extend", - "key": "Shift+w", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.word.previous.extend", - "key": "Shift+b", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.word.end.extend", - "key": "Shift+e", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.word.alt.extend", - "key": "Shift+Alt+w", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.word.alt.previous.extend", - "key": "Shift+Alt+b", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.word.alt.end.extend", - "key": "Shift+Alt+e", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.search.extend", - "key": "Shift+/", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.search.backwards.extend", - "key": "Shift+Alt+/", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.goto.extend", - "key": "Shift+g", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.to.included.backwards", - "key": "Alt+f", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.to.excluded.backwards", - "key": "Alt+t", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.enclosing.backwards", - "key": "Alt+m", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.to.included.extend.backwards", - "key": "Alt+Shift+f", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.to.excluded.extend.backwards", - "key": "Alt+Shift+t", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.select.enclosing.extend.backwards", - "key": "Alt+Shift+m", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.0", - "key": "0", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.1", - "key": "1", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.2", - "key": "2", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.3", - "key": "3", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.4", - "key": "4", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.5", - "key": "5", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.6", - "key": "6", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.7", - "key": "7", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.8", - "key": "8", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.count.9", - "key": "9", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.cancel", - "key": "v", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.cancel", - "key": "Shift+d", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.cancel", - "key": "Shift+v", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.cancel", - "key": "Shift+y", - "when": "editorTextFocus && dance.mode == 'normal'" - }, - { - "command": "dance.cancel", + "command": "dance.ignore", "key": "Shift+2", "when": "editorTextFocus && dance.mode == 'normal'" }, { - "command": "dance.cancel", + "command": "dance.ignore", "key": "Shift+3", "when": "editorTextFocus && dance.mode == 'normal'" }, { - "command": "dance.cancel", + "command": "dance.ignore", "key": "Shift+6", "when": "editorTextFocus && dance.mode == 'normal'" }, { - "command": "dance.cancel", + "command": "dance.ignore", "key": ",", "when": "editorTextFocus && dance.mode == 'normal'" }, { - "command": "dance.cancel", + "command": "dance.ignore", "key": "'", "when": "editorTextFocus && dance.mode == 'normal'" } diff --git a/package.ts b/package.ts deleted file mode 100644 index 6f2e197..0000000 --- a/package.ts +++ /dev/null @@ -1,391 +0,0 @@ -import { writeFileSync } from "fs"; - -import { Command, additionalKeyBindings, commands } from "./commands"; - -// Key bindings -// ============================================================================ - -const keybindings: { - command: string; - key: string; - when: string; - args?: any; -}[] = additionalKeyBindings.concat(); - -const alphanum = [..."abcdefghijklmnopqrstuvwxyz0123456789"], - keysToAssign = new Set([...alphanum, ...alphanum.map((x) => `Shift+${x}`), ...",'"]); - -for (const command of Object.values(commands)) { - for (const { key, when } of command.keybindings) { - keysToAssign.delete(key); - keybindings.push({ command: command.id, key, when }); - } -} - -for (const keyToAssign of keysToAssign) { - keybindings.push({ - command: "dance.cancel", - key: keyToAssign, - when: "editorTextFocus && dance.mode == 'normal'", - }); -} - -// Menus -// ============================================================================ - -const menus: Record< - string, - { items: Record } -> = { - object: { - items: { - "b()": { - command: Command.objectsPerformSelection, - args: [{ object: "parens" }], - text: "parenthesis block", - }, - "B{}": { - command: Command.objectsPerformSelection, - args: [{ object: "braces" }], - text: "braces block", - }, - "r[]": { - command: Command.objectsPerformSelection, - args: [{ object: "brackets" }], - text: "brackets block", - }, - "a<>": { - command: Command.objectsPerformSelection, - args: [{ object: "angleBrackets" }], - text: "angle block", - }, - 'Q"': { - command: Command.objectsPerformSelection, - args: [{ object: "doubleQuoteString" }], - text: "double quote string", - }, - "q'": { - command: Command.objectsPerformSelection, - args: [{ object: "singleQuoteString" }], - text: "single quote string", - }, - "g`": { - command: Command.objectsPerformSelection, - args: [{ object: "graveQuoteString" }], - text: "grave quote string", - }, - "w": { - command: Command.objectsPerformSelection, - args: [{ object: "word" }], - text: "word", - }, - "W": { - command: Command.objectsPerformSelection, - args: [{ object: "WORD" }], - text: "WORD", - }, - "s": { - command: Command.objectsPerformSelection, - args: [{ object: "sentence" }], - text: "sentence", - }, - "p": { - command: Command.objectsPerformSelection, - args: [{ object: "paragraph" }], - text: "paragraph", - }, - " ": { - command: Command.objectsPerformSelection, - args: [{ object: "whitespaces" }], - text: "whitespaces", - }, - "i": { - command: Command.objectsPerformSelection, - args: [{ object: "indent" }], - text: "indent", - }, - "n": { - command: Command.objectsPerformSelection, - args: [{ object: "number" }], - text: "number", - }, - "u": { - command: Command.objectsPerformSelection, - args: [{ object: "argument" }], - text: "argument", - }, - "c": { - command: Command.objectsPerformSelection, - args: [{ object: "custom" }], - text: "custom object desc", - }, - }, - }, -}; - -for (const [suffix, desc] of [ - ["", "go to"], - [".extend", "extend to"], -]) { - menus["goto" + suffix] = { - items: { - "h": { - text: `${desc} line start`, - command: "dance.goto.lineStart" + suffix, - }, - "l": { text: `${desc} line end`, command: "dance.goto.lineEnd" + suffix }, - "i": { - text: `${desc} non-blank line start`, - command: "dance.goto.lineStart.nonBlank" + suffix, - }, - "g": { - text: `${desc} first line`, - command: "dance.goto.firstLine" + suffix, - }, - "k": { - text: `${desc} first line`, - command: "dance.goto.firstLine" + suffix, - }, - "j": { - text: `${desc} last line`, - command: "dance.goto.lastLine" + suffix, - }, - "e": { - text: `${desc} last char of last line`, - command: "dance.goto.lastCharacter" + suffix, - }, - "t": { - text: `${desc} the first displayed line`, - command: "dance.goto.firstVisibleLine" + suffix, - }, - "c": { - text: `${desc} the middle displayed line`, - command: "dance.goto.middleVisibleLine" + suffix, - }, - "b": { - text: `${desc} the last displayed line`, - command: "dance.goto.lastVisibleLine" + suffix, - }, - "f": { - text: `${desc} file whose name is selected`, - command: "dance.goto.selectedFile" + suffix, - }, - ".": { - text: `${desc} last buffer modification position`, - command: "dance.goto.lastModification" + suffix, - }, - }, - }; -} - -// Package information -// ============================================================================ - -const pkg = { - name: "dance", - displayName: "Dance", - description: "Make those cursors dance with Kakoune-inspired keybindings.", - version: "0.4.2", - license: "ISC", - - publisher: "gregoire", - author: { - name: "Grégoire Geis", - email: "opensource@gregoirege.is", - }, - - repository: { - type: "git", - url: "https://github.com/71/dance.git", - }, - - readme: "README.md", - - categories: ["Keymaps", "Other"], - - main: "./out/src/extension.js", - - engines: { - vscode: "^1.44.0", - }, - - scripts: { - "check": "eslint .", - "format": "eslint . --fix", - "generate": "ts-node ./commands/generate.ts && ts-node package.ts", - "vscode:prepublish": "yarn run generate && yarn run compile", - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", - "test": "yarn run compile && node ./out/test/run.js", - "package": "vsce package", - "publish": "vsce publish", - }, - - devDependencies: { - "@types/glob": "^7.1.1", - "@types/js-yaml": "^3.12.3", - "@types/mocha": "^8.0.3", - "@types/node": "^14.6.0", - "@types/vscode": "^1.44.0", - "@typescript-eslint/eslint-plugin": "^4.18.0", - "@typescript-eslint/parser": "^4.18.0", - "eslint": "^7.22.0", - "glob": "^7.1.6", - "js-yaml": "^3.13.0", - "mocha": "^8.1.1", - "source-map-support": "^0.5.19", - "ts-node": "^9.1.1", - "typescript": "^4.2.3", - "vsce": "^1.87.0", - "vscode-test": "^1.3.0", - }, - - activationEvents: ["*"], - contributes: { - configuration: { - type: "object", - title: "Dance", - properties: { - "dance.enabled": { - type: "boolean", - default: true, - description: "Controls whether the Dance keybindings are enabled.", - }, - "dance.normalMode.lineHighlight": { - type: ["string", "null"], - default: "editor.hoverHighlightBackground", - markdownDescription: - "Controls the line highlighting applied to active lines in normal mode. " - + "Can be an hex color, a [theme color](" - + "https://code.visualstudio.com/api/references/theme-color) or null.", - }, - "dance.insertMode.lineHighlight": { - type: ["string", "null"], - default: null, - markdownDescription: - "Controls the line highlighting applied to active lines in insert mode. " - + "Can be an hex color, a [theme color](" - + "https://code.visualstudio.com/api/references/theme-color) or null.", - }, - "dance.normalMode.lineNumbers": { - enum: ["off", "on", "relative", "inherit"], - default: "relative", - description: "Controls the display of line numbers in normal mode.", - enumDescriptions: [ - "No line numbers.", - "Absolute line numbers.", - "Relative line numbers.", - "Inherit from `editor.lineNumbers`.", - ], - }, - "dance.insertMode.lineNumbers": { - enum: ["off", "on", "relative", "inherit"], - default: "inherit", - description: "Controls the display of line numbers in insert mode.", - enumDescriptions: [ - "No line numbers.", - "Absolute line numbers.", - "Relative line numbers.", - "Inherit from `editor.lineNumbers`.", - ], - }, - "dance.normalMode.cursorStyle": { - enum: [ - "line", - "block", - "underline", - "line-thin", - "block-outline", - "underline-thin", - "inherit", - ], - default: "inherit", - description: "Controls the cursor style in normal mode.", - }, - "dance.insertMode.cursorStyle": { - enum: [ - "line", - "block", - "underline", - "line-thin", - "block-outline", - "underline-thin", - "inherit", - ], - default: "inherit", - description: "Controls the cursor style in insert mode.", - }, - "dance.insertMode.selectionStyle": { - type: "object", - default: { - borderColor: "$editor.selectionBackground", - borderStyle: "solid", - borderWidth: "2px", - borderRadius: "1px", - }, - description: "The style to apply to selections in insert mode.", - properties: (Object as any).fromEntries( - [ - "backgroundColor", - "borderColor", - "borderStyle", - "borderWidth", - "borderRadius", - ].map((x) => [x, { type: "string" }]), - ), - }, - "dance.selectionBehavior": { - enum: ["caret", "character"], - default: "caret", - description: "Controls how selections behave within VS Code.", - markdownEnumDescriptions: [ - "Selections are anchored to carets, which is the native VS Code behavior; that is, " - + "they are positioned *between* characters and can therefore be empty.", - "Selections are anchored to characters, like Kakoune; that is, they are positioned " - + "*on* characters, and therefore cannot be empty. Additionally, one-character " - + "selections will behave as if they were non-directional, like Kakoune.", - ], - }, - "dance.menus": { - type: "object", - additionalProperties: { - type: "object", - properties: { - items: { - type: "object", - additionalProperties: { - type: ["object", "null"], - properties: { - text: { - type: "string", - }, - command: { - type: "string", - }, - args: { - type: "array", - }, - }, - }, - }, - }, - additionalProperties: false, - }, - default: menus, - }, - }, - }, - commands: Object.values(commands).map((x) => ({ - command: x.id, - title: x.title, - description: x.description, - category: "Dance", - })), - keybindings, - }, -}; - -// Save to package.json -// ============================================================================ - -writeFileSync("./package.json", JSON.stringify(pkg, undefined, 2) + "\n", "utf8"); diff --git a/recipes/README.md b/recipes/README.md new file mode 100644 index 0000000..8243e39 --- /dev/null +++ b/recipes/README.md @@ -0,0 +1,98 @@ +Recipes +======= + +This directory contains Dance "recipes" -- example configurations and commands +meant to show what can be done with Dance. + +For more examples, please see the [test suite](../test) and the [Dance API]( +../src/api) documentation, both of which contain many different tested +examples. + +## Generating code using commented JavaScript code + +The following script can be used: +```js +await run(Selections.map((text) => text.replace(/^\/\/ |START$[\s\S]+?END$/gm, ""))); +``` + +Before: +``` +// await replace((text) => text.replace(/(\/\/ START\n)([\s\S]+?)(\/\/ END\n)/m, (_, before, after) => +^ +// before + "const alphabet = " + JSON.stringify( +// Array.from({ length: 26 }, (_, i) => String.fromCharCode(97 + i)), +// undefined, 2, +// ) + "\n" + after); +// START + +// END + ^ 0 +``` + +After: +``` +// await replace((text) => text.replace(/(\/\/ START\n)([\s\S]+?)(\/\/ END\n)/m, (_, before, after) => +^ +// before + "const alphabet = " + JSON.stringify( +// Array.from({ length: 26 }, (_, i) => String.fromCharCode(97 + i)), +// undefined, 2, +// ) + ";\n" + after); +// START +const alphabet = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z" +]; +// END + ^ 0 +``` + +## Using `jj` to escape `insert` mode + +Using the `prefix` argument of the [`dance.openMenu`](../src/commands#openmenu) +command: + +```json +{ + "key": "j", + "command": "dance.openMenu", + "args": { + "menu": { + "items": { + "j": { + "text": "escape to Normal", + "command": "dance.modes.set.normal", + }, + }, + }, + "prefix": "j", + }, + "when": "editorTextFocus && dance.mode == 'insert'", +} +``` + +For more information, please refer to [this issue]( +https://github.com/71/dance/issues/74#issuecomment-819557435). diff --git a/recipes/evil-dance.md b/recipes/evil-dance.md new file mode 100644 index 0000000..56a7056 --- /dev/null +++ b/recipes/evil-dance.md @@ -0,0 +1,90 @@ +# Evil Dance + +Evil Dance is a script that provides additional features to Dance by hooking +into the main process of VS Code and communicating with the instance of Dance +in the extension host. + +It could bring nice features, but is and will remain unstable. Use at your own +risks. + +## The script + +Add the following code to `/resources/app/out/vs/loader.js`: + +```js +// Since modules are loaded asynchronously, try to load the script below every +// second. After 10 failed tries, give up. +let danceRetries = 0; +let danceRetryToken = setInterval(() => { + try { + const { CommandsRegistry } = require("vs/platform/commands/common/commands"); + const { IModelService } = require("vs/editor/common/services/modelService"); + const { ITextMateService } = require("vs/workbench/services/textMate/common/textMateService"); + + clearInterval(danceRetryToken); + + CommandsRegistry.registerCommand( + "dance.tokenizeLines", + async (accessor, resource, startLine, endLine = startLine) => { + // Convert line numbers from 0-indexing to 1-indexing. + startLine++; + endLine++; + + // Find model for specified document. + const modelService = accessor.get(IModelService), + model = modelService.getModel(resource); + + if (model == null) { + return; + } + + // Find grammar for specified document. + const textMateService = accessor.get(ITextMateService), + language = model.getLanguageIdentifier().language, + grammar = await textMateService.createGrammar(language); + + if (grammar == null) { + return; + } + + // Set-up state for the first line of the range. + let state = null; + + for (let i = 1; i < startLine; i++) { + state = grammar.tokenizeLine(model.getLineContent(i), state).ruleStack; + } + + // Tokenize lines in given range and add them to the result. + const tokenizedLines = []; + + for (let i = startLine; i <= endLine; i++) { + const tokenizationResult = grammar.tokenizeLine(model.getLineContent(i), state); + + tokenizedLines.push(tokenizationResult.tokens); + state = tokenizationResult.ruleStack; + } + + return tokenizedLines; + }, + ); + } catch (e) { + if (danceRetries++ === 10) { + console.error("Could not register dance.tokenizeLines.", e); + clearInterval(danceRetryToken); + } + } +}, 1000); +``` + +This will register a global command `dance.tokenizeLines` that will query the +internal VS Code state and return token information for the given lines in the +specified document. Right now, it's no used anywhere, but I could see myself +making commands that use this information to better find what should or should +not be selected. + +The path to `loader.js` can be found by running the following code in the +developer tools of VS Code: + +```js +path.join(process.resourcesPath, "app", "out", "vs", "loader.js") +``` diff --git a/src/api/clipboard.ts b/src/api/clipboard.ts new file mode 100644 index 0000000..320cc4e --- /dev/null +++ b/src/api/clipboard.ts @@ -0,0 +1,16 @@ +import * as vscode from "vscode"; +import { Context } from "./context"; + +/** + * Copies the given text to the clipboard. + */ +export function copy(text: string) { + return Context.wrap(vscode.env.clipboard.writeText(text)); +} + +/** + * Returns the text in the clipboard. + */ +export function clipboard() { + return Context.wrap(vscode.env.clipboard.readText()); +} diff --git a/src/api/context.ts b/src/api/context.ts new file mode 100644 index 0000000..ffb2093 --- /dev/null +++ b/src/api/context.ts @@ -0,0 +1,526 @@ +import * as vscode from "vscode"; + +import { EditNotAppliedError, EditorRequiredError } from "./errors"; +import { Selections } from "./selections"; +import { CommandDescriptor } from "../commands"; +import { PerEditorState } from "../state/editors"; +import { Extension } from "../state/extension"; +import { Mode, SelectionBehavior } from "../state/modes"; +import { noUndoStops, performDummyEdit } from "../utils/misc"; + +let currentContext: ContextWithoutActiveEditor | undefined; + +/** + * @see Context.WithoutActiveEditor + */ +class ContextWithoutActiveEditor { + /** + * Returns the current execution context, or throws an error if called outside + * of an execution context. + */ + public static get current() { + if (currentContext === undefined) { + throw new Error("attempted to access context object outside of execution context"); + } + + return currentContext; + } + + /** + * Returns the current execution context, or `undefined` if called outside of + * an execution context. + */ + public static get currentOrUndefined() { + return currentContext; + } + + /** + * Equivalent to calling `wrap` on `Context.current`. If there is no current + * context, it returns the `thenable` directly. + */ + public static wrap(thenable: Thenable) { + return this.currentOrUndefined?.wrap(thenable) ?? thenable; + } + + /** + * Equivalent to calling `then` on the current context. If there is no current + * context, it returns the `thenable.then` directly. + */ + public static then( + thenable: Thenable, + onFulfilled?: (value: T) => R, + onRejected?: (reason: any) => R, + ) { + return this.currentOrUndefined?.then(thenable, onFulfilled, onRejected) + ?? thenable.then(onFulfilled, onRejected); + } + + /** + * Equivalent to calling `setup` on the current context. + */ + public static setup() { + return this.current.setup(); + } + + public constructor( + /** + * The global extension state. + */ + public readonly extension: Extension, + + /** + * The token used to cancel an operation running in the current context. + */ + public readonly cancellationToken: vscode.CancellationToken, + + /** + * The descriptor of the command that led to the creation of this context. + */ + public readonly commandDescriptor?: CommandDescriptor, + ) {} + + /** + * Creates a new promise that executes within the current context. + */ + public createPromise( + executor: (resolve: (value: T) => void, reject: (error: any) => void) => void, + ) { + return this.wrap(new Promise(executor)); + } + + /** + * Runs the given function within the current context. + */ + public run(f: (context: this) => T) { + const previousContext = currentContext; + + currentContext = this; + + try { + return f(this); + } finally { + currentContext = previousContext; + } + } + + /** + * Runs the given async function within the current context. + */ + public async runAsync(f: (context: this) => T): Promise ? R : T> { + const previousContext = currentContext; + + currentContext = this; + + try { + return await f(this) as any; + } finally { + currentContext = previousContext; + } + } + + /** + * Returns a promise whose continuations will be wrapped in a way that + * preserves the current context. + * + * Await a call to `setup` in an async function to make ensure that all + * subsequent `await` expressions preserve the current context. + */ + public setup() { + return this.wrap(Promise.resolve()); + } + + /** + * Wraps the given promise in a way that preserves the current context in + * `then`. + */ + public wrap(thenable: Thenable): Thenable { + return { + then: (onFulfilled?: (value: T) => R, onRejected?: (reason: any) => R) => { + return this.then(thenable, onFulfilled, onRejected); + }, + }; + } + + /** + * Wraps the continuation of a promise in order to preserve the current + * context. + */ + public then( + thenable: Thenable, + onFulfilled?: (value: T) => R, + onRejected?: (reason: any) => R, + ) { + if (onFulfilled !== undefined) { + const f = onFulfilled; + + onFulfilled = (value: T) => this.runAsync(() => f(value) as any) as any; + } + + if (onRejected !== undefined) { + const f = onRejected; + + onRejected = (reason: any) => this.runAsync(() => f(reason) as any) as any; + } + + return this.wrap(thenable.then(onFulfilled, onRejected)); + } +} + +const enum ContextFlags { + None = 0, + ShouldInsertUndoStop = 1, +} + +/** + * The context of execution of a script. + */ +export class Context extends ContextWithoutActiveEditor { + /** + * Returns the current execution context, or throws an error if called outside + * of an execution context or if the execution context does not have an + * active editor. + */ + public static get current() { + if (!(currentContext instanceof Context)) { + throw new Error("current context does not have an active text editor"); + } + + return currentContext; + } + + /** + * Returns the current execution context, or `undefined` if called outside of + * an execution context or if the execution context does not have an active + * editor. + */ + public static get currentOrUndefined() { + if (currentContext === undefined || !(currentContext instanceof Context)) { + return undefined; + } + + return currentContext; + } + + private _flags = ContextFlags.None; + + private _document: vscode.TextDocument; + private _editor: vscode.TextEditor; + private _mode: Mode; + + /** + * The current `vscode.TextDocument`. + */ + public get document() { + return this._document; + } + + /** + * The current `vscode.TextEditor`. + * + * Avoid accessing `editor.selections` -- selections may need to be + * transformed before being returned or updated, which is why + * `context.selections` should be preferred. + */ + public get editor() { + return this._editor as Omit; + } + + /** + * The `Mode` associated with the current `vscode.TextEditor`. + */ + public get mode() { + return this._mode; + } + + /** + * The selection behavior for this context. + * + * @deprecated Try to avoid using this property. + */ + public get selectionBehavior() { + return this._mode.selectionBehavior; + } + + /** + * The current selections. + * + * Selections returned by this property **may be different** from the ones + * returned by `editor.selections`. If the current selection behavior is + * `Character`, strictly forward-facing (i.e. `active > anchor`) selections + * will be made longer by one character. + */ + public get selections() { + const editor = this.editor as vscode.TextEditor; + + if (this.selectionBehavior === SelectionBehavior.Character) { + return Selections.fromCharacterMode(editor.selections, editor.document); + } + + return editor.selections; + } + + /** + * Sets the current selections. + * + * If the current selection behavior is `Character`, strictly forward-facing + * (i.e. `active > anchor`) selections will be made shorter by one character. + */ + public set selections(selections: readonly vscode.Selection[]) { + const editor = this.editor as vscode.TextEditor; + + if (this.selectionBehavior === SelectionBehavior.Character) { + selections = Selections.toCharacterMode(selections, editor.document); + } + + editor.selections = selections as vscode.Selection[]; + } + + /** + * Equivalent to `selections[0]`. + * + * @see selections + */ + public get mainSelection() { + const editor = this.editor as vscode.TextEditor; + + if (this.selectionBehavior === SelectionBehavior.Character) { + return Selections.fromCharacterMode([editor.selection], editor.document)[0]; + } + + return editor.selection; + } + + public constructor( + state: PerEditorState, + cancellationToken: vscode.CancellationToken, + commandDescriptor?: CommandDescriptor, + ) { + super(state.extension, cancellationToken, commandDescriptor); + + this._document = state.editor.document; + this._editor = state.editor; + this._mode = state.mode; + } + + /** + * Returns the mode-specific state for the current context. + */ + public getState() { + return this.extension.editors.getState(this._editor)!; + } + + /** + * Performs changes on the editor of the context. + */ + public edit( + f: (editBuilder: vscode.TextEditorEdit, selections: readonly vscode.Selection[], + document: vscode.TextDocument) => T, + ) { + let value: T; + + const document = this.document, + selections = f.length >= 2 ? this.selections : []; + + return this.wrap( + this.editor.edit( + (editBuilder) => value = f(editBuilder, selections, document), + noUndoStops, + ).then((succeeded) => { + EditNotAppliedError.throwIfNotApplied(succeeded); + + this._flags |= ContextFlags.ShouldInsertUndoStop; + + return value; + }), + ); + } + + /** + * Returns whether edits have been performed in this context but not committed + * with `insertUndoStop`. + */ + public hasEditsWithoutUndoStops() { + return (this._flags & ContextFlags.ShouldInsertUndoStop) === ContextFlags.ShouldInsertUndoStop; + } + + /** + * Inserts an undo stop if needed. + */ + public insertUndoStop() { + if (!this.hasEditsWithoutUndoStops()) { + return Promise.resolve(); + } + + return this.wrap(performDummyEdit(this._editor)); + } + + /** + * Switches the context to the given document. + */ + public async switchToDocument(document: vscode.TextDocument, alsoFocusEditor = false) { + const editor = await vscode.window.showTextDocument(document, undefined, !alsoFocusEditor); + + this._document = document; + this._editor = editor; + this._mode = this.extension.editors.getState(editor).mode; + } + + /** + * Switches the mode of the current editor to the given mode. + */ + public switchToMode(mode: Mode) { + const state = this.extension.editors.getState(this._editor); + + return state.setMode(mode).then(() => { + this._mode = state.mode; + }); + } +} + +export namespace Context { + /** + * The base `Context` class, which does not require an active + * `vscode.TextEditor`. + */ + export const WithoutActiveEditor = ContextWithoutActiveEditor; + + export type WithoutActiveEditor = ContextWithoutActiveEditor; + + /** + * Returns a `Context` or `Context.WithoutActiveEditor` depending on whether + * there is an active text editor. + */ + export function create(extension: Extension, command: CommandDescriptor) { + const activeEditorState = extension.editors.active, + cancellationToken = extension.cancellationToken; + + return activeEditorState === undefined + ? new Context.WithoutActiveEditor(extension, cancellationToken, command) + : new Context(activeEditorState, cancellationToken, command); + } + + /** + * Returns a `Context` or throws an exception if there is no active text + * editor. + */ + export function createWithActiveTextEditor(extension: Extension, command: CommandDescriptor) { + const activeEditorState = extension.editors.active; + + EditorRequiredError.throwUnlessAvailable(activeEditorState); + + return new Context(activeEditorState, extension.cancellationToken, command); + } +} + +/** + * Returns the text of the given range in the current context. + * + * ### Example + * ```js + * const start = new vscode.Position(0, 0), + * end = new vscode.Position(0, 3); + * + * expect( + * text(new vscode.Range(start, end)), + * "to be", + * "foo", + * ); + * ``` + * + * With: + * ``` + * foo bar + * ``` + */ +export function text(range: vscode.Range): string; + +/** + * Returns the text of all the given ranges in the current context. + * + * ### Example + * ```js + * const start1 = new vscode.Position(0, 0), + * end1 = new vscode.Position(0, 3), + * start2 = new vscode.Position(0, 4), + * end2 = new vscode.Position(0, 7); + * + * expect( + * text([new vscode.Range(start1, end1), new vscode.Range(start2, end2)]), + * "to equal", + * ["foo", "bar"], + * ); + * ``` + * + * With: + * ``` + * foo bar + * ``` + */ +export function text(ranges: readonly vscode.Range[]): string[]; + +export function text(ranges: vscode.Range | readonly vscode.Range[]) { + const document = Context.current.document; + + if (Array.isArray(ranges)) { + return ranges.map((range) => document.getText(range)); + } + + return document.getText(ranges as vscode.Range); +} + +/** + * Performs changes on the active editor. + * + * ### Example + * ```js + * await edit((editBuilder) => { + * const start = new vscode.Position(0, 2), + * end = new vscode.Position(0, 4); + * + * editBuilder.delete(new vscode.Range(start, end)); + * }); + * ``` + * + * Before: + * ``` + * hello world + * ^^^^^ 0 + * ``` + * + * After: + * ``` + * heo world + * ^^^ 0 + * ``` + */ +export function edit( + f: (editBuilder: vscode.TextEditorEdit, selections: readonly vscode.Selection[], + document: vscode.TextDocument) => T, + editor?: vscode.TextEditor, +) { + if (editor !== undefined) { + let value: T; + + return editor.edit( + (editBuilder) => value = f(editBuilder, editor!.selections, editor!.document), + noUndoStops, + ).then((succeeded) => { + EditNotAppliedError.throwIfNotApplied(succeeded); + + return value; + }); + } + + return Context.current.edit(f); +} + +/** + * Marks a change, inserting a history undo stop. + */ +export function insertUndoStop(editor?: vscode.TextEditor) { + if (editor !== undefined) { + return performDummyEdit(editor); + } + + return Context.current.insertUndoStop(); +} diff --git a/src/api/edit/index.ts b/src/api/edit/index.ts new file mode 100644 index 0000000..f0a46ae --- /dev/null +++ b/src/api/edit/index.ts @@ -0,0 +1,791 @@ +import * as vscode from "vscode"; + +import { TrackedSelection } from "../../utils/tracked-selection"; +import { Context, edit } from "../context"; +import { Positions } from "../positions"; +import { rotateSelections, Selections } from "../selections"; + +const enum Constants { + PositionMask = 0b00_11_1, + BehaviorMask = 0b11_00_1, +} + +function mapResults( + insertFlags: insert.Flags, + document: vscode.TextDocument, + selections: readonly vscode.Selection[], + replacements: readonly replace.Result[], +) { + let flags = TrackedSelection.Flags.Inclusive, + where = undefined as "start" | "end" | "active" | "anchor" | undefined; + + switch (insertFlags & Constants.PositionMask) { + case insert.Flags.Active: + where = "active"; + break; + + case insert.Flags.Anchor: + where = "anchor"; + break; + + case insert.Flags.Start: + where = "start"; + break; + + case insert.Flags.End: + where = "end"; + break; + } + + if (where !== undefined && (insertFlags & Constants.BehaviorMask) === insert.Flags.Keep) { + flags = (insertFlags & Constants.PositionMask) === insert.Flags.Start + ? TrackedSelection.Flags.StrictStart + : TrackedSelection.Flags.StrictEnd; + } + + const savedSelections = TrackedSelection.fromArray(selections, document), + discardedSelections = new Uint8Array(selections.length); + + const promise = edit((editBuilder) => { + for (let i = 0, len = replacements.length; i < len; i++) { + const result = replacements[i], + selection = selections[i]; + + if (result === undefined) { + editBuilder.delete(selection); + discardedSelections[i] = 1; + } else if (where === undefined) { + editBuilder.replace(selection, result); + + if (TrackedSelection.length(savedSelections, i) !== result.length) { + const documentChangedEvent: vscode.TextDocumentContentChangeEvent[] = [{ + range: selection, + rangeOffset: TrackedSelection.startOffset(savedSelections, i), + rangeLength: TrackedSelection.length(savedSelections, i), + text: result, + }]; + + TrackedSelection.updateAfterDocumentChanged(savedSelections, documentChangedEvent, flags); + } + } else { + const position = selection[where]; + + editBuilder.replace(position, result); + + const selectionOffset = TrackedSelection.startOffset(savedSelections, i), + selectionLength = TrackedSelection.length(savedSelections, i); + + const documentChangedEvent: vscode.TextDocumentContentChangeEvent[] = [{ + range: new vscode.Range(position, position), + rangeOffset: position === selection.start + ? selectionOffset + : selectionOffset + selectionLength, + rangeLength: 0, + text: result, + }]; + + TrackedSelection.updateAfterDocumentChanged(savedSelections, documentChangedEvent, flags); + } + } + }).then(() => { + const results: vscode.Selection[] = []; + + for (let i = 0, len = discardedSelections.length; i < len; i++) { + if (discardedSelections[i]) { + continue; + } + + let restoredSelection = TrackedSelection.restore(savedSelections, i, document); + + if (where !== undefined && (insertFlags & Constants.BehaviorMask) === insert.Flags.Select) { + // Selections were extended; we now unselect the previously selected + // text. + const totalLength = TrackedSelection.length(savedSelections, i), + insertedLength = replacements[i]!.length, + previousLength = totalLength - insertedLength; + + if (restoredSelection[where] === restoredSelection.start) { + restoredSelection = Selections.fromStartEnd( + restoredSelection.start, + Positions.offset(restoredSelection.end, -previousLength)!, + restoredSelection.isReversed, + ); + } else { + restoredSelection = Selections.fromStartEnd( + Positions.offset(restoredSelection.start, previousLength)!, + restoredSelection.end, + restoredSelection.isReversed, + ); + } + } + + results.push(restoredSelection); + } + + return results; + }); + + return Context.wrap(promise); +} + +/** + * Inserts text next to the given selections according to the given function. + * + * @param f A mapping function called for each selection; given the text content + * and the index of the selection, it should return the new text content of + * the selection, or `undefined` if it is to be removed. Also works for + * `async` (i.e. `Promise`-returning) functions, in which case **all** results + * must be promises. + * @param selections If `undefined`, the selections of the active editor will be + * used. Otherwise, must be a `vscode.Selection` array which will be mapped + * in the active editor. + * + * ### Example + * ```js + * Selections.set(await insert(insert.Replace, (x) => `${+x * 2}`)); + * ``` + * + * Before: + * ``` + * 1 2 3 + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * 2 4 6 + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + */ +export function insert( + flags: insert.Flags, + f: insert.Callback | insert.Callback, + selections?: readonly vscode.Selection[], +): Thenable { + return insert.byIndex( + flags, + (i, selection, document) => f(document.getText(selection), selection, i, document) as any, + selections, + ); +} + +export namespace insert { + /** + * Insertion flags for `insert`. + */ + export const enum Flags { + /** + * Replace text and select replacement text. + */ + Replace = 0, + + /** + * Insert at active position of selection. + */ + Active = 0b00_00_1, + + /** + * Insert at start of selection. + */ + Start = 0b00_01_1, + + /** + * Insert at end of selection. + */ + End = 0b00_10_1, + + /** + * Insert at anchor of selection. + */ + Anchor = 0b00_11_1, + + /** + * Keep current selections. + */ + Keep = 0b00_00_1, + + /** + * Select inserted text only. + */ + Select = 0b01_00_1, + + /** + * Extend to inserted text. + */ + Extend = 0b10_00_1, + } + + export const Replace = Flags.Replace, + Start = Flags.Start, + End = Flags.End, + Active = Flags.Active, + Anchor = Flags.Anchor, + Keep = Flags.Keep, + Select = Flags.Select, + Extend = Flags.Extend; + + export function flagsAtEdge(edge?: "active" | "anchor" | "start" | "end") { + switch (edge) { + case undefined: + return Flags.Replace; + + case "active": + return Flags.Active; + case "anchor": + return Flags.Anchor; + case "start": + return Flags.Start; + case "end": + return Flags.End; + } + } + + /** + * The result of a callback passed to `insert` or `insert.byIndex`. + */ + export type Result = string | undefined; + + /** + * The result of an async callback passed to `insert` or `insert.byIndex`. + */ + export type AsyncResult = Thenable; + + /** + * A callback passed to `insert`. + */ + export interface Callback { + (text: string, selection: vscode.Selection, index: number, document: vscode.TextDocument): T; + } + + /** + * A callback passed to `insert.byIndex`. + */ + export interface ByIndexCallback { + (index: number, selection: vscode.Selection, document: vscode.TextDocument): T; + } + + /** + * Inserts text next to the given selections according to the given function. + * + * @param f A mapping function called for each selection; given the index, + * range and editor of each selection, it should return the new text content + * of the selection, or `undefined` if it is to be removed. Also works for + * `async` (i.e. `Promise`-returning) functions, in which case **all** + * results must be promises. + * @param selections If `undefined`, the selections of the active editor will + * be used. Otherwise, must be a `vscode.Selection` array which will be + * mapped in the active editor. + * + * ### Example + * ```js + * Selections.set(await insert.byIndex(insert.Start, (i) => `${i + 1}`)); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * 1a 2b 3c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * ### Example + * ```js + * Selections.set(await insert.byIndex(insert.Start | insert.Select, (i) => `${i + 1}`)); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * 1a 2b 3c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * ### Example + * ```js + * Selections.set(await insert.byIndex(insert.Start | insert.Extend, (i) => `${i + 1}`)); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * 1a 2b 3c + * ^^ 0 + * ^^ 1 + * ^^ 2 + * ``` + * + * ### Example + * ```js + * Selections.set(await insert.byIndex(insert.End, (i) => `${i + 1}`)); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * a1 b2 c3 + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * ### Example + * ```js + * Selections.set(await insert.byIndex(insert.End | insert.Select, (i) => `${i + 1}`)); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * a1 b2 c3 + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * ### Example + * ```js + * Selections.set(await insert.byIndex(insert.End | insert.Extend, (i) => `${i + 1}`)); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * a1 b2 c3 + * ^^ 0 + * ^^ 1 + * ^^ 2 + * ``` + */ + export function byIndex( + flags: Flags, + f: ByIndexCallback | ByIndexCallback, + selections: readonly vscode.Selection[] = Context.current.selections, + ): Thenable { + if (selections.length === 0) { + return Context.wrap(Promise.resolve([])); + } + + const document = Context.current.document, + firstResult = f(0, selections[0], document); + + if (typeof firstResult === "object") { + // `f` returns promises. + const promises = [firstResult]; + + for (let i = 1, len = selections.length; i < len; i++) { + promises.push(f(i, selections[i], document) as AsyncResult); + } + + return Context.wrap( + Promise + .all(promises) + .then((results) => mapResults(flags, document, selections, results)), + ); + } + + // `f` returns regular values. + const allResults: Result[] = [firstResult]; + + for (let i = 1, len = selections.length; i < len; i++) { + allResults.push(f(i, selections[i], document) as Result); + } + + return mapResults(flags, document, selections, allResults); + } + + export namespace byIndex { + /** + * Same as `insert.byIndex`, but also inserts strings that end with a + * newline character on the next or previous line. + */ + export async function withFullLines( + flags: Flags, + f: ByIndexCallback | ByIndexCallback, + selections: readonly vscode.Selection[] = Context.current.selections, + ) { + const document = Context.current.document, + allResults = await Promise.all(selections.map((sel, i) => f(i, sel, document))); + + // Separate full-line results from all results. + const results: Result[] = [], + resultsSelections: vscode.Selection[] = [], + fullLineResults: Result[] = [], + fullLineResultsSelections: vscode.Selection[] = [], + isFullLines: boolean[] = []; + + for (let i = 0; i < allResults.length; i++) { + const result = allResults[i]; + + if (result === undefined) { + continue; + } + + if (result.endsWith("\n")) { + fullLineResults.push(result); + fullLineResultsSelections.push(selections[i]); + isFullLines.push(true); + } else { + results.push(result); + resultsSelections.push(selections[i]); + isFullLines.push(false); + } + } + + if (fullLineResults.length === 0) { + return await mapResults(flags, document, resultsSelections, results); + } + + let savedSelections = new TrackedSelection.Set( + TrackedSelection.fromArray(fullLineResultsSelections, document), + document, + ); + + // Insert non-full lines. + const normalSelections = await mapResults(flags, document, resultsSelections, results); + + // Insert full lines. + const fullLineSelections = savedSelections.restore(); + + savedSelections.dispose(); + + const nextFullLineSelections: vscode.Selection[] = [], + insertionPositions: vscode.Position[] = []; + + if ((flags & Constants.PositionMask) === Flags.Start) { + for (const selection of fullLineSelections) { + const insertionPosition = Positions.lineStart(selection.start.line); + + insertionPositions.push(insertionPosition); + + if ((flags & Constants.BehaviorMask) === Flags.Extend) { + nextFullLineSelections.push( + Selections.fromStartEnd( + insertionPosition, selection.end, selection.isReversed, document), + ); + } else if ((flags & Constants.BehaviorMask) === Flags.Select) { + nextFullLineSelections.push(Selections.empty(insertionPosition)); + } else { + // Keep selection as is. + nextFullLineSelections.push(selection); + } + } + } else { + for (const selection of fullLineSelections) { + const insertionPosition = Positions.lineStart(Selections.endLine(selection) + 1); + + insertionPositions.push(insertionPosition); + + if ((flags & Constants.BehaviorMask) === Flags.Extend) { + nextFullLineSelections.push( + Selections.fromStartEnd( + selection.start, insertionPosition, selection.isReversed, document), + ); + } else if ((flags & Constants.BehaviorMask) === Flags.Select) { + nextFullLineSelections.push(Selections.empty(insertionPosition)); + } else { + // Keep selection as is. + nextFullLineSelections.push(selection); + } + } + } + + savedSelections = new TrackedSelection.Set( + TrackedSelection.fromArray(nextFullLineSelections, document), + document, + (flags & Constants.BehaviorMask) === Flags.Keep + ? TrackedSelection.Flags.Strict + : TrackedSelection.Flags.Inclusive, + ); + + await edit((editBuilder) => { + for (let i = 0; i < insertionPositions.length; i++) { + editBuilder.replace(insertionPositions[i], fullLineResults[i]!); + } + }); + + const finalFullLineSelections = savedSelections.restore(); + + savedSelections.dispose(); + + // Merge back selections. + const allSelections: vscode.Selection[] = []; + + for (let i = 0, normalIdx = 0, fullLineIdx = 0; i < isFullLines.length; i++) { + if (isFullLines[i]) { + allSelections.push(finalFullLineSelections[fullLineIdx++]); + } else { + allSelections.push(normalSelections[normalIdx++]); + } + } + + return allSelections; + } + } +} + +/** + * Replaces the given selections according to the given function. + * + * @param f A mapping function called for each selection; given the text content + * and the index of the selection, it should return the new text content of + * the selection, or `undefined` if it is to be removed. Also works for + * `async` (i.e. `Promise`-returning) functions, in which case **all** results + * must be promises. + * @param selections If `undefined`, the selections of the active editor will be + * used. Otherwise, must be a `vscode.Selection` array which will be mapped + * in the active editor. + * + * ### Example + * ```js + * await replace((x) => `${+x * 2}`); + * ``` + * + * Before: + * ``` + * 1 2 3 + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * 2 4 6 + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + */ +export function replace( + f: replace.Callback | replace.Callback, + selections?: readonly vscode.Selection[], +): Thenable { + return insert(insert.Flags.Replace, f, selections); +} + +export namespace replace { + /** + * The result of a callback passed to `replace` or `replace.byIndex`. + */ + export type Result = string | undefined; + + /** + * The result of an async callback passed to `replace` or `replace.byIndex`. + */ + export type AsyncResult = Thenable; + + /** + * A callback passed to `replace`. + */ + export interface Callback { + (text: string, selection: vscode.Selection, index: number, document: vscode.TextDocument): T; + } + + /** + * A callback passed to `replace.byIndex`. + */ + export interface ByIndexCallback { + (index: number, selection: vscode.Selection, document: vscode.TextDocument): T; + } + + /** + * Replaces the given selections according to the given function. + * + * @param f A mapping function called for each selection; given the index, + * range and editor of each selection, it should return the new text content + * of the selection, or `undefined` if it is to be removed. Also works for + * `async` (i.e. `Promise`-returning) functions, in which case **all** + * results must be promises. + * @param selections If `undefined`, the selections of the active editor will + * be used. Otherwise, must be a `vscode.Selection` array which will be + * mapped in the active editor. + * + * ### Example + * ```js + * await replace.byIndex((i) => `${i + 1}`); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * 1 2 3 + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + */ + export function byIndex( + f: ByIndexCallback | ByIndexCallback, + selections?: readonly vscode.Selection[], + ): Thenable { + return insert.byIndex(insert.Flags.Replace, f, selections); + } +} + +/** + * Rotates the given selections and their contents by the given offset. + * + * @param selections If `undefined`, the selections of the active editor will be + * used. Otherwise, must be a `vscode.Selection` array which will be mapped + * in the active editor. + * + * ### Example + * ```js + * await rotate(1); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * b c a + * ^ 1 + * ^ 2 + * ^ 0 + * ``` + */ +export function rotate(by: number, selections?: readonly vscode.Selection[]) { + return rotate + .contentsOnly(by, selections) + .then((selections) => rotate.selectionsOnly(by, selections)); +} + +export namespace rotate { + /** + * Rotates the contents of the given selections by the given offset. + * + * @see rotate + * + * ### Example + * ```js + * await rotate.contentsOnly(1); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * b c a + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + */ + export function contentsOnly( + by: number, + selections: readonly vscode.Selection[] = Context.current.selections, + ) { + const len = selections.length; + + // Handle negative values for `by`: + by = (by % len) + len; + + if (by === len) { + return Context.wrap(Promise.resolve(selections.slice())); + } + + return replace.byIndex( + (i, _, document) => document.getText(selections[(i + by) % len]), + selections, + ); + } + + /** + * Rotates the given selections (but not their contents) by the given offset. + * + * @see rotate + * + * ### Example + * ```js + * rotate.selectionsOnly(1); + * ``` + * + * Before: + * ``` + * a b c + * ^ 0 + * ^ 1 + * ^ 2 + * ``` + * + * After: + * ``` + * a b c + * ^ 1 + * ^ 2 + * ^ 0 + * ``` + */ + export function selectionsOnly(by: number, selections?: readonly vscode.Selection[]) { + Selections.set(rotateSelections(by, selections)); + } +} diff --git a/src/api/edit/linewise.ts b/src/api/edit/linewise.ts new file mode 100644 index 0000000..10b38f6 --- /dev/null +++ b/src/api/edit/linewise.ts @@ -0,0 +1,339 @@ +import * as vscode from "vscode"; + +import { Context, edit } from "../context"; +import { blankCharacters } from "../../utils/charset"; + +/** + * Increases the indentation of the given lines by the given count. + * + * ### Example + * ```js + * await indentLines([0, 1, 3]); + * ``` + * + * Before: + * ``` + * a + * + * c + * d + * ``` + * + * After: + * ``` + * a + * + * c + * d + * ``` + * + * ### Example + * ```js + * await indentLines([0], 2); + * ``` + * + * Before: + * ``` + * a + * ``` + * + * After: + * ``` + * a + * ``` + * + * ### Example + * ```js + * await indentLines([0, 1], 1, true); + * ``` + * + * Before: + * ``` + * a + * + * ``` + * + * After: + * ``` + * a + * ·· + * ``` + */ +export function indentLines(lines: Iterable, times = 1, indentEmpty = false) { + const options = Context.current.editor.options, + indent = options.insertSpaces + ? " ".repeat(options.tabSize as number * times) + : "\t".repeat(times); + + if (indentEmpty) { + return edit((editBuilder) => { + const seen = new Set(); + + for (const line of lines) { + const cnt = seen.size; + + if (seen.add(line).size === cnt) { + // Avoid indenting the same line more than once. + continue; + } + + editBuilder.insert(new vscode.Position(line, 0), indent); + } + }); + } else { + return edit((editBuilder, _, document) => { + const seen = new Set(); + + for (const line of lines) { + const cnt = seen.size; + + if (seen.add(line).size === cnt || document.lineAt(line).isEmptyOrWhitespace) { + // Avoid indenting empty lines or the same line more than once. + continue; + } + + editBuilder.insert(new vscode.Position(line, 0), indent); + } + }); + } +} + +/** + * Decreases the indentation of the given lines by the given count. + * + * ### Example + * ```js + * await deindentLines([0, 1, 3]); + * ``` + * + * Before: + * ``` + * a + * ·· + * c + * d + * ``` + * + * After: + * ``` + * a + * + * c + * d + * ``` + * + * ### Example + * ```js + * await deindentLines([0, 1, 3], 2); + * ``` + * + * Before: + * ``` + * a + * ·· + * c + * d + * ``` + * + * After: + * ``` + * a + * + * c + * d + * ``` + */ +export function deindentLines(lines: Iterable, times = 1, deindentIncomplete = true) { + return edit((editBuilder, _, document) => { + const tabSize = Context.current.editor.options.tabSize as number, + needed = times * tabSize, + seen = new Set(); + + for (const line of lines) { + const cnt = seen.size; + + if (seen.add(line).size === cnt) { + // Avoid deindenting the same line more than once. + continue; + } + + const textLine = document.lineAt(line), + text = textLine.text; + + let column = 0, // Column, accounting for tab size. + j = 0; // Index in source line, and number of characters to remove. + + for (; column < needed; j++) { + const char = text[j]; + + if (char === "\t") { + column += tabSize; + } else if (char === " ") { + column++; + } else { + break; + } + } + + if (!deindentIncomplete && j < text.length) { + j -= j % tabSize; + } + + if (j !== 0) { + editBuilder.delete(textLine.range.with(undefined, textLine.range.start.translate(0, j))); + } + } + }); +} + +/** + * Joins all consecutive lines in the given list together with the given + * separator. + * + * ### Example + * ```js + * await joinLines([0]); + * ``` + * + * Before: + * ``` + * a b + * c d + * e f + * g h + * ``` + * + * After: + * ``` + * a b c d + * e f + * g h + * ``` + * + * ### Example + * ```js + * await joinLines([0, 1]); + * ``` + * + * Before: + * ``` + * a b + * c d + * e f + * g h + * ``` + * + * After: + * ``` + * a b c d + * e f + * g h + * ``` + * + * ### Example + * ```js + * await joinLines([0, 2]); + * ``` + * + * Before: + * ``` + * a b + * c d + * e f + * g h + * ``` + * + * After: + * ``` + * a b c d + * e f g h + * ``` + * + * ### Example + * ```js + * await joinLines([1], " "); + * ``` + * + * Before: + * ``` + * a b + * c d + * e f + * g h + * ``` + * + * After: + * ``` + * a b + * c d e f + * g h + * ``` + */ +export function joinLines(lines: Iterable, separator: string = " ") { + // Sort lines (no need to dedup). + const sortedLines = [...lines].sort((a, b) => a - b); + + if (sortedLines.length === 0) { + return Context.current.wrap(Promise.resolve([])); + } + + // Determine all ranges; (range[i], range[i + 1]) <=> (startLine, length). + const ranges = [sortedLines[0], 0] as number[]; + + for (let i = 1, len = sortedLines.length; i < len; i++) { + const line = sortedLines[i], + lastLine = sortedLines[i - 1]; + + if (line === lastLine) { + continue; + } else if (line === lastLine + 1) { + ranges[ranges.length - 1]++; + } else { + ranges.push(line, 0); + } + } + + return edit((editBuilder, _, document) => { + let diff = 0; + const selections = [] as vscode.Selection[]; + + // Perform edit on each line. + for (let i = 0, len = ranges.length; i < len; i += 2) { + const startLine = ranges[i], + count = ranges[i + 1] || 1; + let prevLine = document.lineAt(startLine), + currentEnd = 0; + + for (let j = 0; j < count; j++) { + const nextLine = document.lineAt(startLine + j + 1); + + // Find index of last non-whitespace character. + let endCharacter = prevLine.text.length; + + while (endCharacter > 0) { + if (!blankCharacters.includes(prevLine.text[endCharacter - 1])) { + break; + } + + endCharacter--; + } + + const start = new vscode.Position(prevLine.lineNumber, endCharacter), + end = new vscode.Position(nextLine.lineNumber, + nextLine.firstNonWhitespaceCharacterIndex), + finalCharacter = currentEnd + endCharacter, + finalStart = new vscode.Position(startLine - diff, finalCharacter), + finalEnd = new vscode.Position(startLine - diff, finalCharacter + separator.length); + + editBuilder.replace(new vscode.Range(start, end), separator); + selections.push(new vscode.Selection(finalStart, finalEnd)); + prevLine = nextLine; + currentEnd = finalCharacter + separator.length - nextLine.firstNonWhitespaceCharacterIndex; + } + + diff += count; + } + + return selections; + }); +} diff --git a/src/api/errors.ts b/src/api/errors.ts new file mode 100644 index 0000000..b41f793 --- /dev/null +++ b/src/api/errors.ts @@ -0,0 +1,214 @@ +import * as vscode from "vscode"; +import { Context } from "./context"; +import { PerEditorState } from "../state/editors"; + +/** + * Asserts that the given condition is true. + */ +export function assert(condition: boolean): asserts condition { + if (!condition) { + const error = new Error( + "internal assertion failed; please report this error on https://github.com/71/dance/issues. " + + "its stacktrace is available in the developer console (Command Palette > Open Developer " + + "Tools).", + ); + + // Log error to ensure its stacktrace can be found. + console.error(error); + + throw error; + } +} + +/** + * Throws an exception indicating that the caller is not implemented yet. + */ +export function todo(): never { + const context = Context.WithoutActiveEditor.currentOrUndefined; + + if (context?.commandDescriptor !== undefined) { + throw new Error(`command not implemented: ${context.commandDescriptor.identifier}`); + } + + throw new Error("function not implemented"); +} + +/** + * An error thrown when no selections remain. + */ +export class EmptySelectionsError extends Error { + public constructor(message = "no selections remain") { + super(message); + } + + /** + * Throws if the given selections are empty. + */ + public static throwIfEmpty(selections: readonly vscode.Selection[]) { + if (selections.length === 0) { + throw new EmptySelectionsError(); + } + } + + /** + * Throws if the selections of the given register are empty. + */ + public static throwIfRegisterIsEmpty( + selections: readonly T[] | undefined, + registerName: string, + ): asserts selections is readonly T[] { + if (selections === undefined || selections.length === 0) { + throw new EmptySelectionsError(`no selections are saved in register "${registerName}"`); + } + } +} + +/** + * Error thrown when a given argument is not as expected. + */ +export class ArgumentError extends Error { + public constructor(message: string, public readonly argumentName?: string) { + super(message); + } + + public static validate( + argumentName: string, + condition: boolean, + message: string | (() => string), + ): asserts condition { + if (!condition) { + if (typeof message === "function") { + message = message(); + } + + throw new ArgumentError(message, argumentName); + } + } +} + +/** + * Error thrown when a user input is not as expected. + */ +export class InputError extends ArgumentError { + public constructor(message: string) { + super(message, "input"); + } + + public static validateInput( + condition: boolean, + message: string, + ): asserts condition { + if (!condition) { + throw new this(message); + } + } +} + +/** + * Error thrown when a function that is expected to return a selection returns + * something else. + */ +export class NotASelectionError extends ArgumentError { + public constructor(public readonly value: unknown) { + super("value is not a selection"); + } + + /** + * Throws if the given value is not a `vscode.Selection`. + */ + public static throwIfNotASelection(value: unknown): asserts value is vscode.Selection { + if (!(value instanceof vscode.Selection)) { + throw new NotASelectionError(value); + } + } + + /** + * Throws if the given list contains a value that is not a `vscode.Selection`, + * or if the list is empty. + */ + public static throwIfNotASelectionArray(value: unknown): asserts value is vscode.Selection[] { + if (!Array.isArray(value) || value.length === 0) { + throw new EmptySelectionsError(); + } + + for (let i = 0, len = value.length; i < len; i++) { + NotASelectionError.throwIfNotASelection(value[i]); + } + } +} + +/** + * Error thrown when an action requiring an editor is executed without an + * active `vscode.TextEditor`. + */ +export class EditorRequiredError extends Error { + public constructor() { + super("active editor required"); + } + + public static throwUnlessAvailable( + editorState: T | undefined, + ): asserts editorState is T { + if (editorState === undefined) { + throw new EditorRequiredError(); + } + } +} + +/** + * Error thrown when a cancellation is requested. + */ +export class CancellationError extends Error { + public constructor( + public readonly reason: CancellationError.Reason, + ) { + super(reason); + } + + public static throwIfCancellationRequested(token: vscode.CancellationToken) { + if (token.isCancellationRequested) { + throw new CancellationError(CancellationError.Reason.CancellationToken); + } + } +} + +export namespace CancellationError { + export const enum Reason { + CancellationToken = "cancellation token was used", + PressedEscape = "user pressed ", + } +} + +/** + * Error thrown when two arrays that are expected to have the same length have + * different lengths + */ +export class LengthMismatchError extends Error { + public constructor() { + super("length mismatch"); + } + + public static throwIfLengthMismatch(a: readonly A[], b: readonly B[]) { + if (a.length !== b.length) { + throw new LengthMismatchError(); + } + } +} + +/** + * An error thrown when a `TextEditor.edit` call returns `false`. + */ +export class EditNotAppliedError extends Error { + public constructor() { + super("TextEditor edit failed"); + } + + /** + * Throws if the given value is `false`. + */ + public static throwIfNotApplied(editWasApplied: boolean): asserts editWasApplied { + if (!editWasApplied) { + throw new EditNotAppliedError(); + } + } +} diff --git a/src/api/functional.ts b/src/api/functional.ts new file mode 100644 index 0000000..91f8122 --- /dev/null +++ b/src/api/functional.ts @@ -0,0 +1,416 @@ +import * as vscode from "vscode"; + +/** + * A VS Code `Position`, `Range` or `Selection`. + */ +export type PRS = vscode.Position | vscode.Range | vscode.Selection; + +/** + * A VS Code `Position` or `Selection`. + */ +export type PS = vscode.Position | vscode.Selection; + +/** + * Returns whether the given value is a `vscode.Position` object. + * + * ### Example + * + * ```js + * const position = new vscode.Position(0, 0), + * range = new vscode.Range(position, position), + * selection = new vscode.Selection(position, position); + * + * assert(isPosition(position)); + * assert(!isPosition(range)); + * assert(!isPosition(selection)); + * ``` + */ +export function isPosition(x: unknown): x is vscode.Position { + return x != null && (x as object).constructor === vscode.Position; +} + +/** + * Returns whether the given value is a `vscode.Range` object. + * + * ### Example + * + * ```js + * const position = new vscode.Position(0, 0), + * range = new vscode.Range(position, position), + * selection = new vscode.Selection(position, position); + * + * assert(!isRange(position)); + * assert(isRange(range)); + * assert(!isRange(selection)); + * ``` + */ +export function isRange(x: unknown): x is vscode.Range { + return x != null && (x as object).constructor === vscode.Range; +} + +/** + * Returns whether the given value is a `vscode.Selection` object. + * + * ### Example + * + * ```js + * const position = new vscode.Position(0, 0), + * range = new vscode.Range(position, position), + * selection = new vscode.Selection(position, position); + * + * assert(!isSelection(position)); + * assert(!isSelection(range)); + * assert(isSelection(selection)); + * ``` + */ +export function isSelection(x: unknown): x is vscode.Selection { + return x != null && (x as object).constructor === vscode.Selection; +} + +/** + * Returns a `PRS` whose start position is mapped using the given function. + * + * ### Example + * + * ```js + * const p1 = new vscode.Position(0, 0), + * p2 = new vscode.Position(0, 1); + * + * assert.deepStrictEqual( + * mapStart(p1, (x) => x.translate(1)), + * new vscode.Position(1, 0), + * ); + * assert.deepStrictEqual( + * mapStart(new vscode.Range(p1, p2), (x) => x.translate(1)), + * new vscode.Range(p2, new vscode.Position(1, 0)), + * ); + * assert.deepStrictEqual( + * mapStart(new vscode.Selection(p1, p2), (x) => x.translate(1)), + * new vscode.Selection(new vscode.Position(1, 0), p2), + * ); + * assert.deepStrictEqual( + * mapStart(new vscode.Selection(p2, p1), (x) => x.translate(1)), + * new vscode.Selection(p2, new vscode.Position(1, 0)), + * ); + * ``` + */ +export function mapStart(x: T, f: (_: vscode.Position) => vscode.Position) { + if (isSelection(x)) { + return x.start === x.anchor + ? new vscode.Selection(f(x.start), x.end) as T + : new vscode.Selection(x.end, f(x.start)) as T; + } + + if (isRange(x)) { + return new vscode.Range(f(x.start), x.end) as T; + } + + return f(x as vscode.Position) as T; +} + +/** + * Returns a `PRS` whose end position is mapped using the given function. + * + * ### Example + * + * ```js + * const p1 = new vscode.Position(0, 0), + * p2 = new vscode.Position(0, 1); + * + * assert.deepStrictEqual( + * mapEnd(p1, (x) => x.translate(1)), + * new vscode.Position(1, 0), + * ); + * assert.deepStrictEqual( + * mapEnd(new vscode.Range(p1, p2), (x) => x.translate(1)), + * new vscode.Range(p1, new vscode.Position(1, 1)), + * ); + * assert.deepStrictEqual( + * mapEnd(new vscode.Selection(p1, p2), (x) => x.translate(1)), + * new vscode.Selection(p1, new vscode.Position(1, 1)), + * ); + * assert.deepStrictEqual( + * mapEnd(new vscode.Selection(p2, p1), (x) => x.translate(1)), + * new vscode.Selection(new vscode.Position(1, 1), p1), + * ); + * ``` + */ +export function mapEnd(x: T, f: (_: vscode.Position) => vscode.Position) { + if (isSelection(x)) { + return x.start === x.anchor + ? new vscode.Selection(x.start, f(x.end)) as T + : new vscode.Selection(f(x.end), x.start) as T; + } + + if (isRange(x)) { + return new vscode.Range(x.start, f(x.end)) as T; + } + + return f(x as vscode.Position) as T; +} + +/** + * Returns a `PS` whose active position is mapped using the given function. + * + * ### Example + * + * ```js + * const p1 = new vscode.Position(0, 0), + * p2 = new vscode.Position(0, 1); + * + * assert.deepStrictEqual( + * mapActive(p1, (x) => x.translate(1)), + * new vscode.Position(1, 0), + * ); + * assert.deepStrictEqual( + * mapActive(new vscode.Selection(p1, p2), (x) => x.translate(1)), + * new vscode.Selection(p1, new vscode.Position(1, 1)), + * ); + * ``` + */ +export function mapActive(x: T, f: (_: vscode.Position) => vscode.Position) { + if (isSelection(x)) { + return new vscode.Selection(x.anchor, f(x.active)) as T; + } + + return f(x as vscode.Position) as T; +} + +/** + * Returns a `PRS` whose start and end positions are mapped using the given + * function. + * + * ### Example + * + * ```js + * const p1 = new vscode.Position(0, 0), + * p2 = new vscode.Position(0, 1); + * + * assert.deepStrictEqual( + * mapBoth(p1, (x) => x.translate(1)), + * new vscode.Position(1, 0), + * ); + * assert.deepStrictEqual( + * mapBoth(new vscode.Range(p1, p2), (x) => x.translate(1)), + * new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 1)), + * ); + * assert.deepStrictEqual( + * mapBoth(new vscode.Selection(p1, p2), (x) => x.translate(1)), + * new vscode.Selection(new vscode.Position(1, 0), new vscode.Position(1, 1)), + * ); + * assert.deepStrictEqual( + * mapBoth(new vscode.Selection(p2, p1), (x) => x.translate(1)), + * new vscode.Selection(new vscode.Position(1, 1), new vscode.Position(1, 0)), + * ); + * ``` + */ +export function mapBoth(x: T, f: (_: vscode.Position) => vscode.Position) { + if (isSelection(x)) { + return new vscode.Selection(f(x.anchor), f(x.active)) as T; + } + + if (isRange(x)) { + return new vscode.Range(f(x.start), f(x.end)) as T; + } + + return f(x as vscode.Position) as T; +} + +/** + * Given a function type with at least one parameter, returns a pair of all the + * argument types except the last one, and then the last argument type. + */ +export type SplitParameters = F extends (...args: infer AllArgs) => any + ? AllArgs extends [...infer Args, infer LastArg] ? [Args, LastArg] : never + : never; + +/** + * Returns a function that takes the `n - 1` first parameters of `f`, and + * returns yet another function that takes the last parameter of `f`, and + * returns `f(...args, lastArg)`. + * + * ### Example + * + * ```js + * const add2 = (a, b) => a + b, + * add3 = (a, b, c) => a + b + c; + * + * expect(add2(1, 2)).to.be.equal(3); + * expect(add3(1, 2, 3)).to.be.equal(6); + * + * expect(curry(add2)(1)(2)).to.be.equal(3); + * expect(curry(add3)(1, 2)(3)).to.be.equal(6); + * ``` + */ +export function curry any>(f: F, ...counts: number[]) { + if (counts.length === 0) { + return (...args: SplitParameters[0]) => (lastArg: SplitParameters[1]) => { + return f(...args, lastArg); + }; + } + + // TODO: review this + let curried: any = f; + + for (let i = counts.length - 1; i >= 0; i--) { + const prev = curried, + len = counts[i]; + + curried = (...args: any[]) => (...newArgs: any[]) => { + const allArgs = args; + let i = 0; + + for (; i < newArgs.length && i < len; i++) { + allArgs.push(newArgs[i]); + } + + for (; i < len; i++) { + allArgs.push(undefined); + } + + return prev(...allArgs); + }; + } + + return curried; +} + +/* eslint-disable max-len */ + +// In case we need `pipe` for more than 10 functions: +// +// Array.from({ length: 10 }, (_, n) => { +// const lo = n => String.fromCharCode(97 + n), +// hi = n => String.fromCharCode(65 + n); +// +// return ` +// /** +// * Returns a function that maps all non-\`undefined\` values +// * through the given function and returns the remaining results. +// */ +// export function pipe<${ +// Array.from({ length: n + 2 }, (_, i) => hi(i)).join(", ") +// }>(${ +// Array.from({ length: n + 1 }, (_, i) => `${lo(i)}: (_: ${hi(i)}) => ${hi(i + 1)} | undefined`).join(", ") +// }): (values: readonly A[]) => ${hi(n + 1)}[];`; +// }).join("\n") + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + */ +export function pipe(a: (_: A) => B | undefined): (values: readonly A[]) => B[]; + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + * + * ### Example + * + * ```js + * const doubleNumbers = pipe((n) => typeof n === "number" ? n : undefined, + * (n) => n * 2); + * + * assert.deepStrictEqual( + * doubleNumbers([1, "a", 2, null, 3, {}]), + * [2, 4, 6], + * ); + * ``` + */ +export function pipe(a: (_: A) => B | undefined, b: (_: B) => C | undefined): (values: readonly A[]) => C[]; + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + */ +export function pipe(a: (_: A) => B | undefined, b: (_: B) => C | undefined, c: (_: C) => D | undefined): (values: readonly A[]) => D[]; + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + */ +export function pipe(a: (_: A) => B | undefined, b: (_: B) => C | undefined, c: (_: C) => D | undefined, d: (_: D) => E | undefined): (values: readonly A[]) => E[]; + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + */ +export function pipe(a: (_: A) => B | undefined, b: (_: B) => C | undefined, c: (_: C) => D | undefined, d: (_: D) => E | undefined, e: (_: E) => F | undefined): (values: readonly A[]) => F[]; + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + */ +export function pipe(a: (_: A) => B | undefined, b: (_: B) => C | undefined, c: (_: C) => D | undefined, d: (_: D) => E | undefined, e: (_: E) => F | undefined, f: (_: F) => G | undefined): (values: readonly A[]) => G[]; + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + */ +export function pipe(a: (_: A) => B | undefined, b: (_: B) => C | undefined, c: (_: C) => D | undefined, d: (_: D) => E | undefined, e: (_: E) => F | undefined, f: (_: F) => G | undefined, g: (_: G) => H | undefined): (values: readonly A[]) => H[]; + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + */ +export function pipe(a: (_: A) => B | undefined, b: (_: B) => C | undefined, c: (_: C) => D | undefined, d: (_: D) => E | undefined, e: (_: E) => F | undefined, f: (_: F) => G | undefined, g: (_: G) => H | undefined, h: (_: H) => I | undefined): (values: readonly A[]) => I[]; + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + */ +export function pipe(a: (_: A) => B | undefined, b: (_: B) => C | undefined, c: (_: C) => D | undefined, d: (_: D) => E | undefined, e: (_: E) => F | undefined, f: (_: F) => G | undefined, g: (_: G) => H | undefined, h: (_: H) => I | undefined, i: (_: I) => J | undefined): (values: readonly A[]) => J[]; + +/** + * Returns a function that maps all non-`undefined` values + * through the given function and returns the remaining results. + */ +export function pipe(a: (_: A) => B | undefined, b: (_: B) => C | undefined, c: (_: C) => D | undefined, d: (_: D) => E | undefined, e: (_: E) => F | undefined, f: (_: F) => G | undefined, g: (_: G) => H | undefined, h: (_: H) => I | undefined, i: (_: I) => J | undefined, j: (_: J) => K | undefined): (values: readonly A[]) => K[]; +/* eslint-enable max-len */ + +export function pipe(...functions: ((_: unknown) => unknown)[]) { + return (values: readonly unknown[]) => { + const results = [], + vlen = values.length, + flen = functions.length; + + for (let i = 0; i < vlen; i++) { + let value = values[i]; + + for (let j = 0; value !== undefined && j < flen; j++) { + value = functions[j](value); + } + + if (value !== undefined) { + results.push(value); + } + } + + return results; + }; +} + +/** + * Same as `pipe`, but also works with async functions. + */ +export function pipeAsync(...functions: ((_: unknown) => unknown)[]) { + return async (values: readonly unknown[]) => { + const results = [], + vlen = values.length, + flen = functions.length; + + for (let i = 0; i < vlen; i++) { + let value = values[i]; + + for (let j = 0; value !== undefined && j < flen; j++) { + value = await functions[j](value); + } + + if (value !== undefined) { + results.push(value); + } + } + + return results; + }; +} diff --git a/src/api/history.ts b/src/api/history.ts new file mode 100644 index 0000000..b176831 --- /dev/null +++ b/src/api/history.ts @@ -0,0 +1,17 @@ +import * as vscode from "vscode"; + +import { Context } from "./context"; + +/** + * Un-does the last action. + */ +export function undo() { + return Context.wrap(vscode.commands.executeCommand("undo")); +} + +/** + * Re-does the last action. + */ +export function redo() { + return Context.wrap(vscode.commands.executeCommand("redo")); +} diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..baa3802 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,71 @@ +import * as vscode from "vscode"; + +export * from "./clipboard"; +export * from "./context"; +export * from "./edit"; +export * from "./edit/linewise"; +export * from "./errors"; +export * from "./functional"; +export * from "./history"; +export * from "./keybindings"; +export * from "./lines"; +export * from "./menu"; +export * from "./modes"; +export * from "./positions"; +export * from "./prompt"; +export * from "./registers"; +export * from "./run"; +export * from "./search"; +export * from "./search/lines"; +export * from "./search/move"; +export * from "./search/move-to"; +export * from "./search/pairs"; +export * from "./selections"; + +/** + * Direction of an operation. + */ +export const enum Direction { + /** + * Forward direction (`1`). + */ + Forward = 1, + + /** + * Backward direction (`-1`). + */ + Backward = -1, +} + +/** + * Behavior of a shift. + */ +export const enum Shift { + /** + * Jump to the position. + */ + Jump, + + /** + * Select to the position. + */ + Select, + + /** + * Extend to the position. + */ + Extend, +} + +export const Forward = Direction.Forward, + Backward = Direction.Backward, + Jump = Shift.Jump, + Select = Shift.Select, + Extend = Shift.Extend; + +/** + * Returns the module exported by the extension with the given identifier. + */ +export function extension(extensionId: string) { + return vscode.extensions.getExtension(extensionId)?.exports; +} diff --git a/src/api/keybindings/built-in.build.ts b/src/api/keybindings/built-in.build.ts new file mode 100644 index 0000000..26d84f0 --- /dev/null +++ b/src/api/keybindings/built-in.build.ts @@ -0,0 +1,11 @@ +import { Builder } from "../../../meta"; + +export async function build(builder: Builder) { + const modules = await builder.getCommandModules(), + keybindings = modules.flatMap((module) => module.keybindings), + keybindingsCode = JSON.stringify(keybindings, undefined, 2) + .replace(/"(\w+)":/g, "$1:") + .replace(/([0-9a-z}"])$/gm, "$1,"); + + return `\nconst builtinKeybindings = ${keybindingsCode};\n`; +} diff --git a/src/api/keybindings/built-in.ts b/src/api/keybindings/built-in.ts new file mode 100644 index 0000000..e26937c --- /dev/null +++ b/src/api/keybindings/built-in.ts @@ -0,0 +1,1117 @@ +// +// Content below this line was auto-generated by built-in.build.ts. Do not edit manually. + +const builtinKeybindings = [ + { + key: "Shift+7", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Align selections", + command: "dance.edit.align", + }, + { + key: "Alt+`", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Swap case", + command: "dance.edit.case.swap", + }, + { + key: "`", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Transform to lower case", + command: "dance.edit.case.toLower", + }, + { + key: "Shift+`", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Transform to upper case", + command: "dance.edit.case.toUpper", + }, + { + key: "Shift+Alt+7", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Copy indentation", + command: "dance.edit.copyIndentation", + }, + { + key: "Shift+Alt+,", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Deindent selected lines", + command: "dance.edit.deindent", + }, + { + key: "Shift+,", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Deindent selected lines (including incomplete indent)", + command: "dance.edit.deindent.withIncomplete", + }, + { + key: "Alt+D", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Delete", + command: "dance.edit.delete", + }, + { + key: "Alt+C", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Delete and switch to Insert", + command: "dance.edit.delete-insert", + }, + { + key: "Shift+.", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Indent selected lines", + command: "dance.edit.indent", + }, + { + key: "Shift+Alt+.", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Indent selected lines (including empty lines)", + command: "dance.edit.indent.withEmpty", + }, + { + key: "Shift+Alt+R", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Insert contents of register", + command: "dance.edit.insert", + }, + { + key: "Alt+J", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Join lines", + command: "dance.edit.join", + }, + { + key: "Shift+Alt+J", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Join lines and select inserted separators", + command: "dance.edit.join.select", + }, + { + key: "Shift+Alt+O", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Insert new line above each selection", + command: "dance.edit.newLine.above", + }, + { + key: "Shift+O", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Insert new line above and switch to insert", + command: "dance.edit.newLine.above.insert", + }, + { + key: "Alt+O", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Insert new line below each selection", + command: "dance.edit.newLine.below", + }, + { + key: "O", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Insert new line below and switch to insert", + command: "dance.edit.newLine.below.insert", + }, + { + key: "P", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Paste after", + command: "dance.edit.paste.after", + }, + { + key: "Alt+P", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Paste after and select", + command: "dance.edit.paste.after.select", + }, + { + key: "Shift+P", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Paste before", + command: "dance.edit.paste.before", + }, + { + key: "Shift+Alt+P", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Paste before and select", + command: "dance.edit.paste.before.select", + }, + { + key: "R", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Replace characters", + command: "dance.edit.replaceCharacters", + }, + { + key: "Ctrl+R", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Pick register and replace", + command: "dance.edit.selectRegister-insert", + }, + { + key: "Ctrl+R", + when: "editorTextFocus && dance.mode == 'insert'", + title: "Pick register and replace", + command: "dance.edit.selectRegister-insert", + }, + { + key: "D", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Copy and delete", + command: "dance.edit.yank-delete", + }, + { + key: "C", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Copy, delete and switch to Insert", + command: "dance.edit.yank-delete-insert", + }, + { + key: "Shift+R", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Copy and replace", + command: "dance.edit.yank-replace", + }, + { + key: "Q", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Replay recording", + command: "dance.history.recording.play", + }, + { + key: "Shift+Q", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Start recording", + command: "dance.history.recording.start", + }, + { + key: "Escape", + when: "editorTextFocus && dance.mode == 'normal' && dance.isRecording", + title: "Stop recording", + command: "dance.history.recording.stop", + }, + { + key: "Shift+U", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Redo", + command: "dance.history.redo", + }, + { + key: "Shift+Alt+U", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Redo a change of selections", + command: "dance.history.redo.selections", + }, + { + key: ".", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Repeat last edit without a command", + command: "dance.history.repeat.edit", + }, + { + key: "Alt+.", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Repeat last seek", + command: "dance.history.repeat.seek", + }, + { + key: "U", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Undo", + command: "dance.history.undo", + }, + { + key: "Alt+U", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Undo a change of selections", + command: "dance.history.undo.selections", + }, + { + key: "Escape", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Cancel Dance operation", + command: "dance.cancel", + }, + { + key: "Escape", + when: "editorTextFocus && dance.mode == 'input'", + title: "Cancel Dance operation", + command: "dance.cancel", + }, + { + key: "Shift+'", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select register for next command", + command: "dance.selectRegister", + }, + { + key: "0", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 0 to the counter", + command: "dance.updateCount", + args: { + addDigits: 0, + }, + }, + { + key: "1", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 1 to the counter", + command: "dance.updateCount", + args: { + addDigits: 1, + }, + }, + { + key: "2", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 2 to the counter", + command: "dance.updateCount", + args: { + addDigits: 2, + }, + }, + { + key: "3", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 3 to the counter", + command: "dance.updateCount", + args: { + addDigits: 3, + }, + }, + { + key: "4", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 4 to the counter", + command: "dance.updateCount", + args: { + addDigits: 4, + }, + }, + { + key: "5", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 5 to the counter", + command: "dance.updateCount", + args: { + addDigits: 5, + }, + }, + { + key: "6", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 6 to the counter", + command: "dance.updateCount", + args: { + addDigits: 6, + }, + }, + { + key: "7", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 7 to the counter", + command: "dance.updateCount", + args: { + addDigits: 7, + }, + }, + { + key: "8", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 8 to the counter", + command: "dance.updateCount", + args: { + addDigits: 8, + }, + }, + { + key: "9", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add the digit 9 to the counter", + command: "dance.updateCount", + args: { + addDigits: 9, + }, + }, + { + key: "Shift+;", + when: "editorTextFocus && dance.mode == 'normal'", + command: "workbench.action.showCommands", + }, + { + key: "A", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Insert after", + command: "dance.modes.insert.after", + }, + { + key: "I", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Insert before", + command: "dance.modes.insert.before", + }, + { + key: "Shift+A", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Insert at line end", + command: "dance.modes.insert.lineEnd", + }, + { + key: "Shift+I", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Insert at line start", + command: "dance.modes.insert.lineStart", + }, + { + key: "Escape", + when: "editorTextFocus && dance.mode == 'insert'", + title: "Set mode to Normal", + command: "dance.modes.set.normal", + }, + { + key: "Ctrl+V", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Temporart Insert mode", + command: "dance.modes.set.temporarily.insert", + }, + { + key: "Ctrl+V", + when: "editorTextFocus && dance.mode == 'insert'", + title: "Temporary Normal mode", + command: "dance.modes.set.temporarily.normal", + }, + { + key: "/", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Search", + command: "dance.search", + }, + { + key: "Alt+/", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Search backward", + command: "dance.search.backward", + }, + { + key: "Shift+Alt+/", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Search backward (extend)", + command: "dance.search.backward.extend", + }, + { + key: "Shift+/", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Search (extend)", + command: "dance.search.extend", + }, + { + key: "N", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select next match", + command: "dance.search.next", + }, + { + key: "Shift+N", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add next match", + command: "dance.search.next.add", + }, + { + key: "Alt+N", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select previous match", + command: "dance.search.previous", + }, + { + key: "Shift+Alt+N", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Add previous match", + command: "dance.search.previous.add", + }, + { + key: "Shift+Alt+8", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Search current selection", + command: "dance.search.selection", + }, + { + key: "Shift+8", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Search current selection (smart)", + command: "dance.search.selection.smart", + }, + { + key: "T", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to character (excluded)", + command: "dance.seek", + }, + { + key: "Alt+A", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select whole object", + command: "dance.seek.askObject", + }, + { + key: "Alt+A", + when: "editorTextFocus && dance.mode == 'insert'", + title: "Select whole object", + command: "dance.seek.askObject", + }, + { + key: "]", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to whole object end", + command: "dance.seek.askObject.end", + }, + { + key: "Shift+]", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to whole object end", + command: "dance.seek.askObject.end", + }, + { + key: "Alt+I", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select inner object", + command: "dance.seek.askObject.inner", + }, + { + key: "Alt+I", + when: "editorTextFocus && dance.mode == 'insert'", + title: "Select inner object", + command: "dance.seek.askObject.inner", + }, + { + key: "Alt+]", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to inner object end", + command: "dance.seek.askObject.inner.end", + }, + { + key: "Shift+Alt+]", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to inner object end", + command: "dance.seek.askObject.inner.end.extend", + }, + { + key: "Alt+[", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to inner object start", + command: "dance.seek.askObject.inner.start", + }, + { + key: "Shift+Alt+[", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to inner object start", + command: "dance.seek.askObject.inner.start.extend", + }, + { + key: "[", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to whole object start", + command: "dance.seek.askObject.start", + }, + { + key: "Shift+[", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to whole object start", + command: "dance.seek.askObject.start", + }, + { + key: "Alt+T", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to character (excluded, backward)", + command: "dance.seek.backward", + }, + { + key: "M", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to next enclosing character", + command: "dance.seek.enclosing", + }, + { + key: "Alt+M", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to previous enclosing character", + command: "dance.seek.enclosing.backward", + }, + { + key: "Shift+M", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to next enclosing character", + command: "dance.seek.enclosing.extend", + }, + { + key: "Shift+Alt+M", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to previous enclosing character", + command: "dance.seek.enclosing.extend.backward", + }, + { + key: "Shift+T", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to character (excluded)", + command: "dance.seek.extend", + }, + { + key: "Shift+Alt+T", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to character (excluded, backward)", + command: "dance.seek.extend.backward", + }, + { + key: "F", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to character (included)", + command: "dance.seek.included", + }, + { + key: "Alt+F", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to character (included, backward)", + command: "dance.seek.included.backward", + }, + { + key: "Shift+F", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to character (included)", + command: "dance.seek.included.extend", + }, + { + key: "Shift+Alt+F", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to character (included, backward)", + command: "dance.seek.included.extend.backward", + }, + { + key: "W", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to next word start", + command: "dance.seek.word", + }, + { + key: "B", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to previous word start", + command: "dance.seek.word.backward", + }, + { + key: "Shift+W", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to next word start", + command: "dance.seek.word.extend", + }, + { + key: "Shift+B", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to previous word start", + command: "dance.seek.word.extend.backward", + }, + { + key: "Alt+W", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to next non-whitespace word start", + command: "dance.seek.word.ws", + }, + { + key: "Alt+B", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to previous non-whitespace word start", + command: "dance.seek.word.ws.backward", + }, + { + key: "Shift+Alt+W", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to next non-whitespace word start", + command: "dance.seek.word.ws.extend", + }, + { + key: "Shift+Alt+B", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to previous non-whitespace word start", + command: "dance.seek.word.ws.extend.backward", + }, + { + key: "E", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to next word end", + command: "dance.seek.wordEnd", + }, + { + key: "Shift+E", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to next word end", + command: "dance.seek.wordEnd.extend", + }, + { + key: "Alt+E", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to next non-whitespace word end", + command: "dance.seek.wordEnd.ws", + }, + { + key: "Shift+Alt+E", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to next non-whitespace word end", + command: "dance.seek.wordEnd.ws.extend", + }, + { + key: "Shift+5", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select whole buffer", + command: "dance.select.buffer", + }, + { + key: "Shift+J", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend down", + command: "dance.select.down.extend", + }, + { + key: "Shift+Down", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend down", + command: "dance.select.down.extend", + }, + { + key: "J", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Jump down", + command: "dance.select.down.jump", + }, + { + key: "Down", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Jump down", + command: "dance.select.down.jump", + }, + { + key: "Shift+H", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend left", + command: "dance.select.left.extend", + }, + { + key: "Shift+Left", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend left", + command: "dance.select.left.extend", + }, + { + key: "H", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Jump left", + command: "dance.select.left.jump", + }, + { + key: "Left", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Jump left", + command: "dance.select.left.jump", + }, + { + key: "X", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select line below", + command: "dance.select.line.below", + }, + { + key: "Shift+X", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to line below", + command: "dance.select.line.below.extend", + }, + { + key: "Alt+L", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to line end", + command: "dance.select.lineEnd", + }, + { + key: "End", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to line end", + command: "dance.select.lineEnd", + }, + { + key: "Shift+Alt+L", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to line end", + command: "dance.select.lineEnd.extend", + }, + { + key: "Shift+End", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to line end", + command: "dance.select.lineEnd.extend", + }, + { + key: "Alt+H", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to line start", + command: "dance.select.lineStart", + }, + { + key: "Home", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select to line start", + command: "dance.select.lineStart", + }, + { + key: "Shift+Alt+H", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to line start", + command: "dance.select.lineStart.extend", + }, + { + key: "Shift+Home", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to line start", + command: "dance.select.lineStart.extend", + }, + { + key: "Shift+L", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend right", + command: "dance.select.right.extend", + }, + { + key: "Shift+Right", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend right", + command: "dance.select.right.extend", + }, + { + key: "L", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Jump right", + command: "dance.select.right.jump", + }, + { + key: "Right", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Jump right", + command: "dance.select.right.jump", + }, + { + key: "Shift+G", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend to", + command: "dance.select.to.extend", + }, + { + key: "G", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Go to", + command: "dance.select.to.jump", + }, + { + key: "Shift+K", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend up", + command: "dance.select.up.extend", + }, + { + key: "Shift+Up", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Extend up", + command: "dance.select.up.extend", + }, + { + key: "K", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Jump up", + command: "dance.select.up.jump", + }, + { + key: "Up", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Jump up", + command: "dance.select.up.jump", + }, + { + key: "Ctrl+F", + when: "editorTextFocus && dance.mode == 'normal'", + command: "dance.select.vertically", + args: { + direction: 1, + by: "page", + }, + }, + { + key: "Ctrl+F", + when: "editorTextFocus && dance.mode == 'insert'", + command: "dance.select.vertically", + args: { + direction: 1, + by: "page", + }, + }, + { + key: "Ctrl+D", + when: "editorTextFocus && dance.mode == 'normal'", + command: "dance.select.vertically", + args: { + direction: 1, + by: "halfPage", + }, + }, + { + key: "Ctrl+D", + when: "editorTextFocus && dance.mode == 'insert'", + command: "dance.select.vertically", + args: { + direction: 1, + by: "halfPage", + }, + }, + { + key: "Ctrl+B", + when: "editorTextFocus && dance.mode == 'normal'", + command: "dance.select.vertically", + args: { + direction: -1, + by: "page", + }, + }, + { + key: "Ctrl+B", + when: "editorTextFocus && dance.mode == 'insert'", + command: "dance.select.vertically", + args: { + direction: -1, + by: "page", + }, + }, + { + key: "Ctrl+U", + when: "editorTextFocus && dance.mode == 'normal'", + command: "dance.select.vertically", + args: { + direction: -1, + by: "halfPage", + }, + }, + { + key: "Ctrl+U", + when: "editorTextFocus && dance.mode == 'insert'", + command: "dance.select.vertically", + args: { + direction: -1, + by: "halfPage", + }, + }, + { + key: "Alt+;", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Change direction of selections", + command: "dance.selections.changeDirection", + }, + { + key: "Alt+Space", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Clear main selections", + command: "dance.selections.clear.main", + }, + { + key: "Space", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Clear secondary selections", + command: "dance.selections.clear.secondary", + }, + { + key: "Shift+C", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Copy selections below", + command: "dance.selections.copy", + }, + { + key: "Shift+Alt+C", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Copy selections above", + command: "dance.selections.copy.above", + }, + { + key: "Alt+X", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Expand to lines", + command: "dance.selections.expandToLines", + }, + { + key: "Shift+Alt+;", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Forward selections", + command: "dance.selections.faceForward", + }, + { + key: "Shift+4", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Filter selections", + command: "dance.selections.filter", + }, + { + key: "Alt+K", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Keep matching selections", + command: "dance.selections.filter.regexp", + }, + { + key: "Shift+Alt+K", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Clear matching selections", + command: "dance.selections.filter.regexp.inverse", + }, + { + key: "Shift+Alt+-", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Merge contiguous selections", + command: "dance.selections.merge", + }, + { + key: "Shift+Alt+\\", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Pipe selections", + command: "dance.selections.pipe", + }, + { + key: "Shift+1", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Pipe and append", + command: "dance.selections.pipe.append", + }, + { + key: "Shift+Alt+1", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Pipe and prepend", + command: "dance.selections.pipe.prepend", + }, + { + key: "Shift+\\", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Pipe and replace", + command: "dance.selections.pipe.replace", + }, + { + key: ";", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Reduce selections to their cursor", + command: "dance.selections.reduce", + }, + { + key: "Shift+Alt+S", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Reduce selections to their ends", + command: "dance.selections.reduce.edges", + }, + { + key: "Z", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Restore selections", + command: "dance.selections.restore", + }, + { + key: "Alt+Z", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Combine register selections with current ones", + command: "dance.selections.restore.withCurrent", + }, + { + key: "Shift+Alt+Z", + when: "editorTextFocus && dance.mode == 'normal'", + command: "dance.selections.restore.withCurrent", + args: { + reverse: true, + }, + }, + { + key: "Shift+Z", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Save selections", + command: "dance.selections.save", + }, + { + key: "Y", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Copy selections text", + command: "dance.selections.saveText", + }, + { + key: "S", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Select within selections", + command: "dance.selections.select", + }, + { + key: "Shift+S", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Split selections", + command: "dance.selections.split", + }, + { + key: "Alt+S", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Split selections at line boundaries", + command: "dance.selections.splitLines", + }, + { + key: "Enter", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Toggle selection indices", + command: "dance.selections.toggleIndices", + }, + { + key: "Shift+Alt+X", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Trim lines", + command: "dance.selections.trimLines", + }, + { + key: "Shift+-", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Trim whitespace", + command: "dance.selections.trimWhitespace", + }, + { + key: "Shift+9", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Rotate selections clockwise", + command: "dance.selections.rotate.both", + }, + { + key: "Shift+0", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Rotate selections counter-clockwise", + command: "dance.selections.rotate.both.reverse", + }, + { + key: "Shift+Alt+9", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Rotate selections clockwise (selections only)", + command: "dance.selections.rotate.selections", + }, + { + key: "Shift+Alt+0", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Rotate selections counter-clockwise (selections only)", + command: "dance.selections.rotate.selections.reverse", + }, + { + key: "V", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Show view menu", + command: "dance.openMenu", + args: { + input: "view", + }, + }, + { + key: "Shift+V", + when: "editorTextFocus && dance.mode == 'normal'", + title: "Show view menu (locked)", + command: "dance.openMenu", + args: { + input: "view", + locked: true, + }, + }, +]; diff --git a/src/api/keybindings/index.ts b/src/api/keybindings/index.ts new file mode 100644 index 0000000..7bf50ef --- /dev/null +++ b/src/api/keybindings/index.ts @@ -0,0 +1,7 @@ +// TODO: API for generating JSON for keybindings +/** + * API for generating VS Code-compatible keybindings. + */ +export namespace Keybindings { + +} diff --git a/layouts/azerty.json b/src/api/keybindings/layout-azerty.ts similarity index 56% rename from layouts/azerty.json rename to src/api/keybindings/layout-azerty.ts index 53741ea..feaa272 100644 --- a/layouts/azerty.json +++ b/src/api/keybindings/layout-azerty.ts @@ -4,471 +4,471 @@ // Therefore, some commands were moved around to make them available // in a 'natural' way. -[ +export const overrides = [ { "key": "ctrl+g", - "command": "workbench.action.gotoLine" + "command": "workbench.action.gotoLine", }, { "key": "ctrl+g", - "command": "-workbench.action.gotoLine" + "command": "-workbench.action.gotoLine", }, { "key": "1", "command": "dance.selections.align", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+0", "command": "dance.count.0", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "0", "command": "-dance.count.0", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+1", "command": "dance.count.1", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "1", "command": "-dance.count.1", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+2", "command": "dance.count.2", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "2", "command": "-dance.count.2", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+3", "command": "dance.count.3", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "3", "command": "-dance.count.3", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+4", "command": "dance.count.4", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "4", "command": "-dance.count.4", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+5", "command": "dance.count.5", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "5", "command": "-dance.count.5", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+6", "command": "dance.count.6", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "6", "command": "-dance.count.6", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+7", "command": "dance.count.7", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "7", "command": "-dance.count.7", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+8", "command": "dance.count.8", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "8", "command": "-dance.count.8", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+9", "command": "dance.count.9", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "9", "command": "-dance.count.9", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_102", "command": "dance.deindent", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+oem_102", "command": "dance.indent", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.selections.align", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.deindent", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.indent", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_1", "command": "dance.pipe.filter", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+4", "command": "-dance.pipe.filter", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_8", "command": "dance.pipe.append", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+1", "command": "-dance.pipe.append", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+oem_8", "command": "dance.pipe.prepend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+1", "command": "-dance.pipe.prepend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+oem_8", "command": "dance.pipe.ignore", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+oem_5", "command": "-dance.pipe.ignore", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+oem_8", "command": "dance.pipe.replace", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+oem_5", "command": "-dance.pipe.replace", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+1", "command": "dance.selections.align.copy", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+unknown", "command": "-dance.selections.align.copy", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+oem_102", "command": "dance.indent.withEmpty", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+unknown", "command": "-dance.indent.withEmpty", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+oem_102", "command": "dance.deindent.further", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+unknown", "command": "-dance.deindent.further", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+oem_5", "command": "dance.objects.selectToEnd.extend.inner", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+unknown", "command": "-dance.objects.selectToEnd.extend.inner", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+oem_3", "command": "dance.objects.selectToStart.extend.inner", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+unknown", "command": "-dance.objects.selectToStart.extend.inner", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+oem_5", "command": "dance.objects.selectToEnd.extend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+unknown", "command": "-dance.objects.selectToEnd.extend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+oem_3", "command": "dance.objects.selectToStart.extend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+unknown", "command": "-dance.objects.selectToStart.extend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+oem_period", "command": "dance.selections.flip", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+oem_1", "command": "-dance.selections.flip", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+oem_period", "command": "dance.selections.forward", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+oem_1", "command": "-dance.selections.forward", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+8", "command": "dance.selections.merge", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+unknown", "command": "-dance.selections.merge", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_period", "command": "dance.selections.reduce", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_1", "command": "-dance.selections.reduce", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+oem_period", "command": "dance.repeat.insert", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_period", "command": "-dance.repeat.insert", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_4", "command": "dance.rotate", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.rotate", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "5", "command": "dance.rotate.backwards", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.rotate.backwards", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+oem_4", "command": "dance.rotate.content", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+unknown", "command": "-dance.rotate.content", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+5", "command": "dance.rotate.content.backwards", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+unknown", "command": "-dance.rotate.content.backwards", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_2", "command": "dance.search", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.search", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+oem_2", "command": "dance.search.extend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+unknown", "command": "-dance.search.extend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+oem_2", "command": "dance.search.backwards", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+unknown", "command": "-dance.search.backwards", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+oem_2", "command": "dance.search.backwards.extend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "shift+alt+unknown", "command": "-dance.search.backwards.extend", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "3", "command": "dance.registers.select", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.registers.select", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+oem_5", "command": "dance.objects.selectToEnd.inner", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+unknown", "command": "-dance.objects.selectToEnd.inner", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.toUpperCase", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_3", "command": "-dance.toLowerCase", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_3", "command": "dance.objects.selectToStart", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.objects.selectToStart", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_5", "command": "dance.objects.selectToEnd", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "unknown", "command": "-dance.objects.selectToEnd", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+oem_3", "command": "dance.objects.selectToStart.inner", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+unknown", "command": "-dance.objects.selectToStart.inner", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "oem_7", - "command": "dance.toLowerCase" + "command": "dance.toLowerCase", }, { "key": "shift+oem_7", - "command": "dance.toUpperCase" + "command": "dance.toUpperCase", }, { "key": "alt+oem_7", "command": "dance.swapCase", - "when": "editorTextFocus && dance.mode == 'normal'" + "when": "editorTextFocus && dance.mode == 'normal'", }, { "key": "alt+oem_3", "command": "-dance.swapCase", - "when": "editorTextFocus && dance.mode == 'normal'" - } -] + "when": "editorTextFocus && dance.mode == 'normal'", + }, +]; diff --git a/src/api/lines.ts b/src/api/lines.ts new file mode 100644 index 0000000..f407f6b --- /dev/null +++ b/src/api/lines.ts @@ -0,0 +1,180 @@ +import * as vscode from "vscode"; + +import { Context } from "./context"; +import { assert } from "./errors"; + +/** + * Returns the 0-based number of the first visible line in the current editor. + */ +export function firstVisibleLine(editor = Context.current.editor) { + return editor.visibleRanges[0].start.line; +} + +/** + * Returns the 0-based number of the middle visible line in the current editor. + */ +export function middleVisibleLine(editor = Context.current.editor) { + const range = editor.visibleRanges[0]; + + return ((range.start.line + range.end.line) / 2) | 0; +} + +/** + * Returns the 0-based number of the last visible line in the current editor. + */ +export function lastVisibleLine(editor = Context.current.editor) { + return editor.visibleRanges[0].end.line; +} + +export namespace Lines { + /** + * Returns the text contents of the given line. + */ + export function text(line: number, document = Context.current.document) { + return document.lineAt(line).text; + } + + /** + * Returns the length of the given line. + */ + export function length(line: number, document = Context.current.document) { + return document.lineAt(line).text.length; + } + + /** + * Returns whether the given line is empty. + */ + export function isEmpty(line: number, document = Context.current.document) { + return length(line, document) === 0; + } + + /** + * Returns the given line number, possibly modified to fit in the current + * document. + */ + export function clamp(line: number, document?: vscode.TextDocument) { + if (line < 0) { + return 0; + } + + const lastLine = (document ?? Context.current.document).lineCount - 1; + + if (line > lastLine) { + return lastLine; + } + + return line; + } +} + +function diffAddedByTabs(text: string, editor: Pick) { + const tabSize = editor.options.tabSize as number; + let total = 0; + + for (const ch of text) { + if (ch === "\t") { + total += tabSize - 1; + } + } + + return total; +} + +/** + * Returns the position corresponding to the character at the given position, + * taking into account tab characters that precede it. + */ +export function column( + position: vscode.Position, + editor?: Pick, +): vscode.Position; + +/** + * Returns the render column corresponding to the specified character in the + * specified line, taking into account tab characters that precede it. + */ +export function column( + line: number, + character: number, + editor?: Pick, +): number; + +export function column( + line: number | vscode.Position, + character?: number | Pick, + editor?: Pick, +) { + if (typeof line === "number") { + editor ??= Context.current.editor; + + const text = editor.document.lineAt(line).text.slice(0, character as number); + + return text.length + diffAddedByTabs(text, editor); + } + + editor ??= Context.current.editor; + + const text = editor.document.lineAt(line.line).text.slice(0, line.character); + + return new vscode.Position(line.line, text.length + diffAddedByTabs(text, editor)); +} + +export namespace column { + /** + * Returns the `vscode.Position`-compatible position for the given position. + * Reverses the diff added by `column`. + */ + export function character( + position: vscode.Position, + editor?: Pick, + ): vscode.Position; + + /** + * Returns the `vscode.Position`-compatible character for the given column. + * Reverses the diff added by `column`. + */ + export function character( + line: number, + character: number, + editor?: Pick, + ): number; + + export function character( + line: number | vscode.Position, + character?: number | Pick, + editor?: Pick, + ) { + if (typeof line === "number") { + editor ??= Context.current.editor; + + const text = editor.document.lineAt(line).text.slice(0, character as number); + + return text.length - diffAddedByTabs(text, editor); + } + + editor ??= Context.current.editor; + + const text = editor.document.lineAt(line.line).text.slice(0, line.character); + + return new vscode.Position(line.line, text.length - diffAddedByTabs(text, editor)); + } +} + +/** + * Same as `Lines.length`, but also increases the count according to tab + * characters so that the result matches the rendered view. + * + * @see Lines.length + */ +export function columns( + line: number | vscode.Position, + editor: Pick = Context.current.editor, +): number { + if (typeof line !== "number") { + line = line.line; + } + + const text = editor.document.lineAt(line).text; + + return text.length + diffAddedByTabs(text, editor); +} diff --git a/src/api/menu.ts b/src/api/menu.ts new file mode 100644 index 0000000..41a182c --- /dev/null +++ b/src/api/menu.ts @@ -0,0 +1,173 @@ +import * as vscode from "vscode"; + +import { Context, prompt } from "."; + +export interface Menu { + readonly items: Menu.Items; +} + +export namespace Menu { + export interface Items { + [keys: string]: Item; + } + + export interface Item { + readonly text: string; + readonly command: string; + readonly args?: any[]; + } +} + +/** + * Validates the given menu and returns a list of strings representing errors + * with the given menu. If that list is empty, the menu is valid and can be + * used. + */ +export function validateMenu(menu: Menu) { + if (typeof menu !== "object" || menu === null) { + return ["menu must be an object"]; + } + + if (typeof menu.items !== "object" || Object.keys(menu.items ?? {}).length === 0) { + return ['menu must have an subobject "items" with at least two entries.']; + } + + const seenKeyCodes = new Map(), + errors = [] as string[]; + + for (const key in menu.items) { + const item = menu.items[key], + itemDisplay = JSON.stringify(key); + + if (typeof item !== "object" || item === null) { + errors.push(`item ${itemDisplay} must be an object.`); + continue; + } + + if (typeof item.text !== "string" || item.text.length === 0) { + errors.push(`item ${itemDisplay} must have a non-empty "text" property.`); + continue; + } + + if (typeof item.command !== "string" || item.command.length === 0) { + errors.push(`item ${itemDisplay} must have a non-empty "command" property.`); + continue; + } + + if (key.length === 0) { + errors.push(`item ${itemDisplay} must be a non-empty string key.`); + continue; + } + + for (let i = 0; i < key.length; i++) { + const keyCode = key.charCodeAt(i), + prevKey = seenKeyCodes.get(keyCode); + + if (prevKey) { + errors.push(`menu has duplicate key '${key[i]}' (specified by '${prevKey}' and '${key}').`); + continue; + } + + seenKeyCodes.set(keyCode, key); + } + } + + return errors; +} + +/** + * Shows the given menu to the user, awaiting a choice. + */ +export async function showMenu( + menu: Menu, + additionalArgs: readonly any[] = [], + prefix?: string, +) { + const entries = Object.entries(menu.items); + const items = entries.map((x) => [x[0], x[1].text] as const); + const choice = await prompt.one(items); + + if (typeof choice === "string") { + if (prefix !== undefined) { + await vscode.commands.executeCommand("default:type", { text: prefix + choice }); + } + + return; + } + + const pickedItem = entries[choice][1], + args = mergeArgs(pickedItem.args, additionalArgs); + + return Context.WithoutActiveEditor.wrap( + vscode.commands.executeCommand(pickedItem.command, ...args), + ); +} + +export namespace showMenu { + /** + * Shows the menu with the given name. + */ + export function byName( + menuName: string, + additionalArgs: readonly any[] = [], + prefix?: string, + ) { + const menu = Context.WithoutActiveEditor.current.extension.menus.get(menuName); + + if (menu === undefined) { + return Promise.reject(new Error(`menu ${JSON.stringify(menuName)} does not exist`)); + } + + return showMenu(menu, additionalArgs, prefix); + } +} + +/** + * Shows the given menu to the user, not dismissing it when a key is pressed. + */ +export async function showLockedMenu( + menu: Menu, + additionalArgs: readonly any[] = [], +) { + const entries = Object.entries(menu.items), + items = entries.map(([keys, item]) => + [keys, item.text, () => + vscode.commands.executeCommand( + item.command, ...mergeArgs(item.args, additionalArgs))] as const); + + await prompt.one.locked(items); +} + +export namespace showLockedMenu { + /** + * Shows the menu with the given name. + */ + export function byName( + menuName: string, + additionalArgs: readonly any[] = [], + ) { + const menu = Context.WithoutActiveEditor.current.extension.menus.get(menuName); + + if (menu === undefined) { + return Promise.reject(new Error(`menu ${JSON.stringify(menuName)} does not exist`)); + } + + return showLockedMenu(menu, additionalArgs); + } +} + +function mergeArgs(args: readonly any[] | undefined, additionalArgs: readonly any[]) { + if (args == null) { + return additionalArgs; + } else if (additionalArgs.length > 0) { + return args.length > additionalArgs.length + ? args.map((arg, i) => + i < additionalArgs.length && additionalArgs[i] + ? Object.assign({}, additionalArgs[i], arg) + : arg) + : additionalArgs.map((arg, i) => + i < args.length ? Object.assign({}, arg, args[i]) : arg); + } else { + return args; + } +} diff --git a/src/api/modes.ts b/src/api/modes.ts new file mode 100644 index 0000000..9eece3d --- /dev/null +++ b/src/api/modes.ts @@ -0,0 +1,38 @@ +import { Context } from "./context"; + +/** + * Switches to the mode with the given name. + */ +export function toMode(modeName: string): Thenable; + +/** + * Temporarily switches to the mode with the given name. + */ +export function toMode(modeName: string, count: number): Thenable; + +export function toMode(modeName: string, count?: number) { + const context = Context.current, + mode = context.extension.modes.get(modeName); + + if (mode === undefined || mode.isPendingDeletion) { + throw new Error(`mode ${JSON.stringify(modeName)} does not exist`); + } + + if (!count) { + return context.switchToMode(mode); + } + + const editorState = context.getState(); + + const disposable = context.extension + .createAutoDisposable() + .addDisposable(context.extension.editors.onModeDidChange((editorState) => { + if (editorState.editor === context.editor && editorState.mode !== mode) { + disposable.dispose(); + } + })) + .disposeOnEvent(editorState.onVisibilityDidChange); + + // TODO: watch document changes and command executions + return context.switchToMode(mode); +} diff --git a/src/api/positions.ts b/src/api/positions.ts new file mode 100644 index 0000000..e71c69f --- /dev/null +++ b/src/api/positions.ts @@ -0,0 +1,167 @@ +import * as vscode from "vscode"; + +import { Direction } from "."; +import { Context } from "./context"; + +/** + * Returns the position right after the given position, or `undefined` if + * `position` is the last position of the document. + */ +export function nextPosition(position: vscode.Position, document?: vscode.TextDocument) { + document ??= Context.current.document; + + const line = position.line, + character = position.character, + textLineLen = document.lineAt(line).text.length; + + if (character < textLineLen) { + return new vscode.Position(line, character + 1); + } + + if (line === document.lineCount - 1) { + return undefined; + } + + return new vscode.Position(line + 1, 0); +} + +/** + * Returns the position right before the given position, or `undefined` if + * `position` is the first position of the document. + */ +export function previousPosition(position: vscode.Position, document?: vscode.TextDocument) { + const line = position.line, + character = position.character; + + if (character > 0) { + return new vscode.Position(line, character - 1); + } + + if (line === 0) { + return undefined; + } + + return new vscode.Position( + line - 1, + (document ?? Context.current.document).lineAt(line - 1).text.length, + ); +} + +/** + * Returns the position at a given (possibly negative) offset from the given + * position, or `undefined` if such a position would go out of the bounds of the + * document. + */ +export function offsetPosition( + position: vscode.Position, + by: number, + document?: vscode.TextDocument, +) { + if (by === 0) { + return position; + } + + if (by === 1) { + return nextPosition(position, document); + } + + if (by === -1) { + return previousPosition(position, document); + } + + document ??= Context.current.document; + + const offset = document.offsetAt(position) + by; + + if (offset === -1) { + return undefined; + } + + return document.positionAt(document.offsetAt(position) + by); +} + +export namespace offsetPosition { + /** + * Same as `offsetPosition`, but clamps to document edges. + */ + export function orEdge( + position: vscode.Position, + by: number, + document?: vscode.TextDocument, + ) { + const result = offsetPosition(position, by, document); + + if (result === undefined) { + return by < 0 ? Positions.zero : Positions.last(document); + } + + return result; + } +} + +/** + * Operations on `vscode.Position`s. + */ +export namespace Positions { + export const next = nextPosition, + previous = previousPosition, + offset = offsetPosition; + + /** + * The (0, 0) position. + */ + export const zero = new vscode.Position(0, 0); + + /** + * Returns the last position of the given document. + */ + export function last(document = Context.current.document) { + return document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end; + } + + /** + * Returns the position at the given line and character. + */ + export function at(line: number, character: number) { + return new vscode.Position(line, character); + } + + /** + * Returns the position at the start of the given line. + */ + export function lineStart(line: number) { + return new vscode.Position(line, 0); + } + + /** + * Returns the position at the first non-blank character of the given line, or + * the end of the line if the line is fully blank. + */ + export function nonBlankLineStart(line: number, document = Context.current.document) { + return new vscode.Position(line, document.lineAt(line).firstNonWhitespaceCharacterIndex); + } + + /** + * Returns the position at the end of the given line. + */ + export function lineEnd(line: number, document = Context.current.document) { + return new vscode.Position(line, document.lineAt(line).text.length); + } + + /** + * Returns the position after the end of the given line, i.e. the first + * position of the next line. + */ + export function lineBreak(line: number, document = Context.current.document) { + return line + 1 === document.lineCount ? lineEnd(line, document) : lineStart(line + 1); + } + + /** + * Returns the last position of the current document when going in the given + * direction. If `Backward`, this is `Positions.zero`. If `Forward`, this is + * `Positions.last(document)`. + */ + export function edge(direction: Direction, document?: vscode.TextDocument) { + return direction === Direction.Backward ? Positions.zero : last(document); + } +} diff --git a/src/api/prompt.ts b/src/api/prompt.ts new file mode 100644 index 0000000..6ee6c9d --- /dev/null +++ b/src/api/prompt.ts @@ -0,0 +1,426 @@ +import * as vscode from "vscode"; +import { Context } from "../api"; +import { CancellationError } from "./errors"; + +/** + * Displays a prompt to the user. + */ +export function prompt( + opts: vscode.InputBoxOptions, + context = Context.WithoutActiveEditor.current, +) { + return context.wrap(vscode.window.showInputBox(opts, context.cancellationToken) + .then((v) => { + if (v === undefined) { + const reason = context.cancellationToken?.isCancellationRequested + ? CancellationError.Reason.CancellationToken + : CancellationError.Reason.PressedEscape; + + return Promise.reject(new CancellationError(reason)); + } + + return v; + })); +} + +export namespace prompt { + type RegExpFlag = "m" | "u" | "s" | "y" | "i" | "g"; + type RegExpFlags = RegExpFlag + | `${RegExpFlag}${RegExpFlag}` + | `${RegExpFlag}${RegExpFlag}${RegExpFlag}` + | `${RegExpFlag}${RegExpFlag}${RegExpFlag}${RegExpFlag}`; + + /** + * Returns `vscode.InputBoxOptions` that only validate if a number in a given + * range is entered. + */ + export function numberOpts( + opts: { integer?: boolean; range?: [number, number] } = {}, + ): vscode.InputBoxOptions { + return { + validateInput(input) { + const n = +input; + + if (isNaN(n)) { + return "Invalid number."; + } + + if (opts.range && (n < opts.range[0] || n > opts.range[1])) { + return `Number out of range ${JSON.stringify(opts.range)}.`; + } + + if (opts.integer && (n | 0) !== n) { + return `Number must be an integer.`; + } + + return; + }, + }; + } + + /** + * Equivalent to `+await prompt(numberOpts(), context)`. + */ + export function number( + opts: Parameters[0], + context = Context.WithoutActiveEditor.current, + ) { + return prompt(numberOpts(opts), context).then((x) => +x); + } + + /** + * Returns `vscode.InputBoxOptions` that only validate if a valid ECMAScript + * regular expression is entered. + */ + export function regexpOpts(flags: RegExpFlags): vscode.InputBoxOptions { + return { + prompt: "Regular expression", + validateInput(input) { + if (input.length === 0) { + return "RegExp cannot be empty"; + } + + try { + new RegExp(input, flags); + + return undefined; + } catch { + return "invalid RegExp"; + } + }, + }; + } + + /** + * Equivalent to `new RegExp(await prompt(regexpOpts(flags), context), flags)`. + */ + export function regexp( + flags: RegExpFlags, + context = Context.WithoutActiveEditor.current, + ) { + return prompt(regexpOpts(flags), context).then((x) => new RegExp(x, flags)); + } + + /** + * Prompts the user for a result interactively. + */ + export function interactive( + compute: (input: string) => T | Thenable, + reset: () => void, + options: vscode.InputBoxOptions = {}, + interactive: boolean = true, + ): Thenable { + let result: T; + const validateInput = options.validateInput; + + if (!interactive) { + return prompt(options).then((value) => compute(value)); + } + + return prompt({ + ...options, + async validateInput(input) { + const validationError = await validateInput?.(input); + + if (validationError) { + return validationError; + } + + try { + result = await compute(input); + return; + } catch (e) { + return `${e}`; + } + }, + }).then( + () => result, + (err) => { + reset(); + throw err; + }, + ); + } + + export type ListPair = readonly [string, string]; + + /** + * Prompts the user to choose one item among a list of items, and returns the + * index of the item that was picked. + */ + export function one( + items: readonly ListPair[], + init?: (quickPick: vscode.QuickPick) => void, + context = Context.WithoutActiveEditor.current, + ) { + return promptInList(false, items, init ?? (() => {}), context.cancellationToken); + } + + export namespace one { + /** + * Prompts the user for actions in a menu, only hiding it when a + * cancellation is requested or `Escape` pressed. + */ + export function locked( + items: readonly (readonly [string, string, () => void])[], + init?: (quickPick: vscode.QuickPick) => void, + cancellationToken = Context.WithoutActiveEditor.current.cancellationToken, + ) { + const itemsKeys = items.map(([k, _]) => k.includes(", ") ? k.split(", ") : [...k]); + + return new Promise((resolve, reject) => { + const quickPick = vscode.window.createQuickPick(), + quickPickItems = [] as vscode.QuickPickItem[]; + + let isCaseSignificant = false; + + for (let i = 0; i < items.length; i++) { + const [label, description] = items[i]; + + quickPickItems.push({ label, description }); + isCaseSignificant = isCaseSignificant || label.toLowerCase() !== label; + } + + quickPick.items = quickPickItems; + quickPick.placeholder = "Press one of the below keys."; + + const subscriptions = [ + quickPick.onDidChangeValue((rawKey) => { + quickPick.value = ""; + + // This causes the menu to disappear and reappear for a frame, but + // without this the shown items don't get refreshed after the value + // change above. + quickPick.items = quickPickItems; + + let key = rawKey; + + if (!isCaseSignificant) { + key = key.toLowerCase(); + } + + const index = itemsKeys.findIndex((x) => x.includes(key)); + + if (index !== -1) { + items[index][2](); + } + }), + + quickPick.onDidHide(() => { + subscriptions.splice(0).forEach((s) => s.dispose()); + + resolve(); + }), + + quickPick.onDidAccept(() => { + subscriptions.splice(0).forEach((s) => s.dispose()); + + const picked = quickPick.selectedItems[0]; + + try { + items.find((x) => x[1] === picked.description)![2](); + } finally { + resolve(); + } + }), + + cancellationToken?.onCancellationRequested(() => { + subscriptions.splice(0).forEach((s) => s.dispose()); + + reject(new CancellationError(CancellationError.Reason.CancellationToken)); + }), + + quickPick, + ]; + + init?.(quickPick); + + quickPick.show(); + }); + } + } + + /** + * Prompts the user to choose many items among a list of items, and returns a + * list of indices of picked items. + */ + export function many( + items: readonly ListPair[], + init?: (quickPick: vscode.QuickPick) => void, + context = Context.WithoutActiveEditor.current, + ) { + return promptInList(true, items, init ?? (() => {}), context.cancellationToken); + } +} + +/** + * Awaits a keypress from the user and returns the entered key. + */ +export function keypress(context = Context.current): Promise { + if (context.cancellationToken.isCancellationRequested) { + return Promise.reject(new CancellationError(CancellationError.Reason.CancellationToken)); + } + + const previousMode = context.mode; + + return context.switchToMode(context.extension.modes.inputMode).then(() => + new Promise((resolve, reject) => { + try { + const subscriptions = [ + vscode.commands.registerCommand("type", ({ text }: { text: string }) => { + if (subscriptions.length > 0) { + subscriptions.splice(0).forEach((s) => s.dispose()); + context.switchToMode(previousMode).then(() => resolve(text)); + } + }), + + context.cancellationToken.onCancellationRequested(() => { + if (subscriptions.length > 0) { + subscriptions.splice(0).forEach((s) => s.dispose()); + context.switchToMode(previousMode) + .then(() => reject(new CancellationError(CancellationError.Reason.PressedEscape))); + } + }), + ]; + } catch { + reject(new Error("unable to listen to keyboard events; is an extension " + + 'overriding the "type" command (e.g VSCodeVim)?')); + } + }), + ); +} + +export namespace keypress { + /** + * Awaits a keypress describing a register and returns the specified register. + */ + export async function forRegister(context = Context.current) { + const firstKey = await keypress(context); + + if (firstKey !== " ") { + return context.extension.registers.get(firstKey); + } + + const secondKey = await keypress(context); + + return context.extension.registers.forDocument(context.document).get(secondKey); + } +} + +function promptInList( + canPickMany: true, + items: readonly (readonly [string, string])[], + init: (quickPick: vscode.QuickPick) => void, + cancellationToken: vscode.CancellationToken, +): Thenable; +function promptInList( + canPickMany: false, + items: readonly (readonly [string, string])[], + init: (quickPick: vscode.QuickPick) => void, + cancellationToken: vscode.CancellationToken, +): Thenable; + +function promptInList( + canPickMany: boolean, + items: readonly (readonly [string, string])[], + init: (quickPick: vscode.QuickPick) => void, + cancellationToken: vscode.CancellationToken, +): Thenable { + const itemsKeys = items.map(([k, _]) => k.includes(", ") ? k.split(", ") : [...k]); + + return new Promise((resolve, reject) => { + const quickPick = vscode.window.createQuickPick(), + quickPickItems = [] as vscode.QuickPickItem[]; + + let isCaseSignificant = false; + + for (let i = 0; i < items.length; i++) { + const [label, description] = items[i]; + + quickPickItems.push({ label, description }); + isCaseSignificant = isCaseSignificant || label.toLowerCase() !== label; + } + + quickPick.items = quickPickItems; + quickPick.placeholder = "Press one of the below keys."; + quickPick.canSelectMany = canPickMany; + + const subscriptions = [ + quickPick.onDidChangeValue((rawKey) => { + if (subscriptions.length === 0) { + return; + } + + let key = rawKey; + + if (!isCaseSignificant) { + key = key.toLowerCase(); + } + + const index = itemsKeys.findIndex((x) => x.includes(key)); + + subscriptions.splice(0).forEach((s) => s.dispose()); + + if (index === -1) { + return resolve(rawKey); + } + + if (canPickMany) { + resolve([index]); + } else { + resolve(index); + } + }), + + quickPick.onDidAccept(() => { + if (subscriptions.length === 0) { + return; + } + + let picked = quickPick.selectedItems; + + if (picked !== undefined && picked.length === 0) { + picked = quickPick.activeItems; + } + + subscriptions.splice(0).forEach((s) => s.dispose()); + + if (picked === undefined) { + return reject(new CancellationError(CancellationError.Reason.PressedEscape)); + } + + if (canPickMany) { + resolve(picked.map((x) => items.findIndex((item) => item[1] === x.description))); + } else { + resolve(items.findIndex((x) => x[1] === picked[0].description)); + } + }), + + quickPick.onDidHide(() => { + if (subscriptions.length === 0) { + return; + } + + subscriptions.splice(0).forEach((s) => s.dispose()); + + reject(new CancellationError(CancellationError.Reason.PressedEscape)); + }), + + cancellationToken?.onCancellationRequested(() => { + if (subscriptions.length === 0) { + return; + } + + subscriptions.splice(0).forEach((s) => s.dispose()); + + reject(new CancellationError(CancellationError.Reason.CancellationToken)); + }), + + quickPick, + ]; + + init(quickPick); + + quickPick.show(); + }); +} diff --git a/src/api/registers.ts b/src/api/registers.ts new file mode 100644 index 0000000..827b9ff --- /dev/null +++ b/src/api/registers.ts @@ -0,0 +1,53 @@ +import * as vscode from "vscode"; +import { Register } from "../state/registers"; +import { Context } from "./context"; + +/** + * Returns the `i`th string in the register with the given name, or `undefined` + * if no value is available. + */ +export function register(name: string, i: number): Thenable; + +/** + * Returns the strings in the register with the given name, or `undefined` if no + * values are available. + */ +export function register(name: string): Thenable; + +export function register(name: string, i?: number) { + const register: Register = Context.current.extension.registers.get(name); + + register.ensureCanRead(); + + if (i === undefined) { + return register.get(); + } + + return register.get().then((values) => values?.[i]); +} + +export namespace register { + /** + * Returns the `i`th selection in the register with the given name, or + * `undefined` if no selection is available. + */ + export function selection(name: string, i: number): Thenable; + + /** + * Returns the selections in the register with the given name, or `undefined` + * if no selections are available. + */ + export function selection(name: string): Thenable; + + export function selection(name: string, i?: number): any { + const register: Register = Context.current.extension.registers.get(name); + + register.ensureCanReadSelections(); + + if (i === undefined) { + return Promise.resolve(register.getSelections()); + } + + return Promise.resolve(register.getSelections()?.[i]); + } +} diff --git a/src/api/run.ts b/src/api/run.ts new file mode 100644 index 0000000..faedb91 --- /dev/null +++ b/src/api/run.ts @@ -0,0 +1,464 @@ +import * as vscode from "vscode"; +import * as api from "."; +import { CommandDescriptor } from "../commands"; +import { parseRegExpWithReplacement } from "../utils/regexp"; +import { Context } from "./context"; + +/** + * Runs the given string of JavaScript code. + */ +export function run(string: string, context?: object): Thenable; + +/** + * Runs the given strings of JavaScript code. + */ +export function run(strings: readonly string[], context?: object): Thenable; + +export function run(strings: string | readonly string[], context: object = {}) { + const isSingleStringArgument = typeof strings === "string"; + + if (isSingleStringArgument) { + strings = [strings as string]; + } + + const functions: ((...args: any[]) => Thenable)[] = []; + + for (const code of strings) { + functions.push(run.compileFunction(code, Object.keys(context))); + } + + const parameterValues = run.parameterValues(); + + if (isSingleStringArgument) { + return Context.WithoutActiveEditor.wrap( + functions[0](...parameterValues, ...Object.values(context)), + ); + } + + const promises: Thenable[] = [], + contextValues = Object.values(context); + + for (const func of functions) { + promises.push(func(...parameterValues, ...contextValues)); + } + + return Context.WithoutActiveEditor.wrap(Promise.all(promises)); +} + +const cachedParameterNames = [] as string[], + cachedParameters = [] as unknown[]; + +function ensureCacheIsPopulated() { + if (cachedParameterNames.length > 0) { + return; + } + + for (const name in api) { + cachedParameterNames.push(name); + cachedParameters.push((api as any)[name]); + } + + cachedParameterNames.push("vscode"); + cachedParameters.push(vscode); + + Object.freeze(cachedParameterNames); + Object.freeze(cachedParameters); +} + +export namespace run { + /** + * Returns the parameter names given to dynamically run functions. + */ + export function parameterNames() { + ensureCacheIsPopulated(); + + return cachedParameterNames as readonly string[]; + } + + /** + * Returns the parameter values given to dynamically run functions. + */ + export function parameterValues() { + ensureCacheIsPopulated(); + + return cachedParameters as readonly unknown[]; + } + + let canRunArbitraryCode = true; + + /** + * Disables usage of the `compileFunction` and `run` functions, preventing the + * execution of arbitrary user inputs. + * + * For security purposes, execution cannot be re-enabled after calling this + * function. + */ + export function disable() { + canRunArbitraryCode = false; + } + + interface CompiledFunction { + (...args: any[]): Thenable; + } + + const AsyncFunction: new (...names: string[]) => CompiledFunction = + async function () {}.constructor as any, + functionCache = new Map(); + + type CachedFunction = [function: CompiledFunction, lastAccessTimestamp: number]; + + /** + * A few common inputs. + */ + const safeExpressions = [ + /^(\$\$?|[in]|\d+) *([=!]==?|[<>]=?|&{1,2}|\|{1,2}) *(\$\$?|[in]|\d+)$/, + /^i( + 1)?$/, + /^`\${await register\(["']\w+["'], *[i0-9]\)}` !== ["']false["']$/, + ]; + + /** + * Compiles the given JavaScript code into a function. + */ + export function compileFunction(code: string, additionalParameterNames: readonly string[] = []) { + if (!canRunArbitraryCode && !safeExpressions.some((re) => re.test(code))) { + throw new Error("execution of arbitrary code is disabled"); + } + + const cacheId = additionalParameterNames.join(";") + code, + cached = functionCache.get(cacheId); + + if (cached !== undefined) { + cached[1] = Date.now(); + + return cached[0]; + } + + let func: CompiledFunction; + + try { + // Wrap code in block to allow shadowing of parameters. + func = new AsyncFunction(...parameterNames(), ...additionalParameterNames, `{\n${code}\n}`); + } catch (e) { + throw new Error(`cannot parse function body: ${code}: ${e}`); + } + + functionCache.set(cacheId, [func, Date.now()]); + + return func; + } + + /** + * Removes all functions that were not used in the last n milliseconds from + * the cache. + */ + export function clearCache(olderThanMs: number): void; + + /** + * Removes all functions that were not used in the last 5 minutes from the + * cache. + */ + export function clearCache(): void; + + export function clearCache(olderThanMs = 1000 * 60 * 5) { + if (olderThanMs === 0) { + return functionCache.clear(); + } + + const olderThan = Date.now() - olderThanMs, + toDelete = [] as string[]; + + for (const [code, value] of functionCache) { + if (value[1] < olderThan) { + toDelete.push(code); + } + } + + for (const code of toDelete) { + functionCache.delete(code); + } + } +} + +/** + * Runs the VS Code command with the given identifier and optional arguments. + */ +export function command(commandName: string, ...args: readonly any[]): Thenable { + return commands([commandName, ...args]).then((x) => x[0]); +} + +/** + * Runs the VS Code commands with the given identifiers and optional arguments. + */ +export async function commands(...commands: readonly command.Any[]): Promise { + const extension = Context.WithoutActiveEditor.current.extension, + batches = [] as ([CommandDescriptor, any][] | [string, any])[], + currentBatch = [] as [CommandDescriptor, any][]; + + // Build and validate commands. + for (let i = 0, len = commands.length; i < len; i++) { + let commandName: string, + commandArguments: any; + + const command = commands[i]; + + if (typeof command === "string") { + commandName = command; + commandArguments = undefined; + } else if (Array.isArray(command)) { + commandName = command[0]; + commandArguments = command.slice(1); + + if (typeof commandName !== "string") { + throw new Error("the first element of a command tuple must be a command name"); + } + } else if (typeof command === "object" && command !== null) { + commandName = (command as command.Command).command; + commandArguments = (command as command.Command).args; + + if (typeof commandName !== "string") { + throw new Error("the \"command\" property of a command object must be a command name"); + } + } else { + throw new Error( + "commands must be command names, {command: string, args: any} objects or arrays", + ); + } + + if (commandName.startsWith(".")) { + commandName = `dance${commandName}`; + } + + if (commandName.startsWith("dance.")) { + const descriptor = extension.commands[commandName]; + + if (descriptor === undefined) { + throw new Error(`command ${JSON.stringify(commandName)} does not exist`); + } + + const argument = Array.isArray(commandArguments) ? commandArguments[0] : commandArguments; + + currentBatch.push([descriptor, argument]); + } else { + if (currentBatch.length > 0) { + batches.push(currentBatch.splice(0)); + } + + batches.push([commandName, commandArguments]); + } + } + + if (currentBatch.length > 0) { + batches.push(currentBatch); + } + + // Execute all commands. + const results = []; + + for (const batch of batches) { + if (typeof batch[0] === "string") { + results.push(await vscode.commands.executeCommand(batch[0], batch[1])); + } else { + const context = Context.WithoutActiveEditor.current; + + for (const pair of batch as [CommandDescriptor, any][]) { + const [descriptor, argument] = pair, + ownedArgument = pair[1] = Object.assign({}, argument); + + if (ownedArgument.try) { + delete ownedArgument.try; + + try { + results.push(await descriptor.handler(context, ownedArgument)); + } catch { + results.push(undefined); + } + } else { + results.push(await descriptor.handler(context, ownedArgument)); + } + } + } + } + + const recorder = extension.recorder; + + for (const batch of batches) { + if (typeof batch[0] === "string") { + recorder.recordExternalCommand(batch[0], batch[1]); + } else { + for (const [descriptor, argument] of batch as [CommandDescriptor, any][]) { + recorder.recordCommand(descriptor, argument); + } + } + } + + return results; +} + +export namespace command { + /** + * A tuple given to `command`. + */ + export type Tuple = readonly [commandId: string, ...args: any[]]; + + /** + * An object given to `command`. + */ + export interface Command { + readonly command: string; + readonly args?: any; + } + + export type Any = string | Tuple | Command; +} + +let canExecuteArbitraryCommands = true; + +/** + * Executes a shell command. + */ +export function execute( + command: string, + input?: string, + cancellationToken = Context.WithoutActiveEditor.current.cancellationToken, +) { + if (!canExecuteArbitraryCommands) { + return Context.WithoutActiveEditor.wrap( + Promise.reject(new Error("execution of arbitrary commands is disabled")), + ); + } + + return Context.WithoutActiveEditor.wrap(import("child_process").then((cp) => + new Promise<{ readonly val: string } | { readonly err: string }>((resolve) => { + const shell = getShell() ?? true, + child = cp.spawn(command, { shell, stdio: "pipe" }); + + let stdout = "", + stderr = ""; + + const disposable = cancellationToken.onCancellationRequested(() => { + child.kill("SIGINT"); + }); + + child.stdout.on("data", (chunk: Buffer) => (stdout += chunk.toString("utf-8"))); + child.stderr.on("data", (chunk: Buffer) => (stderr += chunk.toString("utf-8"))); + child.stdin.end(input, "utf-8"); + + child.once("error", (err) => { + disposable.dispose(); + + resolve({ err: err.message }); + }); + child.once("exit", (code) => { + disposable.dispose(); + + code === 0 + ? resolve({ val: stdout.trimRight() }) + : resolve({ + err: `Command exited with error ${code}: ${ + stderr.length > 0 ? stderr.trimRight() : "" + }`, + }); + }); + })), + ); +} + +export namespace execute { + /** + * Disables usage of the `execute` function, preventing the execution of + * arbitrary user commands. + * + * For security purposes, execution cannot be re-enabled after calling this + * function. + */ + export function disable() { + canExecuteArbitraryCommands = false; + } +} + +function getShell() { + let os: string; + + switch (process.platform) { + case "cygwin": + case "linux": + os = "linux"; + break; + + case "darwin": + os = "osx"; + break; + + case "win32": + os = "windows"; + break; + + default: + return undefined; + } + + const config = vscode.workspace.getConfiguration("terminal"); + + return config.get(`integrated.automationShell.${os}`) + ?? process.env.SHELL + ?? undefined; +} + +/** + * Runs the given string of JavaScript code on the given input, except in two + * cases: + * 1. If the string is a complete RegExp expression, instead its match will be + * returned. + * 2. If the string starts with "#", instead a command will be run. + */ +export function switchRun(string: string, context: { $: string } & Record) { + if (string.length === 0) { + // An empty expression is just `undefined`. + return Context.WithoutActiveEditor.wrap(Promise.resolve()); + } + + if (string[0] === "/") { + // RegExp replace or match. + const [regexp, replacement] = parseRegExpWithReplacement(string); + + if (replacement === undefined) { + return Context.WithoutActiveEditor.wrap(Promise.resolve(regexp.exec(context.$))); + } + + return Context.WithoutActiveEditor.wrap(Promise.resolve(context.$.replace(regexp, replacement))); + } + + if (string[0] === "#") { + // Shell command. + return execute(string.slice(1), context.$); + } + + // JavaScript expression. + return run("return " + string, context); +} + +export namespace switchRun { + /** + * Validates the given input string. If it is invalid, an exception will be + * thrown. + */ + export function validate(string: string) { + if (string.trim().length === 0) { + throw new Error("the given string cannot be empty"); + } + + if (string[0] === "/") { + parseRegExpWithReplacement(string); + return; + } + + if (string[0] === "#") { + if (string.slice(1).trim().length === 0) { + throw new Error("the given shell command cannot be empty"); + } + return; + } + + run.compileFunction("return " + string); + } +} diff --git a/src/api/search/index.ts b/src/api/search/index.ts new file mode 100644 index 0000000..60e11b7 --- /dev/null +++ b/src/api/search/index.ts @@ -0,0 +1,386 @@ +import * as vscode from "vscode"; +import * as regexp from "../../utils/regexp"; + +import { Context, Direction } from ".."; +import { Positions } from "../positions"; + +/** + * Searches backward or forward for a pattern starting at the given position. + * + * @see search.backward,search.forward + */ +export function search( + direction: Direction, + re: RegExp, + origin: vscode.Position, + end?: vscode.Position, +) { + return direction === Direction.Backward + ? search.backward(re, origin, end) + : search.forward(re, origin, end); +} + +export namespace search { + /** + * The type of the result of a search: a `[startPosition, match]` pair if the + * search succeeded, and `undefined` otherwise. + */ + export type Result = [vscode.Position, RegExpMatchArray] | undefined; + + /** + * Searches backward for a pattern starting at the given position. + * + * ### Example + * + * ```ts + * const [p1, [t1]] = search.backward(/\w/, new vscode.Position(0, 1))!; + * + * assert.deepStrictEqual(p1, new vscode.Position(0, 0)); + * assert.strictEqual(t1, "a"); + * + * const [p2, [t2]] = search.backward(/\w/, new vscode.Position(0, 2))!; + * + * assert.deepStrictEqual(p2, new vscode.Position(0, 1)); + * assert.strictEqual(t2, "b"); + * + * const [p3, [t3]] = search.backward(/\w+/, new vscode.Position(0, 2))!; + * + * assert.deepStrictEqual(p3, new vscode.Position(0, 0)); + * assert.strictEqual(t3, "ab"); + * + * assert.strictEqual( + * search.backward(/\w/, new vscode.Position(0, 0)), + * undefined, + * ); + * ``` + * + * With: + * ``` + * abc + * ``` + */ + export function backward(re: RegExp, origin: vscode.Position, end?: vscode.Position): Result { + end ??= Positions.zero; + + const document = Context.current.document, + searchStart = document.offsetAt(end), + searchEnd = document.offsetAt(origin), + possibleSearchLength = searchEnd - searchStart; + + if (possibleSearchLength < 0) { + return; + } + + if (possibleSearchLength > 2_000) { + const staticMatches = regexp.matchesStaticStrings(re); + + if (staticMatches !== undefined) { + return searchOneOfBackward(re, staticMatches, origin, end, document); + } + + if (!regexp.canMatchLineFeed(re)) { + return searchSingleLineRegExpBackward(re, origin, end, document); + } + } + + return searchNaiveBackward(re, origin, end, document); + } + + /** + * Searches forward for a pattern starting at the given position. + * + * ### Example + * + * ```ts + * const [p1, [t1]] = search.forward(/\w/, new vscode.Position(0, 0))!; + * + * assert.deepStrictEqual(p1, new vscode.Position(0, 0)); + * assert.strictEqual(t1, "a"); + * + * const [p2, [t2]] = search.forward(/\w/, new vscode.Position(0, 1))!; + * + * assert.deepStrictEqual(p2, new vscode.Position(0, 1)); + * assert.strictEqual(t2, "b"); + * + * const [p3, [t3]] = search.forward(/\w+/, new vscode.Position(0, 1))!; + * + * assert.deepStrictEqual(p3, new vscode.Position(0, 1)); + * assert.strictEqual(t3, "bc"); + * + * assert.strictEqual( + * search.forward(/\w/, new vscode.Position(0, 3)), + * undefined, + * ); + * ``` + * + * With: + * ``` + * abc + * ``` + */ + export function forward(re: RegExp, origin: vscode.Position, end?: vscode.Position): Result { + const document = Context.current.document; + + end ??= Positions.last(document); + + const searchStart = document.offsetAt(origin), + searchEnd = document.offsetAt(end), + possibleSearchLength = searchEnd - searchStart; + + if (possibleSearchLength < 0) { + return; + } + + if (possibleSearchLength > 2_000) { + const staticMatches = regexp.matchesStaticStrings(re); + + if (staticMatches !== undefined) { + return searchOneOfForward(re, staticMatches, origin, end, document); + } + + if (!regexp.canMatchLineFeed(re)) { + return searchSingleLineRegExpForward(re, origin, end, document); + } + } + + return searchNaiveForward(re, origin, end, document); + } +} + +function maxLines(strings: readonly string[]) { + let max = 1; + + for (const string of strings) { + let lines = 1; + + for (let i = 0, len = string.length; i < len; i++) { + if (string.charCodeAt(i) === 10 /* \n */) { + lines++; + } + } + + if (lines > max) { + max = lines; + } + } + + return max; +} + +function searchNaiveBackward( + re: RegExp, + origin: vscode.Position, + end: vscode.Position, + document: vscode.TextDocument, +) { + re.lastIndex = 0; + + // Find all matches before the origin and take the last one. + const searchRange = new vscode.Range(end, origin), + match = regexp.execLast(re, document.getText(searchRange)); + + if (match === null) { + return; + } + + return [Positions.offset(end, match.index), match] as search.Result; +} + +function searchNaiveForward( + re: RegExp, + origin: vscode.Position, + end: vscode.Position, + document: vscode.TextDocument, +) { + re.lastIndex = 0; + + // Look for a match in all the rest of the document. + const searchRange = new vscode.Range(origin, end), + match = re.exec(document.getText(searchRange)); + + if (match === null) { + return; + } + + const matchPosition = document.positionAt(document.offsetAt(origin) + match.index); + + return [matchPosition, match] as search.Result; +} + +function searchSingleLineRegExpBackward( + re: RegExp, + origin: vscode.Position, + end: vscode.Position, + document: vscode.TextDocument, +) { + re.lastIndex = 0; + + // Loop for a match line by line, starting at the current line. + const currentLine = document.lineAt(origin), + match = regexp.execLast(re, currentLine.text.slice(0, origin.character)); + + if (match !== null) { + return [new vscode.Position(origin.line, match.index), match] as search.Result; + } + + const endLine = end.line; + + for (let line = origin.line - 1; line > endLine; line--) { + const textLine = document.lineAt(line), + match = regexp.execLast(re, textLine.text); + + if (match !== null) { + return [new vscode.Position(line, match.index), match] as search.Result; + } + } + + const endMatch = regexp.execLast(re, document.lineAt(endLine).text.slice(end.character)); + + if (endMatch !== null) { + const endCharacter = end.character + endMatch.index; + + return [new vscode.Position(endLine, endCharacter), endMatch] as search.Result; + } + + return; +} + +function searchSingleLineRegExpForward( + re: RegExp, + origin: vscode.Position, + end: vscode.Position, + document: vscode.TextDocument, +) { + re.lastIndex = 0; + + // Loop for a match line by line, starting at the current line. + const currentLine = document.lineAt(origin), + match = re.exec(currentLine.text.slice(origin.character)); + + if (match !== null) { + return [origin.translate(undefined, match.index), match] as search.Result; + } + + const endLine = end.line; + + for (let line = origin.line + 1; line < endLine; line++) { + const textLine = document.lineAt(line), + match = re.exec(textLine.text); + + if (match !== null) { + return [new vscode.Position(line, match.index), match] as search.Result; + } + } + + const endMatch = re.exec(document.lineAt(endLine).text.slice(0, end.character)); + + if (endMatch !== null) { + return [new vscode.Position(endLine, endMatch.index), endMatch] as search.Result; + } + + return; +} + +function searchOneOfBackward( + re: RegExp, + oneOf: readonly string[], + origin: vscode.Position, + end: vscode.Position, + document: vscode.TextDocument, +) { + const lineRange = maxLines(oneOf); + + if (lineRange === 1) { + return searchSingleLineRegExpBackward(re, origin, end, document); + } + + const endLine = end.line; + + if (origin.line - endLine < lineRange) { + return; + } + + re.lastIndex = 0; + + const originLine = origin.line, + lines = [document.lineAt(originLine).text.slice(0, origin.character)], + joiner = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n"; + + for (let i = 1; i < lineRange; i++) { + lines.unshift(document.lineAt(originLine - i).text); + } + + const lineToSlice = end.character === 0 ? -1 : endLine; + + for (let line = originLine - lineRange + 1; line >= endLine; line--) { + lines[0] = document.lineAt(line).text; + + if (line === lineToSlice) { + lines[0] = lines[0].slice(end.character); + } + + const text = lines.join(joiner), + match = re.exec(text); + + if (match !== null) { + return [new vscode.Position(line, match.index), match] as search.Result; + } + + for (let i = lineRange; i > 0; i--) { + lines[i] = lines[i + 1]; + } + } + + return; +} + +function searchOneOfForward( + re: RegExp, + oneOf: readonly string[], + origin: vscode.Position, + end: vscode.Position, + document: vscode.TextDocument, +) { + const lineRange = maxLines(oneOf); + + if (lineRange === 1) { + return searchSingleLineRegExpForward(re, origin, end, document); + } + + const endLine = end.line; + + if (origin.line + lineRange >= endLine) { + return; + } + + re.lastIndex = 0; + + const originLine = origin.line, + lines = [document.lineAt(originLine).text.slice(origin.character)], + joiner = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n"; + + for (let i = 1; i < lineRange; i++) { + lines.push(document.lineAt(originLine + i).text); + } + + for (let line = originLine, loopEnd = endLine - lineRange + 1; line < loopEnd; line++) { + const text = lines.join(joiner), + match = re.exec(text); + + if (match !== null) { + return [new vscode.Position(line, match.index), match] as search.Result; + } + + for (let i = 1; i < lineRange; i++) { + lines[i - 1] = lines[i]; + } + + lines[lines.length - 1] = document.lineAt(line + lineRange).text; + + if (line === loopEnd - 1) { + lines[lines.length - 1] = lines[lines.length - 1].slice(0, end.character); + } + } + + return; +} diff --git a/src/api/search/lines.ts b/src/api/search/lines.ts new file mode 100644 index 0000000..b4a74b3 --- /dev/null +++ b/src/api/search/lines.ts @@ -0,0 +1,54 @@ +import * as vscode from "vscode"; + +import { Context } from "../context"; +import { Positions } from "../positions"; +import { lineByLine } from "./move"; + +/** + * Returns the range of lines matching the given `RegExp` before and after + * the given origin position. + */ +export function matchingLines( + re: RegExp, + origin: number | vscode.Position, + document = Context.current.document, +) { + const start = matchingLines.backward(re, origin, document), + end = matchingLines.forward(re, origin, document); + + return new vscode.Range(start, end); +} + +export namespace matchingLines { + /** + * Returns the position of the first line matching the given `RegExp`, + * starting at the `origin` line (included). + */ + export function backward( + re: RegExp, + origin: number | vscode.Position, + document = Context.current.document, + ) { + return lineByLine.backward( + (text, position) => re.test(text) ? position : undefined, + typeof origin === "number" ? Positions.lineStart(origin) : origin, + document, + ) ?? Positions.zero; + } + + /** + * Returns the position of the last line matching the given `RegExp`, starting + * at the `origin` line (included). + */ + export function forward( + re: RegExp, + origin: number | vscode.Position, + document = Context.current.document, + ) { + return lineByLine.forward( + (text, position) => re.test(text) ? position : undefined, + typeof origin === "number" ? Positions.lineStart(origin) : origin, + document, + ) ?? Positions.lineStart(document.lineCount - 1); + } +} diff --git a/src/api/search/move-to.ts b/src/api/search/move-to.ts new file mode 100644 index 0000000..dc0127a --- /dev/null +++ b/src/api/search/move-to.ts @@ -0,0 +1,100 @@ +import * as vscode from "vscode"; + +import { Context, Direction } from ".."; +import { Positions } from "../positions"; + +/** + * Moves the given position towards the given direction until the given string + * is found. If found, its start position will be returned; otherwise, + * `undefined` will be returned. + * + * When navigating backward, the returned position **will include** the given + * input, which is not the case when navigating forward. Please make sure that + * this is what you want. + */ +export function moveTo( + direction: Direction, + string: string, + origin: vscode.Position, + document = Context.current.document, +) { + let line = origin.line, + character: number | undefined = origin.character; + + for (;;) { + const text = document.lineAt(line).text; + + if (character === undefined) { + character = text.length; + } + + const idx = direction === Direction.Backward + ? text.lastIndexOf(string, character) + : text.indexOf(string, character); + + if (idx !== -1) { + return new vscode.Position(line, idx); + } + + // No match on this line, let's keep going. + if (direction === Direction.Backward) { + if (line === 0) { + return undefined; + } + + line--; + character = undefined; + } else { + if (line === document.lineCount - 1) { + return undefined; + } + + line++; + character = 0; + } + } +} + +export namespace moveTo { + /** + * Same as `moveTo`, but also ensures that the result is excluded by + * translating the resulting position by `input.length` when going backward. + * + * @see moveTo + */ + export function excluded( + direction: Direction, + string: string, + origin: vscode.Position, + document?: vscode.TextDocument, + ) { + const result = moveTo(direction, string, origin, document); + + if (result !== undefined && direction === Direction.Backward) { + return Positions.offset(result, string.length, document); + } + + return result; + } + + /** + * Same as `moveTo`, but also ensures that the result is included by + * translating the resulting position by `input.length` when going forward. + * + * @see moveTo + */ + export function included( + direction: Direction, + string: string, + origin: vscode.Position, + document?: vscode.TextDocument, + ) { + const result = moveTo(direction, string, origin, document); + + if (result !== undefined && direction === Direction.Forward) { + return Positions.offset(result, string.length, document); + } + + return result; + } +} diff --git a/src/api/search/move.ts b/src/api/search/move.ts new file mode 100644 index 0000000..f1a8b41 --- /dev/null +++ b/src/api/search/move.ts @@ -0,0 +1,601 @@ +import * as vscode from "vscode"; + +import { Context, Direction } from ".."; +import { Positions } from "../positions"; + +/** + * Moves the given position towards the given direction as long as the given + * function returns a non-`undefined` value. + * + * @see moveWhile.backward,takeWhile.forward + */ +export function moveWith( + direction: Direction, + reduce: moveWith.Reduce, + startState: T, + origin: vscode.Position, + document?: vscode.TextDocument, +): vscode.Position { + return direction === Direction.Backward + ? moveWith.backward(reduce, startState, origin, document) + : moveWith.forward(reduce, startState, origin, document); +} + +export namespace moveWith { + /** + * A reduce function passed to `moveWith`. + */ + export interface Reduce { + (character: string, state: T): T | undefined; + } + + /** + * Whether the last call to `moveWith` (and variants) reached the edge of the + * document. + */ + export declare const reachedDocumentEdge: boolean; + + /** + * Moves the given position backward as long as the state returned by the + * given function is not `undefined`. + * + * ### Example + * + * ```js + * assert.deepStrictEqual( + * moveWith.backward((c, i) => +c === +i - 1 ? c : undefined, + * "8", new vscode.Position(0, 8)), + * new vscode.Position(0, 7), + * ); + * ``` + * + * With: + * ``` + * 1234578 + * ``` + */ + export function backward( + reduce: Reduce, + startState: T, + origin: vscode.Position, + document = Context.current.document, + ) { + didReachDocumentEdge = false; + + const currentLineText = document.lineAt(origin).text; + let state: T | undefined = startState; + + for (let i = origin.character - 1; i >= 0; i--) { + if ((state = reduce(currentLineText[i], state)) === undefined) { + return new vscode.Position(origin.line, i + 1); + } + } + + for (let line = origin.line - 1; line >= 0; line--) { + const lineText = document.lineAt(line).text; + + if ((state = reduce("\n", state)) === undefined) { + return new vscode.Position(line + 1, 0); + } + + for (let i = lineText.length - 1; i >= 0; i--) { + if ((state = reduce(lineText[i], state)) === undefined) { + return new vscode.Position(line, i + 1); + } + } + } + + didReachDocumentEdge = true; + + return new vscode.Position(0, 0); + } + + /** + * Moves the given position forward as long as the state returned by the given + * function is not `undefined`. + * + * ### Example + * + * ```js + * assert.deepStrictEqual( + * moveWith.forward((c, i) => +c === +i + 1 ? c : undefined, + * "1", new vscode.Position(0, 8)), + * new vscode.Position(0, 7), + * ); + * ``` + * + * With: + * ``` + * 1234578 + * ``` + */ + export function forward( + reduce: Reduce, + startState: T, + origin: vscode.Position, + document = Context.current.document, + ) { + didReachDocumentEdge = false; + + const currentLineText = document.lineAt(origin).text; + let state: T | undefined = startState; + + for (let i = origin.character; i < currentLineText.length; i++) { + if ((state = reduce(currentLineText[i], state)) === undefined) { + return new vscode.Position(origin.line, i); + } + } + + if ((state = reduce("\n", state)) === undefined) { + return new vscode.Position(origin.line, currentLineText.length); + } + + for (let line = origin.line + 1; line < document.lineCount; line++) { + const lineText = document.lineAt(line).text; + + for (let i = 0; i < lineText.length; i++) { + if ((state = reduce(lineText[i], state)) === undefined) { + return new vscode.Position(line, i); + } + } + + if ((state = reduce("\n", state)) === undefined) { + return new vscode.Position(line, lineText.length); + } + } + + didReachDocumentEdge = true; + + return document.lineAt(document.lineCount - 1).range.end; + } + + /** + * Same as `moveWith`, but using raw char codes. + * + * @see moveWith,byCharCode.backward,byCharCode.forward + */ + export function byCharCode( + direction: Direction, + reduce: byCharCode.Reduce, + startState: T, + origin: vscode.Position, + document?: vscode.TextDocument, + ): vscode.Position { + return direction === Direction.Backward + ? byCharCode.backward(reduce, startState, origin, document) + : byCharCode.forward(reduce, startState, origin, document); + } + + export namespace byCharCode { + /** + * A reduce function passed to `moveWith.byCharCode`. + */ + export interface Reduce { + (charCode: number, state: T): T | undefined; + } + + /** + * Same as `moveWith.backward`, but using raw char codes. + * + * @see moveWith.backward + */ + export function backward( + reduce: Reduce, + startState: T, + origin: vscode.Position, + document = Context.current.document, + ) { + didReachDocumentEdge = false; + + const currentLineText = document.lineAt(origin).text; + let state: T | undefined = startState; + + for (let i = origin.character - 1; i >= 0; i--) { + if ((state = reduce(currentLineText.charCodeAt(i), state)) === undefined) { + return new vscode.Position(origin.line, i + 1); + } + } + + for (let line = origin.line - 1; line >= 0; line--) { + const lineText = document.lineAt(line).text; + + if ((state = reduce(10 /* \n */, state)) === undefined) { + return new vscode.Position(line + 1, 0); + } + + for (let i = lineText.length - 1; i >= 0; i--) { + if ((state = reduce(lineText.charCodeAt(i), state)) === undefined) { + return new vscode.Position(line, i + 1); + } + } + } + + didReachDocumentEdge = true; + return new vscode.Position(0, 0); + } + + /** + * Same as `moveWith.forward`, but using raw char codes. + * + * @see moveWith.forward + */ + export function forward( + reduce: Reduce, + startState: T, + origin: vscode.Position, + document = Context.current.document, + ) { + didReachDocumentEdge = false; + + const currentLineText = document.lineAt(origin).text; + let state: T | undefined = startState; + + for (let i = origin.character; i < currentLineText.length; i++) { + if ((state = reduce(currentLineText.charCodeAt(i), state)) === undefined) { + return new vscode.Position(origin.line, i); + } + } + + if ((state = reduce(10 /* \n */, state)) === undefined) { + return new vscode.Position(origin.line, currentLineText.length); + } + + for (let line = origin.line + 1; line < document.lineCount; line++) { + const lineText = document.lineAt(line).text; + + for (let i = 0; i < lineText.length; i++) { + if ((state = reduce(lineText.charCodeAt(i), state)) === undefined) { + return new vscode.Position(line, i); + } + } + + if ((state = reduce(10 /* \n */, state)) === undefined) { + return new vscode.Position(line, lineText.length); + } + } + + didReachDocumentEdge = true; + return document.lineAt(document.lineCount - 1).range.end; + } + } +} + +/** + * Moves the given position towards the given direction as long as the given + * predicate is true. + * + * @see moveWhile.backward,takeWhile.forward + */ +export function moveWhile( + direction: Direction, + predicate: moveWhile.Predicate, + origin: vscode.Position, + document?: vscode.TextDocument, +): vscode.Position { + return direction === Direction.Backward + ? moveWhile.backward(predicate, origin, document) + : moveWhile.forward(predicate, origin, document); +} + +export namespace moveWhile { + /** + * A predicate passed to `moveWhile`. + */ + export interface Predicate { + (character: string): boolean; + } + + /** + * Whether the last call to `moveWhile` (and variants) reached the edge of the + * document. + */ + export declare const reachedDocumentEdge: boolean; + + /** + * Moves the given position backward as long as the given predicate is true. + * + * ### Example + * + * ```js + * assert.deepStrictEqual( + * moveWhile.backward((c) => /\w/.test(c), new vscode.Position(0, 3)), + * new vscode.Position(0, 0), + * ); + * + * assert.deepStrictEqual( + * moveWhile.backward((c) => c === "c", new vscode.Position(0, 3)), + * new vscode.Position(0, 2), + * ); + * + * assert.deepStrictEqual( + * moveWhile.backward((c) => c === "b", new vscode.Position(0, 3)), + * new vscode.Position(0, 3), + * ); + * ``` + * + * With: + * ``` + * abc + * ``` + */ + export function backward( + predicate: Predicate, + origin: vscode.Position, + document?: vscode.TextDocument, + ): vscode.Position { + return moveWith.backward((ch) => predicate(ch) ? null : undefined, null, origin, document); + } + + /** + * Moves the given position forward as long as the given predicate is true. + * + * ### Example + * + * ```js + * assert.deepStrictEqual( + * moveWhile.forward((c) => /\w/.test(c), new vscode.Position(0, 0)), + * new vscode.Position(0, 3), + * ); + * + * assert.deepStrictEqual( + * moveWhile.forward((c) => c === "a", new vscode.Position(0, 0)), + * new vscode.Position(0, 1), + * ); + * + * assert.deepStrictEqual( + * moveWhile.forward((c) => c === "b", new vscode.Position(0, 0)), + * new vscode.Position(0, 0), + * ); + * ``` + * + * With: + * ``` + * abc + * ``` + */ + export function forward( + predicate: Predicate, + origin: vscode.Position, + document?: vscode.TextDocument, + ): vscode.Position { + return moveWith.forward((ch) => predicate(ch) ? null : undefined, null, origin, document); + } + + /** + * Same as `moveWith`, but using raw char codes. + * + * @see moveWith,byCharCode.backward,byCharCode.forward + */ + export function byCharCode( + direction: Direction, + predicate: byCharCode.Predicate, + origin: vscode.Position, + document?: vscode.TextDocument, + ): vscode.Position { + return direction === Direction.Backward + ? byCharCode.backward(predicate, origin, document) + : byCharCode.forward(predicate, origin, document); + } + + export namespace byCharCode { + /** + * A predicate passed to `moveWhile.byCharCode`. + */ + export interface Predicate { + (charCode: number): boolean; + } + + /** + * Same as `moveWhile.backward`, but using raw char codes. + * + * @see moveWhile.backward + */ + export function backward( + predicate: Predicate, + origin: vscode.Position, + document?: vscode.TextDocument, + ): vscode.Position { + return moveWith.byCharCode.backward( + (ch) => predicate(ch) ? null : undefined, + null, + origin, + document, + ); + } + + /** + * Same as `moveWhile.forward`, but using raw char codes. + * + * @see moveWhile.forward + */ + export function forward( + predicate: Predicate, + origin: vscode.Position, + document?: vscode.TextDocument, + ): vscode.Position { + return moveWith.byCharCode.forward( + (ch) => predicate(ch) ? null : undefined, + null, + origin, + document, + ); + } + } +} + +/** + * Moves the given position line-by-line towards the given direction as long as + * the given function returns `undefined`, and returns the first non-`undefined` + * value it returns or `undefined` if the edge of the document is reached. + * + * @see lineByLine.backward,lineByLine.forward + */ +export function lineByLine( + direction: Direction, + seek: lineByLine.Seek, + origin: vscode.Position, + document?: vscode.TextDocument, +) { + return direction === Direction.Backward + ? lineByLine.backward(seek, origin, document) + : lineByLine.forward(seek, origin, document); +} + +export namespace lineByLine { + /** + * A reduce function passed to `lineByLine`. + */ + export interface Seek { + (lineText: string, lineStart: vscode.Position): T | undefined; + } + + /** + * Whether the last call to `lineByLine` (and variants) reached the edge of + * the document. + */ + export declare const reachedDocumentEdge: boolean; + + /** + * Moves the given position backward line-by-line as long as the given + * function returns `undefined`, and returns the first non-`undefined` + * value it returns or `undefined` if the start of the document is reached. + */ + export function backward( + seek: Seek, + origin: vscode.Position, + document = Context.current.document, + ) { + didReachDocumentEdge = false; + + const originLine = document.lineAt(origin), + originLineText = originLine.text.slice(0, origin.character), + originResult = seek(originLineText, Positions.lineStart(origin.line)); + + if (originResult !== undefined) { + return originResult; + } + + for (let line = origin.line - 1; line >= 0; line--) { + const lineText = document.lineAt(line).text, + result = seek(lineText, Positions.lineStart(line)); + + if (result !== undefined) { + return result; + } + } + + didReachDocumentEdge = true; + + return undefined; + } + + /** + * Moves the given position forward line-by-line as long as the given + * function returns `undefined`, and returns the first non-`undefined` + * value it returns or `undefined` if the end of the document is reached. + */ + export function forward( + seek: Seek, + origin: vscode.Position, + document = Context.current.document, + ) { + didReachDocumentEdge = false; + + const originLine = document.lineAt(origin), + originLineText = originLine.text.slice(origin.character), + originResult = seek(originLineText, origin); + + if (originResult !== undefined) { + return originResult; + } + + for (let line = origin.line + 1, lineCount = document.lineCount; line < lineCount; line++) { + const lineText = document.lineAt(line).text, + result = seek(lineText, Positions.lineStart(line)); + + if (result !== undefined) { + return result; + } + } + + didReachDocumentEdge = true; + + return undefined; + } +} + +/** + * Advances in the given direction as long as lines are empty, and returns the + * position closest to the origin in a non-empty line. The origin line will + * always be skipped. + */ +export function skipEmptyLines( + direction: Direction, + origin: number | vscode.Position, + document = Context.current.document, +) { + didReachDocumentEdge = false; + + let line = typeof origin === "number" ? origin : origin.line; + + while (line >= 0 && line < document.lineCount) { + const lineLength = document.lineAt(line).text.length; + + if (lineLength > 0) { + return new vscode.Position( + line, + direction === Direction.Backward ? lineLength : 0, + ); + } + + line += direction; + } + + didReachDocumentEdge = true; + + return Positions.edge(direction, document); +} + +export namespace skipEmptyLines { + /** + * Whether the last call to `skipEmptyLines` (and variants) reached the edge + * of the document. + */ + export declare const reachedDocumentEdge: boolean; + + /** + * Same as `skipEmptyLines` with a `Backward` direction. + * + * @see skipEmptyLines + */ + export function backward(origin: number | vscode.Position, document?: vscode.TextDocument) { + return skipEmptyLines(Direction.Backward, origin, document); + } + + /** + * Same as `skipEmptyLines` with a `Forward` direction. + * + * @see skipEmptyLines + */ + export function forward(origin: number | vscode.Position, document?: vscode.TextDocument) { + return skipEmptyLines(Direction.Forward, origin, document); + } +} + +let didReachDocumentEdge = false; +const getReachedDocumentEdge = { + get() { + return didReachDocumentEdge; + }, +}; + +for (const obj of [ + lineByLine, + moveWhile, + moveWhile.byCharCode, + moveWith, + moveWith.byCharCode, + skipEmptyLines, +]) { + Object.defineProperty(obj, "reachedDocumentEdge", getReachedDocumentEdge); +} diff --git a/src/api/search/pairs.ts b/src/api/search/pairs.ts new file mode 100644 index 0000000..614c3bd --- /dev/null +++ b/src/api/search/pairs.ts @@ -0,0 +1,177 @@ +import * as vscode from "vscode"; +import { search } from "."; +import { Direction } from ".."; +import { anyRegExp, escapeForRegExp } from "../../utils/regexp"; +import { Context } from "../context"; +import { ArgumentError } from "../errors"; +import { Positions } from "../positions"; + +/** + * A pair of opening and closing patterns. + */ +export class Pair { + private readonly _re: RegExp; + private readonly _closeGroup: number; + + public constructor( + public readonly open: RegExp, + public readonly close: RegExp, + ) { + const [re, group] = anyRegExp(open, close); + + this._re = re; + this._closeGroup = group; + } + + public searchMatching( + direction: Direction, + searchOrigin: vscode.Position, + balance = 1, + ): search.Result { + ArgumentError.validate("balance", balance !== 0, "balance cannot be null"); + + const re = this._re, + closeGroup = this._closeGroup; + + for (;;) { + const result = search(direction, re, searchOrigin); + + if (result === undefined) { + return undefined; + } + + const match = result[1]; + + if (match[closeGroup] === undefined) { + // Opening pattern matched. + balance++; + } else { + // Closing pattern matched. + balance--; + } + + if (balance === 0) { + return result; + } + + if (direction === Direction.Forward) { + searchOrigin = Positions.offset(result[0], match[0].length)!; + } else { + searchOrigin = result[0]; + } + } + } + + public findMatching( + direction: Direction, + searchOrigin: vscode.Position, + balance = 1, + included = true, + ) { + const result = this.searchMatching(direction, searchOrigin, balance); + + if (result === undefined) { + return undefined; + } + + if (direction === Direction.Backward) { + return included ? result[0] : Positions.offset(result[0], result[1][0].length)!; + } + + return included ? Positions.offset(result[0], result[1][0].length)! : result[0]; + } + + public searchOpening( + searchOrigin: vscode.Position, + balance = -1, + ): search.Result { + return this.searchMatching(Direction.Backward, searchOrigin, balance); + } + + public searchClosing( + searchOrigin: vscode.Position, + balance = this.open.source === this.close.source ? -1 : 1, + ): search.Result { + return this.searchMatching(Direction.Forward, searchOrigin, balance); + } +} + +/** + * Returns a new `Pair`. + */ +export function pair(open: string | RegExp, close: string | RegExp): Pair { + if (typeof open === "string") { + open = new RegExp(escapeForRegExp(open), "um"); + } + if (typeof close === "string") { + close = new RegExp(escapeForRegExp(close), "um"); + } + + return new Pair(open, close); +} + +/** + * Returns the next selection enclosed in one of the given pairs. The resulting + * selection is anchored on the first closing or opening pattern encountered in + * the given direction, and its cursor will be on its matching pattern. + */ +export function surroundedBy( + pairs: readonly Pair[], + direction: Direction, + searchOrigin: vscode.Position, + open = true, + document = Context.current.document, +) { + const re = new RegExp(pairs.map((p) => `(${p.open.source})|(${p.close.source})`).join("|"), "u"), + anchorSearch = search(direction, re, searchOrigin); + + if (anchorSearch === undefined) { + return undefined; + } + + const match = anchorSearch[1], + index = match.findIndex((x, i) => i > 0 && x !== undefined) - 1, + pairIndex = (index & 0xffff_fffe) >> 1, + pair = pairs[pairIndex]; + + // Then, find the matching char of the anchor. + let anchor = anchorSearch[0], + active: vscode.Position; + + if (index & 1) { + // Index is odd + // <=> match is for closing pattern + // <=> we go backward looking for the opening pattern + const activeSearch = pair.searchOpening(anchor); + + if (activeSearch === undefined) { + return undefined; + } + + if (open) { + anchor = Positions.offset(anchor, match[0].length, document)!; + active = activeSearch[0]; + } else { + active = Positions.offset(activeSearch[0], activeSearch[1][0].length, document)!; + } + } else { + // Index is even + // <=> match is for opening pattern + // <=> we go forward looking for the closing pattern + const searchAnchor = Positions.offset(anchor, match[0].length, document)!, + activeSearch = pair.searchClosing(searchAnchor); + + if (activeSearch === undefined) { + return undefined; + } + + if (open) { + active = Positions.offset(activeSearch[0], activeSearch[1][0].length, document)!; + } else { + anchor = searchAnchor; + active = activeSearch[0]; + } + } + + return new vscode.Selection(anchor, active); +} diff --git a/src/api/search/range.ts b/src/api/search/range.ts new file mode 100644 index 0000000..662da3f --- /dev/null +++ b/src/api/search/range.ts @@ -0,0 +1,634 @@ +import * as vscode from "vscode"; + +import { CharCodes } from "../../utils/regexp"; +import { Direction } from ".."; +import { Context } from "../context"; +import { Positions } from "../positions"; +import { moveWhile } from "./move"; +import { CharSet, getCharSetFunction } from "../../utils/charset"; +import { Lines } from "../lines"; +import { Selections } from "../selections"; + +export namespace Range { + /** + * A function that, given a position, returns the start of the object to which + * the position belongs. + */ + export interface SeekStart { + (position: vscode.Position, inner: boolean, document: vscode.TextDocument): vscode.Position; + } + + /** + * A function that, given a position, returns the end of the object to which + * the position belongs. + * + * If the whole object is being saught, the start position of the object will + * also be given. + */ + export interface SeekEnd { + (position: vscode.Position, inner: boolean, + document: vscode.TextDocument, start?: vscode.Position): vscode.Position; + } + + /** + * A function that, given a position, returns the range of teh object to which + * the position belongs. + */ + export interface Seek { + (position: vscode.Position, inner: boolean, document: vscode.TextDocument): vscode.Selection; + + readonly start: SeekStart; + readonly end: SeekEnd; + } + + /** + * Returns the range of the argument at the given position. + */ + export function argument( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + ) { + return new vscode.Selection( + argument.start(position, inner, document), + argument.end(position, inner, document), + ); + } + + export namespace argument { + /** + * Returns the start position of the argument at the given position. + */ + export function start( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + ) { + return toArgumentEdge(position, inner, Direction.Backward, document); + } + + /** + * Returns the end position of the argument at the given position. + */ + export function end( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + ) { + return toArgumentEdge(position, inner, Direction.Forward, document); + } + } + + /** + * Returns the range of lines with the same indent as the line at the given + * position. + */ + export function indent( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + ) { + // When selecting a whole indent object, scanning separately to start and + // then to end will lead to wrong results like two different indentation + // levels and skipping over blank lines more than needed. We can mitigate + // this by finding the start first and then scan from there to find the end + // of indent block. + const start = indent.start(position, inner, document), + end = indent.end(start, inner, document); + + return new vscode.Selection(start, end); + } + + export namespace indent { + /** + * Returns the start position of the indent block at the given position. + */ + export function start( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + ) { + return toIndentEdge(position, inner, Direction.Backward, document); + } + + /** + * Returns the end position of the indent block at the given position. + */ + export function end( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + start?: vscode.Position, + ) { + return toIndentEdge(start ?? position, inner, Direction.Forward, document); + } + } + + /** + * Returns the range of the paragraph that wraps the given position. + */ + export function paragraph( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + ) { + let start: vscode.Position; + + if (position.line + 1 < document.lineCount + && Lines.isEmpty(position.line, document) && !Lines.isEmpty(position.line + 1, document)) { + // Special case: if current line is empty, check next line and select + // the NEXT paragraph if next line is not empty. + start = Positions.lineStart(position.line + 1); + } else { + start = toParagraphStart(position, document); + } + + const end = toParagraphEnd(start, inner, document); + + return new vscode.Selection(start, end); + } + + export namespace paragraph { + /** + * Returns the start position of the paragraph that wraps the given + * position. + */ + export function start( + position: vscode.Position, + _inner: boolean, + document = Context.current.document, + ) { + if (position.line > 0 && Lines.isEmpty(position.line, document)) { + position = Positions.lineStart(position.line - 1); // Re-anchor to the previous line. + } + + return toParagraphStart(position, document); + } + + /** + * Returns the end position of the paragraph that wraps given position. + */ + export function end( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + start?: vscode.Position, + ) { + if (start !== undefined) { + // It's much easier to check from start. + position = start; + } + + return toParagraphEnd(position, inner, document); + } + } + + /** + * Returns the range of the sentence that wraps the given position. + */ + export function sentence( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + ) { + const beforeBlank = toBeforeBlank(position, document, /* canSkipToPrevious= */ false), + start = toSentenceStart(beforeBlank, document), + end = sentence.end(start, inner, document); + + return new vscode.Selection(start, end); + } + + export namespace sentence { + /** + * Returns the start position of the sentence that wraps the given position. + */ + export function start( + position: vscode.Position, + _inner: boolean, + document = Context.current.document, + ) { + // Special case to allow jumping to the previous sentence when position is + // at current sentence start / leading blank chars. + const beforeBlank = toBeforeBlank(position, document, /* canSkipToPrevious= */ true); + + return toSentenceStart(beforeBlank, document); + } + + /** + * Returns the end position of the sentence that wraps given position. + */ + export function end( + position: vscode.Position, + inner: boolean, + document = Context.current.document, + start?: vscode.Position, + ) { + if (start !== undefined) { + // It is imposssible to determine if active is at leading or trailing or + // in-sentence blank characters by just looking ahead. Therefore, we + // search from the sentence start, which may be slightly less efficient + // but always accurate. + position = start; + } + + if (Lines.isEmpty(position.line, document)) { + // We're on an empty line which does not belong to last sentence or this + // sentence. If next line is also empty, we should just stay here. + // However, start scanning from the next line if it is not empty. + if (position.line + 1 >= document.lineCount || Lines.isEmpty(position.line + 1, document)) { + return position; + } else { + position = Positions.lineStart(position.line + 1); + } + } + + const isBlank = getCharSetFunction(CharSet.Blank, document); + + let hadLf = false; + const innerEnd = moveWhile.byCharCode.forward( + (charCode) => { + if (charCode === CharCodes.LF) { + if (hadLf) { + return false; + } + + hadLf = true; + } else { + hadLf = false; + + if (punctCharCodes.indexOf(charCode) >= 0) { + return false; + } + } + + return true; + }, + position, + document, + ); + + if (moveWhile.reachedDocumentEdge) { + return innerEnd; + } + + // If a sentence ends with two LFs in a row, then the first LF is part of + // the inner & outer sentence while the second LF should be excluded. + if (hadLf) { + if (inner) { + return Positions.previous(innerEnd, document)!; + } + + return innerEnd; + } + + if (inner) { + return innerEnd; + } + + // If a sentence ends with punct char, then any blank characters after it + // but BEFORE any line breaks belongs to the outer sentence. + let col = innerEnd.character + 1; + const text = document.lineAt(innerEnd.line).text; + + while (col < text.length && isBlank(text.charCodeAt(col))) { + col++; + } + + if (col >= text.length) { + return Positions.lineBreak(innerEnd.line, document); + } + + return new vscode.Position(innerEnd.line, col); + } + } +} + +const punctCharCodes = new Uint32Array(Array.from(".!?¡§¶¿;՞。", (ch) => ch.charCodeAt(0))); +// ^ +// I bet that's the first time you see a Greek question mark used as an actual +// Greek question mark, rather than as a "prank" semicolon. + +function toArgumentEdge( + from: vscode.Position, + inner: boolean, + direction: Direction, + document: vscode.TextDocument, +) { + const paren = direction === Direction.Backward ? CharCodes.LParen : CharCodes.RParen; + + let bbalance = 0, + pbalance = 0; + + const afterSkip = moveWhile.byCharCode( + direction, + (charCode) => { + if (charCode === paren && pbalance === 0 && bbalance === 0) { + return false; + } else if (charCode === CharCodes.LParen) { + pbalance++; + } else if (charCode === CharCodes.LBracket) { + bbalance++; + } else if (charCode === CharCodes.RParen) { + pbalance--; + } else if (charCode === CharCodes.RBracket) { + bbalance--; + } else if (pbalance !== 0 || bbalance !== 0) { + // Nop. + } else if (charCode === CharCodes.Comma) { + return false; + } + return true; + }, + from, + document, + ); + + let end: vscode.Position; + + if (moveWhile.reachedDocumentEdge) { + end = afterSkip; + } else { + const charCode = document.lineAt(afterSkip.line).text.charCodeAt(afterSkip.character); + // Make sure parens are not included in the object. Deliminator commas + // after the argument is included as outer, but ones before are NOT. + + // TODO: Kakoune seems to have more sophisticated edge cases for commas, + // e.g. outer last argument includes the comma before it, plus more edge + // cases for who owns the whitespace. Those are not implemented for now + // because they require extensive tests and mess a lot with the logic of + // selecting the whole object. + if (inner || charCode === paren || direction === Direction.Backward) { + end = Positions.offset(afterSkip, -direction, document)!; + } else { + end = afterSkip; + } + } + + if (!inner) { + return end; + } + + const isBlank = getCharSetFunction(CharSet.Blank, document); + + // Exclude any surrounding whitespaces. + return moveWhile.byCharCode(-direction, isBlank, end, document); +} + +function toIndentEdge( + from: vscode.Position, + inner: boolean, + direction: Direction, + document: vscode.TextDocument, +) { + let line = from.line, + textLine = document.lineAt(line); + + // First, scan backwards through blank lines. (Note that whitespace-only + // lines do not count -- those have a proper indentation level and should + // be treated as the inner part of the indent block.) + while (textLine.text.length === 0) { + line += direction; + + if (line < 0) { + return Positions.zero; + } + + if (line >= document.lineCount) { + return Positions.last(document); + } + + textLine = document.lineAt(line); + } + + const indent = textLine.firstNonWhitespaceCharacterIndex; + let lastNonBlankLine = line; + + for (;;) { + line += direction; + + if (line < 0) { + return Positions.zero; + } + + if (line >= document.lineCount) { + return Positions.last(document); + } + + textLine = document.lineAt(line); + + if (textLine.text.length === 0) { + continue; + } + + if (textLine.firstNonWhitespaceCharacterIndex < indent) { + const resultLine = inner ? lastNonBlankLine : line - direction; + + return direction === Direction.Backward + ? Positions.lineStart(resultLine) + : Positions.lineBreak(resultLine, document); + } + + lastNonBlankLine = line; + } +} + +function toParagraphStart( + position: vscode.Position, + document: vscode.TextDocument, +) { + let line = position.line; + + // Move past any trailing empty lines. + while (line >= 0 && Lines.isEmpty(line, document)) { + line--; + } + + if (line <= 0) { + return Positions.zero; + } + + // Then move to the start of the paragraph (non-empty lines). + while (line > 0 && !Lines.isEmpty(line - 1, document)) { + line--; + } + + return Positions.lineStart(line); +} + +function toParagraphEnd( + position: vscode.Position, + inner: boolean, + document: vscode.TextDocument, +) { + let line = position.line; + + // Move to the end of the paragraph (non-empty lines) + while (line < document.lineCount && !Lines.isEmpty(line, document)) { + line++; + } + + if (line >= document.lineCount) { + return Positions.last(document); + } + + if (inner) { + if (line > 0) { + line--; + } + + return Positions.lineBreak(line, document); + } + + // Then move to the last trailing empty line. + while (line + 1 < document.lineCount && Lines.isEmpty(line + 1, document)) { + line++; + } + + return Positions.lineBreak(line, document); +} + +function toBeforeBlank( + position: vscode.Position, + document: vscode.TextDocument, + canSkipToPrevious: boolean, +) { + const isBlank = getCharSetFunction(CharSet.Blank, document); + + let jumpedOverBlankLine = false, + hadLf = true; + + const beforeBlank = moveWhile.byCharCode.backward( + (charCode) => { + if (charCode === CharCodes.LF) { + if (hadLf) { + jumpedOverBlankLine = true; + + return canSkipToPrevious; + } + + hadLf = true; + + return true; + } else { + hadLf = false; + + return isBlank(charCode); + } + }, + position, + document, + ); + + if (moveWhile.reachedDocumentEdge) { + return position; + } + + const beforeBlankChar = document.lineAt(beforeBlank.line).text.charCodeAt(beforeBlank.character), + hitPunctChar = punctCharCodes.includes(beforeBlankChar); + + if (jumpedOverBlankLine && (!canSkipToPrevious || !hitPunctChar)) { + // We jumped over blank lines but didn't hit a punct char. Don't accept. + return position; + } + + if (!hitPunctChar || canSkipToPrevious || position.line === beforeBlank.line) { + return beforeBlank; + } + + // Example below: we started from '|' and found the '.'. + // foo. + // | bar + // In this case, technically we started from the second sentence + // and reached the first sentence. This is not permitted when when + // allowSkipToPrevious is false, so let's go back. + return position; +} + +function toSentenceStart( + position: vscode.Position, + document: vscode.TextDocument, +) { + const isBlank = getCharSetFunction(CharSet.Blank, document); + let originLineText = document.lineAt(position.line).text; + + if (originLineText.length === 0 && position.line + 1 >= document.lineCount) { + if (position.line === 0) { + // There is only one line and that line is empty. What a life. + return Positions.zero; + } + + // Special case: If at the last line, search from the previous line. + originLineText = document.lineAt(position.line - 1).text; + position = new vscode.Position(position.line - 1, originLineText.length); + } + + if (originLineText.length === 0) { + // This line is empty. Just go to the first non-blank char on next line. + const nextLineText = document.lineAt(position.line + 1).text; + let col = 0; + + while (col < nextLineText.length && isBlank(nextLineText.charCodeAt(col))) { + col++; + } + + return new vscode.Position(position.line + 1, col); + } + + let first = true, + hadLf = false; + + const afterSkip = moveWhile.byCharCode.backward( + (charCode) => { + if (charCode === CharCodes.LF) { + first = false; + + if (hadLf) { + return false; + } + + hadLf = true; + } else { + hadLf = false; + + if (first) { + // Don't need to check if first character encountered is punct -- + // that may be the current sentence end. + first = false; + + return true; + } + + if (punctCharCodes.indexOf(charCode) >= 0) { + return false; + } + } + + return true; + }, + position, + document, + ); + + // If we hit two LFs or document start, the current sentence starts at the + // first non-blank character after that. + if (hadLf || moveWhile.reachedDocumentEdge) { + const start = moveWhile.byCharCode.forward(isBlank, afterSkip, document); + + if (moveWhile.reachedDocumentEdge) { + return Positions.zero; + } + + return start; + } + + // If we hit a punct char, then the current sentence starts on the first + // non-blank character on the same line, or the line break. + let col = afterSkip.character; + const text = document.lineAt(afterSkip.line).text; + + while (col < text.length && isBlank(text.charCodeAt(col))) { + col++; + } + + return new vscode.Position(afterSkip.line, col); +} diff --git a/src/api/search/word.ts b/src/api/search/word.ts new file mode 100644 index 0000000..c1871e9 --- /dev/null +++ b/src/api/search/word.ts @@ -0,0 +1,130 @@ +import * as vscode from "vscode"; + +import { Direction, skipEmptyLines } from ".."; +import { SelectionBehavior } from "../../state/modes"; +import { CharSet, getCharSetFunction } from "../../utils/charset"; +import { Context } from "../context"; + +const enum WordCategory { + Word, + Blank, + Punctuation, +} + +function categorize( + charCode: number, + isBlank: (charCode: number) => boolean, + isWord: (charCode: number) => boolean, +) { + return isWord(charCode) + ? WordCategory.Word + : charCode === 0 || isBlank(charCode) ? WordCategory.Blank : WordCategory.Punctuation; +} + +/** + * Starting at the given `origin` position, seeks the next (or previous) word. + * Returns a selection wrapping the next word. + */ +export function wordBoundary( + direction: Direction, + origin: vscode.Position, + stopAtEnd: boolean, + wordCharset: CharSet, + context = Context.current, +) { + let anchor = undefined, + active = origin; + + const document = context.document, + text = document.lineAt(active.line).text, + lineEndCol = context.selectionBehavior === SelectionBehavior.Caret + ? text.length + : text.length - 1; + + const isWord = getCharSetFunction(wordCharset, document), + isBlank = getCharSetFunction(CharSet.Blank, document), + isPunctuation = getCharSetFunction(CharSet.Punctuation, document); + + // Starting from active, try to seek to the word start. + const isAtLineBoundary = direction === Direction.Forward + ? (active.character >= lineEndCol) + : (active.character === 0 || active.character === 1); + + if (isAtLineBoundary) { + const afterEmptyLines = skipEmptyLines(direction, active.line + direction, document); + + if (skipEmptyLines.reachedDocumentEdge) { + return undefined; + } + + anchor = afterEmptyLines; + } else { + let shouldSkip: boolean; + + if (context.selectionBehavior === SelectionBehavior.Character) { + // Skip current character if it is at boundary. + // (e.g. "ab[c] " =>`w`) + const col = active.character - +(direction === Direction.Backward), + characterCategory = categorize(text.charCodeAt(col), isBlank, isWord), + nextCharacterCategory = categorize(text.charCodeAt(col + direction), isBlank, isWord); + + shouldSkip = characterCategory !== nextCharacterCategory; + + if (shouldSkip && stopAtEnd === (direction === Direction.Forward) + && (characterCategory === WordCategory.Blank)) { + shouldSkip = false; + } + } else { + shouldSkip = false; + } + + anchor = shouldSkip ? new vscode.Position(active.line, active.character + direction) : active; + } + + active = anchor; + + // Scan within the current line until the word ends. + const curLineText = document.lineAt(active).text; + let nextCol = active.character; // The next character to be tested. + + if (direction === Direction.Backward) { + nextCol--; + } + + if (stopAtEnd === (direction === Direction.Forward)) { + // Select the whitespace before word, if any. + while (nextCol >= 0 && nextCol < curLineText.length + && isBlank(curLineText.charCodeAt(nextCol))) { + nextCol += direction; + } + } + + if (nextCol >= 0 && nextCol < curLineText.length) { + const startCharCode = curLineText.charCodeAt(nextCol), + isSameCategory = isWord(startCharCode) ? isWord : isPunctuation; + + while (nextCol >= 0 && nextCol < curLineText.length + && isSameCategory(curLineText.charCodeAt(nextCol))) { + nextCol += direction; + } + } + + if (stopAtEnd === (direction === Direction.Backward)) { + // Select the whitespace after word, if any. + while (nextCol >= 0 && nextCol < curLineText.length + && isBlank(curLineText.charCodeAt(nextCol))) { + nextCol += direction; + } + } + + if (direction === Direction.Backward) { + // If we reach here, nextCol must be the first character we encounter + // that does not belong to the current word (or -1 / line break). + // Exclude it. + active = new vscode.Position(active.line, nextCol + 1); + } else { + active = new vscode.Position(active.line, nextCol); + } + + return new vscode.Selection(anchor, active); +} diff --git a/src/api/selections.ts b/src/api/selections.ts new file mode 100644 index 0000000..b35c4e6 --- /dev/null +++ b/src/api/selections.ts @@ -0,0 +1,1892 @@ +import * as vscode from "vscode"; + +import { Direction, Shift } from "."; +import { Context } from "./context"; +import { NotASelectionError } from "./errors"; +import { Positions } from "./positions"; +import { execRange, splitRange } from "../utils/regexp"; +import { Lines } from "./lines"; +import { SelectionBehavior } from "../state/modes"; + +/** + * Sets the selections of the given editor. + * + * @param editor A `vscode.TextEditor` whose selections will be updated, or + * `undefined` to update the selections of the active text editor. + * + * ### Example + * + * ```js + * const start = new vscode.Position(0, 6), + * end = new vscode.Position(0, 11); + * + * setSelections([new vscode.Selection(start, end)]); + * ``` + * + * Before: + * ``` + * hello world + * ^ 0 + * ``` + * + * After: + * ``` + * hello world + * ^^^^^ 0 + * ``` + * + * ### Example + * ```js + * assert.throws(() => setSelections([]), EmptySelectionsError); + * assert.throws(() => setSelections([1 as any]), NotASelectionError); + * ``` + */ +export function setSelections(selections: readonly vscode.Selection[]) { + NotASelectionError.throwIfNotASelectionArray(selections); + + Context.current.selections = selections; + Selections.reveal(selections[0]); + + return selections; +} + +/** + * Removes selections that do not match the given predicate. + * + * @param selections The `vscode.Selection` array to filter from, or `undefined` + * to filter the selections of the active text editor. + * + * ### Example + * + * ```js + * const atChar = (character: number) => new vscode.Position(0, character); + * + * assert.deepStrictEqual( + * filterSelections((text) => !isNaN(+text)), + * [new vscode.Selection(atChar(4), atChar(7))], + * ); + * ``` + * + * With: + * ``` + * foo 123 + * ^^^ 0 + * ^^^ 1 + * ``` + */ +export function filterSelections( + predicate: filterSelections.Predicate, + selections?: readonly vscode.Selection[], +): vscode.Selection[]; + +/** + * Removes selections that do not match the given async predicate. + * + * @param selections The `vscode.Selection` array to filter from, or `undefined` + * to filter the selections of the active text editor. + * + * ### Example + * + * ```js + * const atChar = (character: number) => new vscode.Position(0, character); + * + * assert.deepStrictEqual( + * await filterSelections(async (text) => !isNaN(+text)), + * [new vscode.Selection(atChar(4), atChar(7))], + * ); + * ``` + * + * With: + * ``` + * foo 123 + * ^^^ 0 + * ^^^ 1 + * ``` + */ +export function filterSelections( + predicate: filterSelections.Predicate>, + selections?: readonly vscode.Selection[], +): Thenable; + +export function filterSelections( + predicate: filterSelections.Predicate | filterSelections.Predicate>, + selections?: readonly vscode.Selection[], +) { + return filterSelections.byIndex( + (i, selection, document) => predicate(document.getText(selection), selection, i) as any, + selections, + ) as any; +} + +export namespace filterSelections { + /** + * A predicate passed to `filterSelections`. + */ + export interface Predicate> { + (text: string, selection: vscode.Selection, index: number): T; + } + + /** + * A predicate passed to `filterSelections.byIndex`. + */ + export interface ByIndexPredicate> { + (index: number, selection: vscode.Selection, document: vscode.TextDocument): T; + } + + /** + * Removes selections that do not match the given predicate. + * + * @param selections The `vscode.Selection` array to filter from, or + * `undefined` to filter the selections of the active text editor. + */ + export function byIndex( + predicate: ByIndexPredicate, + selections?: readonly vscode.Selection[], + ): vscode.Selection[]; + + /** + * Removes selections that do not match the given async predicate. + * + * @param selections The `vscode.Selection` array to filter from, or + * `undefined` to filter the selections of the active text editor. + */ + export function byIndex( + predicate: ByIndexPredicate>, + selections?: readonly vscode.Selection[], + ): Thenable; + + export function byIndex( + predicate: ByIndexPredicate | ByIndexPredicate>, + selections?: readonly vscode.Selection[], + ) { + const context = Context.current, + document = context.document; + + if (selections === undefined) { + selections = context.selections; + } + + const firstSelection = selections[0], + firstResult = predicate(0, firstSelection, document); + + if (typeof firstResult === "boolean") { + if (selections.length === 1) { + return firstResult ? [firstResult] : []; + } + + const resultingSelections = firstResult ? [firstSelection] : []; + + for (let i = 1; i < selections.length; i++) { + const selection = selections[i]; + + if (predicate(i, selection, document) as boolean) { + resultingSelections.push(selection); + } + } + + return resultingSelections; + } else { + if (selections.length === 1) { + return context.then(firstResult, (value) => value ? [firstSelection] : []); + } + + const promises = [firstResult]; + + for (let i = 1; i < selections.length; i++) { + const selection = selections[i]; + + promises.push(predicate(i, selection, document) as Thenable); + } + + const savedSelections = selections.slice(); // In case the original + // selections are mutated. + + return context.then(Promise.all(promises), (results) => { + const resultingSelections = []; + + for (let i = 0; i < results.length; i++) { + if (results[i]) { + resultingSelections.push(savedSelections[i]); + } + } + + return resultingSelections; + }); + } + } +} + +/** + * Applies a function to all the given selections, and returns the array of all + * of its non-`undefined` results. + * + * @param selections The `vscode.Selection` array to map from, or `undefined` + * to map the selections of the active text editor. + * + * ### Example + * + * ```js + * assert.deepStrictEqual( + * mapSelections((text) => isNaN(+text) ? undefined : +text), + * [123], + * ); + * ``` + * + * With: + * ``` + * foo 123 + * ^^^ 0 + * ^^^ 1 + * ``` + */ +export function mapSelections( + f: mapSelections.Mapper, + selections?: readonly vscode.Selection[], +): T[]; + +/** + * Applies an async function to all the given selections, and returns the array + * of all of its non-`undefined` results. + * + * @param selections The `vscode.Selection` array to map from, or `undefined` + * to map the selections of the active text editor. + * + * ### Example + * + * ```js + * assert.deepStrictEqual( + * await mapSelections(async (text) => isNaN(+text) ? undefined : +text), + * [123], + * ); + * ``` + * + * With: + * ``` + * foo 123 + * ^^^ 0 + * ^^^ 1 + * ``` + */ +export function mapSelections( + f: mapSelections.Mapper>, + selections?: readonly vscode.Selection[], +): Thenable; + +export function mapSelections( + f: mapSelections.Mapper | mapSelections.Mapper>, + selections?: readonly vscode.Selection[], +) { + return mapSelections.byIndex( + (i, selection, document) => f(document.getText(selection), selection, i), + selections, + ) as any; +} + +export namespace mapSelections { + /** + * A mapper function passed to `mapSelections`. + */ + export interface Mapper { + (text: string, selection: vscode.Selection, index: number): T; + } + + /** + * A mapper function passed to `mapSelections.byIndex`. + */ + export interface ByIndexMapper { + (index: number, selection: vscode.Selection, document: vscode.TextDocument): T | undefined; + } + + /** + * Applies a function to all the given selections, and returns the array of + * all of its non-`undefined` results. + * + * @param selections The `vscode.Selection` array to map from, or `undefined` + * to map the selections of the active text editor. + */ + export function byIndex( + f: ByIndexMapper, + selections?: readonly vscode.Selection[], + ): T[]; + + /** + * Applies an async function to all the given selections, and returns the + * array of all of its non-`undefined` results. + * + * @param selections The `vscode.Selection` array to map from, or `undefined` + * to map the selections of the active text editor. + */ + export function byIndex( + f: ByIndexMapper>, + selections?: readonly vscode.Selection[], + ): Thenable; + + export function byIndex( + f: ByIndexMapper | ByIndexMapper>, + selections?: readonly vscode.Selection[], + ) { + const context = Context.current, + document = context.document; + + if (selections === undefined) { + selections = context.selections; + } + + const firstSelection = selections[0], + firstResult = f(0, firstSelection, document); + + if (firstResult === undefined || typeof (firstResult as Thenable)?.then !== "function") { + const results = firstResult !== undefined ? [firstResult as T] : []; + + for (let i = 1; i < selections.length; i++) { + const selection = selections[i], + value = f(i, selection, document) as T | undefined; + + if (value !== undefined) { + results.push(value); + } + } + + return results; + } else { + if (selections.length === 1) { + return context.then(firstResult as Thenable, (result) => { + return result !== undefined ? [result] : []; + }); + } + + const promises = [firstResult as Thenable]; + + for (let i = 1; i < selections.length; i++) { + const selection = selections[i], + promise = f(i, selection, document) as Thenable; + + promises.push(promise); + } + + return context.then(Promise.all(promises), (results) => { + const filteredResults = []; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + + if (result !== undefined) { + filteredResults.push(result); + } + } + + return filteredResults; + }); + } + } +} + +/** + * Sets the selections of the current editor after transforming them according + * to the given function. + * + * ### Example + * + * ```js + * const reverseUnlessNumber = (text: string, sel: vscode.Selection) => + * isNaN(+text) ? new vscode.Selection(sel.active, sel.anchor) : undefined; + * + * updateSelections(reverseUnlessNumber); + * ``` + * + * Before: + * ``` + * foo 123 + * ^^^ 0 + * ^^^ 1 + * ``` + * + * After: + * ``` + * foo 123 + * |^^ 0 + * ``` + * + * ### Example + * + * ```js + * assert.throws(() => updateSelections(() => undefined), EmptySelectionsError); + * ``` + * + * With: + * ``` + * foo 123 + * ^^^ 0 + * ``` + */ +export function updateSelections( + f: mapSelections.Mapper, +): vscode.Selection[]; + +/** + * Sets the selections of the current editor after transforming them according + * to the given async function. + * + * ### Example + * + * ```js + * const reverseIfNumber = async (text: string, sel: vscode.Selection) => + * !isNaN(+text) ? new vscode.Selection(sel.active, sel.anchor) : undefined; + * + * await updateSelections(reverseIfNumber); + * ``` + * + * Before: + * ``` + * foo 123 + * ^^^ 0 + * ^^^ 1 + * ``` + * + * After: + * ``` + * foo 123 + * |^^ 0 + * ``` + */ +export function updateSelections( + f: mapSelections.Mapper>, +): Thenable; + +export function updateSelections( + f: mapSelections.Mapper + | mapSelections.Mapper>, +): any { + const selections = mapSelections(f as any); + + if (Array.isArray(selections)) { + return setSelections(selections); + } + + return (selections as Thenable).then(setSelections); +} + +function mapFallbackSelections(values: (vscode.Selection | readonly [vscode.Selection])[]) { + let selectionsCount = 0, + fallbackSelectionsCount = 0; + + for (const value of values) { + if (Array.isArray(value)) { + fallbackSelectionsCount++; + } else if (value !== undefined) { + selectionsCount++; + } + } + + if (selectionsCount > 0) { + const selections: vscode.Selection[] = []; + + for (const value of values) { + if (value !== undefined && !Array.isArray(value)) { + selections.push(value as vscode.Selection); + } + } + + return selections; + } + + if (fallbackSelectionsCount > 0) { + const selections: vscode.Selection[] = []; + + for (const value of values) { + if (Array.isArray(value)) { + selections.push(value[0]); + } + } + + return selections; + } + + return []; +} + +export namespace updateSelections { + /** + * Sets the selections of the current editor after transforming them according + * to the given function. + */ + export function byIndex( + f: mapSelections.ByIndexMapper, + ): vscode.Selection[]; + + /** + * Sets the selections of the current editor after transforming them according + * to the given async function. + */ + export function byIndex( + f: mapSelections.ByIndexMapper>, + ): Thenable; + + export function byIndex( + f: mapSelections.ByIndexMapper + | mapSelections.ByIndexMapper>, + ): any { + const selections = mapSelections.byIndex(f as any); + + if (Array.isArray(selections)) { + return setSelections(selections); + } + + return (selections as Thenable).then(setSelections); + } + + /** + * A possible return value for a function passed to `withFallback`. An + * array with a single selection corresponds to a fallback selection. + */ + export type SelectionOrFallback = vscode.Selection | readonly [vscode.Selection] | undefined; + + /** + * Same as `updateSelections`, but additionally lets `f` return a fallback + * selection. If no selection remains after the end of the update, fallback + * selections will be used instead. + */ + export function withFallback>( + f: mapSelections.Mapper, + ): T extends Thenable ? Thenable : vscode.Selection[] { + const selections = mapSelections(f as any); + + if (Array.isArray(selections)) { + return setSelections(mapFallbackSelections(selections)) as any; + } + + return (selections as Thenable<(vscode.Selection | readonly [vscode.Selection])[]>) + .then((values) => setSelections(mapFallbackSelections(values))) as any; + } + + export namespace withFallback { + /** + * Same as `withFallback`, but does not pass the text of each selection. + */ + export function byIndex>( + f: mapSelections.ByIndexMapper, + ): T extends Thenable ? Thenable : vscode.Selection[] { + const selections = mapSelections.byIndex(f as any); + + if (Array.isArray(selections)) { + return setSelections(mapFallbackSelections(selections)) as any; + } + + return (selections as Thenable<(vscode.Selection | readonly [vscode.Selection])[]>) + .then((values) => setSelections(mapFallbackSelections(values))) as any; + } + } +} + +/** + * Rotates selections in the given direction. + * + * ### Example + * + * ```js + * setSelections(rotateSelections(1)); + * ``` + * + * Before: + * ``` + * foo bar baz + * ^^^ 0 ^^^ 2 + * ^^^ 1 + * ``` + * + * After: + * ``` + * foo bar baz + * ^^^ 1 ^^^ 0 + * ^^^ 2 + * ``` + * + * ### Example + * + * ```js + * setSelections(rotateSelections(-1)); + * ``` + * + * Before: + * ``` + * foo bar baz + * ^^^ 0 ^^^ 2 + * ^^^ 1 + * ``` + * + * After: + * ``` + * foo bar baz + * ^^^ 2 ^^^ 1 + * ^^^ 0 + * ``` + */ +export function rotateSelections( + by: Direction | number, + selections: readonly vscode.Selection[] = Context.current.selections, +) { + const len = selections.length; + + // Handle negative values for `by`: + by = (by % len) + len; + + if (by === len) { + return selections.slice(); + } + + const newSelections = new Array(selections.length); + + for (let i = 0; i < len; i++) { + newSelections[(i + by) % len] = selections[i]; + } + + return newSelections; +} + +/** + * Returns an array containing all the unique lines included in the given or + * active selections. Though the resulting array is not sorted, it is likely + * that consecutive lines will be consecutive in the array as well. + * + * ### Example + * + * ```js + * expect(selectionsLines(), "to only contain", 0, 1, 3, 4, 5, 6); + * ``` + * + * With: + * ``` + * ab + * ^^ 0 + * cd + * ^ 1 + * ef + * gh + * ^ 2 + * ^ 3 + * ij + * ^ 3 + * kl + * | 4 + * mn + * ^^ 5 + * op + * ``` + */ +export function selectionsLines( + selections: readonly vscode.Selection[] = Context.current.selections, +) { + const lines: number[] = []; + + for (const selection of selections) { + const startLine = selection.start.line, + endLine = Selections.endLine(selection); + + // The first and last lines of the selection may contain other selections, + // so we check for duplicates with them. However, the intermediate + // lines are known to belong to one selection only, so there's no need + // for that with them. + if (lines.indexOf(startLine) === -1) { + lines.push(startLine); + } + + for (let i = startLine + 1; i < endLine; i++) { + lines.push(i); + } + + if (endLine !== startLine && lines.indexOf(endLine) === -1) { + lines.push(endLine); + } + } + + return lines; +} + +/** + * Returns the selections obtained by splitting the contents of all the given + * selections using the given RegExp. + */ +export function splitSelections(re: RegExp, selections = Context.current.selections) { + const document = Context.current.document; + + return Selections.map((text, selection) => { + const offset = document.offsetAt(selection.start); + + return splitRange(text, re).map(([start, end]) => + Selections.fromStartEnd(offset + start, offset + end, selection.isReversed), + ); + }, selections).flat(); +} + +/** + * Returns the selections obtained by finding all the matches within the given + * selections using the given RegExp. + */ +export function selectWithinSelections(re: RegExp, selections = Context.current.selections) { + if (!re.global) { + re = new RegExp(re.source, re.flags + "g"); + } + + const document = Context.current.document; + + return Selections.map((text, selection) => { + const offset = document.offsetAt(selection.start); + + return execRange(text, re).map(([start, end]) => + Selections.fromStartEnd(offset + start, offset + end, selection.isReversed), + ); + }, selections).flat(); +} + +/** + * Reveals selections in the current editor. + */ +export function revealSelections(selection?: vscode.Selection) { + const editor = Context.current.editor; + + editor.revealRange(selection ?? (editor as vscode.TextEditor).selection); +} + +/** + * Given an array of selections, returns an array of selections where all + * overlapping selections have been merged. + * + * ### Example + * + * Equal selections. + * + * ```ts + * expect(mergeOverlappingSelections(Selections.current), "to equal", [Selections.current[0]]); + * ``` + * + * With: + * ``` + * abcd + * ^^ 0 + * ^^ 1 + * ``` + * + * ### Example + * + * Equal empty selections. + * + * ```ts + * expect(mergeOverlappingSelections(Selections.current), "to equal", [Selections.current[0]]); + * ``` + * + * With: + * ``` + * abcd + * | 0 + * | 1 + * ``` + * + * ### Example + * + * Overlapping selections. + * + * ```ts + * expect(mergeOverlappingSelections(Selections.current), "to satisfy", [ + * expect.it("to start at coords", 0, 0).and("to end at coords", 0, 4), + * ]); + * ``` + * + * With: + * ``` + * abcd + * ^^^ 0 + * ^^^ 1 + * ``` + * + * ### Example + * + * Consecutive selections. + * + * ```ts + * expect(Selections.mergeOverlapping(Selections.current), "to equal", Selections.current); + * + * expect(Selections.mergeConsecutive(Selections.current), "to satisfy", [ + * expect.it("to start at coords", 0, 0).and("to end at coords", 0, 4), + * ]); + * ``` + * + * With: + * ``` + * abcd + * ^^ 0 + * ^^ 1 + * ``` + * + * ### Example + * + * Consecutive selections (reversed). + * + * ```ts + * expect(Selections.mergeOverlapping(Selections.current), "to equal", Selections.current); + * + * expect(Selections.mergeConsecutive(Selections.current), "to satisfy", [ + * expect.it("to start at coords", 0, 0).and("to end at coords", 0, 4), + * ]); + * ``` + * + * With: + * ``` + * abcd + * ^^ 1 + * ^^ 0 + * ``` + */ +export function mergeOverlappingSelections( + selections: readonly vscode.Selection[], + alsoMergeConsecutiveSelections = false, +) { + const len = selections.length, + ignoreSelections = new Uint8Array(selections.length); + let newSelections: vscode.Selection[] | undefined; + + for (let i = 0; i < len; i++) { + if (ignoreSelections[i] === 1) { + continue; + } + + const a = selections[i]; + let aStart = a.start, + aEnd = a.end, + aIsEmpty = aStart.isEqual(aEnd), + changed = false; + + for (let j = i + 1; j < len; j++) { + if (ignoreSelections[j] === 1) { + continue; + } + + const b = selections[j], + bStart = b.start, + bEnd = b.end; + + if (aIsEmpty) { + if (bStart.isEqual(bEnd)) { + if (bStart.isEqual(aStart)) { + // A and B are two equal empty selections, and we can keep A. + ignoreSelections[j] = 1; + changed = true; + } else { + // A and B are two different empty selections, we don't change + // anything. + } + + continue; + } + + if (bStart.isBeforeOrEqual(aStart) && bEnd.isAfterOrEqual(bStart)) { + // The empty selection A is included in B. + aStart = bStart; + aEnd = bEnd; + aIsEmpty = false; + changed = true; + ignoreSelections[j] = 1; + + continue; + } + + // The empty selection A is strictly before or after B. + continue; + } + + if (aStart.isAfterOrEqual(bStart) + && (aStart.isBefore(bEnd) || (alsoMergeConsecutiveSelections && aStart.isEqual(bEnd)))) { + // Selection A starts within selection B... + if (aEnd.isBeforeOrEqual(bEnd)) { + // ... and ends within selection B (it is included in selection B). + aStart = b.start; + aEnd = b.end; + } else { + // ... and ends after selection B. + if (aStart.isEqual(bStart)) { + // B is included in A: avoid creating a new selection needlessly. + ignoreSelections[j] = 1; + newSelections ??= selections.slice(0, i); + continue; + } + aStart = bStart; + } + } else if ((aEnd.isAfter(bStart) || (alsoMergeConsecutiveSelections && aEnd.isEqual(bStart))) + && aEnd.isBeforeOrEqual(bEnd)) { + // Selection A ends within selection B. Furthermore, we know that + // selection A does not start within selection B, so it starts before + // selection B. + aEnd = bEnd; + } else { + // Selection A neither starts nor ends in selection B, so there is no + // overlap. + continue; + } + + // B is NOT included in A; we must look at selections we previously saw + // again since they may now overlap with the new selection we will create. + changed = true; + ignoreSelections[j] = 1; + + j = i; // `j++` above will set `j` to `i + 1`. + } + + if (changed) { + // Selections have changed: make sure the `newSelections` are initialized + // and push the new selection. + if (newSelections === undefined) { + newSelections = selections.slice(0, i); + } + + newSelections.push(Selections.fromStartEnd(aStart, aEnd, a.isReversed)); + } else if (newSelections !== undefined) { + // Selection did not change, but a previous selection did; push existing + // selection to new array. + newSelections.push(a); + } else { + // Selections have not changed. Just keep going. + } + } + + return newSelections !== undefined ? newSelections : selections; +} + +/** + * Operations on `vscode.Selection`s. + */ +export namespace Selections { + export const filter = filterSelections, + lines = selectionsLines, + map = mapSelections, + reveal = revealSelections, + rotate = rotateSelections, + selectWithin = selectWithinSelections, + set = setSelections, + split = splitSelections, + update = updateSelections, + mergeOverlapping = mergeOverlappingSelections, + mergeConsecutive = (selections: readonly vscode.Selection[]) => + mergeOverlappingSelections(selections, /* alsoMergeConsecutiveSelections= */ true); + + export declare const current: readonly vscode.Selection[]; + + Object.defineProperty(Selections, "current", { + get() { + return Context.current.selections; + }, + }); + + /** + * Returns a selection spanning the entire buffer. + */ + export function wholeBuffer(document = Context.current.document) { + return new vscode.Selection(Positions.zero, Positions.last(document)); + } + + /** + * Returns the active position (or cursor) of a selection. + */ + export function active(selection: vscode.Selection) { + return selection.active; + } + + /** + * Returns the anchor position of a selection. + */ + export function anchor(selection: vscode.Selection) { + return selection.anchor; + } + + /** + * Returns the start position of a selection. + */ + export function start(selection: vscode.Range) { + return selection.start; + } + + /** + * Returns the end position of a selection. + */ + export function end(selection: vscode.Range) { + return selection.end; + } + + /** + * Returns the given selection if it faces forward (`active >= anchor`), or + * the reverse of the given selection otherwise. + */ + export function forward(selection: vscode.Selection) { + const active = selection.active, + anchor = selection.anchor; + + return active.isAfterOrEqual(anchor) ? selection : new vscode.Selection(active, anchor); + } + + /** + * Returns the given selection if it faces backward (`active <= anchor`), or + * the reverse of the given selection otherwise. + */ + export function backward(selection: vscode.Selection) { + const active = selection.active, + anchor = selection.anchor; + + return active.isBeforeOrEqual(anchor) ? selection : new vscode.Selection(active, anchor); + } + + /** + * Returns a new empty selection starting and ending at the given position. + */ + export function empty(position: vscode.Position): vscode.Selection; + + /** + * Returns a new empty selection starting and ending at the given line and + * character. + */ + export function empty(line: number, character: number): vscode.Selection; + + export function empty(positionOrLine: vscode.Position | number, character?: number) { + if (typeof positionOrLine === "number") { + positionOrLine = new vscode.Position(positionOrLine, character!); + } + + return new vscode.Selection(positionOrLine, positionOrLine); + } + + /** + * Returns whether the two given ranges overlap. + */ + export function overlap(a: vscode.Range, b: vscode.Range) { + const aStart = a.start, + aEnd = a.end, + bStart = b.start, + bEnd = b.end; + + return !(aStart.line < bStart.line + || (aEnd.line === bEnd.line && aEnd.character < bStart.character)) + && !(bStart.line < aStart.line + || (bEnd.line === aEnd.line && bEnd.character < aStart.character)); + } + + /** + * Returns the line of the end of the given selection. If the selection ends + * at the first character of a line and is not empty, this is equal to + * `end.line - 1`. Otherwise, this is `end.line`. + */ + export function endLine(selection: vscode.Selection | vscode.Range) { + const startLine = selection.start.line, + end = selection.end, + endLine = end.line, + endCharacter = end.character; + + if (startLine !== endLine && endCharacter === 0) { + // If the selection ends after a line break, do not consider the next line + // selected. This is because a selection has to end on the very first + // caret position of the next line in order to select the last line break. + // For example, `vscode.TextLine.rangeIncludingLineBreak` does this: + // https://github.com/microsoft/vscode/blob/c8b27b9db6afc26cf82cf07a9653c89cdd930f6a/src/vs/workbench/api/common/extHostDocumentData.ts#L273 + return endLine - 1; + } + + return endLine; + } + + /** + * Returns the character of the end of the given selection. If the selection + * ends at the first character of a line and is not empty, this is equal to + * the length of the previous line plus one. Otherwise, this is + * `end.character`. + * + * @see endLine + */ + export function endCharacter( + selection: vscode.Selection | vscode.Range, + document?: vscode.TextDocument, + ) { + const startLine = selection.start.line, + end = selection.end, + endLine = end.line, + endCharacter = end.character; + + if (startLine !== endLine && endCharacter === 0) { + return (document ?? Context.current.document).lineAt(endLine - 1).text.length + 1; + } + + return endCharacter; + } + + /** + * Returns the end position of the given selection. If the selection ends at + * the first character of a line and is not empty, this is equal to the + * position at the end of the previous line. Otherwise, this is `end`. + */ + export function endPosition( + selection: vscode.Selection | vscode.Range, + document?: vscode.TextDocument, + ) { + const line = endLine(selection); + + if (line !== selection.end.line) { + return new vscode.Position( + line, + (document ?? Context.current.document).lineAt(line).text.length, + ); + } + + return selection.end; + } + + /** + * Returns the line of the active position of the given selection. If the + * selection faces forward (the active position is the end of the selection), + * returns `endLine(selection)`. Otherwise, returns `active.line`. + */ + export function activeLine(selection: vscode.Selection) { + if (selection.isReversed) { + return selection.active.line; + } + + return endLine(selection); + } + + /** + * Returns the character of the active position of the given selection. + * + * @see activeLine + */ + export function activeCharacter(selection: vscode.Selection, document?: vscode.TextDocument) { + if (selection.isReversed) { + return selection.active.character; + } + + return endCharacter(selection, document); + } + + /** + * Returns the position of the active position of the given selection. + */ + export function activePosition(selection: vscode.Selection, document?: vscode.TextDocument) { + if (selection.isReversed) { + return selection.active; + } + + return endPosition(selection, document); + } + + /** + * Returns whether the selection spans a single line. This differs from + * `selection.isSingleLine` because it also handles cases where the selection + * wraps an entire line (its end position is on the first character of the + * next line). + */ + export function isSingleLine(selection: vscode.Selection) { + return selection.start.line === endLine(selection); + } + + /** + * Returns whether the given selection has length `1`. + */ + export function isSingleCharacter( + selection: vscode.Selection | vscode.Range, + document = Context.current.document, + ) { + const start = selection.start, + end = selection.end; + + if (start.line === end.line) { + return start.character === end.character - 1; + } + + if (start.line === end.line - 1) { + return end.character === 0 && document.lineAt(start.line).text.length === start.character; + } + + return false; + } + + /** + * Returns whether the given selection has length `1` and corresponds to an + * empty selection extended by one character by `fromCharacterMode`. + */ + export function isNonDirectional(selection: vscode.Selection, context = Context.current) { + return context.selectionBehavior === SelectionBehavior.Character + && !selection.isReversed + && isSingleCharacter(selection, context.document); + } + + /** + * The position from which a seek operation should start. This is equivalent + * to `selection.active` except when the selection is non-directional, in + * which case this is whatever position is **furthest** from the given + * direction (in order to include the current character in the search). + * + * A position other than active (typically, the `anchor`) can be specified to + * seek from that position. + */ + export function seekFrom( + selection: vscode.Selection, + direction: Direction, + position = selection.active, + context = Context.current, + ) { + if (context.selectionBehavior === SelectionBehavior.Character) { + const doc = context.document; + + return direction === Direction.Forward + ? (position === selection.start ? position : Positions.previous(position, doc) ?? position) + : (position === selection.end ? position : Positions.next(position, doc) ?? position); + } + + return position; + } + + /** + * Returns the start position of the active character of the selection. + * + * If the current character behavior is `Caret`, this is `selection.active`. + */ + export function activeStart(selection: vscode.Selection, context = Context.current) { + const active = selection.active; + + if (context.selectionBehavior !== SelectionBehavior.Character) { + return active; + } + + const start = selection.start; + + if (isSingleCharacter(selection, context.document)) { + return start; + } + + return active === start ? start : Positions.previous(active, context.document)!; + } + + /** + * Returns the end position of the active character of the selection. + * + * If the current character behavior is `Caret`, this is `selection.active`. + */ + export function activeEnd(selection: vscode.Selection, context = Context.current) { + const active = selection.active; + + if (context.selectionBehavior !== SelectionBehavior.Character) { + return active; + } + + const end = selection.end; + + if (isSingleCharacter(selection, context.document)) { + return end; + } + + return active === end ? end : Positions.next(active, context.document)!; + } + + /** + * Returns `activeStart(selection)` if `direction === Backward`, and + * `activeEnd(selection)` otherwise. + */ + export function activeTowards( + selection: vscode.Selection, + direction: Direction, + context = Context.current, + ) { + return direction === Direction.Backward + ? activeStart(selection, context) + : activeEnd(selection, context); + } + + /** + * Shifts the given selection to the given position using the specified + * `Shift` behavior: + * - If `Shift.Jump`, `result.active == result.anchor == position`. + * - If `Shift.Select`, `result.active == position`, `result.anchor == selection.active`. + * - If `Shift.Extend`, `result.active == position`, `result.anchor == selection.anchor`. + * + * ### Example + * + * ```js + * const s1 = Selections.empty(0, 0), + * shifted1 = Selections.shift(s1, Positions.at(0, 4), Select); + * + * expect(shifted1, "to have anchor at coords", 0, 0).and("to have cursor at coords", 0, 4); + * ``` + * + * With + * + * ``` + * line with 23 characters + * ``` + * + * ### Example + * + * ```js + * setSelectionBehavior(SelectionBehavior.Character); + * ``` + */ + export function shift( + selection: vscode.Selection, + position: vscode.Position, + shift: Shift, + context = Context.current, + ) { + let anchor = shift === Shift.Jump + ? position + : shift === Shift.Select + ? selection.active + : selection.anchor; + + if (context.selectionBehavior === SelectionBehavior.Character && shift !== Shift.Jump) { + const direction = anchor.isAfter(position) ? Direction.Backward : Direction.Forward; + + anchor = seekFrom(selection, direction, anchor, context); + } + + return new vscode.Selection(anchor, position); + } + + /** + * Same as `shift`, but also extends the active character towards the given + * direction in character selection mode. If `direction === Forward`, the + * active character will be selected such that + * `activeEnd(selection) === active`. If `direction === Backward`, the + * active character will be selected such that + * `activeStart(selection) === active`. + */ + export function shiftTowards( + selection: vscode.Selection, + position: vscode.Position, + shift: Shift, + direction: Direction, + context = Context.current, + ) { + if (context.selectionBehavior === SelectionBehavior.Character + && direction === Direction.Backward) { + position = Positions.next(position) ?? position; + } + + return Selections.shift(selection, position, shift, context); + } + + /** + * Returns whether the given selection spans an entire line. + * + * ### Example + * + * ```js + * expect(Selections.isEntireLine(Selections.current[0]), "to be true"); + * expect(Selections.isEntireLine(Selections.current[1]), "to be false"); + * ``` + * + * With: + * ``` + * abc + * ^^^^ 0 + * + * def + * ^^^ 1 + * ``` + * + * ### Example + * Use `isEntireLines` for multi-line selections. + * + * ```js + * expect(Selections.isEntireLine(Selections.current[0]), "to be false"); + * ``` + * + * With: + * ``` + * abc + * ^^^^ 0 + * def + * ^^^^ 0 + * + * ``` + */ + export function isEntireLine(selection: vscode.Selection | vscode.Range) { + const start = selection.start, + end = selection.end; + + return start.character === 0 && end.character === 0 && start.line === end.line - 1; + } + + /** + * Returns whether the given selection spans one or more entire lines. + * + * ### Example + * + * ```js + * expect(Selections.isEntireLines(Selections.current[0]), "to be true"); + * expect(Selections.isEntireLines(Selections.current[1]), "to be true"); + * expect(Selections.isEntireLines(Selections.current[2]), "to be false"); + * ``` + * + * With: + * ``` + * abc + * ^^^^ 0 + * def + * ^^^^ 0 + * ghi + * ^^^^ 1 + * jkl + * ^^^^ 2 + * mno + * ^^^ 2 + * ``` + */ + export function isEntireLines(selection: vscode.Selection | vscode.Range) { + const start = selection.start, + end = selection.end; + + return start.character === 0 && end.character === 0 && start.line !== end.line; + } + + export function startsWithEntireLine(selection: vscode.Selection | vscode.Range) { + const start = selection.start; + + return start.character === 0 && start.line !== selection.end.line; + } + + export function endsWithEntireLine(selection: vscode.Selection | vscode.Range) { + const end = selection.end; + + return end.character === 0 && selection.start.line !== end.line; + } + + export function activeLineIsFullySelected(selection: vscode.Selection) { + return selection.active === selection.start + ? startsWithEntireLine(selection) + : endsWithEntireLine(selection); + } + + export function isMovingTowardsAnchor(selection: vscode.Selection, direction: Direction) { + return direction === Direction.Backward + ? selection.active === selection.end + : selection.active === selection.start; + } + + /** + * Returns the length of the given selection. + * + * ### Example + * + * ```js + * expect(Selections.length(Selections.current[0]), "to be", 7); + * expect(Selections.length(Selections.current[1]), "to be", 1); + * expect(Selections.length(Selections.current[2]), "to be", 0); + * ``` + * + * With: + * ``` + * abc + * ^^^^ 0 + * def + * ^^^ 0 + * ghi + * ^ 1 + * | 2 + * ``` + */ + export function length( + selection: vscode.Selection | vscode.Range, + document = Context.current.document, + ) { + const start = selection.start, + end = selection.end; + + if (start.line === end.line) { + return end.character - start.character; + } + + return document.offsetAt(end) - document.offsetAt(start); + } + + /** + * Returns a selection starting at the given position or offset and with the + * specified length. + */ + export function fromLength( + start: number | vscode.Position, + length: number, + reversed = false, + document = Context.current.document, + ) { + let startOffset: number, + startPosition: vscode.Position; + + if (length === 0) { + if (typeof start === "number") { + startPosition = document.positionAt(start); + } else { + startPosition = start; + } + + return new vscode.Selection(startPosition, startPosition); + } + + if (typeof start === "number") { + startOffset = start; + startPosition = document.positionAt(start); + } else { + startOffset = document.offsetAt(start); + startPosition = start; + } + + const endPosition = document.positionAt(startOffset + length); + + return reversed + ? new vscode.Selection(endPosition, startPosition) + : new vscode.Selection(startPosition, endPosition); + } + + /** + * Returns a new selection given its start and end positions. If `reversed` is + * false, the returned solution will be such that `start === anchor` and + * `end === active`. Otherwise, the returned solution will be such that + * `start === active` and `end === anchor`. + * + * ### Example + * + * ```js + * const p0 = new vscode.Position(0, 0), + * p1 = new vscode.Position(0, 1); + * + * expect(Selections.fromStartEnd(p0, p1, false), "to satisfy", { + * start: p0, + * end: p1, + * anchor: p0, + * active: p1, + * isReversed: false, + * }); + * + * expect(Selections.fromStartEnd(p0, p1, true), "to satisfy", { + * start: p0, + * end: p1, + * anchor: p1, + * active: p0, + * isReversed: true, + * }); + * ``` + */ + export function fromStartEnd( + start: vscode.Position | number, + end: vscode.Position | number, + reversed: boolean, + document?: vscode.TextDocument, + ) { + if (typeof start === "number") { + if (document === undefined) { + document = Context.current.document; + } + + start = document.positionAt(start); + } + + if (typeof end === "number") { + if (document === undefined) { + document = Context.current.document; + } + + end = document.positionAt(end); + } + + return reversed ? new vscode.Selection(end, start) : new vscode.Selection(start, end); + } + + /** + * Returns the selection with the given anchor and active positions. + */ + export function fromAnchorActive( + anchor: vscode.Position, + active: vscode.Position, + ): vscode.Selection; + + /** + * Returns the selection with the given anchor and active positions. + */ + export function fromAnchorActive( + anchorLine: number, + anchorCharacter: number, + active: vscode.Position, + ): vscode.Selection; + + /** + * Returns the selection with the given anchor and active positions. + */ + export function fromAnchorActive( + anchor: vscode.Position, + activeLine: number, + activeCharacter: number, + ): vscode.Selection; + + /** + * Returns the selection with the given anchor and active position + * coordinates. + */ + export function fromAnchorActive( + anchorLine: number, + anchorCharacter: number, + activeLine: number, + activeCharacter: number, + ): vscode.Selection; + + export function fromAnchorActive( + anchorOrAnchorLine: number | vscode.Position, + activeOrAnchorCharacterOrActiveLine: number | vscode.Position, + activeOrActiveLineOrActiveCharacter?: number | vscode.Position, + activeCharacter?: number, + ) { + if (activeCharacter !== undefined) { + // Four arguments: this is the last overload. + const anchorLine = anchorOrAnchorLine as number, + anchorCharacter = activeOrAnchorCharacterOrActiveLine as number, + activeLine = activeOrActiveLineOrActiveCharacter as number; + + return new vscode.Selection(anchorLine, anchorCharacter, activeLine, activeCharacter); + } + + if (activeOrActiveLineOrActiveCharacter === undefined) { + // Two arguments: this is the first overload. + const anchor = anchorOrAnchorLine as vscode.Position, + active = activeOrAnchorCharacterOrActiveLine as vscode.Position; + + return new vscode.Selection(anchor, active); + } + + if (typeof activeOrActiveLineOrActiveCharacter === "number") { + // Third argument is a number: this is the third overload. + const anchor = anchorOrAnchorLine as vscode.Position, + activeLine = activeOrAnchorCharacterOrActiveLine as number, + activeCharacter = activeOrActiveLineOrActiveCharacter as number; + + return new vscode.Selection(anchor, new vscode.Position(activeLine, activeCharacter)); + } + + // Third argument is a position: this is the second overload. + const anchorLine = anchorOrAnchorLine as number, + anchorCharacter = activeOrAnchorCharacterOrActiveLine as number, + active = activeOrActiveLineOrActiveCharacter as vscode.Position; + + return new vscode.Selection(new vscode.Position(anchorLine, anchorCharacter), active); + } + + /** + * Shorthand for `fromAnchorActive`. + */ + export const from = fromAnchorActive; + + /** + * Transforms a list of caret-mode selections (that is, regular selections as + * manipulated internally) into a list of character-mode selections (that is, + * selections modified to include a block character in them). + * + * This function should be used before setting the selections of a + * `vscode.TextEditor` if the selection behavior is `Character`. + * + * ### Example + * Forward-facing, non-empty selections are reduced by one character. + * + * ```js + * // One-character selection becomes empty. + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 0, 1)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 0), + * ]); + * + * // One-character selection becomes empty (at line break). + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 1, 0)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 1), + * ]); + * + * // Forward-facing selection becomes shorter. + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 1, 1)]), "to satisfy", [ + * expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 1, 0), + * ]); + * + * // One-character selection becomes empty (reversed). + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 0, 0)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 0), + * ]); + * + * // One-character selection becomes empty (reversed, at line break). + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 0, 0, 1)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 1), + * ]); + * + * // Reversed selection stays as-is. + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 1, 0, 0)]), "to satisfy", [ + * expect.it("to have anchor at coords", 1, 1).and("to have cursor at coords", 0, 0), + * ]); + * ``` + * + * With: + * ``` + * a + * b + * ``` + */ + export function toCharacterMode( + selections: readonly vscode.Selection[], + document?: vscode.TextDocument, + ) { + const characterModeSelections = [] as vscode.Selection[]; + + for (const selection of selections) { + const selectionActive = selection.active, + selectionActiveLine = selectionActive.line, + selectionActiveCharacter = selectionActive.character, + selectionAnchor = selection.anchor, + selectionAnchorLine = selectionAnchor.line, + selectionAnchorCharacter = selectionAnchor.character; + let active = selectionActive, + anchor = selectionAnchor, + changed = false; + + if (selectionAnchorLine === selectionActiveLine) { + if (selectionAnchorCharacter === selectionActiveCharacter) { + // Selection is empty: go to previous position. + anchor = active = Positions.previous(active, document) ?? active; + changed = active !== selectionActive; + } else if (selectionAnchorCharacter + 1 === selectionActiveCharacter) { + // Selection is one-character long: make it empty. + active = selectionAnchor; + changed = true; + } else if (selectionAnchorCharacter - 1 === selectionActiveCharacter) { + // Selection is reversed and one-character long: make it empty. + anchor = selectionActive; + changed = true; + } else if (selectionAnchorCharacter < selectionActiveCharacter) { + // Selection is strictly forward-facing: make it shorter. + active = new vscode.Position(selectionActiveLine, selectionActiveCharacter - 1); + changed = true; + } else { + // Selection is reversed: do nothing. + } + } else if (selectionAnchorLine < selectionActiveLine) { + // Selection is strictly forward-facing: make it shorter. + if (selectionActiveCharacter > 0) { + active = new vscode.Position(selectionActiveLine, selectionActiveCharacter - 1); + changed = true; + } else { + // The active character is the first one, so we have to get some + // information from the document. + if (document === undefined) { + document = Context.current.document; + } + + const activePrevLine = selectionActiveLine - 1, + activePrevLineLength = document.lineAt(activePrevLine).text.length; + + active = new vscode.Position(activePrevLine, activePrevLineLength); + changed = true; + } + } else if (selectionAnchorLine === selectionActiveLine + 1 + && selectionAnchorCharacter === 0 + && selectionActiveCharacter === Lines.length(selectionActiveLine, document)) { + // Selection is reversed and one-character long: make it empty. + anchor = selectionActive; + changed = true; + } else { + // Selection is reversed: do nothing. + } + + characterModeSelections.push(changed ? new vscode.Selection(anchor, active) : selection); + } + + return characterModeSelections; + } + + /** + * Reverses the changes made by `toCharacterMode` by increasing by one the + * length of every empty or forward-facing selection. + * + * This function should be used on the selections of a `vscode.TextEditor` if + * the selection behavior is `Character`. + * + * ### Example + * Selections remain empty in empty documents. + * + * ```js + * expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 0), + * ]); + * ``` + * + * With: + * ``` + * ``` + * + * ### Example + * Empty selections automatically become 1-character selections. + * + * ```js + * expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ + * expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 0, 1), + * ]); + * + * // At the end of the line, it selects the line ending: + * expect(Selections.fromCharacterMode([Selections.empty(0, 1)]), "to satisfy", [ + * expect.it("to have anchor at coords", 0, 1).and("to have cursor at coords", 1, 0), + * ]); + * + * // But it does nothing at the end of the document: + * expect(Selections.fromCharacterMode([Selections.empty(2, 0)]), "to satisfy", [ + * expect.it("to be empty at coords", 2, 0), + * ]); + * ``` + * + * With: + * ``` + * a + * b + * + * ``` + */ + export function fromCharacterMode( + selections: readonly vscode.Selection[], + document?: vscode.TextDocument, + ) { + const caretModeSelections = [] as vscode.Selection[]; + + for (const selection of selections) { + const selectionActive = selection.active, + selectionActiveLine = selectionActive.line, + selectionActiveCharacter = selectionActive.character, + selectionAnchor = selection.anchor, + selectionAnchorLine = selectionAnchor.line, + selectionAnchorCharacter = selectionAnchor.character; + let active = selectionActive, + changed = false; + + const isEmptyOrForwardFacing = selectionAnchorLine < selectionActiveLine + || (selectionAnchorLine === selectionActiveLine + && selectionAnchorCharacter <= selectionActiveCharacter); + + if (isEmptyOrForwardFacing) { + // Selection is empty or forward-facing: extend it if possible. + if (document === undefined) { + document = Context.current.document; + } + + const lineLength = document.lineAt(selectionActiveLine).text.length; + + if (selectionActiveCharacter === lineLength) { + // Character is at the end of the line. + if (selectionActiveLine + 1 < document.lineCount) { + // This is not the last line: we can extend the selection. + active = new vscode.Position(selectionActiveLine + 1, 0); + changed = true; + } else { + // This is the last line: we cannot do anything. + } + } else { + // Character is not at the end of the line: we can extend the selection. + active = new vscode.Position(selectionActiveLine, selectionActiveCharacter + 1); + changed = true; + } + } + + caretModeSelections.push(changed ? new vscode.Selection(selectionAnchor, active) : selection); + } + + return caretModeSelections; + } +} diff --git a/src/commands/README.build.ts b/src/commands/README.build.ts new file mode 100644 index 0000000..6f8078d --- /dev/null +++ b/src/commands/README.build.ts @@ -0,0 +1,118 @@ +import { Builder, parseKeys, unindent } from "../../meta"; + +export async function build(builder: Builder) { + const commandModules = await builder.getCommandModules(); + + return unindent(4, ` +
+ Quick reference + ${toTable(commandModules)} +
+ + ${commandModules.map((module) => unindent(8, ` + ## [\`${module.name}\`](./${module.name}.ts) + + ${module.doc!.trim()} + + ${module.functions.map((f) => unindent(12, ` + ### [\`${module.name === "misc" ? "" : module.name + "."}${f.nameWithDot}\`](./${ + module.name}.ts#L${f.startLine + 1}-L${f.endLine + 1}) + + ${f.doc} + ${(() => { + const supportedInputs = determineSupportedInputs(f); + + return supportedInputs.length === 0 + ? "" + : "This command:" + supportedInputs.map((x) => `\n- ${x}.`).join(""); + })()} + `).trim()).join("\n\n")} + `).trim()).join("\n\n")} + `); +} + +function toTable(modules: readonly Builder.ParsedModule[]) { + const rows: string[][] = modules.flatMap((module) => { + const modulePrefix = module.name === "misc" ? "" : module.name + ".", + allCommands = [] as (Builder.ParsedFunction | Builder.AdditionalCommand)[]; + + allCommands.push(...module.functions); + allCommands.push( + ...module.additional + .concat(...module.functions.flatMap((f) => f.additional)) + .filter((a) => a.qualifiedIdentifier && a.identifier), + ); + + allCommands.sort((a, b) => { + const aName = "name" in a ? a.name : a.qualifiedIdentifier!, + bName = "name" in b ? b.name : b.qualifiedIdentifier!; + + return aName.localeCompare(bName); + }); + + return allCommands.map((f, i, { length }) => { + const identifier = "name" in f ? modulePrefix + f.nameWithDot : f.qualifiedIdentifier, + summary = "summary" in f ? f.summary : f.title, + keys = parseKeys(("properties" in f ? f.properties.keys : f.keys) ?? ""), + link = "name" in f + ? `#${(modulePrefix + f.nameWithDot).replace(/\./g, "")}` + : `./${module.name}.ts#L${f.line + 1}`; + + return [ + i === 0 + ? `
${module.name}` + : "", + `${identifier}`, + `${summary}`, + `${ + keys.map(({ key, when }) => `${key} (${when})`).join("") + }`, + ]; + }); + }); + + return ` + + + + ${["Category", "Identifier", "Title", "Default keybindings"] + .map((h) => ``).join("")} + + + + ${rows.map((row) => `${row.join("")}`).join("\n ")} + +
${h}
+ `; +} + +function determineSupportedInputs(f: Builder.ParsedFunction) { + const supported: string[] = []; + let requiresActiveEditor = false; + + for (const [name, type] of f.parameters) { + let match: RegExpExecArray | null; + + if (name === "count" || name === "repetitions") { + supported.push("may be repeated with a given number of repetitions"); + } else if (match = /^RegisterOr<"(\w+)"(?:, .+)?>$/.exec(type)) { + supported.push(`accepts a register (by default, it uses \`${match[1]}\`)`); + } else if (match = /^InputOr<(\w+)>$/.exec(type)) { + supported.push(`takes an input of type \`${match[1]}\``); + } else if (match = /^Argument<(.+)>( \| undefined)$/.exec(type)) { + supported.push(`takes an argument \`${name}\` of type \`${match[1]}\``); + } else if (name === "input") { + supported.push(`takes an input of type \`${type}\``); + } else if (name === "argument") { + supported.push(`accepts an argument of type \`${type}\``); + } else if (/^(Context|vscode.Text(Editor|Document))$/.test(type) || name === "selections") { + requiresActiveEditor = true; + } + } + + if (!requiresActiveEditor) { + supported.push("does not require an active text editor"); + } + + return supported.sort(); +} diff --git a/src/commands/README.md b/src/commands/README.md new file mode 100644 index 0000000..69381c7 --- /dev/null +++ b/src/commands/README.md @@ -0,0 +1,1208 @@ +# Dance commands + + + +
+Quick reference + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryIdentifierTitleDefault keybindings
devdev.setSelectionBehaviorSet the selection behavior of the specified mode
editedit.alignAlign selectionsShift+7 (editorTextFocus && dance.mode == 'normal')
edit.case.swapSwap caseAlt+` (editorTextFocus && dance.mode == 'normal')
edit.case.toLowerTransform to lower case` (editorTextFocus && dance.mode == 'normal')
edit.case.toUpperTransform to upper caseShift+` (editorTextFocus && dance.mode == 'normal')
edit.copyIndentationCopy indentationShift+Alt+7 (editorTextFocus && dance.mode == 'normal')
edit.deindentDeindent selected linesShift+Alt+, (editorTextFocus && dance.mode == 'normal')
edit.deindent.withIncompleteDeindent selected lines (including incomplete indent)Shift+, (editorTextFocus && dance.mode == 'normal')
edit.deleteDeleteAlt+D (editorTextFocus && dance.mode == 'normal')
edit.delete-insertDelete and switch to InsertAlt+C (editorTextFocus && dance.mode == 'normal')
edit.newLine.above.insertInsert new line above and switch to insertShift+O (editorTextFocus && dance.mode == 'normal')
edit.newLine.below.insertInsert new line below and switch to insertO (editorTextFocus && dance.mode == 'normal')
edit.paste.afterPaste afterP (editorTextFocus && dance.mode == 'normal')
edit.paste.after.selectPaste after and selectAlt+P (editorTextFocus && dance.mode == 'normal')
edit.paste.beforePaste beforeShift+P (editorTextFocus && dance.mode == 'normal')
edit.paste.before.selectPaste before and selectShift+Alt+P (editorTextFocus && dance.mode == 'normal')
edit.selectRegister-insertPick register and replaceCtrl+R (editorTextFocus && dance.mode == 'normal')Ctrl+R (editorTextFocus && dance.mode == 'insert')
edit.yank-deleteCopy and deleteD (editorTextFocus && dance.mode == 'normal')
edit.yank-delete-insertCopy, delete and switch to InsertC (editorTextFocus && dance.mode == 'normal')
edit.yank-replaceCopy and replaceShift+R (editorTextFocus && dance.mode == 'normal')
edit.indentIndent selected linesShift+. (editorTextFocus && dance.mode == 'normal')
edit.indent.withEmptyIndent selected lines (including empty lines)Shift+Alt+. (editorTextFocus && dance.mode == 'normal')
edit.insertInsert contents of registerShift+Alt+R (editorTextFocus && dance.mode == 'normal')
edit.joinJoin linesAlt+J (editorTextFocus && dance.mode == 'normal')
edit.join.selectJoin lines and select inserted separatorsShift+Alt+J (editorTextFocus && dance.mode == 'normal')
edit.newLine.aboveInsert new line above each selectionShift+Alt+O (editorTextFocus && dance.mode == 'normal')
edit.newLine.belowInsert new line below each selectionAlt+O (editorTextFocus && dance.mode == 'normal')
edit.replaceCharactersReplace charactersR (editorTextFocus && dance.mode == 'normal')
historyhistory.repeat.seekRepeat last seekAlt+. (editorTextFocus && dance.mode == 'normal')
history.repeat.selectionRepeat last selection change
history.recording.playReplay recordingQ (editorTextFocus && dance.mode == 'normal')
history.recording.startStart recordingShift+Q (editorTextFocus && dance.mode == 'normal')
history.recording.stopStop recordingEscape (editorTextFocus && dance.mode == 'normal' && dance.isRecording)
history.redoRedoShift+U (editorTextFocus && dance.mode == 'normal')
history.redo.selectionsRedo a change of selectionsShift+Alt+U (editorTextFocus && dance.mode == 'normal')
history.repeatRepeat last change
history.repeat.editRepeat last edit without a command. (editorTextFocus && dance.mode == 'normal')
history.undoUndoU (editorTextFocus && dance.mode == 'normal')
history.undo.selectionsUndo a change of selectionsAlt+U (editorTextFocus && dance.mode == 'normal')
keybindingskeybindings.setupSet up Dance keybindings
misccancelCancel Dance operationEscape (editorTextFocus && dance.mode == 'normal')Escape (editorTextFocus && dance.mode == 'input')
ignoreIgnore key
openMenuOpen menu
runRun code
selectRegisterSelect register for next commandShift+' (editorTextFocus && dance.mode == 'normal')
updateCountUpdate Dance count
modesmodes.insert.afterInsert afterA (editorTextFocus && dance.mode == 'normal')
modes.insert.beforeInsert beforeI (editorTextFocus && dance.mode == 'normal')
modes.insert.lineEndInsert at line endShift+A (editorTextFocus && dance.mode == 'normal')
modes.insert.lineStartInsert at line startShift+I (editorTextFocus && dance.mode == 'normal')
modes.set.insertSet mode to Insert
modes.set.normalSet mode to NormalEscape (editorTextFocus && dance.mode == 'insert')
modes.set.temporarily.insertTemporart Insert modeCtrl+V (editorTextFocus && dance.mode == 'normal')
modes.set.temporarily.normalTemporary Normal modeCtrl+V (editorTextFocus && dance.mode == 'insert')
modes.setSet Dance mode
modes.set.temporarilySet Dance mode temporarily
searchsearch.nextSelect next matchN (editorTextFocus && dance.mode == 'normal')
search.searchSearch/ (editorTextFocus && dance.mode == 'normal')
search.backwardSearch backwardAlt+/ (editorTextFocus && dance.mode == 'normal')
search.backward.extendSearch backward (extend)Shift+Alt+/ (editorTextFocus && dance.mode == 'normal')
search.extendSearch (extend)Shift+/ (editorTextFocus && dance.mode == 'normal')
search.next.addAdd next matchShift+N (editorTextFocus && dance.mode == 'normal')
search.previousSelect previous matchAlt+N (editorTextFocus && dance.mode == 'normal')
search.previous.addAdd previous matchShift+Alt+N (editorTextFocus && dance.mode == 'normal')
search.selection.smartSearch current selection (smart)Shift+8 (editorTextFocus && dance.mode == 'normal')
search.selectionSearch current selectionShift+Alt+8 (editorTextFocus && dance.mode == 'normal')
seekseek.enclosingSelect to next enclosing characterM (editorTextFocus && dance.mode == 'normal')
seek.objectSelect object
seek.seekSelect to character (excluded)T (editorTextFocus && dance.mode == 'normal')
seek.askObjectSelect whole objectAlt+A (editorTextFocus && dance.mode == 'normal')Alt+A (editorTextFocus && dance.mode == 'insert')
seek.askObject.endSelect to whole object end] (editorTextFocus && dance.mode == 'normal')
seek.askObject.endExtend to whole object endShift+] (editorTextFocus && dance.mode == 'normal')
seek.askObject.innerSelect inner objectAlt+I (editorTextFocus && dance.mode == 'normal')Alt+I (editorTextFocus && dance.mode == 'insert')
seek.askObject.inner.endSelect to inner object endAlt+] (editorTextFocus && dance.mode == 'normal')
seek.askObject.inner.end.extendExtend to inner object endShift+Alt+] (editorTextFocus && dance.mode == 'normal')
seek.askObject.inner.startSelect to inner object startAlt+[ (editorTextFocus && dance.mode == 'normal')
seek.askObject.inner.start.extendExtend to inner object startShift+Alt+[ (editorTextFocus && dance.mode == 'normal')
seek.askObject.startSelect to whole object start[ (editorTextFocus && dance.mode == 'normal')
seek.askObject.startExtend to whole object startShift+[ (editorTextFocus && dance.mode == 'normal')
seek.backwardSelect to character (excluded, backward)Alt+T (editorTextFocus && dance.mode == 'normal')
seek.enclosing.backwardSelect to previous enclosing characterAlt+M (editorTextFocus && dance.mode == 'normal')
seek.enclosing.extendExtend to next enclosing characterShift+M (editorTextFocus && dance.mode == 'normal')
seek.enclosing.extend.backwardExtend to previous enclosing characterShift+Alt+M (editorTextFocus && dance.mode == 'normal')
seek.extendExtend to character (excluded)Shift+T (editorTextFocus && dance.mode == 'normal')
seek.extend.backwardExtend to character (excluded, backward)Shift+Alt+T (editorTextFocus && dance.mode == 'normal')
seek.includedSelect to character (included)F (editorTextFocus && dance.mode == 'normal')
seek.included.backwardSelect to character (included, backward)Alt+F (editorTextFocus && dance.mode == 'normal')
seek.included.extendExtend to character (included)Shift+F (editorTextFocus && dance.mode == 'normal')
seek.included.extend.backwardExtend to character (included, backward)Shift+Alt+F (editorTextFocus && dance.mode == 'normal')
seek.word.backwardSelect to previous word startB (editorTextFocus && dance.mode == 'normal')
seek.word.extendExtend to next word startShift+W (editorTextFocus && dance.mode == 'normal')
seek.word.extend.backwardExtend to previous word startShift+B (editorTextFocus && dance.mode == 'normal')
seek.word.wsSelect to next non-whitespace word startAlt+W (editorTextFocus && dance.mode == 'normal')
seek.word.ws.backwardSelect to previous non-whitespace word startAlt+B (editorTextFocus && dance.mode == 'normal')
seek.word.ws.extendExtend to next non-whitespace word startShift+Alt+W (editorTextFocus && dance.mode == 'normal')
seek.word.ws.extend.backwardExtend to previous non-whitespace word startShift+Alt+B (editorTextFocus && dance.mode == 'normal')
seek.wordEndSelect to next word endE (editorTextFocus && dance.mode == 'normal')
seek.wordEnd.extendExtend to next word endShift+E (editorTextFocus && dance.mode == 'normal')
seek.wordEnd.wsSelect to next non-whitespace word endAlt+E (editorTextFocus && dance.mode == 'normal')
seek.wordEnd.ws.extendExtend to next non-whitespace word endShift+Alt+E (editorTextFocus && dance.mode == 'normal')
seek.wordSelect to next word startW (editorTextFocus && dance.mode == 'normal')
selectselect.bufferSelect whole bufferShift+5 (editorTextFocus && dance.mode == 'normal')
select.firstVisibleLineSelect to first visible line
select.horizontallySelect horizontally
select.lastLineSelect to last line
select.lastModificationSelect to last modification
select.lastVisibleLineSelect to last visible line
select.line.aboveSelect line above
select.line.above.extendExtend to line above
select.line.belowSelect line belowX (editorTextFocus && dance.mode == 'normal')
select.line.below.extendExtend to line belowShift+X (editorTextFocus && dance.mode == 'normal')
select.lineEndSelect to line endAlt+L (editorTextFocus && dance.mode == 'normal')End (editorTextFocus && dance.mode == 'normal')
select.lineStartSelect to line startAlt+H (editorTextFocus && dance.mode == 'normal')Home (editorTextFocus && dance.mode == 'normal')
select.middleVisibleLineSelect to middle visible line
select.documentEnd.extendExtend to last character
select.documentEnd.jumpJump to last character
select.down.extendExtend downShift+J (editorTextFocus && dance.mode == 'normal')Shift+Down (editorTextFocus && dance.mode == 'normal')
select.down.jumpJump downJ (editorTextFocus && dance.mode == 'normal')Down (editorTextFocus && dance.mode == 'normal')
select.firstLine.extendExtend to first line
select.firstLine.jumpJump to first line
select.firstVisibleLine.extendExtend to first visible line
select.firstVisibleLine.jumpJump to first visible line
select.lastLine.extendExtend to last line
select.lastLine.jumpJump to last line
select.lastModification.extendExtend to last modification
select.lastModification.jumpJump to last modification
select.lastVisibleLine.extendExtend to last visible line
select.lastVisibleLine.jumpJump to last visible line
select.left.extendExtend leftShift+H (editorTextFocus && dance.mode == 'normal')Shift+Left (editorTextFocus && dance.mode == 'normal')
select.left.jumpJump leftH (editorTextFocus && dance.mode == 'normal')Left (editorTextFocus && dance.mode == 'normal')
select.lineEnd.extendExtend to line endShift+Alt+L (editorTextFocus && dance.mode == 'normal')Shift+End (editorTextFocus && dance.mode == 'normal')
select.lineStart.extendExtend to line startShift+Alt+H (editorTextFocus && dance.mode == 'normal')Shift+Home (editorTextFocus && dance.mode == 'normal')
select.lineStart.jumpJump to line start
select.lineStart.skipBlank.extendExtend to line start (skip blank)
select.lineStart.skipBlank.jumpJump to line start (skip blank)
select.middleVisibleLine.extendExtend to middle visible line
select.middleVisibleLine.jumpJump to middle visible line
select.right.extendExtend rightShift+L (editorTextFocus && dance.mode == 'normal')Shift+Right (editorTextFocus && dance.mode == 'normal')
select.right.jumpJump rightL (editorTextFocus && dance.mode == 'normal')Right (editorTextFocus && dance.mode == 'normal')
select.to.extendExtend toShift+G (editorTextFocus && dance.mode == 'normal')
select.to.jumpGo toG (editorTextFocus && dance.mode == 'normal')
select.up.extendExtend upShift+K (editorTextFocus && dance.mode == 'normal')Shift+Up (editorTextFocus && dance.mode == 'normal')
select.up.jumpJump upK (editorTextFocus && dance.mode == 'normal')Up (editorTextFocus && dance.mode == 'normal')
select.toSelect to
select.verticallySelect vertically
selectionsselections.changeDirectionChange direction of selectionsAlt+; (editorTextFocus && dance.mode == 'normal')
selections.copyCopy selections belowShift+C (editorTextFocus && dance.mode == 'normal')
selections.expandToLinesExpand to linesAlt+X (editorTextFocus && dance.mode == 'normal')
selections.filterFilter selectionsShift+4 (editorTextFocus && dance.mode == 'normal')
selections.mergeMerge contiguous selectionsShift+Alt+- (editorTextFocus && dance.mode == 'normal')
selections.openOpen selected file
selections.pipePipe selectionsShift+Alt+\ (editorTextFocus && dance.mode == 'normal')
selections.reduceReduce selections to their cursor; (editorTextFocus && dance.mode == 'normal')
selections.restoreRestore selectionsZ (editorTextFocus && dance.mode == 'normal')
selections.restore.withCurrentCombine register selections with current onesAlt+Z (editorTextFocus && dance.mode == 'normal')
selections.saveSave selectionsShift+Z (editorTextFocus && dance.mode == 'normal')
selections.saveTextCopy selections textY (editorTextFocus && dance.mode == 'normal')
selections.selectSelect within selectionsS (editorTextFocus && dance.mode == 'normal')
selections.clear.mainClear main selectionsAlt+Space (editorTextFocus && dance.mode == 'normal')
selections.clear.secondaryClear secondary selectionsSpace (editorTextFocus && dance.mode == 'normal')
selections.copy.aboveCopy selections aboveShift+Alt+C (editorTextFocus && dance.mode == 'normal')
selections.faceBackwardBackward selections
selections.faceForwardForward selectionsShift+Alt+; (editorTextFocus && dance.mode == 'normal')
selections.filter.regexpKeep matching selectionsAlt+K (editorTextFocus && dance.mode == 'normal')
selections.filter.regexp.inverseClear matching selectionsShift+Alt+K (editorTextFocus && dance.mode == 'normal')
selections.hideIndicesHide selection indices
selections.pipe.appendPipe and appendShift+1 (editorTextFocus && dance.mode == 'normal')
selections.pipe.prependPipe and prependShift+Alt+1 (editorTextFocus && dance.mode == 'normal')
selections.pipe.replacePipe and replaceShift+\ (editorTextFocus && dance.mode == 'normal')
selections.reduce.edgesReduce selections to their endsShift+Alt+S (editorTextFocus && dance.mode == 'normal')
selections.showIndicesShow selection indices
selections.splitSplit selectionsShift+S (editorTextFocus && dance.mode == 'normal')
selections.splitLinesSplit selections at line boundariesAlt+S (editorTextFocus && dance.mode == 'normal')
selections.toggleIndicesToggle selection indicesEnter (editorTextFocus && dance.mode == 'normal')
selections.trimLinesTrim linesShift+Alt+X (editorTextFocus && dance.mode == 'normal')
selections.trimWhitespaceTrim whitespaceShift+- (editorTextFocus && dance.mode == 'normal')
selections.rotateselections.rotate.bothRotate selections clockwiseShift+9 (editorTextFocus && dance.mode == 'normal')
selections.rotate.contentsRotate selections clockwise (contents only)
selections.rotate.selectionsRotate selections clockwise (selections only)Shift+Alt+9 (editorTextFocus && dance.mode == 'normal')
selections.rotate.both.reverseRotate selections counter-clockwiseShift+0 (editorTextFocus && dance.mode == 'normal')
selections.rotate.contents.reverseRotate selections counter-clockwise (contents only)
selections.rotate.selections.reverseRotate selections counter-clockwise (selections only)Shift+Alt+0 (editorTextFocus && dance.mode == 'normal')
viewview.lineReveals a position based on the main cursor
+ +
+ +## [`dev`](./dev.ts) + +Developer utilities for Dance. + +### [`dev.setSelectionBehavior`](./dev.ts#L10-L18) + +Set the selection behavior of the specified mode. + +This command: +- does not require an active text editor. +- takes an argument `value` of type `"caret" | "character"`. + +## [`edit`](./edit.ts) + +Perform changes on the text content of the document. + +See https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#changes. + +### [`edit.insert`](./edit.ts#L27-L60) + +Insert contents of register. + +A `where` argument may be specified to state where the text should be +inserted relative to each selection. If unspecified, each selection will be +replaced by the text. + + +#### Additional commands + +| Title | Identifier | Keybinding | Commands | +| ---------------------------------- | ----------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------- | +| Pick register and replace | `selectRegister-insert` | `c-r` (normal), `c-r` (insert) | `[".selectRegister"], [".edit.insert"]` | +| Paste before | `paste.before` | `s-p` (normal) | `[".edit.insert", { handleNewLine: true, where: "start" }]` | +| Paste after | `paste.after` | `p` (normal) | `[".edit.insert", { handleNewLine: true, where: "end" }]` | +| Paste before and select | `paste.before.select` | `s-a-p` (normal) | `[".edit.insert", { handleNewLine: true, where: "start", select: true }]` | +| Paste after and select | `paste.after.select` | `a-p` (normal) | `[".edit.insert", { handleNewLine: true, where: "end" , select: true }]` | +| Delete | `delete` | `a-d` (normal) | `[".edit.insert", { register: "_" }]` | +| Delete and switch to Insert | `delete-insert` | `a-c` (normal) | `[".modes.set", { input: "insert" }], [".edit.insert", { register: "_" }]` | +| Copy and delete | `yank-delete` | `d` (normal) | `[".selections.saveText"], [".edit.insert", { register: "_" }]` | +| Copy and replace | `yank-replace` | `s-r` (normal) | `[".selections.saveText"], [".edit.insert"]` | +| Copy, delete and switch to Insert | `yank-delete-insert` | `c` (normal) | `[".selections.saveText"], [".modes.set", { input: "insert" }], [".edit.insert", { register: "_" }]` | + +This command: +- accepts a register (by default, it uses `dquote`). +- takes an argument `adjust` of type `boolean`. +- takes an argument `handleNewLine` of type `boolean`. +- takes an argument `select` of type `boolean`. +- takes an argument `where` of type `"active" | "anchor" | "start" | "end" | undefined`. + +### [`edit.join`](./edit.ts#L91-L96) + +Join lines. + + +This command: +- takes an argument `separator` of type `string`. + +### [`edit.join.select`](./edit.ts#L100-L105) + +Join lines and select inserted separators. + + +This command: +- takes an argument `separator` of type `string`. + +### [`edit.indent`](./edit.ts#L109-L114) + +Indent selected lines. + + +This command: +- may be repeated with a given number of repetitions. + +### [`edit.indent.withEmpty`](./edit.ts#L118-L123) + +Indent selected lines (including empty lines). + + +This command: +- may be repeated with a given number of repetitions. + +### [`edit.deindent`](./edit.ts#L127-L132) + +Deindent selected lines. + + +This command: +- may be repeated with a given number of repetitions. + +### [`edit.deindent.withIncomplete`](./edit.ts#L136-L141) + +Deindent selected lines (including incomplete indent). + + +This command: +- may be repeated with a given number of repetitions. + +### [`edit.case.toLower`](./edit.ts#L145-L150) + +Transform to lower case. + +### [`edit.case.toUpper`](./edit.ts#L154-L159) + +Transform to upper case. + +### [`edit.case.swap`](./edit.ts#L163-L168) + +Swap case. + +### [`edit.replaceCharacters`](./edit.ts#L183-L192) + +Replace characters. + + +This command: +- may be repeated with a given number of repetitions. +- takes an input of type `string`. + +### [`edit.align`](./edit.ts#L235-L248) + +Align selections. + +Align selections, aligning the cursor of each selection by inserting spaces +before the first character of each selection. + + +This command: +- takes an argument `fill` of type `string`. + +### [`edit.copyIndentation`](./edit.ts#L263-L276) + +Copy indentation. + +Copy the indentation of the main selection (or the count one if a count is +given) to all other ones. + + +This command: +- may be repeated with a given number of repetitions. + +### [`edit.newLine.above`](./edit.ts#L305-L316) + +Insert new line above each selection. + + +#### Additional keybindings + +| Title | Identifier | Keybinding | Commands | +| ------------------------------------------ | ---------------------- | -------------- | -------------------------------------------------------------------------------- | +| Insert new line above and switch to insert | `newLine.above.insert` | `s-o` (normal) | `[".modes.set", { input: "insert" }], [".edit.newLine.above", { select: true }]` | + +This command: +- takes an argument `select` of type `boolean`. + +### [`edit.newLine.below`](./edit.ts#L340-L351) + +Insert new line below each selection. + + +#### Additional keybindings + +| Title | Identifier | Keybinding | Commands | +| ------------------------------------------ | ---------------------- | ------------ | -------------------------------------------------------------------------------- | +| Insert new line below and switch to insert | `newLine.below.insert` | `o` (normal) | `[".modes.set", { input: "insert" }], [".edit.newLine.below", { select: true }]` | + +This command: +- takes an argument `select` of type `boolean`. + +## [`history`](./history.ts) + +Interact with history. + +### [`history.undo`](./history.ts#L13-L18) + +Undo. + + +This command: +- does not require an active text editor. + +### [`history.redo`](./history.ts#L22-L27) + +Redo. + + +This command: +- does not require an active text editor. + +### [`history.undo.selections`](./history.ts#L31-L36) + +Undo a change of selections. + + +This command: +- does not require an active text editor. + +### [`history.redo.selections`](./history.ts#L40-L45) + +Redo a change of selections. + + +This command: +- does not require an active text editor. + +### [`history.repeat`](./history.ts#L49-L64) + +Repeat last change. + + +| Title | Identifier | Keybinding | Commands | +| ---------------------------- | ------------------ | -------------- | --------------------------------------------------------------------------- | +| Repeat last selection change | `repeat.selection` | | `[".history.repeat", { include: "dance\\.(seek|select|selections)\\..+" }]` | +| Repeat last seek | `repeat.seek` | `a-.` (normal) | `[".history.repeat", { include: "dance\\.seek\\..+" }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `include` of type `string | RegExp`. + +### [`history.repeat.edit`](./history.ts#L92-L98) + +Repeat last edit without a command. + + +This command: +- may be repeated with a given number of repetitions. + +### [`history.recording.play`](./history.ts#L130-L141) + +Replay recording. + + +This command: +- accepts a register (by default, it uses `arobase`). +- does not require an active text editor. +- may be repeated with a given number of repetitions. + +### [`history.recording.start`](./history.ts#L157-L166) + +Start recording. + + +This command: +- accepts a register (by default, it uses `arobase`). + +### [`history.recording.stop`](./history.ts#L178-L187) + +Stop recording. + + +This command: +- accepts a register (by default, it uses `arobase`). + +## [`keybindings`](./keybindings.ts) + +Utilities for setting up keybindings. + +### [`keybindings.setup`](./keybindings.ts#L11-L14) + +Set up Dance keybindings. + +This command: +- accepts a register (by default, it uses `dquote`). + +## [`misc`](./misc.ts) + +Miscellaneous commands that don't deserve their own category. + +By default, Dance also exports the following keybindings for existing +commands: + +| Keybinding | Command | +| -------------- | ----------------------------------- | +| `s-;` (normal) | `["workbench.action.showCommands"]` | + +### [`cancel`](./misc.ts#L20-L25) + +Cancel Dance operation. + + +This command: +- does not require an active text editor. + +### [`ignore`](./misc.ts#L31-L34) + +Ignore key. + +This command: +- does not require an active text editor. + +### [`run`](./misc.ts#L40-L48) + +Run code. + +This command: +- takes an argument `commands` of type `api.command.Any[]`. + +### [`selectRegister`](./misc.ts#L81-L91) + +Select register for next command. + +When selecting a register, the next key press is used to determine what +register is selected. If this key is a `space` character, then a new key +press is awaited again and the returned register will be specific to the +current document. + +### [`updateCount`](./misc.ts#L110-L137) + +Update Dance count. + +Update the current counter used to repeat the next command. + +#### Additional keybindings + +| Title | Keybinding | Command | +| ------------------------------ | ------------ | ------------------------------------ | +| Add the digit 0 to the counter | `0` (normal) | `[".updateCount", { addDigits: 0 }]` | +| Add the digit 1 to the counter | `1` (normal) | `[".updateCount", { addDigits: 1 }]` | +| Add the digit 2 to the counter | `2` (normal) | `[".updateCount", { addDigits: 2 }]` | +| Add the digit 3 to the counter | `3` (normal) | `[".updateCount", { addDigits: 3 }]` | +| Add the digit 4 to the counter | `4` (normal) | `[".updateCount", { addDigits: 4 }]` | +| Add the digit 5 to the counter | `5` (normal) | `[".updateCount", { addDigits: 5 }]` | +| Add the digit 6 to the counter | `6` (normal) | `[".updateCount", { addDigits: 6 }]` | +| Add the digit 7 to the counter | `7` (normal) | `[".updateCount", { addDigits: 7 }]` | +| Add the digit 8 to the counter | `8` (normal) | `[".updateCount", { addDigits: 8 }]` | +| Add the digit 9 to the counter | `9` (normal) | `[".updateCount", { addDigits: 9 }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `addDigits` of type `number`. +- takes an input of type `number`. + +### [`openMenu`](./misc.ts#L165-L184) + +Open menu. + +If no input is specified, a prompt will ask for the name of the menu to open. + +Alternatively, a `menu` can be inlined in the arguments. + +Pass a `prefix` argument to insert the prefix string followed by the typed +key if it does not match any menu entry. This can be used to implement chords +like `jj`. + +This command: +- does not require an active text editor. +- takes an argument `locked` of type `boolean`. +- takes an argument `menu` of type `Menu`. +- takes an argument `pass` of type `any[]`. +- takes an argument `prefix` of type `string`. +- takes an input of type `string`. + +## [`modes`](./modes.ts) + +Set modes. + +### [`modes.set`](./modes.ts#L11-L30) + +Set Dance mode. + +#### Variants + +| Title | Identifier | Keybinding | Command | +| ------------------ | ------------ | ----------------- | ------------------------------------- | +| Set mode to Normal | `set.normal` | `escape` (insert) | `[".modes.set", { input: "normal" }]` | +| Set mode to Insert | `set.insert` | | `[".modes.set", { input: "insert" }]` | + +Other variants are provided to switch to insert mode: + +| Title | Identifier | Keybinding | Commands | +| -------------------- | ------------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Insert before | `insert.before` | `i` (normal) | `[".selections.faceBackward"], [".modes.set", { input: "insert" }], [".selections.reduce", { where: "start" }]` | +| Insert after | `insert.after` | `a` (normal) | `[".selections.faceForward"] , [".modes.set", { input: "insert" }], [".selections.reduce", { where: "end" }]` | +| Insert at line start | `insert.lineStart` | `s-i` (normal) | `[".select.lineStart", { shift: "jump", skipBlank: true }], [".modes.set", { input: "insert" }], [".selections.reduce", { where: "start" }]` | +| Insert at line end | `insert.lineEnd` | `s-a` (normal) | `[".select.lineEnd" , { shift: "jump" }], [".modes.set", { input: "insert" }], [".selections.reduce", { where: "end" }]` | + +This command: +- takes an input of type `string`. + +### [`modes.set.temporarily`](./modes.ts#L34-L44) + +Set Dance mode temporarily. + +#### Variants + +| Title | Identifier | Keybindings | Commands | +| --------------------- | ------------------------ | -------------- | ------------------------------------------------- | +| Temporary Normal mode | `set.temporarily.normal` | `c-v` (insert) | `[".modes.set.temporarily", { input: "normal" }]` | +| Temporart Insert mode | `set.temporarily.insert` | `c-v` (normal) | `[".modes.set.temporarily", { input: "insert" }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an input of type `string`. + +## [`search`](./search.ts) + +Search for patterns and replace or add selections. + +### [`search.search`](./search.ts#L18-L41) + +Search. + + +| Title | Identifier | Keybinding | Command | +| ------------------------ | ----------------- | -------------- | ------------------------------------------------- | +| Search (extend) | `extend` | `?` (normal) | `[".search", { shift: "extend" }]` | +| Search backward | `backward` | `a-/` (normal) | `[".search", { direction: -1 }]` | +| Search backward (extend) | `backward.extend` | `a-?` (normal) | `[".search", { direction: -1, shift: "extend" }]` | + +This command: +- accepts a register (by default, it uses `slash`). +- may be repeated with a given number of repetitions. +- takes an argument `add` of type `boolean`. +- takes an argument `interactive` of type `boolean`. +- takes an input of type `Input`. + +### [`search.selection`](./search.ts#L87-L102) + +Search current selection. + + +| Title | Identifier | Keybinding | Command | +| -------------------------------- | ----------------- | ------------ | ---------------------------------------- | +| Search current selection (smart) | `selection.smart` | `*` (normal) | `[".search.selection", { smart: true }]` | + +This command: +- accepts a register (by default, it uses `slash`). +- takes an argument `smart` of type `boolean`. + +### [`search.next`](./search.ts#L131-L150) + +Select next match. + + +| Title | Identifier | Keybinding | Command | +| --------------------- | -------------- | ---------------- | ------------------------------------------------ | +| Add next match | `next.add` | `s-n` (normal) | `[".search.next", { add: true }]` | +| Select previous match | `previous` | `a-n` (normal) | `[".search.next", { direction: -1 }]` | +| Add previous match | `previous.add` | `s-a-n` (normal) | `[".search.next", { direction: -1, add: true }]` | + +This command: +- accepts a register (by default, it uses `slash`). +- may be repeated with a given number of repetitions. +- takes an argument `add` of type `boolean`. + +## [`seek`](./seek.ts) + +Update selections based on the text surrounding them. + +### [`seek.seek`](./seek.ts#L16-L41) + +Select to character (excluded). + + +#### Variants + +| Title | Identifier | Keybinding | Command | +| ---------------------------------------- | -------------------------- | ---------------- | -------------------------------------------------------------- | +| Extend to character (excluded) | `extend` | `s-t` (normal) | `[".seek", { shift: "extend" }]` | +| Select to character (excluded, backward) | `backward` | `a-t` (normal) | `[".seek", { direction: -1 }]` | +| Extend to character (excluded, backward) | `extend.backward` | `s-a-t` (normal) | `[".seek", { shift: "extend", direction: -1 }]` | +| Select to character (included) | `included` | `f` (normal) | `[".seek", { include: true }]` | +| Extend to character (included) | `included.extend` | `s-f` (normal) | `[".seek", { include: true, shift: "extend" }]` | +| Select to character (included, backward) | `included.backward` | `a-f` (normal) | `[".seek", { include: true, direction: -1 }]` | +| Extend to character (included, backward) | `included.extend.backward` | `s-a-f` (normal) | `[".seek", { include: true, shift: "extend", direction: -1 }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `include` of type `boolean`. +- takes an input of type `string`. + +### [`seek.enclosing`](./seek.ts#L81-L101) + +Select to next enclosing character. + + +#### Variants + +| Title | Identifier | Keybinding | Command | +| -------------------------------------- | --------------------------- | ---------------- | --------------------------------------------------------- | +| Extend to next enclosing character | `enclosing.extend` | `s-m` (normal) | `[".seek.enclosing", { shift: "extend" }]` | +| Select to previous enclosing character | `enclosing.backward` | `a-m` (normal) | `[".seek.enclosing", { direction: -1 }]` | +| Extend to previous enclosing character | `enclosing.extend.backward` | `s-a-m` (normal) | `[".seek.enclosing", { shift: "extend", direction: -1 }]` | + +This command: +- takes an argument `open` of type `boolean`. +- takes an argument `pairs` of type `readonly string[]`. + +### [`seek.word`](./seek.ts#L158-L189) + +Select to next word start. + +Select the word and following whitespaces on the right of the end of each selection. + + +#### Variants + +| Title | Identifier | Keybinding | Command | +| -------------------------------------------- | ------------------------- | ---------------- | -------------------------------------------------------------------------------- | +| Extend to next word start | `word.extend` | `s-w` (normal) | `[".seek.word", { shift: "extend" }]` | +| Select to previous word start | `word.backward` | `b` (normal) | `[".seek.word", { direction: -1 }]` | +| Extend to previous word start | `word.extend.backward` | `s-b` (normal) | `[".seek.word", { shift: "extend", direction: -1 }]` | +| Select to next non-whitespace word start | `word.ws` | `a-w` (normal) | `[".seek.word", { ws: true }]` | +| Extend to next non-whitespace word start | `word.ws.extend` | `s-a-w` (normal) | `[".seek.word", { ws: true, shift: "extend" }]` | +| Select to previous non-whitespace word start | `word.ws.backward` | `a-b` (normal) | `[".seek.word", { ws: true, direction: -1 }]` | +| Extend to previous non-whitespace word start | `word.ws.extend.backward` | `s-a-b` (normal) | `[".seek.word", { ws: true, shift: "extend", direction: -1 }]` | +| Select to next word end | `wordEnd` | `e` (normal) | `[".seek.word", { stopAtEnd: true }]` | +| Extend to next word end | `wordEnd.extend` | `s-e` (normal) | `[".seek.word", { stopAtEnd: true , shift: "extend" }]` | +| Select to next non-whitespace word end | `wordEnd.ws` | `a-e` (normal) | `[".seek.word", { stopAtEnd: true , ws: true }]` | +| Extend to next non-whitespace word end | `wordEnd.ws.extend` | `s-a-e` (normal) | `[".seek.word", { stopAtEnd: true , ws: true, shift: "extend" }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `stopAtEnd` of type `boolean`. +- takes an argument `ws` of type `boolean`. + +### [`seek.object`](./seek.ts#L233-L273) + +Select object. + + +#### Object patterns +- Pairs: `(?#inner)`. +- Character sets: `[]+`. + - Can be preceded by `(?[]+)` and followed by +`(?[]+)` for whole objects. +- Matches that may only span a single line: `(?#singleline)`. +- Predefined: `(?#predefined=)`. + +#### Variants + +| Title | Identifier | Keybinding | Command | +| ---------------------------- | ------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------- | +| Select whole object | `askObject` | `a-a` (normal), `a-a` (insert) | `[".openMenu", { input: "object" }]` | +| Select inner object | `askObject.inner` | `a-i` (normal), `a-i` (insert) | `[".openMenu", { input: "object", pass: [{ inner: true }] }]` | +| Select to whole object start | `askObject.start` | `[` (normal) | `[".openMenu", { input: "object", pass: [{ where: "start" }] }]` | +| Extend to whole object start | `askObject.start` | `{` (normal) | `[".openMenu", { input: "object", pass: [{ where: "start", shift: "extend" }] }]` | +| Select to inner object start | `askObject.inner.start` | `a-[` (normal) | `[".openMenu", { input: "object", pass: [{ inner: true, where: "start" }] }]` | +| Extend to inner object start | `askObject.inner.start.extend` | `a-{` (normal) | `[".openMenu", { input: "object", pass: [{ inner: true, where: "start", shift: "extend" }] }]` | +| Select to whole object end | `askObject.end` | `]` (normal) | `[".openMenu", { input: "object", pass: [{ where: "end" }] }]` | +| Extend to whole object end | `askObject.end` | `}` (normal) | `[".openMenu", { input: "object", pass: [{ where: "end" , shift: "extend" }] }]` | +| Select to inner object end | `askObject.inner.end` | `a-]` (normal) | `[".openMenu", { input: "object", pass: [{ inner: true, where: "end" }] }]` | +| Extend to inner object end | `askObject.inner.end.extend` | `a-}` (normal) | `[".openMenu", { input: "object", pass: [{ inner: true, where: "end" , shift: "extend" }] }]` | + +This command: +- takes an argument `inner` of type `boolean`. +- takes an argument `where` of type `"start" | "end"`. +- takes an input of type `string`. + +## [`select`](./select.ts) + +Update selections based on their position in the document. + +### [`select.buffer`](./select.ts#L14-L19) + +Select whole buffer. + +### [`select.vertically`](./select.ts#L32-L65) + +Select vertically. + + +#### Variants + +| Title | Identifier | Keybinding | Command | +| ----------- | ------------- | --------------------------------- | ------------------------------------------------------------ | +| Jump down | `down.jump` | `j` (normal) , `down` (normal) | `[".select.vertically", { direction: 1, shift: "jump" }]` | +| Extend down | `down.extend` | `s-j` (normal), `s-down` (normal) | `[".select.vertically", { direction: 1, shift: "extend" }]` | +| Jump up | `up.jump` | `k` (normal) , `up` (normal) | `[".select.vertically", { direction: -1, shift: "jump" }]` | +| Extend up | `up.extend` | `s-k` (normal), `s-up` (normal) | `[".select.vertically", { direction: -1, shift: "extend" }]` | + +The following keybindings are also defined: + +| Keybinding | Command | +| ------------------------------ | ----------------------------------------------------------- | +| `c-f` (normal), `c-f` (insert) | `[".select.vertically", { direction: 1, by: "page" }]` | +| `c-d` (normal), `c-d` (insert) | `[".select.vertically", { direction: 1, by: "halfPage" }]` | +| `c-b` (normal), `c-b` (insert) | `[".select.vertically", { direction: -1, by: "page" }]` | +| `c-u` (normal), `c-u` (insert) | `[".select.vertically", { direction: -1, by: "halfPage" }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `avoidEol` of type `boolean`. +- takes an argument `by` of type `"page" | "halfPage"`. + +### [`select.horizontally`](./select.ts#L188-L210) + +Select horizontally. + + +#### Variants + +| Title | Identifier | Keybinding | Command | +| ------------ | -------------- | ---------------------------------- | -------------------------------------------------------------- | +| Jump right | `right.jump` | `l` (normal) , `right` (normal) | `[".select.horizontally", { direction: 1, shift: "jump" }]` | +| Extend right | `right.extend` | `s-l` (normal), `s-right` (normal) | `[".select.horizontally", { direction: 1, shift: "extend" }]` | +| Jump left | `left.jump` | `h` (normal) , `left` (normal) | `[".select.horizontally", { direction: -1, shift: "jump" }]` | +| Extend left | `left.extend` | `s-h` (normal), `s-left` (normal) | `[".select.horizontally", { direction: -1, shift: "extend" }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `avoidEol` of type `boolean`. + +### [`select.to`](./select.ts#L251-L269) + +Select to. + +If a count is specified, this command will shift to the start of the given +line. If no count is specified, this command will shift open the `goto` menu. + +#### Variants + +| Title | Identifier | Keybinding | Command | +| --------- | ----------- | -------------- | ------------------------------------- | +| Go to | `to.jump` | `g` (normal) | `[".select.to", { shift: "jump" }]` | +| Extend to | `to.extend` | `s-g` (normal) | `[".select.to", { shift: "extend" }]` | + +This command: +- accepts an argument of type `object`. +- may be repeated with a given number of repetitions. + +### [`select.line.below`](./select.ts#L279-L284) + +Select line below. + + +This command: +- may be repeated with a given number of repetitions. + +### [`select.line.below.extend`](./select.ts#L309-L314) + +Extend to line below. + + +This command: +- may be repeated with a given number of repetitions. + +### [`select.line.above`](./select.ts#L341-L344) + +Select line above. + +This command: +- may be repeated with a given number of repetitions. + +### [`select.line.above.extend`](./select.ts#L368-L371) + +Extend to line above. + +This command: +- may be repeated with a given number of repetitions. + +### [`select.lineStart`](./select.ts#L417-L439) + +Select to line start. + + +#### Variants + +| Title | Identifier | Keybinding | Command | +| -------------------- | ------------------ | ----------------------------------- | ------------------------------------------------------------- | +| Jump to line start | `lineStart.jump` | | `[".select.lineStart", { shift: "jump" }]` | +| Extend to line start | `lineStart.extend` | `s-a-h` (normal), `s-home` (normal) | `[".select.lineStart", { shift: "extend" }]` | +| Jump to line start (skip blank) | `lineStart.skipBlank.jump` | | `[".select.lineStart", { skipBlank: true, shift: "jump" }]` | +| Extend to line start (skip blank) | `lineStart.skipBlank.extend` | | `[".select.lineStart", { skipBlank: true, shift: "extend" }]` | +| Jump to first line | `firstLine.jump` | | `[".select.lineStart", { count: 0, shift: "jump" }]` | +| Extend to first line | `firstLine.extend` | | `[".select.lineStart", { count: 0, shift: "extend" }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `skipBlank` of type `boolean`. + +### [`select.lineEnd`](./select.ts#L464-L482) + +Select to line end. + + +#### Variants + +| Title | Identifier | Keybinding | Command | +| ------------------------ | -------------------- | ---------------------------------- | ---------------------------------------------------------- | +| Extend to line end | `lineEnd.extend` | `s-a-l` (normal), `s-end` (normal) | `[".select.lineEnd", { shift: "extend" }]` | +| Jump to last character | `documentEnd.jump` | | `[".select.lineEnd", { count: MAX_INT, shift: "jump" }]` | +| Extend to last character | `documentEnd.extend` | | `[".select.lineEnd", { count: MAX_INT, shift: "extend" }]` | + +This command: +- may be repeated with a given number of repetitions. + +### [`select.lastLine`](./select.ts#L498-L508) + +Select to last line. + +#### Variants + +| Title | Identifier | Command | +| ------------------- | ----------------- | ------------------------------------------- | +| Jump to last line | `lastLine.jump` | `[".select.lastLine", { shift: "jump" }]` | +| Extend to last line | `lastLine.extend` | `[".select.lastLine", { shift: "extend" }]` | + +### [`select.firstVisibleLine`](./select.ts#L519-L529) + +Select to first visible line. + +#### Variants + +| Title | Identifier | Command | +| ---------------------------- | ------------------------- | --------------------------------------------------- | +| Jump to first visible line | `firstVisibleLine.jump` | `[".select.firstVisibleLine", { shift: "jump" }]` | +| Extend to first visible line | `firstVisibleLine.extend` | `[".select.firstVisibleLine", { shift: "extend" }]` | + +### [`select.middleVisibleLine`](./select.ts#L536-L546) + +Select to middle visible line. + +#### Variants + +| Title | Identifier | Command | +| ----------------------------- | -------------------------- | ---------------------------------------------------- | +| Jump to middle visible line | `middleVisibleLine.jump` | `[".select.middleVisibleLine", { shift: "jump" }]` | +| Extend to middle visible line | `middleVisibleLine.extend` | `[".select.middleVisibleLine", { shift: "extend" }]` | + +### [`select.lastVisibleLine`](./select.ts#L553-L563) + +Select to last visible line. + +#### Variants + +| Title | Identifier | Command | +| --------------------------- | ------------------------ | -------------------------------------------------- | +| Jump to last visible line | `lastVisibleLine.jump` | `[".select.lastVisibleLine", { shift: "jump" }]` | +| Extend to last visible line | `lastVisibleLine.extend` | `[".select.lastVisibleLine", { shift: "extend" }]` | + +### [`select.lastModification`](./select.ts#L570-L580) + +Select to last modification. + +#### Variants + +| Title | Identifier | Command | +| --------------------------- | ------------------------- | --------------------------------------------------- | +| Jump to last modification | `lastModification.jump` | `[".select.lastModification", { shift: "jump" }]` | +| Extend to last modification | `lastModification.extend` | `[".select.lastModification", { shift: "extend" }]` | + +## [`selections`](./selections.ts) + +Interacting with selections. + +### [`selections.saveText`](./selections.ts#L19-L28) + +Copy selections text. + + +This command: +- accepts a register (by default, it uses `dquote`). + +### [`selections.save`](./selections.ts#L32-L45) + +Save selections. + + +This command: +- accepts a register (by default, it uses `caret`). +- takes an argument `style` of type `object`. +- takes an argument `until` of type `AutoDisposable.Event[]`. + +### [`selections.restore`](./selections.ts#L72-L80) + +Restore selections. + + +This command: +- accepts a register (by default, it uses `caret`). + +### [`selections.restore.withCurrent`](./selections.ts#L91-L110) + +Combine register selections with current ones. + + +The following keybinding is also available: + +| Keybinding | Command | +| ---------------- | -------------------------------------------------------- | +| `s-a-z` (normal) | `[".selections.restore.withCurrent", { reverse: true }]` | + +See https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#marks + +This command: +- accepts a register (by default, it uses `caret`). +- takes an argument `reverse` of type `boolean`. + +### [`selections.pipe`](./selections.ts#L217-L239) + +Pipe selections. + +Run the specified command or code with the contents of each selection, and +save the result to a register. + + +See https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#changes-through-external-programs + +#### Additional commands + +| Title | Identifier | Keybinding | Commands | +| ------------------- | -------------- | -------------- | --------------------------------------------------------------------------- | +| Pipe and replace | `pipe.replace` | `|` (normal) | `[".selections.pipe"], [".edit.insert", { register: "|" }]` | +| Pipe and append | `pipe.append` | `!` (normal) | `[".selections.pipe"], [".edit.insert", { register: "|", where: "end" }]` | +| Pipe and prepend | `pipe.prepend` | `a-!` (normal) | `[".selections.pipe"], [".edit.insert", { register: "|", where: "start" }]` | + +This command: +- accepts a register (by default, it uses `pipe`). +- takes an input of type `string`. + +### [`selections.filter`](./selections.ts#L286-L308) + +Filter selections. + + +#### Variants + +| Title | Identifier | Keybinding | Commands | +| -------------------------- | ----------------------- | ------------------ | -------------------------------------------------------------- | +| Keep matching selections | `filter.regexp` | `a-k` (normal) | `[".selections.filter", { defaultInput: "/" }]` | +| Clear matching selections | `filter.regexp.inverse` | `s-a-k` (normal) | `[".selections.filter", { defaultInput: "/", inverse: true }]` | +| Clear secondary selections | `clear.secondary` | `space` (normal) | `[".selections.filter", { input: "i === 0" }]` | +| Clear main selections | `clear.main` | `a-space` (normal) | `[".selections.filter", { input: "i !== 0" }]` | + +This command: +- takes an argument `defaultInput` of type `string`. +- takes an argument `interactive` of type `boolean`. +- takes an argument `inverse` of type `boolean`. +- takes an input of type `Input`. + +### [`selections.select`](./selections.ts#L343-L354) + +Select within selections. + + +This command: +- takes an argument `interactive` of type `boolean`. +- takes an input of type `Input`. + +### [`selections.split`](./selections.ts#L373-L385) + +Split selections. + + +This command: +- takes an argument `excludeEmpty` of type `boolean`. +- takes an argument `interactive` of type `boolean`. +- takes an input of type `Input`. + +### [`selections.splitLines`](./selections.ts#L408-L418) + +Split selections at line boundaries. + + +This command: +- may be repeated with a given number of repetitions. + +### [`selections.expandToLines`](./selections.ts#L459-L466) + +Expand to lines. + +Expand selections to contain full lines (including end-of-line characters). + +### [`selections.trimLines`](./selections.ts#L493-L500) + +Trim lines. + +Trim selections to only contain full lines (from start to line break). + +### [`selections.trimWhitespace`](./selections.ts#L525-L532) + +Trim whitespace. + +Trim whitespace at beginning and end of selections. + +### [`selections.reduce`](./selections.ts#L551-L569) + +Reduce selections to their cursor. + + + +#### Variant + +| Title | Identifier | Keybinding | Command | +| ------------------------------- | -------------- | ---------------- | ------------------------------------------- | +| Reduce selections to their ends | `reduce.edges` | `s-a-s` (normal) | `[".selections.reduce", { where: "both" }]` | + +This command: +- takes an argument `where` of type `"active" | "anchor" | "start" | "end" | "both"`. + +### [`selections.changeDirection`](./selections.ts#L606-L621) + +Change direction of selections. + + + +#### Variants + +| Title | Identifier | Keybinding | Command | +| ------------------- | -------------- | -------------- | ---------------------------------------------------- | +| Forward selections | `faceForward` | `a-:` (normal) | `[".selections.changeDirection", { direction: 1 }]` | +| Backward selections | `faceBackward` | | `[".selections.changeDirection", { direction: -1 }]` | + +### [`selections.copy`](./selections.ts#L646-L664) + +Copy selections below. + + +#### Variant + +| Title | Identifier | Keybinding | Command | +| --------------------- | ------------ | ---------------- | ----------------------------------------- | +| Copy selections above | `copy.above` | `s-a-c` (normal) | `[".selections.copy", { direction: -1 }]` | + +This command: +- may be repeated with a given number of repetitions. + +### [`selections.merge`](./selections.ts#L698-L703) + +Merge contiguous selections. + +### [`selections.open`](./selections.ts#L707-L710) + +Open selected file. + +### [`selections.toggleIndices`](./selections.ts#L725-L742) + +Toggle selection indices. + + +#### Variants + +| Title | Identifier | Command | +| ---------------------- | ------------- | --------------------------------------------------- | +| Show selection indices | `showIndices` | `[".selections.toggleIndices", { display: true }]` | +| Hide selection indices | `hideIndices` | `[".selections.toggleIndices", { display: false }]` | + +This command: +- takes an argument `display` of type `boolean | undefined`. +- takes an argument `until` of type `AutoDisposable.Event[]`. + +## [`selections.rotate`](./selections.rotate.ts) + +Rotate selection indices and contents. + +### [`selections.rotate.both`](./selections.rotate.ts#L9-L20) + +Rotate selections clockwise. + + +The following keybinding is also available: + +| Title | Identifier | Keybinding | Command | +| ----------------------------------- | -------------- | ------------ | ------------------------------------------- | +| Rotate selections counter-clockwise | `both.reverse` | `)` (normal) | `[".selections.rotate", { reverse: true }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `reverse` of type `boolean`. + +### [`selections.rotate.contents`](./selections.rotate.ts#L28-L37) + +Rotate selections clockwise (contents only). + +The following command is also available: + +| Title | Identifier | Command | +| --------------------------------------------------- | ------------------ | ---------------------------------------------------- | +| Rotate selections counter-clockwise (contents only) | `contents.reverse` | `[".selections.rotate.contents", { reverse: true }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `reverse` of type `boolean`. + +### [`selections.rotate.selections`](./selections.rotate.ts#L45-L56) + +Rotate selections clockwise (selections only). + + +The following keybinding is also available: + +| Title | Identifier | Keybinding | Command | +| ----------------------------------------------------- | -------------------- | -------------- | ------------------------------------------------------ | +| Rotate selections counter-clockwise (selections only) | `selections.reverse` | `a-)` (normal) | `[".selections.rotate.selections", { reverse: true }]` | + +This command: +- may be repeated with a given number of repetitions. +- takes an argument `reverse` of type `boolean`. + +## [`view`](./view.ts) + +Moving the editor view. + +#### Predefined keybindings + +| Title | Keybinding | Command | +| ----------------------- | -------------- | ------------------------------------------------ | +| Show view menu | `v` (normal) | `[".openMenu", { input: "view" }]` | +| Show view menu (locked) | `s-v` (normal) | `[".openMenu", { input: "view", locked: true }]` | + +### [`view.line`](./view.ts#L18-L24) + +Reveals a position based on the main cursor. + +This command: +- takes an argument `at` of type `"top" | "center" | "bottom"`. diff --git a/src/commands/changes.ts b/src/commands/changes.ts deleted file mode 100644 index 54be25a..0000000 --- a/src/commands/changes.ts +++ /dev/null @@ -1,205 +0,0 @@ -// Changes -// https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#changes -import * as vscode from "vscode"; - -import { Command, CommandFlags, registerCommand } from "."; - -registerCommand(Command.join, CommandFlags.Edit, () => { - return vscode.commands.executeCommand("editor.action.joinLines").then(() => void 0); -}); - -registerCommand( - Command.joinSelect, - CommandFlags.ChangeSelections | CommandFlags.Edit, - ({ editor }, _, undoStops) => { - // Select all line endings. - const selections = editor.selections, - len = selections.length, - newSelections = [] as vscode.Selection[], - document = editor.document; - - for (let i = 0; i < len; i++) { - const selection = selections[i], - startLine = selection.start.line, - endLine = selection.end.line, - startAnchor = new vscode.Position(startLine, Number.MAX_SAFE_INTEGER), - startActive = new vscode.Position( - startLine + 1, - document.lineAt(startLine + 1).firstNonWhitespaceCharacterIndex, - ); - - newSelections.push(new vscode.Selection(startAnchor, startActive)); - - for (let line = startLine + 1; line < endLine; line++) { - const anchor = new vscode.Position(line, Number.MAX_SAFE_INTEGER), - active = new vscode.Position( - line + 1, - document.lineAt(line + 1).firstNonWhitespaceCharacterIndex, - ); - - newSelections.push(new vscode.Selection(anchor, active)); - } - } - - editor.selections = newSelections; - - // Replace all line endings by spaces. - return editor - .edit((builder) => { - for (const selection of editor.selections) { - builder.replace(selection, " "); - } - }, undoStops) - .then(() => void 0); - }, -); - -function getSelectionsLines(selections: vscode.Selection[]) { - const lines: number[] = []; - - for (const selection of selections) { - const startLine = selection.start.line; - let endLine = selection.end.line; - - if (startLine !== endLine && selection.end.character === 0) { - // If the selection ends after a line break, do not consider the next line - // selected. This is because a selection has to end on the very first - // caret position of the next line in order to select the last line break. - // For example, `vscode.TextLine.rangeIncludingLineBreak` does this: - // https://github.com/microsoft/vscode/blob/c8b27b9db6afc26cf82cf07a9653c89cdd930f6a/src/vs/workbench/api/common/extHostDocumentData.ts#L273 - endLine--; - } - - // The first and last lines of the selection may contain other selections, - // so we check for duplicates with them. However, the intermediate - // lines are known to belong to one selection only, so there's no need - // for that with them. - if (lines.indexOf(startLine) === -1) { - lines.push(startLine); - } - - for (let i = startLine + 1; i < endLine; i++) { - lines.push(i); - } - - if (lines.indexOf(endLine) === -1) { - lines.push(endLine); - } - } - - return lines; -} - -function indent(editor: vscode.TextEditor, ignoreEmpty: boolean) { - return editor - .edit((builder) => { - const indent = editor.options.insertSpaces === true - ? " ".repeat(editor.options.tabSize as number) - : "\t"; - - for (const i of getSelectionsLines(editor.selections)) { - if (ignoreEmpty && editor.document.lineAt(i).isEmptyOrWhitespace) { - continue; - } - - builder.insert(new vscode.Position(i, 0), indent); - } - }) - .then(() => void 0); -} - -registerCommand(Command.indent, CommandFlags.Edit, ({ editor }) => indent(editor, true)); -registerCommand(Command.indentWithEmpty, CommandFlags.Edit, ({ editor }) => indent(editor, false)); - -function deindent(editor: vscode.TextEditor, repetitions: number, further: boolean) { - return editor - .edit((builder) => { - const doc = editor.document; - const tabSize = editor.options.tabSize as number; - - // Number of blank characters needed to deindent: - const needed = repetitions * tabSize; - - for (const i of getSelectionsLines(editor.selections)) { - const line = doc.lineAt(i), - text = line.text; - - let column = 0, // Column, accounting for tab size - j = 0; // Index in source line, and number of characters to remove - - for (; column < needed; j++) { - const char = text[j]; - - if (char === "\t") { - column += tabSize; - } else if (char === " ") { - column++; - } else { - break; - } - } - - if (further && column === needed && j < text.length) { - // TODO - } - - if (j !== 0) { - builder.delete(line.range.with(undefined, line.range.start.translate(0, j))); - } - } - }) - .then(() => void 0); -} - -registerCommand(Command.deindent, CommandFlags.Edit, ({ editor }, { repetitions }) => - deindent(editor, repetitions, false), -); -registerCommand(Command.deindentFurther, CommandFlags.Edit, ({ editor }, { repetitions }) => - deindent(editor, repetitions, true), -); - -registerCommand(Command.toLowerCase, CommandFlags.Edit, ({ editor }) => - editor - .edit((builder) => { - const doc = editor.document; - - for (const selection of editor.selections) { - builder.replace(selection, doc.getText(selection).toLocaleLowerCase()); - } - }) - .then(() => void 0), -); - -registerCommand(Command.toUpperCase, CommandFlags.Edit, ({ editor }) => - editor - .edit((builder) => { - const doc = editor.document; - - for (const selection of editor.selections) { - builder.replace(selection, doc.getText(selection).toLocaleUpperCase()); - } - }) - .then(() => void 0), -); - -registerCommand(Command.swapCase, CommandFlags.Edit, ({ editor }) => - editor - .edit((builder) => { - const doc = editor.document; - - for (const selection of editor.selections) { - const text = doc.getText(selection); - let builtText = ""; - - for (let i = 0; i < text.length; i++) { - const x = text[i], - loCase = x.toLocaleLowerCase(); - - builtText += loCase === x ? x.toLocaleUpperCase() : loCase; - } - - builder.replace(selection, builtText); - } - }) - .then(() => void 0), -); diff --git a/src/commands/count.ts b/src/commands/count.ts deleted file mode 100644 index 623f0ca..0000000 --- a/src/commands/count.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Command, CommandFlags, registerCommand } from "."; - -for (let i = 0; i < 10; i++) { - const j = i; - - registerCommand( - ("dance.count." + j) as Command, - CommandFlags.IgnoreInHistory, - (_, { extension }) => { - extension.currentCount = extension.currentCount * 10 + j; - }, - ); -} diff --git a/src/commands/dev.ts b/src/commands/dev.ts new file mode 100644 index 0000000..07dd833 --- /dev/null +++ b/src/commands/dev.ts @@ -0,0 +1,31 @@ +import { Argument } from "."; +import { Extension } from "../state/extension"; +import { SelectionBehavior } from "../state/modes"; + +/** + * Developer utilities for Dance. + */ +declare module "./dev"; + +/** + * Set the selection behavior of the specified mode. + */ +export function setSelectionBehavior( + extension: Extension, + + mode: Argument, + value?: Argument<"caret" | "character">, +) { + const selectedMode = extension.modes.get(mode); + + if (selectedMode !== undefined) { + if (value === undefined) { + value = selectedMode.selectionBehavior === SelectionBehavior.Caret ? "character" : "caret"; + } + + selectedMode.update( + "_selectionBehavior", + value === "character" ? SelectionBehavior.Character : SelectionBehavior.Caret, + ); + } +} diff --git a/src/commands/edit.ts b/src/commands/edit.ts new file mode 100644 index 0000000..fb91ff8 --- /dev/null +++ b/src/commands/edit.ts @@ -0,0 +1,405 @@ +import * as vscode from "vscode"; +import * as api from "../api"; + +import { + Context, + deindentLines, + edit, + indentLines, + joinLines, + keypress, + LengthMismatchError, + replace, + Selections, + selectionsLines, + setSelections, +} from "../api"; +import { Register } from "../state/registers"; +import { Argument, InputOr, RegisterOr } from "."; + +/** + * Perform changes on the text content of the document. + * + * See https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#changes. + */ +declare module "./edit"; + +/** + * Insert contents of register. + * + * A `where` argument may be specified to state where the text should be + * inserted relative to each selection. If unspecified, each selection will be + * replaced by the text. + * + * @keys `s-a-r` (normal) + * + * #### Additional commands + * + * | Title | Identifier | Keybinding | Commands | + * | ---------------------------------- | ----------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------- | + * | Pick register and replace | `selectRegister-insert` | `c-r` (normal), `c-r` (insert) | `[".selectRegister"], [".edit.insert"]` | + * | Paste before | `paste.before` | `s-p` (normal) | `[".edit.insert", { handleNewLine: true, where: "start" }]` | + * | Paste after | `paste.after` | `p` (normal) | `[".edit.insert", { handleNewLine: true, where: "end" }]` | + * | Paste before and select | `paste.before.select` | `s-a-p` (normal) | `[".edit.insert", { handleNewLine: true, where: "start", select: true }]` | + * | Paste after and select | `paste.after.select` | `a-p` (normal) | `[".edit.insert", { handleNewLine: true, where: "end" , select: true }]` | + * | Delete | `delete` | `a-d` (normal) | `[".edit.insert", { register: "_" }]` | + * | Delete and switch to Insert | `delete-insert` | `a-c` (normal) | `[".modes.set", { input: "insert" }], [".edit.insert", { register: "_" }]` | + * | Copy and delete | `yank-delete` | `d` (normal) | `[".selections.saveText"], [".edit.insert", { register: "_" }]` | + * | Copy and replace | `yank-replace` | `s-r` (normal) | `[".selections.saveText"], [".edit.insert"]` | + * | Copy, delete and switch to Insert | `yank-delete-insert` | `c` (normal) | `[".selections.saveText"], [".modes.set", { input: "insert" }], [".edit.insert", { register: "_" }]` | + */ +export async function insert( + _: Context, + selections: readonly vscode.Selection[], + register: RegisterOr<"dquote", Register.Flags.CanRead>, + + adjust: Argument = false, + handleNewLine: Argument = false, + select: Argument = false, + where?: Argument<"active" | "anchor" | "start" | "end" | undefined>, +) { + let contents = await register.get(); + + if (contents === undefined) { + throw new Error(`register "${register.name}" does not contain any saved text`); + } + + if (adjust) { + contents = extendArrayToLength(contents, selections.length); + } else { + LengthMismatchError.throwIfLengthMismatch(selections, contents); + } + + if (where === undefined) { + Selections.set(await replace.byIndex((i) => contents![i], selections)); + return; + } + + if (!["active", "anchor", "start", "end"].includes(where)) { + throw new Error(`"where" must be one of "active", "anchor", "start", "end", or undefined`); + } + + const flags = api.insert.flagsAtEdge(where) | (select ? api.insert.Select : api.insert.Keep); + + Selections.set( + handleNewLine + ? await api.insert.byIndex.withFullLines(flags, (i) => contents![i], selections) + : await api.insert.byIndex(flags, (i) => contents![i], selections), + ); +} + +/** + * Join lines. + * + * @keys `a-j` (normal) + */ +export function join(_: Context, separator?: Argument) { + return joinLines(selectionsLines(), separator); +} + +/** + * Join lines and select inserted separators. + * + * @keys `s-a-j` (normal) + */ +export function join_select(_: Context, separator?: Argument) { + return joinLines(selectionsLines(), separator).then(setSelections); +} + +/** + * Indent selected lines. + * + * @keys `>` (normal) + */ +export function indent(_: Context, repetitions: number) { + return indentLines(selectionsLines(), repetitions, /* indentEmpty= */ false); +} + +/** + * Indent selected lines (including empty lines). + * + * @keys `a->` (normal) + */ +export function indent_withEmpty(_: Context, repetitions: number) { + return indentLines(selectionsLines(), repetitions, /* indentEmpty= */ true); +} + +/** + * Deindent selected lines. + * + * @keys `a-<` (normal) + */ +export function deindent(_: Context, repetitions: number) { + return deindentLines(selectionsLines(), repetitions, /* deindentIncomplete= */ false); +} + +/** + * Deindent selected lines (including incomplete indent). + * + * @keys `<` (normal) + */ +export function deindent_withIncomplete(_: Context, repetitions: number) { + return deindentLines(selectionsLines(), repetitions, /* deindentIncomplete= */ true); +} + +/** + * Transform to lower case. + * + * @keys `` ` `` (normal) + */ +export function case_toLower(_: Context) { + return replace((text) => text.toLocaleLowerCase()); +} + +/** + * Transform to upper case. + * + * @keys `` s-` `` (normal) + */ +export function case_toUpper(_: Context) { + return replace((text) => text.toLocaleUpperCase()); +} + +/** + * Swap case. + * + * @keys `` a-` `` (normal) + */ +export function case_swap(_: Context) { + return replace((text) => { + let builtText = ""; + + for (let i = 0, len = text.length; i < len; i++) { + const x = text[i], + loCase = x.toLocaleLowerCase(); + + builtText += loCase === x ? x.toLocaleUpperCase() : loCase; + } + + return builtText; + }); +} + +/** + * Replace characters. + * + * @keys `r` (normal) + */ +export async function replaceCharacters( + _: Context, + repetitions: number, + inputOr: InputOr, +) { + const input = (await inputOr(() => keypress(_))).repeat(repetitions); + + return _.run(() => edit((editBuilder, selections, document) => { + for (const selection of selections) { + let i = selection.start.line; + + if (selection.end.line === i) { + // A single line-selection; replace the selection directly + editBuilder.replace( + selection, + input!.repeat(selection.end.character - selection.start.character), + ); + + continue; + } + + // Replace in first line + const firstLine = document.lineAt(i).range.with(selection.start); + + editBuilder.replace( + firstLine, + input!.repeat(firstLine.end.character - firstLine.start.character), + ); + + // Replace in intermediate lines + while (++i < selection.end.line) { + const line = document.lineAt(i); + + editBuilder.replace(line.range, input!.repeat(line.text.length)); + } + + // Replace in last line + const lastLine = document.lineAt(i).range.with(undefined, selection.end); + + editBuilder.replace( + lastLine, + input!.repeat(lastLine.end.character - lastLine.start.character), + ); + } + })); +} + +/** + * Align selections. + * + * Align selections, aligning the cursor of each selection by inserting spaces + * before the first character of each selection. + * + * @keys `&` (normal) + */ +export function align( + _: Context, + selections: readonly vscode.Selection[], + + fill: Argument = " ", +) { + const startChar = selections.reduce( + (max, sel) => (sel.start.character > max ? sel.start.character : max), + 0, + ); + + return edit((builder, selections) => { + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i]; + + builder.insert(selection.start, fill.repeat(startChar - selection.start.character)); + } + }); +} + +/** + * Copy indentation. + * + * Copy the indentation of the main selection (or the count one if a count is + * given) to all other ones. + * + * @keys `a-&` (normal) + */ +export function copyIndentation( + _: Context, + document: vscode.TextDocument, + selections: readonly vscode.Selection[], + count: number, +) { + const sourceSelection = selections[count] ?? selections[0], + sourceIndent = document + .lineAt(sourceSelection.start) + .firstNonWhitespaceCharacterIndex; + + return edit((builder, selections, document) => { + for (let i = 0, len = selections.length; i < len; i++) { + if (i === sourceSelection.start.line) { + continue; + } + + const line = document.lineAt(selections[i].start), + indent = line.firstNonWhitespaceCharacterIndex; + + if (indent > sourceIndent) { + builder.delete( + line.range.with( + undefined, + line.range.start.translate(undefined, indent - sourceIndent), + ), + ); + } else if (indent < sourceIndent) { + builder.insert(line.range.start, " ".repeat(indent - sourceIndent)); + } + } + }); +} + +/** + * Insert new line above each selection. + * + * @keys `s-a-o` (normal) + * + * #### Additional keybindings + * + * | Title | Identifier | Keybinding | Commands | + * | ------------------------------------------ | ---------------------- | -------------- | -------------------------------------------------------------------------------- | + * | Insert new line above and switch to insert | `newLine.above.insert` | `s-o` (normal) | `[".modes.set", { input: "insert" }], [".edit.newLine.above", { select: true }]` | + */ +export function newLine_above(_: Context, select: Argument = false) { + if (select) { + Selections.update.byIndex(prepareSelectionForLineInsertion); + + // Use built-in `insertLineBefore` command. It gives us less control, but at + // least it handles indentation well. + return vscode.commands.executeCommand("editor.action.insertLineBefore"); + } + + return edit((builder, selections, document) => { + const newLine = document.eol === vscode.EndOfLine.LF ? "\n" : "\r\n", + processedLines = new Set(); + + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i], + activeLine = Selections.activeLine(selection); + + if (processedLines.size !== processedLines.add(activeLine).size) { + builder.insert(new vscode.Position(activeLine, 0), newLine); + } + } + }); +} + +/** + * Insert new line below each selection. + * + * @keys `a-o` (normal) + * + * #### Additional keybindings + * + * | Title | Identifier | Keybinding | Commands | + * | ------------------------------------------ | ---------------------- | ------------ | -------------------------------------------------------------------------------- | + * | Insert new line below and switch to insert | `newLine.below.insert` | `o` (normal) | `[".modes.set", { input: "insert" }], [".edit.newLine.below", { select: true }]` | + */ +export function newLine_below(_: Context, select: Argument = false) { + if (select) { + Selections.update.byIndex(prepareSelectionForLineInsertion); + + // Use built-in `insertLineAfter` command. It gives us less control, but at + // least it handles indentation well. + return vscode.commands.executeCommand("editor.action.insertLineAfter"); + } + + return edit((builder, selections, document) => { + const newLine = document.eol === vscode.EndOfLine.LF ? "\n" : "\r\n", + processedLines = new Set(); + + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i], + activeLine = Selections.activeLine(selection); + + if (processedLines.size !== processedLines.add(activeLine).size) { + builder.insert(new vscode.Position(activeLine + 1, 0), newLine); + } + } + }); +} + +function prepareSelectionForLineInsertion(_: number, selection: vscode.Selection) { + const activeLine = Selections.activeLine(selection); + + if (selection.active.line !== activeLine) { + return new vscode.Selection(selection.anchor, selection.active.with(activeLine)); + } + + return selection; +} + +function extendArrayToLength(array: readonly T[], length: number) { + const arrayLen = array.length; + + if (length > arrayLen) { + const newArray = [] as T[]; + + for (let i = 0; i < arrayLen; i++) { + newArray.push(array[i]); + } + + const last = array[arrayLen - 1]; + + for (let i = arrayLen; i < length; i++) { + newArray.push(last); + } + + return newArray; + } else { + return array.slice(0, length); + } +} diff --git a/src/commands/goto.ts b/src/commands/goto.ts deleted file mode 100644 index e90dbae..0000000 --- a/src/commands/goto.ts +++ /dev/null @@ -1,272 +0,0 @@ -import * as path from "path"; -import * as vscode from "vscode"; - -import { Command, CommandFlags, CommandState, InputKind, registerCommand } from "."; -import { EditorState } from "../state/editor"; -import { SelectionBehavior } from "../state/extension"; -import { - CoordMapper, - DoNotExtend, - Extend, - ExtendBehavior, - SelectionHelper, - jumpTo, -} from "../utils/selectionHelper"; - -const getMenu = (name: string) => (editorState: EditorState) => { - const menuItems = editorState.extension.menus.get(name)!.items; - - return Object.entries(menuItems).map((x) => [x[0], x[1].text]) as [string, string][]; -}; - -const executeMenuItem = async (editorState: EditorState, name: string, i: number) => { - const menuItems = editorState.extension.menus.get(name)!.items; - const menuItem = Object.values(menuItems)[i]; - - await vscode.commands.executeCommand(menuItem.command, menuItem.args); -}; - -// TODO: Make just merely opening the menu not count as a command execution -// and do not record it. The count+goto version (e.g. `10g`) should still count. -registerCommand( - Command.goto, - CommandFlags.ChangeSelections, - InputKind.ListOneItemOrCount, - getMenu("goto"), - (editorState, state) => { - if (state.input === null) { - const { editor } = editorState, - { document } = editor; - let line = state.currentCount - 1; - - if (line >= document.lineCount) { - line = document.lineCount - 1; - } - - const active = new vscode.Position(line, 0), - anchor = new vscode.Position(line, 0); - - editor.selections = [new vscode.Selection(anchor, active)]; - - return; - } else { - return executeMenuItem(editorState, "goto", state.input); - } - }, -); - -// TODO: Make just merely opening the menu not count as a command execution -// and do not record it. The count+goto version (e.g. `10G`) should still count. -registerCommand( - Command.gotoExtend, - CommandFlags.ChangeSelections, - InputKind.ListOneItemOrCount, - getMenu("goto.extend"), - (editorState, state) => { - if (state.input === null) { - const { editor } = editorState, - { document, selection } = editor; - let line = state.currentCount - 1; - - if (line >= document.lineCount) { - line = document.lineCount - 1; - } - - const anchor = selection.anchor, - active = new vscode.Position(line, 0); - - editor.selections = [new vscode.Selection(anchor, active)]; - - return; - } else { - return executeMenuItem(editorState, "goto.extend", state.input); - } - }, -); - -const toStartCharacterFunc: CoordMapper = (from, { editorState }, i) => { - editorState.preferredColumns[i] = 0; - - return from.with(undefined, 0); -}; -const toFirstNonBlankCharacterFunc: CoordMapper = (from, { editor, editorState }, i) => { - const column = editor.document.lineAt(from).firstNonWhitespaceCharacterIndex; - - editorState.preferredColumns[i] = column; - - return from.with(undefined, column); -}; -const toEndCharacterFunc: CoordMapper = (from, helper, i) => { - const lineLen = helper.editor.document.lineAt(from).text.length; - - helper.editorState.preferredColumns[i] = Number.MAX_SAFE_INTEGER; - - if (lineLen === 0 || helper.selectionBehavior === SelectionBehavior.Caret) { - return from.with(undefined, lineLen); - } else { - return from.with(undefined, lineLen - 1); - } -}; - -function toCharacter(func: CoordMapper, extend: ExtendBehavior) { - const mapper = jumpTo(func, extend); - // TODO: Should also reveal selection active(s) after moving. - return (editorState: EditorState, commandState: CommandState) => - SelectionHelper.for(editorState, commandState).mapEach(mapper); -} - -registerCommand( - Command.gotoLineStart, - CommandFlags.ChangeSelections, - toCharacter(toStartCharacterFunc, DoNotExtend), -); -registerCommand( - Command.gotoLineStartExtend, - CommandFlags.ChangeSelections, - toCharacter(toStartCharacterFunc, Extend), -); -registerCommand( - Command.gotoLineStartNonBlank, - CommandFlags.ChangeSelections, - toCharacter(toFirstNonBlankCharacterFunc, DoNotExtend), -); -registerCommand( - Command.gotoLineStartNonBlankExtend, - CommandFlags.ChangeSelections, - toCharacter(toFirstNonBlankCharacterFunc, Extend), -); -registerCommand( - Command.gotoLineEnd, - CommandFlags.ChangeSelections, - toCharacter(toEndCharacterFunc, DoNotExtend), -); -registerCommand( - Command.gotoLineEndExtend, - CommandFlags.ChangeSelections, - toCharacter(toEndCharacterFunc, Extend), -); - -const toFirstVisibleLineFunc: CoordMapper = (from, { editor }) => - from.with(editor.visibleRanges[0].start.line, 0); - -const toLastVisibleLineFunc: CoordMapper = (from, { editor }) => - from.with(editor.visibleRanges[0].end.line, 0); - -const toMiddleVisibleLineFunc: CoordMapper = (from, { editor }) => - from.with(((editor.visibleRanges[0].end.line + editor.visibleRanges[0].start.line) / 2) | 0, 0); - -registerCommand( - Command.gotoFirstVisibleLine, - CommandFlags.ChangeSelections, - toCharacter(toFirstVisibleLineFunc, DoNotExtend), -); -registerCommand( - Command.gotoFirstVisibleLineExtend, - CommandFlags.ChangeSelections, - toCharacter(toFirstVisibleLineFunc, Extend), -); -registerCommand( - Command.gotoMiddleVisibleLine, - CommandFlags.ChangeSelections, - toCharacter(toMiddleVisibleLineFunc, DoNotExtend), -); -registerCommand( - Command.gotoMiddleVisibleLineExtend, - CommandFlags.ChangeSelections, - toCharacter(toMiddleVisibleLineFunc, Extend), -); -registerCommand( - Command.gotoLastVisibleLine, - CommandFlags.ChangeSelections, - toCharacter(toLastVisibleLineFunc, DoNotExtend), -); -registerCommand( - Command.gotoLastVisibleLineExtend, - CommandFlags.ChangeSelections, - toCharacter(toLastVisibleLineFunc, Extend), -); - -const toFirstLineFunc: CoordMapper = () => new vscode.Position(0, 0); - -const toLastLineStartFunc: CoordMapper = (_, helper) => { - const document = helper.editor.document; - let line = document.lineCount - 1; - - // In case of trailing line break, go to the second last line. - if (line > 0 && document.lineAt(document.lineCount - 1).text.length === 0) { - line--; - } - - return new vscode.Position(line, 0); -}; - -// TODO: Also need to set preferredColumn to max. -const toLastLineEndFunc: CoordMapper = (_, helper) => { - const document = helper.editor.document; - const line = document.lineCount - 1; - const lineLen = document.lineAt(document.lineCount - 1).text.length; - return new vscode.Position(line, lineLen); -}; - -registerCommand( - Command.gotoFirstLine, - CommandFlags.ChangeSelections, - toCharacter(toFirstLineFunc, DoNotExtend), -); -registerCommand( - Command.gotoFirstLineExtend, - CommandFlags.ChangeSelections, - toCharacter(toFirstLineFunc, Extend), -); - -registerCommand( - Command.gotoLastLine, - CommandFlags.ChangeSelections, - toCharacter(toLastLineStartFunc, DoNotExtend), -); -registerCommand( - Command.gotoLastLineExtend, - CommandFlags.ChangeSelections, - toCharacter(toLastLineStartFunc, Extend), -); -registerCommand( - Command.gotoLastCharacter, - CommandFlags.ChangeSelections, - toCharacter(toLastLineEndFunc, DoNotExtend), -); -registerCommand( - Command.gotoLastCharacterExtend, - CommandFlags.ChangeSelections, - toCharacter(toLastLineEndFunc, Extend), -); - -registerCommand(Command.gotoSelectedFile, CommandFlags.ChangeSelections, ({ editor }) => { - const basePath = path.dirname(editor.document.fileName); - - return Promise.all(editor.selections.map((selection) => { - const filename = editor.document.getText(selection), - filepath = path.resolve(basePath, filename); - - return vscode.workspace.openTextDocument(filepath).then(vscode.window.showTextDocument); - })).then(() => void 0); -}); - -function toLastBufferModification(editorState: EditorState, extend: ExtendBehavior) { - const { documentState, editor } = editorState; - - if (documentState.recordedChanges.length > 0) { - const range = documentState.recordedChanges[documentState.recordedChanges.length - 1].range, - selection = range.selection(documentState.document); - - editor.selection = extend - ? new vscode.Selection(editor.selection.anchor, selection.active) - : selection; - } -} - -registerCommand(Command.gotoLastModification, CommandFlags.ChangeSelections, (editorState) => - toLastBufferModification(editorState, DoNotExtend), -); -registerCommand(Command.gotoLastModificationExtend, CommandFlags.ChangeSelections, (editorState) => - toLastBufferModification(editorState, Extend), -); diff --git a/src/commands/history.ts b/src/commands/history.ts index 55d67c8..68c0461 100644 --- a/src/commands/history.ts +++ b/src/commands/history.ts @@ -1,100 +1,198 @@ import * as vscode from "vscode"; -import { Command, CommandDescriptor, CommandFlags, registerCommand } from "."; +import { Argument, CommandDescriptor, RegisterOr } from "."; +import { ArgumentError, Context } from "../api"; +import { Register } from "../state/registers"; +import { ActiveRecording, Recorder, Recording } from "../state/recorder"; -registerCommand( - Command.historyUndo, - CommandFlags.ChangeSelections | CommandFlags.Edit | CommandFlags.IgnoreInHistory, - () => { - return vscode.commands.executeCommand("undo"); - }, -); +/** + * Interact with history. + */ +declare module "./history"; -registerCommand( - Command.historyRedo, - CommandFlags.ChangeSelections | CommandFlags.Edit | CommandFlags.IgnoreInHistory, - () => { - return vscode.commands.executeCommand("redo"); - }, -); +/** + * Undo. + * + * @keys `u` (normal) + */ +export function undo() { + return vscode.commands.executeCommand("undo"); +} -registerCommand( - Command.historyRepeat, - CommandFlags.ChangeSelections | CommandFlags.Edit | CommandFlags.IgnoreInHistory, - (editorState) => { - const commands = editorState.recordedCommands; +/** + * Redo. + * + * @keys `s-u` (normal) + */ +export function redo() { + return vscode.commands.executeCommand("redo"); +} - if (commands.length === 0) { - return; +/** + * Undo a change of selections. + * + * @keys `a-u` (normal) + */ +export function undo_selections() { + return vscode.commands.executeCommand("cursorUndo"); +} + +/** + * Redo a change of selections. + * + * @keys `s-a-u` (normal) + */ +export function redo_selections() { + return vscode.commands.executeCommand("cursorRedo"); +} + +/** + * Repeat last change. + * + * @noreplay + * + * | Title | Identifier | Keybinding | Commands | + * | ---------------------------- | ------------------ | -------------- | --------------------------------------------------------------------------- | + * | Repeat last selection change | `repeat.selection` | | `[".history.repeat", { include: "dance\\.(seek|select|selections)\\..+" }]` | + * | Repeat last seek | `repeat.seek` | `a-.` (normal) | `[".history.repeat", { include: "dance\\.seek\\..+" }]` | + */ +export async function repeat( + _: Context, + repetitions: number, + + include: Argument = /.+/, +) { + if (typeof include === "string") { + include = new RegExp(include, "u"); + } + + let commandDescriptor: CommandDescriptor, + commandArgument: object; + + const cursor = _.extension.recorder.cursorFromEnd(); + + for (;;) { + if (cursor.is(Recording.ActionType.Command) + && include.test(cursor.commandDescriptor().identifier)) { + commandDescriptor = cursor.commandDescriptor(); + commandArgument = cursor.commandArgument(); + break; } - const lastCommandState = commands[commands.length - 1]; - - return CommandDescriptor.execute(editorState, lastCommandState); - }, -); - -registerCommand( - Command.historyRepeatSelection, - CommandFlags.ChangeSelections | CommandFlags.IgnoreInHistory, - (editorState) => { - const commands = editorState.recordedCommands; - - for (let i = commands.length - 1; i >= 0; i--) { - const commandState = commands[i]; - - if ( - commandState.descriptor.flags & CommandFlags.ChangeSelections - && !(commandState.descriptor.flags & CommandFlags.Edit) - ) { - return CommandDescriptor.execute(editorState, commandState); - } - } - - return; - }, -); - -registerCommand( - Command.historyRepeatEdit, - CommandFlags.Edit | CommandFlags.IgnoreInHistory, - (editorState) => { - const commands = editorState.recordedCommands; - - for (let i = commands.length - 1; i >= 0; i--) { - const commandState = commands[i]; - - if (commandState.descriptor.flags & CommandFlags.Edit) { - return CommandDescriptor.execute(editorState, commandState); - } - } - - return; - }, -); - -const ObjectOrSelectToCommands = new Set([ - Command.objectsPerformSelection, - Command.selectToExcluded, - Command.selectToExcludedBackwards, - Command.selectToExcludedExtend, - Command.selectToExcludedExtendBackwards, - Command.selectToIncluded, - Command.selectToIncludedBackwards, - Command.selectToIncludedExtend, - Command.selectToIncludedExtendBackwards, -]); - -registerCommand(Command.repeatObjectOrSelectTo, CommandFlags.ChangeSelections, (editorState) => { - const commands = editorState.recordedCommands; - - for (let i = commands.length - 1; i >= 0; i--) { - const commandState = commands[i]; - - if (ObjectOrSelectToCommands.has(commandState.descriptor.command)) { - return CommandDescriptor.execute(editorState, commandState); + if (!cursor.previous()) { + throw new Error("no previous command matching " + include); } } - return undefined; -}); + for (let i = 0; i < repetitions; i++) { + await commandDescriptor.replay(_, commandArgument); + } +} + +/** + * Repeat last edit without a command. + * + * @keys `.` (normal) + * @noreplay + */ +export async function repeat_edit(_: Context, repetitions: number) { + const recorder = _.extension.recorder, + cursor = recorder.cursorFromEnd(); + let startCursor: Recorder.Cursor | undefined, + endCursor: Recorder.Cursor | undefined; + + for (;;) { + if (cursor.is(Recording.ActionType.Command) + && cursor.commandDescriptor().identifier === "dance.modes.set") { + const modeName = cursor.commandArgument().input as string; + + if (modeName === "normal") { + endCursor = cursor.clone(); + } else if (modeName === "insert" && endCursor !== undefined) { + startCursor = cursor.clone(); + break; + } + } + + if (!cursor.previous()) { + throw new Error("cannot find switch to normal or insert mode"); + } + } + + // TODO: almost there, but not completely + for (let i = 0; i < repetitions; i++) { + for (let cursor = startCursor.clone(); cursor.isBeforeOrEqual(endCursor); cursor.next()) { + await cursor.replay(_); + } + } +} + +/** + * Replay recording. + * + * @keys `q` (normal) + * @noreplay + */ +export async function recording_play( + _: Context.WithoutActiveEditor, + + repetitions: number, + register: RegisterOr<"arobase", Register.Flags.CanReadWriteMacros>, +) { + const recording = register.getRecording(); + + ArgumentError.validate( + "recording", + recording !== undefined, + () => `register "${register.name}" does not hold a recording`, + ); + + for (let i = 0; i < repetitions; i++) { + await recording.replay(_); + } +} + +const recordingPerRegister = new WeakMap(); + +/** + * Start recording. + * + * @keys `s-q` (normal) + * @noreplay + */ +export function recording_start( + _: Context, + register: RegisterOr<"arobase", Register.Flags.CanReadWriteMacros>, +) { + ArgumentError.validate( + "register", + !recordingPerRegister.has(register), + "a recording is already active", + ); + + const recording = _.extension.recorder.startRecording(); + + recordingPerRegister.set(register, recording); +} + +/** + * Stop recording. + * + * @keys `escape` (normal, recording) + * @noreplay + */ +export function recording_stop( + _: Context, + register: RegisterOr<"arobase", Register.Flags.CanReadWriteMacros>, +) { + const recording = recordingPerRegister.get(register); + + ArgumentError.validate( + "register", + recording !== undefined, + "no recording is active in the given register", + ); + + recordingPerRegister.delete(register); + register.setRecording(recording.complete()); +} diff --git a/src/commands/index.ts b/src/commands/index.ts index d265e5b..6ae1ac4 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,534 +1,183 @@ import * as vscode from "vscode"; -import { Register } from "../registers"; -import { EditorState } from "../state/editor"; -import { Extension, Mode, SelectionBehavior } from "../state/extension"; -import { keypress, prompt, promptInList, promptRegex } from "../utils/prompt"; -import { Command } from "../../commands"; -import { DocumentStart } from "../utils/selectionHelper"; +import { Context, EditorRequiredError, insertUndoStop } from "../api"; +import { Extension } from "../state/extension"; +import { Register, Registers } from "../state/registers"; -export import Command = Command; +/** + * Indicates that a register is expected; if no register is given, the + * specified default should be given instead. + */ +export type RegisterOr<_Default extends keyof Registers, + Flags extends Register.Flags = Register.Flags.None> + = Register.WithFlags; -export const enum CommandFlags { - /** No particular flags. */ - None = 0, - - /** Switch to normal mode after operation. */ - SwitchToNormal = 1 << 1, - - /** Switch to insert mode after operation before the cursor. */ - SwitchToInsertBefore = 1 << 2, - - /** Switch to insert mode after operation after the cursor. */ - SwitchToInsertAfter = 1 << 3, - - /** Restore previous mode after operation. */ - RestoreMode = 1 << 4, - - /** Ignores the command in history. */ - IgnoreInHistory = 1 << 5, - - /** Edits the content of the editor. */ - Edit = 1 << 6, - - /** Changes the current selections. */ - ChangeSelections = 1 << 7, - - /** Do not reset preferred columns for moving up and down. */ - DoNotResetPreferredColumns = 1 << 8, - - /** Allow the command to be run without an active editor. */ - CanRunWithoutEditor = 1 << 9, -} - -export class CommandState { - private readonly _followingChanges = [] as vscode.TextDocumentContentChangeEvent[]; - - /** - * The number of times that a command should be repeated. - * - * Equivalent to `currentCount === 0 ? 1 : currentCount`. - */ - public get repetitions() { - const count = this.currentCount; - - return count === 0 ? 1 : count; - } - - /** - * The insert-mode changes that were recorded after invoking this command. - */ - public get followingChanges() { - return this._followingChanges as readonly vscode.TextDocumentContentChangeEvent[]; - } - - public readonly currentCount: number; - public readonly currentRegister?: Register; - public readonly selectionBehavior: SelectionBehavior; - - public constructor( - public readonly descriptor: CommandDescriptor, - public readonly input: InputTypeMap[Input], - public readonly extension: Extension, - public readonly argument: any, - ) { - this.currentCount = extension.currentCount; - this.currentRegister = extension.currentRegister; - this.selectionBehavior = extension.selectionBehavior; - } - - /** - * Records the given changes as having happened after this command. - */ - public recordFollowingChanges(changes: readonly vscode.TextDocumentContentChangeEvent[]) { - this._followingChanges.push(...changes); - } -} - -export interface UndoStops { - readonly undoStopBefore: boolean; - readonly undoStopAfter: boolean; -} - -export type Action = ( - editorState: EditorState, - state: CommandState, - undoStops: UndoStops, -) => void | Thenable; - -export const enum InputKind { - None, - - RegExp, - ListOneItem, - ListManyItems, - Text, - Key, - ListOneItemOrCount, -} - -export interface InputTypeMap { - readonly [InputKind.None]: void; - readonly [InputKind.RegExp]: RegExp; - readonly [InputKind.ListOneItem]: number; - readonly [InputKind.ListManyItems]: number[]; - readonly [InputKind.Text]: string; - readonly [InputKind.Key]: string; - readonly [InputKind.ListOneItemOrCount]: number | null; -} - -export interface InputDescrMap { - readonly [InputKind.None]: undefined; - readonly [InputKind.ListOneItem]: [string, string][]; - readonly [InputKind.ListManyItems]: [string, string][]; - readonly [InputKind.RegExp]: string; - readonly [InputKind.Text]: vscode.InputBoxOptions & { - readonly setup?: (editorState: EditorState) => void; - readonly onDidCancel?: (editorState: EditorState) => void; - }; - readonly [InputKind.Key]: undefined; - readonly [InputKind.ListOneItemOrCount]: [string, string][]; +/** + * Indicates that an input is expected; if no input is given, the specified + * function will be used to update the input value in subsequent executions of + * this command. + */ +export interface InputOr { + (promptDefaultInput: () => T): T; + (promptDefaultInput: () => Thenable): Thenable; } /** - * Defines a command's behavior, as well as its inputs. + * Indicates that an input is expected. */ -export class CommandDescriptor { - /** - * Whether errors in command executions should lead to hard exceptions. - */ - public static throwOnError = false; +export type Input = T | undefined; + +/** + * A function used to update the input value in subsequent executions of this + * command. + */ +export interface SetInput { + (input: T): void; +} + +/** + * Indicates that a value passed as a command argument is expected. + */ +export type Argument = T; + +/** + * The type of a `Context` passed to a command, based on whether the command + * requires an active text editor or not. + */ +export type ContextType + = RequiresActiveEditor extends true ? Context : Context.WithoutActiveEditor; + +/** + * The type of the handler of a `CommandDescriptor`. + */ +export interface Handler { + (context: ContextType, + argument: Record): unknown | Thenable; +} + +/** + * The descriptor of a command. + */ +export class CommandDescriptor { + public get requiresActiveEditor() { + return (this.flags & CommandDescriptor.Flags.RequiresActiveEditor) !== 0; + } public constructor( - public readonly command: Command, - public readonly flags: CommandFlags, - public readonly input: Input, - public readonly inputDescr: (editorState: EditorState) => InputDescrMap[Input], - public readonly action: Action, + /** + * The unique identifier of the command. + */ + public readonly identifier: string, + + /** + * The handler of the command. + */ + public readonly handler: Handler, + + /** + * The flags of the command. + */ + public readonly flags: Flags, ) { Object.freeze(this); } /** - * Returns the the input for the current command, after requesting it from the - * user if the given argument does not already specify it. + * Executes the command with the given argument. */ - public async getInput( - editorState: EditorState, - argument: any, - cancellationToken?: vscode.CancellationToken, - ): Promise { - let input: InputTypeMap[Input] | undefined; - - switch (this.input) { - case InputKind.RegExp: - if (typeof argument === "object" && argument?.input instanceof RegExp) { - input = argument.input; - } else if (typeof argument === "object" && typeof argument.input === "string") { - input = new RegExp(argument.input, this.inputDescr(editorState) as string) as any; - } else { - input = await promptRegex(this.inputDescr(editorState) as string, cancellationToken) as any; - } - break; - case InputKind.ListOneItem: - if (typeof argument === "object" && typeof argument.input === "string") { - input = argument.input; - } else { - input = await promptInList( - false, this.inputDescr(editorState) as [string, string][], cancellationToken) as any; - } - break; - case InputKind.ListOneItemOrCount: - if (typeof argument === "object" && typeof argument.input === "string") { - input = argument.input; - } else if (editorState.extension.currentCount === 0) { - input = await promptInList( - false, this.inputDescr(editorState) as [string, string][], cancellationToken) as any; - } else { - input = null as any; - } - break; - case InputKind.ListManyItems: - if (typeof argument === "object" && typeof argument.input === "string") { - input = argument.input; - } else { - input = await promptInList( - true, this.inputDescr(editorState) as [string, string][], cancellationToken) as any; - } - break; - case InputKind.Text: - const inputDescr = this.inputDescr(editorState) as InputDescrMap[InputKind.Text]; - - if (inputDescr.setup !== undefined) { - inputDescr.setup(editorState); - } - - if (typeof argument === "object" && typeof argument.input === "string") { - const error = await Promise.resolve(inputDescr.validateInput?.(argument.input)); - if (error) { - throw new Error(`invalid text input: ${error}`); - } - input = argument.input; - } else { - input = await prompt(inputDescr, cancellationToken) as any; - } - break; - case InputKind.Key: - if (typeof argument === "object" && typeof argument.input === "string") { - input = argument.input; - } else { - const prevMode = editorState.mode; - - editorState.setMode(Mode.Awaiting); - input = await keypress(cancellationToken) as any; - editorState.setMode(prevMode); - } - break; - } - - return input; + public replay(context: ContextType, argument: Record) { + return this.handler(context, argument); } /** - * Executes the command completely, prompting the user for input and saving - * history entries if needed. + * Invokes the command with the given argument. */ - public async execute(editorState: EditorState, argument: any) { - const { extension, editor } = editorState; + public async invoke(extension: Extension, argument: unknown) { + const context = Context.create(extension, this); - if (editor.selections.length === 0) { - // Most commands won't work without any selection at all. So let's force - // one. This mainly happens when document is empty. - editor.selections = [new vscode.Selection(DocumentStart, DocumentStart)]; + if (this.requiresActiveEditor && !(context instanceof Context)) { + throw new EditorRequiredError(); } - const flags = this.flags; - const cts = new vscode.CancellationTokenSource(); + const ownedArgument = Object.assign({}, argument) as Record; - extension.cancellationTokenSource?.cancel(); - extension.cancellationTokenSource = cts; - - const input = await this.getInput(editorState, argument, cts.token); - - if (this.input !== InputKind.None && input === undefined) { - const inputDescr = this.inputDescr?.(editorState); - - if (typeof inputDescr === "object" && "onDidCancel" in inputDescr) { - inputDescr.onDidCancel?.(editorState); - } - - return; + if (ownedArgument.count === undefined && extension.currentCount !== 0) { + ownedArgument.count = extension.currentCount; + } + if (ownedArgument.register === undefined && extension.currentRegister !== undefined) { + ownedArgument.register = extension.currentRegister; } - if ( - this.flags & CommandFlags.ChangeSelections - && !(this.flags & CommandFlags.DoNotResetPreferredColumns) - ) { - editorState.preferredColumns.length = 0; - } + extension.currentCount = 0; + extension.currentRegister = undefined; - const commandState = new CommandState( - this, - input as InputTypeMap[Input], - extension, - argument, - ); + let result: unknown; - if (!this.command.startsWith("dance.count.")) { - extension.currentCount = 0; - } - - editorState.ignoreSelectionChanges = true; - - let result; try { - result = this.action(editorState, commandState, { - undoStopBefore: true, - undoStopAfter: true, - }); - if (result !== undefined) { - if (typeof result === "object" && typeof result.then === "function") { - result = await result; - } - } + result = await this.handler(context as any, ownedArgument); } catch (e) { - if (CommandDescriptor.throwOnError) { - console.error(e); - throw e; - } - - let message = e; - - if (typeof e === "object" && e !== null && e.constructor === Error) { - // Note that we purposedly do not use `instanceof` above to keep - // prefixes like "SyntaxError:". - message = e.message; - } - - // Or show error in the status bar? VSCode does not have a way to dismiss - // messages, but they recommend setting the status bar instead. - // See: https://github.com/Microsoft/vscode/issues/2732 - vscode.window.showErrorMessage(`Error executing command "${this.command}": ${message}`); - return; - } - - if (flags & (CommandFlags.SwitchToInsertBefore | CommandFlags.SwitchToInsertAfter)) { - // Ensure selections face the right way. - const selections = editor.selections, - len = selections.length, - shouldBeReversed = (flags & CommandFlags.SwitchToInsertBefore) !== 0; - - for (let i = 0; i < len; i++) { - const selection = selections[i]; - - selections[i] - = selection.isReversed === shouldBeReversed - ? selection - : new vscode.Selection(selection.active, selection.anchor); - } - - // Make selections empty. - editorState.setMode(Mode.Insert); - - for (let i = 0; i < len; i++) { - const position = flags & CommandFlags.SwitchToInsertBefore - ? selections[i].start - : selections[i].end; - - selections[i] = new vscode.Selection(position, position); - } - - editor.selections = selections; - } else if (flags & CommandFlags.SwitchToNormal) { - editorState.setMode(Mode.Normal); - } - - if (flags & CommandFlags.ChangeSelections) { - // Scroll to cursor if needed - const position = editor.selection.active; - - editor.revealRange(new vscode.Range(position, position)); - } - - if (remainingNormalCommands === 1) { - remainingNormalCommands = 0; - - editorState.setMode(Mode.Insert); - } else if (remainingNormalCommands > 1) { - remainingNormalCommands--; - } - - editorState.ignoreSelectionChanges = false; - editorState.recordCommand(commandState); - editorState.normalizeSelections(); - } - - /** - * Executes the given command using the given state. - */ - public static async execute( - editorState: EditorState, - commandState: CommandState, - ) { - const { editor } = editorState; - let result = commandState.descriptor.action(editorState, commandState, { - undoStopBefore: true, - undoStopAfter: true, - }); - - if (result !== undefined) { - if (typeof result === "object" && typeof result.then === "function") { - result = await result; - } - - if (typeof result === "function") { - await editor.edit(result); - } - } - } - - /** - * Executes the given commands as part of a batch operation started by a - * macro, for example. - */ - public static async executeMany( - editorState: EditorState, - commands: readonly CommandState[], - ) { - // In a batch execution, we don't change modes, and some things are not - // prompted again. - // Furthermore, a single entry is added to VS Code's history for the entire - // batch operation. - let firstEditIdx = 0, - lastEditIdx = commands.length - 1; - - for (let i = 0; i < commands.length; i++) { - if (commands[i].descriptor.flags & CommandFlags.Edit) { - firstEditIdx = i; - break; - } - } - - for (let i = commands.length - 1; i >= 0; i--) { - if (commands[i].descriptor.flags & CommandFlags.Edit) { - lastEditIdx = i; - break; - } - } - - let currentMode = editorState.mode; - const { editor } = editorState; - - for (let i = 0; i < commands.length; i++) { - const commandState = commands[i], - descriptor = commandState.descriptor; - const undoStops = { - undoStopBefore: i === firstEditIdx, - undoStopAfter: i === lastEditIdx, - }; - - let result = descriptor.action(editorState, commandState, undoStops); - - if (result !== undefined) { - if (typeof result === "object" && typeof result.then === "function") { - result = await result; - } - - if (typeof result === "function") { - await editor.edit(result, undoStops); - } - } - - if ( - descriptor.flags - & (CommandFlags.SwitchToInsertBefore | CommandFlags.SwitchToInsertAfter) - ) { - currentMode = Mode.Insert; - } else if (descriptor.flags & CommandFlags.SwitchToNormal) { - currentMode = Mode.Normal; - } - } - - editorState.setMode(currentMode); - } - - public register(extension: Extension) { - if (this.flags & CommandFlags.CanRunWithoutEditor) { - return vscode.commands.registerCommand(this.command, (arg) => { - const editor = vscode.window.activeTextEditor; - - if (editor === undefined) { - // @ts-ignore - return this.action(undefined, new CommandState(this, undefined, extension, arg)); - } - - return this.execute(extension.getEditorState(editor).updateEditor(editor), arg); - }); - } - - return vscode.commands.registerCommand(this.command, (arg) => { - const editor = vscode.window.activeTextEditor; - - if (editor === undefined) { + if ((ownedArgument as { readonly try: boolean }).try) { return; } - return this.execute(extension.getEditorState(editor).updateEditor(editor), arg); - }); + throw e; + } + + // Record command *after* executing it, to ensure it did not encounter + // an error. + extension.recorder.recordCommand(this, ownedArgument); + + if (this.requiresActiveEditor) { + await (context as Context).insertUndoStop(); + } + + return result; + } + + /** + * Invokes the command with the given argument, ensuring that errors are + * reporting to the user instead of throwing them. + */ + public invokeSafely(extension: Extension, argument: unknown) { + return extension.runPromiseSafely( + () => this.invoke(extension, argument), + () => undefined, + (e) => `error executing command "${this.identifier}": ${e.message}`, + ); + } + + /** + * Registers the command for use by VS Code. + */ + public register(extension: Extension): vscode.Disposable { + return vscode.commands.registerCommand( + this.identifier, + (argument) => this.invokeSafely(extension, argument), + ); } } -export const commands: CommandDescriptor[] = []; -export const commandsByName: Record> = {} as any; +export namespace CommandDescriptor { + /** + * Flags describing the behavior of some commands. + */ + export const enum Flags { + /** No specific behavior. */ + None = 0b0000, -export const preferredColumnsPerEditor = new WeakMap(); -export let remainingNormalCommands = 0; + /** An active editor must be available. */ + RequiresActiveEditor = 0b0001, -export function registerCommand( - command: Command, - flags: CommandFlags, - action: Action, -): void; -export function registerCommand( - command: Command, - flags: CommandFlags, - input: Input, - inputDescr: (editorState: EditorState) => InputDescrMap[Input], - action: Action, -): void; - -export function registerCommand(...args: readonly any[]) { - const descriptor = args.length === 3 - ? new CommandDescriptor(args[0], args[1], InputKind.None, () => void 0, args[2]) - : new CommandDescriptor(args[0], args[1], args[2], args[3], args[4]); - - commands.push(descriptor); - commandsByName[args[0] as Command] = descriptor; + /** The command should not be replayed in macros and repeats. */ + DoNotReplay = 0b0010, + } } -export function setRemainingNormalCommands(remaining: number) { - remainingNormalCommands = remaining + 1; - // ^^^ to account for the currently - // executing command. +/** + * A record from command identifier to command descriptor. + */ +export interface Commands { + readonly [commandIdentifier: string]: CommandDescriptor; } - -import "./changes"; -import "./count"; -import "./goto"; -import "./history"; -import "./insert"; -import "./macros"; -import "./mark"; -import "./menus"; -import "./misc"; -import "./modes"; -import "./move"; -import "./pipe"; -import "./rotate"; -import "./search"; -import "./select"; -import "./selections"; -import "./selectObject"; -import "./yankPaste"; - -Object.freeze(commandsByName); diff --git a/src/commands/insert.ts b/src/commands/insert.ts deleted file mode 100644 index cd79780..0000000 --- a/src/commands/insert.ts +++ /dev/null @@ -1,208 +0,0 @@ -import * as vscode from "vscode"; - -import { Command, CommandDescriptor, CommandFlags, registerCommand } from "."; -import { SelectionBehavior } from "../state/extension"; -import { SelectionHelper } from "../utils/selectionHelper"; - -registerCommand( - Command.insertBefore, - CommandFlags.ChangeSelections | CommandFlags.SwitchToInsertBefore, - () => { - // Nop. - }, -); - -registerCommand( - Command.insertAfter, - CommandFlags.ChangeSelections | CommandFlags.SwitchToInsertAfter, - () => { - // Nop. - }, -); - -registerCommand( - Command.insertLineStart, - CommandFlags.ChangeSelections | CommandFlags.SwitchToInsertBefore, - ({ editor }) => { - const selections = editor.selections, - len = selections.length; - - for (let i = 0; i < len; i++) { - const selection = selections[i], - lineStart = editor.document.lineAt(selection.start.line) - .firstNonWhitespaceCharacterIndex; - - selections[i] = new vscode.Selection( - selection.anchor, - new vscode.Position(selection.start.line, lineStart), - ); - } - - editor.selections = selections; - }, -); - -registerCommand( - Command.insertLineEnd, - CommandFlags.ChangeSelections | CommandFlags.SwitchToInsertAfter, - ({ editor }) => { - const selections = editor.selections, - len = selections.length; - - for (let i = 0; i < len; i++) { - const selection = selections[i]; - - selections[i] = new vscode.Selection( - selection.anchor, - new vscode.Position(selection.end.line, Number.MAX_SAFE_INTEGER), - ); - } - - editor.selections = selections; - }, -); - -function normalizeSelectionsForLineInsertion(editor: vscode.TextEditor) { - editor.selections = editor.selections.map((selection) => { - let { active } = selection; - - if (active.character === 0 && !selection.isReversed && active.line > 0) { - active = active.translate(-1); - } - - return new vscode.Selection(active, active); - }); -} - -registerCommand( - Command.insertNewLineAbove, - CommandFlags.Edit | CommandFlags.SwitchToInsertBefore, - ({ editor }, { selectionBehavior }) => { - if (selectionBehavior === SelectionBehavior.Character) { - normalizeSelectionsForLineInsertion(editor); - } - - return vscode.commands.executeCommand("editor.action.insertLineBefore"); - }, -); - -registerCommand( - Command.insertNewLineBelow, - CommandFlags.Edit | CommandFlags.SwitchToInsertBefore, - ({ editor }, { selectionBehavior }) => { - if (selectionBehavior === SelectionBehavior.Character) { - normalizeSelectionsForLineInsertion(editor); - } - - return vscode.commands.executeCommand("editor.action.insertLineAfter"); - }, -); - -registerCommand(Command.newLineAbove, CommandFlags.Edit, (editorState, state, undoStops) => - editorState.editor - .edit((builder) => { - const { editor } = editorState; - const newLine = editor.document.eol === vscode.EndOfLine.LF ? "\n" : "\r\n"; - const processedLines = new Set(); - - const selections = editor.selections, - len = selections.length, - selectionHelper = SelectionHelper.for(editorState, state); - - for (let i = 0; i < len; i++) { - const selection = selections[i], - activeLine - = selection.active === selection.end - ? selectionHelper.endLine(selection) - : selection.active.line; - - if (processedLines.size !== processedLines.add(activeLine).size) { - builder.insert(new vscode.Position(activeLine, 0), newLine); - } - } - }, undoStops) - .then(() => void 0), -); - -registerCommand(Command.newLineBelow, CommandFlags.Edit, (editorState, state, undoStops) => - editorState.editor - .edit((builder) => { - const { editor } = editorState; - const newLine = editor.document.eol === vscode.EndOfLine.LF ? "\n" : "\r\n"; - const processedLines = new Set(); - - const selections = editor.selections, - len = selections.length, - selectionHelper = SelectionHelper.for(editorState, state); - - for (let i = 0; i < len; i++) { - const selection = selections[i], - activeLine - = selection.active === selection.end - ? selectionHelper.endLine(selection) - : selection.active.line; - - if (processedLines.size !== processedLines.add(activeLine).size) { - builder.insert(new vscode.Position(activeLine + 1, 0), newLine); - } - } - }, undoStops) - .then(() => void 0), -); - -registerCommand(Command.repeatInsert, CommandFlags.Edit, async ({ editor }, state) => { - const editorState = state.extension.getEditorState(editor); - - let switchToInsert: undefined | typeof editorState.recordedCommands[0]; - let i = editorState.recordedCommands.length - 1; - - for (; i >= 0; i--) { - if (editorState.recordedCommands[i].descriptor.flags & CommandFlags.SwitchToInsertBefore) { - switchToInsert = editorState.recordedCommands[i]; - break; - } - } - - if (switchToInsert === undefined) { - return; - } - - const start = i; - let switchToNormal: undefined | typeof editorState.recordedCommands[0]; - - for (i++; i < editorState.recordedCommands.length; i++) { - if (editorState.recordedCommands[i].descriptor.flags & CommandFlags.SwitchToNormal) { - switchToNormal = editorState.recordedCommands[i]; - break; - } - } - - if (switchToNormal === undefined) { - return; - } - - await CommandDescriptor.execute(editorState, editorState.recordedCommands[start]); - - const end = i; - - await editor.edit((builder) => { - for (let i = state.currentCount || 1; i > 0; i--) { - for (let j = start; j <= end; j++) { - const commandState = editorState.recordedCommands[j], - changes = commandState.followingChanges; - - if (changes === undefined) { - continue; - } - - for (const change of changes) { - if (change.rangeLength === 0) { - builder.insert(editor.selection.active, change.text); - } else { - builder.replace(editor.selection, change.text); - } - } - } - } - }); -}); diff --git a/src/commands/keybindings.ts b/src/commands/keybindings.ts new file mode 100644 index 0000000..2f2b538 --- /dev/null +++ b/src/commands/keybindings.ts @@ -0,0 +1,33 @@ +import * as vscode from "vscode"; +import { RegisterOr } from "."; +import { Context, prompt, todo } from "../api"; +import { Register } from "../state/registers"; + +/** + * Utilities for setting up keybindings. + */ +declare module "./keybindings"; + +/** + * Set up Dance keybindings. + */ +export async function setup(_: Context, register: RegisterOr<"dquote", Register.Flags.CanWrite>) { + await vscode.commands.executeCommand("workbench.action.openGlobalKeybindingsFile"); + await _.switchToDocument(_.extension.editors.active!.editor.document); + + const action = await prompt.one([ + ["y", "yank keybindings to register"], + ["a", "append keybindings"], + ["p", "prepend keybindings"], + ]); + + if (typeof action === "string") { + return; + } + + const keybindings = await prompt.many([ + ["d", "default keybindings"], + ]); + + todo(); +} diff --git a/src/commands/load-all.build.ts b/src/commands/load-all.build.ts new file mode 100644 index 0000000..efbdf08 --- /dev/null +++ b/src/commands/load-all.build.ts @@ -0,0 +1,199 @@ +import * as assert from "assert"; +import { Builder, unindent } from "../../meta"; + +export async function build(builder: Builder) { + const modules = await builder.getCommandModules(); + + return unindent(4, ` + ${modules.map((module) => unindent(8, ` + /** + * Loads the "${module.name}" module and returns its defined commands. + */ + async function load${capitalize(module.name!)}Module(): Promise { + const {${ + module.functionNames + .map((name) => "\n" + " ".repeat(16) + name + ",") + .sort() + .join("")} + } = await import("./${module.name}"); + + return [${ + module.functions + .map((f) => ` + new CommandDescriptor( + "dance.${f.qualifiedName}", + ${determineFunctionExpression(f)}, + ${determineFunctionFlags(f)}, + ),`) + .sort() + .join("")}${ + module.additional.concat(...module.functions.map((f) => f.additional)) + .filter((x) => x.identifier !== undefined && x.commands !== undefined) + .map((x) => ` + new CommandDescriptor( + "dance.${x.qualifiedIdentifier}", + ${buildCommandsExpression(x)}, + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ),`) + .sort() + .join("")} + ]; + } + `).trim()).join("\n\n")} + + /** + * Loads and returns all defined commands. + */ + export async function loadCommands(): Promise { + const allModules = await Promise.all([${ + modules + .map((module) => `\n${" ".repeat(8)}load${capitalize(module.name!)}Module(),`) + .join("")} + ]); + + return Object.freeze( + Object.fromEntries(allModules.flat().map((desc) => [desc.identifier, desc])), + ); + } + `); +} + +function capitalize(text: string) { + return text.replace(/(\.|^)[a-z]/g, (x, dot) => x.slice(dot.length).toUpperCase()); +} + +function determineFunctionExpression(f: Builder.ParsedFunction) { + const givenParameters: string[] = []; + let takeArgument = false; + + for (const [name, type] of f.parameters) { + switch (name) { + + // Arguments, input. + case "argument": + takeArgument = true; + givenParameters.push("argument"); + break; + + case "input": + takeArgument = true; + givenParameters.push("getInput(argument)"); + break; + + case "setInput": + takeArgument = true; + givenParameters.push("getSetInput(argument)"); + break; + + case "inputOr": + takeArgument = true; + givenParameters.push("getInputOr(argument)"); + break; + + case "direction": + takeArgument = true; + givenParameters.push("getDirection(argument)"); + break; + + case "shift": + takeArgument = true; + givenParameters.push("getShift(argument)"); + break; + + // Implicit context. + case "_": + givenParameters.push("_"); + break; + + // Context (without active editor). + case "cancellationToken": + givenParameters.push("_.cancellationToken"); + break; + + case "extension": + givenParameters.push("_.extension"); + break; + + case "modes": + givenParameters.push("_.extension.modes"); + break; + + case "registers": + givenParameters.push("_.extension.registers"); + break; + + case "count": + takeArgument = true; + givenParameters.push("getCount(_, argument)"); + break; + + case "repetitions": + takeArgument = true; + givenParameters.push("getRepetitions(_, argument)"); + break; + + case "register": + takeArgument = true; + + const match = /^RegisterOr<"(\w+)"(?:, (.+))>$/.exec(type); + + assert(match !== null); + + const flags = match[2] ?? "Register.Flags.None", + registerString = `getRegister(_, argument, ${JSON.stringify(match[1])}, ${flags})`; + + givenParameters.push(registerString); + break; + + // Context (with active editor). + case "document": + givenParameters.push("_.document"); + break; + + case "selections": + givenParameters.push("_.selections"); + break; + + // ?? + default: + if (type.startsWith("Context")) { + givenParameters.push("_"); + } else if (type.startsWith("Argument<")) { + takeArgument = true; + givenParameters.push("argument." + name); + } else { + throw new Error(`unknown parameter ${JSON.stringify([name, type])}`); + } + } + } + + const inputParameters = ["_", ...(takeArgument ? ["argument"] : [])], + call = `${f.name}(${givenParameters.join(", ")})`; + + return `(${inputParameters.join(", ")}) => _.runAsync((_) => ${call})`; +} + +function determineFunctionFlags(f: Builder.ParsedFunction) { + const flags = [] as string[]; + + if (f.parameters.some(([_, t]) => ["Context"].includes(t)) + || f.parameters.some(([p]) => ["document", "selections"].includes(p))) { + flags.push("RequiresActiveEditor"); + } + + if ("noreplay" in f.properties) { + flags.push("DoNotReplay"); + } + + if (flags.length === 0) { + return "CommandDescriptor.Flags.None"; + } + + return flags.map((flag) => "CommandDescriptor.Flags." + flag).join(" | "); +} + +function buildCommandsExpression(f: Builder.AdditionalCommand) { + const commands = f.commands!.replace(/ +/g, " ").replace(/ \}\]/g, ", ...argument }]"); + + return `(_, argument) => _.runAsync(() => commands(${commands}))`; +} diff --git a/src/commands/load-all.ts b/src/commands/load-all.ts new file mode 100644 index 0000000..ffc2d12 --- /dev/null +++ b/src/commands/load-all.ts @@ -0,0 +1,1289 @@ +import { ArgumentError, commands, Context, Direction, EditorRequiredError, Shift } from "../api"; +import { Register } from "../state/registers"; +import { CommandDescriptor, Commands } from "."; + +function getRegister( + _: Context.WithoutActiveEditor, + argument: { register?: string | Register }, + defaultRegisterName: string, + requiredFlags: F, +): Register.WithFlags { + let register = argument.register; + const extension = _.extension; + + if (typeof register === "string") { + if (register.startsWith(" ")) { + if (!(_ instanceof Context)) { + throw new EditorRequiredError(); + } + + register = extension.registers.forDocument(_.document).get(register.slice(1)); + } else { + register = extension.registers.get(register); + } + } else if (!(register instanceof Register)) { + register = extension.registers.get(defaultRegisterName); + } + + register.checkFlags(requiredFlags); + + return (argument.register = register as any); +} + +function getCount(_: Context.WithoutActiveEditor, argument: { count?: number }) { + const count = +(argument.count as any); + + if (count >= 0 && Number.isInteger(count)) { + return count; + } + + return (argument.count = 0); +} + +function getRepetitions(_: Context.WithoutActiveEditor, argument: { count?: number }) { + const count = getCount(_, argument); + + if (count <= 0) { + return 1; + } + + return count; +} + +function getDirection(argument: { direction?: number | string }) { + const direction = argument.direction; + + if (direction === undefined) { + return undefined; + } + + if (typeof direction === "number") { + if (direction === 1 || direction === -1) { + return direction as Direction; + } + } else if (typeof direction === "string") { + if (direction === "forward") { + return Direction.Forward; + } + + if (direction === "backward") { + return Direction.Backward; + } + } + + throw new ArgumentError( + '"direction" must be "forward", "backward", 1, -1, or undefined', + "direction", + ); +} + +function getShift(argument: { shift?: number | string }) { + const shift = argument.shift; + + if (shift === undefined) { + return undefined; + } + + if (typeof shift === "number") { + if (shift === 0 || shift === 1 || shift === 2) { + return shift as Shift; + } + } else if (typeof shift === "string") { + if (shift === "jump") { + return Shift.Jump; + } + + if (shift === "select") { + return Shift.Select; + } + + if (shift === "extend") { + return Shift.Extend; + } + } + + throw new ArgumentError( + '"shift" must be "jump", "select", "extend", 0, 1, 2, or undefined', + "shift", + ); +} + +function getInput(argument: { input?: any }) { + return argument.input; +} + +function getSetInput(argument: { input?: any }) { + return (input: unknown) => argument.input = input; +} + +function getInputOr(argument: { input?: any }): any { + const defaultInput = argument.input; + + if (defaultInput != null) { + return () => defaultInput; + } + + return (promptDefaultInput: () => any) => { + const result = promptDefaultInput(); + + if (typeof result.then === "function") { + return (result as Thenable).then((x) => (argument.input = x)); + } + + return (argument.input = result); + }; +} + +/* eslint-disable max-len */ + +// +// Content below this line was auto-generated by load-all.build.ts. Do not edit manually. + +/** + * Loads the "dev" module and returns its defined commands. + */ +async function loadDevModule(): Promise { + const { + setSelectionBehavior, + } = await import("./dev"); + + return [ + new CommandDescriptor( + "dance.dev.setSelectionBehavior", + (_, argument) => _.runAsync((_) => setSelectionBehavior(_.extension, argument.mode, argument.value)), + CommandDescriptor.Flags.None, + ), + ]; +} + +/** + * Loads the "edit" module and returns its defined commands. + */ +async function loadEditModule(): Promise { + const { + align, + case_swap, + case_toLower, + case_toUpper, + copyIndentation, + deindent, + deindent_withIncomplete, + indent, + indent_withEmpty, + insert, + join, + join_select, + newLine_above, + newLine_below, + replaceCharacters, + } = await import("./edit"); + + return [ + new CommandDescriptor( + "dance.edit.align", + (_, argument) => _.runAsync((_) => align(_, _.selections, argument.fill)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.case.swap", + (_) => _.runAsync((_) => case_swap(_)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.case.toLower", + (_) => _.runAsync((_) => case_toLower(_)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.case.toUpper", + (_) => _.runAsync((_) => case_toUpper(_)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.copyIndentation", + (_, argument) => _.runAsync((_) => copyIndentation(_, _.document, _.selections, getCount(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.deindent", + (_, argument) => _.runAsync((_) => deindent(_, getRepetitions(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.deindent.withIncomplete", + (_, argument) => _.runAsync((_) => deindent_withIncomplete(_, getRepetitions(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.indent", + (_, argument) => _.runAsync((_) => indent(_, getRepetitions(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.indent.withEmpty", + (_, argument) => _.runAsync((_) => indent_withEmpty(_, getRepetitions(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.insert", + (_, argument) => _.runAsync((_) => insert(_, _.selections, getRegister(_, argument, "dquote", Register.Flags.CanRead), argument.adjust, argument.handleNewLine, argument.select, argument.where)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.join", + (_, argument) => _.runAsync((_) => join(_, argument.separator)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.join.select", + (_, argument) => _.runAsync((_) => join_select(_, argument.separator)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.newLine.above", + (_, argument) => _.runAsync((_) => newLine_above(_, argument.select)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.newLine.below", + (_, argument) => _.runAsync((_) => newLine_below(_, argument.select)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.replaceCharacters", + (_, argument) => _.runAsync((_) => replaceCharacters(_, getRepetitions(_, argument), getInputOr(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.edit.delete", + (_, argument) => _.runAsync(() => commands([".edit.insert", { register: "_", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.delete-insert", + (_, argument) => _.runAsync(() => commands([".modes.set", { input: "insert", ...argument }], [".edit.insert", { register: "_", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.newLine.above.insert", + (_, argument) => _.runAsync(() => commands([".modes.set", { input: "insert", ...argument }], [".edit.newLine.above", { select: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.newLine.below.insert", + (_, argument) => _.runAsync(() => commands([".modes.set", { input: "insert", ...argument }], [".edit.newLine.below", { select: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.paste.after", + (_, argument) => _.runAsync(() => commands([".edit.insert", { handleNewLine: true, where: "end", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.paste.after.select", + (_, argument) => _.runAsync(() => commands([".edit.insert", { handleNewLine: true, where: "end" , select: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.paste.before", + (_, argument) => _.runAsync(() => commands([".edit.insert", { handleNewLine: true, where: "start", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.paste.before.select", + (_, argument) => _.runAsync(() => commands([".edit.insert", { handleNewLine: true, where: "start", select: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.selectRegister-insert", + (_, argument) => _.runAsync(() => commands([".selectRegister"], [".edit.insert"])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.yank-delete", + (_, argument) => _.runAsync(() => commands([".selections.saveText"], [".edit.insert", { register: "_", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.yank-delete-insert", + (_, argument) => _.runAsync(() => commands([".selections.saveText"], [".modes.set", { input: "insert", ...argument }], [".edit.insert", { register: "_", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.edit.yank-replace", + (_, argument) => _.runAsync(() => commands([".selections.saveText"], [".edit.insert"])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + ]; +} + +/** + * Loads the "history" module and returns its defined commands. + */ +async function loadHistoryModule(): Promise { + const { + recording_play, + recording_start, + recording_stop, + redo, + redo_selections, + repeat, + repeat_edit, + undo, + undo_selections, + } = await import("./history"); + + return [ + new CommandDescriptor( + "dance.history.recording.play", + (_, argument) => _.runAsync((_) => recording_play(_, getRepetitions(_, argument), getRegister(_, argument, "arobase", Register.Flags.CanReadWriteMacros))), + CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.history.recording.start", + (_, argument) => _.runAsync((_) => recording_start(_, getRegister(_, argument, "arobase", Register.Flags.CanReadWriteMacros))), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.history.recording.stop", + (_, argument) => _.runAsync((_) => recording_stop(_, getRegister(_, argument, "arobase", Register.Flags.CanReadWriteMacros))), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.history.redo", + (_) => _.runAsync((_) => redo()), + CommandDescriptor.Flags.None, + ), + new CommandDescriptor( + "dance.history.redo.selections", + (_) => _.runAsync((_) => redo_selections()), + CommandDescriptor.Flags.None, + ), + new CommandDescriptor( + "dance.history.repeat", + (_, argument) => _.runAsync((_) => repeat(_, getRepetitions(_, argument), argument.include)), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.history.repeat.edit", + (_, argument) => _.runAsync((_) => repeat_edit(_, getRepetitions(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.history.undo", + (_) => _.runAsync((_) => undo()), + CommandDescriptor.Flags.None, + ), + new CommandDescriptor( + "dance.history.undo.selections", + (_) => _.runAsync((_) => undo_selections()), + CommandDescriptor.Flags.None, + ), + new CommandDescriptor( + "dance.history.repeat.seek", + (_, argument) => _.runAsync(() => commands([".history.repeat", { include: "dance\\.seek\\..+", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.history.repeat.selection", + (_, argument) => _.runAsync(() => commands([".history.repeat", { include: "dance\\.(seek|select|selections)\\..+", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + ]; +} + +/** + * Loads the "keybindings" module and returns its defined commands. + */ +async function loadKeybindingsModule(): Promise { + const { + setup, + } = await import("./keybindings"); + + return [ + new CommandDescriptor( + "dance.keybindings.setup", + (_, argument) => _.runAsync((_) => setup(_, getRegister(_, argument, "dquote", Register.Flags.CanWrite))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + ]; +} + +/** + * Loads the "misc" module and returns its defined commands. + */ +async function loadMiscModule(): Promise { + const { + cancel, + ignore, + openMenu, + run, + selectRegister, + updateCount, + } = await import("./misc"); + + return [ + new CommandDescriptor( + "dance.cancel", + (_) => _.runAsync((_) => cancel(_.extension)), + CommandDescriptor.Flags.None, + ), + new CommandDescriptor( + "dance.ignore", + (_) => _.runAsync((_) => ignore()), + CommandDescriptor.Flags.None, + ), + new CommandDescriptor( + "dance.openMenu", + (_, argument) => _.runAsync((_) => openMenu(_, getInputOr(argument), argument.menu, argument.prefix, argument.pass, argument.locked)), + CommandDescriptor.Flags.None, + ), + new CommandDescriptor( + "dance.run", + (_, argument) => _.runAsync((_) => run(_, getInputOr(argument), argument.commands)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selectRegister", + (_, argument) => _.runAsync((_) => selectRegister(_, getInputOr(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.updateCount", + (_, argument) => _.runAsync((_) => updateCount(_, getCount(_, argument), _.extension, getInputOr(argument), argument.addDigits)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + ]; +} + +/** + * Loads the "modes" module and returns its defined commands. + */ +async function loadModesModule(): Promise { + const { + set, + set_temporarily, + } = await import("./modes"); + + return [ + new CommandDescriptor( + "dance.modes.set", + (_, argument) => _.runAsync((_) => set(_, getInputOr(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.modes.set.temporarily", + (_, argument) => _.runAsync((_) => set_temporarily(_, getInputOr(argument), getRepetitions(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.modes.insert.after", + (_, argument) => _.runAsync(() => commands([".selections.faceForward"] , [".modes.set", { input: "insert", ...argument }], [".selections.reduce", { where: "end", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.modes.insert.before", + (_, argument) => _.runAsync(() => commands([".selections.faceBackward"], [".modes.set", { input: "insert", ...argument }], [".selections.reduce", { where: "start", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.modes.insert.lineEnd", + (_, argument) => _.runAsync(() => commands([".select.lineEnd" , { shift: "jump", ...argument }], [".modes.set", { input: "insert", ...argument }], [".selections.reduce", { where: "end", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.modes.insert.lineStart", + (_, argument) => _.runAsync(() => commands([".select.lineStart", { shift: "jump", skipBlank: true, ...argument }], [".modes.set", { input: "insert", ...argument }], [".selections.reduce", { where: "start", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.modes.set.insert", + (_, argument) => _.runAsync(() => commands([".modes.set", { input: "insert", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.modes.set.normal", + (_, argument) => _.runAsync(() => commands([".modes.set", { input: "normal", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.modes.set.temporarily.insert", + (_, argument) => _.runAsync(() => commands([".modes.set.temporarily", { input: "insert", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.modes.set.temporarily.normal", + (_, argument) => _.runAsync(() => commands([".modes.set.temporarily", { input: "normal", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + ]; +} + +/** + * Loads the "search" module and returns its defined commands. + */ +async function loadSearchModule(): Promise { + const { + next, + search, + selection, + } = await import("./search"); + + return [ + new CommandDescriptor( + "dance.search", + (_, argument) => _.runAsync((_) => search(_, getRegister(_, argument, "slash", Register.Flags.CanWrite), getRepetitions(_, argument), argument.add, getDirection(argument), argument.interactive, getShift(argument), getInput(argument), getSetInput(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.search.next", + (_, argument) => _.runAsync((_) => next(_, _.document, getRegister(_, argument, "slash", Register.Flags.CanRead), getRepetitions(_, argument), argument.add, getDirection(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.search.selection", + (_, argument) => _.runAsync((_) => selection(_.document, _.selections, getRegister(_, argument, "slash", Register.Flags.CanWrite), argument.smart)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.search.backward", + (_, argument) => _.runAsync(() => commands([".search", { direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.search.backward.extend", + (_, argument) => _.runAsync(() => commands([".search", { direction: -1, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.search.extend", + (_, argument) => _.runAsync(() => commands([".search", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.search.next.add", + (_, argument) => _.runAsync(() => commands([".search.next", { add: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.search.previous", + (_, argument) => _.runAsync(() => commands([".search.next", { direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.search.previous.add", + (_, argument) => _.runAsync(() => commands([".search.next", { direction: -1, add: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.search.selection.smart", + (_, argument) => _.runAsync(() => commands([".search.selection", { smart: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + ]; +} + +/** + * Loads the "seek" module and returns its defined commands. + */ +async function loadSeekModule(): Promise { + const { + enclosing, + object, + seek, + word, + } = await import("./seek"); + + return [ + new CommandDescriptor( + "dance.seek", + (_, argument) => _.runAsync((_) => seek(_, getInputOr(argument), getRepetitions(_, argument), getDirection(argument), getShift(argument), argument.include)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.seek.enclosing", + (_, argument) => _.runAsync((_) => enclosing(_, getDirection(argument), getShift(argument), argument.open, argument.pairs)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.seek.object", + (_, argument) => _.runAsync((_) => object(_, getInputOr(argument), argument.inner, argument.where, getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.seek.word", + (_, argument) => _.runAsync((_) => word(_, getRepetitions(_, argument), argument.stopAtEnd, argument.ws, getDirection(argument), getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.seek.askObject", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.askObject.end", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", pass: [{ where: "end" , shift: "extend", ...argument }], ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.askObject.end", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", pass: [{ where: "end", ...argument }], ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.askObject.inner", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", pass: [{ inner: true, ...argument }], ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.askObject.inner.end", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", pass: [{ inner: true, where: "end", ...argument }], ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.askObject.inner.end.extend", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", pass: [{ inner: true, where: "end" , shift: "extend", ...argument }], ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.askObject.inner.start", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", pass: [{ inner: true, where: "start", ...argument }], ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.askObject.inner.start.extend", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", pass: [{ inner: true, where: "start", shift: "extend", ...argument }], ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.askObject.start", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", pass: [{ where: "start", ...argument }], ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.askObject.start", + (_, argument) => _.runAsync(() => commands([".openMenu", { input: "object", pass: [{ where: "start", shift: "extend", ...argument }], ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.backward", + (_, argument) => _.runAsync(() => commands([".seek", { direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.enclosing.backward", + (_, argument) => _.runAsync(() => commands([".seek.enclosing", { direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.enclosing.extend", + (_, argument) => _.runAsync(() => commands([".seek.enclosing", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.enclosing.extend.backward", + (_, argument) => _.runAsync(() => commands([".seek.enclosing", { shift: "extend", direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.extend", + (_, argument) => _.runAsync(() => commands([".seek", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.extend.backward", + (_, argument) => _.runAsync(() => commands([".seek", { shift: "extend", direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.included", + (_, argument) => _.runAsync(() => commands([".seek", { include: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.included.backward", + (_, argument) => _.runAsync(() => commands([".seek", { include: true, direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.included.extend", + (_, argument) => _.runAsync(() => commands([".seek", { include: true, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.included.extend.backward", + (_, argument) => _.runAsync(() => commands([".seek", { include: true, shift: "extend", direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.word.backward", + (_, argument) => _.runAsync(() => commands([".seek.word", { direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.word.extend", + (_, argument) => _.runAsync(() => commands([".seek.word", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.word.extend.backward", + (_, argument) => _.runAsync(() => commands([".seek.word", { shift: "extend", direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.word.ws", + (_, argument) => _.runAsync(() => commands([".seek.word", { ws: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.word.ws.backward", + (_, argument) => _.runAsync(() => commands([".seek.word", { ws: true, direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.word.ws.extend", + (_, argument) => _.runAsync(() => commands([".seek.word", { ws: true, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.word.ws.extend.backward", + (_, argument) => _.runAsync(() => commands([".seek.word", { ws: true, shift: "extend", direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.wordEnd", + (_, argument) => _.runAsync(() => commands([".seek.word", { stopAtEnd: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.wordEnd.extend", + (_, argument) => _.runAsync(() => commands([".seek.word", { stopAtEnd: true , shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.wordEnd.ws", + (_, argument) => _.runAsync(() => commands([".seek.word", { stopAtEnd: true , ws: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.seek.wordEnd.ws.extend", + (_, argument) => _.runAsync(() => commands([".seek.word", { stopAtEnd: true , ws: true, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + ]; +} + +/** + * Loads the "select" module and returns its defined commands. + */ +async function loadSelectModule(): Promise { + const { + buffer, + firstVisibleLine, + horizontally, + lastLine, + lastModification, + lastVisibleLine, + lineEnd, + lineStart, + line_above, + line_above_extend, + line_below, + line_below_extend, + middleVisibleLine, + to, + vertically, + } = await import("./select"); + + return [ + new CommandDescriptor( + "dance.select.buffer", + (_) => _.runAsync((_) => buffer(_)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.firstVisibleLine", + (_, argument) => _.runAsync((_) => firstVisibleLine(_, getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.horizontally", + (_, argument) => _.runAsync((_) => horizontally(_, argument.avoidEol, getRepetitions(_, argument), getDirection(argument), getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.lastLine", + (_, argument) => _.runAsync((_) => lastLine(_, _.document, getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.lastModification", + (_, argument) => _.runAsync((_) => lastModification(_, getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.lastVisibleLine", + (_, argument) => _.runAsync((_) => lastVisibleLine(_, getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.line.above", + (_, argument) => _.runAsync((_) => line_above(_, getCount(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.line.above.extend", + (_, argument) => _.runAsync((_) => line_above_extend(_, getCount(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.line.below", + (_, argument) => _.runAsync((_) => line_below(_, getCount(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.line.below.extend", + (_, argument) => _.runAsync((_) => line_below_extend(_, getCount(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.lineEnd", + (_, argument) => _.runAsync((_) => lineEnd(_, getCount(_, argument), getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.lineStart", + (_, argument) => _.runAsync((_) => lineStart(_, getCount(_, argument), getShift(argument), argument.skipBlank)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.middleVisibleLine", + (_, argument) => _.runAsync((_) => middleVisibleLine(_, getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.to", + (_, argument) => _.runAsync((_) => to(_, getCount(_, argument), argument, getShift(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.vertically", + (_, argument) => _.runAsync((_) => vertically(_, _.selections, argument.avoidEol, getRepetitions(_, argument), getDirection(argument), getShift(argument), argument.by)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.select.documentEnd.extend", + (_, argument) => _.runAsync(() => commands([".select.lineEnd", { count: 2147483647, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.documentEnd.jump", + (_, argument) => _.runAsync(() => commands([".select.lineEnd", { count: 2147483647, shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.down.extend", + (_, argument) => _.runAsync(() => commands([".select.vertically", { direction: 1, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.down.jump", + (_, argument) => _.runAsync(() => commands([".select.vertically", { direction: 1, shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.firstLine.extend", + (_, argument) => _.runAsync(() => commands([".select.lineStart", { count: 0, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.firstLine.jump", + (_, argument) => _.runAsync(() => commands([".select.lineStart", { count: 0, shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.firstVisibleLine.extend", + (_, argument) => _.runAsync(() => commands([".select.firstVisibleLine", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.firstVisibleLine.jump", + (_, argument) => _.runAsync(() => commands([".select.firstVisibleLine", { shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lastLine.extend", + (_, argument) => _.runAsync(() => commands([".select.lastLine", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lastLine.jump", + (_, argument) => _.runAsync(() => commands([".select.lastLine", { shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lastModification.extend", + (_, argument) => _.runAsync(() => commands([".select.lastModification", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lastModification.jump", + (_, argument) => _.runAsync(() => commands([".select.lastModification", { shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lastVisibleLine.extend", + (_, argument) => _.runAsync(() => commands([".select.lastVisibleLine", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lastVisibleLine.jump", + (_, argument) => _.runAsync(() => commands([".select.lastVisibleLine", { shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.left.extend", + (_, argument) => _.runAsync(() => commands([".select.horizontally", { direction: -1, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.left.jump", + (_, argument) => _.runAsync(() => commands([".select.horizontally", { direction: -1, shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lineEnd.extend", + (_, argument) => _.runAsync(() => commands([".select.lineEnd", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lineStart.extend", + (_, argument) => _.runAsync(() => commands([".select.lineStart", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lineStart.jump", + (_, argument) => _.runAsync(() => commands([".select.lineStart", { shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lineStart.skipBlank.extend", + (_, argument) => _.runAsync(() => commands([".select.lineStart", { skipBlank: true, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.lineStart.skipBlank.jump", + (_, argument) => _.runAsync(() => commands([".select.lineStart", { skipBlank: true, shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.middleVisibleLine.extend", + (_, argument) => _.runAsync(() => commands([".select.middleVisibleLine", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.middleVisibleLine.jump", + (_, argument) => _.runAsync(() => commands([".select.middleVisibleLine", { shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.right.extend", + (_, argument) => _.runAsync(() => commands([".select.horizontally", { direction: 1, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.right.jump", + (_, argument) => _.runAsync(() => commands([".select.horizontally", { direction: 1, shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.to.extend", + (_, argument) => _.runAsync(() => commands([".select.to", { shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.to.jump", + (_, argument) => _.runAsync(() => commands([".select.to", { shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.up.extend", + (_, argument) => _.runAsync(() => commands([".select.vertically", { direction: -1, shift: "extend", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.select.up.jump", + (_, argument) => _.runAsync(() => commands([".select.vertically", { direction: -1, shift: "jump", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + ]; +} + +/** + * Loads the "selections" module and returns its defined commands. + */ +async function loadSelectionsModule(): Promise { + const { + changeDirection, + copy, + expandToLines, + filter, + merge, + open, + pipe, + reduce, + restore, + restore_withCurrent, + save, + saveText, + select, + split, + splitLines, + toggleIndices, + trimLines, + trimWhitespace, + } = await import("./selections"); + + return [ + new CommandDescriptor( + "dance.selections.changeDirection", + (_, argument) => _.runAsync((_) => changeDirection(_, getDirection(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.copy", + (_, argument) => _.runAsync((_) => copy(_, _.document, _.selections, getRepetitions(_, argument), getDirection(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.expandToLines", + (_) => _.runAsync((_) => expandToLines(_)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.filter", + (_, argument) => _.runAsync((_) => filter(_, getInput(argument), getSetInput(argument), argument.defaultInput, argument.inverse, argument.interactive)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.merge", + (_) => _.runAsync((_) => merge(_)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.open", + (_) => _.runAsync((_) => open(_)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.pipe", + (_, argument) => _.runAsync((_) => pipe(_, getRegister(_, argument, "pipe", Register.Flags.CanWrite), getInputOr(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.reduce", + (_, argument) => _.runAsync((_) => reduce(_, argument.where)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.restore", + (_, argument) => _.runAsync((_) => restore(_, getRegister(_, argument, "caret", Register.Flags.CanReadSelections))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.restore.withCurrent", + (_, argument) => _.runAsync((_) => restore_withCurrent(_, _.document, getRegister(_, argument, "caret", Register.Flags.CanReadSelections), argument.reverse)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.save", + (_, argument) => _.runAsync((_) => save(_, _.document, _.selections, getRegister(_, argument, "caret", Register.Flags.CanWriteSelections), argument.style, argument.until)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.saveText", + (_, argument) => _.runAsync((_) => saveText(_.document, _.selections, getRegister(_, argument, "dquote", Register.Flags.CanWrite))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.select", + (_, argument) => _.runAsync((_) => select(_, argument.interactive, getInput(argument), getSetInput(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.split", + (_, argument) => _.runAsync((_) => split(_, argument.excludeEmpty, argument.interactive, getInput(argument), getSetInput(argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.splitLines", + (_, argument) => _.runAsync((_) => splitLines(_, _.document, _.selections, getRepetitions(_, argument))), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.toggleIndices", + (_, argument) => _.runAsync((_) => toggleIndices(_, argument.display, argument.until)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.trimLines", + (_) => _.runAsync((_) => trimLines(_)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.trimWhitespace", + (_) => _.runAsync((_) => trimWhitespace(_)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.clear.main", + (_, argument) => _.runAsync(() => commands([".selections.filter", { input: "i !== 0", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.clear.secondary", + (_, argument) => _.runAsync(() => commands([".selections.filter", { input: "i === 0", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.copy.above", + (_, argument) => _.runAsync(() => commands([".selections.copy", { direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.faceBackward", + (_, argument) => _.runAsync(() => commands([".selections.changeDirection", { direction: -1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.faceForward", + (_, argument) => _.runAsync(() => commands([".selections.changeDirection", { direction: 1, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.filter.regexp", + (_, argument) => _.runAsync(() => commands([".selections.filter", { defaultInput: "/", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.filter.regexp.inverse", + (_, argument) => _.runAsync(() => commands([".selections.filter", { defaultInput: "/", inverse: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.hideIndices", + (_, argument) => _.runAsync(() => commands([".selections.toggleIndices", { display: false, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.pipe.append", + (_, argument) => _.runAsync(() => commands([".selections.pipe"], [".edit.insert", { register: "|", where: "end", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.pipe.prepend", + (_, argument) => _.runAsync(() => commands([".selections.pipe"], [".edit.insert", { register: "|", where: "start", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.pipe.replace", + (_, argument) => _.runAsync(() => commands([".selections.pipe"], [".edit.insert", { register: "|", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.reduce.edges", + (_, argument) => _.runAsync(() => commands([".selections.reduce", { where: "both", ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.showIndices", + (_, argument) => _.runAsync(() => commands([".selections.toggleIndices", { display: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + ]; +} + +/** + * Loads the "selections.rotate" module and returns its defined commands. + */ +async function loadSelectionsRotateModule(): Promise { + const { + both, + contents, + selections, + } = await import("./selections.rotate"); + + return [ + new CommandDescriptor( + "dance.selections.rotate.both", + (_, argument) => _.runAsync((_) => both(_, getRepetitions(_, argument), argument.reverse)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.rotate.contents", + (_, argument) => _.runAsync((_) => contents(_, getRepetitions(_, argument), argument.reverse)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.rotate.selections", + (_, argument) => _.runAsync((_) => selections(_, getRepetitions(_, argument), argument.reverse)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + new CommandDescriptor( + "dance.selections.rotate.both.reverse", + (_, argument) => _.runAsync(() => commands([".selections.rotate", { reverse: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.rotate.contents.reverse", + (_, argument) => _.runAsync(() => commands([".selections.rotate.contents", { reverse: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + new CommandDescriptor( + "dance.selections.rotate.selections.reverse", + (_, argument) => _.runAsync(() => commands([".selections.rotate.selections", { reverse: true, ...argument }])), + CommandDescriptor.Flags.RequiresActiveEditor | CommandDescriptor.Flags.DoNotReplay, + ), + ]; +} + +/** + * Loads the "view" module and returns its defined commands. + */ +async function loadViewModule(): Promise { + const { + line, + } = await import("./view"); + + return [ + new CommandDescriptor( + "dance.view.line", + (_, argument) => _.runAsync((_) => line(_, argument.at)), + CommandDescriptor.Flags.RequiresActiveEditor, + ), + ]; +} + +/** + * Loads and returns all defined commands. + */ +export async function loadCommands(): Promise { + const allModules = await Promise.all([ + loadDevModule(), + loadEditModule(), + loadHistoryModule(), + loadKeybindingsModule(), + loadMiscModule(), + loadModesModule(), + loadSearchModule(), + loadSeekModule(), + loadSelectModule(), + loadSelectionsModule(), + loadSelectionsRotateModule(), + loadViewModule(), + ]); + + return Object.freeze( + Object.fromEntries(allModules.flat().map((desc) => [desc.identifier, desc])), + ); +} diff --git a/src/commands/macros.ts b/src/commands/macros.ts deleted file mode 100644 index 0dd7152..0000000 --- a/src/commands/macros.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Command, CommandDescriptor, CommandFlags, registerCommand } from "."; -import { MacroRegister, Register } from "../registers"; - -registerCommand( - Command.macrosRecordStart, - CommandFlags.IgnoreInHistory, - (editorState, { currentRegister, extension }) => { - const reg - = ((currentRegister as any) as MacroRegister & Register) ?? extension.registers.arobase; - - if (typeof reg.setMacro === "function") { - return editorState.startMacroRecording(reg)?.then(() => void 0); - } - - return; - }, -); - -registerCommand( - Command.macrosRecordStop, - CommandFlags.SwitchToNormal | CommandFlags.IgnoreInHistory, - (editorState) => { - return editorState.stopMacroRecording()?.then((recording) => { - const commands = editorState.recordedCommands.slice(recording.lastHistoryEntry); - - recording.register.setMacro( - commands.filter((x) => !(x.descriptor.flags & CommandFlags.IgnoreInHistory)), - ); - }); - }, -); - -registerCommand( - Command.macrosPlay, - CommandFlags.ChangeSelections | CommandFlags.Edit, - (editorState, { currentRegister, extension, repetitions }) => { - const reg = ((currentRegister as any) as MacroRegister) ?? extension.registers.arobase; - - if (typeof reg.getMacro === "function") { - const commands = reg.getMacro(); - - if (commands !== undefined) { - for (let i = repetitions; i > 0; i--) { - CommandDescriptor.executeMany(editorState, commands); - } - } - } - }, -); diff --git a/src/commands/mark.ts b/src/commands/mark.ts deleted file mode 100644 index efefed5..0000000 --- a/src/commands/mark.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Marks -// https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#marks -import * as vscode from "vscode"; - -import { Command, CommandFlags, InputKind, registerCommand } from "."; -import { Register } from "../registers"; -import { SavedSelection } from "../utils/savedSelection"; - -registerCommand( - Command.registersSelect, - CommandFlags.IgnoreInHistory, - InputKind.Key, - () => void 0, - (_, { extension, input: key }) => { - extension.currentRegister = extension.registers.get(key); - }, -); - -const marksByRegister = new Map< - Register, - WeakMap ->(); - -function marksForRegister(register: Register) { - let map = marksByRegister.get(register); - - if (map === undefined) { - marksByRegister.set(register, (map = new WeakMap())); - } - - return map; -} - -registerCommand( - Command.marksSaveSelections, - CommandFlags.None, - ({ editor, extension, documentState }, { currentRegister }) => { - const map = marksForRegister(currentRegister ?? extension.registers.caret), - existingMarks = map.get(editor.document); - - if (existingMarks !== undefined) { - documentState.forgetSelections(existingMarks); - } - - map.set( - editor.document, - editor.selections.map((selection) => documentState.saveSelection(selection)), - ); - }, -); - -registerCommand( - Command.marksRestoreSelections, - CommandFlags.ChangeSelections, - ({ editor, extension }, { currentRegister }) => { - const map = marksForRegister(currentRegister ?? extension.registers.caret), - marks = map.get(editor.document); - - if (marks !== undefined) { - editor.selections = marks.map((savedSelection) => savedSelection.selection(editor.document)); - } - }, -); - -const combineOpts: () => [string, string][] = () => [ - ["a", "Append lists"], - ["u", "Union"], - ["i", "Intersection"], - ["<", "Select leftmost cursor"], - [">", "Select rightmost cursor"], - ["+", "Select longest"], - ["-", "Select shortest"], -]; - -function combineSelections( - editor: vscode.TextEditor, - from: vscode.Selection[], - add: vscode.Selection[], - type: number, -) { - if (type === 0) { - editor.selections = from.concat(add); - - return; - } - - if (from.length !== add.length) { - throw new Error("the current and marked selections have different sizes"); - } - - const selections = [] as vscode.Selection[]; - - for (let i = 0; i < from.length; i++) { - const a = from[i], - b = add[i]; - - switch (type) { - case 1: { - const anchor = a.start.isBefore(b.start) ? a.start : b.start, - active = a.end.isAfter(b.end) ? a.end : b.end; - - selections.push(new vscode.Selection(anchor, active)); - break; - } - - case 2: { - const anchor = a.start.isAfter(b.start) ? a.start : b.start, - active = a.end.isBefore(b.end) ? a.end : b.end; - - selections.push(new vscode.Selection(anchor, active)); - break; - } - - case 3: - if (a.active.isBeforeOrEqual(b.active)) { - selections.push(a); - } else { - selections.push(b); - } - break; - - case 4: - if (a.active.isAfterOrEqual(b.active)) { - selections.push(a); - } else { - selections.push(b); - } - break; - - case 5: { - const aLength = editor.document.offsetAt(a.end) - editor.document.offsetAt(a.start), - bLength = editor.document.offsetAt(b.end) - editor.document.offsetAt(b.start); - - if (aLength > bLength) { - selections.push(a); - } else { - selections.push(b); - } - break; - } - - case 6: { - const aLength = editor.document.offsetAt(a.end) - editor.document.offsetAt(a.start), - bLength = editor.document.offsetAt(b.end) - editor.document.offsetAt(b.start); - - if (aLength < bLength) { - selections.push(a); - } else { - selections.push(b); - } - break; - } - } - } - - editor.selections = selections; -} - -registerCommand( - Command.marksCombineSelectionsFromCurrent, - CommandFlags.ChangeSelections, - InputKind.ListOneItem, - combineOpts, - ({ editor, extension }, { currentRegister, input }) => { - const map = marksForRegister(currentRegister ?? extension.registers.caret), - marks = map.get(editor.document); - - if (marks === undefined) { - return; - } - - combineSelections( - editor, - editor.selections, - marks.map((savedSelection) => savedSelection.selection(editor.document)), - input, - ); - }, -); - -registerCommand( - Command.marksCombineSelectionsFromRegister, - CommandFlags.ChangeSelections, - InputKind.ListOneItem, - combineOpts, - ({ editor, extension }, { currentRegister, input }) => { - const map = marksForRegister(currentRegister ?? extension.registers.caret), - marks = map.get(editor.document); - - if (marks === undefined) { - return; - } - - combineSelections( - editor, - marks.map((savedSelection) => savedSelection.selection(editor.document)), - editor.selections, - input, - ); - }, -); diff --git a/src/commands/menus.ts b/src/commands/menus.ts deleted file mode 100644 index 51c10ac..0000000 --- a/src/commands/menus.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as vscode from "vscode"; - -import { CommandFlags, registerCommand } from "."; -import { Extension } from "../state/extension"; -import { promptInList } from "../utils/prompt"; -import { Command } from "../../commands"; - -registerCommand( - Command.openMenu, - CommandFlags.CanRunWithoutEditor, - (_, { argument, extension }) => { - if (typeof argument !== "object" || argument === null || typeof argument.menu !== "string") { - throw new Error(`invalid argument`); - } - - const menuName = argument.menu; - - if (Object.keys(argument).length > 1) { - const argFields = Object.assign({}, argument); - delete argFields.menu; - openMenu(menuName, extension, argFields); - } else { - openMenu(menuName, extension); - } - }, -); - -export async function openMenu( - menuName: string, - extension: Extension, - argFields?: Record, -) { - const menu = extension.menus.get(menuName); - - if (menu === undefined) { - throw new Error(`menu ${JSON.stringify(menuName)} does not exist`); - } - - const entries = Object.entries(menu.items); - const items = entries.map((x) => [x[0], x[1].text]) as [string, string][]; - const choice = await promptInList(false, items, extension.cancellationTokenSource?.token); - - if (choice === undefined) { - return; - } - - const pickedItem = entries[choice][1]; - - let args = pickedItem.args ?? []; - if (argFields) { - args = [Object.assign({}, argFields, args[0]), ...args.slice(1)]; - } - - await vscode.commands.executeCommand(pickedItem.command, ...args); -} diff --git a/src/commands/misc.ts b/src/commands/misc.ts index 3eacaec..5e8e605 100644 --- a/src/commands/misc.ts +++ b/src/commands/misc.ts @@ -1,128 +1,219 @@ -import * as vscode from "vscode"; -import { - Command, - CommandDescriptor, - CommandFlags, - CommandState, - InputKind, - commandsByName, - registerCommand, -} from "."; +import * as api from "../api"; -registerCommand(Command.cancel, CommandFlags.IgnoreInHistory, () => { - // Nop, because the caller cancels everything before calling us. -}); +import { Argument, InputOr } from "."; +import { Context, InputError, keypress, Menu, prompt, showLockedMenu, showMenu, validateMenu } from "../api"; +import { Extension } from "../state/extension"; +import { Register } from "../state/registers"; -type RunFunction = (vscodeObj: typeof vscode, danceObj: object, args: any) => Promise | any; -type RunFunctionConstructor = - (vscodeObj: "vscode", danceObj: "dance", args: "args", code: string) => RunFunction; +/** + * Miscellaneous commands that don't deserve their own category. + * + * By default, Dance also exports the following keybindings for existing + * commands: + * + * | Keybinding | Command | + * | -------------- | ----------------------------------- | + * | `s-;` (normal) | `["workbench.action.showCommands"]` | + */ +declare module "./misc"; -const AsyncFunction = async function () {}.constructor as RunFunctionConstructor; +/** + * Cancel Dance operation. + * + * @keys `escape` (normal), `escape` (input) + */ +export function cancel(extension: Extension) { + // Calling a new command resets pending operations, so we don't need to do + // anything special here. + extension.cancelLastOperation(); +} -registerCommand(Command.run, CommandFlags.IgnoreInHistory, async (editorState, state) => { - let code = state.argument?.code; +/** + * Ignore key. + */ +export function ignore() { + // Used to intercept and ignore key presses in a given mode. +} + +let lastRunCode: string | undefined; + +/** + * Run code. + */ +export async function run( + _: Context, + inputOr: InputOr, + + commands?: Argument, +) { + if (Array.isArray(commands)) { + return api.commands(...commands); + } + + let code = await inputOr(() => prompt({ + prompt: "Code to run", + validateInput(value) { + try { + api.run.compileFunction(value); + + return; + } catch (e) { + if (e instanceof SyntaxError) { + return `invalid syntax: ${e.message}`; + } + + return e?.message ?? `${e}`; + } + }, + value: lastRunCode, + valueSelection: lastRunCode === undefined ? undefined : [0, lastRunCode.length], + }, _)); if (Array.isArray(code)) { code = code.join("\n"); } else if (typeof code !== "string") { - throw new Error(`expected code to be a string or an array, but it was ${code}`); + return new InputError(`expected code to be a string or an array, but it was ${code}`); } - let func: RunFunction; + return _.run(() => api.run(code as string)); +} - try { - func = AsyncFunction("vscode", "dance", "args", code) as any; - } catch (e) { - throw new Error(`cannot parse function body: ${code}: ${e}`); +/** + * Select register for next command. + * + * When selecting a register, the next key press is used to determine what + * register is selected. If this key is a `space` character, then a new key + * press is awaited again and the returned register will be specific to the + * current document. + * + * @keys `"` (normal) + */ +export async function selectRegister(_: Context, inputOr: InputOr) { + const input = await inputOr(() => keypress.forRegister(_)); + + if (typeof input === "string") { + if (input.length === 0) { + return; + } + + const extension = _.extension, + registers = extension.registers; + + extension.currentRegister = input.startsWith(" ") + ? registers.forDocument(_.document).get(input.slice(1)) + : registers.get(input); + } else { + _.extension.currentRegister = input; + } +} + +/** + * Update Dance count. + * + * Update the current counter used to repeat the next command. + * + * #### Additional keybindings + * + * | Title | Keybinding | Command | + * | ------------------------------ | ------------ | ------------------------------------ | + * | Add the digit 0 to the counter | `0` (normal) | `[".updateCount", { addDigits: 0 }]` | + * | Add the digit 1 to the counter | `1` (normal) | `[".updateCount", { addDigits: 1 }]` | + * | Add the digit 2 to the counter | `2` (normal) | `[".updateCount", { addDigits: 2 }]` | + * | Add the digit 3 to the counter | `3` (normal) | `[".updateCount", { addDigits: 3 }]` | + * | Add the digit 4 to the counter | `4` (normal) | `[".updateCount", { addDigits: 4 }]` | + * | Add the digit 5 to the counter | `5` (normal) | `[".updateCount", { addDigits: 5 }]` | + * | Add the digit 6 to the counter | `6` (normal) | `[".updateCount", { addDigits: 6 }]` | + * | Add the digit 7 to the counter | `7` (normal) | `[".updateCount", { addDigits: 7 }]` | + * | Add the digit 8 to the counter | `8` (normal) | `[".updateCount", { addDigits: 8 }]` | + * | Add the digit 9 to the counter | `9` (normal) | `[".updateCount", { addDigits: 9 }]` | + */ +export async function updateCount( + _: Context, + count: number, + extension: Extension, + inputOr: InputOr, + + addDigits?: Argument, +) { + if (typeof addDigits === "number") { + let nextPowerOfTen = 1; + + if (addDigits <= 0) { + addDigits = 0; + nextPowerOfTen = 10; + } + + while (nextPowerOfTen <= addDigits) { + nextPowerOfTen *= 10; + } + + extension.currentCount = count * nextPowerOfTen + addDigits; + + return; } - const danceInterface = Object.freeze({ - async execute(...commands: any[]) { - const batches = [] as (readonly CommandState[] | [string, any])[], - currentBatch = [] as CommandState[], - extension = editorState.extension; + const input = +await inputOr(() => prompt.number({ integer: true, range: [0, 1_000_000] }, _)); - if (commands.length === 2 && typeof commands[0] === "string") { - commands = [commands]; + InputError.validateInput(!isNaN(input), "value is not a number"); + InputError.validateInput(input >= 0, "value is negative"); + + extension.currentCount = input; +} + +let lastPickedMenu: string | undefined; + +/** + * Open menu. + * + * If no input is specified, a prompt will ask for the name of the menu to open. + * + * Alternatively, a `menu` can be inlined in the arguments. + * + * Pass a `prefix` argument to insert the prefix string followed by the typed + * key if it does not match any menu entry. This can be used to implement chords + * like `jj`. + */ +export async function openMenu( + _: Context.WithoutActiveEditor, + + inputOr: InputOr, + menu?: Argument, + prefix?: Argument, + pass: Argument = [], + locked: Argument = false, +) { + if (typeof menu === "object") { + const errors = validateMenu(menu); + + if (errors.length > 0) { + throw new Error(`invalid menu: ${errors.join(", ")}`); + } + + if (locked) { + return showLockedMenu(menu, pass); + } + + return showMenu(menu, pass, prefix); + } + + const menus = _.extension.menus; + const input = await inputOr(() => prompt({ + prompt: "Menu name", + validateInput(value) { + if (menus.has(value)) { + lastPickedMenu = value; + return; } - // Build and validate commands. - for (let i = 0, len = commands.length; i < len; i++) { - let commandName: string, - commandArgument: any; - - const command = commands[i]; - - if (typeof command === "string") { - commandName = command; - commandArgument = undefined; - } else if (Array.isArray(command) && command.length === 1) { - if (typeof command[0] !== "string") { - throw new Error("the first element of an execute tuple must be a command name"); - } - - commandName = command[0]; - commandArgument = undefined; - } else if (Array.isArray(command) && command.length === 2) { - if (typeof command[0] !== "string") { - throw new Error("the first element of an execute tuple must be a command name"); - } - - commandName = command[0]; - commandArgument = command[1]; - } else { - throw new Error( - "execute arguments must be command names or [command name, argument] tuples", - ); - } - - if (commandName.startsWith(".")) { - commandName = `dance${commandName}`; - } - - if (commandName in commandsByName) { - const descriptor = commandsByName[commandName as Command], - input = await descriptor.getInput( - editorState, - commandArgument, - extension.cancellationTokenSource?.token); - - if (descriptor.input !== InputKind.None && input === undefined) { - return; - } - - currentBatch.push(new CommandState(descriptor, input, extension, commandArgument)); - } else { - if (commandName.startsWith("dance.")) { - throw new Error(`Dance command ${JSON.stringify(commandName)} does not exist`); - } - - if (currentBatch.length > 0) { - batches.push(currentBatch.splice(0)); - } - - batches.push([commandName, commandArgument]); - } - } - - if (currentBatch.length > 0) { - batches.push(currentBatch); - } - - // Execute all commands. - for (const batch of batches) { - if (typeof batch[0] === "string") { - await vscode.commands.executeCommand(batch[0], batch[1]); - } else { - await CommandDescriptor.executeMany(editorState, batch); - } - } + return `menu ${JSON.stringify(value)} does not exist`; }, - }); + placeHolder: [...menus.keys()].sort().join(", ") || "no menu defined", + value: lastPickedMenu, + }, _)); - try { - await func(vscode, danceInterface, state.argument); - } catch (e) { - throw new Error(`code threw an exception: ${e}`); + if (locked) { + return showLockedMenu.byName(input, pass); } -}); + + return showMenu.byName(input, pass, prefix); +} diff --git a/src/commands/modes.ts b/src/commands/modes.ts index 36ca1e6..27b4476 100644 --- a/src/commands/modes.ts +++ b/src/commands/modes.ts @@ -1,31 +1,62 @@ import * as vscode from "vscode"; -import { Command, CommandFlags, registerCommand, setRemainingNormalCommands } from "."; -import { Mode } from "../state/extension"; +import { InputOr } from "."; +import { Context, prompt, toMode } from "../api"; -registerCommand(Command.setInsert, CommandFlags.SwitchToInsertBefore, () => { - // Nop. -}); +/** + * Set modes. + */ +declare module "./modes"; -registerCommand(Command.setNormal, CommandFlags.SwitchToNormal, () => { - // Nop. -}); +/** + * Set Dance mode. + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | ------------------ | ------------ | ----------------- | ------------------------------------- | + * | Set mode to Normal | `set.normal` | `escape` (insert) | `[".modes.set", { input: "normal" }]` | + * | Set mode to Insert | `set.insert` | | `[".modes.set", { input: "insert" }]` | + * + * Other variants are provided to switch to insert mode: + * + * | Title | Identifier | Keybinding | Commands | + * | -------------------- | ------------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | + * | Insert before | `insert.before` | `i` (normal) | `[".selections.faceBackward"], [".modes.set", { input: "insert" }], [".selections.reduce", { where: "start" }]` | + * | Insert after | `insert.after` | `a` (normal) | `[".selections.faceForward"] , [".modes.set", { input: "insert" }], [".selections.reduce", { where: "end" }]` | + * | Insert at line start | `insert.lineStart` | `s-i` (normal) | `[".select.lineStart", { shift: "jump", skipBlank: true }], [".modes.set", { input: "insert" }], [".selections.reduce", { where: "start" }]` | + * | Insert at line end | `insert.lineEnd` | `s-a` (normal) | `[".select.lineEnd" , { shift: "jump" }], [".modes.set", { input: "insert" }], [".selections.reduce", { where: "end" }]` | + */ +export async function set(_: Context, inputOr: InputOr) { + await toMode(await inputOr(() => prompt(validateModeName()))); +} -registerCommand( - Command.tmpInsert, - CommandFlags.SwitchToInsertBefore, - (editorState, { repetitions }) => { - const subscription = vscode.commands.registerCommand("type", (...args) => { - if (--repetitions === 0) { - subscription.dispose(); - editorState.setMode(Mode.Normal); +/** + * Set Dance mode temporarily. + * + * #### Variants + * + * | Title | Identifier | Keybindings | Commands | + * | --------------------- | ------------------------ | -------------- | ------------------------------------------------- | + * | Temporary Normal mode | `set.temporarily.normal` | `c-v` (insert) | `[".modes.set.temporarily", { input: "normal" }]` | + * | Temporart Insert mode | `set.temporarily.insert` | `c-v` (normal) | `[".modes.set.temporarily", { input: "insert" }]` | + */ +export async function set_temporarily(_: Context, inputOr: InputOr, repetitions: number) { + await toMode(await inputOr(() => prompt(validateModeName())), repetitions); +} + +function validateModeName(ctx = Context.WithoutActiveEditor.current) { + const modes = ctx.extension.modes; + + return { + prompt: "Mode name", + validateInput(value) { + if (modes.get(value) !== undefined) { + return; } - return vscode.commands.executeCommand("default:type", ...args); - }); - }, -); - -registerCommand(Command.tmpNormal, CommandFlags.SwitchToNormal, (_, { repetitions }) => { - setRemainingNormalCommands(repetitions); -}); + return `mode ${JSON.stringify(value)} does not exist`; + }, + placeHolder: [...modes.userModes()].map((m) => m.name).sort().join(", "), + } as vscode.InputBoxOptions; +} diff --git a/src/commands/move.ts b/src/commands/move.ts deleted file mode 100644 index ebbbaf0..0000000 --- a/src/commands/move.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Movement -// https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#movement -import * as vscode from "vscode"; - -import { Command, CommandFlags, registerCommand } from "."; -import { - Backward, - Direction, - DoNotExtend, - Extend, - ExtendBehavior, - Forward, - jumpTo, -} from "../utils/selectionHelper"; -import { Coord, SelectionHelper } from "../utils/selectionHelper"; -import { SelectionBehavior } from "../state/extension"; - -// Move around (h, j, k, l, H, J, K, L, arrows, shift+arrows) -// =============================================================================================== - -function revealActiveTowards(direction: Direction, editor: vscode.TextEditor) { - let revealPosition = undefined as vscode.Position | undefined; - for (let i = 0; i < editor.selections.length; i++) { - const activePosition = editor.selections[i].active; - - if (revealPosition === undefined || revealPosition.compareTo(activePosition) * direction > 0) { - revealPosition = activePosition; - } - } - editor.revealRange(new vscode.Range(revealPosition!, revealPosition!)); -} - -function registerMoveHorizontal(command: Command, direction: Direction, extend: ExtendBehavior) { - const selectionMapper = jumpTo((from, helper) => { - return helper.coordAt(helper.offsetAt(from) + helper.state.repetitions * direction); - }, extend); - - registerCommand(command, CommandFlags.ChangeSelections, (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(selectionMapper); - revealActiveTowards(direction, editorState.editor); - }); -} - -function registerMoveVertical(command: Command, direction: Direction, extend: ExtendBehavior) { - const selectionMapper = jumpTo((from, helper, i) => { - const targetLine = from.line + helper.state.repetitions * direction; - let actualLine = targetLine; - if (actualLine < 0) { - actualLine = 0; - } else if (targetLine > helper.editor.document.lineCount - 1) { - actualLine = helper.editor.document.lineCount - 1; - } - - const lineLen = helper.editor.document.lineAt(actualLine).text.length; - if (lineLen === 0) { - // Select the line break on an empty line. - return new Coord(actualLine, 0); - } - - const preferredColumn = helper.editorState.preferredColumns![i]; - if (preferredColumn >= lineLen) { - if (helper.selectionBehavior === SelectionBehavior.Character) { - return new Coord(actualLine, lineLen - 1); - } else { - return new Coord(actualLine, lineLen); - } - } - return new Coord(actualLine, preferredColumn); - }, extend); - - registerCommand( - command, - CommandFlags.ChangeSelections | CommandFlags.DoNotResetPreferredColumns, - (editorState, state) => { - const { editor, preferredColumns } = editorState, - selectionHelper = SelectionHelper.for(editorState, state); - - if (preferredColumns.length === 0) { - for (let i = 0; i < editor.selections.length; i++) { - const column = selectionHelper.activeCoord(editor.selections[i]).character; - - preferredColumns.push(column); - } - } - SelectionHelper.for(editorState, state).mapEach(selectionMapper); - revealActiveTowards(direction, editorState.editor); - }, - ); -} - -// Move/extend left/down/up/right - -registerMoveHorizontal(Command.left, Backward, DoNotExtend); -registerMoveHorizontal(Command.leftExtend, Backward, Extend); -registerMoveHorizontal(Command.right, Forward, DoNotExtend); -registerMoveHorizontal(Command.rightExtend, Forward, Extend); -registerMoveVertical(Command.up, Backward, DoNotExtend); -registerMoveVertical(Command.upExtend, Backward, Extend); -registerMoveVertical(Command.down, Forward, DoNotExtend); -registerMoveVertical(Command.downExtend, Forward, Extend); - -// Move up/down (ctrl-[bfud]) -// =============================================================================================== - -function scrollBy(iterations: number, to: "up" | "down", by: "page" | "halfPage") { - return vscode.commands.executeCommand("editorScroll", { - to, - by, - value: iterations, - revealCursor: true, - }) as Promise; -} - -registerCommand(Command.upPage, CommandFlags.ChangeSelections, (_, { repetitions }) => - scrollBy(repetitions, "up", "page"), -); -registerCommand(Command.upHalfPage, CommandFlags.ChangeSelections, (_, { repetitions }) => - scrollBy(repetitions, "up", "halfPage"), -); -registerCommand(Command.downPage, CommandFlags.ChangeSelections, (_, { repetitions }) => - scrollBy(repetitions, "down", "page"), -); -registerCommand(Command.downHalfPage, CommandFlags.ChangeSelections, (_, { repetitions }) => - scrollBy(repetitions, "down", "halfPage"), -); - -// Other bindings (%) -// =============================================================================================== - -registerCommand(Command.selectBuffer, CommandFlags.ChangeSelections, ({ editor }) => { - const start = new vscode.Position(0, 0); - const end = editor.document.lineAt(editor.document.lineCount - 1).range.end; - - editor.selection = new vscode.Selection(start, end); -}); diff --git a/src/commands/pipe.ts b/src/commands/pipe.ts deleted file mode 100644 index 987bc32..0000000 --- a/src/commands/pipe.ts +++ /dev/null @@ -1,355 +0,0 @@ -// Pipes -// https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#changes-through-external-programs -import * as cp from "child_process"; -import * as vscode from "vscode"; - -import { Command, CommandFlags, InputKind, registerCommand } from "."; -import { SelectionHelper } from "../utils/selectionHelper"; - -function getShell() { - let os: string; - - if (process.platform === "cygwin") { - os = "linux"; - } else if (process.platform === "linux") { - os = "linux"; - } else if (process.platform === "darwin") { - os = "osx"; - } else if (process.platform === "win32") { - os = "windows"; - } else { - return undefined; - } - - const config = vscode.workspace.getConfiguration("terminal"); - - return ( - config.get(`integrated.automationShell.${os}`) ?? process.env.SHELL ?? undefined - ); -} - -function execWithInput(command: string, input: string) { - return new Promise<{ readonly val: string } | { readonly err: string }>((resolve) => { - const shell = getShell() ?? true, - child = cp.spawn(command, { shell, stdio: "pipe" }); - - let stdout = "", - stderr = ""; - - child.stdout.on("data", (chunk: Buffer) => (stdout += chunk.toString("utf-8"))); - child.stderr.on("data", (chunk: Buffer) => (stderr += chunk.toString("utf-8"))); - child.stdin.end(input, "utf-8"); - - child.once("error", (err) => resolve({ err: err.message })); - child.once("exit", (code) => - code === 0 - ? resolve({ val: stdout.trimRight() }) - : resolve({ - err: `Command exited with error ${code}: ${ - stderr.length > 0 ? stderr.trimRight() : "" - }`, - }), - ); - }); -} - -function parseRegExp(regexp: string) { - if (regexp.length < 3 || regexp[0] !== "/") { - return "Invalid RegExp."; - } - - let pattern = ""; - let replacement: string | undefined = undefined; - let flags: string | undefined = undefined; - - for (let i = 1; i < regexp.length; i++) { - const ch = regexp[i]; - - if (flags !== undefined) { - // Parse flags - if (ch !== "m" && ch !== "i" && ch !== "g") { - return `Unknown flag '${ch}'.`; - } - - flags += ch; - } else if (replacement !== undefined) { - // Parse replacement string - if (ch === "/") { - flags = ""; - } else if (ch === "\\") { - if (i === regexp.length - 1) { - return "Unexpected end of RegExp."; - } - - replacement += ch + regexp[++i]; - } else { - replacement += ch; - } - } else { - // Parse pattern - if (ch === "/") { - replacement = ""; - } else if (ch === "\\") { - if (i === regexp.length - 1) { - return "Unexpected end of RegExp."; - } - - pattern += ch + regexp[++i]; - } else { - pattern += ch; - } - } - } - - try { - return [new RegExp(pattern, flags), replacement] as [RegExp, string | undefined]; - } catch { - return "Invalid RegExp."; - } -} - -function pipe(command: string, selections: string[]) { - if (command.startsWith("#")) { - // Shell - command = command.substr(1); - - return Promise.all(selections.map((selection) => execWithInput(command, selection))); - } else if (command.startsWith("/")) { - // RegExp replace (note that it's safe to call parseRegExp since the input - // is validated) - const [regexp, replacement] = parseRegExp(command) as [RegExp, string]; - - return Promise.resolve(selections.map((x) => ({ val: x.replace(regexp, replacement) }))); - } else { - // JavaScript - const funct = new Function("$", "i", "$$", "return " + command) as ( - $: string, - i: number, - $$: string[], - ) => any; - - return Promise.resolve( - selections.map(($, i, $$) => { - let result: any; - - try { - result = funct($, i, $$); - } catch { - return { err: "Exception thrown in given expression." }; - } - - if (result === null) { - return { val: "null" }; - } - if (result === undefined) { - return { val: "" }; - } - if (typeof result === "string") { - return { val: result }; - } - if (typeof result === "number" || typeof result === "boolean") { - return { val: result.toString() }; - } - if (typeof result === "object") { - return { val: JSON.stringify(result) }; - } - - return { err: "Invalid returned value." }; - }), - ); - } -} - -function displayErrors(errors: { err?: string }[]) { - let message = ""; - let errorCount = 0; - - for (const error of errors) { - if (error.err !== undefined && error.err.length > 0) { - message += `- "${error.err}".`; - errorCount++; - } - } - - if (errorCount === 0) { - return false; - } - if (errorCount === 1) { - message = `Error running shell command: ${message.substr(2)}`; - } else { - message = `Errors running shell command:\n${message}`; - } - - vscode.window.showErrorMessage(message); - - return true; -} - -const getInputBoxOptions = (expectReplacement: boolean) => - ({ - validateInput(input) { - if (input.trim().length === 0) { - return "The given command cannot be empty."; - } - - if (input[0] === "/") { - const result = parseRegExp(input); - - if (typeof result === "string") { - return result; - } - if (expectReplacement && result[1] === undefined) { - return "Missing replacement part in RegExp."; - } - - return; - } - - if (input[0] === "#") { - if (input.substr(1).trim().length === 0) { - return "The given shell command cannot be empty."; - } - - return; - } - - try { - new Function("$", "$$", "i", "return " + input); - } catch { - return "Invalid expression."; - } - - return undefined; - }, - - prompt: "Enter an expression", - } as vscode.InputBoxOptions); - -const inputBoxOptions = getInputBoxOptions(false); -const inputBoxOptionsWithReplacement = getInputBoxOptions(true); -const getInputBoxOptionsWithoutReplacement = () => inputBoxOptions; -const getInputBoxOptionsWithReplacement = () => inputBoxOptionsWithReplacement; - -function pipeInput(input: string, editor: vscode.TextEditor) { - return pipe(input, editor.selections.map(editor.document.getText)) as Thenable< - { val?: string; err?: string }[] - >; -} - -registerCommand( - Command.pipeFilter, - CommandFlags.ChangeSelections, - InputKind.Text, - getInputBoxOptionsWithoutReplacement, - async ({ editor }, state) => { - const outputs = await pipeInput(state.input, editor); - - displayErrors(outputs); - - const selections = [] as vscode.Selection[]; - - for (let i = 0; i < outputs.length; i++) { - const output = outputs[i]; - - if (!output.err && output.val !== "false") { - selections.push(editor.selections[i]); - } - } - - editor.selections = selections; - }, -); - -registerCommand( - Command.pipeIgnore, - CommandFlags.None, - InputKind.Text, - getInputBoxOptionsWithoutReplacement, - async ({ editor }, state) => { - const outputs = await pipeInput(state.input, editor); - - displayErrors(outputs); - }, -); - -registerCommand( - Command.pipeReplace, - CommandFlags.Edit, - InputKind.Text, - getInputBoxOptionsWithReplacement, - async ({ editor }, state, undoStops) => { - const outputs = await pipeInput(state.input, editor); - - if (displayErrors(outputs)) { - return; - } - - await editor.edit((builder) => { - const selections = editor.selections; - - for (let i = 0; i < outputs.length; i++) { - builder.replace(selections[i], outputs[i].val!); - } - }, undoStops); - }, -); - -registerCommand( - Command.pipeAppend, - CommandFlags.Edit, - InputKind.Text, - getInputBoxOptionsWithoutReplacement, - async (editorState, state, undoStops) => { - const { editor } = editorState; - const outputs = await pipeInput(state.input, editor); - - if (displayErrors(outputs)) { - return; - } - - const selections = editor.selections, - selectionLengths = [] as number[], - selectionHelper = SelectionHelper.for(editorState, state); - - await editor.edit((builder) => { - for (let i = 0; i < outputs.length; i++) { - const content = outputs[i].val!, - selection = selections[i]; - - builder.insert(selection.end, content); - - selectionLengths.push(selectionHelper.selectionLength(selection)); - } - }, undoStops); - - // Restore selections that were extended automatically. - for (let i = 0; i < outputs.length; i++) { - selections[i] = selectionHelper.selectionFromLength( - selections[i].anchor, - selectionLengths[i], - ); - } - - editor.selections = selections; - }, -); - -registerCommand( - Command.pipePrepend, - CommandFlags.Edit, - InputKind.Text, - getInputBoxOptionsWithoutReplacement, - async ({ editor }, state, undoStops) => { - const outputs = await pipeInput(state.input, editor); - - if (displayErrors(outputs)) { - return; - } - - await editor.edit((builder) => { - for (let i = 0; i < outputs.length; i++) { - builder.insert(editor.selections[i].start, outputs[i].val!); - } - }, undoStops); - }, -); diff --git a/src/commands/rotate.ts b/src/commands/rotate.ts deleted file mode 100644 index 72fe401..0000000 --- a/src/commands/rotate.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as vscode from "vscode"; - -import { Command, CommandFlags, registerCommand } from "."; - -function rotateSelections(editor: vscode.TextEditor) { - const selections = editor.selections.slice(), - last = selections.length - 1, - firstSelection = selections[0]; - - for (let i = 0; i < last; i++) { - selections[i] = selections[i + 1]; - } - - selections[last] = firstSelection; - editor.selections = selections; -} - -function rotateSelectionsBackwards(editor: vscode.TextEditor) { - const selections = editor.selections.slice(), - last = selections.length - 1, - lastSelection = selections[last]; - - for (let i = last; i > 0; i--) { - selections[i] = selections[i - 1]; - } - - selections[0] = lastSelection; - editor.selections = selections; -} - -function rotateSelectionsContent( - editor: vscode.TextEditor, - undoStops: { undoStopBefore: boolean; undoStopAfter: boolean }, -) { - return editor.edit((builder) => { - const { document: doc, selections } = editor; - - for (let i = 0; i < selections.length - 1; i++) { - builder.replace(selections[i + 1], doc.getText(selections[i])); - } - - builder.replace(selections[0], doc.getText(selections[selections.length - 1])); - }, undoStops); -} - -function rotateSelectionsContentBackwards( - editor: vscode.TextEditor, - undoStops: { undoStopBefore: boolean; undoStopAfter: boolean }, -) { - return editor.edit((builder) => { - const { document, selections } = editor; - - for (let i = 0; i < selections.length - 1; i++) { - builder.replace(selections[i], document.getText(selections[i + 1])); - } - - builder.replace(selections[selections.length - 1], document.getText(selections[0])); - }, undoStops); -} - -registerCommand(Command.rotate, CommandFlags.ChangeSelections, ({ editor }) => - rotateSelections(editor), -); - -registerCommand(Command.rotateBackwards, CommandFlags.ChangeSelections, ({ editor }) => - rotateSelectionsBackwards(editor), -); - -registerCommand(Command.rotateContentOnly, CommandFlags.Edit, ({ editor }, _, undoStops) => - rotateSelectionsContent(editor, undoStops).then(() => {}), -); - -registerCommand(Command.rotateContentOnlyBackwards, CommandFlags.Edit, ({ editor }, _, undoStops) => - rotateSelectionsContentBackwards(editor, undoStops).then(() => {}), -); - -registerCommand( - Command.rotateContent, - CommandFlags.ChangeSelections | CommandFlags.Edit, - ({ editor }, _, undoStops) => - rotateSelectionsContent(editor, undoStops).then(() => rotateSelections(editor)), -); - -registerCommand( - Command.rotateContentBackwards, - CommandFlags.ChangeSelections | CommandFlags.Edit, - ({ editor }, _, undoStops) => - rotateSelectionsContentBackwards(editor, undoStops).then(() => - rotateSelectionsBackwards(editor), - ), -); diff --git a/src/commands/search.ts b/src/commands/search.ts index d8b718a..f6f7f41 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -1,371 +1,255 @@ -// Search -// https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#searching import * as vscode from "vscode"; +import * as api from "../api"; -import { Command, CommandFlags, InputKind, registerCommand } from "."; -import { Extension, SelectionBehavior } from "../state/extension"; -import { WritableRegister } from "../registers"; -import { SavedSelection } from "../utils/savedSelection"; -import { - Backward, - Coord, - Direction, - DoNotExtend, - DocumentStart, - Extend, - ExtendBehavior, - Forward, - SelectionHelper, -} from "../utils/selectionHelper"; +import { Argument, Input, RegisterOr, SetInput } from "."; +import { Context, Direction, EmptySelectionsError, Positions, prompt, Selections } from "../api"; +import { Register } from "../state/registers"; +import { manipulateSelectionsInteractively } from "../utils/misc"; +import { escapeForRegExp } from "../utils/regexp"; import { CharSet, getCharSetFunction } from "../utils/charset"; -function isMultilineRegExp(regex: string) { - const len = regex.length; - let negate = false; +/** + * Search for patterns and replace or add selections. + */ +declare module "./search"; - for (let i = 0; i < len; i++) { - const ch = regex[i]; +let lastSearchInput: RegExp | undefined; - if (negate) { - if (ch === "]") { - negate = false; - } else if (ch === "\\") { - if (regex[i + 1] === "S") { - return true; - } +/** + * Search. + * + * @keys `/` (normal) + * + * | Title | Identifier | Keybinding | Command | + * | ------------------------ | ----------------- | -------------- | ------------------------------------------------- | + * | Search (extend) | `extend` | `?` (normal) | `[".search", { shift: "extend" }]` | + * | Search backward | `backward` | `a-/` (normal) | `[".search", { direction: -1 }]` | + * | Search backward (extend) | `backward.extend` | `a-?` (normal) | `[".search", { direction: -1, shift: "extend" }]` | + */ +export function search( + _: Context, + register: RegisterOr<"slash", Register.Flags.CanWrite>, + repetitions: number, - i++; // Ignore next character - } else { - continue; - } - } else if (ch === "[" && regex[i + 1] === "^") { - negate = true; - i++; - } else if (ch === "\\") { - if (regex[i + 1] === "s" || regex[i + 1] === "n") { - return true; - } + add: Argument = false, + direction: Direction = Direction.Forward, + interactive: Argument = true, + shift: api.Shift = api.Shift.Jump, - i++; // Ignore next character - } else if (ch === "$" && i < len - 1) { - return true; - } - } - - return false; -} - -function execFromEnd(regex: RegExp, input: string) { - let match = regex.exec(input); - - if (match === null || match[0].length === 0) { - return null; - } - - for (;;) { - const newMatch = regex.exec(input); - - if (newMatch === null) { - return match; - } - if (newMatch[0].length === 0) { - return null; + input: Input, + setInput: SetInput, +) { + return manipulateSelectionsInteractively(_, input, setInput, interactive, { + ...prompt.regexpOpts("mug"), + value: lastSearchInput?.source, + }, (input, selections) => { + if (typeof input === "string") { + input = new RegExp(input, "mug"); } - match = newMatch; - } -} + lastSearchInput = input; + register.set([]); -function documentEnd(document: vscode.TextDocument) { - const lastLine = document.lineCount - 1; - return new vscode.Position(lastLine, document.lineAt(lastLine).text.length); -} + const newSelections = add ? selections.slice() : [], + regexpMatches = [] as RegExpMatchArray[]; -function getSearchRange( - selection: vscode.Selection, - document: vscode.TextDocument, - direction: Direction, - isWrapped: boolean, -): vscode.Range { - if (isWrapped) { - return new vscode.Range(DocumentStart, documentEnd(document)); - } else if (direction === Forward) { - return new vscode.Range(selection.end, documentEnd(document)); - } else { - return new vscode.Range(DocumentStart, selection.start); - } -} + newSelections.push(...Selections.map.byIndex((_i, selection, document) => { + let newSelection = selection; -interface SearchState { - selectionBehavior: SelectionBehavior; - regex?: RegExp; -} + for (let j = 0; j < repetitions; j++) { + const searchResult = nextImpl( + input as RegExp, direction, newSelection, undefined, undefined, document, + /* allowWrapping= */ shift !== api.Shift.Extend, regexpMatches, regexpMatches.length); -function needleInHaystack( - direction: Direction, - allowWrapping: boolean, -): ( - selection: vscode.Selection, - helper: SelectionHelper, -) => [Coord, Coord] | undefined { - return (selection, helper) => { - const document = helper.editor.document; - const regex = helper.state.regex!; - // Try finding in the normal search range first, then the wrapped search - // range. - for (const isWrapped of [false, true]) { - const searchRange = getSearchRange(selection, document, direction, isWrapped); - const text = document.getText(searchRange); - regex.lastIndex = 0; - const match = direction === Forward ? regex.exec(text) : execFromEnd(regex, text); - if (match) { - const startOffset = helper.offsetAt(searchRange.start) + match.index; - const firstCharacter = helper.coordAt(startOffset); - const lastCharacter = helper.coordAt(startOffset + match[0].length - 1); - return [firstCharacter, lastCharacter]; - } - if (!allowWrapping) { - break; - } - } - return undefined; - }; -} - -function moveToNeedleInHaystack( - direction: Direction, - extend: ExtendBehavior, -): ( - selection: vscode.Selection, - helper: SelectionHelper, -) => vscode.Selection | undefined { - const find = needleInHaystack(direction, !extend); - return (selection, helper) => { - const result = find(selection, helper); - if (result === undefined) { - return undefined; - } - const [start, end] = result; - if (extend) { - return helper.extend(selection, direction === Forward ? end : start); - } else { - // When not extending, the result selection should always face forward, - // regardless of old selection or search direction. - return helper.selectionBetween(start, end); - } - }; -} - -function registerSearchCommand(command: Command, direction: Direction, extend: ExtendBehavior) { - let initialSelections: readonly SavedSelection[], - register: WritableRegister, - helper: SelectionHelper; - - const mapper = moveToNeedleInHaystack(direction, extend); - - registerCommand( - command, - CommandFlags.ChangeSelections, - InputKind.Text, - () => ({ - prompt: "Search RegExp", - - setup(editorState) { - const { documentState, editor, extension } = editorState; - helper = SelectionHelper.for(editorState, { - selectionBehavior: extension.selectionBehavior, - }); - initialSelections = editor.selections.map((selection) => - documentState.saveSelection(selection), - ); - - const targetRegister = extension.currentRegister; - - if (targetRegister === undefined || !targetRegister.canWrite()) { - register = extension.registers.slash; - } else { - register = targetRegister; - } - }, - - validateInput(input: string) { - if (input.length === 0) { - return "RegExp cannot be empty"; - } - - const editor = helper.editor; - const selections = initialSelections.map((selection) => - selection.selection(editor.document), - ); - - let regex: RegExp; - const flags = (isMultilineRegExp(input) ? "m" : "") + (direction === Backward ? "g" : ""); - - try { - regex = new RegExp(input, flags); - } catch { - editor.selections = selections; - return "invalid ECMA RegExp"; - } - helper.state.regex = regex; - - const newSelections = []; - const len = selections.length; - const repetitions = helper.editorState.extension.currentCount || 1; - for (let i = 0; i < len; i++) { - let newSelection: vscode.Selection | undefined = selections[i]; - for (let r = 0; r < repetitions; r++) { - newSelection = mapper(newSelection, helper); - if (!newSelection) { - break; - } - } - if (newSelection) { - newSelections.push(newSelection); - } - } - if (newSelections.length === 0) { - editor.selections = selections; - editor.revealRange(editor.selection); - return "No matches found"; - } else { - editor.selections = newSelections; - editor.revealRange(editor.selection); + if (searchResult === undefined) { return undefined; } - }, - onDidCancel({ editor, documentState }) { - editor.selections = initialSelections.map((selection) => - selection.selection(editor.document), - ); - documentState.forgetSelections(initialSelections); - }, - }), - ({ editor, documentState }) => { - register.set(editor, [helper.state.regex!.source]); - documentState.forgetSelections(initialSelections); - }, - ); -} -registerSearchCommand(Command.search, Forward, DoNotExtend); -registerSearchCommand(Command.searchBackwards, Backward, DoNotExtend); -registerSearchCommand(Command.searchExtend, Forward, Extend); -registerSearchCommand(Command.searchBackwardsExtend, Backward, Extend); - -function setSearchSelection(source: string, editor: vscode.TextEditor, state: Extension) { - try { - new RegExp(source, "g"); - } catch { - return Promise.reject(new Error( - "this should not happen -- please report this error along with the faulty RegExp")); - } - - const register = state.currentRegister; - - if (register === undefined || !register.canWrite()) { - state.registers.slash.set(editor, [source]); - } else { - register.set(editor, [source]); - } - - return Promise.resolve(); -} - -function escapeRegExp(str: string) { - // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -registerCommand(Command.searchSelection, CommandFlags.ChangeSelections, ({ editor, extension }) => { - const text = escapeRegExp(editor.document.getText(editor.selection)); - - return setSearchSelection(text, editor, extension); -}); - -registerCommand( - Command.searchSelectionSmart, - CommandFlags.ChangeSelections, - ({ editor, extension }) => { - const isWord = getCharSetFunction(CharSet.Word, editor.document), - firstLine = editor.document.lineAt(editor.selection.start).text, - firstLineStart = editor.selection.start.character; - - let text = escapeRegExp(editor.document.getText(editor.selection)); - - if (firstLineStart === 0 || !isWord(firstLine.charCodeAt(firstLineStart - 1))) { - text = `\\b${text}`; - } - - const lastLine = editor.document.lineAt(editor.selection.end).text, - lastLineEnd = editor.selection.end.character; - - if (lastLineEnd >= lastLine.length || !isWord(lastLine.charCodeAt(lastLineEnd))) { - text = `${text}\\b`; - } - - return setSearchSelection(text, editor, extension); - }, -); - -function nextNeedleInHaystack( - direction: Direction, -): ( - selection: vscode.Selection, - helper: SelectionHelper, -) => vscode.Selection | undefined { - const find = needleInHaystack(direction, /* allowWrapping = */ true); - return (selection, helper) => { - const result = find(selection, helper); - if (result === undefined) { - return undefined; - } - const [start, end] = result; - // The result selection should always face forward, - // regardless of old selection or search direction. - return helper.selectionBetween(start, end); - }; -} - -function registerNextCommand(command: Command, direction: Direction, replace: boolean) { - const mapper = nextNeedleInHaystack(direction); - registerCommand( - command, - CommandFlags.ChangeSelections, - async (editorState, { currentRegister, selectionBehavior, repetitions }) => { - const { editor, extension } = editorState; - const regexStr = await (currentRegister ?? extension.registers.slash).get(editor); - - if (regexStr === undefined || regexStr.length === 0) { - return; + newSelection = searchResult; } - const regex = new RegExp(regexStr[0], "g"), - selections = editor.selections; - const searchState = { selectionBehavior, regex }; - const helper = SelectionHelper.for(editorState, searchState); - let cur = selections[0]; + if (shift === api.Shift.Jump) { + return newSelection; + } + + const position = direction === Direction.Forward ? newSelection.end : newSelection.start; + + return Selections.shift(selection, position, shift, _); + }, selections)); + + Selections.set(newSelections); + _.extension.registers.updateRegExpMatches(regexpMatches); + + return register.set([input.source]).then(() => input as RegExp); + }); +} + +/** + * Search current selection. + * + * @keys `a-*` (normal) + * + * | Title | Identifier | Keybinding | Command | + * | -------------------------------- | ----------------- | ------------ | ---------------------------------------- | + * | Search current selection (smart) | `selection.smart` | `*` (normal) | `[".search.selection", { smart: true }]` | + */ +export function selection( + document: vscode.TextDocument, + selections: readonly vscode.Selection[], + + register: RegisterOr<"slash", Register.Flags.CanWrite>, + smart: Argument = false, +) { + const texts = [] as string[], + isWord = smart ? getCharSetFunction(CharSet.Word, document) : undefined; + + for (const selection of selections) { + let text = escapeForRegExp(document.getText(selection)); + + if (smart) { + const firstLine = document.lineAt(selection.start).text, + firstLineStart = selection.start.character; + + if (firstLineStart === 0 || !isWord!(firstLine.charCodeAt(firstLineStart - 1))) { + text = `\\b${text}`; + } + + const lastLine = selection.isSingleLine ? firstLine : document.lineAt(selection.end).text, + lastLineEnd = selection.end.character; + + if (lastLineEnd >= lastLine.length || !isWord!(lastLine.charCodeAt(lastLineEnd))) { + text = `${text}\\b`; + } + } + + texts.push(text); + } + + register.set(texts); +} + +/** + * Select next match. + * + * @keys `n` (normal) + * + * | Title | Identifier | Keybinding | Command | + * | --------------------- | -------------- | ---------------- | ------------------------------------------------ | + * | Add next match | `next.add` | `s-n` (normal) | `[".search.next", { add: true }]` | + * | Select previous match | `previous` | `a-n` (normal) | `[".search.next", { direction: -1 }]` | + * | Add previous match | `previous.add` | `s-a-n` (normal) | `[".search.next", { direction: -1, add: true }]` | + */ +export async function next( + _: Context, + document: vscode.TextDocument, + register: RegisterOr<"slash", Register.Flags.CanRead>, + repetitions: number, + + add: Argument = false, + direction: Direction = Direction.Forward, +) { + const reStrs = await register.get(); + + if (reStrs === undefined || reStrs.length === 0) { + return; + } + + const re = new RegExp(reStrs[0], "mu"), + allRegexpMatches = [] as RegExpMatchArray[]; + + if (!add) { + Selections.update.byIndex((_i, selection) => { + for (let j = 0; j < repetitions; j++) { + const next = nextImpl( + re, direction, selection, undefined, undefined, document, /* allowWrapping= */ true, + allRegexpMatches, allRegexpMatches.length); - for (let i = repetitions; i > 0; i--) { - const next = mapper(cur, helper); if (next === undefined) { - throw new Error("no matches found"); + return undefined; } - cur = next; - if (replace) { - selections[0] = cur; - } else { - selections.unshift(cur); - } + selection = next; } - editor.selections = selections; - }, - ); + return selection; + }); + + _.extension.registers.updateRegExpMatches(allRegexpMatches); + return; + } + + const selections = _.selections.slice(), + allSelections = selections.slice(); + + for (let i = 0; i < repetitions; i++) { + const newSelections = [] as vscode.Selection[], + regexpMatches = [] as RegExpMatchArray[]; + + for (let j = 0; j < selections.length; j++) { + const selection = selections[j], + next = nextImpl( + re, direction, selection, undefined, undefined, document, /* allowWrapping= */ true, + regexpMatches, regexpMatches.length); + + if (next !== undefined) { + selections[j] = next; + newSelections.push(next); + } + } + + if (newSelections.length === 0) { + const target = direction === Direction.Backward ? "previous" : "next", + times = repetitions === 1 ? "time" : "times"; + + throw new EmptySelectionsError( + `no selection could advance to ${target} match ${repetitions} ${times}`, + ); + } + + allSelections.unshift(...newSelections); + allRegexpMatches.unshift(...regexpMatches); + } + + Selections.set(allSelections); + _.extension.registers.updateRegExpMatches(allRegexpMatches); } -registerNextCommand(Command.searchNext, Forward, true); -registerNextCommand(Command.searchNextAdd, Forward, false); -registerNextCommand(Command.searchPrevious, Backward, true); -registerNextCommand(Command.searchPreviousAdd, Backward, false); +function nextImpl( + re: RegExp, + direction: Direction, + selection: vscode.Selection, + searchStart: vscode.Position | undefined, + searchEnd: vscode.Position | undefined, + document: vscode.TextDocument, + allowWrapping: boolean, + matches: RegExpMatchArray[] | undefined, + matchesIndex: number, +): vscode.Selection | undefined { + searchStart ??= direction === Direction.Backward ? selection.start : selection.end; + + const searchResult = api.search(direction, re, searchStart, searchEnd); + + if (searchResult === undefined) { + if (allowWrapping) { + if (direction === Direction.Backward) { + searchStart = Positions.last(document); + searchEnd = Positions.zero; + } else { + searchStart = Positions.zero; + searchEnd = Positions.last(document); + } + + return nextImpl( + re, direction, selection, searchStart, searchEnd, document, false, matches, matchesIndex); + } + + return; + } + + if (matches !== undefined) { + matches[matchesIndex] = searchResult[1]; + } + + return Selections.fromLength( + searchResult[0], searchResult[1][0].length, /* isReversed= */ false, document); +} diff --git a/src/commands/seek.ts b/src/commands/seek.ts new file mode 100644 index 0000000..fbfe293 --- /dev/null +++ b/src/commands/seek.ts @@ -0,0 +1,462 @@ +import * as vscode from "vscode"; + +import { Argument, InputOr } from "."; +import { ArgumentError, assert, Context, Direction, keypress, Lines, moveTo, moveWhile, Pair, pair, Positions, prompt, Selections, Shift, surroundedBy, todo } from "../api"; +import { Range } from "../api/search/range"; +import { wordBoundary } from "../api/search/word"; +import { SelectionBehavior } from "../state/modes"; +import { CharSet } from "../utils/charset"; +import { execRange } from "../utils/regexp"; + +/** + * Update selections based on the text surrounding them. + */ +declare module "./seek"; + +/** + * Select to character (excluded). + * + * @keys `t` (normal) + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | ---------------------------------------- | -------------------------- | ---------------- | -------------------------------------------------------------- | + * | Extend to character (excluded) | `extend` | `s-t` (normal) | `[".seek", { shift: "extend" }]` | + * | Select to character (excluded, backward) | `backward` | `a-t` (normal) | `[".seek", { direction: -1 }]` | + * | Extend to character (excluded, backward) | `extend.backward` | `s-a-t` (normal) | `[".seek", { shift: "extend", direction: -1 }]` | + * | Select to character (included) | `included` | `f` (normal) | `[".seek", { include: true }]` | + * | Extend to character (included) | `included.extend` | `s-f` (normal) | `[".seek", { include: true, shift: "extend" }]` | + * | Select to character (included, backward) | `included.backward` | `a-f` (normal) | `[".seek", { include: true, direction: -1 }]` | + * | Extend to character (included, backward) | `included.extend.backward` | `s-a-f` (normal) | `[".seek", { include: true, shift: "extend", direction: -1 }]` | + */ +export async function seek( + _: Context, + inputOr: InputOr, + + repetitions: number, + direction = Direction.Forward, + shift = Shift.Select, + include: Argument = false, +) { + const input = await inputOr(() => keypress(_)); + + Selections.update.byIndex((_, selection, document) => { + let position: vscode.Position | undefined = Selections.seekFrom(selection, -direction); + + for (let i = 0; i < repetitions; i++) { + position = Positions.offset(position, direction, document); + + if (position === undefined) { + return undefined; + } + + position = moveTo.excluded(direction, input, position, document); + + if (position === undefined) { + return undefined; + } + } + + if (include && !(shift === Shift.Extend && direction === Direction.Backward && position.isAfter(selection.anchor))) { + position = Positions.offset(position, input.length * direction); + + if (position === undefined) { + return undefined; + } + } + + return Selections.shift(selection, position, shift); + }); +} + +const defaultEnclosingPatterns = [ + "\\[", "\\]", + "\\(", "\\)", + "\\{", "\\}", + "/\\*", "\\*/", + "\\bbegin\\b", "\\bend\\b", +]; + +/** + * Select to next enclosing character. + * + * @keys `m` (normal) + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | -------------------------------------- | --------------------------- | ---------------- | --------------------------------------------------------- | + * | Extend to next enclosing character | `enclosing.extend` | `s-m` (normal) | `[".seek.enclosing", { shift: "extend" }]` | + * | Select to previous enclosing character | `enclosing.backward` | `a-m` (normal) | `[".seek.enclosing", { direction: -1 }]` | + * | Extend to previous enclosing character | `enclosing.extend.backward` | `s-a-m` (normal) | `[".seek.enclosing", { shift: "extend", direction: -1 }]` | + */ +export function enclosing( + _: Context, + + direction = Direction.Forward, + shift = Shift.Select, + open: Argument = true, + pairs: Argument = defaultEnclosingPatterns, +) { + ArgumentError.validate( + "pairs", + (pairs.length & 1) === 0, + "an even number of pairs must be given", + ); + + const selectionBehavior = _.selectionBehavior, + compiledPairs = [] as Pair[]; + + for (let i = 0; i < pairs.length; i += 2) { + compiledPairs.push(pair(new RegExp(pairs[i], "mu"), new RegExp(pairs[i + 1], "mu"))); + } + + // This command intentionally ignores repetitions to be consistent with + // Kakoune. + // It only finds one next enclosing character and drags only once to its + // matching counterpart. Repetitions > 1 does exactly the same with rep=1, + // even though executing the command again will jump back and forth. + Selections.update.byIndex((_, selection, document) => { + // First, find an enclosing char (which may be the current character). + let currentCharacter = selection.active; + + if (selectionBehavior === SelectionBehavior.Caret) { + if (direction === Direction.Backward && selection.isReversed) { + // When moving backwards, the first character to consider is the + // character to the left, not the right. However, we hackily special + // case `|[foo]>` (> is anchor, | is active) to jump to the end in the + // current group. + currentCharacter = Positions.previous(currentCharacter, document) ?? currentCharacter; + } else if (direction === Direction.Forward && !selection.isReversed && !selection.isEmpty) { + // Similarly, we special case `<[foo]|` to jump back in the current + // group. + currentCharacter = Positions.previous(currentCharacter, document) ?? currentCharacter; + } + } + + if (selectionBehavior === SelectionBehavior.Caret && direction === Direction.Backward) { + // When moving backwards, the first character to consider is the + // character to the left, not the right. + currentCharacter = Positions.previous(currentCharacter, document) ?? currentCharacter; + } + + const enclosedRange = surroundedBy(compiledPairs, direction, currentCharacter, open, document); + + if (enclosedRange === undefined) { + return undefined; + } + + if (shift === Shift.Extend) { + return new vscode.Selection(selection.anchor, enclosedRange.active); + } + + return enclosedRange; + }); +} + +/** + * Select to next word start. + * + * Select the word and following whitespaces on the right of the end of each selection. + * + * @keys `w` (normal) + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | -------------------------------------------- | ------------------------- | ---------------- | -------------------------------------------------------------------------------- | + * | Extend to next word start | `word.extend` | `s-w` (normal) | `[".seek.word", { shift: "extend" }]` | + * | Select to previous word start | `word.backward` | `b` (normal) | `[".seek.word", { direction: -1 }]` | + * | Extend to previous word start | `word.extend.backward` | `s-b` (normal) | `[".seek.word", { shift: "extend", direction: -1 }]` | + * | Select to next non-whitespace word start | `word.ws` | `a-w` (normal) | `[".seek.word", { ws: true }]` | + * | Extend to next non-whitespace word start | `word.ws.extend` | `s-a-w` (normal) | `[".seek.word", { ws: true, shift: "extend" }]` | + * | Select to previous non-whitespace word start | `word.ws.backward` | `a-b` (normal) | `[".seek.word", { ws: true, direction: -1 }]` | + * | Extend to previous non-whitespace word start | `word.ws.extend.backward` | `s-a-b` (normal) | `[".seek.word", { ws: true, shift: "extend", direction: -1 }]` | + * | Select to next word end | `wordEnd` | `e` (normal) | `[".seek.word", { stopAtEnd: true }]` | + * | Extend to next word end | `wordEnd.extend` | `s-e` (normal) | `[".seek.word", { stopAtEnd: true , shift: "extend" }]` | + * | Select to next non-whitespace word end | `wordEnd.ws` | `a-e` (normal) | `[".seek.word", { stopAtEnd: true , ws: true }]` | + * | Extend to next non-whitespace word end | `wordEnd.ws.extend` | `s-a-e` (normal) | `[".seek.word", { stopAtEnd: true , ws: true, shift: "extend" }]` | + */ +export function word( + _: Context, + + repetitions: number, + stopAtEnd: Argument = false, + ws: Argument = false, + direction = Direction.Forward, + shift = Shift.Select, +) { + const charset = ws ? CharSet.NonBlank : CharSet.Word; + + Selections.update.withFallback.byIndex((_i, selection) => { + const anchor = Selections.seekFrom(selection, direction, selection.anchor, _); + let active = Selections.seekFrom(selection, direction, selection.active, _); + + for (let i = 0; i < repetitions; i++) { + const mapped = wordBoundary(direction, active, stopAtEnd, charset, _); + + if (mapped === undefined) { + if (direction === Direction.Backward && active.line > 0) { + // This is a special case in Kakoune and we try to mimic it + // here. + // Instead of overflowing, put anchor at document start and + // active always on the first character on the second line. + const end = _.selectionBehavior === SelectionBehavior.Caret + ? Positions.lineStart(1) + : (Lines.isEmpty(1) ? Positions.lineStart(2) : Positions.at(1, 1)); + + return new vscode.Selection(Positions.lineStart(0), end); + } + + if (shift === Shift.Extend) { + return [new vscode.Selection(anchor, selection.active)]; + } + + return [selection]; + } + + selection = mapped; + active = selection.active; + } + + if (shift === Shift.Extend) { + return new vscode.Selection(anchor, selection.active); + } + + return selection; + }); +} + +let lastObjectInput: string | undefined; + +/** + * Select object. + * + * @param input The pattern of object to select; see + * [object patterns](#object-patterns) below for more information. + * @param inner If `true`, only the "inner" part of the object will be selected. + * The definition of the "inner" part depends on the object. + * @param where What end of the object should be sought. If `undefined`, the + * object will be selected from start to end regardless of the `shift`. + * + * #### Object patterns + * - Pairs: `(?#inner)`. + * - Character sets: `[]+`. + * - Can be preceded by `(?[]+)` and followed by + * `(?[]+)` for whole objects. + * - Matches that may only span a single line: `(?#singleline)`. + * - Predefined: `(?#predefined=)`. + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | ---------------------------- | ------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------- | + * | Select whole object | `askObject` | `a-a` (normal), `a-a` (insert) | `[".openMenu", { input: "object" }]` | + * | Select inner object | `askObject.inner` | `a-i` (normal), `a-i` (insert) | `[".openMenu", { input: "object", pass: [{ inner: true }] }]` | + * | Select to whole object start | `askObject.start` | `[` (normal) | `[".openMenu", { input: "object", pass: [{ where: "start" }] }]` | + * | Extend to whole object start | `askObject.start` | `{` (normal) | `[".openMenu", { input: "object", pass: [{ where: "start", shift: "extend" }] }]` | + * | Select to inner object start | `askObject.inner.start` | `a-[` (normal) | `[".openMenu", { input: "object", pass: [{ inner: true, where: "start" }] }]` | + * | Extend to inner object start | `askObject.inner.start.extend` | `a-{` (normal) | `[".openMenu", { input: "object", pass: [{ inner: true, where: "start", shift: "extend" }] }]` | + * | Select to whole object end | `askObject.end` | `]` (normal) | `[".openMenu", { input: "object", pass: [{ where: "end" }] }]` | + * | Extend to whole object end | `askObject.end` | `}` (normal) | `[".openMenu", { input: "object", pass: [{ where: "end" , shift: "extend" }] }]` | + * | Select to inner object end | `askObject.inner.end` | `a-]` (normal) | `[".openMenu", { input: "object", pass: [{ inner: true, where: "end" }] }]` | + * | Extend to inner object end | `askObject.inner.end.extend` | `a-}` (normal) | `[".openMenu", { input: "object", pass: [{ inner: true, where: "end" , shift: "extend" }] }]` | + */ +export async function object( + _: Context, + + inputOr: InputOr, + inner: Argument = false, + where?: Argument<"start" | "end">, + shift = Shift.Select, +) { + const input = await inputOr(() => prompt({ + prompt: "Object description", + value: lastObjectInput, + })); + + let match: RegExpExecArray | null; + + if (match = /^(.+)\(\?#inner\)(.+)$/s.exec(input)) { + const openRe = new RegExp(preprocessRegExp(match[1]), "u"), + closeRe = new RegExp(preprocessRegExp(match[2]), "u"), + p = pair(openRe, closeRe); + + if (where === "start") { + return Selections.update.byIndex((_i, selection) => { + const startResult = p.searchOpening(Selections.activeStart(selection, _)); + + if (startResult === undefined) { + return undefined; + } + + const start = inner + ? Positions.offset(startResult[0], startResult[1][0].length, _.document) ?? startResult[0] + : startResult[0]; + + return Selections.shift(selection, start, shift, _); + }); + } + + if (where === "end") { + return Selections.update.byIndex((_i, selection) => { + const endResult = p.searchClosing(Selections.activeEnd(selection, _)); + + if (endResult === undefined) { + return undefined; + } + + const end = inner + ? endResult[0] + : Positions.offset(endResult[0], endResult[1][0].length, _.document) ?? endResult[0]; + + return Selections.shift(selection, end, shift, _); + }); + } + + return shiftWhere( + _, + (selection, _) => surroundedBy( + [p], Direction.Backward, Selections.activeEnd(selection, _), !inner, _.document), + shift, + where, + ); + } + + if (match = + /^(?:\(\?(\[.+?\])\+\))?(\[.+\])\+(?:\(\?(\[.+?\])\+\))?$/.exec(input)) { + const re = new RegExp(match[2], "u"), + beforeRe = inner || match[1] === undefined ? undefined : new RegExp(match[1], "u"), + afterRe = inner || match[3] === undefined ? undefined : new RegExp(match[3], "u"); + + return shiftWhere( + _, + (selection, _) => { + let start = moveWhile.backward((c) => re.test(c), selection.active, _.document), + end = moveWhile.forward((c) => re.test(c), selection.active, _.document); + + if (beforeRe !== undefined) { + start = moveWhile.backward((c) => beforeRe.test(c), start, _.document); + } + + if (afterRe !== undefined) { + end = moveWhile.forward((c) => afterRe.test(c), end, _.document); + } + + return new vscode.Selection(start, end); + }, + shift, + where, + ); + } + + if (match = /^\(\?#singleline\)(.+)$/.exec(input)) { + const re = new RegExp(preprocessRegExp(match[1]), "u"); + + return shiftWhere( + _, + (selection, _) => { + const line = Selections.activeLine(selection), + lineText = _.document.lineAt(line).text, + matches = execRange(lineText, re); + + // Find match at text position. + const character = Selections.activeCharacter(selection, _.document); + + for (const m of matches) { + let [start, end] = m; + + if (start <= character && character <= end) { + if (inner && m[2].groups !== undefined) { + const match = m[2]; + + if ("before" in match.groups!) { + start += match.groups.before.length; + } + if ("after" in match.groups!) { + end -= match.groups.after.length; + } + } + + return new vscode.Selection( + new vscode.Position(line, start), + new vscode.Position(line, end), + ); + } + } + + return undefined; + }, + shift, + where, + ); + } + + if (match = /^\(\?#predefined=(argument|indent|paragraph|sentence)\)$/.exec(input)) { + let f: Range.Seek; + + switch (match[1]) { + case "argument": + case "indent": + case "paragraph": + case "sentence": + f = Range[match[1]]; + break; + + default: + assert(false); + } + + if (where === "start") { + Selections.update.byIndex((_i, selection, document) => + Selections.shift( + selection, + f.start(Selections.activePosition(selection, _.document), inner, document), + shift, + _, + ), + ); + } else if (where === "end") { + Selections.update.byIndex((_i, selection, document) => + Selections.shift( + selection, + f.end(selection.active, inner, document), + shift, + _, + ), + ); + } else { + Selections.update.byIndex((_, selection, document) => f(selection.active, inner, document)); + } + + return; + } + + throw new Error("unknown object " + JSON.stringify(input)); +} + +function preprocessRegExp(re: string) { + return re.replace(/\(\?#noescape\)/g, "(?<=(? vscode.Selection | undefined, + shift: Shift, + where: "start" | "end" | undefined, +) { + Selections.update.byIndex((_, selection) => { + const result = f(selection, context); + + if (result === undefined) { + return undefined; + } + + if (where === undefined) { + return result; + } + + return Selections.shift(selection, result[where], shift, context); + }); +} diff --git a/src/commands/select.ts b/src/commands/select.ts index 0d0b416..3c91083 100644 --- a/src/commands/select.ts +++ b/src/commands/select.ts @@ -1,713 +1,585 @@ -// Select / extend -// https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#movement import * as vscode from "vscode"; +import * as api from "../api"; -import { Command, CommandFlags, CommandState, InputKind, registerCommand } from "."; -import { EditorState } from "../state/editor"; -import { SelectionBehavior } from "../state/extension"; -import { CharSet, getCharSetFunction } from "../utils/charset"; -import { - Backward, - Coord, - CoordMapper, - Direction, - DoNotExtend, - Extend, - ExtendBehavior, - Forward, - RemoveSelection, - SelectionHelper, - SelectionMapper, - moveActiveCoord, - seekToRange, -} from "../utils/selectionHelper"; +import { Argument } from "."; +import { column, columns, Context, Direction, Lines, Positions, Selections, Shift, showMenu, todo } from "../api"; +import { SelectionBehavior } from "../state/modes"; +import { PerEditorState } from "../state/editors"; -// Move / extend to character (f, t, F, T, Alt+[ft], Alt+[FT]) -// =============================================================================================== +/** + * Update selections based on their position in the document. + */ +declare module "./select"; -function toNextCharacter(direction: Direction, include: boolean): CoordMapper { - return (from, helper) => { - const key = helper.state.input as string; - const active = from; - - let line = active.line; - let character: number | undefined = active.character; - - for (let i = helper.state.repetitions; i > 0; i--) { - for (;;) { - const text = helper.editor.document.lineAt(line).text; - if (character === undefined) { - character = text.length; - } - const idx: number - = direction === Backward - ? text.lastIndexOf(key, character - 1) - : text.indexOf(key, character + 1); - - if (idx !== -1) { - character = idx; - - break; - } - - // No match on this line, let's keep going. - const isDocumentEdge - = direction === Backward ? line-- === 0 : ++line === helper.editor.document.lineCount; - - if (isDocumentEdge) { - // ... except if we've reached the start or end of the document. - return RemoveSelection; - } - - character = direction === Backward ? undefined : 0; - } - } - if (!include) { - character += direction === Backward ? 1 : -1; - } - return new Coord(line, character); - }; +/** + * Select whole buffer. + * + * @keys `%` (normal) + */ +export function buffer(_: Context) { + Selections.set([Selections.wholeBuffer()]); } -function registerSelectTo( - commandName: Command, - include: boolean, - extend: ExtendBehavior, - direction: Direction, +interface PreferredColumnsState { + disposable: vscode.Disposable; + expectedSelections: readonly vscode.Selection[]; + preferredColumns: number[]; +} + +const preferredColumnsToken = + PerEditorState.registerState(/* isDisposable= */ false); + +/** + * Select vertically. + * + * @param avoidEol If `true`, selections will not select the line break + * character but will instead move to the last character. + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | ----------- | ------------- | --------------------------------- | ------------------------------------------------------------ | + * | Jump down | `down.jump` | `j` (normal) , `down` (normal) | `[".select.vertically", { direction: 1, shift: "jump" }]` | + * | Extend down | `down.extend` | `s-j` (normal), `s-down` (normal) | `[".select.vertically", { direction: 1, shift: "extend" }]` | + * | Jump up | `up.jump` | `k` (normal) , `up` (normal) | `[".select.vertically", { direction: -1, shift: "jump" }]` | + * | Extend up | `up.extend` | `s-k` (normal), `s-up` (normal) | `[".select.vertically", { direction: -1, shift: "extend" }]` | + * + * The following keybindings are also defined: + * + * | Keybinding | Command | + * | ------------------------------ | ----------------------------------------------------------- | + * | `c-f` (normal), `c-f` (insert) | `[".select.vertically", { direction: 1, by: "page" }]` | + * | `c-d` (normal), `c-d` (insert) | `[".select.vertically", { direction: 1, by: "halfPage" }]` | + * | `c-b` (normal), `c-b` (insert) | `[".select.vertically", { direction: -1, by: "page" }]` | + * | `c-u` (normal), `c-u` (insert) | `[".select.vertically", { direction: -1, by: "halfPage" }]` | + */ +export function vertically( + _: Context, + selections: readonly vscode.Selection[], + + avoidEol: Argument = false, + repetitions: number, + direction = Direction.Forward, + shift = Shift.Select, + by?: Argument<"page" | "halfPage">, ) { - const mapper = moveActiveCoord(toNextCharacter(direction, include), extend); - registerCommand( - commandName, - CommandFlags.ChangeSelections, - InputKind.Key, - () => void 0, - (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(mapper); - // TODO: Reveal - }, - ); -} + // Adjust repetitions if a `by` parameter is given. + if (by !== undefined) { + const visibleRange = _.editor.visibleRanges[0]; -registerSelectTo(Command.selectToIncluded, true, DoNotExtend, Forward); -registerSelectTo(Command.selectToIncludedExtend, true, Extend, Forward); -registerSelectTo(Command.selectToExcluded, false, DoNotExtend, Forward); -registerSelectTo(Command.selectToExcludedExtend, false, Extend, Forward); - -registerSelectTo(Command.selectToIncludedBackwards, true, DoNotExtend, Backward); -registerSelectTo(Command.selectToIncludedExtendBackwards, true, Extend, Backward); -registerSelectTo(Command.selectToExcludedBackwards, false, DoNotExtend, Backward); -registerSelectTo(Command.selectToExcludedExtendBackwards, false, Extend, Backward); - -// Move / extend to word begin / end (w, b, e, W, B, E, alt+[wbe], alt+[WBE]) -// =============================================================================================== - -function skipEmptyLines( - coord: Coord, - document: vscode.TextDocument, - direction: Direction, -): Coord | undefined { - let { line } = coord; - - line += direction; - while (line >= 0 && line < document.lineCount) { - const textLine = document.lineAt(line); - if (textLine.text.length > 0) { - const edge = direction === Backward ? textLine.text.length - 1 : 0; - return new Coord(line, edge); + if (by === "page") { + repetitions *= visibleRange.end.line - visibleRange.start.line; + } else if (by === "halfPage") { + repetitions *= ((visibleRange.end.line - visibleRange.start.line) / 2) | 0; } - line += direction; } - return undefined; -} -function categorize( - charCode: number, - isBlank: (charCode: number) => boolean, - isWord: (charCode: number) => boolean, -) { - return isWord(charCode) ? "word" : charCode === 0 || isBlank(charCode) ? "blank" : "punct"; -} + const document = _.document, + isCharacterMode = _.selectionBehavior === SelectionBehavior.Character; -function selectByWord( - editorState: EditorState, - state: CommandState, - extend: ExtendBehavior, - direction: Direction, - end: boolean, - wordCharset: CharSet, -) { - const helper = SelectionHelper.for(editorState, state); - const { repetitions } = state; - const document = editorState.editor.document; - const isWord = getCharSetFunction(wordCharset, document), - isBlank = getCharSetFunction(CharSet.Blank, document), - isPunctuation = getCharSetFunction(CharSet.Punctuation, document); + // TODO: test logic with tabs + const activeEnd = (selection: vscode.Selection) => { + const active = selection.active; - for (let i = repetitions; i > 0; i--) { - helper.mapEach( - seekToRange( - (from) => { - let anchor = undefined, - active = from; - const text = document.lineAt(active.line).text; - const lineEndCol = helper.selectionBehavior === SelectionBehavior.Caret - ? text.length - : text.length - 1; - // 1. Starting from active, try to seek to the word start. - const isAtLineBoundary = direction === Forward - ? (active.character >= lineEndCol) - : (active.character === 0); - if (isAtLineBoundary) { - const afterEmptyLines = skipEmptyLines(active, document, direction); - if (afterEmptyLines === undefined) { - if (direction === Backward && active.line > 0) { - // This is a special case in Kakoune and we try to mimic it - // here. - // Instead of overflowing, put anchor at document start and - // active always on the first character on the second line. - return [new Coord(0, 0), new Coord(1, 0)]; - } else { - // Otherwise the selection overflows. - return { remove: true, fallback: [anchor, active] }; - } - } - anchor = afterEmptyLines; - } else if (direction === Backward && active.character >= text.length) { - anchor = new Coord(active.line, text.length - 1); - } else { - let shouldSkip; - if (helper.selectionBehavior === SelectionBehavior.Character) { - // Skip current character if it is at boundary. - // (e.g. "ab[c] " =>`w`) - const column = active.character; - shouldSkip - = categorize(text.charCodeAt(column), isBlank, isWord) - !== categorize(text.charCodeAt(column + direction), isBlank, isWord); - } else { - // Ignore the character on the right of the caret. - shouldSkip = direction === Backward; - } - anchor = shouldSkip ? new Coord(active.line, active.character + direction) : active; - } + if (active === selection.end && Selections.endsWithEntireLine(selection)) { + return columns(active.line - 1, _.editor) + 1; + } else if (active === selection.start && isCharacterMode) { + return column(active.line, active.character, _.editor) + 1; + } - active = anchor; + return column(active.line, active.character, _.editor); + }; - // 2. Then scan within the current line until the word ends. + // Get or create the `PreferredColumnsState` for this editor. + const editorState = _.getState(); + let preferredColumnsState = editorState.get(preferredColumnsToken); - const curLineText = document.lineAt(active).text; - let nextCol = active.character; // The next character to be tested. - if (end) { - // Select the whitespace before word, if any. - while ( - nextCol >= 0 - && nextCol < curLineText.length - && isBlank(curLineText.charCodeAt(nextCol)) - ) { - nextCol += direction; - } - } - if (nextCol >= 0 && nextCol < curLineText.length) { - const startCharCode = curLineText.charCodeAt(nextCol); - const isSameCategory = isWord(startCharCode) ? isWord : isPunctuation; - while ( - nextCol >= 0 - && nextCol < curLineText.length - && isSameCategory(curLineText.charCodeAt(nextCol)) - ) { - nextCol += direction; - } - } - if (!end) { - // Select the whitespace after word, if any. - while ( - nextCol >= 0 - && nextCol < curLineText.length - && isBlank(curLineText.charCodeAt(nextCol)) - ) { - nextCol += direction; - } - } - // If we reach here, nextCol must be the first character we encounter - // that does not belong to the current word (or -1 / line break). - // Exclude it. - active = new Coord(active.line, nextCol - direction); - return [anchor!, active]; - }, - extend, - /* singleCharDirection = */ direction, - ), + if (preferredColumnsState === undefined) { + // That disposable will be automatically disposed of when the selections in + // the editor change due to an action outside of the current command. When + // it is disposed, it will clear the preferred columns for this editor. + const disposable = _.extension + .createAutoDisposable() + .disposeOnEvent(editorState.onEditorWasClosed) + .addDisposable(vscode.window.onDidChangeTextEditorSelection((e) => { + if (editorState.editor !== e.textEditor) { + return; + } + + const expectedSelections = preferredColumnsState!.expectedSelections; + + if (e.selections.length === expectedSelections.length + && e.selections.every((sel, i) => sel.isEqual(expectedSelections[i]))) { + return; + } + + editorState.store(preferredColumnsToken, undefined); + disposable.dispose(); + })); + + editorState.store( + preferredColumnsToken, + preferredColumnsState = { + disposable, + expectedSelections: [], + preferredColumns: selections.map((sel) => activeEnd(sel)), + }, ); } + + Selections.update.byIndex((i, selection) => { + // TODO: handle tab characters + const activeLine = Selections.activeLine(selection), + targetLine = Lines.clamp(activeLine + repetitions * direction, document), + targetLineLength = columns(targetLine, _.editor); + + if (targetLineLength === 0) { + let targetPosition = Positions.lineStart(targetLine); + + if (isCharacterMode) { + if (direction === Direction.Forward || shift === Shift.Jump) { + targetPosition = Positions.next(targetPosition, document) ?? targetPosition; + } + + if (direction === Direction.Backward && shift === Shift.Extend + && Selections.isSingleCharacter(selection, document)) { + selection = new vscode.Selection( + Positions.next(selection.anchor, document) ?? selection.anchor, selection.active); + } + } + + return Selections.shift(selection, targetPosition, shift); + } + + let targetColumn: number; + + const preferredColumns = preferredColumnsState!.preferredColumns, + preferredColumn = i < preferredColumns.length + ? preferredColumns[i] + : activeEnd(selection); + + if (preferredColumn <= targetLineLength) { + targetColumn = preferredColumn; + } else if (isCharacterMode && targetLine + 1 < document.lineCount && !avoidEol) { + return Selections.shift(selection, new vscode.Position(targetLine + 1, 0), shift); + } else { + targetColumn = targetLineLength; + } + + let newPosition = new vscode.Position( + targetLine, column.character(targetLine, targetColumn, _.editor)); + + if (isCharacterMode && shift !== Shift.Jump) { + const edge = shift === Shift.Extend ? selection.anchor : selection.active; + + if (newPosition.isBefore(edge)) { + // Selection is going up or down above the cursor: we must account for + // the translation to character mode. + newPosition = Positions.previous(newPosition, document) ?? newPosition; + } + } + + return Selections.shift(selection, newPosition, shift); + }); + + preferredColumnsState.expectedSelections = editorState.editor.selections; } -registerCommand(Command.selectWord, CommandFlags.ChangeSelections, (editorState, state) => - selectByWord(editorState, state, DoNotExtend, Forward, false, CharSet.Word), -); -registerCommand(Command.selectWordExtend, CommandFlags.ChangeSelections, (editorState, state) => - selectByWord(editorState, state, Extend, Forward, false, CharSet.Word), -); -registerCommand(Command.selectWordAlt, CommandFlags.ChangeSelections, (editorState, state) => - selectByWord(editorState, state, DoNotExtend, Forward, false, CharSet.NonBlank), -); -registerCommand(Command.selectWordAltExtend, CommandFlags.ChangeSelections, (editorState, state) => - selectByWord(editorState, state, Extend, Forward, false, CharSet.NonBlank), -); -registerCommand(Command.selectWordEnd, CommandFlags.ChangeSelections, (editorState, state) => - selectByWord(editorState, state, DoNotExtend, Forward, true, CharSet.Word), -); -registerCommand(Command.selectWordEndExtend, CommandFlags.ChangeSelections, (editorState, state) => - selectByWord(editorState, state, Extend, Forward, true, CharSet.Word), -); -registerCommand(Command.selectWordAltEnd, CommandFlags.ChangeSelections, (editorState, state) => - selectByWord(editorState, state, DoNotExtend, Forward, true, CharSet.NonBlank), -); -registerCommand( - Command.selectWordAltEndExtend, - CommandFlags.ChangeSelections, - (editorState, state) => selectByWord(editorState, state, Extend, Forward, true, CharSet.NonBlank), -); -registerCommand(Command.selectWordPrevious, CommandFlags.ChangeSelections, (editorState, state) => - selectByWord(editorState, state, DoNotExtend, Backward, true, CharSet.Word), -); -registerCommand( - Command.selectWordPreviousExtend, - CommandFlags.ChangeSelections, - (editorState, state) => selectByWord(editorState, state, Extend, Backward, true, CharSet.Word), -); -registerCommand( - Command.selectWordAltPrevious, - CommandFlags.ChangeSelections, - (editorState, state) => - selectByWord(editorState, state, DoNotExtend, Backward, true, CharSet.NonBlank), -); -registerCommand( - Command.selectWordAltPreviousExtend, - CommandFlags.ChangeSelections, - (editorState, state) => - selectByWord(editorState, state, Extend, Backward, true, CharSet.NonBlank), -); +/** + * Select horizontally. + * + * @param avoidEol If `true`, selections will automatically skip to the next + * line instead of going after the last character. Does not skip empty lines. + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | ------------ | -------------- | ---------------------------------- | -------------------------------------------------------------- | + * | Jump right | `right.jump` | `l` (normal) , `right` (normal) | `[".select.horizontally", { direction: 1, shift: "jump" }]` | + * | Extend right | `right.extend` | `s-l` (normal), `s-right` (normal) | `[".select.horizontally", { direction: 1, shift: "extend" }]` | + * | Jump left | `left.jump` | `h` (normal) , `left` (normal) | `[".select.horizontally", { direction: -1, shift: "jump" }]` | + * | Extend left | `left.extend` | `s-h` (normal), `s-left` (normal) | `[".select.horizontally", { direction: -1, shift: "extend" }]` | + */ +export function horizontally( + _: Context, -// Line selecting key bindings (x, X, alt+[xX], home, end) -// =============================================================================================== + avoidEol: Argument = false, + repetitions: number, + direction = Direction.Forward, + shift = Shift.Select, +) { + const mayNeedAdjustment = direction === Direction.Backward + && _.selectionBehavior === SelectionBehavior.Character; -registerCommand( - Command.selectLine, - CommandFlags.ChangeSelections, - (editorState, { currentCount }) => { - const editor = editorState.editor, - selections = editor.selections, - len = selections.length, - selectionHelper = SelectionHelper.for(editorState); + Selections.update.byIndex((_i, selection, document) => { + let active = selection.active === selection.start + ? Selections.activeStart(selection, _) + : Selections.activeEnd(selection, _); - if (currentCount === 0 || currentCount === 1) { - for (let i = 0; i < len; i++) { - const selection = selections[i], - isFullLine = selectionHelper.isEntireLines(selection); - let line = selectionHelper.activeLine(selection); + if (mayNeedAdjustment) { + if (shift === Shift.Extend && Selections.isSingleCharacter(selection)) { + active = selection.start; + } else if (shift === Shift.Jump && selection.active === selection.start) { + active = Positions.next(active, _.document) ?? active; + } + } - if (isFullLine) { + let target = Positions.offset(active, direction * repetitions, document) ?? active; + + if (avoidEol) { + switch (_.selectionBehavior) { + case SelectionBehavior.Caret: + if (target.character === Lines.length(target.line, document) && target.character > 0) { + target = Positions.offset(target, direction, document) ?? target; + } + break; + + case SelectionBehavior.Character: + if (target.character === 0 + && (direction === Direction.Forward || target.line === 0 + || !Lines.isEmpty(target.line - 1, document))) { + target = Positions.offset(target, direction, document) ?? target; + } + break; + } + } + + return Selections.shift(selection, target, shift); + }); +} + +/** + * Select to. + * + * If a count is specified, this command will shift to the start of the given + * line. If no count is specified, this command will shift open the `goto` menu. + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | --------- | ----------- | -------------- | ------------------------------------- | + * | Go to | `to.jump` | `g` (normal) | `[".select.to", { shift: "jump" }]` | + * | Extend to | `to.extend` | `s-g` (normal) | `[".select.to", { shift: "extend" }]` | + */ +export function to( + _: Context, + count: number, + argument: object, + shift = Shift.Select, +) { + if (count === 0) { + // TODO: Make just merely opening the menu not count as a command execution + // and do not record it. + return showMenu.byName("goto", [argument]); + } + + return lineStart(_, count, shift); +} + +/** + * Select line below. + * + * @keys `x` (normal) + */ +export function line_below(_: Context, count: number) { + if (count === 0 || count === 1) { + Selections.update.byIndex((_, selection) => { + let line = Selections.activeLine(selection); + + if (Selections.isEntireLines(selection) && !selection.isReversed) { + line++; + } + + return new vscode.Selection(line, 0, line + 1, 0); + }); + } else { + Selections.update.byIndex((_, selection, document) => { + const lastLine = document.lineCount - 1; + let line = Math.min(Selections.activeLine(selection) + count - 1, lastLine); + + if (Selections.isEntireLines(selection) && line < lastLine) { + line++; + } + + return new vscode.Selection(line, 0, line + 1, 0); + }); + } +} + +/** + * Extend to line below. + * + * @keys `s-x` (normal) + */ +export function line_below_extend(_: Context, count: number) { + if (count === 0 || count === 1) { + Selections.update.byIndex((_, selection, document) => { + const isFullLine = Selections.isEntireLine(selection), + isSameLine = Selections.isSingleLine(selection), + isFullLineDiff = isFullLine && !(isSameLine && selection.isReversed) ? 1 : 0, + activeLine = Selections.activeLine(selection); + + const anchor = isSameLine ? Positions.lineStart(activeLine) : selection.anchor, + active = Positions.lineBreak(activeLine + isFullLineDiff, document); + + return new vscode.Selection(anchor, active); + }); + } else { + Selections.update.byIndex((_, selection, document) => { + const activeLine = Selections.activeLine(selection), + line = Math.min(activeLine + count - 1, document.lineCount - 1), + isSameLine = Selections.isSingleLine(selection); + + const anchor = isSameLine ? Positions.lineStart(activeLine) : selection.anchor, + active = Positions.lineBreak(line, document); + + return new vscode.Selection(anchor, active); + }); + } +} + +/** + * Select line above. + */ +export function line_above(_: Context, count: number) { + if (count === 0 || count === 1) { + Selections.update.byIndex((_, selection) => { + let line = Selections.activeLine(selection); + + if (!Selections.isEntireLines(selection)) { + line++; + } + + return new vscode.Selection(line, 0, line - 1, 0); + }); + } else { + Selections.update.byIndex((_, selection) => { + let line = Math.max(Selections.activeLine(selection) - count + 1, 0); + + if (!Selections.isEntireLines(selection)) { + line++; + } + + return new vscode.Selection(line, 0, line - 1, 0); + }); + } +} + +/** + * Extend to line above. + */ +export function line_above_extend(_: Context, count: number) { + if (count === 0 || count === 1) { + Selections.update.byIndex((_, selection) => { + if (selection.isSingleLine) { + let line = Selections.activeLine(selection); + + if (!Selections.isEntireLines(selection)) { line++; } - selections[i] = new vscode.Selection(line, 0, line + 1, 0); + return new vscode.Selection(line, 0, line - 1, 0); } - } else { - for (let i = 0; i < len; i++) { - const selection = selections[i], - targetLine = Math.min( - selectionHelper.activeLine(selection) + currentCount - 1, - editor.document.lineCount - 1, - ); - selections[i] = new vscode.Selection(targetLine, 0, targetLine + 1, 0); + if (selection.active === selection.end && Selections.isEntireLine(selection)) { + const line = Selections.activeLine(selection); + + return new vscode.Selection(line + 1, 0, line - 1, 0); } - } - editor.selections = selections; - }, -); + const isFullLine = Selections.activeLineIsFullySelected(selection), + isFullLineDiff = isFullLine ? -1 : 0, + active = new vscode.Position(Selections.activeLine(selection) + isFullLineDiff, 0); -registerCommand( - Command.selectLineExtend, - CommandFlags.ChangeSelections, - (editorState, { currentCount, selectionBehavior }) => { - const editor = editorState.editor, - selections = editor.selections, - len = selections.length, - selectionHelper = SelectionHelper.for(editorState); - - if (currentCount === 0 || currentCount === 1) { - for (let i = 0; i < len; i++) { - const selection = selections[i], - isSameLine = selectionHelper.isSingleLine(selection), - isFullLineDiff = selectionHelper.isEntireLine(selection) ? 1 : 0; - - const anchor = isSameLine ? selection.anchor.with(undefined, 0) : selection.anchor; - const active - = selection.active.character === 0 && !selection.isReversed && !isSameLine - ? selection.active.translate(1 + isFullLineDiff) - : new vscode.Position(selectionHelper.activeLine(selection) + 1 + isFullLineDiff, 0); - - selections[i] = new vscode.Selection(anchor, active); - } - } else { - for (let i = 0; i < len; i++) { - const selection = selections[i], - targetLine = Math.min( - selectionHelper.activeLine(selection) + currentCount - 1, - editor.document.lineCount - 1, - ), - isSameLine = selectionHelper.isSingleLine(selection); - - const anchor = isSameLine ? selection.anchor.with(undefined, 0) : selection.anchor; - const active = new vscode.Position(targetLine + 1, 0); - - selections[i] = new vscode.Selection(anchor, active); - } - } - - editor.selections = selections; - }, -); - -const toLineBegin: CoordMapper = (from) => from.with(undefined, 0); - -const selectToLineBegin = moveActiveCoord(toLineBegin, DoNotExtend); -registerCommand(Command.selectToLineBegin, CommandFlags.ChangeSelections, (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(selectToLineBegin); -}); - -const selectToLineBeginExtend = moveActiveCoord(toLineBegin, Extend); -registerCommand( - Command.selectToLineBeginExtend, - CommandFlags.ChangeSelections, - (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(selectToLineBeginExtend); - }, -); - -const toLineEnd: CoordMapper = (from, helper) => { - let newCol = helper.editor.document.lineAt(from.line).text.length; - if (newCol > 0 && helper.selectionBehavior === SelectionBehavior.Character) { - newCol--; - } - return from.with(undefined, newCol); -}; - -const selectToLineEnd = moveActiveCoord(toLineEnd, DoNotExtend); -registerCommand(Command.selectToLineEnd, CommandFlags.ChangeSelections, (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(selectToLineEnd); -}); - -const selectToLineEndExtend = moveActiveCoord(toLineEnd, Extend); -registerCommand( - Command.selectToLineEndExtend, - CommandFlags.ChangeSelections, - (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(selectToLineEndExtend); - }, -); - -const expandLine: SelectionMapper = (selection, helper) => { - // This command is idempotent. state.currentCount is intentionally ignored. - const { start, end } = selection, - document = helper.editor.document; - // Move start to line start and end to include line break. - const newStart = start.with(undefined, 0); - let newEnd; - if (end.character === 0) { - // End is next line start, which means the selection already includes - // the line break of last line. - newEnd = end; - } else if (end.line + 1 < document.lineCount) { - // Move end to the next line start to include the line break. - newEnd = new vscode.Position(end.line + 1, 0); + return new vscode.Selection(selection.anchor, active); + }); } else { - // End is at the last line, so try to include all text. - const textLen = document.lineAt(end.line).text.length; - newEnd = end.with(undefined, textLen); - } - // After expanding, the selection should be in the same direction as before. - if (selection.isReversed) { - return new vscode.Selection(newEnd, newStart); - } else { - return new vscode.Selection(newStart, newEnd); - } -}; + Selections.update.byIndex((_, selection, document) => { + let line = Math.max(Selections.activeLine(selection) - count, 0), + anchor = selection.anchor; -registerCommand(Command.expandLines, CommandFlags.ChangeSelections, (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(expandLine); -}); - -const trimToFullLines: SelectionMapper = (selection, helper) => { - // This command is idempotent. state.currentCount is intentionally ignored. - const { start, end } = selection; - // If start is not at line start, move it to the next line start. - const newStart = start.character === 0 ? start : new vscode.Position(start.line + 1, 0); - // Move end to the line start, so that the selection ends with a line break. - const newEnd = end.with(undefined, 0); - - if (newStart.isAfterOrEqual(newEnd)) { - return RemoveSelection; - } // No full line contained. - - // After trimming, the selection should be in the same direction as before. - // Except when selecting only one empty line in non-directional mode, prefer - // to keep the selection facing forward. - if (selection.isReversed - && !(helper.selectionBehavior === SelectionBehavior.Character - && newStart.line + 1 === newEnd.line)) { - return new vscode.Selection(newEnd, newStart); - } else { - return new vscode.Selection(newStart, newEnd); - } -}; - -registerCommand(Command.trimLines, CommandFlags.ChangeSelections, (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(trimToFullLines); -}); - -/** - * Starting from `current` (inclusive), find the first character that does not - * satisfy `condition` in the direction and return its Coord. - * - * @param current Coord of the first character to test - * @param condition will be only executed on character codes, not line breaks - * @returns the Coord of the first character that does not satisfy condition, - * which may be `current`. Or `undefined` if document edge is reached. - */ -export function skipWhile( - direction: Direction, - current: Coord, - condition: (charCode: number) => boolean, - document: vscode.TextDocument, - endLine?: number, -): Coord | undefined { - let col = current.character, - line = current.line, - text = document.lineAt(line).text; - if (endLine === undefined) { - endLine = direction === Forward ? document.lineCount - 1 : 0; - } - - while (col < 0 || col >= text.length || condition(text.charCodeAt(col))) { - col += direction; - if (col < 0 || col >= text.length) { - line += direction; - if (line < 0 || line * direction > endLine * direction) { - return undefined; + if (selection.active === selection.end) { + anchor = selection.active; } - text = document.lineAt(line).text; - col = direction === Forward ? 0 : text.length - 1; - } + + if (selection.isSingleLine) { + anchor = Positions.lineBreak(selection.anchor.line, document); + line++; + } else if (!Selections.startsWithEntireLine(selection)) { + line++; + } + + return new vscode.Selection(anchor, new vscode.Position(line, 0)); + }); } - return new Coord(line, col); } -const LF = "\n".charCodeAt(0); /** - * Starting from `current` (inclusive), find the first character or line break - * that does not satisfy `condition` in the direction and return its Coord. + * Select to line start. * - * @param current Coord of the first character to test - * @param condition will be called with charCode (or LF for line break). - * @returns the Coord of the first character/LF that does not satisfy condition, - * which may be `current`. Or `undefined` if document edge is reached. - */ -export function skipWhileX( - direction: Direction, - current: Coord, - condition: (charCode: number) => boolean, - document: vscode.TextDocument, - endLine?: number, -): Coord | undefined { - let col = current.character, - line = current.line; - if (endLine === undefined) { - endLine = direction === Forward ? document.lineCount - 1 : 0; - } - - while (line >= 0 && line * direction <= endLine * direction) { - const text = document.lineAt(line).text; - if (direction === Backward && col >= text.length) { - if (!condition(LF)) { - return new Coord(line, text.length); - } - col = text.length - 1; - } - while (col >= 0 && col < text.length) { - if (!condition(text.charCodeAt(col))) { - return new Coord(line, col); - } - col += direction; - } - - if (direction === Forward && !condition(LF)) { - return new Coord(line, text.length); - } - col = direction === Forward ? 0 : Number.MAX_SAFE_INTEGER; - line += direction; - } - return undefined; -} - -const trimSelections: SelectionMapper = (selection, helper) => { - // This command is idempotent. state.currentCount is intentionally ignored. - const document = helper.editor.document; - const isBlank = getCharSetFunction(CharSet.Blank, document); - - const firstCharacter = selection.start; - const lastCharacter = helper.coordAt(helper.offsetAt(selection.end) - 1); - - const start = skipWhile(Forward, firstCharacter, isBlank, document, lastCharacter.line); - const end = skipWhile(Backward, lastCharacter, isBlank, document, firstCharacter.line); - if (!start || !end || start.isAfter(end)) { - return RemoveSelection; - } - - if (selection.isReversed) { - return helper.selectionBetween(end, start, /* singleCharDirection = */ Backward); - } else { - return helper.selectionBetween(start, end, /* singleCharDirection = */ Forward); - } -}; - -registerCommand(Command.trimSelections, CommandFlags.ChangeSelections, (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(trimSelections); -}); - -// Select enclosing (m, M, alt+[mM]) -// =============================================================================================== - -const enclosingChars = new Uint8Array(Array.from("(){}[]<>", (ch) => ch.charCodeAt(0))); -const isNotEnclosingChar = (charCode: number) => enclosingChars.indexOf(charCode) === -1; - -/** - * Find the matching matchingChar, balanced by balancingChar. + * @keys `a-h` (normal), `home` (normal) * - * The character at start does not contribute to balance, and will not be - * returned as result either. Every other balancingChar will cause the next - * matchingChar to be ignored. + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | -------------------- | ------------------ | ----------------------------------- | ------------------------------------------------------------- | + * | Jump to line start | `lineStart.jump` | | `[".select.lineStart", { shift: "jump" }]` | + * | Extend to line start | `lineStart.extend` | `s-a-h` (normal), `s-home` (normal) | `[".select.lineStart", { shift: "extend" }]` | + * | Jump to line start (skip blank) | `lineStart.skipBlank.jump` | | `[".select.lineStart", { skipBlank: true, shift: "jump" }]` | + * | Extend to line start (skip blank) | `lineStart.skipBlank.extend` | | `[".select.lineStart", { skipBlank: true, shift: "extend" }]` | + * | Jump to first line | `firstLine.jump` | | `[".select.lineStart", { count: 0, shift: "jump" }]` | + * | Extend to first line | `firstLine.extend` | | `[".select.lineStart", { count: 0, shift: "extend" }]` | */ -export function findMatching( - direction: Direction, - start: Coord, - matchingChar: number, - balancingChar: number, - document: vscode.TextDocument, +export function lineStart( + _: Context, + + count: number, + shift = Shift.Select, + skipBlank = false, ) { - let isStart = true; - let balance = 0; - const active = skipWhile( - direction, - start, - (charCode) => { - if (isStart) { - isStart = false; - return true; - } - if (charCode === matchingChar) { - if (balance === 0) { - return false; - } - balance--; - } else if (charCode === balancingChar) { - balance++; - } - return true; - }, - document, + if (count > 0) { + const selection = _.selections[0], + newLine = Math.min(_.document.lineCount, count) - 1, + newPosition = skipBlank + ? Positions.nonBlankLineStart(newLine, _.document) + : Positions.lineStart(newLine), + newSelection = Selections.shift(selection, newPosition, shift); + + Selections.set([newSelection]); + + return; + } + + Selections.update.byIndex((_, selection) => + Selections.shift( + selection, + skipBlank + ? Positions.nonBlankLineStart(Selections.activeLine(selection)) + : Positions.lineStart(Selections.activeLine(selection)), + shift, + ), ); - return active; } -function selectEnclosing(extend: ExtendBehavior, direction: Direction) { - // This command intentionally ignores repetitions to be consistent with - // Kakoune. - // It only finds one next enclosing character and drags only once to its - // matching counterpart. Repetitions > 1 does exactly the same with rep=1, - // even though executing the command again will jump back and forth. +/** + * Select to line end. + * + * @keys `a-l` (normal), `end` (normal) + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | ------------------------ | -------------------- | ---------------------------------- | ---------------------------------------------------------- | + * | Extend to line end | `lineEnd.extend` | `s-a-l` (normal), `s-end` (normal) | `[".select.lineEnd", { shift: "extend" }]` | + * | Jump to last character | `documentEnd.jump` | | `[".select.lineEnd", { count: MAX_INT, shift: "jump" }]` | + * | Extend to last character | `documentEnd.extend` | | `[".select.lineEnd", { count: MAX_INT, shift: "extend" }]` | + */ +export function lineEnd( + _: Context, - const mapper = seekToRange( - (from, helper, i) => { - const document = helper.editor.document; - // First, find an enclosing char (which may be the current character). - let currentCharacter = from; - if (helper.selectionBehavior === SelectionBehavior.Caret) { - // When moving backwards, the first character to consider is the - // character to the left, not the right. However, we hackily special - // case `|[foo]>` (> is anchor, | is active) to jump to the end in the - // current group. - const selection = helper.editor.selections[i]; - if (direction === Backward && selection.isReversed) { - currentCharacter = helper.coordAt(helper.offsetAt(currentCharacter) - 1); - } - // Similarly, we special case `<[foo]|` to jump back in the current - // group. - if (direction === Forward && !selection.isReversed && !selection.isEmpty) { - currentCharacter = helper.coordAt(helper.offsetAt(currentCharacter) - 1); - } - } - if (helper.selectionBehavior === SelectionBehavior.Caret && direction === Backward) { - // When moving backwards, the first character to consider is the - // character to the left, not the right. - currentCharacter = helper.coordAt(helper.offsetAt(currentCharacter) - 1); - } - const anchor = skipWhile(direction, currentCharacter, isNotEnclosingChar, document); - if (!anchor) { - return RemoveSelection; - } + count: number, + shift = Shift.Select, +) { + if (count > 0) { + const selection = _.selections[0], + newLine = Math.min(_.document.lineCount, count) - 1, + newSelection = Selections.shift(selection, Positions.lineEnd(newLine), shift); - // Then, find the matching char of the anchor. - const enclosingChar = document.lineAt(anchor.line).text.charCodeAt(anchor.character), - idxOfEnclosingChar = enclosingChars.indexOf(enclosingChar); + Selections.set([newSelection]); - let active; - if (idxOfEnclosingChar & 1) { - // Odd enclosingChar index - // <=> enclosingChar is closing character - // <=> we go backward looking for the opening character - const matchingChar = enclosingChars[idxOfEnclosingChar - 1]; - active = findMatching(Backward, anchor, matchingChar, enclosingChar, document); - } else { - // Even enclosingChar index - // <=> enclosingChar is opening character - // <=> we go forward looking for the closing character - const matchingChar = enclosingChars[idxOfEnclosingChar + 1]; - active = findMatching(Forward, anchor, matchingChar, enclosingChar, document); - } + return; + } - if (!active) { - return RemoveSelection; - } - return [anchor, active]; - }, - extend, - /* singleCharDirection = */ direction, + Selections.update.byIndex((_, selection, doc) => + Selections.shift(selection, Positions.lineEnd(Selections.activeLine(selection), doc), shift), ); - - return (editorState: EditorState, state: CommandState) => { - SelectionHelper.for(editorState, state).mapEach(mapper); - }; } -registerCommand( - Command.selectEnclosing, - CommandFlags.ChangeSelections, - selectEnclosing(DoNotExtend, Forward), -); -registerCommand( - Command.selectEnclosingExtend, - CommandFlags.ChangeSelections, - selectEnclosing(Extend, Forward), -); -registerCommand( - Command.selectEnclosingBackwards, - CommandFlags.ChangeSelections, - selectEnclosing(DoNotExtend, Backward), -); -registerCommand( - Command.selectEnclosingExtendBackwards, - CommandFlags.ChangeSelections, - selectEnclosing(Extend, Backward), -); +/** + * Select to last line. + * + * #### Variants + * + * | Title | Identifier | Command | + * | ------------------- | ----------------- | ------------------------------------------- | + * | Jump to last line | `lastLine.jump` | `[".select.lastLine", { shift: "jump" }]` | + * | Extend to last line | `lastLine.extend` | `[".select.lastLine", { shift: "extend" }]` | + */ +export function lastLine(_: Context, document: vscode.TextDocument, shift = Shift.Select) { + let line = document.lineCount - 1; + + // In case of trailing line break, go to the second last line. + if (line > 0 && document.lineAt(document.lineCount - 1).text.length === 0) { + line--; + } + + Selections.set([Selections.shift(_.mainSelection, Positions.lineStart(line), shift)]); +} + +/** + * Select to first visible line. + * + * #### Variants + * + * | Title | Identifier | Command | + * | ---------------------------- | ------------------------- | --------------------------------------------------- | + * | Jump to first visible line | `firstVisibleLine.jump` | `[".select.firstVisibleLine", { shift: "jump" }]` | + * | Extend to first visible line | `firstVisibleLine.extend` | `[".select.firstVisibleLine", { shift: "extend" }]` | + */ +export function firstVisibleLine(_: Context, shift = Shift.Select) { + const selection = _.mainSelection, + toPosition = Positions.lineStart(api.firstVisibleLine(_.editor)); + + Selections.set([Selections.shift(selection, toPosition, shift)]); +} + +/** + * Select to middle visible line. + * + * #### Variants + * + * | Title | Identifier | Command | + * | ----------------------------- | -------------------------- | ---------------------------------------------------- | + * | Jump to middle visible line | `middleVisibleLine.jump` | `[".select.middleVisibleLine", { shift: "jump" }]` | + * | Extend to middle visible line | `middleVisibleLine.extend` | `[".select.middleVisibleLine", { shift: "extend" }]` | + */ +export function middleVisibleLine(_: Context, shift = Shift.Select) { + const selection = _.mainSelection, + toPosition = Positions.lineStart(api.middleVisibleLine(_.editor)); + + Selections.set([Selections.shift(selection, toPosition, shift)]); +} + +/** + * Select to last visible line. + * + * #### Variants + * + * | Title | Identifier | Command | + * | --------------------------- | ------------------------ | -------------------------------------------------- | + * | Jump to last visible line | `lastVisibleLine.jump` | `[".select.lastVisibleLine", { shift: "jump" }]` | + * | Extend to last visible line | `lastVisibleLine.extend` | `[".select.lastVisibleLine", { shift: "extend" }]` | + */ +export function lastVisibleLine(_: Context, shift = Shift.Select) { + const selection = _.mainSelection, + toPosition = Positions.lineStart(api.lastVisibleLine(_.editor)); + + Selections.set([Selections.shift(selection, toPosition, shift)]); +} + +/** + * Select to last modification. + * + * #### Variants + * + * | Title | Identifier | Command | + * | --------------------------- | ------------------------- | --------------------------------------------------- | + * | Jump to last modification | `lastModification.jump` | `[".select.lastModification", { shift: "jump" }]` | + * | Extend to last modification | `lastModification.extend` | `[".select.lastModification", { shift: "extend" }]` | + */ +export function lastModification(_: Context, shift = Shift.Select) { + const selection = _.mainSelection, + toPosition = todo(); + + Selections.set([Selections.shift(selection, toPosition, shift)]); +} diff --git a/src/commands/selectObject.ts b/src/commands/selectObject.ts deleted file mode 100644 index f0e15b3..0000000 --- a/src/commands/selectObject.ts +++ /dev/null @@ -1,897 +0,0 @@ -// Objects -// https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#object-selection - -import * as vscode from "vscode"; -import { Command, CommandFlags, CommandState, registerCommand } from "."; -import { - Backward, - Coord, - CoordMapper, - Direction, - DoNotExtend, - DocumentStart, - Extend, - Forward, - RemoveSelection, - SelectionHelper, - SelectionMapper, - moveActiveCoord, - seekToRange, -} from "../utils/selectionHelper"; -import { CharSet, getCharSetFunction } from "../utils/charset"; -import { findMatching, skipWhile, skipWhileX } from "./select"; -import { SelectionBehavior } from "../state/extension"; - -// Selecting is a bit harder than it sounds like: -// 1. Dealing with multiple lines, whether forwards or backwards, is a bit of a -// pain. -// 2. Dealing with strings is a bit of a pain -// 3. Dealing with inner objects is a bit of a pain - -const [ - LPAREN, - RPAREN, - LSQBRACKET, - RSQBRACKET, - LCRBRACKET, - RCRBRACKET, - LCHEVRON, - RCHEVRON, - LF, - SPACE, - QUOTE_DBL, - QUOTE_SGL, - BACKTICK, - BACKSLASH, - COMMA, -] = Array.from("()[]{}<>\n \"'`\\,", (ch) => ch.charCodeAt(0)); - -function objectActions( - toStart: CoordMapper, - toEnd: CoordMapper, - toStartInner: CoordMapper, - toEndInner: CoordMapper, - scanFromStart: boolean = false, -) { - const selectObject: SelectionMapper = (selection, helper, i) => { - const active = helper.activeCoord(selection); - const start = toStart(active, helper, i); - if ("remove" in start) { - return RemoveSelection; - } - const end = toEnd(scanFromStart ? start : active, helper, i); - if ("remove" in end) { - return RemoveSelection; - } - return helper.selectionBetween(start, end); - }; - const selectObjectInner: SelectionMapper = (selection, helper, i) => { - const active = helper.activeCoord(selection); - const start = toStartInner(active, helper, i); - if ("remove" in start) { - return RemoveSelection; - } - const end = toEndInner(scanFromStart ? start : active, helper, i); - if ("remove" in end) { - return RemoveSelection; - } - return helper.selectionBetween(start, end); - }; - - return { - select: { - outer: selectObject, - inner: selectObjectInner, - }, - selectToStart: { - outer: { - doNotExtend: moveActiveCoord(toStart, DoNotExtend), - extend: moveActiveCoord(toStart, Extend), - }, - inner: { - doNotExtend: moveActiveCoord(toStartInner, DoNotExtend), - extend: moveActiveCoord(toStartInner, Extend), - }, - }, - selectToEnd: { - outer: { - doNotExtend: moveActiveCoord(toEnd, DoNotExtend), - extend: moveActiveCoord(toEnd, Extend), - }, - inner: { - doNotExtend: moveActiveCoord(toEndInner, DoNotExtend), - extend: moveActiveCoord(toEndInner, Extend), - }, - }, - }; -} - -function objectWithinPair(startCharCode: number, endCharCode: number) { - const toStart: CoordMapper = (active, helper) => - findMatching(Backward, active, startCharCode, endCharCode, helper.editor.document) - ?? RemoveSelection; - const toEnd: CoordMapper = (active, helper) => - findMatching(Forward, active, endCharCode, startCharCode, helper.editor.document) - ?? RemoveSelection; - - const toStartInner: CoordMapper = (active, helper, i) => { - const pos = toStart(active, helper, i); - if ("remove" in pos) { - return pos; - } - return helper.nextPos(pos); - }; - - const toEndInner: CoordMapper = (active, helper, i) => { - const pos = toEnd(active, helper, i); - if ("remove" in pos) { - return pos; - } - return helper.prevPos(pos); - }; - - const actions = objectActions(toStart, toEnd, toStartInner, toEndInner); - - // Special cases for selectObject and selectObjectInner when active is at the - // start / end of an object, so that it always select a whole object within - // a matching pair. e.g. (12345) when active at first character should select - // the whole thing instead of error. - const defaultSelect = actions.select.outer; - const defaultSelectInner = actions.select.inner; - actions.select.outer = (selection, helper, i) => { - const active = helper.activeCoord(selection); - const currentCharCode = helper.editor.document - .lineAt(active.line) - .text.charCodeAt(active.character); - if (currentCharCode === startCharCode) { - const end = toEnd(active, helper, i); - if ("remove" in end) { - return RemoveSelection; - } - return helper.selectionBetween(active, end); - } else if (currentCharCode === endCharCode) { - const start = toStart(active, helper, i); - if ("remove" in start) { - return RemoveSelection; - } - return helper.selectionBetween(start, active); - } else { - return defaultSelect(selection, helper, i); - } - }; - actions.select.inner = (selection, helper, i) => { - const active = helper.activeCoord(selection); - const currentCharCode = helper.editor.document - .lineAt(active.line) - .text.charCodeAt(active.character); - if (currentCharCode === startCharCode) { - const end = toEndInner(active, helper, i); - if ("remove" in end) { - return RemoveSelection; - } - return helper.selectionBetween(helper.nextPos(active), end); - } else if (currentCharCode === endCharCode) { - const start = toStartInner(active, helper, i); - if ("remove" in start) { - return RemoveSelection; - } - return helper.selectionBetween(start, helper.prevPos(active)); - } else { - return defaultSelectInner(selection, helper, i); - } - }; - - return actions; -} - -type CharCodePredicate = (charCode: number) => boolean; -function objectWithCharSet(charSet: CharSet | CharCodePredicate) { - function toEdge(direction: Direction, includeTrailingWhitespace: boolean): CoordMapper { - return (active, helper) => { - const { document } = helper.editor; - const isInSet - = typeof charSet === "function" ? charSet : getCharSetFunction(charSet, document); - let col = active.character; - const text = document.lineAt(active.line).text; - if (col >= text.length) { - // A charset object cannot contain line break, therefore the cursor - // active is not within any such object. - return RemoveSelection; - } - while (isInSet(text.charCodeAt(col))) { - col += direction; - if (col < 0 || col >= text.length) { - return active.with(undefined, col - direction); - } - } - if (col === active.character) { - // The cursor active is on a character outside charSet. - return RemoveSelection; - } - if (includeTrailingWhitespace) { - const isBlank = getCharSetFunction(CharSet.Blank, document); - while (isBlank(text.charCodeAt(col))) { - col += direction; - if (col < 0 || col >= text.length) { - return active.with(undefined, col - direction); - } - } - } - return active.with(undefined, col - direction); - }; - } - - const toStart = toEdge(Backward, false); - const toStartInner = toStart; - const toEnd = toEdge(Forward, true); - const toEndInner = toEdge(Forward, false); - return objectActions(toStart, toEnd, toStartInner, toEndInner); -} - -function sentenceObject() { - // I bet that's the first time you see a Greek question mark used as an actual - // Greek question mark, rather than as a "prank" semicolon. - const punctCharCodes = new Uint32Array(Array.from(".!?¡§¶¿;՞。", (ch) => ch.charCodeAt(0))); - - function toBeforeBlank(allowSkipToPrevious: boolean) { - return (oldActive: Coord, helper: SelectionHelper) => { - const document = helper.editor.document; - const isBlank = getCharSetFunction(CharSet.Blank, document); - const origin = oldActive; // TODO: Adjust for caret mode. - - let jumpedOverBlankLine = false; - - let skipCurrent = allowSkipToPrevious; - let hadLf = true; - const beforeBlank = skipWhileX( - Backward, - origin, - (charCode) => { - if (charCode === LF) { - if (hadLf) { - jumpedOverBlankLine = true; - return allowSkipToPrevious; - } - hadLf = true; - skipCurrent = false; - return true; - } else { - hadLf = false; - if (skipCurrent) { - skipCurrent = false; - return true; - } - return isBlank(charCode); - } - }, - document, - ); - - if (beforeBlank === undefined) { - return origin; - } - - const beforeBlankChar = document - .lineAt(beforeBlank.line) - .text.charCodeAt(beforeBlank.character); - const hitPunctChar = punctCharCodes.includes(beforeBlankChar); - if (jumpedOverBlankLine && (!allowSkipToPrevious || !hitPunctChar)) { - // We jumped over blank lines but didn't hit a punct char. Don't accept. - return origin; - } - // let result = beforeBlank.isEqual(DocumentStart) - // ? beforeBlank - // : helper.prevPos(beforeBlank) - if (!hitPunctChar) { - return beforeBlank; - } - if (allowSkipToPrevious) { - return { prevSentenceEnd: beforeBlank }; - } - if (origin.line === beforeBlank.line) { - return beforeBlank; - } - // Example below: we started from '|' and found the '.'. - // foo. - // | bar - // In this case, technically we started from the second sentence - // and reached the first sentence. This is not permitted when when - // allowSkipToPrevious is false, so let's go back. - return origin; - }; - } - - const toSentenceStart = (origin: Coord, helper: SelectionHelper): Coord => { - const document = helper.editor.document; - const isBlank = getCharSetFunction(CharSet.Blank, document); - - let originLineText = document.lineAt(origin.line).text; - if (originLineText.length === 0 && origin.line + 1 >= document.lineCount) { - if (origin.line === 0) { - // There is only one line and that line is empty. What a life. - return DocumentStart; - } - // Special case: If at the last line, search from the previous line. - originLineText = document.lineAt(origin.line - 1).text; - origin = origin.with(origin.line - 1, originLineText.length); - } - if (originLineText.length === 0) { - // This line is empty. Just go to the first non-blank char on next line. - const nextLineText = document.lineAt(origin.line + 1).text; - let col = 0; - while (col < nextLineText.length && isBlank(nextLineText.charCodeAt(col))) { - col++; - } - return new Coord(origin.line + 1, col); - } - - let first = true; - let hadLf = false; - const afterSkip = skipWhileX( - Backward, - origin, - (charCode) => { - if (charCode === LF) { - first = false; - if (hadLf) { - return false; - } - hadLf = true; - } else { - hadLf = false; - if (first) { - // Don't need to check if first character encountered is punct -- - // that may be the current sentence end. - first = false; - return true; - } - if (punctCharCodes.indexOf(charCode) >= 0) { - return false; - } - } - return true; - }, - document, - ); - - // If we hit two LFs or document start, the current sentence starts at the - // first non-blank character after that. - if (hadLf || !afterSkip) { - return skipWhileX(Forward, afterSkip ?? DocumentStart, isBlank, document) ?? DocumentStart; - } - - // If we hit a punct char, then the current sentence starts on the first - // non-blank character on the same line, or the line break. - let col = afterSkip.character + 1; - const text = document.lineAt(afterSkip.line).text; - while (col < text.length && isBlank(text.charCodeAt(col))) { - col++; - } - return afterSkip.with(undefined, col); - }; - function toEnd(inner: boolean): CoordMapper { - return (origin, helper) => { - const document = helper.editor.document; - - if (document.lineAt(origin.line).text.length === 0) { - // We're on an empty line which does not belong to last sentence or this - // sentence. If next line is also empty, we should just stay here. - // However, start scanning from the next line if it is not empty. - if ( - origin.line + 1 >= document.lineCount - || document.lineAt(origin.line + 1).text.length === 0 - ) { - return origin; - } else { - origin = new Coord(origin.line + 1, 0); - } - } - - const isBlank = getCharSetFunction(CharSet.Blank, document); - - let hadLf = false; - const innerEnd = skipWhileX( - Forward, - origin, - (charCode) => { - if (charCode === LF) { - if (hadLf) { - return false; - } - hadLf = true; - } else { - hadLf = false; - if (punctCharCodes.indexOf(charCode) >= 0) { - return false; - } - } - return true; - }, - document, - ); - - if (!innerEnd) { - return helper.lastCoord(); - } - - // If a sentence ends with two LFs in a row, then the first LF is part of - // the inner & outer sentence while the second LF should be excluded. - if (hadLf) { - return helper.prevPos(innerEnd); - } - - if (inner) { - return innerEnd; - } - // If a sentence ends with punct char, then any blank characters after it - // but BEFORE any line breaks belongs to the outer sentence. - let col = innerEnd.character + 1; - const text = document.lineAt(innerEnd.line).text; - while (col < text.length && isBlank(text.charCodeAt(col))) { - col++; - } - return innerEnd.with(undefined, col - 1); - }; - } - const toBeforeBlankCurrent = toBeforeBlank(false); - const toCurrentStart: CoordMapper = (oldActive, helper, i) => { - let beforeBlank = toBeforeBlankCurrent(oldActive, helper); - if ("prevSentenceEnd" in beforeBlank) { - beforeBlank = oldActive; - } - return toSentenceStart(beforeBlank, helper); - }; - - // It is imposssible to determine if active is at leading or trailing or - // in-sentence blank characters by just looking ahead. Therefore, we search - // from the sentence start, which may be slightly less efficient but - // always accurate. - const scanFromStart = true; - const actions = objectActions( - toCurrentStart, - toEnd(false), - toCurrentStart, - toEnd(true), - scanFromStart, - ); - - // Special cases to allow jumping to the previous sentence when active is at - // current sentence start / leading blank chars. - const toBeforeBlankOrPrev = toBeforeBlank(true); - actions.selectToStart.inner = actions.selectToStart.outer = { - extend: moveActiveCoord((oldActive, helper, i) => { - let beforeBlank = toBeforeBlankOrPrev(oldActive, helper); - if ("prevSentenceEnd" in beforeBlank) { - beforeBlank = beforeBlank.prevSentenceEnd; - } - return toSentenceStart(beforeBlank, helper); - }, Extend), - doNotExtend: (selection: vscode.Selection, helper: SelectionHelper) => { - const oldActive = helper.activeCoord(selection); - const beforeBlank = toBeforeBlankOrPrev(oldActive, helper); - - if ("prevSentenceEnd" in beforeBlank) { - const newAnchor = beforeBlank.prevSentenceEnd; - console.log("hit prev end", helper._visualizeCoord(newAnchor)); - // Special case: re-anchor when skipping to last sentence end. - return helper.selectionBetween(newAnchor, toSentenceStart(newAnchor, helper)); - } else { - const newActive = toSentenceStart(beforeBlank, helper); - if (helper.selectionBehavior === SelectionBehavior.Caret) { - // TODO: Optimize to avoid coordAt / offsetAt. - const activePos = selection.active.isBeforeOrEqual(newActive) - ? helper.coordAt(helper.offsetAt(newActive) + 1) - : newActive; - return new vscode.Selection(selection.active, activePos); - } - return helper.selectionBetween(oldActive, newActive); - } - }, - }; - return actions; -} - -function paragraphObject() { - const lookBack: CoordMapper = (active, helper) => { - const { line } = active; - if ( - line > 0 - && active.character === 0 - && helper.editor.document.lineAt(line - 1).text.length === 0 - ) { - return new Coord(line - 1, 0); // Re-anchor to the previous line. - } - return active; - }; - const lookAhead: CoordMapper = (active, helper) => { - const { line } = active; - if (helper.editor.document.lineAt(line).text.length === 0) { - return new Coord(line + 1, 0); - } - return active; - }; - - const toCurrentStart: CoordMapper = (active, helper) => { - const { document } = helper.editor; - let { line } = active; - - // Move past any trailing empty lines. - while (line >= 0 && document.lineAt(line).text.length === 0) { - line--; - } - if (line <= 0) { - return DocumentStart; - } - - // Then move to the start of the paragraph (non-empty lines). - while (line > 0 && document.lineAt(line - 1).text.length > 0) { - line--; - } - return new Coord(line, 0); - }; - - function toEnd(inner: boolean): CoordMapper { - return (active, helper) => { - const { document } = helper.editor; - let { line } = active; - - // Move to the end of the paragraph (non-empty lines) - while (line < document.lineCount && document.lineAt(line).text.length > 0) { - line++; - } - if (line >= document.lineCount) { - return helper.lastCoord(); - } - if (inner) { - if (line > 0) { - line--; - } - return new Coord(line, document.lineAt(line).text.length); - } - - // Then move to the last trailing empty line. - while (line + 1 < document.lineCount && document.lineAt(line + 1).text.length === 0) { - line++; - } - return new Coord(line, document.lineAt(line).text.length); - }; - } - - function selectToEdge(direction: Direction, adjust: CoordMapper, toEdge: CoordMapper) { - return { - extend: moveActiveCoord((oldActive, helper, i) => { - const adjusted = adjust(oldActive, helper, i); - if ("remove" in adjusted) { - return RemoveSelection; - } - if ( - direction === Forward - && helper.editor.document.lineAt(adjusted.line).text.length === 0 - ) { - return toEdge(new Coord(adjusted.line + 1, 0), helper, i); - } else { - return toEdge(adjusted, helper, i); - } - }, Extend), - doNotExtend: seekToRange((oldActive, helper, i) => { - const anchor = adjust(oldActive, helper, i); - if ("remove" in anchor) { - return RemoveSelection; - } - - let active; - if (direction === Forward && helper.editor.document.lineAt(anchor.line).text.length === 0) { - active = toEdge(new Coord(anchor.line + 1, 0), helper, i); - } else { - active = toEdge(anchor, helper, i); - } - - if ("remove" in active) { - return RemoveSelection; - } - return [anchor, active]; - }, DoNotExtend), - }; - } - - function select(inner: boolean): SelectionMapper { - const toEndFunc = toEnd(inner); - return (selection, helper, i) => { - const active = helper.activeCoord(selection); - const { document } = helper.editor; - - let start; - if ( - active.line + 1 < document.lineCount - && document.lineAt(active.line).text.length === 0 - && document.lineAt(active.line + 1).text.length - ) { - // Special case: if current line is empty, check next line and select - // the NEXT paragraph if next line is not empty. - start = new Coord(active.line + 1, 0); - } else { - const startResult = toCurrentStart(active, helper, i); - if ("remove" in startResult) { - return RemoveSelection; - } - start = startResult; - } - // It's just much easier to check from start. - const end = toEndFunc(start, helper, i); - if ("remove" in end) { - return RemoveSelection; - } - return helper.selectionBetween(start, end); - }; - } - - const selectToStart = selectToEdge(Backward, lookBack, toCurrentStart); - return { - select: { - outer: select(/* inner = */ false), - inner: select(/* inner = */ true), - }, - selectToEnd: { - outer: selectToEdge(Forward, lookAhead, toEnd(/* inner = */ false)), - inner: selectToEdge(Forward, lookAhead, toEnd(/* inner = */ true)), - }, - selectToStart: { - outer: selectToStart, - inner: selectToStart, - }, - }; -} - -function whitespacesObject() { - // The "inner" versions of a whitespaces object excludes all line breaks and - // the "outer" versions includes line breaks as well. Unlike other objects, - // there are no actual "surrounding" parts of objects. - - // The objectWithCharSet helper function can handle the inline whitespaces. - const actions = objectWithCharSet(CharSet.Blank); - - // Let's then overwrite logic for "outer" actions to include line breaks. - const toStart: CoordMapper = (active, helper) => { - const { document } = helper.editor; - const isBlank = getCharSetFunction(CharSet.Blank, document); - const afterSkip = skipWhileX(Backward, active, isBlank, document); - if (!afterSkip) { - return DocumentStart; - } - if (afterSkip.isEqual(active)) { - return RemoveSelection; - } - return helper.nextPos(afterSkip); - }; - const toEnd: CoordMapper = (active, helper) => { - const { document } = helper.editor; - const isBlank = getCharSetFunction(CharSet.Blank, document); - const afterSkip = skipWhileX(Forward, active, isBlank, document); - if (!afterSkip) { - return helper.lastCoord(); - } - if (afterSkip.isEqual(active)) { - return RemoveSelection; - } - return helper.prevPos(afterSkip); - }; - actions.select.outer = (selection, helper, i) => { - const active = helper.activeCoord(selection); - const start = toStart(active, helper, i); - const end = toEnd(active, helper, i); - if ("remove" in start || "remove" in end) { - return RemoveSelection; - } - return helper.selectionBetween(start, end); - }; - actions.selectToStart.outer = { - doNotExtend: moveActiveCoord(toStart, DoNotExtend), - extend: moveActiveCoord(toStart, Extend), - }; - actions.selectToEnd.outer = { - doNotExtend: moveActiveCoord(toEnd, DoNotExtend), - extend: moveActiveCoord(toEnd, Extend), - }; - return actions; -} - -function indentObject() { - function toEdge(direction: Direction, inner: boolean): CoordMapper { - return (oldActive, helper) => { - const { document } = helper.editor; - let { line } = oldActive; - let lineObj = document.lineAt(line); - - // First, scan backwards through blank lines. (Note that whitespace-only - // lines do not count -- those have a proper indentation level and should - // be treated as the inner part of the indent block.) - while (lineObj.text.length === 0) { - line += direction; - if (line < 0) { - return DocumentStart; - } - if (line >= document.lineCount) { - return helper.lastCoord(); - } - lineObj = document.lineAt(line); - } - - const indent = lineObj.firstNonWhitespaceCharacterIndex; - let lastNonBlankLine = line; - - for (;;) { - line += direction; - if (line < 0) { - return DocumentStart; - } - if (line >= document.lineCount) { - return helper.lastCoord(); - } - lineObj = document.lineAt(line); - - if (lineObj.text.length === 0) { - continue; - } - if (lineObj.firstNonWhitespaceCharacterIndex < indent) { - const resultLine = inner ? lastNonBlankLine : line - direction; - if (direction === Forward && resultLine + 1 === document.lineCount) { - return helper.lastCoord(); - } - const resultCol = direction === Backward ? 0 : document.lineAt(resultLine).text.length; - return new Coord(resultLine, resultCol); - } - lastNonBlankLine = line; - } - }; - } - const toStart = toEdge(Backward, false), - toStartInner = toEdge(Backward, true), - toEnd = toEdge(Forward, false), - toEndInner = toEdge(Forward, true); - - // When selecting a whole indent object, scanning separately toStart and then - // toEnd will lead to wrong results like two different indentation levels and - // skipping over blank lines more than needed. We can mitigate this by finding - // the start first and then scan from there to find the end of indent block. - const scanFromStart = true; - return objectActions(toStart, toEnd, toStartInner, toEndInner, scanFromStart); -} - -function numberObject() { - // TODO: Handle optional leading minus sign for numbers. - const numberCharCodes = new Uint32Array(Array.from("0123456789.", (ch) => ch.charCodeAt(0))); - - // Numbers cannot have trailing whitespaces even for outer in Kakoune, and - // let's match the behavior here. - const actions = objectWithCharSet((charCode) => numberCharCodes.indexOf(charCode) >= 0); - actions.select.outer = actions.select.inner; - actions.selectToEnd.outer = actions.selectToEnd.inner; - actions.selectToStart.outer = actions.selectToStart.inner; - return actions; -} - -function argumentObject() { - function toEdge(direction: Direction, inner: boolean): CoordMapper { - const paren = direction === Backward ? LPAREN : RPAREN; - return (oldActive, helper) => { - const { document } = helper.editor; - let bbalance = 0, - pbalance = 0; - const afterSkip = skipWhile( - direction, - oldActive, - (charCode) => { - // TODO: Kak does not care about strings or ignoring braces in strings - // but maybe we should add a setting or an alternative command for - // that. - if (charCode === paren && pbalance === 0 && bbalance === 0) { - return false; - } else if (charCode === LPAREN) { - pbalance++; - } else if (charCode === LSQBRACKET) { - bbalance++; - } else if (charCode === RPAREN) { - pbalance--; - } else if (charCode === RSQBRACKET) { - bbalance--; - } else if (pbalance !== 0 || bbalance !== 0) { - // Nop. - } else if (charCode === COMMA) { - return false; - } - return true; - }, - helper.editor.document, - ); - - let end; - if (afterSkip === undefined) { - end = direction === Backward ? DocumentStart : helper.lastCoord(); - } else { - const charCode = document.lineAt(afterSkip.line).text.charCodeAt(afterSkip.character); - // Make sure parens are not included in the object. Deliminator commas - // after the argument is included as outer, but ones before are NOT. - - // TODO: Kakoune seems to have more sophisticated edge cases for commas, - // e.g. outer last argument includes the comma before it, plus more edge - // cases for who owns the whitespace. Those are not implemented for now - // because they require extensive tests and mess a lot with the logic of - // selecting the whole object. - if (inner || charCode === paren || direction === Backward) { - end = direction === Backward ? helper.nextPos(afterSkip) : helper.prevPos(afterSkip); - } else { - end = afterSkip; - } - } - if (!inner) { - return end; - } - const isBlank = getCharSetFunction(CharSet.Blank, document); - // Exclude any surrounding whitespaces. - end = skipWhileX(-direction, end, isBlank, helper.editor.document); - if (!end) { - return direction === Backward ? DocumentStart : helper.lastCoord(); - } - return end; - }; - } - - const toStart = toEdge(Backward, false), - toStartInner = toEdge(Backward, true), - toEnd = toEdge(Forward, false), - toEndInner = toEdge(Forward, true); - return objectActions(toStart, toEnd, toStartInner, toEndInner); -} - -type ObjectAction = "select" | "selectToStart" | "selectToEnd"; -const dispatch = { - parens: objectWithinPair(LPAREN, RPAREN), - braces: objectWithinPair(LCRBRACKET, RCRBRACKET), - brackets: objectWithinPair(LSQBRACKET, RSQBRACKET), - angleBrackets: objectWithinPair(LCHEVRON, RCHEVRON), - doubleQuoteString: objectWithinPair(QUOTE_DBL, QUOTE_DBL), - singleQuoteString: objectWithinPair(QUOTE_SGL, QUOTE_SGL), - graveQuoteString: objectWithinPair(BACKTICK, BACKTICK), - word: objectWithCharSet(CharSet.Word), - WORD: objectWithCharSet(CharSet.NonBlank), - sentence: sentenceObject(), - paragraph: paragraphObject(), - whitespaces: whitespacesObject(), - indent: indentObject(), - number: numberObject(), - argument: argumentObject(), - // TODO: custom -}; - -registerCommand( - Command.objectsPerformSelection, - CommandFlags.ChangeSelections, - (editorState, state) => { - if (!state.argument || !state.argument.object) { - throw new Error( - "Argument must have shape " - + "{object: string, action: string, extend?: boolean, inner?: boolean}", - ); - } - const dispatch2 = dispatch[state.argument.object as keyof typeof dispatch]; - if (!dispatch2) { - throw new Error( - "Invalid argument: object must be a string and one of " + Object.keys(dispatch).join(","), - ); - } - const dispatch3 = dispatch2[state.argument.action as ObjectAction]; - if (!dispatch3) { - throw new Error( - "Invalid argument: action must be a string and one of " + Object.keys(dispatch2).join(","), - ); - } - const bound = state.argument.inner ? "inner" : "outer"; - let mapper = dispatch3[bound]; - if (typeof mapper === "object") { - const extend = state.argument.extend ? "extend" : "doNotExtend"; - mapper = mapper[extend]; - } - const helper = SelectionHelper.for(editorState, state); - helper.mapEach(mapper); - }, -); diff --git a/src/commands/selections.rotate.ts b/src/commands/selections.rotate.ts new file mode 100644 index 0000000..fcea8ec --- /dev/null +++ b/src/commands/selections.rotate.ts @@ -0,0 +1,62 @@ +import { Argument } from "."; +import { Context, rotate } from "../api"; + +/** + * Rotate selection indices and contents. + */ +declare module "./selections.rotate"; + +/** + * Rotate selections clockwise. + * + * @keys `(` (normal) + * + * The following keybinding is also available: + * + * | Title | Identifier | Keybinding | Command | + * | ----------------------------------- | -------------- | ------------ | ------------------------------------------- | + * | Rotate selections counter-clockwise | `both.reverse` | `)` (normal) | `[".selections.rotate", { reverse: true }]` | + */ +export function both(_: Context, repetitions: number, reverse: Argument = false) { + if (reverse) { + repetitions = -repetitions; + } + + return rotate(repetitions); +} + +/** + * Rotate selections clockwise (contents only). + * + * The following command is also available: + * + * | Title | Identifier | Command | + * | --------------------------------------------------- | ------------------ | ---------------------------------------------------- | + * | Rotate selections counter-clockwise (contents only) | `contents.reverse` | `[".selections.rotate.contents", { reverse: true }]` | + */ +export function contents(_: Context, repetitions: number, reverse: Argument = false) { + if (reverse) { + repetitions = -repetitions; + } + + return rotate.contentsOnly(repetitions); +} + +/** + * Rotate selections clockwise (selections only). + * + * @keys `a-(` (normal) + * + * The following keybinding is also available: + * + * | Title | Identifier | Keybinding | Command | + * | ----------------------------------------------------- | -------------------- | -------------- | ------------------------------------------------------ | + * | Rotate selections counter-clockwise (selections only) | `selections.reverse` | `a-)` (normal) | `[".selections.rotate.selections", { reverse: true }]` | + */ +export function selections(_: Context, repetitions: number, reverse: Argument = false) { + if (reverse) { + repetitions = -repetitions; + } + + return rotate.selectionsOnly(repetitions); +} diff --git a/src/commands/selections.ts b/src/commands/selections.ts index 06e9596..4f0a110 100644 --- a/src/commands/selections.ts +++ b/src/commands/selections.ts @@ -1,452 +1,898 @@ -// Manipulate existing selections. import * as vscode from "vscode"; -import { Command, CommandFlags, CommandState, InputKind, registerCommand } from "."; -import { - Backward, - Direction, - DoNotExtend, - Forward, - SelectionHelper, - SelectionMapper, - jumpTo, -} from "../utils/selectionHelper"; -import { EditorState } from "../state/editor"; +import { Argument, Input, InputOr, RegisterOr, SetInput } from "."; +import { ArgumentError, Context, Direction, EmptySelectionsError, moveWhile, Positions, prompt, Selections, switchRun, todo } from "../api"; +import { PerEditorState } from "../state/editors"; +import { Mode, SelectionBehavior } from "../state/modes"; +import { Register } from "../state/registers"; +import { CharSet, getCharacters } from "../utils/charset"; +import { AutoDisposable } from "../utils/disposables"; +import { manipulateSelectionsInteractively } from "../utils/misc"; +import { SettingsValidator } from "../utils/settings-validator"; +import { TrackedSelection } from "../utils/tracked-selection"; -// Swap cursors (;, a-;, a-:) -// =============================================================================================== +/** + * Interacting with selections. + */ +declare module "./selections"; -const reduceToActive: SelectionMapper = jumpTo((active) => active, DoNotExtend); -registerCommand(Command.selectionsReduce, CommandFlags.ChangeSelections, (editorState, state) => { - SelectionHelper.for(editorState, state).mapEach(reduceToActive); -}); +/** + * Copy selections text. + * + * @keys `y` (normal) + */ +export function saveText( + document: vscode.TextDocument, + selections: readonly vscode.Selection[], + register: RegisterOr<"dquote", Register.Flags.CanWrite>, +) { + register.set(selections.map(document.getText.bind(document))); +} -registerCommand(Command.selectionsFlip, CommandFlags.ChangeSelections, ({ editor }) => { - const selections = editor.selections, - len = selections.length; +/** + * Save selections. + * + * @keys `s-z` (normal) + */ +export function save( + _: Context, + document: vscode.TextDocument, + selections: readonly vscode.Selection[], + register: RegisterOr<"caret", Register.Flags.CanWriteSelections>, - for (let i = 0; i < len; i++) { - const selection = selections[i]; + style?: Argument, + until?: Argument, +) { + const trackedSelections = TrackedSelection.fromArray(selections, document); + let trackedSelectionSet: TrackedSelection.Set; - selections[i] = new vscode.Selection(selection.active, selection.anchor); + if (typeof style === "object") { + const validator = new SettingsValidator(), + renderOptions = Mode.decorationObjectToDecorationRenderOptions(style, validator); + + validator.throwErrorIfNeeded(); + + trackedSelectionSet = + new TrackedSelection.StyledSet(trackedSelections, _.getState(), renderOptions); + } else { + trackedSelectionSet = new TrackedSelection.Set(trackedSelections, document); } - editor.selections = selections; -}); + const disposable = _.extension + .createAutoDisposable() + .addNotifyingDisposable(trackedSelectionSet); -registerCommand(Command.selectionsForward, CommandFlags.ChangeSelections, ({ editor }) => { - const selections = editor.selections, - len = selections.length; + if (Array.isArray(until)) { + until.forEach((until) => disposable.disposeOnUserEvent(until, _)); + } - for (let i = 0; i < len; i++) { - const selection = selections[i]; + register.replaceSelectionSet(trackedSelectionSet)?.dispose(); +} - if (selection.isReversed) { - selections[i] = new vscode.Selection(selection.active, selection.anchor); +/** + * Restore selections. + * + * @keys `z` (normal) + */ +export function restore( + _: Context, + register: RegisterOr<"caret", Register.Flags.CanReadSelections>, +) { + const selectionSet = register.getSelectionSet(); + + if (selectionSet === undefined) { + throw new EmptySelectionsError(`no selections are saved in register "${register.name}"`); + } + + return _.switchToDocument(selectionSet.document, /* alsoFocusEditor= */ true) + .then(() => _.selections = selectionSet.restore()); +} + +/** + * Combine register selections with current ones. + * + * @keys `a-z` (normal) + * + * The following keybinding is also available: + * + * | Keybinding | Command | + * | ---------------- | -------------------------------------------------------- | + * | `s-a-z` (normal) | `[".selections.restore.withCurrent", { reverse: true }]` | + * + * See https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#marks + */ +export async function restore_withCurrent( + _: Context, + document: vscode.TextDocument, + register: RegisterOr<"caret", Register.Flags.CanReadSelections>, + + reverse: Argument = false, +) { + const savedSelections = register.getSelections(); + + EmptySelectionsError.throwIfRegisterIsEmpty(savedSelections, register.name); + + let from = savedSelections, + add = _.selections; + + if (reverse) { + from = _.selections; + add = savedSelections; + } + + const type = await prompt.one([ + ["a", "Append lists"], + ["u", "Union"], + ["i", "Intersection"], + ["<", "Select leftmost cursor"], + [">", "Select rightmost cursor"], + ["+", "Select longest"], + ["-", "Select shortest"], + ]); + + if (typeof type === "string") { + return; + } + + if (type === 0) { + _.selections = from.concat(add); + + return; + } + + if (from.length !== add.length) { + throw new Error("the current and register selections have different sizes"); + } + + const selections = [] as vscode.Selection[]; + + for (let i = 0; i < from.length; i++) { + const a = from[i], + b = add[i]; + + switch (type) { + case 1: { + const anchor = a.start.isBefore(b.start) ? a.start : b.start, + active = a.end.isAfter(b.end) ? a.end : b.end; + + selections.push(new vscode.Selection(anchor, active)); + break; + } + + case 2: { + const anchor = a.start.isAfter(b.start) ? a.start : b.start, + active = a.end.isBefore(b.end) ? a.end : b.end; + + selections.push(new vscode.Selection(anchor, active)); + break; + } + + case 3: + if (a.active.isBeforeOrEqual(b.active)) { + selections.push(a); + } else { + selections.push(b); + } + break; + + case 4: + if (a.active.isAfterOrEqual(b.active)) { + selections.push(a); + } else { + selections.push(b); + } + break; + + case 5: { + const aLength = document.offsetAt(a.end) - document.offsetAt(a.start), + bLength = document.offsetAt(b.end) - document.offsetAt(b.start); + + if (aLength > bLength) { + selections.push(a); + } else { + selections.push(b); + } + break; + } + + case 6: { + const aLength = document.offsetAt(a.end) - document.offsetAt(a.start), + bLength = document.offsetAt(b.end) - document.offsetAt(b.start); + + if (aLength < bLength) { + selections.push(a); + } else { + selections.push(b); + } + break; + } } } - editor.selections = selections; -}); + _.selections = selections; +} -registerCommand(Command.selectionsBackward, CommandFlags.ChangeSelections, ({ editor }) => { - const selections = editor.selections, - len = selections.length; +let lastPipeInput: string | undefined; - for (let i = 0; i < len; i++) { - const selection = selections[i]; - - if (!selection.isReversed) { - selections[i] = new vscode.Selection(selection.active, selection.anchor); - } - } - - editor.selections = selections; -}); - -// Align (&, a-&) -// =============================================================================================== - -registerCommand(Command.selectionsAlign, CommandFlags.Edit, ({ editor }, _, undoStops) => { - const startChar = editor.selections.reduce( - (max, sel) => (sel.start.character > max ? sel.start.character : max), - 0, - ); - - return editor - .edit((builder) => { - const selections = editor.selections, - len = selections.length; - - for (let i = 0; i < len; i++) { - const selection = selections[i]; - - builder.insert(selection.start, " ".repeat(startChar - selection.start.character)); +/** + * Pipe selections. + * + * Run the specified command or code with the contents of each selection, and + * save the result to a register. + * + * @keys `a-|` (normal) + * + * See https://github.com/mawww/kakoune/blob/master/doc/pages/keys.asciidoc#changes-through-external-programs + * + * #### Additional commands + * + * | Title | Identifier | Keybinding | Commands | + * | ------------------- | -------------- | -------------- | --------------------------------------------------------------------------- | + * | Pipe and replace | `pipe.replace` | `|` (normal) | `[".selections.pipe"], [".edit.insert", { register: "|" }]` | + * | Pipe and append | `pipe.append` | `!` (normal) | `[".selections.pipe"], [".edit.insert", { register: "|", where: "end" }]` | + * | Pipe and prepend | `pipe.prepend` | `a-!` (normal) | `[".selections.pipe"], [".edit.insert", { register: "|", where: "start" }]` | + */ +export async function pipe( + _: Context, + register: RegisterOr<"pipe", Register.Flags.CanWrite>, + inputOr: InputOr, +) { + const input = await inputOr(() => prompt({ + prompt: "Expression", + validateInput(value) { + try { + switchRun.validate(value); + lastPipeInput = value; + } catch (e) { + return e?.message ?? `${e}`; } - }, undoStops) - .then(() => undefined); -}); + }, + value: lastPipeInput, + }, _)); -registerCommand(Command.selectionsAlignCopy, CommandFlags.Edit, ({ editor }, state, undoStops) => { - const sourceSelection = editor.selections[state.currentCount - 1] ?? editor.selection; - const sourceIndent = editor.document.lineAt(sourceSelection.start) - .firstNonWhitespaceCharacterIndex; + const selections = _.selections, + document = _.document, + selectionsStrings = selections.map((selection) => document.getText(selection)); - return editor - .edit((builder) => { - const selections = editor.selections, - len = selections.length; + const results = await Promise.all(_.run((_) => selectionsStrings.map((string, i, strings) => + switchRun(input!, { $: string, $$: strings, i, n: strings.length }), + ))); - for (let i = 0; i < len; i++) { - if (i === sourceSelection.start.line) { - continue; - } + const strings = results.map((result) => { + if (result === null) { + return "null"; + } + if (result === undefined) { + return ""; + } + if (typeof result === "string") { + return result; + } + if (typeof result === "number" || typeof result === "boolean") { + return result.toString(); + } + if (typeof result === "object") { + return JSON.stringify(result); + } - const line = editor.document.lineAt(selections[i].start); - const indent = line.firstNonWhitespaceCharacterIndex; + throw new Error("invalid returned value by expression"); + }); - if (indent > sourceIndent) { - builder.delete( - line.range.with( - undefined, - line.range.start.translate(undefined, indent - sourceIndent), - ), - ); - } else if (indent < sourceIndent) { - builder.insert(line.range.start, " ".repeat(indent - sourceIndent)); - } + await register.set(strings); +} + +let lastFilterInput: string | undefined; + +/** + * Filter selections. + * + * @keys `$` (normal) + * + * #### Variants + * + * | Title | Identifier | Keybinding | Commands | + * | -------------------------- | ----------------------- | ------------------ | -------------------------------------------------------------- | + * | Keep matching selections | `filter.regexp` | `a-k` (normal) | `[".selections.filter", { defaultInput: "/" }]` | + * | Clear matching selections | `filter.regexp.inverse` | `s-a-k` (normal) | `[".selections.filter", { defaultInput: "/", inverse: true }]` | + * | Clear secondary selections | `clear.secondary` | `space` (normal) | `[".selections.filter", { input: "i === 0" }]` | + * | Clear main selections | `clear.main` | `a-space` (normal) | `[".selections.filter", { input: "i !== 0" }]` | + */ +export function filter( + _: Context, + + input: Input, + setInput: SetInput, + defaultInput?: Argument, + inverse: Argument = false, + interactive: Argument = true, +) { + const document = _.document, + strings = _.selections.map((selection) => document.getText(selection)); + + return manipulateSelectionsInteractively(_, input, setInput, interactive, { + prompt: "Expression", + validateInput(value) { + try { + switchRun.validate(value); + lastFilterInput = value; + } catch (e) { + return e?.message ?? `${e}`; } - }, undoStops) - .then(() => void 0); -}); + }, + value: defaultInput ?? lastFilterInput, + valueSelection: defaultInput + ? [defaultInput.length, defaultInput.length] + : lastFilterInput + ? [0, lastFilterInput.length] + : undefined, + }, (input, selections) => { + return Selections.filter.byIndex(async (i) => { + const context = { $: strings[i], $$: strings, i, n: strings.length }; -// Clear, filter (spc, a-spc, a-k, a-K) -// =============================================================================================== - -registerCommand(Command.selectionsClear, CommandFlags.ChangeSelections, ({ editor }) => { - editor.selections = [editor.selection]; -}); - -registerCommand(Command.selectionsClearMain, CommandFlags.ChangeSelections, ({ editor }) => { - const selections = editor.selections; - - if (selections.length > 1) { - selections.shift(); - editor.selections = selections; - } -}); - -registerCommand( - Command.selectionsKeepMatching, - CommandFlags.ChangeSelections, - InputKind.RegExp, - () => "", - ({ editor }, { input: regex }) => { - const document = editor.document, - newSelections = editor.selections.filter((selection) => - regex.test(document.getText(selection)), - ); - - if (newSelections.length > 0) { - editor.selections = newSelections; - } - }, -); - -registerCommand( - Command.selectionsClearMatching, - CommandFlags.ChangeSelections, - InputKind.RegExp, - () => "", - ({ editor }, { input: regex }) => { - const document = editor.document, - newSelections = editor.selections.filter( - (selection) => !regex.test(document.getText(selection)), - ); - - if (newSelections.length > 0) { - editor.selections = newSelections; - } - }, -); - -// Select within, split (s, S) -// =============================================================================================== - -registerCommand( - Command.select, - CommandFlags.ChangeSelections, - InputKind.RegExp, - () => "gm", - ({ editor }, { input: regex }) => { - const { document, selections } = editor, - len = selections.length, - newSelections = [] as vscode.Selection[]; - - for (let i = 0; i < len; i++) { - const selection = selections[i], - selectionText = document.getText(selection), - selectionStartOffset = document.offsetAt(selection.start); - - let match: RegExpExecArray | null; - - while ((match = regex.exec(selectionText))) { - const anchor = document.positionAt(selectionStartOffset + match.index); - const active = document.positionAt(selectionStartOffset + match.index + match[0].length); - - newSelections.push(new vscode.Selection(anchor, active)); - - if (match[0].length === 0) { - regex.lastIndex++; - } + try { + return !!(await switchRun(input, context)) !== inverse; + } catch { + return inverse; } + }, selections).then(Selections.set).then(() => input); + }); +} + +let lastSelectInput: RegExp | undefined; + +/** + * Select within selections. + * + * @keys `s` (normal) + */ +export function select( + _: Context, + + interactive: Argument = true, + input: Input, + setInput: SetInput, +) { + return manipulateSelectionsInteractively(_, input, setInput, interactive, { + ...prompt.regexpOpts("mu"), + value: lastSelectInput?.source, + }, (input, selections) => { + if (typeof input === "string") { + input = new RegExp(input, "mu"); } - if (newSelections.length > 0) { - editor.selections = newSelections; - } - }, -); + lastSelectInput = input; -registerCommand( - Command.split, - CommandFlags.ChangeSelections, - InputKind.RegExp, - () => "gm", - ({ editor }, { input: regex }) => { - const { document, selections } = editor, - len = selections.length, - newSelections = [] as vscode.Selection[]; + Selections.set(Selections.selectWithin(input, selections)); - for (let i = 0; i < len; i++) { - const selection = selections[i], - selectionText = document.getText(selection), - selectionStartOffset = document.offsetAt(selection.start); + return Promise.resolve(input); + }); +} - let match: RegExpExecArray | null; - let index = 0; +let lastSplitInput: RegExp | undefined; - while ((match = regex.exec(selectionText))) { - const anchor = document.positionAt(selectionStartOffset + index); - const active = document.positionAt(selectionStartOffset + match.index); +/** + * Split selections. + * + * @keys `s-s` (normal) + */ +export function split( + _: Context, - newSelections.push(new vscode.Selection(anchor, active)); - - index = match.index + match[0].length; - - if (match[0].length === 0) { - regex.lastIndex++; - } - } - - newSelections.push( - new vscode.Selection(document.positionAt(selectionStartOffset + index), selection.end), - ); + excludeEmpty: Argument = false, + interactive: Argument = true, + input: Input, + setInput: SetInput, +) { + return manipulateSelectionsInteractively(_, input, setInput, interactive, { + ...prompt.regexpOpts("mu"), + value: lastSplitInput?.source, + }, (input, selections) => { + if (typeof input === "string") { + input = new RegExp(input, "mu"); } - editor.selections = newSelections; - }, -); + lastSplitInput = input; -// Split lines, select first & last, merge (a-s, a-S, a-_) -// =============================================================================================== + let split = Selections.split(input, selections); -registerCommand(Command.splitLines, CommandFlags.ChangeSelections, ({ editor }) => { - const selections = editor.selections, - len = selections.length, - newSelections = [] as vscode.Selection[]; + if (excludeEmpty) { + split = split.filter((s) => !s.isEmpty); + } - for (let i = 0; i < len; i++) { - const selection = selections[i]; + Selections.set(split); - if (selection.isSingleLine) { - newSelections.unshift(selection); + return Promise.resolve(input); + }); +} + +/** + * Split selections at line boundaries. + * + * @keys `a-s` (normal) + */ +export function splitLines( + _: Context, + document: vscode.TextDocument, + selections: readonly vscode.Selection[], + repetitions: number, +) { + const newSelections = [] as vscode.Selection[]; + + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i], + start = selection.start, + end = selection.end, + startLine = start.line, + endLine = end.line, + isReversed = selection.isReversed; + + if (startLine === endLine) { + newSelections.push(selection); return; } - const startLine = selection.start.line, - startIndex = newSelections.length, - isReversed = selection.isReversed; - - // Compute end line. - const endAnchor = selection.end.with(undefined, 0), - endActive = selection.end; - - const endSelection = new vscode.Selection(endAnchor, endActive); - // Add start line. newSelections.push( - new vscode.Selection( - selection.start, - selection.start.with(undefined, Number.MAX_SAFE_INTEGER), - ), + Selections.fromStartEnd(start, Positions.lineEnd(startLine, document), isReversed, document), ); // Add intermediate lines. - for (let line = startLine + 1; line < selection.end.line; line++) { - const anchor = new vscode.Position(line, 0), - active = new vscode.Position(line, Number.MAX_SAFE_INTEGER); + for (let line = startLine + repetitions; line < endLine; line += repetitions) { + const start = Positions.lineStart(line), + end = Positions.lineEnd(line, document); - newSelections.unshift(new vscode.Selection(anchor, active)); + newSelections.push(Selections.fromStartEnd(start, end, isReversed, document)); } // Add end line. - newSelections.unshift(endSelection); - - // Restore direction of each line. - if (isReversed) { - for (let i = startIndex; i < newSelections.length; i++) { - newSelections[i] = new vscode.Selection(newSelections[i].active, newSelections[i].anchor); - } + if (endLine % repetitions === 0) { + newSelections.push( + Selections.fromStartEnd(Positions.lineStart(endLine), end, isReversed, document), + ); } } - editor.selections = newSelections; -}); + Selections.set(newSelections); +} -registerCommand(Command.selectFirstLast, CommandFlags.ChangeSelections, ({ editor }) => { - const { document, selections } = editor, - len = selections.length, - newSelections = [] as vscode.Selection[]; +/** + * Expand to lines. + * + * Expand selections to contain full lines (including end-of-line characters). + * + * @keys `a-x` (normal) + */ +export function expandToLines(_: Context) { + return Selections.update.byIndex((_, selection, document) => { + const start = selection.start, + end = selection.end; - for (let i = 0; i < len; i++) { - const selection = selections[i], - selectionStartOffset = document.offsetAt(selection.start), - selectionEndOffset = document.offsetAt(selection.end); + // Move start to line start and end to include line break. + const newStart = start.with(undefined, 0); + let newEnd: vscode.Position; - if (selectionEndOffset - selectionStartOffset < 2) { - newSelections.push(selection); + if (end.character === 0) { + // End is next line start, which means the selection already includes the + // line break of last line. + newEnd = end; + } else if (end.line + 1 < document.lineCount) { + // Move end to the next line start to include the line break. + newEnd = new vscode.Position(end.line + 1, 0); } else { - // Select start character. - { - const start = selection.start, - end = document.positionAt(document.offsetAt(start) + 1); + // End is at the last line, so try to include all text. + const textLen = document.lineAt(end.line).text.length; + newEnd = end.with(undefined, textLen); + } - newSelections.push(new vscode.Selection(start, end)); + // After expanding, the selection should be in the same direction as before. + return Selections.fromStartEnd(newStart, newEnd, selection.isReversed); + }); +} + +/** + * Trim lines. + * + * Trim selections to only contain full lines (from start to line break). + * + * @keys `s-a-x` (normal) + */ +export function trimLines(_: Context) { + return Selections.update.byIndex((_, selection) => { + const start = selection.start, + end = selection.end; + + // If start is not at line start, move it to the next line start. + const newStart = start.character === 0 ? start : new vscode.Position(start.line + 1, 0); + // Move end to the line start, so that the selection ends with a line break. + const newEnd = new vscode.Position(end.line, 0); + + if (newStart.isAfterOrEqual(newEnd)) { + return undefined; // No full line contained. + } + + // After trimming, the selection should be in the same direction as before. + // Except when selecting only one empty line in non-directional mode, prefer + // to keep the selection facing forward. + if (selection.isReversed && newStart.line + 1 !== newEnd.line) { + return new vscode.Selection(newEnd, newStart); + } else { + return new vscode.Selection(newStart, newEnd); + } + }); +} + +/** + * Trim whitespace. + * + * Trim whitespace at beginning and end of selections. + * + * @keys `_` (normal) + */ +export function trimWhitespace(_: Context) { + const blank = getCharacters(CharSet.Blank, _.document), + isBlank = (character: string) => blank.includes(character); + + return Selections.update.byIndex((_, selection, document) => { + const firstCharacter = selection.start, + lastCharacter = selection.end; + + const start = moveWhile.forward(isBlank, firstCharacter, document), + end = moveWhile.backward(isBlank, lastCharacter, document); + + if (start.isAfter(end)) { + return undefined; + } + + return Selections.fromStartEnd(start, end, selection.isReversed); + }); +} + +/** + * Reduce selections to their cursor. + * + * @param where Which edge each selection should be reduced to; defaults to + * "active". + * + * @keys `;` (normal) + * + * #### Variant + * + * | Title | Identifier | Keybinding | Command | + * | ------------------------------- | -------------- | ---------------- | ------------------------------------------- | + * | Reduce selections to their ends | `reduce.edges` | `s-a-s` (normal) | `[".selections.reduce", { where: "both" }]` | + */ +export function reduce( + _: Context, + + where: Argument<"active" | "anchor" | "start" | "end" | "both"> = "active", +) { + ArgumentError.validate( + "where", + ["active", "anchor", "start", "end", "both"].includes(where), + `"where" must be "active", "anchor", "start", "end", "both", or undefined`, + ); + + const takeWhere = _.selectionBehavior === SelectionBehavior.Character + ? (selection: vscode.Selection, prop: Exclude) => { + const result = selection[prop]; + + if (result === selection.end && !result.isEqual(selection.start)) { + return Positions.previous(result)!; + } + + return result; + } + : (selection: vscode.Selection, prop: Exclude) => selection[prop]; + + if (where !== "both") { + Selections.update.byIndex((_, selection) => Selections.empty(takeWhere(selection, where))); + + return; + } + + Selections.set(_.selections.flatMap((selection) => { + if (selection.isEmpty || Selections.isNonDirectional(selection)) { + return [selection]; + } + + return [ + Selections.empty(takeWhere(selection, "active")), + Selections.empty(takeWhere(selection, "anchor")), + ]; + })); +} + +/** + * Change direction of selections. + * + * @param direction If unspecified, flips each direction. Otherwise, ensures + * that all selections face the given direction. + * + * @keys `a-;` (normal) + * + * #### Variants + * + * | Title | Identifier | Keybinding | Command | + * | ------------------- | -------------- | -------------- | ---------------------------------------------------- | + * | Forward selections | `faceForward` | `a-:` (normal) | `[".selections.changeDirection", { direction: 1 }]` | + * | Backward selections | `faceBackward` | | `[".selections.changeDirection", { direction: -1 }]` | + */ +export function changeDirection(_: Context, direction?: Direction) { + switch (direction) { + case Direction.Backward: + Selections.update.byIndex((_, selection) => + selection.isReversed || selection.isEmpty || Selections.isNonDirectional(selection) + ? selection + : new vscode.Selection(selection.end, selection.start)); + break; + + case Direction.Forward: + Selections.update.byIndex((_, selection) => + selection.isReversed + ? new vscode.Selection(selection.start, selection.end) + : selection); + break; + + default: + Selections.update.byIndex((_, selection) => + selection.isEmpty || Selections.isNonDirectional(selection) + ? selection + : new vscode.Selection(selection.active, selection.anchor)); + break; + } +} + +/** + * Copy selections below. + * + * @keys `s-c` (normal) + * + * #### Variant + * + * | Title | Identifier | Keybinding | Command | + * | --------------------- | ------------ | ---------------- | ----------------------------------------- | + * | Copy selections above | `copy.above` | `s-a-c` (normal) | `[".selections.copy", { direction: -1 }]` | + */ +export function copy( + _: Context, + document: vscode.TextDocument, + selections: readonly vscode.Selection[], + repetitions: number, + + direction = Direction.Forward, +) { + const newSelections = [] as vscode.Selection[], + lineCount = document.lineCount; + + for (const selection of selections) { + const activeLine = Selections.activeLine(selection); + let currentLine = activeLine + direction; + + for (let i = 0; i < repetitions;) { + if (currentLine < 0 || currentLine >= lineCount) { + break; } - // Select end character. - { - const end = selection.end, - start = document.positionAt(document.offsetAt(end) - 1); + const copiedSelection = tryCopySelection(document, selection, currentLine); - newSelections.push(new vscode.Selection(start, end)); + if (copiedSelection === undefined) { + currentLine += direction; + continue; } + + newSelections.push(copiedSelection); + + i++; + currentLine = direction === Direction.Backward + ? copiedSelection.end.line - 1 + : copiedSelection.start.line + 1; } } - editor.selections = newSelections; -}); + newSelections.push(...selections); -// Copy selections (C, a-C) -// =============================================================================================== + Selections.set(newSelections); +} + +/** + * Merge contiguous selections. + * + * @keys `a-_` (normal) + */ +export function merge(_: Context) { + Selections.set(Selections.mergeConsecutive(Selections.current)); +} + +/** + * Open selected file. + */ +export async function open(_: Context) { + const path = await import("path"), + basePath = path.dirname(_.document.fileName); + + await Promise.all( + Selections.map((text) => + vscode.workspace + .openTextDocument(path.resolve(basePath, text)) + .then(vscode.window.showTextDocument), + ), + ); +} + +const indicesToken = PerEditorState.registerState(/* isDisposable= */ true); + +/** + * Toggle selection indices. + * + * @keys `enter` (normal) + * + * #### Variants + * + * | Title | Identifier | Command | + * | ---------------------- | ------------- | --------------------------------------------------- | + * | Show selection indices | `showIndices` | `[".selections.toggleIndices", { display: true }]` | + * | Hide selection indices | `hideIndices` | `[".selections.toggleIndices", { display: false }]` | + */ +export function toggleIndices( + _: Context, + + display: Argument = undefined, + until: Argument = [], +) { + const editorState = _.getState(); + let disposable = editorState.get(indicesToken); + + if (disposable !== undefined) { + // Indices already exist; remove them. + if (display !== true) { + editorState.store(indicesToken, undefined); + disposable.dispose(); + } + + return; + } + + // Indices do not exist yet; add them. + if (display === false) { + return; + } + + const indicesDecorationType = vscode.window.createTextEditorDecorationType({ + after: { + color: new vscode.ThemeColor("textLink.activeForeground"), + margin: "0 0 0 20px", + }, + isWholeLine: true, + }); + + function onDidChangeSelection(editor: vscode.TextEditor) { + // Collect selection indices for each line; keep the column of the cursor in + // memory for later. + const selections = editor.selections, + selectionsPerLine = new Map(); + + for (let i = 0; i < selections.length; i++) { + const selection = selections[i], + active = selection.active, + activeLine = Selections.activeLine(selection), + activeCharacter = activeLine === active.line + ? active.character + : Number.MAX_SAFE_INTEGER, // We were at the end of the line. + selectionsForLine = selectionsPerLine.get(activeLine); + + if (selectionsForLine === undefined) { + selectionsPerLine.set(activeLine, [[activeCharacter, i]]); + } else { + selectionsForLine.push([activeCharacter, i]); + } + } + + // For each line with selections, add a new decoration. + const ranges = [] as vscode.DecorationOptions[]; + + for (const [line, selectionsForLine] of selectionsPerLine) { + // Sort selection indices by their column to make sure they match the + // order seen by the user. + selectionsForLine.sort((a, b) => a[0] - b[0]); + + const rangePosition = new vscode.Position(line, 0), + range = new vscode.Range(rangePosition, rangePosition); + + ranges.push({ + range, + renderOptions: { + after: { + contentText: "#" + selectionsForLine.map((x) => x[1]).join(", #"), + }, + }, + }); + } + + editor.setDecorations(indicesDecorationType, ranges); + } + + disposable = _.extension + .createAutoDisposable() + .addDisposable(indicesDecorationType) + .addDisposable(vscode.window.onDidChangeTextEditorSelection((e) => + e.textEditor === editorState.editor && onDidChangeSelection(e.textEditor), + )) + .addDisposable(editorState.onVisibilityDidChange((e) => + e.isVisible && onDidChangeSelection(e.editor), + )); + + editorState.store(indicesToken, disposable); + + if (Array.isArray(until)) { + until.forEach((until) => disposable!.disposeOnUserEvent(until, _)); + } + + onDidChangeSelection(editorState.editor); +} function tryCopySelection( - selectionHelper: SelectionHelper, document: vscode.TextDocument, selection: vscode.Selection, newActiveLine: number, ) { const active = selection.active, anchor = selection.anchor, - activeLine = selectionHelper.activeLine(selection); + activeLine = Selections.activeLine(selection), + endCharacter = Selections.endCharacter(selection, document); + let activeCharacter = selection.end === active ? endCharacter : active.character, + anchorCharacter = selection.end === anchor ? endCharacter : anchor.character; if (activeLine === anchor.line) { - const newLine = document.lineAt(newActiveLine); + const newLineLength = document.lineAt(newActiveLine).text.length; - // TODO: Generalize below for all cases - return newLine.text.length >= selection.end.character - ? new vscode.Selection(anchor.with(newActiveLine), - active.with(newActiveLine, selectionHelper.activeCharacter(selection))) - : undefined; + if (endCharacter > newLineLength) { + if (endCharacter !== newLineLength + 1) { + return undefined; + } + + return selection.end === active + ? new vscode.Selection(newActiveLine, anchorCharacter, newActiveLine + 1, 0) + : new vscode.Selection(newActiveLine + 1, 0, newActiveLine, activeCharacter); + } + + return new vscode.Selection(newActiveLine, anchorCharacter, newActiveLine, activeCharacter); } - const newAnchorLine = newActiveLine + anchor.line - activeLine; + let newAnchorLine = newActiveLine + anchor.line - activeLine; if (newAnchorLine < 0 || newAnchorLine >= document.lineCount) { return undefined; } - const newAnchorTextLine = document.lineAt(newAnchorLine); + const newAnchorLineLength = document.lineAt(newAnchorLine).text.length; - if (anchor.character > newAnchorTextLine.text.length) { - return undefined; + if (anchorCharacter > newAnchorLineLength) { + if (anchorCharacter !== newAnchorLineLength + 1) { + return undefined; + } + + newAnchorLine++; + anchorCharacter = 0; } - const newActiveTextLine = document.lineAt(newActiveLine); + const newActiveLineLength = document.lineAt(newActiveLine).text.length; - if (active.character > newActiveTextLine.text.length) { - return undefined; + if (active.character > newActiveLineLength) { + if (activeCharacter !== newActiveLineLength + 1) { + return undefined; + } + + newActiveLine++; + activeCharacter = 0; } - const newSelection = new vscode.Selection(anchor.with(newAnchorLine), active.with(newActiveLine)); - const hasOverlap - = !( - selection.start.line < newSelection.start.line - || (selection.end.line === newSelection.start.line - && selection.end.character < newSelection.start.character) - ) - && !( - newSelection.start.line < selection.start.line - || (newSelection.end.line === selection.start.line - && newSelection.end.character < selection.start.character) - ); + const newSelection = new vscode.Selection(newAnchorLine, anchorCharacter, + newActiveLine, activeCharacter); - if (hasOverlap) { + if (Selections.overlap(selection, newSelection)) { return undefined; } return newSelection; } - -function copySelections( - editorState: EditorState, - { repetitions }: CommandState, - direction: Direction, -) { - const editor = editorState.editor, - selections = editor.selections, - len = selections.length, - document = editor.document, - lineCount = document.lineCount, - selectionHelper = SelectionHelper.for(editorState); - - for (let i = 0; i < len; i++) { - const selection = selections[i], - selectionActiveLine = selectionHelper.activeLine(selection); - - for ( - let i = 0, currentLine = selectionActiveLine + direction; - i < repetitions && currentLine >= 0 && currentLine < lineCount; - - ) { - const copiedSelection = tryCopySelection(selectionHelper, document, selection, currentLine); - - if (copiedSelection !== undefined) { - if (!selections.some((s) => s.contains(copiedSelection))) { - selections.push(copiedSelection); - } - - i++; - - if (direction === Backward) { - currentLine = copiedSelection.end.line - 1; - } else { - currentLine = copiedSelection.start.line + 1; - } - } else { - currentLine += direction; - } - } - } - - editor.selections = selections; -} - -registerCommand(Command.selectCopy, CommandFlags.ChangeSelections, (editorState, state) => - copySelections(editorState, state, Forward), -); -registerCommand(Command.selectCopyBackwards, CommandFlags.ChangeSelections, (editorState, state) => - copySelections(editorState, state, Backward), -); diff --git a/src/commands/view.ts b/src/commands/view.ts new file mode 100644 index 0000000..6300859 --- /dev/null +++ b/src/commands/view.ts @@ -0,0 +1,29 @@ +import * as vscode from "vscode"; + +import { Argument } from "."; +import { Context, Selections } from "../api"; + +/** + * Moving the editor view. + * + * #### Predefined keybindings + * + * | Title | Keybinding | Command | + * | ----------------------- | -------------- | ------------------------------------------------ | + * | Show view menu | `v` (normal) | `[".openMenu", { input: "view" }]` | + * | Show view menu (locked) | `s-v` (normal) | `[".openMenu", { input: "view", locked: true }]` | + */ +declare module "./view"; + +/** + * Reveals a position based on the main cursor. + */ +export function line( + _: Context, + at: Argument<"top" | "center" | "bottom"> = "center", +) { + return vscode.commands.executeCommand( + "revealLine", + { at, lineNumber: Selections.activeLine(_.mainSelection) }, + ); +} diff --git a/src/commands/yankPaste.ts b/src/commands/yankPaste.ts deleted file mode 100644 index fa7983e..0000000 --- a/src/commands/yankPaste.ts +++ /dev/null @@ -1,326 +0,0 @@ -import * as vscode from "vscode"; - -import { Command, CommandFlags, CommandState, InputKind, UndoStops, registerCommand } from "."; -import { Extension } from "../state/extension"; -import { SelectionHelper } from "../utils/selectionHelper"; - -function getRegister(state: CommandState, ctx: Extension) { - return state.currentRegister || ctx.registers.dquote; -} - -function deleteSelections(editor: vscode.TextEditor, undoStops: UndoStops) { - return editor.edit((builder) => { - const selections = editor.selections, - len = selections.length; - - for (let i = 0; i < len; i++) { - builder.delete(selections[i]); - } - }, undoStops); -} - -registerCommand( - Command.deleteYank, - CommandFlags.Edit, - async ({ editor, extension }, state, undoStops) => { - const reg = getRegister(state, extension); - - if (reg.canWrite()) { - await reg.set(editor, editor.selections.map(editor.document.getText)); - } - - await deleteSelections(editor, undoStops); - }, -); - -registerCommand( - Command.deleteInsertYank, - CommandFlags.Edit | CommandFlags.SwitchToInsertBefore, - async ({ editor, extension }, state, undoStops) => { - const reg = getRegister(state, extension); - - if (reg.canWrite()) { - await reg.set(editor, editor.selections.map(editor.document.getText)); - } - - await deleteSelections(editor, undoStops); - }, -); - -registerCommand(Command.deleteNoYank, CommandFlags.Edit, ({ editor }, _, undoStops) => { - return deleteSelections(editor, undoStops).then(() => undefined); -}); - -registerCommand( - Command.deleteInsertNoYank, - CommandFlags.Edit | CommandFlags.SwitchToInsertBefore, - ({ editor }, _, undoStops) => { - return deleteSelections(editor, undoStops).then(() => undefined); - }, -); - -registerCommand(Command.yank, CommandFlags.None, ({ editor, extension }, state) => { - const reg = getRegister(state, extension); - - if (reg.canWrite()) { - return reg.set(editor, editor.selections.map(editor.document.getText)); - } - - return undefined; -}); - -async function getContentsToPaste( - editor: vscode.TextEditor, - state: CommandState, - ctx: Extension, -) { - const yanked = await getRegister(state, ctx).get(editor); - const amount = editor.selections.length; - - if (yanked === undefined) { - return undefined; - } - - const results = [] as string[], - yankedLength = yanked.length; - - let i = 0; - - for (; i < amount && i < yankedLength; i++) { - results.push(yanked[i]); - } - - for (; i < amount; i++) { - results.push(yanked[yankedLength - 1]); - } - - return results; -} - -registerCommand(Command.pasteAfter, CommandFlags.Edit, async (editorState, state, undoStops) => { - const { editor, extension } = editorState, - selections = editor.selections, - selectionHelper = SelectionHelper.for(editorState, state); - - const contents = await getContentsToPaste(editor, state, extension); - - if (contents === undefined) { - return; - } - - const selectionLengths = [] as number[]; - - await editor.edit((builder) => { - for (let i = 0; i < contents.length; i++) { - const content = contents[i], - selection = selections[i]; - - if (content.endsWith("\n")) { - builder.insert(new vscode.Position(selectionHelper.endLine(selection) + 1, 0), content); - } else { - builder.insert(selection.end, content); - } - - selectionLengths.push(selectionHelper.selectionLength(selection)); - } - }, undoStops); - - // Restore selections that were extended automatically. - for (let i = 0; i < contents.length; i++) { - selections[i] = selectionHelper.selectionFromLength(selections[i].anchor, selectionLengths[i]); - } - - editor.selections = selections; -}); - -registerCommand( - Command.pasteBefore, - CommandFlags.Edit, - async ({ editor, extension }, state, undoStops) => { - const contents = await getContentsToPaste(editor, state, extension); - - if (contents === undefined) { - return; - } - - await editor.edit((builder) => { - for (let i = 0; i < contents.length; i++) { - const content = contents[i], - selection = editor.selections[i]; - - if (content.endsWith("\n")) { - builder.insert(selection.start.with(undefined, 0), content); - } else { - builder.insert(selection.start, content); - } - } - }, undoStops); - }, -); - -registerCommand( - Command.pasteSelectAfter, - CommandFlags.ChangeSelections | CommandFlags.Edit, - async (editorState, state, undoStops) => { - const { editor, extension } = editorState, - contents = await getContentsToPaste(editor, state, extension); - - if (contents === undefined) { - return; - } - - const reverseSelection = [] as boolean[], - selectionHelper = SelectionHelper.for(editorState, state); - - await editor.edit((builder) => { - for (let i = 0; i < contents.length; i++) { - const content = contents[i], - selection = editor.selections[i]; - - if (content.endsWith("\n")) { - builder.insert(selection.end.with(selectionHelper.endLine(selection) + 1, 0), content); - } else { - builder.insert(selection.end, content); - } - - reverseSelection.push(selection.isEmpty); - } - }, undoStops); - - // Reverse selections that were empty, since they are now extended in the - // wrong way. - for (let i = 0; i < contents.length; i++) { - const content = contents[i]; - - if (!content.endsWith("\n") && reverseSelection[i]) { - editor.selections[i] = new vscode.Selection( - editor.selections[i].active, - editor.selections[i].anchor, - ); - } - } - - // eslint-disable-next-line no-self-assign - editor.selections = editor.selections; // Force update. - }, -); - -registerCommand( - Command.pasteSelectBefore, - CommandFlags.ChangeSelections | CommandFlags.Edit, - async ({ editor, extension }, state, undoStops) => { - const contents = await getContentsToPaste(editor, state, extension); - - if (contents === undefined) { - return; - } - - await editor.edit((builder) => { - for (let i = 0; i < contents.length; i++) { - const content = contents[i], - selection = editor.selections[i]; - - if (content.endsWith("\n")) { - builder.replace(selection.start.with(undefined, 0), content); - } else { - builder.replace(selection.start, content); - } - } - }, undoStops); - }, -); - -registerCommand( - Command.pasteReplace, - CommandFlags.Edit, - async ({ editor, extension }, state, undoStops) => { - const contents = await getContentsToPaste(editor, state, extension); - if (contents === undefined) { - return; - } - - const reg = getRegister(state, extension); - if (reg.canWrite()) { - await reg.set(editor, editor.selections.map(editor.document.getText)); - } - - await editor.edit((builder) => { - for (let i = 0; i < contents.length; i++) { - const content = contents[i], - selection = editor.selections[i]; - - builder.replace(selection, content); - } - }, undoStops); - }, -); - -registerCommand( - Command.pasteReplaceEvery, - CommandFlags.Edit, - async ({ editor, extension }, state, undoStops) => { - const selections = editor.selections; - const contents = await getRegister(state, extension).get(editor); - - if (contents === undefined || contents.length !== selections.length) { - return; - } - - await editor.edit((builder) => { - for (let i = 0; i < contents.length; i++) { - builder.replace(selections[i], contents[i]); - } - }, undoStops); - }, -); - -registerCommand( - Command.replaceCharacters, - CommandFlags.Edit, - InputKind.Key, - () => void 0, - ({ editor }, { repetitions, input: key }, undoStops) => { - const string = key.repeat(repetitions); - - return editor - .edit((builder) => { - for (const selection of editor.selections) { - let i = selection.start.line; - - if (selection.end.line === i) { - // A single line-selection; replace the selection directly - builder.replace( - selection, - string.repeat(selection.end.character - selection.start.character), - ); - - continue; - } - - // Replace in first line - const firstLine = editor.document.lineAt(i).range.with(selection.start); - - builder.replace( - firstLine, - string.repeat(firstLine.end.character - firstLine.start.character), - ); - - // Replace in intermediate lines - while (++i < selection.end.line) { - const line = editor.document.lineAt(i); - - builder.replace(line.range, string.repeat(line.text.length)); - } - - // Replace in last line - const lastLine = editor.document.lineAt(i).range.with(undefined, selection.end); - - builder.replace( - lastLine, - string.repeat(lastLine.end.character - lastLine.start.character), - ); - } - }, undoStops) - .then(() => void 0); - }, -); diff --git a/src/extension.ts b/src/extension.ts index d66a26c..710dacd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,7 @@ import * as vscode from "vscode"; +import * as api from "./api"; +import { loadCommands } from "./commands/load-all"; import { Extension } from "./state/extension"; /** @@ -10,37 +12,41 @@ export const extensionName = "dance"; /** * Global state of the extension. */ -export let extensionState: Extension; +export let extensionState: Extension | undefined; + +let isActivated = false; /** * Function called by VS Code to activate the extension. */ -export function activate(context: vscode.ExtensionContext) { - extensionState = new Extension(); +export function activate() { + isActivated = true; - context.subscriptions.push( - vscode.commands.registerCommand(extensionName + ".toggle", () => - extensionState.setEnabled(!extensionState.enabled, false), - ), - ); + const extensionData = vscode.extensions.getExtension(`gregoire.${extensionName}`), + extensionPackageJSON = extensionData?.packageJSON; - if (process.env.VERBOSE_LOGGING === "true") { - // Log all commands we need to implement - Promise.all([vscode.commands.getCommands(true), import("../commands/index")]).then( - ([registeredCommands, { commands }]) => { - for (const command of Object.values(commands)) { - if (registeredCommands.indexOf(command.id) === -1) { - console.warn("Command", command.id, "is defined but not implemented."); - } - } - }, - ); + if (extensionPackageJSON?.[`${extensionName}.disableArbitraryCodeExecution`]) { + api.run.disable(); } + + if (extensionPackageJSON?.[`${extensionName}.disableArbitraryCommandExecution`]) { + api.execute.disable(); + } + + return loadCommands().then((commands) => { + if (isActivated) { + return { api, extension: (extensionState = new Extension(commands)) }; + } + return; + }); } /** * Function called by VS Code to deactivate the extension. */ export function deactivate() { - extensionState.dispose(); + isActivated = false; + extensionState?.dispose(); } + +export { api }; diff --git a/src/registers.ts b/src/registers.ts deleted file mode 100644 index c660be0..0000000 --- a/src/registers.ts +++ /dev/null @@ -1,147 +0,0 @@ -import * as vscode from "vscode"; - -import { CommandState } from "./commands"; - -export interface Register { - readonly name: string; - - canWrite(): this is WritableRegister; - get(editor: vscode.TextEditor): Thenable; -} - -export interface MacroRegister { - getMacro(): CommandState[] | undefined; - setMacro(data: CommandState[]): void; -} - -export interface WritableRegister extends Register { - set(editor: vscode.TextEditor, values: string[]): Thenable; -} - -export class GeneralPurposeRegister implements Register, WritableRegister, MacroRegister { - public values: string[] | undefined; - public macroCommands: CommandState[] | undefined; - - public canWrite() { - return !this.readonly; - } - - public constructor(public readonly name: string, public readonly readonly = false) {} - - public set(_: vscode.TextEditor, values: string[]) { - this.values = values; - - return Promise.resolve(); - } - - public get() { - return Promise.resolve(this.values); - } - - public getMacro() { - return this.macroCommands; - } - - public setMacro(data: CommandState[]) { - this.macroCommands = data; - } -} - -export class SpecialRegister implements Register { - public canWrite() { - return this.setter !== undefined; - } - - public constructor( - public readonly name: string, - public readonly getter: (editor: vscode.TextEditor) => Thenable, - public readonly setter?: (editor: vscode.TextEditor, values: string[]) => Thenable, - ) {} - - public get(editor: vscode.TextEditor) { - return this.getter(editor); - } - - public set(editor: vscode.TextEditor, values: string[]) { - if (this.setter === undefined) { - throw new Error("Cannot set read-only register."); - } - - return this.setter(editor, values); - } -} - -export class ClipboardRegister implements Register { - private lastSelections!: string[]; - private lastText!: string; - - public readonly name = '"'; - - public canWrite() { - return true; - } - - public async get() { - const text = await vscode.env.clipboard.readText(); - - return this.lastText === text ? this.lastSelections : [text]; - } - - public set(editor: vscode.TextEditor, values: string[]) { - this.lastSelections = values; - this.lastText = values.join(editor.document.eol === 1 ? "\n" : "\r\n"); - - return vscode.env.clipboard.writeText(this.lastText); - } -} - -export class Registers { - public readonly alpha: Record = {}; - - public readonly dquote = new ClipboardRegister(); - public readonly slash = new GeneralPurposeRegister("/"); - public readonly arobase = new GeneralPurposeRegister("@"); - public readonly caret = new GeneralPurposeRegister("^"); - public readonly pipe = new GeneralPurposeRegister("|"); - - public readonly percent = new SpecialRegister("%", (editor) => - Promise.resolve([editor.document.fileName]), - ); - public readonly dot = new SpecialRegister(".", (editor) => - Promise.resolve(editor.selections.map(editor.document.getText)), - ); - public readonly hash = new SpecialRegister("#", (editor) => - Promise.resolve(editor.selections.map((_, i) => i.toString())), - ); - public readonly underscore = new SpecialRegister("_", (______) => Promise.resolve([""])); - public readonly colon = new GeneralPurposeRegister(":", true); - - public get(key: string) { - switch (key) { - case '"': - return this.dquote; - case "/": - return this.slash; - case "@": - return this.arobase; - case "^": - return this.caret; - case "|": - return this.pipe; - - case "%": - return this.percent; - case ".": - return this.dot; - case "#": - return this.hash; - case "_": - return this.underscore; - case ":": - return this.colon; - - default: - return this.alpha[key] || (this.alpha[key] = new GeneralPurposeRegister(key)); - } - } -} diff --git a/src/state/document.ts b/src/state/document.ts deleted file mode 100644 index 2f4ffae..0000000 --- a/src/state/document.ts +++ /dev/null @@ -1,186 +0,0 @@ -import * as vscode from "vscode"; - -import { EditorState } from "./editor"; -import { Extension } from "./extension"; -import { CommandState, InputKind } from "../commands"; -import { assert } from "../utils/assert"; -import { SavedSelection } from "../utils/savedSelection"; - -/** - * Document-specific state. - */ -export class DocumentState { - private readonly _editorStates: EditorState[] = []; - private readonly _savedSelections: SavedSelection[] = []; - - public constructor( - /** The extension for which state is being kept. */ - public readonly extension: Extension, - - /** The editor for which state is being kept. */ - public readonly document: vscode.TextDocument, - ) {} - - /** - * Disposes of the resources owned by and of the subscriptions of this - * instance. - */ - public dispose() { - const editorStates = this._editorStates.splice(0); - - for (let i = 0, len = editorStates.length; i < len; i++) { - editorStates[i].dispose(); - } - - this._savedSelections.length = 0; - } - - /** - * Returns the `EditorState` of each known `vscode.TextEditor`, where - * `editor.document === this.document`. - */ - public editorStates() { - return this._editorStates as readonly EditorState[]; - } - - /** - * Gets the `EditorState` for the given `vscode.TextEditor`, where - * `editor.document === this.document`. - */ - public getEditorState(editor: vscode.TextEditor) { - assert(editor.document === this.document); - - const editorStates = this._editorStates, - len = editorStates.length; - - for (let i = 0; i < len; i++) { - const editorState = editorStates[i]; - - if (editorState.isFor(editor)) { - return editorState.updateEditor(editor); - } - } - - const editorState = new EditorState(this, editor); - - this._editorStates.push(editorState); - - return editorState; - } - - /** - * Called when `vscode.workspace.onDidChangeTextDocument` is triggered. - */ - public onDidChangeTextDocument(e: vscode.TextDocumentChangeEvent) { - const savedSelections = this._savedSelections; - - if (savedSelections !== undefined) { - for (let i = 0; i < savedSelections.length; i++) { - const savedSelection = savedSelections[i]; - - for (let j = 0; j < e.contentChanges.length; j++) { - savedSelection.updateAfterDocumentChanged(e.contentChanges[j]); - } - } - } - - const editorStates = this._editorStates; - - for (let i = 0; i < editorStates.length; i++) { - editorStates[i].onDidChangeTextDocument(e); - } - - this.recordChanges(e.contentChanges); - } - - // ============================================================================================= - // == SAVED SELECTIONS ======================================================================= - // ============================================================================================= - - /** - * Saves the given selection, tracking changes to the given document and - * updating the selection correspondingly over time. - */ - public saveSelection(selection: vscode.Selection) { - const anchorOffset = this.document.offsetAt(selection.anchor), - activeOffset = this.document.offsetAt(selection.active), - savedSelection = new SavedSelection(anchorOffset, activeOffset); - - this._savedSelections.push(savedSelection); - - return savedSelection; - } - - /** - * Forgets the given saved selections. - */ - public forgetSelections(selections: readonly SavedSelection[]) { - const savedSelections = this._savedSelections; - - if (savedSelections !== undefined) { - for (let i = 0; i < selections.length; i++) { - const index = savedSelections.indexOf(selections[i]); - - if (index !== -1) { - savedSelections.splice(index, 1); - } - } - } - } - - // ============================================================================================= - // == HISTORY ================================================================================ - // ============================================================================================= - - private _lastCommand?: CommandState; - private readonly _recordedChanges = [] as RecordedChange[]; - - /** - * The changes that were last made in this editor. - */ - public get recordedChanges() { - return this._recordedChanges as readonly RecordedChange[]; - } - - /** - * Adds the given changes to the history of the editor following the given - * command. - */ - private recordChanges(changes: readonly vscode.TextDocumentContentChangeEvent[]) { - this._lastCommand?.recordFollowingChanges(changes); - - const recordedChanges = this._recordedChanges; - - if (recordedChanges.length + changes.length > 100) { - recordedChanges.splice(0, recordedChanges.length + changes.length - 100); - } - - for (let i = 0, len = changes.length; i < len; i++) { - const change = changes[i], - savedSelection = new SavedSelection( - change.rangeOffset, - change.rangeOffset + change.rangeLength, - ); - - this._savedSelections.push(savedSelection); - recordedChanges.push(new RecordedChange(savedSelection, change.text)); - } - } - - /** - * Records invocation of a command. - */ - public recordCommand(state: CommandState) { - this._lastCommand = state; - } -} - -export class RecordedChange { - public constructor( - /** The range that got replaced. */ - public readonly range: SavedSelection, - - /** The new text for the range. */ - public readonly text: string, - ) {} -} diff --git a/src/state/editor.ts b/src/state/editor.ts deleted file mode 100644 index 05c1d5e..0000000 --- a/src/state/editor.ts +++ /dev/null @@ -1,584 +0,0 @@ -import * as vscode from "vscode"; - -import { DocumentState } from "./document"; -import { Mode, SelectionBehavior } from "./extension"; -import { Command, CommandState, InputKind } from "../commands"; -import { extensionName } from "../extension"; -import { assert } from "../utils/assert"; -import { SavedSelection } from "../utils/savedSelection"; -import { MacroRegister } from "../registers"; - -/** - * Editor-specific state. - */ -export class EditorState { - /** - * The internal identifir of the ID. - * - * Unlike documents, VS Code does not reuse `vscode.TextEditor` objects, - * so comparing by reference using `===` may not always return `true`, - * even for the same document. To keep editor-specific state anyway, - * we're using its internal identifier, which seems to be unique and - * to stay the same over time. - */ - private _id: string; - - /** The last matching editor. */ - private _editor: vscode.TextEditor; - - /** Selections that we had before entering insert mode. */ - private _insertModeSelections?: readonly SavedSelection[]; - - /** - * Whether a selection change event should be expected while in insert mode. - */ - private _expectSelectionChangeEvent = false; - - /** Whether the next selection change event should be ignored. */ - private _ignoreSelectionChangeEvent = false; - - /** The ongoing recording of a macro in this editor. */ - private _macroRecording?: MacroRecording; - - private _mode!: Mode; - - /** - * The mode of the editor. - */ - public get mode() { - return this._mode; - } - - /** - * The extension for which state is being kept. - */ - public get extension() { - return this.documentState.extension; - } - - /** - * The editor for which state is being kept. - */ - public get editor() { - return this._editor; - } - - /** - * Whether the editor for which state is being kept is the active text editor. - */ - public get isActive() { - return vscode.window.activeTextEditor === this._editor; - } - - public get selectionBehavior() { - return this.documentState.extension.selectionBehavior; - } - - /** - * Preferred columns when navigating up and down. - */ - public readonly preferredColumns: number[] = []; - - public constructor( - /** The state of the document for which this editor exists. */ - public readonly documentState: DocumentState, - - /** The text editor for which state is being kept. */ - editor: vscode.TextEditor, - ) { - this._id = getEditorId(editor); - this._editor = editor; - - this.setMode(Mode.Normal); - } - - /** - * Disposes of the resources owned by and of the subscriptions of this - * instance. - */ - public dispose() { - const lineNumbering = vscode.workspace.getConfiguration("editor").get("lineNumbers"), - options = this._editor.options; - - options.lineNumbers - = lineNumbering === "on" - ? vscode.TextEditorLineNumbersStyle.On - : lineNumbering === "relative" - ? vscode.TextEditorLineNumbersStyle.Relative - : lineNumbering === "interval" - ? vscode.TextEditorLineNumbersStyle.Relative + 1 - : vscode.TextEditorLineNumbersStyle.Off; - - this.clearDecorations(this.extension.normalMode.decorationType); - this.clearDecorations(this.extension.insertMode.decorationType); - this._macroRecording?.dispose(); - } - - /** - * Updates the instance of `vscode.TextEditor` for this editor. - */ - public updateEditor(editor: vscode.TextEditor) { - assert(this.isFor(editor)); - - this._editor = editor; - - return this; - } - - /** - * Returns whether this state is for the given editor. - */ - public isFor(editor: vscode.TextEditor) { - return this.documentState.document === editor.document && this._id === getEditorId(editor); - } - - /** - * Sets the mode of the editor. - */ - public setMode(mode: Mode) { - if (this._mode === mode) { - return; - } - - const { insertMode, normalMode } = this.extension, - documentState = this.documentState; - - this._mode = mode; - - if (mode === Mode.Insert) { - this.clearDecorations(normalMode.decorationType); - this.setDecorations(insertMode.decorationType); - - const selections = this.editor.selections, - documentState = this.documentState, - savedSelections = [] as SavedSelection[]; - - for (let i = 0, len = selections.length; i < len; i++) { - savedSelections.push(documentState.saveSelection(selections[i])); - } - - this._insertModeSelections = savedSelections; - this._ignoreSelectionChangeEvent = true; - - if (this.extension.insertModeSelectionStyle !== undefined) { - this.editor.setDecorations(this.extension.insertModeSelectionStyle, selections); - } - } else { - if (this._insertModeSelections !== undefined && this._insertModeSelections.length > 0) { - const savedSelections = this._insertModeSelections, - editorSelections = this._editor.selections, - document = this.documentState.document; - - assert(editorSelections.length === savedSelections.length); - - for (let i = 0, len = savedSelections.length; i < len; i++) { - editorSelections[i] = savedSelections[i].selection(document); - } - - documentState.forgetSelections(this._insertModeSelections); - - this._editor.selections = editorSelections; - this._insertModeSelections = undefined; - } - - this.clearDecorations(insertMode.decorationType); - this.clearDecorations(this.extension.insertModeSelectionStyle); - this.setDecorations(normalMode.decorationType); - - this.normalizeSelections(); - } - - if (this.isActive) { - this.onDidBecomeActive(); - } - } - - /** - * Starts recording a macro, setting up relevant handlers and UI elements. - */ - public startMacroRecording(register: MacroRegister & { readonly name: string }) { - if (this._macroRecording !== undefined) { - return undefined; - } - - const statusBarItem = vscode.window.createStatusBarItem(); - - statusBarItem.command = Command.macrosRecordStop; - statusBarItem.text = `Macro recording in ${register.name}`; - - this._macroRecording = new MacroRecording(register, this._commands.length, statusBarItem); - - return this._macroRecording.show().then(() => this._macroRecording); - } - - /** - * Stops recording a macro, disposing of its resources. - */ - public stopMacroRecording() { - const recording = this._macroRecording; - - if (recording === undefined) { - return undefined; - } - - this._macroRecording = undefined; - - return recording.dispose().then(() => recording); - } - - /** - * Called when `vscode.window.onDidChangeActiveTextEditor` is triggered with - * this editor. - */ - public onDidBecomeActive() { - const { editor, mode } = this, - modeConfiguration - = mode === Mode.Insert ? this.extension.insertMode : this.extension.normalMode; - - if (mode === Mode.Insert) { - this.extension.statusBarItem.text = "$(pencil) INSERT"; - } else if (mode === Mode.Normal) { - this.extension.statusBarItem.text = "$(beaker) NORMAL"; - } - - this._macroRecording?.show(); - - editor.options.lineNumbers = modeConfiguration.lineNumbers; - editor.options.cursorStyle = modeConfiguration.cursorStyle; - - vscode.commands.executeCommand("setContext", extensionName + ".mode", mode); - } - - /** - * Called when `vscode.window.onDidChangeActiveTextEditor` is triggered with - * another editor. - */ - public onDidBecomeInactive() { - if (this.mode === Mode.Awaiting) { - this.setMode(Mode.Normal); - } - - this._macroRecording?.hide(); - } - - /** - * Called when `vscode.window.onDidChangeTextEditorSelection` is triggered. - */ - public onDidChangeTextEditorSelection(e: vscode.TextEditorSelectionChangeEvent) { - const mode = this.mode; - - if (mode === Mode.Awaiting) { - this.setMode(Mode.Normal); - } - - // Update decorations. - if (mode === Mode.Insert) { - if (this._ignoreSelectionChangeEvent) { - this._ignoreSelectionChangeEvent = false; - - return; - } - - this.setDecorations(this.extension.insertMode.decorationType); - - // Update insert mode decorations that keep track of previous selections. - const mustDropSelections - = e.kind === vscode.TextEditorSelectionChangeKind.Command - || e.kind === vscode.TextEditorSelectionChangeKind.Mouse - || !this._expectSelectionChangeEvent; - - const selectionStyle = this.extension.insertModeSelectionStyle, - decorationRanges = [] as vscode.Range[]; - - if (mustDropSelections) { - this._insertModeSelections = []; - - if (selectionStyle !== undefined) { - this.editor.setDecorations(selectionStyle, []); - } - } else if (selectionStyle !== undefined) { - const insertModeSelections = this._insertModeSelections; - - if (insertModeSelections !== undefined) { - const document = this.documentState.document; - - for (let i = 0, len = insertModeSelections.length; i < len; i++) { - const insertModeSelection = insertModeSelections[i]; - - if (insertModeSelection.activeOffset !== insertModeSelection.anchorOffset) { - decorationRanges.push(insertModeSelection.selection(document)); - } - } - } - - this.editor.setDecorations(selectionStyle, decorationRanges); - } - } else { - this.setDecorations(this.extension.normalMode.decorationType); - } - - // Debounce normalization. - if (this.normalizeTimeoutToken !== undefined) { - clearTimeout(this.normalizeTimeoutToken); - this.normalizeTimeoutToken = undefined; - } - - if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) { - this.normalizeTimeoutToken = setTimeout(() => { - this.normalizeSelections(); - this.normalizeTimeoutToken = undefined; - }, 200); - } else { - this.normalizeSelections(); - } - } - - /** - * Called when `vscode.workspace.onDidChangeTextDocument` is triggered on the - * document of the editor. - */ - public onDidChangeTextDocument(e: vscode.TextDocumentChangeEvent) { - if (this._mode === Mode.Insert) { - const changes = e.contentChanges; - - if (this.editor.selections.length !== changes.length) { - return; - } - - // Find all matching selections for the given changes. - // If all selections have a match, we can continue. - const remainingSelections = new Set(this.editor.selections); - - for (let i = 0, len = changes.length; i < len; i++) { - const change = changes[i]; - - for (const selection of remainingSelections) { - if ( - selection.active.isEqual(change.range.start) - || selection.active.isEqual(change.range.end) - ) { - remainingSelections.delete(selection); - - break; - } - } - - if (remainingSelections.size !== len - i - 1) { - return; - } - } - - this._expectSelectionChangeEvent = true; - - setImmediate(() => (this._expectSelectionChangeEvent = false)); - } - } - - // ============================================================================================= - // == DECORATIONS ============================================================================ - // ============================================================================================= - - private clearDecorations(decorationType: vscode.TextEditorDecorationType | undefined) { - if (decorationType !== undefined) { - this._editor.setDecorations(decorationType, []); - } - } - - public setDecorations(decorationType: vscode.TextEditorDecorationType | undefined) { - if (decorationType === undefined) { - return; - } - - const editor = this._editor, - selection = editor.selection, - extension = this.extension; - - if ( - selection.end.character === 0 - && selection.end.line > 0 - && extension.selectionBehavior === SelectionBehavior.Character - ) { - editor.setDecorations(decorationType, [ - new vscode.Range(selection.start, selection.end.with(selection.end.line - 1, 0)), - ]); - editor.options.cursorStyle = vscode.TextEditorCursorStyle.LineThin; - } else { - editor.setDecorations(decorationType, [selection]); - editor.options.cursorStyle - = this.mode === Mode.Insert - ? extension.insertMode.cursorStyle - : extension.normalMode.cursorStyle; - } - } - - // ============================================================================================= - // == HISTORY ================================================================================ - // ============================================================================================= - - private readonly _commands = [] as CommandState[]; - - /** - * The commands that were last used in this editor, from the earliest to the - * latest. - */ - public get recordedCommands() { - return this._commands as readonly CommandState[]; - } - - /** - * Records invocation of a command. - */ - public recordCommand(state: CommandState) { - this.documentState.recordCommand(state); - this._commands.push(state); - - if (this._macroRecording) { - if (this._commands.length === 50) { - vscode.window.showWarningMessage( - "You're recording a lot of commands. This may increase memory usage.", - ); - } - } else { - // If not recording, limit history to 20 items to avoid eating RAM. - while (this._commands.length > 20) { - this._commands.shift(); - } - } - } - - // ============================================================================================= - // == SELECTION NORMALIZATION ================================================================ - // ============================================================================================= - - private normalizeTimeoutToken: NodeJS.Timeout | undefined = undefined; - - /** - * Whether selection changes should be ignored, therefore not automatically - * normalizing selections. - */ - public ignoreSelectionChanges = false; - - /** - * Make all selections in the editor non-empty by selecting at least one - * character. - */ - public normalizeSelections() { - if ( - this._mode !== Mode.Normal - || this.extension.selectionBehavior === SelectionBehavior.Caret - || this.ignoreSelectionChanges - ) { - return; - } - - const editor = this._editor; - - // Since this is called every time when selection changes, avoid allocations - // unless really needed and iterate manually without using helper functions. - let normalizedSelections: vscode.Selection[] | undefined = undefined; - - for (let i = 0; i < editor.selections.length; i++) { - const selection = editor.selections[i]; - const isReversedOneCharacterSelection = selection.isSingleLine - ? selection.anchor.character === selection.active.character + 1 - : selection.anchor.character === 0 - && selection.anchor.line === selection.active.line + 1 - && editor.document.lineAt(selection.active).text.length === selection.active.character; - - if (isReversedOneCharacterSelection) { - if (normalizedSelections === undefined) { - // Change needed. Allocate the new array and copy what we have so far. - normalizedSelections = editor.selections.slice(0, i); - } - - normalizedSelections.push(new vscode.Selection(selection.active, selection.anchor)); - } else if (selection.isEmpty) { - if (normalizedSelections === undefined) { - // Change needed. Allocate the new array and copy what we have so far. - normalizedSelections = editor.selections.slice(0, i); - } - - const active = selection.active; - - if (active.character >= editor.document.lineAt(active.line).range.end.character) { - // Selection is at line end. Select line break. - if (active.line === editor.document.lineCount - 1) { - // Selection is at the very end of the document as well. Select the - // last character instead. - if (active.character === 0) { - if (active.line === 0) { - // There is no character in this document, so we give up on - // normalizing. - continue; - } else { - normalizedSelections.push( - new vscode.Selection( - new vscode.Position(active.line - 1, Number.MAX_SAFE_INTEGER), - active, - ), - ); - } - } else { - normalizedSelections.push(new vscode.Selection(active.translate(0, -1), active)); - } - } else { - normalizedSelections.push( - new vscode.Selection(active, new vscode.Position(active.line + 1, 0)), - ); - } - } else { - const offset = editor.document.offsetAt(selection.active); - const nextPos = editor.document.positionAt(offset + 1); - - if (nextPos.isAfter(selection.active)) { - // Move cursor forward. - normalizedSelections.push(new vscode.Selection(active, active.translate(0, 1))); - } else { - // Selection is at the very end of the document. Select the last - // character instead. - normalizedSelections.push(new vscode.Selection(active.translate(0, -1), active)); - } - } - } else if (normalizedSelections !== undefined) { - normalizedSelections.push(selection); - } - } - - if (normalizedSelections !== undefined) { - editor.selections = normalizedSelections; - } - } -} - -function getEditorId(editor: vscode.TextEditor) { - return ((editor as unknown) as { readonly id: string }).id; -} - -/** - * An ongoing recording of a macro. - */ -export class MacroRecording { - public constructor( - public readonly register: MacroRegister, - public lastHistoryEntry: number, - public readonly statusBarItem: vscode.StatusBarItem, - ) {} - - public show() { - this.statusBarItem.show(); - - return vscode.commands.executeCommand("setContext", extensionName + ".recordingMacro", true); - } - - public hide() { - this.statusBarItem.hide(); - - return vscode.commands.executeCommand("setContext", extensionName + ".recordingMacro", false); - } - - public dispose() { - this.statusBarItem.dispose(); - - return vscode.commands.executeCommand("setContext", extensionName + ".recordingMacro", false); - } -} diff --git a/src/state/editors.ts b/src/state/editors.ts new file mode 100644 index 0000000..8c0347b --- /dev/null +++ b/src/state/editors.ts @@ -0,0 +1,590 @@ +import * as vscode from "vscode"; + +import { assert, command, commands, Context, Positions, Selections, selectionsLines } from "../api"; +import { extensionName } from "../extension"; +import { Extension } from "./extension"; +import { Mode, SelectionBehavior } from "./modes"; + +/** + * Dance-specific state related to a single `vscode.TextEditor`. + */ +export class PerEditorState implements vscode.Disposable { + private readonly _onEditorWasClosed = new vscode.EventEmitter(); + private readonly _onVisibilityDidChange = new vscode.EventEmitter(); + private _isVisible = true; + private _mode!: Mode; + private _modeChangeSubscription!: vscode.Disposable; + + /** + * The corresponding visible `vscode.TextEditor`. + */ + public get editor() { + return this._editor; + } + + /** + * The current mode of the editor. + */ + public get mode() { + return this._mode; + } + + /** + * Whether the editor for which state is being kept is the active text editor. + */ + public get isActive() { + return vscode.window.activeTextEditor === this._editor; + } + + /** + * Whether the editor for which state is being kept is visible. + */ + public get isVisible() { + return this._isVisible; + } + + /** + * An event which fires when the `editor` is permanently closed. + */ + public get onEditorWasClosed() { + return this._onEditorWasClosed.event; + } + + /** + * An event which fires when the `editor` becomes visible or hidden. Read + * `isVisible` to find the new value. + */ + public get onVisibilityDidChange() { + return this._onVisibilityDidChange.event; + } + + public constructor( + public readonly extension: Extension, + private _editor: vscode.TextEditor, + mode: Mode, + ) { + for (let i = 0; i < PerEditorState._registeredStates.length; i++) { + this._storage.push(undefined); + } + + this.setMode(mode); + } + + public dispose() { + const options = this._editor.options, + mode = this._mode, + vscodeMode = mode.modes.vscodeMode; + + options.cursorStyle = vscodeMode.cursorStyle; + options.lineNumbers = vscodeMode.lineNumbers; + + if (this._isVisible) { + this._isVisible = false; + this._onVisibilityDidChange.fire(this); + } + + this._onEditorWasClosed.fire(this); + + this._onEditorWasClosed.dispose(); + this._onVisibilityDidChange.dispose(); + + this._modeChangeSubscription.dispose(); + + for (let i = 0; i < this._storage.length; i++) { + if (PerEditorState._registeredStates[i]) { + (this._storage[i] as vscode.Disposable | undefined)?.dispose(); + } + } + } + + // Storage. + // ============================================================================================= + + private static readonly _registeredStates: boolean[] = []; + + /** + * Returns a `Token` that can later be used to store editor-specific data. + */ + public static registerState(isDisposable: T extends vscode.Disposable ? true : false) { + return this._registeredStates.push(isDisposable) - 1 as unknown as PerEditorState.Token; + } + + private readonly _storage: unknown[] = []; + + /** + * Returns the object assigned to the given token. + */ + public get(token: PerEditorState.Token) { + return this._storage[token as unknown as number] as T | undefined; + } + + /** + * Stores a value that is related to the editor for which the state is kept. + */ + public store(token: PerEditorState.Token, value: T | undefined) { + const previousValue = this._storage[token as unknown as number]; + + this._storage[token as unknown as number] = value; + + return previousValue as T | undefined; + } + + // Changing modes. + // ============================================================================================= + + /** + * Whether the editor is currently executing functions to change modes. + */ + private _isChangingMode = false; + + /** + * Sets the mode of the editor. + */ + public async setMode(mode: Mode) { + if (this._isChangingMode) { + throw new Error("calling EditorState.setMode in a mode change handler is forbidden"); + } + + if (this._mode === mode) { + return; + } + + this._isChangingMode = true; + + const previousMode = this._mode; + + if (previousMode !== undefined) { + this._modeChangeSubscription.dispose(); + this._clearDecorations(previousMode); + + await this._runCommands(previousMode.onLeaveMode, (e) => + `error trying to execute onLeaveMode commands for mode ${ + JSON.stringify(previousMode.name)}: ${e}`, + ); + + if (previousMode.selectionBehavior !== mode.selectionBehavior) { + this._updateSelectionsAfterBehaviorChange(mode); + } + } + + this._mode = mode; + this._modeChangeSubscription = mode.onChanged(([mode, props]) => { + for (const prop of props) { + switch (prop) { + case "cursorStyle": + this._editor.options.cursorStyle = mode.cursorStyle; + break; + + case "lineNumbers": + this._editor.options.lineNumbers = mode.lineNumbers; + break; + + case "decorations": + case "lineHighlight": + case "selectionDecorationType": + this._updateDecorations(mode); + break; + + case "selectionBehavior": + this._updateSelectionsAfterBehaviorChange(mode); + break; + } + } + }); + this._updateDecorations(mode); + + await this._runCommands(mode.onEnterMode, (e) => + `error trying to execute onEnterMode commands for mode ${JSON.stringify(mode.name)}: ${e}`, + ); + + if (this.isActive) { + await this.notifyDidBecomeActive(); + } + + this._isChangingMode = false; + + this.extension.editors.notifyDidChangeMode(this); + } + + private _runCommands( + commandsToRun: readonly command.Any[], + error: (e: any) => string, + ) { + const context = new Context(this, this.extension.cancellationToken); + + return this.extension.runPromiseSafely( + () => context.run(() => commands(...commandsToRun)), + () => undefined, + error, + ); + } + + // Externally-triggered events. + // ============================================================================================= + + /** + * Called when `vscode.window.onDidChangeActiveTextEditor` is triggered with + * this editor. + * + * @deprecated Do not call -- internal implementation detail. + */ + public notifyDidBecomeActive() { + const { editor, mode } = this; + + this.extension.statusBar.activeModeSegment.setContent(mode.name); + + editor.options.lineNumbers = mode.lineNumbers; + editor.options.cursorStyle = mode.cursorStyle; + + return vscode.commands.executeCommand("setContext", extensionName + ".mode", mode.name); + } + + /** + * Called when `vscode.window.onDidChangeActiveTextEditor` is triggered with + * another editor. + * + * @deprecated Do not call -- internal implementation detail. + */ + public notifyDidBecomeInactive(newEditorIsActive: boolean) { + if (!newEditorIsActive) { + this.extension.statusBar.activeModeSegment.setContent(""); + + return vscode.commands.executeCommand("setContext", extensionName + ".mode", undefined); + } + + return Promise.resolve(); + } + + /** + * Called when `vscode.window.onDidChangeTextEditorSelection` is triggered on + * this editor. + * + * @deprecated Do not call -- internal implementation detail. + */ + public notifyDidChangeTextEditorSelection() { + this._updateDecorations(this._mode); + } + + /** + * Called when the editor becomes visible again. + * + * @deprecated Do not call -- internal implementation detail. + */ + public notifyDidBecomeVisible(editor: vscode.TextEditor) { + assert(this._editor.document === editor.document); + + this._editor = editor; + this._isVisible = true; + this._onVisibilityDidChange.fire(this); + } + + /** + * Called when the editor was hidden, but not closed. + * + * @deprecated Do not call -- internal implementation detail. + */ + public notifyDidBecomeHidden() { + this._isVisible = false; + this._onVisibilityDidChange.fire(this); + } + + // Updating decorations. + // ============================================================================================= + + private _clearDecorations(mode: Mode) { + const editor = this._editor, + empty = [] as never[]; + + for (const decoration of mode.decorations) { + editor.setDecorations(decoration.type, empty); + } + + editor.setDecorations(this.extension.editors.characterDecorationType, empty); + } + + private _updateDecorations(mode: Mode) { + const editor = this._editor, + allSelections = editor.selections; + + for (const decoration of mode.decorations) { + const selections = + decoration.applyTo === "all" + ? allSelections + : decoration.applyTo === "main" + ? [allSelections[0]] + : allSelections.slice(1); + + if (decoration.renderOptions.isWholeLine) { + const lines = selectionsLines(selections), + ranges: vscode.Range[] = []; + + for (let i = 0, len = lines.length; i < len; i++) { + const startLine = lines[i]; + let endLine = startLine; + + while (i + 1 < lines.length && lines[i + 1] === endLine + 1) { + i++; + endLine++; + } + + const start = new vscode.Position(startLine, 0), + end = startLine === endLine ? start : new vscode.Position(endLine, 0); + + ranges.push(new vscode.Range(start, end)); + } + + editor.setDecorations(decoration.type, ranges); + } else { + editor.setDecorations(decoration.type, selections); + } + } + + if (mode.selectionBehavior === SelectionBehavior.Character) { + const document = this._editor.document, + ranges = [] as vscode.Range[]; + + for (let i = 0; i < allSelections.length; i++) { + const selection = allSelections[i]; + + if (!selection.isEmpty) { + const end = Positions.next(selection.active, document); + + if (end !== undefined) { + const active = selection.active, + start = active.character === 0 || active === selection.start + ? active + : new vscode.Position(active.line, active.character - 1); + + ranges.push(new vscode.Range(start, end)); + } + } + } + + editor.setDecorations(this.extension.editors.characterDecorationType, ranges); + } else { + editor.setDecorations(this.extension.editors.characterDecorationType, []); + } + + editor.options.cursorStyle = mode.cursorStyle; + editor.options.lineNumbers = mode.lineNumbers; + } + + private _updateSelectionsAfterBehaviorChange(mode: Mode) { + const editor = this._editor, + document = editor.document, + selections = editor.selections; + + editor.selections = mode.selectionBehavior === SelectionBehavior.Character + ? Selections.toCharacterMode(selections, document) + : Selections.fromCharacterMode(selections, document); + } +} + +export namespace PerEditorState { + export declare class Token { + private can_never_implement_this: never; + } +} + +/** + * The set of all known editors, and their associated `PerEditorState`. + */ +export class Editors implements vscode.Disposable { + private readonly _editors = new Map(); + private readonly _fallbacks = new Map(); + private readonly _onModeDidChange = new vscode.EventEmitter(); + private readonly _subscriptions: vscode.Disposable[] = []; + private _activeEditor?: PerEditorState; + + /** + * @deprecated Do not access -- internal implementation detail. + */ + public readonly characterDecorationType = vscode.window.createTextEditorDecorationType({ + backgroundColor: new vscode.ThemeColor("editor.selectionBackground"), + }); + + /** + * The Dance-specific state for the active `vscode.TextEditor`, or `undefined` + * if `vscode.window.activeTextEditor === undefined`. + */ + public get active() { + return this._activeEditor; + } + + /** + * An event which fires on editor mode change. + */ + public readonly onModeDidChange = this._onModeDidChange.event; + + public constructor( + private readonly _extension: Extension, + ) { + vscode.window.onDidChangeActiveTextEditor( + this._handleDidChangeActiveTextEditor, this, this._subscriptions); + vscode.window.onDidChangeTextEditorSelection( + this._handleDidChangeTextEditorSelection, this, this._subscriptions); + vscode.window.onDidChangeVisibleTextEditors( + this._handleDidChangeVisibleTextEditors, this, this._subscriptions); + vscode.workspace.onDidCloseTextDocument( + this._handleDidCloseTextDocument, this, this._subscriptions); + + process.nextTick(() => { + this._handleDidChangeVisibleTextEditors(vscode.window.visibleTextEditors); + + const activeTextEditor = vscode.window.activeTextEditor; + + if (activeTextEditor !== undefined) { + this._activeEditor = this._editors.get(activeTextEditor); + this._activeEditor?.notifyDidBecomeActive(); + } + }); + } + + public dispose() { + this._subscriptions.splice(0).forEach((d) => d.dispose()); + this.characterDecorationType.dispose(); + } + + /** + * Returns the Dance-specific state for the given `vscode.TextEditor`. + */ + public getState(editor: vscode.TextEditor) { + const state = this._editors.get(editor); + + if (state === undefined) { + throw new Error( + "given editor does not have an equivalent EditorState; has it gone out of view?", + ); + } + + return state; + } + + private _handleDidChangeActiveTextEditor(e: vscode.TextEditor | undefined) { + if (e === undefined) { + this._activeEditor?.notifyDidBecomeInactive(false); + this._activeEditor = undefined; + } else { + // Note that the call to `get` below requires that visible editors are + // updated via `onDidChangeVisibleTextEditors`. Thankfully + // `onDidChangeActiveTextEditor` is indeed triggered *after* that + // event. + this._activeEditor?.notifyDidBecomeInactive(true); + this._activeEditor = this._editors.get(e); + this._activeEditor?.notifyDidBecomeActive(); + } + } + + private _handleDidChangeTextEditorSelection(e: vscode.TextEditorSelectionChangeEvent) { + this._editors.get(e.textEditor)?.notifyDidChangeTextEditorSelection(); + } + + private _handleDidChangeVisibleTextEditors(visibleEditors: readonly vscode.TextEditor[]) { + const hiddenEditors = new Map(this._editors), + addedEditors = new Set(); + + for (const visibleEditor of visibleEditors) { + if (!hiddenEditors.delete(visibleEditor)) { + // `visibleEditor` had not been previously added to `_editors`, so it's + // a new editor. + addedEditors.add(visibleEditor); + } + } + + // Now `hiddenEditors` only contains editors that are no longer visible, and + // `addedEditors` only contains editors that were not visible previously. + const defaultMode = this._extension.modes.defaultMode; + + for (const addedEditor of addedEditors) { + const fallback = this._fallbacks.get(addedEditor.document); + + if (fallback !== undefined) { + fallback.notifyDidBecomeVisible(addedEditor); + this._editors.set(addedEditor, fallback); + this._fallbacks.delete(addedEditor.document); + } else { + this._editors.set( + addedEditor, new PerEditorState(this._extension, addedEditor, defaultMode)); + } + } + + // Dispose of no-longer-visible editors. + const addedFallbacks = new Set(); + + for (const [hiddenEditor, state] of hiddenEditors) { + this._editors.delete(hiddenEditor); + + // As soon as editors go out of view, their related `vscode.TextEditor` + // instances are made obsolete. However, we'd still like to keep state + // like active mode and selections in case the user reopens that editor + // shortly. + const fallback = this._fallbacks.get(hiddenEditor.document); + + if (fallback === undefined) { + this._fallbacks.set(hiddenEditor.document, state); + addedFallbacks.add(hiddenEditor.document); + } else if (isMoreInteresting(fallback.editor, hiddenEditor)) { + fallback.dispose(); + this._fallbacks.set(hiddenEditor.document, state); + addedFallbacks.add(hiddenEditor.document); + } else { + state.dispose(); + } + } + + for (const addedFallback of addedFallbacks) { + this._fallbacks.get(addedFallback)!.notifyDidBecomeHidden(); + } + } + + private _handleDidCloseTextDocument(document: vscode.TextDocument) { + // Dispose of fallback editor, if any. + const fallback = this._fallbacks.get(document); + + if (fallback !== undefined) { + this._fallbacks.delete(document); + fallback.dispose(); + } else { + // There is no fallback editor, so there might be visible editors related + // to that document. + for (const editor of vscode.window.visibleTextEditors) { + if (editor.document === document) { + const state = this._editors.get(editor); + + if (state !== undefined) { + this._editors.delete(editor); + state.dispose(); + } + } + } + } + } + + /** + * @deprecated Do not call -- internal implementation detail. + */ + public notifyDidChangeMode(state: PerEditorState) { + this._onModeDidChange.fire(state); + } +} + +function getTotalRange(editor: vscode.TextEditor) { + let minStart = editor.document.lineCount - 1, + maxEnd = 0; + + for (const range of editor.visibleRanges) { + minStart = Math.min(range.start.line, minStart); + maxEnd = Math.max(range.end.line, maxEnd); + } + + return maxEnd - minStart; +} + +function isMoreInteresting( + currentEditor: vscode.TextEditor, + potentiallyMoreInteresting: vscode.TextEditor, +) { + // Compute the total range of each editor; the "most interesting editor" is + // the one with the greatest total range. + return getTotalRange(currentEditor) < getTotalRange(potentiallyMoreInteresting); +} diff --git a/src/state/extension.ts b/src/state/extension.ts index a5c2b51..b76453d 100644 --- a/src/state/extension.ts +++ b/src/state/extension.ts @@ -1,439 +1,181 @@ import * as vscode from "vscode"; -import { DocumentState } from "./document"; -import { EditorState } from "./editor"; -import { commands } from "../commands"; import { extensionName } from "../extension"; -import { Register, Registers } from "../registers"; - -// ============================================================================================= -// == MODE-SPECIFIC CONFIGURATION ============================================================ -// ============================================================================================= - -export const enum Mode { - Normal = "normal", - Insert = "insert", - - Awaiting = "awaiting", -} - -export const enum SelectionBehavior { - Caret = 1, - Character = 2, -} - -export namespace ModeConfiguration { - export type CursorStyle = - | "line" - | "block" - | "underline" - | "line-thin" - | "block-outline" - | "underline-thin" - | "inherit"; - export type LineNumbers = "on" | "off" | "relative" | "inherit"; -} - -export interface GotoMenuItem { - readonly text: string; - readonly command: string; - readonly args?: any[]; -} - -export interface GotoMenu { - readonly items: Record; -} - -/** - * Mode-specific configuration. - */ -export class ModeConfiguration { - private constructor( - public readonly mode: Mode, - public readonly modePrefix: string, - - public lineNumbers: vscode.TextEditorLineNumbersStyle, - public cursorStyle: vscode.TextEditorCursorStyle, - public decorationType?: vscode.TextEditorDecorationType, - ) {} - - public static insert() { - return new ModeConfiguration( - Mode.Insert, - "insertMode", - - vscode.TextEditorLineNumbersStyle.On, - vscode.TextEditorCursorStyle.Line, - ); - } - - public static normal() { - return new ModeConfiguration( - Mode.Normal, - "normalMode", - - vscode.TextEditorLineNumbersStyle.Relative, - vscode.TextEditorCursorStyle.Line, - ); - } - - public observeLineHighlightPreference(extension: Extension, defaultValue: string | null) { - extension.observePreference( - this.modePrefix + ".lineHighlight", - defaultValue, - (value) => { - this.decorationType?.dispose(); - - if (value === null || value.length === 0) { - return (this.decorationType = undefined); - } - - this.decorationType = vscode.window.createTextEditorDecorationType({ - backgroundColor: value[0] === "#" ? value : new vscode.ThemeColor(value), - isWholeLine: true, - }); - - for (const editor of extension.editorStates()) { - if (editor.mode === this.mode && editor.isActive) { - editor.setDecorations(this.decorationType); - } - } - - return; - }, - true, - ); - } - - public observeLineNumbersPreference( - extension: Extension, - defaultValue: ModeConfiguration.LineNumbers, - ) { - extension.observePreference( - this.modePrefix + ".lineNumbers", - defaultValue, - (value) => { - this.lineNumbers = this.lineNumbersStringToLineNumbersStyle(value); - }, - true, - ); - } - - public updateLineNumbers(extension: Extension, defaultValue: ModeConfiguration.LineNumbers) { - this.lineNumbers = this.lineNumbersStringToLineNumbersStyle( - extension.configuration.get(this.modePrefix + ".lineNumbers") ?? defaultValue, - ); - } - - public observeCursorStylePreference( - extension: Extension, - defaultValue: ModeConfiguration.CursorStyle, - ) { - extension.observePreference( - this.modePrefix + ".cursorStyle", - defaultValue, - (value) => { - this.cursorStyle = this.cursorStyleStringToCursorStyle(value); - }, - true, - ); - } - - public updateCursorStyle(extension: Extension, defaultValue: ModeConfiguration.CursorStyle) { - this.cursorStyle = this.cursorStyleStringToCursorStyle( - extension.configuration.get(this.modePrefix + ".cursorStyle") ?? defaultValue, - ); - } - - private lineNumbersStringToLineNumbersStyle(lineNumbers: ModeConfiguration.LineNumbers) { - switch (lineNumbers) { - case "on": - return vscode.TextEditorLineNumbersStyle.On; - case "off": - return vscode.TextEditorLineNumbersStyle.Off; - case "relative": - return vscode.TextEditorLineNumbersStyle.Relative; - case "inherit": - default: - const vscodeLineNumbers = vscode.workspace - .getConfiguration() - .get("editor.lineNumbers", "on"); - - switch (vscodeLineNumbers) { - case "on": - return vscode.TextEditorLineNumbersStyle.On; - case "off": - return vscode.TextEditorLineNumbersStyle.Off; - case "relative": - return vscode.TextEditorLineNumbersStyle.Relative; - case "interval": // This is a real option but its not in vscode.d.ts - return 3; - default: - return vscode.TextEditorLineNumbersStyle.On; - } - } - } - - private cursorStyleStringToCursorStyle(cursorStyle: ModeConfiguration.CursorStyle) { - switch (cursorStyle) { - case "block": - return vscode.TextEditorCursorStyle.Block; - case "block-outline": - return vscode.TextEditorCursorStyle.BlockOutline; - case "line": - return vscode.TextEditorCursorStyle.Line; - case "line-thin": - return vscode.TextEditorCursorStyle.LineThin; - case "underline": - return vscode.TextEditorCursorStyle.Underline; - case "underline-thin": - return vscode.TextEditorCursorStyle.UnderlineThin; - - case "inherit": - default: - const vscodeCursorStyle = vscode.workspace - .getConfiguration() - .get("editor.cursorStyle", "line"); - - switch (vscodeCursorStyle) { - case "block": - return vscode.TextEditorCursorStyle.Block; - case "block-outline": - return vscode.TextEditorCursorStyle.BlockOutline; - case "line": - return vscode.TextEditorCursorStyle.Line; - case "line-thin": - return vscode.TextEditorCursorStyle.LineThin; - case "underline": - return vscode.TextEditorCursorStyle.Underline; - case "underline-thin": - return vscode.TextEditorCursorStyle.UnderlineThin; - default: - return vscode.TextEditorCursorStyle.Line; - } - } - } -} +import { Register, Registers } from "./registers"; +import { assert, CancellationError, Menu, validateMenu } from "../api"; +import { Modes } from "./modes"; +import { SettingsValidator } from "../utils/settings-validator"; +import { Recorder } from "./recorder"; +import { Commands } from "../commands"; +import { AutoDisposable } from "../utils/disposables"; +import { StatusBar } from "./status-bar"; +import { Editors } from "./editors"; // =============================================================================================== // == EXTENSION ================================================================================ // =============================================================================================== -function showValidationError(message: string) { - vscode.window.showErrorMessage(`Configuration error: ${message}`); -} - /** * Global state of the extension. */ export class Extension implements vscode.Disposable { - // Events. - private readonly configurationChangeHandlers = new Map void>(); - private readonly subscriptions: vscode.Disposable[] = []; + // Misc. + private readonly _configurationChangeHandlers = new Map void>(); + private readonly _subscriptions: vscode.Disposable[] = []; // Configuration. - private readonly _gotoMenus = new Map(); - private _selectionBehavior = SelectionBehavior.Caret; + // ========================================================================== - public configuration = vscode.workspace.getConfiguration(extensionName); - - public get selectionBehavior() { - return this._selectionBehavior; - } + private readonly _gotoMenus = new Map(); public get menus() { - return this._gotoMenus as ReadonlyMap; + return this._gotoMenus as ReadonlyMap; } - // General state. - public readonly statusBarItem: vscode.StatusBarItem; - - public enabled: boolean = false; + // State. + // ========================================================================== /** - * The `CancellationTokenSource` for cancellable operations running in this - * editor. + * `StatusBar` for this instance of the extension. */ - public cancellationTokenSource?: vscode.CancellationTokenSource; + public readonly statusBar = new StatusBar(); /** * `Registers` for this instance of the extension. */ public readonly registers = new Registers(); - // Mode-specific configuration. - public readonly insertMode = ModeConfiguration.insert(); - public readonly normalMode = ModeConfiguration.normal(); + /** + * `Modes` for this instance of the extension. + */ + public readonly modes = new Modes(this); - public insertModeSelectionStyle?: vscode.TextEditorDecorationType; + /** + * `Recorder` for this instance of the extension. + */ + public readonly recorder = new Recorder(this.statusBar); + + /** + * `Editors` for this instance of the extension. + */ + public readonly editors = new Editors(this); // Ephemeral state needed by commands. - public currentCount: number = 0; - public currentRegister: Register | undefined = undefined; + // ========================================================================== - public constructor() { - this.statusBarItem = vscode.window.createStatusBarItem(undefined, 100); - this.statusBarItem.tooltip = "Current mode"; + private _currentCount = 0; + private _currentRegister?: Register; - // This needs to be before setEnabled for normalizing selections on start. - this.observePreference<"caret" | "character">( - "selectionBehavior", - "caret", - (value) => { - this._selectionBehavior - = value === "caret" ? SelectionBehavior.Caret : SelectionBehavior.Character; - }, - true, - ); + /** + * The counter for the next command. + */ + public get currentCount() { + return this._currentCount; + } - // Configuration: line highlight. - this.insertMode.observeLineHighlightPreference(this, null); - this.normalMode.observeLineHighlightPreference(this, "editor.hoverHighlightBackground"); + public set currentCount(count: number) { + this._currentCount = count; - // Configuration: line numbering. - this.insertMode.observeLineNumbersPreference(this, "inherit"); - this.normalMode.observeLineNumbersPreference(this, "relative"); + if (count !== 0) { + this.statusBar.countSegment.setContent(count.toString()); + } else { + this.statusBar.countSegment.setContent(); + } + } - this.configurationChangeHandlers.set("editor.lineNumbers", () => { - this.insertMode.updateLineNumbers(this, "inherit"); - this.normalMode.updateLineNumbers(this, "relative"); - }); + /** + * The register to use in the next command. + */ + public get currentRegister() { + return this._currentRegister; + } - // Configuration: cursor style. - this.insertMode.observeCursorStylePreference(this, "inherit"); - this.normalMode.observeCursorStylePreference(this, "inherit"); + public set currentRegister(register: Register | undefined) { + this._currentRegister = register; - this.configurationChangeHandlers.set("editor.cursorStyle", () => { - this.insertMode.updateCursorStyle(this, "inherit"); - this.normalMode.updateCursorStyle(this, "inherit"); - }); - - // Configuration: selection style. - this.observePreference>( - "insertMode.selectionStyle", - {}, - (value) => { - if (typeof value !== "object" || value === null) { - return; - } - - for (const key in value) { - const val = value[key]; - - if (typeof val !== "string") { - return; - } - if (val.startsWith("$")) { - value[key] = new vscode.ThemeColor(val.substr(1)); - } - } - - this.insertModeSelectionStyle?.dispose(); - this.insertModeSelectionStyle = vscode.window.createTextEditorDecorationType(value); - }, - true, - ); + if (register !== undefined) { + this.statusBar.registerSegment.setContent(register.name); + } else { + this.statusBar.registerSegment.setContent(); + } + } + public constructor(public readonly commands: Commands) { // Configuration: menus. - this.observePreference }>>( - "menus", - {}, - (value) => { + this.observePreference>( + ".menus", + (value, validator, inspect) => { this._gotoMenus.clear(); if (typeof value !== "object" || value === null) { - return showValidationError(`"${extensionName}.menus" must be an object.`); + validator.reportInvalidSetting("must be an object"); + return; } for (const menuName in value) { const menu = value[menuName], - builtMenu: GotoMenu = { items: {} }, - menuDisplay = `${extensionName}.menus[${JSON.stringify(menuName)}]`; + validationErrors = validateMenu(menu); - if (typeof menu !== "object" || menu === null) { - showValidationError(`menu ${menuDisplay} must be an object.`); - continue; - } - if (typeof menu.items !== "object" || Object.keys(menu.items ?? {}).length < 2) { - showValidationError( - `menu ${menuDisplay} must have an subobject "items" with at least two entries.`, - ); - continue; - } + if (validationErrors.length === 0) { + const globalConfig = inspect.globalValue?.[menuName], + defaultConfig = inspect.defaultValue?.[menuName]; - const seenKeyCodes = new Map(); - - for (const key in menu.items) { - const item = menu.items[key], - itemDisplay = `${JSON.stringify(key)} of ${menuDisplay}`; - - if (item === null) { - continue; - } - - if (typeof item !== "object") { - showValidationError(`item ${itemDisplay} must be an object.`); - continue; - } - if (typeof item.text !== "string" || item.text.length === 0) { - showValidationError(`item ${itemDisplay} must have a non-empty "text" property.`); - continue; - } - if (typeof item.command !== "string" || item.command.length === 0) { - showValidationError(`item ${itemDisplay} must have a non-empty "command" property.`); - continue; - } - if (key.length === 0) { - showValidationError(`item ${itemDisplay} must be a non-empty string key.`); - continue; - } - - let keyString = ""; - - for (let i = 0; i < key.length; i++) { - const keyCode = key.charCodeAt(i), - prevKey = seenKeyCodes.get(keyCode); - - if (prevKey) { - showValidationError(`menu ${menuDisplay} has duplicate key '${key[i]}' ` - + `(specified by '${prevKey}' and '${key}').`); - continue; + if (globalConfig !== undefined || defaultConfig !== undefined) { + // Menu is a global menu; make sure that the local workspace does + // not override its items. + for (const key in menu.items) { + if (globalConfig !== undefined && key in globalConfig.items) { + menu.items[key] = globalConfig.items[key]; + } else if (defaultConfig !== undefined && key in defaultConfig.items) { + menu.items[key] = defaultConfig.items[key]; + } } - - seenKeyCodes.set(keyCode, key); - keyString = keyString === "" ? key[i] : `${keyString}, ${key[i]}`; } - if (keyString.length === 0) { - continue; + this._gotoMenus.set(menuName, menu); + } else { + validator.enter(menuName); + + for (const error of validationErrors) { + validator.reportInvalidSetting(error); } - builtMenu.items[keyString] = { - command: item.command, - text: item.text, - args: item.args, - }; - } - - if (Object.keys(builtMenu.items).length > 0) { - this._gotoMenus.set(menuName, builtMenu); + validator.leave(); } } }, true, ); - // Lastly, enable the extension and set up modes. - this.setEnabled(this.configuration.get("enabled", true), false); + this._subscriptions.push( + // Update configuration automatically. + vscode.workspace.onDidChangeConfiguration((e) => { + for (const [section, handler] of this._configurationChangeHandlers.entries()) { + if (e.affectsConfiguration(section)) { + handler(); + } + } + }), + ); + + // Register all commands. + for (const descriptor of Object.values(commands)) { + this._subscriptions.push(descriptor.register(this)); + } } /** * Disposes of the extension and all of its resources and subscriptions. */ public dispose() { - this.cancellationTokenSource?.cancel(); - this.setEnabled(false, false); - this.statusBarItem.dispose(); + this._cancellationTokenSource.cancel(); + this._cancellationTokenSource.dispose(); + + this._autoDisposables.forEach((disposable) => disposable.dispose()); + + assert(this._autoDisposables.size === 0); + + this.statusBar.dispose(); } /** @@ -447,142 +189,191 @@ export class Extension implements vscode.Disposable { */ public observePreference( section: string, - defaultValue: T, - handler: (value: T) => void, + handler: (value: T, validator: SettingsValidator, inspect: InspectType) => void, triggerNow = false, ) { - this.configurationChangeHandlers.set("dance." + section, () => { - handler(this.configuration.get(section, defaultValue)); + let configuration: vscode.WorkspaceConfiguration, + fullName: string; + + if (section[0] === ".") { + fullName = extensionName + section; + section = section.slice(1); + configuration = vscode.workspace.getConfiguration(extensionName); + } else { + fullName = section; + configuration = vscode.workspace.getConfiguration(); + } + + const defaultValue = configuration.inspect(section)!.defaultValue!; + + this._configurationChangeHandlers.set(fullName, () => { + const validator = new SettingsValidator(fullName), + configuration = vscode.workspace.getConfiguration(extensionName); + + handler( + configuration.get(section, defaultValue), + validator, + handler.length > 2 ? configuration.inspect(section)! : undefined!, + ); + + validator.displayErrorIfNeeded(); }); if (triggerNow) { - handler(this.configuration.get(section, defaultValue)); - } - } + const validator = new SettingsValidator(fullName); - public setEnabled(enabled: boolean, changeConfiguration: boolean) { - if (enabled === this.enabled) { - return; - } - - this.subscriptions.splice(0).forEach((x) => x.dispose()); - - if (!enabled) { - this.statusBarItem.hide(); - - for (const documentState of this.documentStates()) { - documentState.dispose(); - } - - this._documentStates = new Map(); - - if (changeConfiguration) { - vscode.workspace.getConfiguration(extensionName).update("enabled", false); - } - } else { - this.statusBarItem.show(); - - this.subscriptions.push( - vscode.window.onDidChangeActiveTextEditor((editor) => { - this._activeEditorState?.onDidBecomeInactive(); - - if (editor === undefined) { - this._activeEditorState = undefined; - } else { - this._activeEditorState = this.getEditorState(editor); - this._activeEditorState.onDidBecomeActive(); - } - }), - - vscode.window.onDidChangeTextEditorSelection((e) => { - this._documentStates - .get(e.textEditor.document) - ?.getEditorState(e.textEditor) - ?.onDidChangeTextEditorSelection(e); - }), - - vscode.workspace.onDidChangeTextDocument((e) => { - this._documentStates.get(e.document)?.onDidChangeTextDocument(e); - }), - - vscode.workspace.onDidChangeConfiguration((e) => { - this.configuration = vscode.workspace.getConfiguration(extensionName); - - for (const [section, handler] of this.configurationChangeHandlers.entries()) { - if (e.affectsConfiguration(section)) { - handler(); - } - } - }), + handler( + configuration.get(section, defaultValue), + validator, + handler.length > 2 ? configuration.inspect(section)! : undefined!, ); - for (let i = 0; i < commands.length; i++) { - this.subscriptions.push(commands[i].register(this)); - } - - const activeEditor = vscode.window.activeTextEditor; - - if (activeEditor !== undefined) { - this.getEditorState(activeEditor).onDidBecomeActive(); - } - - if (changeConfiguration) { - vscode.workspace.getConfiguration(extensionName).update("enabled", true); - } + validator.displayErrorIfNeeded(); } - - return (this.enabled = enabled); } // ============================================================================================= - // == DOCUMENT AND EDITOR STATES ============================================================= + // == CANCELLATION =========================================================================== // ============================================================================================= - private _documentStates = new WeakMap(); - private _activeEditorState?: EditorState; + private _cancellationTokenSource = new vscode.CancellationTokenSource(); /** - * Returns the `DocumentState` for the given `vscode.TextDocument`. + * The token for the next command. */ - public getDocumentState(document: vscode.TextDocument) { - let state = this._documentStates.get(document); + public get cancellationToken() { + return this._cancellationTokenSource.token; + } - if (state === undefined) { - this._documentStates.set(document, (state = new DocumentState(this, document))); + /** + * Requests the cancellation of the last operation. + */ + public cancelLastOperation() { + this._cancellationTokenSource.cancel(); + this._cancellationTokenSource.dispose(); + + this._cancellationTokenSource = new vscode.CancellationTokenSource(); + } + + // ============================================================================================= + // == DISPOSABLES ============================================================================ + // ============================================================================================= + + private readonly _autoDisposables = new Set(); + + /** + * Returns an `AutoDisposable` bound to this extension. It is ensured that any + * disposable added to it will be disposed of when the extension is unloaded. + */ + public createAutoDisposable() { + const disposable = new AutoDisposable(); + + disposable.addDisposable({ + dispose: () => this._autoDisposables.delete(disposable), + }); + + this._autoDisposables.add(disposable); + + return disposable; + } + + // ============================================================================================= + // == ERRORS ================================================================================= + // ============================================================================================= + + private _dismissErrorMessage?: () => void; + + /** + * Dismisses a currently shown error message, if any. + */ + public dismissErrorMessage() { + if (this._dismissErrorMessage !== undefined) { + this._dismissErrorMessage(); + this._dismissErrorMessage = undefined; + } + } + + /** + * Displays a dismissable error message in the status bar. + */ + public showDismissableErrorMessage(message: string) { + // Log the error so that long error messages and stacktraces can still be + // accessed by the user. + console.error(message); + + if (message.length > 80) { + message = message.slice(0, 77) + "..."; } - return state; + if (this.statusBar.errorSegment.content !== undefined) { + return this.statusBar.errorSegment.setContent(message); + } + + this.statusBar.errorSegment.setContent(message); + + const dispose = () => { + this.statusBar.errorSegment.setContent(); + this._dismissErrorMessage = undefined; + subscriptions.splice(0).forEach((d) => d.dispose()); + }; + + const subscriptions = [ + vscode.window.onDidChangeActiveTextEditor(dispose), + vscode.window.onDidChangeTextEditorSelection(dispose), + ]; + + this._dismissErrorMessage = dispose; } /** - * Returns the `EditorState` for the given `vscode.TextEditor`. + * Runs the given function, displaying an error message and returning the + * specified value if it throws an exception during its execution. */ - public getEditorState(editor: vscode.TextEditor) { - return this.getDocumentState(editor.document).getEditorState(editor); - } + public runSafely( + f: () => T, + errorValue: () => T, + errorMessage: (error: any) => T extends Thenable ? never : string, + ) { + this.dismissErrorMessage(); - /** - * Returns an iterator over all known `DocumentState`s. - */ - public *documentStates() { - const documents = vscode.workspace.textDocuments, - len = documents.length; - - for (let i = 0; i < len; i++) { - const documentState = this._documentStates.get(documents[i]); - - if (documentState !== undefined) { - yield documentState; + try { + return f(); + } catch (e) { + if (!(e instanceof CancellationError)) { + this.showDismissableErrorMessage(errorMessage(e)); } + + return errorValue(); } } /** - * Returns an iterator over all known `EditorState`s. + * Runs the given async function, displaying an error message and returning + * the specified value if it throws an exception during its execution. */ - public *editorStates() { - for (const documentState of this.documentStates()) { - yield* documentState.editorStates(); + public async runPromiseSafely( + f: () => Thenable, + errorValue: () => T, + errorMessage: (error: any) => string, + ) { + this.dismissErrorMessage(); + + try { + return await f(); + } catch (e) { + if (!(e instanceof CancellationError)) { + this.showDismissableErrorMessage(errorMessage(e)); + } + + return errorValue(); } } } + +type InspectUnknown = Exclude, undefined>; +type InspectType = { + // Replace all properties that are `unknown` by `T | undefined`. + readonly [K in keyof InspectUnknown]: (InspectUnknown[K] & null) extends never + ? InspectUnknown[K] + : T | undefined; +} diff --git a/src/state/modes.ts b/src/state/modes.ts new file mode 100644 index 0000000..56e4d56 --- /dev/null +++ b/src/state/modes.ts @@ -0,0 +1,695 @@ +import * as vscode from "vscode"; +import { command } from "../api"; +import { extensionName } from "../extension"; +import { Extension } from "./extension"; +import { SettingsValidator } from "../utils/settings-validator"; +import { workspaceSettingsPropertyNames } from "../utils/misc"; + +export const enum SelectionBehavior { + Caret = 1, + Character = 2, +} + +/** + * An editing mode. + */ +export class Mode { + private readonly _onChanged = new vscode.EventEmitter(); + private readonly _onDeleted = new vscode.EventEmitter(); + private _changeSubcription: vscode.Disposable | undefined; + + private _raw: Mode.Configuration = {}; + private _inheritsFrom: Mode; + private _cursorStyle = vscode.TextEditorCursorStyle.Line; + private _lineHighlight?: string | vscode.ThemeColor; + private _lineNumbers = vscode.TextEditorLineNumbersStyle.On; + private _selectionDecorationType?: vscode.TextEditorDecorationType; + private _selectionBehavior = SelectionBehavior.Caret; + private _onEnterMode: readonly command.Any[] = []; + private _onLeaveMode: readonly command.Any[] = []; + private _decorations: readonly Mode.Decoration[] = []; + + public get onChanged() { + return this._onChanged.event; + } + + public get onDeleted() { + return this._onDeleted.event; + } + + public get inheritsFrom() { + return this._inheritsFrom; + } + + public get cursorStyle() { + return this._cursorStyle; + } + + public get lineNumbers() { + return this._lineNumbers; + } + + public get lineHighlight() { + return this._lineHighlight; + } + + public get selectionDecorationType() { + return this._selectionDecorationType; + } + + public get selectionBehavior() { + return this._selectionBehavior; + } + + public get onEnterMode() { + return this._onEnterMode; + } + + public get onLeaveMode() { + return this._onLeaveMode; + } + + public get decorations() { + return this._decorations; + } + + public constructor( + public readonly modes: Modes, + public readonly name: string, + rawConfiguration: Mode.Configuration, + public isPendingDeletion = false, + ) { + this._inheritsFrom = modes.vscodeMode; + this._raw = {}; + + if (rawConfiguration != null) { + this.apply(rawConfiguration, new SettingsValidator()); + } + + this._changeSubcription = this._inheritsFrom?.onChanged(this._onParentModeChanged, this); + } + + /** + * Disposes of the mode. + */ + public dispose() { + this._changeSubcription?.dispose(); + + this._onDeleted.fire(this); + + this._onChanged.dispose(); + this._onDeleted.dispose(); + } + + private _onParentModeChanged([inheritFrom, keys]: readonly [Mode, readonly (keyof Mode)[]]) { + const updated = [] as (keyof Mode)[]; + + for (const key of keys) { + if (inheritFrom[key] !== this[key] && key !== "inheritsFrom") { + updated.push(key); + } + } + + if (updated.length > 0) { + this._onChanged.fire([this, updated]); + } + } + + /** + * Updates an underlying value of the mode. + */ + public update(key: `_${K}`, value: this[K]) { + if (this[key as keyof this] === value) { + return; + } + + this[key as keyof this] = value; + this._onChanged.fire([this, [key.slice(1) as keyof Mode]]); + } + + /** + * Applies a new configuration to the mode, notifying subscribers of changes + * if needed. + */ + public apply(raw: Mode.Configuration, validator: SettingsValidator) { + const willInheritFrom = raw.inheritFrom == null + ? this.modes.vscodeMode + : this.modes.getOrCreateDummy(raw.inheritFrom); + const changedProperties: (keyof Mode)[] = []; + + if (willInheritFrom !== this._inheritsFrom) { + this._changeSubcription?.dispose(); + this._inheritsFrom = willInheritFrom; + this._changeSubcription = willInheritFrom.onChanged(this._onParentModeChanged, this); + changedProperties.push("inheritsFrom"); + } + + const up = willInheritFrom, + top = this.modes.vscodeMode, + map = ( + rawName: RN, + name: N, + convert: (value: Exclude, + validator: SettingsValidator) => C, + ) => { + const value = raw[rawName]; + + if (value === undefined || value === "inherit") { + // Unspecified: use parent value. + return up[name]; + } + if (value === null) { + // Null: use VS Code value. + return top[name]; + } + + return validator.forProperty(rawName, (validator) => convert(value as any, validator)); + }; + + // Cursor style. + const cursorStyle = map("cursorStyle", "cursorStyle", Mode.cursorStyleStringToCursorStyle); + + if (this._cursorStyle !== cursorStyle) { + this._cursorStyle = cursorStyle; + changedProperties.push("cursorStyle"); + } + + // Line numbers. + const lineNumbers = map("lineNumbers", "lineNumbers", Mode.lineNumbersStringToLineNumbersStyle); + + if (this._lineNumbers !== lineNumbers) { + this._lineNumbers = lineNumbers; + changedProperties.push("lineNumbers"); + } + + // Selection behavior. + const selectionBehavior = map("selectionBehavior", "selectionBehavior", + Mode.selectionBehaviorStringToSelectionBehavior); + + if (this._selectionBehavior !== selectionBehavior) { + this._selectionBehavior = selectionBehavior; + changedProperties.push("selectionBehavior"); + } + + // Selection decorations. + const disposePreviousDecorations = this._raw?.decorations != null; + let decorations = raw.decorations; + + if (decorations === undefined) { + if (this._raw.decorations !== undefined) { + if (disposePreviousDecorations) { + this._decorations.forEach((d) => d.type.dispose()); + } + + this._decorations = up._decorations; + changedProperties.push("decorations"); + } + } else if (decorations === null) { + if (this._raw.decorations !== null) { + if (disposePreviousDecorations) { + this._decorations.forEach((d) => d.type.dispose()); + } + + this._decorations = top._decorations; + changedProperties.push("decorations"); + } + } else if (JSON.stringify(decorations) !== JSON.stringify(this._raw?.decorations)) { + if (!Array.isArray(decorations)) { + decorations = [decorations as Mode.Configuration.Decoration]; + } + + if (disposePreviousDecorations) { + this._decorations.forEach((d) => d.type.dispose()); + } + + validator.enter("decorations"); + + this._decorations = decorations.flatMap((d) => { + const validatorErrors = validator.errors.length, + renderOptions = Mode.decorationObjectToDecorationRenderOptions(d, validator), + applyTo = Mode.applyToStringToApplyTo(d.applyTo ?? "all", validator); + + if (validator.errors.length > validatorErrors) { + return []; + } + + return [{ + applyTo, + renderOptions, + type: vscode.window.createTextEditorDecorationType(renderOptions), + }]; + }); + + validator.leave(); + changedProperties.push("decorations"); + } + + // Events (subscribers don't care about changes to these properties, so we + // don't add them to the `changedProperties`). + this._onEnterMode = raw.onEnterMode ?? []; + this._onLeaveMode = raw.onLeaveMode ?? []; + + // Save raw JSON for future reference and notify subscribers of changes. + this._raw = raw; + + if (changedProperties.length > 0) { + this._onChanged.fire([this, changedProperties]); + } + } + + /** + * Validates and converts a string to a `vscode.TextEditorLineNumbersStyle` + * enum value. + */ + public static lineNumbersStringToLineNumbersStyle( + lineNumbers: Mode.Configuration.LineNumbers, + validator: SettingsValidator, + ) { + switch (lineNumbers) { + case "on": + return vscode.TextEditorLineNumbersStyle.On; + case "off": + return vscode.TextEditorLineNumbersStyle.Off; + case "relative": + return vscode.TextEditorLineNumbersStyle.Relative; + + default: + validator.reportInvalidSetting(`unrecognized lineNumbers "${lineNumbers}"`, "lineNumbers"); + return vscode.TextEditorLineNumbersStyle.On; + } + } + + /** + * Validates and converts a string to a `vscode.TextEditorCursorStyle` enum + * value. + */ + public static cursorStyleStringToCursorStyle( + cursorStyle: Mode.Configuration.CursorStyle, + validator: SettingsValidator, + ) { + switch (cursorStyle) { + case "block": + return vscode.TextEditorCursorStyle.Block; + case "block-outline": + return vscode.TextEditorCursorStyle.BlockOutline; + case "line": + return vscode.TextEditorCursorStyle.Line; + case "line-thin": + return vscode.TextEditorCursorStyle.LineThin; + case "underline": + return vscode.TextEditorCursorStyle.Underline; + case "underline-thin": + return vscode.TextEditorCursorStyle.UnderlineThin; + + default: + validator.reportInvalidSetting(`unrecognized cursorStyle "${cursorStyle}"`, "cursorStyle"); + return vscode.TextEditorCursorStyle.Line; + } + } + + /** + * Validates and converts a string to a `SelectionBehavior` enum value. + */ + public static selectionBehaviorStringToSelectionBehavior( + behavior: Mode.Configuration.SelectionBehavior, + validator: SettingsValidator, + ) { + switch (behavior) { + case "character": + return SelectionBehavior.Character; + case "caret": + return SelectionBehavior.Caret; + + default: + validator.reportInvalidSetting( + `unrecognized selectionBehavior "${behavior}"`, + "selectionBehavior", + ); + return SelectionBehavior.Caret; + } + } + + /** + * Validates and converts a configuration decoration to an actual + * `vscode.DecorationRenderOptions` object. + */ + public static decorationObjectToDecorationRenderOptions( + object: Mode.Configuration.Decoration, + validator: SettingsValidator, + ) { + const options: vscode.DecorationRenderOptions = {}; + + for (const name of ["backgroundColor", "borderColor"] as const) { + const value = object[name]; + + if (value) { + validator.forProperty(name, (v) => options[name] = this.stringToColor(value, v, "#000")); + } + } + + for (const name of ["borderStyle"] as const) { + const value = object[name]; + + if (value) { + options[name] = value; + } + } + + for (const name of ["isWholeLine"] as const) { + const value = object[name]; + + if (value != null) { + options[name] = !!object[name]; + } + } + + for (const name of ["borderRadius", "borderWidth"] as const) { + const value = object[name]; + + if (value) { + options[name] = value; + } + } + + return options; + } + + /** + * Validates and converts a string value to a valid `applyTo` value. + */ + public static applyToStringToApplyTo(value: string, validator: SettingsValidator) { + const applyTo = value; + + if (!["all", "main", "secondary"].includes(applyTo)) { + validator.reportInvalidSetting(`unrecognized applyTo ${JSON.stringify(applyTo)}`, + "applyTo"); + + return "all"; + } + + return applyTo as "all" | "main" | "secondary"; + } + + /** + * Validates and converts a string value to a string color or + * `vscode.ThemeColor`. + */ + public static stringToColor(value: string, validator: SettingsValidator, invalidValue = "") { + if (typeof value !== "string" || value.length === 0) { + validator.reportInvalidSetting("color must be a non-empty string"); + + return invalidValue; + } + + if (value[0] === "$") { + if (/^\$[\w]+(\.\w+)*$/.test(value)) { + return new vscode.ThemeColor(value.slice(1)); + } + + validator.reportInvalidSetting("invalid color reference " + value); + return invalidValue; + } + + if (value[0] === "#") { + if (/^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/.test(value)) { + return value; + } + + validator.reportInvalidSetting("invalid color " + value); + return invalidValue; + } + + if (value.startsWith("rgb")) { + if (/^rgb\( *\d+ *, *\d+ *, *\d+ *\)$|^rgba\( *\d+ *, *\d+ *, *\d+ *, *\d+ *\)$/.test(value)) { + return value; + } + + validator.reportInvalidSetting("invalid color " + value); + return invalidValue; + } + + validator.reportInvalidSetting("unknown color format " + value); + return invalidValue; + } +} + +export namespace Mode { + /** + * The configuration of a `Mode` as specified in the user preferences. + */ + export interface Configuration { + cursorStyle?: Configuration.CursorStyle; + decorations?: Configuration.Decoration[] | Configuration.Decoration; + inheritFrom?: string | null; + lineHighlight?: string | null; + lineNumbers?: Configuration.LineNumbers; + onEnterMode?: readonly command.Any[]; + onLeaveMode?: readonly command.Any[]; + selectionBehavior?: Configuration.SelectionBehavior | null; + } + + /** + * A mode decoration. + */ + export interface Decoration { + readonly applyTo: "all" | "main" | "secondary"; + readonly renderOptions: vscode.DecorationRenderOptions; + readonly type: vscode.TextEditorDecorationType; + } + + export namespace Configuration { + /** + * A valid cursor style value in a `Mode.Configuration`. + */ + export type CursorStyle = + | "line" + | "block" + | "underline" + | "line-thin" + | "block-outline" + | "underline-thin"; + + /** + * A valid line numbers value in a `Mode.Configuration`. + */ + export type LineNumbers = "on" | "off" | "relative"; + + /** + * A valid selection behavior value in a `Mode.Configuration`. + */ + export type SelectionBehavior = "caret" | "character"; + + /** + * A decoration. + */ + export interface Decoration { + readonly applyTo?: "all" | "main" | "secondary"; + readonly backgroundColor?: string; + readonly borderColor?: string; + readonly borderStyle?: string; + readonly borderRadius?: string; + readonly borderWidth?: string; + readonly isWholeLine?: boolean; + } + } +} + +/** + * The set of all modes. + */ +export class Modes implements Iterable { + private readonly _modes = new Map(); + + private readonly _vscodeMode = new Mode(this, "", undefined!); + private readonly _inputMode = new Mode(this, "input", { cursorStyle: "underline-thin" }); + private _defaultMode = new Mode(this, "default", {}); + + public constructor(extension: Extension) { + for (const builtin of [this._defaultMode, this._inputMode]) { + this._modes.set(builtin.name, builtin); + } + + this._vscodeMode.apply({ + cursorStyle: "line", + inheritFrom: null, + lineHighlight: null, + lineNumbers: "on", + selectionBehavior: "caret", + decorations: [], + }, new SettingsValidator()); + + this._observePreferences(extension); + } + + /** + * The default mode configured using `dance.defaultMode`. + */ + public get defaultMode() { + return this._defaultMode; + } + + /** + * The input mode, set when awaiting user input. + */ + public get inputMode() { + return this._inputMode; + } + + /** + * The "VS Code" mode, which represents the settings assigned to the editor + * without taking Dance settings into account. + */ + public get vscodeMode() { + return this._vscodeMode; + } + + /** + * Returns the `Mode` with the given name, or `undefined` if no such mode is + * defined. + */ + public get(name: string) { + return this._modes.get(name); + } + + /** + * Returns the `Mode` with the given name, or creates one if no such mode is + * defined. + */ + public getOrCreateDummy(name: string) { + let mode = this._modes.get(name); + + if (mode === undefined) { + this._modes.set(name, mode = new Mode(this, name, {}, /* isPendingDeletion= */ true)); + } + + return mode; + } + + public [Symbol.iterator]() { + return this._modes.values(); + } + + public *userModes() { + for (const mode of this._modes.values()) { + if (mode.name !== "input" && !mode.isPendingDeletion) { + yield mode; + } + } + } + + /** + * Starts listening to changes in user preferences that may lead to updates to + * user modes. + */ + private _observePreferences(extension: Extension) { + // Mode definitions. + extension.observePreference(".modes", (value, validator, inspect) => { + let isEmpty = true; + const removeModes = new Set(this._modes.keys()), + expectedDefaultModeName = vscode.workspace.getConfiguration(extensionName) + .get("defaultMode"); + + removeModes.delete(this.inputMode.name); + + for (const modeName in value) { + removeModes.delete(modeName); + + if (modeName === "input" || modeName === "") { + validator.reportInvalidSetting(`a mode cannot be named "${modeName}"`); + continue; + } + + let mode = this._modes.get(modeName); + const configuration = value[modeName]; + + const globalConfig = inspect.globalValue?.[modeName], + defaultConfig = inspect.defaultValue?.[modeName]; + + if (globalConfig !== undefined || defaultConfig !== undefined) { + // Mode is a global mode; make sure that the local workspace does not + // override its `on{Enter,Leave}Mode` hooks (to make sure loading a + // workspace will not execute arbitrary code). + configuration.onEnterMode = globalConfig?.onEnterMode ?? defaultConfig?.onEnterMode; + configuration.onLeaveMode = globalConfig?.onLeaveMode ?? defaultConfig?.onLeaveMode; + } + + if (mode === undefined) { + this._modes.set(modeName, mode = new Mode(this, modeName, configuration)); + + if (modeName === expectedDefaultModeName) { + this._defaultMode.dispose(); + this._defaultMode = mode; + } + } else { + mode.isPendingDeletion = false; + mode.apply(configuration, validator); + } + + isEmpty = false; + } + + if (isEmpty) { + validator.reportInvalidSetting("at least one mode must be defined"); + } + + const actualDefaultModeName = this._defaultMode.name; + + for (const modeName of removeModes) { + if (modeName === actualDefaultModeName) { + validator.reportInvalidSetting( + "default mode was removed, please update dance.defaultMode", + ); + } else { + this._modes.get(modeName)!.dispose(); + } + + this._modes.delete(modeName); + } + }, true); + + // Default mode. + extension.observePreference(".defaultMode", (value, validator) => { + const mode = this._modes.get(value); + + if (mode === undefined) { + return validator.reportInvalidSetting("mode does not exist: " + value); + } + + if (!this._modes.has(this._defaultMode.name)) { + // Default mode had previously been deleted; we can now dispose of it. + this._defaultMode.dispose(); + } + + this._defaultMode = mode; + }, true); + + // VS Code settings. + extension.observePreference( + "editor.cursorStyle", + (value, validator) => { + this._vscodeMode.update( + "_cursorStyle", + Mode.cursorStyleStringToCursorStyle(value, validator), + ); + }, + true, + ); + + extension.observePreference( + "editor.lineNumbers", + (value, validator) => { + this._vscodeMode.update( + "_lineNumbers", + Mode.lineNumbersStringToLineNumbersStyle(value, validator), + ); + }, + true, + ); + } +} + +export namespace Modes { + export interface Configuration { + readonly [modeName: string]: Mode.Configuration; + } +} diff --git a/src/state/recorder.ts b/src/state/recorder.ts new file mode 100644 index 0000000..ab3280b --- /dev/null +++ b/src/state/recorder.ts @@ -0,0 +1,923 @@ +import * as vscode from "vscode"; +import { CommandDescriptor } from "../commands"; +import { noUndoStops } from "../utils/misc"; +import { StatusBar } from "./status-bar"; +import { Context } from "../api/context"; +import { assert, CancellationError, EditorRequiredError, todo } from "../api/errors"; +import { Positions } from "../api/positions"; + +type RecordValue = Recording.ActionType | CommandDescriptor | object | vscode.Uri | number | string; + +const enum Constants { + NextMask = 0xff, + PrevShift = 8, +} + +/** + * A class used to record actions as they happen. + */ +export class Recorder implements vscode.Disposable { + private readonly _previousBuffers: Recorder.Buffer[] = []; + private readonly _subscriptions: vscode.Disposable[] = []; + + private _activeDocument: vscode.TextDocument | undefined; + private _buffer: RecordValue[] = [0]; + private _lastActiveSelections: readonly vscode.Selection[] | undefined; + private _activeRecordingTokens: vscode.Disposable[] = []; + + public constructor( + private readonly _statusBar: StatusBar, + ) { + const activeEditor = vscode.window.activeTextEditor; + + if (activeEditor !== undefined) { + this._activeDocument = activeEditor.document; + this._lastActiveSelections = activeEditor.selections; + } + + this._subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(this._recordActiveTextEditorChange, this), + vscode.window.onDidChangeTextEditorSelection(this._recordExternalSelectionChange, this), + vscode.workspace.onDidChangeTextDocument(this._recordExternalTextChange, this), + ); + } + + public dispose() { + this._activeRecordingTokens.splice(0).forEach((d) => d.dispose()); + this._subscriptions.splice(0).forEach((d) => d.dispose()); + } + + /** + * Starts a record, saving its identifier to the current buffer. + */ + private _startRecord(type: Recording.ActionType) { + (this._buffer[this._buffer.length - 1] as Recording.ActionType) |= type; + } + + /** + * Ends a record, saving its identifier to the current buffer. + */ + private _endRecord(type: Recording.ActionType) { + this._buffer.push(type << Constants.PrevShift); + this._archiveBufferIfNeeded(); + } + + /** + * Archives the current buffer to `_previousBuffers` if its size exceeded a + * threshold and if no recording is currently ongoing. + */ + private _archiveBufferIfNeeded() { + if (this._activeRecordingTokens.length > 0 || this._buffer.length < 8192) { + return; + } + + this._previousBuffers.push(this._buffer); + this._buffer = []; + } + + /** + * Returns the number of available buffers. + */ + public get bufferCount() { + return this._previousBuffers.length + 1; + } + + /** + * Returns the buffer at the given index, if any. + */ + public getBuffer(index: number) { + return index === this._previousBuffers.length ? this._buffer : this._previousBuffers[index]; + } + + /** + * Returns a `Cursor` starting at the start of the recorder. + */ + public cursorFromStart() { + return new Recorder.Cursor(this, 0, 0); + } + + /** + * Returns a `Cursor` starting at the end of the recorder at the time of the + * call. + */ + public cursorFromEnd() { + return new Recorder.Cursor(this, this._previousBuffers.length, this._buffer.length - 1); + } + + /** + * Returns a `Cursor` starting at the start of the specified recording. + */ + public fromRecordingStart(recording: Recording) { + let bufferIdx = this._previousBuffers.indexOf(recording.buffer); + + if (bufferIdx === -1) { + assert(recording.buffer === this._buffer); + + bufferIdx = this._previousBuffers.length; + } + + return new Recorder.Cursor(this, bufferIdx, recording.offset); + } + + /** + * Returns a `Cursor` starting at the end of the specified recording. + */ + public fromRecordingEnd(recording: Recording) { + let bufferIdx = this._previousBuffers.indexOf(recording.buffer); + + if (bufferIdx === -1) { + assert(recording.buffer === this._buffer); + + bufferIdx = this._previousBuffers.length; + } + + return new Recorder.Cursor(this, bufferIdx, recording.offset + recording.length); + } + + /** + * Starts recording a series of actions. + */ + public startRecording() { + const onRecordingCompleted = () => { + const index = this._activeRecordingTokens.indexOf(cancellationTokenSource); + + if (index === -1) { + throw new Error("recording has already been marked as completed"); + } + + this._activeRecordingTokens.splice(index, 1); + cancellationTokenSource.dispose(); + + const activeRecordingsCount = this._activeRecordingTokens.length; + + if (activeRecordingsCount === 0) { + this._statusBar.recordingSegment.setContent(); + vscode.commands.executeCommand("setContext", "dance.isRecording", false); + } else { + this._statusBar.recordingSegment.setContent("" + activeRecordingsCount); + } + + const buffer = this._buffer; + + this._archiveBufferIfNeeded(); + + return new Recording(buffer, offset, buffer.length - offset); + }; + + const offset = this._buffer.length - 1, + cancellationTokenSource = new vscode.CancellationTokenSource(), + recording = new ActiveRecording(onRecordingCompleted, cancellationTokenSource.token), + activeRecordingsCount = this._activeRecordingTokens.push(cancellationTokenSource); + + this._statusBar.recordingSegment.setContent("" + activeRecordingsCount); + + if (activeRecordingsCount === 1) { + vscode.commands.executeCommand("setContext", "dance.isRecording", true); + } + + return recording; + } + + /** + * Replays the action at the given index, and returns the index of the next + * action. This index may be equal to the `length` of the buffer. + */ + public replay(buffer: Recorder.Buffer, index: number, context: Context.WithoutActiveEditor) { + switch ((buffer[index] as number) & Constants.NextMask) { + case Recording.ActionType.Break: + return Promise.resolve(index + 1); + + case Recording.ActionType.Command: + return this.replayCommand(buffer, index, context); + + case Recording.ActionType.ExternalCommand: + return this.replayExternalCommand(buffer, index); + + case Recording.ActionType.TextEditorChange: + return this.replayTextEditorChange(buffer, index); + + case Recording.ActionType.SelectionTranslation: + return Promise.resolve(this.replaySelectionTranslation(buffer, index)); + + case Recording.ActionType.SelectionTranslationToLineEnd: + todo(); + break; + + case Recording.ActionType.TextReplacement: + return this.replayTextReplacement(buffer, index); + + default: + throw new Error("invalid recorder buffer given"); + } + } + + /** + * Returns the record at the given index in the given buffer. + */ + public readRecord(buffer: Recorder.Buffer, index: number) { + switch ((buffer[index] as number) & Constants.NextMask) { + case Recording.ActionType.Break: + return buffer.slice(index, index + 1) as Recording.Entry; + + case Recording.ActionType.Command: + return buffer.slice(index, index + 3) as Recording.Entry; + + case Recording.ActionType.ExternalCommand: + return buffer.slice(index, index + 3) as + Recording.Entry; + + case Recording.ActionType.TextEditorChange: + return buffer.slice(index, index + 2) as + Recording.Entry; + + case Recording.ActionType.SelectionTranslation: + return buffer.slice(index, index + 3) as + Recording.Entry; + + case Recording.ActionType.SelectionTranslationToLineEnd: + todo(); + break; + + case Recording.ActionType.TextReplacement: + return buffer.slice(index, index + 3) as + Recording.Entry; + + default: + throw new Error("invalid recorder buffer given"); + } + } + + /** + * Records the invocation of a command. + */ + public recordCommand(descriptor: CommandDescriptor, argument: Record) { + this._startRecord(Recording.ActionType.Command); + this._buffer.push(descriptor, argument); + this._endRecord(Recording.ActionType.Command); + } + + /** + * Replays the command at the given index, and returns the index of the next + * record in the given buffer. + */ + public async replayCommand( + buffer: Recorder.Buffer, + index: number, + context = Context.WithoutActiveEditor.current, + ) { + assert(((buffer[index] as number) & Constants.NextMask) === Recording.ActionType.Command); + + const descriptor = buffer[index + 1] as CommandDescriptor, + argument = buffer[index + 2] as object; + + if ((descriptor.flags & CommandDescriptor.Flags.DoNotReplay) === 0) { + await descriptor.replay(context, argument); + } + + return index + 3; + } + + /** + * Records the invocation of an external (non-Dance) command. + */ + public recordExternalCommand(identifier: string, argument: Record) { + this._startRecord(Recording.ActionType.ExternalCommand); + this._buffer.push(identifier, argument); + this._endRecord(Recording.ActionType.ExternalCommand); + } + + /** + * Replays the command at the given index, and returns the index of the next + * record in the given buffer. + */ + public replayExternalCommand(buffer: Recorder.Buffer, index: number) { + assert( + ((buffer[index] as number) & Constants.NextMask) === Recording.ActionType.ExternalCommand); + + const descriptor = buffer[index + 1] as string, + argument = buffer[index + 2] as object; + + return vscode.commands.executeCommand(descriptor, argument).then(() => index + 3); + } + + /** + * Records a change in the active text editor. + */ + private _recordActiveTextEditorChange(e: vscode.TextEditor | undefined) { + if (e?.document !== this._activeDocument) { + if (e?.document === undefined) { + this._activeDocument = undefined; + this._lastActiveSelections = undefined; + this._recordBreak(); + } else { + this._activeDocument = e.document; + this._lastActiveSelections = e.selections; + this._startRecord(Recording.ActionType.TextEditorChange); + this._buffer.push(e.document.uri); + this._endRecord(Recording.ActionType.TextEditorChange); + } + } + } + + /** + * Replays a text editor change, and returns the index of the next record in + * the given buffer. + */ + public replayTextEditorChange(buffer: Recorder.Buffer, index: number) { + assert( + ((buffer[index] as number) & Constants.NextMask) === Recording.ActionType.TextEditorChange); + + const documentUri = buffer[index + 1] as vscode.Uri; + + return vscode.window.showTextDocument(documentUri).then(() => index + 2); + } + + /** + * Records a change of a selection. + */ + private _recordExternalSelectionChange(e: vscode.TextEditorSelectionChangeEvent) { + if (vscode.window.activeTextEditor !== e.textEditor) { + return; + } + + const lastSelections = this._lastActiveSelections, + selections = e.selections; + this._lastActiveSelections = selections; + + // Issue: Command is executing but not in a context, so we log selection changes which is bad + if (Context.WithoutActiveEditor.currentOrUndefined !== undefined + || lastSelections === undefined) { + return; + } + + if (lastSelections.length !== selections.length + || e.kind === vscode.TextEditorSelectionChangeKind.Mouse) { + return this._recordBreak(); + } + + // TODO: be able to record jumps to end of line with a line offset + const document = e.textEditor.document; + let commonAnchorOffsetDiff = Number.MAX_SAFE_INTEGER, + commonActiveOffsetDiff = Number.MAX_SAFE_INTEGER; + + for (let i = 0, len = selections.length; i < len; i++) { + const lastSelection = lastSelections[i], + selection = selections[i]; + + const lastAnchorOffset = document.offsetAt(lastSelection.active), + anchorOffset = document.offsetAt(selection.active), + anchorOffsetDiff = anchorOffset - lastAnchorOffset; + + if (commonAnchorOffsetDiff === Number.MAX_SAFE_INTEGER) { + commonAnchorOffsetDiff = anchorOffsetDiff; + } else if (commonAnchorOffsetDiff !== anchorOffsetDiff) { + return this._tryRecordSelectionTranslationToLineEnd(); + } + + const lastActiveOffset = document.offsetAt(lastSelection.active), + activeOffset = document.offsetAt(selection.active), + activeOffsetDiff = activeOffset - lastActiveOffset; + + if (commonActiveOffsetDiff === Number.MAX_SAFE_INTEGER) { + commonActiveOffsetDiff = activeOffsetDiff; + } else if (commonActiveOffsetDiff !== activeOffsetDiff) { + return this._tryRecordSelectionTranslationToLineEnd(); + } + } + + // Merge consecutive events, if any. + if (this._tryMergeSelectionTranslations(commonAnchorOffsetDiff, commonActiveOffsetDiff)) { + return; + } + + this._startRecord(Recording.ActionType.SelectionTranslation); + this._buffer.push(commonAnchorOffsetDiff, commonActiveOffsetDiff); + this._endRecord(Recording.ActionType.SelectionTranslation); + } + + private _tryMergeSelectionTranslations(anchorOffsetDiff2: number, activeOffsetDiff2: number) { + if (anchorOffsetDiff2 !== activeOffsetDiff2) { + return false; + } + + const cursor = this.cursorFromEnd(); + + // "Text change 1 -> selection change 1 -> text change 2 -> selection + // change 2" can be merged into "text change 3 -> selection change 3". + if (!cursor.previous() + || !cursor.is(Recording.ActionType.TextReplacement) + || cursor.offsetFromActive() !== 0) { + return false; + } + + const insertedText2 = cursor.insertedText(), + deletionLength2 = cursor.deletionLength(); + + if (insertedText2.length !== activeOffsetDiff2 || deletionLength2 !== 0) { + return false; + } + + if (!cursor.previous() || !cursor.is(Recording.ActionType.SelectionTranslation)) { + return false; + } + + const anchorOffsetDiff1 = cursor.anchorOffsetDiff(), + activeOffsetDiff1 = cursor.activeOffsetDiff(); + + if (anchorOffsetDiff1 !== activeOffsetDiff1) { + return false; + } + + if (!cursor.previous() + || !cursor.is(Recording.ActionType.TextReplacement) + || cursor.offsetFromActive() !== 0) { + return false; + } + + const insertedText1 = cursor.insertedText(); + + if (insertedText1.length !== activeOffsetDiff1) { + return false; + } + + // This is a match! Update "text change 1 -> selection change 1". + (cursor.buffer as Recorder.MutableBuffer)[cursor.offset + 1] = insertedText1 + insertedText2; + + assert(cursor.next() && cursor.is(Recording.ActionType.SelectionTranslation)); + + const totalDiff = anchorOffsetDiff1 + anchorOffsetDiff2; + + (cursor.buffer as Recorder.MutableBuffer)[cursor.offset + 1] = totalDiff; + (cursor.buffer as Recorder.MutableBuffer)[cursor.offset + 2] = totalDiff; + + // Finally, delete the last entry corresponding to "text change 2" (since + // "selection change 2" hasn't been written yet). + assert(cursor.next() && cursor.is(Recording.ActionType.TextReplacement)); + + (cursor.buffer as Recorder.MutableBuffer).splice(cursor.offset); + this._endRecord(Recording.ActionType.SelectionTranslation); + + return true; + } + + private _tryRecordSelectionTranslationToLineEnd() { + // TODO + return this._recordBreak(); + } + + /** + * Replays the selection translation at the given index, and returns the index + * of the next record in the given buffer. + */ + public replaySelectionTranslation(buffer: Recorder.Buffer, index: number) { + assert(((buffer[index] as number) & Constants.NextMask) + === Recording.ActionType.SelectionTranslation); + + const anchorOffsetDiff = buffer[index + 1] as number, + activeOffsetDiff = buffer[index + 2] as number; + + const editor = vscode.window.activeTextEditor; + + EditorRequiredError.throwUnlessAvailable(editor); + + const document = editor.document, + newSelections = [] as vscode.Selection[]; + + for (const selection of editor.selections) { + const newAnchor = Positions.offset.orEdge(selection.anchor, anchorOffsetDiff, document), + newActive = Positions.offset.orEdge(selection.active, activeOffsetDiff, document); + + newSelections.push(new vscode.Selection(newAnchor, newActive)); + } + + editor.selections = newSelections; + + return index + 3; + } + + /** + * Records a change to a document. + */ + private _recordExternalTextChange(e: vscode.TextDocumentChangeEvent) { + const editor = vscode.window.activeTextEditor; + + if (editor?.document !== e.document) { + return; + } + + const lastSelections = this._lastActiveSelections, + selections = editor.selections; + this._lastActiveSelections = selections; + + if (Context.WithoutActiveEditor.currentOrUndefined !== undefined + || lastSelections === undefined || e.contentChanges.length === 0) { + return; + } + + if (lastSelections.length !== e.contentChanges.length) { + return this._recordBreak(); + } + + const document = e.document, + firstChange = e.contentChanges[0], + firstSelection = lastSelections[0], + commonInsertedText = firstChange.text, + commonDeletionLength = firstChange.rangeLength, + commonOffsetFromActive = + firstChange.rangeOffset - document.offsetAt(firstSelection.active); + + for (let i = 1, len = lastSelections.length; i < len; i++) { + const change = e.contentChanges[i]; + + if (change.text !== commonInsertedText || change.rangeLength !== commonDeletionLength) { + return this._recordBreak(); + } + + const offsetFromActive = change.rangeOffset - document.offsetAt(lastSelections[i].active); + + if (offsetFromActive !== commonOffsetFromActive) { + return this._recordBreak(); + } + } + + // Merge consecutive events, if any. + const cursor = this.cursorFromEnd(); + + if (cursor.previousType() === Recording.ActionType.TextReplacement) { + // If there has been no selection change in the meantime, the text we're + // inserting now is at the start of the text we previously inserted. + const offset = cursor.previousOffset!, + previousOffsetFromActive = this._buffer[offset + 3]; + + if (commonOffsetFromActive === previousOffsetFromActive) { + this._buffer[offset + 1] = commonInsertedText + (this._buffer[offset + 1] as string); + this._buffer[offset + 2] = commonDeletionLength + (this._buffer[offset + 2] as number); + + return; + } + } + + this._startRecord(Recording.ActionType.TextReplacement); + this._buffer.push(commonInsertedText, commonDeletionLength, commonOffsetFromActive); + this._endRecord(Recording.ActionType.TextReplacement); + } + + /** + * Replays the text replacement at the given index, and returns the index of + * the next record in the given buffer. + */ + public replayTextReplacement(buffer: Recorder.Buffer, index: number) { + // FIXME: type "hello world", remove "ld", replay; it should type + // "hello wor" + assert( + ((buffer[index] as number) & Constants.NextMask) === Recording.ActionType.TextReplacement); + + const insertedText = buffer[index + 1] as string, + deletionLength = buffer[index + 2] as number, + offsetFromActive = buffer[index + 3] as number; + + const editor = vscode.window.activeTextEditor; + + EditorRequiredError.throwUnlessAvailable(editor); + + return editor.edit((editBuilder) => { + const document = editor.document; + + for (const selection of editor.selections) { + const rangeStart = Positions.offset(selection.active, offsetFromActive, document); + assert(rangeStart !== undefined); + const rangeEnd = Positions.offset(rangeStart, deletionLength, document); + assert(rangeEnd !== undefined); + + editBuilder.replace(new vscode.Range(rangeStart, rangeEnd), insertedText); + } + }, noUndoStops).then(() => index + 4); + } + + /** + * Records a "break", indicating that a change that cannot be reliably + * replayed just happened. + */ + private _recordBreak() { + const buffer = this._buffer; + + if (buffer.length > 0 && buffer[buffer.length - 1] !== Recording.ActionType.Break) { + this._startRecord(Recording.ActionType.Break); + this._endRecord(Recording.ActionType.Break); + this._activeRecordingTokens.splice(0).forEach((t) => t.dispose()); + } + } +} + +export namespace Recorder { + /** + * A buffer of `Recorder` values. + */ + export type Buffer = readonly RecordValue[]; + + /** + * The mutable version of `Buffer`. + */ + export type MutableBuffer = RecordValue[]; + + /** + * A cursor used to enumerate records in a `Recorder` or `Recording`. + */ + export class Cursor { + private _buffer: Buffer; + private _bufferIdx: number; + private _offset: number; + + public constructor( + /** + * The recorder from which records are read. + */ + public readonly recorder: Recorder, + + buffer: number, + offset: number, + ) { + this._buffer = recorder.getBuffer(buffer); + this._bufferIdx = buffer; + this._offset = offset; + } + + /** + * Returns the buffer storing the current record. + */ + public get buffer() { + return this._buffer; + } + + /** + * Returns the offset of the current record in its buffer. + */ + public get offset() { + return this._offset; + } + + /** + * Returns the offset of the previous record in its buffer, or `undefined` + * if the current record is the first of its buffer. + */ + public get previousOffset() { + return this._offset === 0 + ? undefined + : this._offset - Recording.entrySize[this.previousType()] - 1; + } + + /** + * Returns a different instance of a `Cursor` that points to the same + * record. + */ + public clone() { + return new Cursor(this.recorder, this._bufferIdx, this._offset); + } + + /** + * Returns whether the current cursor is before or equal to the given + * cursor. + */ + public isBeforeOrEqual(other: Cursor) { + return this._bufferIdx < other._bufferIdx + || (this._bufferIdx === other._bufferIdx && this._offset <= other._offset); + } + + /** + * Returns whether the current cursor is after or equal to the given + * cursor. + */ + public isAfterOrEqual(other: Cursor) { + return this._bufferIdx > other._bufferIdx + || (this._bufferIdx === other._bufferIdx && this._offset >= other._offset); + } + + /** + * Replays the record pointed at by the cursor. + */ + public replay(context: Context.WithoutActiveEditor) { + return this.recorder.replay(this._buffer, this._offset, context); + } + + /** + * Returns the type of the current record. + */ + public type() { + return ((this._buffer[this._offset] as number) & Constants.NextMask) as T; + } + + /** + * Returns the type of the previous record. + */ + public previousType() { + return (this._buffer[this._offset] as number >> Constants.PrevShift) as Recording.ActionType; + } + + /** + * Returns whether the cursor points to a record of the given type. + */ + public is(type: T): this is Cursor { + return this.type() as Recording.ActionType === type; + } + + public commandDescriptor(): T extends Recording.ActionType.Command ? CommandDescriptor : never { + return this._buffer[this._offset + 1] as CommandDescriptor as any; + } + + public commandArgument(): T extends Recording.ActionType.Command ? Record : never { + return this._buffer[this._offset + 2] as object as any; + } + + public insertedText(): T extends Recording.ActionType.TextReplacement ? string : never { + return this._buffer[this._offset + 1] as string as any; + } + + public deletionLength(): T extends Recording.ActionType.TextReplacement ? number : never { + return this._buffer[this._offset + 2] as number as any; + } + + public offsetFromActive(): T extends Recording.ActionType.TextReplacement ? number : never { + return this._buffer[this._offset + 3] as number as any; + } + + public anchorOffsetDiff( + ): T extends Recording.ActionType.SelectionTranslation ? number : never { + return this._buffer[this._offset + 1] as number as any; + } + + public activeOffsetDiff( + ): T extends Recording.ActionType.SelectionTranslation ? number : never { + return this._buffer[this._offset + 2] as number as any; + } + + /** + * Switches to the next record, and returns `true` if the operation + * succeeded or `false` if the current record is the last one available. + */ + public next(): this is Cursor { + if (this._offset === this._buffer.length - 1) { + if (this._bufferIdx === this.recorder.bufferCount) { + return false; + } + + this._bufferIdx++; + this._buffer = this.recorder.getBuffer(this._bufferIdx); + this._offset = 0; + + return true; + } + + this._offset += Recording.entrySize[this.type()] + 1; + return true; + } + + /** + * Switches to the previous record, and returns `true` if the operation + * succeeded or `false` if the current record is the first one available. + */ + public previous(): this is Cursor { + if (this._offset === 0) { + if (this._bufferIdx === 0) { + return false; + } + + this._bufferIdx--; + this._buffer = this.recorder.getBuffer(this._bufferIdx); + this._offset = this._buffer.length - 1; + + return true; + } + + this._offset -= Recording.entrySize[this.previousType()] + 1; + return true; + } + + /** + * Returns whether the record pointed at by the cursor is included in the + * specified recording. + */ + public isInRecording(recording: Recording) { + return recording.offset <= this._offset && this._offset < recording.offset + recording.length + && recording.buffer === this._buffer; + } + } +} + +/** + * An ongoing `Recording`. + */ +export class ActiveRecording { + public constructor( + private readonly _notifyCompleted: () => Recording, + + /** + * A cancellation token that will be cancelled if the recording is forcibly + * stopped due to an unknown change. + */ + public readonly cancellationToken: vscode.CancellationToken, + ) {} + + public complete() { + CancellationError.throwIfCancellationRequested(this.cancellationToken); + + return this._notifyCompleted(); + } +} + +/** + * A recording of actions performed in VS Code. + */ +export class Recording { + public readonly buffer: Recorder.Buffer; + public readonly offset: number; + public readonly length: number; + + public constructor(buffer: Recorder.Buffer, offset: number, length: number) { + this.buffer = buffer; + this.offset = offset; + this.length = length; + } + + /** + * Replays the recording in the given context. + */ + public async replay(context = Context.WithoutActiveEditor.current) { + let offset = this.offset; + const buffer = this.buffer, + end = offset + this.length, + recorder = context.extension.recorder; + + while (offset < end) { + offset = await recorder.replay(buffer, offset, context); + } + } +} + +export namespace Recording { + /** + * The type of a recorded action. + */ + export const enum ActionType { + /** + * An action that cannot be reliably replayed and that interrupts a + * recording. + */ + Break, + + /** + * An internal command invocation. + */ + Command, + + /** + * An external command invocation. + */ + ExternalCommand, + + /** + * A translation of all selections. + */ + SelectionTranslation, + + /** + * A translation of all selections to the end of a line. + */ + SelectionTranslationToLineEnd, + + /** + * An active text editor change. + */ + TextEditorChange, + + /** + * A replacement of text near all selections. + */ + TextReplacement, + } + + /** + * A recorded action. + */ + export type Entry = [T, ...EntryMap[T]]; + + /** + * Type map from entry type to entry values. + */ + export interface EntryMap { + readonly [ActionType.Break]: readonly []; + readonly [ActionType.Command]: readonly [command: CommandDescriptor, argument: object]; + readonly [ActionType.ExternalCommand]: readonly [identifier: string, argument: object]; + readonly [ActionType.SelectionTranslation]: readonly [anchorDiff: number, activeDiff: number]; + readonly [ActionType.SelectionTranslationToLineEnd]: readonly []; + readonly [ActionType.TextEditorChange]: readonly [uri: vscode.Uri]; + readonly [ActionType.TextReplacement]: + readonly [insertedText: string, deletionLength: number, offsetFromActive: number]; + } + + /** + * Maps an entry type to the size of its tuple in `EntryMap`. + */ + export const entrySize: { + readonly [K in keyof EntryMap]: EntryMap[K]["length"]; + } = [0, 2, 2, 2, 0, 1, 3]; +} diff --git a/src/state/registers.ts b/src/state/registers.ts new file mode 100644 index 0000000..825437d --- /dev/null +++ b/src/state/registers.ts @@ -0,0 +1,604 @@ +import * as vscode from "vscode"; + +import { ArgumentError, assert, Context, EditNotAppliedError, EditorRequiredError, prompt, Selections } from "../api"; +import { SelectionBehavior } from "./modes"; +import { noUndoStops } from "../utils/misc"; +import { TrackedSelection } from "../utils/tracked-selection"; +import { Recording } from "./recorder"; + +/** + * The base class for all registers. + */ +export abstract class Register { + /** + * The name of the register. + */ + public readonly abstract name: string; + + /** + * The flags of the register, which define what a register can do. + */ + public readonly abstract flags: Register.Flags; + + /** + * Returns whether the register is readable. + */ + public canRead(): this is Register.Readable { + return (this.flags & Register.Flags.CanRead) === Register.Flags.CanRead; + } + + /** + * Returns whether the register is writeable. + */ + public canWrite(): this is Register.Writeable { + return (this.flags & Register.Flags.CanWrite) === Register.Flags.CanWrite; + } + + /** + * Returns whether the register is selections-readeable. + */ + public canReadSelections(): this is Register.ReadableSelections { + return (this.flags & Register.Flags.CanReadSelections) === Register.Flags.CanReadSelections; + } + + /** + * Returns whether the register is selections-writeable. + */ + public canWriteSelections(): this is Register.WriteableSelections { + return (this.flags & Register.Flags.CanWriteSelections) === Register.Flags.CanWriteSelections; + } + + /** + * Returns whether the register can be used to replay recorded commands. + */ + public canReadRecordedCommands(): this is Register.ReadableWriteableMacros { + return (this.flags & Register.Flags.CanReadWriteMacros) === Register.Flags.CanReadWriteMacros; + } + + /** + * Returns whether the register can be used to record commands. + */ + public canWriteRecordedCommands(): this is Register.ReadableWriteableMacros { + return (this.flags & Register.Flags.CanReadWriteMacros) === Register.Flags.CanReadWriteMacros; + } + + /** + * Ensures that the register is readable. + */ + public ensureCanRead(): asserts this is Register.Readable { + this.checkFlags(Register.Flags.CanRead); + } + + /** + * Ensures that the register is writeable. + */ + public ensureCanWrite(): asserts this is Register.Writeable { + this.checkFlags(Register.Flags.CanWrite); + } + + /** + * Ensures that the register is selections-readeable. + */ + public ensureCanReadSelections(): asserts this is Register.ReadableSelections { + this.checkFlags(Register.Flags.CanReadSelections); + } + + /** + * Ensures that the register is selections-writeable. + */ + public ensureCanWriteSelections(): asserts this is Register.WriteableSelections { + this.checkFlags(Register.Flags.CanWriteSelections); + } + + /** + * Ensures that the register can be used to replay recorded commands. + */ + public ensureCanReadRecordedCommands(): asserts this is Register.ReadableWriteableMacros { + this.checkFlags(Register.Flags.CanReadWriteMacros); + } + + /** + * Ensures that the register can be used to record commands. + */ + public ensureCanWriteRecordedCommands(): asserts this is Register.ReadableWriteableMacros { + this.checkFlags(Register.Flags.CanReadWriteMacros); + } + + /** + * Returns whether the current register has the given flags. + */ + public hasFlags(flags: F): this is Register.WithFlags { + return (this.flags & flags) === flags; + } + + /** + * @deprecated Use `Register.ensure*` instead. + */ + public checkFlags(flags: Register.Flags) { + const f = this.flags; + + if ((flags & Register.Flags.CanRead) && !(f & Register.Flags.CanRead)) { + throw new Error(`register "${this.name}" cannot be used to read text`); + } + if ((flags & Register.Flags.CanReadSelections) && !(f & Register.Flags.CanReadSelections)) { + throw new Error(`register "${this.name}" cannot be used to read selections`); + } + if ((flags & Register.Flags.CanReadWriteMacros) && !(f & Register.Flags.CanReadWriteMacros)) { + throw new Error(`register "${this.name}" cannot be used to play or create recordings`); + } + if ((flags & Register.Flags.CanWrite) && !(f & Register.Flags.CanWrite)) { + throw new Error(`register "${this.name}" cannot be used to save text`); + } + if ((flags & Register.Flags.CanWriteSelections) && !(f & Register.Flags.CanWriteSelections)) { + throw new Error(`register "${this.name}" cannot be used to save selections`); + } + } + + /** + * Returns the current register if it has the given flags, or throws an + * exception otherwise. + */ + public withFlags( + flags: F, + ): this extends Register.WithFlags ? this : never { + // Note: using `ensureFlags` below throws an exception. + this.checkFlags(flags); + + return this as any; + } +} + +export namespace Register { + /** + * Flags describing the capabilities of a `Register`. + */ + export const enum Flags { + /** Register does not have any capability. */ + None = 0, + + /** Strings can be read from the register. */ + CanRead = 1, + /** Strings can be written to the register. */ + CanWrite = 2, + + /** Selections can be read from the register. */ + CanReadSelections = 4, + /** Selections can be written to the register. */ + CanWriteSelections = 8, + + /** Command histories can be read from or written to the register. */ + CanReadWriteMacros = 16, + } + + /** + * Given a set of `Flags`, returns what interfaces correspond to these flags. + */ + export type InterfaceFromFlags + = (F extends Flags.CanRead ? Readable : never) + | (F extends Flags.CanReadSelections ? ReadableSelections : never) + | (F extends Flags.CanReadWriteMacros ? ReadableWriteableMacros : never) + | (F extends Flags.CanWrite ? Writeable : never) + | (F extends Flags.CanWriteSelections ? WriteableSelections : never) + ; + + /** + * Given a set of `Flags`, returns the `Register` type augmented with the + * interfaces that correspond to these flags. + */ + export type WithFlags = Register & InterfaceFromFlags; + + export interface Readable { + get(): Thenable; + } + + export interface Writeable { + set(values: readonly string[]): Thenable; + } + + export interface ReadableSelections { + getSelections(): readonly vscode.Selection[] | undefined; + getSelectionSet(): TrackedSelection.Set | undefined; + } + + export interface WriteableSelections { + replaceSelectionSet(selections?: TrackedSelection.Set): TrackedSelection.Set | undefined; + } + + export interface ReadableWriteableMacros { + getRecording(): Recording | undefined; + setRecording(recording: Recording): void; + } +} + +/** + * A general-purpose register, which supports all operations on registers. + */ +class GeneralPurposeRegister extends Register implements Register.Readable, + Register.Writeable, + Register.ReadableSelections, + Register.WriteableSelections, + Register.ReadableWriteableMacros { + public readonly flags = Register.Flags.CanRead + | Register.Flags.CanReadSelections + | Register.Flags.CanReadWriteMacros + | Register.Flags.CanWrite + | Register.Flags.CanWriteSelections; + + private _values?: readonly string[]; + private _recording?: Recording; + private _selections?: TrackedSelection.Set; + + public constructor( + public readonly name: string, + ) { + super(); + } + + public set(values: readonly string[]) { + this._values = values; + + return Promise.resolve(); + } + + public get() { + return Promise.resolve(this._values); + } + + public getSelections() { + return this._selections?.restore(); + } + + public getSelectionSet() { + return this._selections; + } + + public replaceSelectionSet(trackedSelections?: TrackedSelection.Set) { + const previousSelectionSet = this._selections; + + this._selections = trackedSelections; + + return previousSelectionSet; + } + + public getRecording() { + return this._recording; + } + + public setRecording(recording: Recording) { + this._recording = recording; + } +} + +/** + * A special register whose behavior is defined by the functions given to it. + */ +class SpecialRegister extends Register implements Register.Readable, + Register.Writeable { + public readonly flags = this.setter === undefined + ? Register.Flags.CanRead + : Register.Flags.CanRead | Register.Flags.CanWrite; + + public constructor( + public readonly name: string, + public readonly getter: () => Thenable, + public readonly setter?: (values: readonly string[]) => Thenable, + ) { + super(); + } + + public get() { + return this.getter(); + } + + public set(values: readonly string[]) { + if (this.setter === undefined) { + throw new Error("cannot set read-only register"); + } + + return this.setter(values); + } +} + +/** + * A special register that forwards operations to the system clipboard. + */ +class ClipboardRegister extends Register implements Register.Readable, + Register.Writeable { + private _lastStrings?: readonly string[]; + private _lastRawText?: string; + + public readonly name = '"'; + public readonly flags = Register.Flags.CanRead | Register.Flags.CanWrite; + + public get() { + return vscode.env.clipboard.readText().then((text) => + text === this._lastRawText ? this._lastStrings : [text], + ); + } + + public set(values: readonly string[]) { + let newline = "\n"; + + if (Context.currentOrUndefined?.document?.eol === vscode.EndOfLine.CRLF) { + newline = "\r\n"; + } + + this._lastStrings = values; + this._lastRawText = values.join(newline); + + return vscode.env.clipboard.writeText(this._lastRawText); + } +} + +function activeEditor() { + const activeEditor = vscode.window.activeTextEditor; + + EditorRequiredError.throwUnlessAvailable(activeEditor); + + return activeEditor; +} + +/** + * A set of registers. + */ +export abstract class RegisterSet { + private readonly _named = new Map(); + private readonly _letters = Array.from( + { length: 26 }, + (_, i) => new GeneralPurposeRegister(String.fromCharCode(97 + i)) as Register, + ); + private readonly _digits = Array.from( + { length: 10 }, + (_, i) => new SpecialRegister((i + 1).toString(), () => Promise.resolve(this._lastMatches[i])), + ); + + private _lastMatches: readonly (readonly string[])[] = []; + + /** + * The '"' (`dquote`) register, mapped to the system clipboard and default + * register for edit operations. + */ + public readonly dquote = new ClipboardRegister(); + + /** + * The "/" (`slash`) register, default register for search / regex operations. + */ + public readonly slash = new GeneralPurposeRegister("/"); + + /** + * The "@" (`arobase`) register, default register for recordings (aka macros). + */ + public readonly arobase = new GeneralPurposeRegister("@"); + + /** + * The "^" (`caret`) register, default register for saving selections. + */ + public readonly caret = new GeneralPurposeRegister("^"); + + /** + * The "|" (`pipe`) register, default register for outputs of external + * commands. + */ + public readonly pipe = new GeneralPurposeRegister("|"); + + /** + * The "%" (`percent`) register, mapped to the name of the current document. + */ + public readonly percent = new SpecialRegister( + "%", + () => Promise.resolve([activeEditor().document.fileName]), + (values) => { + if (values.length !== 1) { + return Promise.reject(new ArgumentError("a single file name must be selected")); + } + + return vscode.workspace.openTextDocument(values[0]).then(() => {}); + }, + ); + + /** + * The "." (`dot`) register, mapped to the contents of the current selections. + */ + public readonly dot = new SpecialRegister( + ".", + () => { + const editor = activeEditor(), + document = editor.document, + selectionBehavior = Context.currentOrUndefined?.mode?.selectionBehavior, + selections = selectionBehavior === SelectionBehavior.Character + ? Selections.fromCharacterMode(editor.selections, document) + : editor.selections; + + return Promise.resolve(selections.map(document.getText.bind(document))); + }, + (values) => { + const editor = activeEditor(); + + if (values.length !== editor.selections.length) { + return Promise.reject(new ArgumentError("as many selections as values must be given")); + } + + return editor.edit((editBuilder) => { + const document = editor.document, + selectionBehavior = Context.currentOrUndefined?.mode?.selectionBehavior, + selections = selectionBehavior === SelectionBehavior.Character + ? Selections.fromCharacterMode(editor.selections, document) + : editor.selections; + + for (let i = 0; i < selections.length; i++) { + editBuilder.replace(selections[i], values[i]); + } + }, noUndoStops).then((succeeded) => EditNotAppliedError.throwIfNotApplied(succeeded)); + }, + ); + + /** + * The read-only "#" (`hash`) register, mapped to the indices of the current + * selections. + */ + public readonly hash = new SpecialRegister("#", () => + Promise.resolve(activeEditor().selections.map((_, i) => i.toString())), + ); + + /** + * The read-only "_" (`underscore`) register, mapped to an empty string. + */ + public readonly underscore = new SpecialRegister("_", () => Promise.resolve([""])); + + /** + * The ":" (`colon`) register. + * + * In Kakoune it is mapped to the last entered command, but since we don't + * have access to that information in Dance, we map it to a prompt. + */ + public readonly colon = new SpecialRegister(":", () => + prompt({ prompt: ":" }).then((result) => [result]), + ); + + /** + * The `null` register, which forgets selections written to it and always + * returns no strings. + */ + public readonly null = new SpecialRegister( + "null", + () => Promise.resolve([]), + () => Promise.resolve(), + ); + + public constructor() { + for (const [longName, register] of [ + ["dquote", this.dquote] as const, + ["slash", this.slash] as const, + ["arobase", this.arobase] as const, + ["caret", this.caret] as const, + ["pipe", this.pipe] as const, + ["percent", this.percent] as const, + ["dot", this.dot] as const, + ["hash", this.hash] as const, + ["underscore", this.underscore] as const, + ["colon", this.colon] as const, + ]) { + this._letters[longName.charCodeAt(0) - 97 /* a */] = register; + this._named.set(longName, register); + } + + this._named.set("", this.null); + this._named.set("null", this.null); + } + + /** + * Returns the register with the given name or identified by the given key if + * the input is one-character long. + */ + public get(key: string): Register { + if (key.length === 1) { + const charCode = key.charCodeAt(0); + + switch (charCode) { + case 34: // " + return this.dquote; + case 47: // / + return this.slash; + case 64: // @ + return this.arobase; + case 94: // ^ + return this.caret; + case 124: // | + return this.pipe; + + case 37: // % + return this.percent; + case 46: // . + return this.dot; + case 35: // # + return this.hash; + case 95: // _ + return this.underscore; + case 58: // : + return this.colon; + + default: + if (charCode >= 97 /* a */ && charCode <= 122 /* z */) { + return this._letters[charCode - 97]; + } + + if (charCode >= 65 /* A */ && charCode <= 90 /* Z */) { + return this._letters[charCode - 65]; + } + + if (charCode >= 49 /* 1 */ && charCode <= 57 /* 9 */) { + return this._digits[charCode - 49]; + } + } + } + + key = key.toLowerCase(); + + let register = this._named.get(key.toLowerCase()); + + if (register === undefined) { + this._named.set(key, register = new GeneralPurposeRegister(key)); + } + + return register; + } + + /** + * Updates the contents of the numeric registers to hold the groups matched by + * the last `RegExp` search operation. + * + * @deprecated Do not call -- internal implementation detail. + */ + public updateRegExpMatches(matches: RegExpMatchArray[]) { + assert(matches.length > 0); + + const transposed = [] as string[][], + groupsCount = matches[0].length; + + for (let i = 1; i < groupsCount; i++) { + const strings = [] as string[]; + + for (const match of matches) { + strings.push(match[i]); + } + + transposed.push(strings); + } + + this._lastMatches = transposed; + } +} + +/** + * The set of all registers linked to a specific document. + */ +export class DocumentRegisters extends RegisterSet { + public constructor( + /** + * The document to which the registers are linked. + */ + public readonly document: vscode.TextDocument, + ) { + super(); + } +} + +/** + * The set of all registers. + */ +export class Registers extends RegisterSet { + private readonly _perDocument = new WeakMap(); + + /** + * Returns the registers linked to the given document. + */ + public forDocument(document: vscode.TextDocument) { + let registers = this._perDocument.get(document); + + if (registers === undefined) { + this._perDocument.set(document, registers = new DocumentRegisters(document)); + } + + return registers; + } +} diff --git a/src/state/status-bar.ts b/src/state/status-bar.ts new file mode 100644 index 0000000..3a4894e --- /dev/null +++ b/src/state/status-bar.ts @@ -0,0 +1,92 @@ +import * as vscode from "vscode"; + +/** + * Controls the Dance status bar item. + */ +export class StatusBar implements vscode.Disposable { + private readonly _segments: StatusBar.Segment[] = []; + + public readonly activeModeSegment: StatusBar.Segment; + public readonly recordingSegment: StatusBar.Segment; + public readonly countSegment: StatusBar.Segment; + public readonly registerSegment: StatusBar.Segment; + public readonly errorSegment: StatusBar.Segment; + + public constructor() { + this.activeModeSegment = this.addSegment("Dance - Set mode", "zap", "dance.modes.set"); + this.recordingSegment = + this.addSegment("Dance - Stop recording", "record", "dance.history.recording.stop"); + this.countSegment = this.addSegment( + "Dance - Reset count", + "symbol-number", + { command: "dance.updateCount", arguments: [{ input: "0" }], title: "" }, + ); + this.registerSegment = this.addSegment( + "Dance - Unset register", + "clone", + { command: "dance.selectRegister", arguments: [{ input: "" }], title: "" }, + ); + this.errorSegment = this.addSegment( + "Dance - Dismiss error", + "error", + "dance.ignore", + ); + this.errorSegment.statusBarItem.backgroundColor = + new vscode.ThemeColor("statusBarItem.errorBackground"); + } + + public dispose() { + this._segments.splice(0).forEach((s) => s.dispose()); + } + + private addSegment(tooltip: string, icon: string, command: string | vscode.Command) { + const segment = new StatusBar.Segment(tooltip, icon, 100 - this._segments.length, command); + + this._segments.push(segment); + + return segment; + } +} + +export namespace StatusBar { + export class Segment implements vscode.Disposable { + private readonly _statusBarItem: vscode.StatusBarItem; + + private _content?: string; + + public get content() { + return this._content; + } + + public get statusBarItem() { + return this._statusBarItem; + } + + public constructor( + public readonly name: string, + public readonly icon: string, + public readonly priority: number, + command: string | vscode.Command, + ) { + this._statusBarItem = + vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, priority); + this._statusBarItem.tooltip = name; + this._statusBarItem.command = command; + } + + public dispose() { + this._statusBarItem.dispose(); + } + + public setContent(content?: string) { + this._content = content; + + if (content) { + this._statusBarItem.text = `$(${this.icon}) ${content}`; + this._statusBarItem.show(); + } else { + this._statusBarItem.hide(); + } + } + } +} diff --git a/src/utils/assert.ts b/src/utils/assert.ts deleted file mode 100644 index f6ea76a..0000000 --- a/src/utils/assert.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function assert(condition: boolean) { - console.assert(condition); -} diff --git a/src/utils/charset.ts b/src/utils/charset.ts index d7a170a..76a6220 100644 --- a/src/utils/charset.ts +++ b/src/utils/charset.ts @@ -4,7 +4,10 @@ import * as vscode from "vscode"; // == CHARACTER SETS =========================================================================== // =============================================================================================== -const blankCharacters +/** + * A list containing all blank characters. + */ +export const blankCharacters = "\r\n\t " + String.fromCharCode( 0xa0, diff --git a/src/utils/disposables.ts b/src/utils/disposables.ts new file mode 100644 index 0000000..ef7f984 --- /dev/null +++ b/src/utils/disposables.ts @@ -0,0 +1,231 @@ +import * as vscode from "vscode"; +import { Context } from "../api"; +import { PerEditorState } from "../state/editors"; + +declare class WeakRef { + public constructor(value: T); + + public deref(): T | undefined; +} + +export interface NotifyingDisposable extends vscode.Disposable { + readonly onDisposed: vscode.Event; +} + +/** + * A wrapper around a set of `vscode.Disposable`s that can be scheduled to + * automatically be disposed (along with its wrapped disposables) when a certain + * event happens. + */ +export class AutoDisposable implements vscode.Disposable { + private _boundDispose?: AutoDisposable["dispose"] = this.dispose.bind(this); + private readonly _disposables: vscode.Disposable[]; + + public constructor(disposables: vscode.Disposable[] = []) { + this._disposables = disposables; + } + + /** + * Disposes of all the wrapped disposables. + */ + public dispose() { + this._boundDispose = undefined; + + const disposables = this._disposables; + + for (let i = 0, len = disposables.length; i < len; i++) { + disposables[i].dispose(); + } + + disposables.length = 0; + } + + /** + * Whether the `AutoDisposable` has been disposed of. + */ + public get isDisposed() { + return this._boundDispose === undefined; + } + + /** + * Adds a new disposable that will be disposed of when this `AutoDisposable` + * is itself disposed of. + * + * Calling this after disposing of the `AutoDisposable` will immediately + * dispose of the given disposable. + */ + public addDisposable(disposable: vscode.Disposable) { + if (this._boundDispose === undefined) { + disposable.dispose(); + return this; + } + + this._disposables.push(disposable); + + return this; + } + + public addNotifyingDisposable(disposable: NotifyingDisposable) { + return this.addDisposable(disposable).disposeOnEvent(disposable.onDisposed); + } + + /** + * Automatically disposes of this disposable when the given event is + * triggered. + */ + public disposeOnEvent(event: vscode.Event) { + const boundDispose = this._boundDispose; + + if (boundDispose !== undefined) { + this._disposables.push(event(boundDispose)); + } + + return this; + } + + /** + * Automatically disposes of this disposable when the given promise is + * resolved. + */ + public disposeOnPromiseResolution(thenable: Thenable) { + if (this._boundDispose === undefined) { + return this; + } + + const weakThis = new WeakRef(this); + + thenable.then(() => weakThis.deref()?.dispose()); + + return this; + } + + /** + * Automatically disposes of this disposable when the cancellation of the + * given `CancellationToken` is requested. + */ + public disposeOnCancellation(token: vscode.CancellationToken) { + if (this._boundDispose === undefined) { + return this; + } + + if (token.isCancellationRequested) { + this.dispose(); + + return this; + } + + return this.disposeOnEvent(token.onCancellationRequested); + } + + /** + * Automatically disposes of this disposable when `ms` milliseconds have + * elapsed. + */ + public disposeAfterTimeout(ms: number) { + const boundDispose = this._boundDispose; + + if (boundDispose === undefined) { + return this; + } + + const token = setTimeout(boundDispose, ms); + + this._disposables.push({ + dispose() { + clearTimeout(token); + }, + }); + + return this; + } + + public disposeOnUserEvent(event: AutoDisposable.Event, context: Context) { + const editorState = context.extension.editors.getState(context.editor as any)!; + let eventName: AutoDisposable.EventType, + eventOpts: Record; + + if (Array.isArray(event)) { + if (event.length === 0) { + throw new Error(); + } + + if (typeof event[0] === "string") { + eventName = event[0] as AutoDisposable.EventType; + } else { + throw new Error(); + } + + if (event.length === 2) { + eventOpts = event[1]; + + if (typeof eventOpts !== "object" || eventOpts === null) { + throw new Error(); + } + } else if (event.length === 1) { + eventOpts = {}; + } else { + throw new Error(); + } + } else if (typeof event === "string") { + eventName = event; + eventOpts = {}; + } else { + throw new Error(); + } + + switch (eventName) { + case AutoDisposable.EventType.OnEditorWasClosed: + this.disposeOnEvent(editorState.onEditorWasClosed); + break; + + case AutoDisposable.EventType.OnModeDidChange: + const except = [] as string[]; + + if (Array.isArray(eventOpts.except)) { + except.push(...eventOpts.except); + } else if (typeof eventOpts.except === "string") { + except.push(eventOpts.except); + } + + const include = [] as string[]; + + if (Array.isArray(eventOpts.include)) { + include.push(...eventOpts.include); + } else if (typeof eventOpts.include === "string") { + include.push(eventOpts.include); + } + + editorState.extension.editors.onModeDidChange((e) => { + if (e === editorState && !except.includes(e.mode.name) + && (include.length === 0 || include.includes(e.mode.name))) { + this.dispose(); + } + }, undefined, this._disposables); + break; + + case AutoDisposable.EventType.OnSelectionsDidChange: + vscode.window.onDidChangeTextEditorSelection((e) => { + if (editorState.editor === e.textEditor + && e.kind === vscode.TextEditorSelectionChangeKind.Mouse) { + this.dispose(); + } + }, undefined, this._disposables); + break; + + default: + throw new Error(); + } + } +} + +export namespace AutoDisposable { + export const enum EventType { + OnEditorWasClosed = "editor-was-closed", + OnModeDidChange = "mode-did-change", + OnSelectionsDidChange = "selections-did-change", + } + + export type Event = EventType + | readonly [EventType.OnModeDidChange, + { except?: string | string[]; include?: string | string[] }]; +} diff --git a/src/utils/misc.ts b/src/utils/misc.ts new file mode 100644 index 0000000..71ac864 --- /dev/null +++ b/src/utils/misc.ts @@ -0,0 +1,59 @@ +import * as vscode from "vscode"; + +import { Context, prompt, Selections } from "../api"; +import { Input, SetInput } from "../commands"; + +/** + * An object passed to `vscode.TextEditor.edit` to indicate that no undo stops + * should be implicitly inserted. + */ +export const noUndoStops: Parameters[1] = + Object.freeze({ undoStopBefore: false, undoStopAfter: false }); + +const dummyPosition = new vscode.Position(0, 0), + dummyRange = new vscode.Range(dummyPosition, dummyPosition), + dummyUndoStops: Parameters[1] = + Object.freeze({ undoStopBefore: false, undoStopAfter: true }); + +/** + * Performs a dummy edit on a text document, inserting an undo stop. + */ +export function performDummyEdit(editor: vscode.TextEditor) { + // VS Code ignores edits where no interaction is performed with the editor, so + // we delete an empty range at the start of the document. + return editor + .edit((editBuilder) => editBuilder.delete(dummyRange), dummyUndoStops) + .then(() => {}); +} + +export async function manipulateSelectionsInteractively( + _: Context, + input: Input, + setInput: SetInput, + interactive: boolean, + options: vscode.InputBoxOptions, + f: (input: string | I, selections: readonly vscode.Selection[]) => Thenable, +) { + const selections = _.selections; + + function execute(input: string | I) { + return _.runAsync(() => f(input, selections)); + } + + function undo() { + Selections.set(selections); + } + + if (input === undefined) { + setInput(await prompt.interactive(execute, undo, options, interactive)); + } else { + await execute(input); + } +} + +export const workspaceSettingsPropertyNames = [ + "workspaceFolderValue", + "workspaceFolderLanguageValue", + "workspaceValue", + "workspaceLanguageValue", +] as const; \ No newline at end of file diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts deleted file mode 100644 index 8f893b8..0000000 --- a/src/utils/prompt.ts +++ /dev/null @@ -1,131 +0,0 @@ -import * as vscode from "vscode"; - -export function prompt(opts: vscode.InputBoxOptions, cancellationToken?: vscode.CancellationToken) { - return vscode.window.showInputBox(opts, cancellationToken); -} - -export function promptRegex(flags?: string, cancellationToken?: vscode.CancellationToken) { - return prompt( - { - prompt: "Selection RegExp", - validateInput(input: string) { - try { - new RegExp(input); - - return undefined; - } catch { - return "Invalid ECMA RegExp."; - } - }, - }, - cancellationToken, - ).then((x) => x === undefined ? undefined : new RegExp(x, flags)); -} - -export function keypress( - cancellationToken?: vscode.CancellationToken, -): Thenable { - return new Promise((resolve, reject) => { - try { - let done = false; - const subscription = vscode.commands.registerCommand("type", ({ text }: { text: string }) => { - if (!done) { - subscription.dispose(); - done = true; - - resolve(text); - } - }); - - cancellationToken?.onCancellationRequested(() => { - if (!done) { - subscription.dispose(); - done = true; - - resolve(undefined); - } - }); - } catch { - reject(new Error("Unable to listen to keyboard events; is an extension " - + 'overriding the "type" command (e.g VSCodeVim)?')); - } - }); -} - -export function promptInList( - canPickMany: true, - items: [string, string][], - cancellationToken?: vscode.CancellationToken, -): Thenable; -export function promptInList( - canPickMany: false, - items: [string, string][], - cancellationToken?: vscode.CancellationToken, -): Thenable; - -export function promptInList( - canPickMany: boolean, - items: [string, string][], - cancellationToken?: vscode.CancellationToken, -): Thenable { - return new Promise((resolve) => { - const quickPick = vscode.window.createQuickPick(), - quickPickItems = [] as vscode.QuickPickItem[]; - - let isCaseSignificant = false; - - for (let i = 0; i < items.length; i++) { - const [label, description] = items[i]; - - quickPickItems.push({ label, description }); - isCaseSignificant = isCaseSignificant || label.toLowerCase() !== label; - } - - quickPick.title = "Object"; - quickPick.items = quickPickItems; - quickPick.placeholder = "Press one of the below keys."; - quickPick.onDidChangeValue((key) => { - if (!isCaseSignificant) { - key = key.toLowerCase(); - } - - const index = items.findIndex((x) => x[0].split(", ").includes(key)); - - quickPick.dispose(); - - if (canPickMany) { - resolve(index === -1 ? undefined : [index]); - } else { - resolve(index === -1 ? undefined : index); - } - }); - - quickPick.onDidAccept(() => { - let picked = quickPick.selectedItems; - - if (picked !== undefined && picked.length === 0) { - picked = quickPick.activeItems; - } - - quickPick.dispose(); - - if (picked === undefined) { - resolve(undefined); - } - - if (canPickMany) { - resolve(picked.map((x) => items.findIndex((item) => item[1] === x.description))); - } else { - resolve(items.findIndex((x) => x[1] === picked[0].description)); - } - }); - - cancellationToken?.onCancellationRequested(() => { - quickPick.dispose(); - - resolve(undefined); - }); - - quickPick.show(); - }); -} diff --git a/src/utils/regexp.ts b/src/utils/regexp.ts new file mode 100644 index 0000000..e2512ba --- /dev/null +++ b/src/utils/regexp.ts @@ -0,0 +1,1705 @@ +import { group } from "console"; +import { assert } from "../api/errors"; + +/** + * Returns whether this `RegExp` may match on a string that contains a `\n` + * character. + * + * @see https://tc39.es/ecma262/#sec-regexp-regular-expression-objects + */ +export function canMatchLineFeed(re: RegExp) { + // The functions defined below return an integer >= 0 to indicate "keep + // processing at index `i`", and -1 to indicate "a line feed character may be + // matched by this RegExp". + return groupCanMatchLineFeed(0, re, false) === -1; +} + +function groupCanMatchLineFeed(i: number, re: RegExp, inverse: boolean) { + for (const src = re.source; i !== -1 && i < src.length;) { + switch (src.charCodeAt(i)) { + case 41: // ')' + return i + 1; // End of a group we're processing. + + case 40: // '(' + if (src.charCodeAt(i + 1) === 63 /* ? */) { + const next = src.charCodeAt(i + 2); + + if (next === 33 /* ! */) { + i = groupCanMatchLineFeed(i + 3, re, !inverse); + continue; + } else if (next === 61 /* = */ || next === 58 /* : */) { + i += 2; + } else if (next === 60 /* < */) { + i += 3; + + if (src.charCodeAt(i) === 33 /* ! */) { + i = groupCanMatchLineFeed(i + 1, re, !inverse); + continue; + } else if (src.charCodeAt(i) === 61 /* = */) { + i++; + } else { + while (src.charCodeAt(i) !== 62 /* > */) { + i++; + } + } + } else { + assert(false); + } + } + i = groupCanMatchLineFeed(i + 1, re, inverse); + break; + + case 92: // '\' + i = escapedCharacterCanMatchLineFeed(i + 1, re, inverse); + break; + + case 91: // '[' + i = characterSetCanMatchLineFeed(i + 1, re, inverse); + break; + + case 46: // '.' + if (re.dotAll || inverse) { + return -1; + } + i++; + break; + + case 43: // '+' + case 42: // '*' + case 63: // '?' + case 124: // '|' + i++; + break; + + case 123: // '{' + i++; + while (src.charCodeAt(i - 1) !== 125 /* } */) { + i++; + } + break; + + case 93: // ']' + case 125: // '}' + assert(false); + break; + + case 36: // '$' + case 94: // '^' + case 10: // '\n' + if (!inverse) { + return -1; + } + i++; + break; + + default: + if (inverse) { + return -1; + } + i++; + break; + } + } + + return i; +} + +function isDigit(charCode: number) { + return charCode >= 48 /* 0 */ && charCode <= 57 /* 9 */; +} + +function isRange(src: string, i: number, n: number, startInclusive: number, endInclusive: number) { + if (i + n >= src.length) { + return false; + } + + for (let j = 0; j < n; j++) { + const chr = src.charCodeAt(i + j); + + if (chr < startInclusive || chr > endInclusive) { + return false; + } + } + + return true; +} + +function isHex(src: string, i: number, n: number) { + if (i + n >= src.length) { + return false; + } + + for (let j = 0; j < n; j++) { + const c = src.charCodeAt(i + j); + + // '0' '9' 'a' 'f' 'A' 'F' + if ((c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70)) { + continue; + } + + return false; + } + + return true; +} + +function escapedCharacterCanMatchLineFeed(i: number, re: RegExp, inverse: boolean) { + const src = re.source, + chr = src.charCodeAt(i); + + switch (chr) { + case 110: // 'n' + case 115: // 's' + case 66: // 'B' + case 68: // 'D' + case 87: // 'W' + return inverse ? i + 1 : -1; + + case 48: // '0' + if (!isRange(src, i + 1, 2, 48 /* 0 */, 55 /* 7 */)) { + return inverse ? -1 : i + 1; + } + if (src.charCodeAt(i + 1) === 49 /* 1 */ && src.charCodeAt(i + 2) === 50 /* 2 */) { + // 012 = 10 = '\n' + return inverse ? i + 3 : -1; + } + return inverse ? -1 : i + 3; + + case 99: // 'c' + const controlCharacter = src.charCodeAt(i + 1); + + if (controlCharacter === 74 /* J */ || controlCharacter === 106 /* j */) { + return inverse ? i + 2 : -1; + } + return inverse ? -1 : i + 2; + + case 120: // 'x' + if (!isHex(src, i + 1, 2)) { + return inverse ? -1 : i + 1; + } + if (src.charCodeAt(i + 1) === 48 /* 0 */) { + const next = src.charCodeAt(i + 2); + + if (next === 97 /* a */ || next === 65 /* A */) { + // 0x0A = 10 = \n + return inverse ? i + 3 : -1; + } + } + return inverse ? -1 : i + 3; + + case 117: // 'u' + if (src.charCodeAt(i + 1) === 123 /* { */) { + i += 2; + + let x = 0; + + for (let ch = src.charCodeAt(i); ch !== 125 /* } */; i++) { + const v = ch >= 48 /* 0 */ && ch <= 57 /* 9 */ + ? ch - 48 /* 0 */ + : ch >= 97 /* a */ && ch <= 102 /* f */ + ? 10 + ch - 97 /* a */ + : 10 + ch - 65 /* A */; + + x = x * 16 + v; + } + + if (x === 10 /* \n */) { + return inverse ? i + 1 : -1; + } + return inverse ? -1 : i + 1; + } + if (!isHex(src, i + 1, 4)) { + return inverse ? -1 : i + 1; + } + if (src.charCodeAt(i + 1) === 48 /* 0 */ + && src.charCodeAt(i + 2) === 48 /* 0 */ + && src.charCodeAt(i + 3) === 48 /* 0 */) { + const next = src.charCodeAt(i + 4); + + if (next === 97 /* a */ || next === 65 /* A */) { + // 0x000A = 10 = \n + return inverse ? i + 5 : -1; + } + } + return inverse ? -1 : i + 5; + + // @ts-ignore + case 80: // 'P' + if (!re.unicode) { + return inverse ? -1 : i + 1; + } + inverse = !inverse; + // fallthrough + + case 112: // 'p' + if (!re.unicode) { + return inverse ? -1 : i + 1; + } + const start = i - 1; + + i += 2; // Skip over 'p{'. + + while (src.charCodeAt(i) !== 125 /* } */) { + i++; + } + + i++; // Skip over '}'. + + const testRegExpString = src.slice(start, i), + testRegExp = new RegExp(testRegExpString, "u"); + + if (testRegExp.test("\n")) { + return inverse ? i : -1; + } + return inverse ? -1 : i; + + default: + if (chr > 48 /* 0 */ && chr <= 57 /* 9 */) { + // Back-reference is treated by the rest of the processing. + i++; + + while (isDigit(src.charCodeAt(i))) { + i++; + } + + return i; + } + + return inverse ? -1 : i + 1; + } +} + +function characterSetCanMatchLineFeed(i: number, re: RegExp, inverse: boolean) { + const src = re.source, + start = i - 1; + + if (src.charCodeAt(i) === 94 /* ^ */) { + if (src.charCodeAt(i + 1) === 93 /* ] */) { + return inverse ? i + 2 : -1; + } + + i++; + inverse = !inverse; + } + + for (let mayHaveRange = false;;) { + switch (src.charCodeAt(i)) { + case 93: // ']' + if (mayHaveRange) { + // The test below handles inversions, so we must toggle `inverse` if we + // toggled it earlier. + if (src.charCodeAt(start + 2) === 94 /* ^ */) { + inverse = !inverse; + } + + const testRegExpString = src.slice(start, i + 1), + testRegExp = new RegExp(testRegExpString, re.flags); + + if (testRegExp.test("\n")) { + if (!inverse) { + return -1; + } + } else if (inverse) { + return -1; + } + } + return i + 1; + + case 92: // '\' + i = escapedCharacterCanMatchLineFeed(i + 1, re, inverse); + if (i === -1) { + return -1; + } + break; + + case 10: // '\n' + if (!inverse) { + return -1; + } + i++; + break; + + case 45: // '-' + mayHaveRange = true; + break; + + default: + if (inverse) { + return -1; + } + i++; + break; + } + } +} + +const mustBeEscapedToBeStatic = + new Uint8Array([..."()[]{}*+?^$."].map((c) => c.charCodeAt(0))), + mustNotBeEscapedToBeStatic = + new Uint8Array([..."123456789wWdDsSpPbBu"].map((c) => c.charCodeAt(0))); + +/** + * Returns the set of strings that the specified `RegExp` may match. If + * `undefined`, this `RegExp` can match dynamic strings. + */ +export function matchesStaticStrings(re: RegExp) { + const alternatives = [] as string[], + source = re.source; + let alt = ""; + + for (let i = 0, len = source.length; i < len; i++) { + const ch = source.charCodeAt(i); + + if (ch === 124 /* | */) { + if (!alternatives.includes(alt)) { + alternatives.push(alt); + } + + alt = ""; + } else if (ch === 92 /* \ */) { + i++; + + if (i === source.length) { + break; + } + + const next = source.charCodeAt(i); + + switch (next) { + case 110: // n + alt += "\n"; + break; + case 114: // r + alt += "\r"; + break; + case 116: // t + alt += "\t"; + break; + case 102: // f + alt += "\f"; + break; + case 118: // v + alt += "\v"; + break; + + case 99: // c + const controlCh = source.charCodeAt(i + 1), + isUpper = 65 <= controlCh && controlCh <= 90, + offset = (isUpper ? 65 /* A */ : 107 /* a */) - 1; + + alt += String.fromCharCode(controlCh - offset); + break; + + case 48: // 0 + if (isRange(source, i + 1, 2, 48 /* 0 */, 55 /* 7 */)) { + alt += String.fromCharCode(parseInt(source.substr(i + 1, 2), 8)); + i += 2; + } else { + alt += "\0"; + } + break; + + case 120: // x + if (isHex(source, i + 1, 2)) { + alt += String.fromCharCode(parseInt(source.substr(i + 1, 2), 16)); + i += 2; + } else { + alt += "x"; + } + break; + + case 117: // u + if (source.charCodeAt(i + 1) === 123 /* { */) { + const end = source.indexOf("}", i + 2); + + if (end === -1) { + return; + } + + alt += String.fromCharCode(parseInt(source.slice(i + 2, end), 16)); + i = end + 1; + } else if (isHex(source, i + 1, 4)) { + alt += String.fromCharCode(parseInt(source.substr(i + 1, 4), 16)); + i += 4; + } else { + alt += "u"; + } + return; + + default: + if (mustNotBeEscapedToBeStatic.indexOf(next) !== -1) { + return; + } + + alt += source[i]; + break; + } + } else { + if (mustBeEscapedToBeStatic.indexOf(ch) !== -1) { + return; + } + + alt += source[i]; + } + } + + if (!alternatives.includes(alt)) { + alternatives.push(alt); + } + + return alternatives; +} + +export interface Node> { + toString(): string; + + firstCharacter(): CharacterSet | undefined; + reverse(state: Node.ReverseState): To; +} + +export namespace Node { + export type Inner> = T extends Node ? R : never; + + export interface ReverseState { + readonly expression: Expression; + readonly reversedGroups: (Group | undefined)[]; + } +} + +export class Sequence implements Node { + public constructor( + public readonly nodes: readonly Sequence.Node[], + ) {} + + public toString() { + return this.nodes.join(""); + } + + public reverse(state: Node.ReverseState): Sequence { + return new Sequence([...this.nodes.map((n) => n.reverse(state))].reverse()); + } + + public firstCharacter() { + for (const node of this.nodes) { + const firstCharacter = node.firstCharacter(); + + if (firstCharacter !== undefined) { + return firstCharacter; + } + } + + return undefined; + } +} + +export namespace Sequence { + export type Node = Repeat | Anchor; +} + +export abstract class Disjunction> implements Node { + public constructor( + public readonly alternatives: readonly Sequence[], + ) {} + + protected prefix() { + return "("; + } + + protected suffix() { + return ")"; + } + + public toString() { + return this.prefix() + this.alternatives.join() + this.suffix(); + } + + public firstCharacter(): CharacterSet | undefined { + const firstCharacters = this.alternatives + .map((a) => a.firstCharacter()) + .filter((x) => x !== undefined) as CharacterSet[]; + + if (firstCharacters.length === 0) { + return undefined; + } + + return firstCharacters[0].merge(...firstCharacters.slice(1)); + } + + public abstract reverse(state: Node.ReverseState): To; +} + +export class Group extends Disjunction { + public constructor( + alternatives: readonly Sequence[], + public readonly index?: number, + public readonly name?: string, + ) { + super(alternatives); + } + + protected prefix() { + if (this.name !== undefined) { + return "(?<" + this.name + ">"; + } + if (this.index === undefined) { + return "(?:"; + } + return "("; + } + + public reverse(state: Node.ReverseState): Group | NumericEscape | Backreference { + if (this.index !== undefined && state.reversedGroups[this.index - 1] !== undefined) { + return new NumericEscape(this.index); + } + + return new Group( + this.alternatives.map((a) => a.reverse(state) as Sequence), + this.index, + this.name, + ); + } +} + +export class Raw implements Node { + public constructor( + public readonly string: string, + ) {} + + public toString() { + return this.string.replace(/[()[\]{}*+?^$.]/g, "\\$&"); + } + + public reverse() { + return new Raw([...this.string].reverse().join()); + } + + public firstCharacter() { + return new CharacterSet([this], false); + } + + public static readonly a = new Raw("a"); + public static readonly z = new Raw("z"); + public static readonly A = new Raw("A"); + public static readonly Z = new Raw("Z"); + public static readonly _ = new Raw("_"); + public static readonly _0 = new Raw("0"); + public static readonly _9 = new Raw("9"); + public static readonly newLine = new Raw("\n"); +} + +export class CharacterSet implements Node { + public constructor( + public readonly alternatives: readonly CharacterSet.Alternative[], + public readonly isNegated: boolean, + ) {} + + public toString() { + if (this === CharacterSet.digit) { + return "\\d"; + } + if (this === CharacterSet.notDigit) { + return "\\D"; + } + if (this === CharacterSet.word) { + return "\\w"; + } + if (this === CharacterSet.notWord) { + return "\\W"; + } + if (this === CharacterSet.whitespace) { + return "\\s"; + } + if (this === CharacterSet.notWhitespace) { + return "\\S"; + } + + const contents = this.alternatives.map( + (c) => Array.isArray(c) ? `${c[0]}-${c[1]}` : c.toString(), + ).join(""); + + return `[${this.isNegated ? "^" : ""}${contents}]`; + } + + public negate() { + return new CharacterSet(this.alternatives, !this.isNegated); + } + + public makePositive(hasUnicodeFlag?: boolean) { + if (!this.isNegated) { + return this; + } + + const characterClasses = [] as CharacterClass[], + ranges = [0, hasUnicodeFlag ? 0x10FFFF : 0xFFFF]; + + for (let alternative of this.alternatives) { + if (!Array.isArray(alternative)) { + if (alternative instanceof CharacterClass) { + characterClasses.push(alternative.negate()); + continue; + } + + alternative = [alternative, alternative]; + } + + const [alt0, alt1] = alternative, + negStart = alt0 instanceof Raw ? alt0.string.charCodeAt(0) : alt0.value, // S + negEnd = alt1 instanceof Raw ? alt1.string.charCodeAt(0) : alt1.value; // E + + for (let i = 0; i < ranges.length; i += 2) { + // Let's consider that the range we need to erase is S -> E, and the + // positive range we're erasing from is s -> e. + const posStart = ranges[i], // s + posEnd = ranges[i + 1]; // e + + if (negEnd < posStart || negStart > posEnd) { + // S E s e or s e S E + // ^^^ ^^^ nothing to erase + } else if (negStart <= posStart) { + // S s ... + if (negEnd >= posEnd) { + // S s e E + // ^^^^^^^ erased + ranges.splice(i, 2); + i -= 2; + } else { + // S s E e + // ^^^^^ erased, s becomes E + 1 + ranges[i] = negEnd + 1; + } + } else if (negEnd >= posEnd) { + // s S e E + // ^^^^^ erased, e becomes S - 1 + ranges[i + 1] = negStart - 1; + } else { + // s S E e + // ^^^ erase, e becomes S - 1 and a new range is added + ranges[i + 1] = negStart - 1; + ranges.splice(i + 2, 0, negEnd + 1, posEnd); + } + } + } + + const alternatives = characterClasses as CharacterSet.Alternative[]; + + for (let i = 0; i < ranges.length; i += 2) { + const rangeStart = ranges[i], + rangeEnd = ranges[i + 1]; + + if (rangeStart === rangeEnd) { + alternatives.push(Escaped.fromCharCode(rangeStart)); + } else { + alternatives.push(Escaped.fromCharCode(rangeStart), Escaped.fromCharCode(rangeEnd)); + } + } + + alternatives.push(...characterClasses); + + return new CharacterSet(alternatives, false); + } + + public merge(...others: readonly CharacterSet[]) { + const alternatives = + this.makePositive().alternatives.slice() as CharacterSet.Alternative[]; + + for (const other of others) { + for (const alternative of other.makePositive().alternatives) { + if (alternatives.indexOf(alternative) === -1) { + alternatives.push(alternative); + } + } + } + + return new CharacterSet(alternatives, false); + } + + public reverse() { + return this; + } + + public firstCharacter() { + return this; + } + + public static readonly digit = new CharacterSet([[Raw._0, Raw._9]], false); + public static readonly word = new CharacterSet( + [[Raw._0, Raw._9], [Raw.a, Raw.z], [Raw.A, Raw.Z], Raw._], false); + public static readonly whitespace = new CharacterSet( + [..."\t\n\v\f\r \u00a0\u2000\u2001\u2002\u2003\u2004\u2005", + ..."\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000"].map((c) => new Raw(c)), false); + + public static readonly notDigit = new CharacterSet(CharacterSet.digit.alternatives, true); + public static readonly notWord = new CharacterSet(CharacterSet.word.alternatives, true); + public static readonly notWhitespace = new CharacterSet( + CharacterSet.whitespace.alternatives, true); +} + +export namespace CharacterSet { + export type AlternativeAtom = Raw | Escaped; + export type Alternative = AlternativeAtom | CharacterClass | [AlternativeAtom, AlternativeAtom]; +} + +export class Escaped implements Node { + public constructor( + public readonly type: "x" | "u" | "c" | "0", + public readonly value: number, + ) {} + + public toString() { + const type = this.type, + value = this.value, + str = type === "0" + ? value.toString(8).padStart(2, "0") + : type === "c" + ? String.fromCharCode(65 + value) + : type === "x" + ? value.toString(16).padStart(2, "0") + : value <= 0xFFFF + ? value.toString(16).padStart(4, "0") + : "{" + value.toString(16) + "}"; + + return "\\" + type + str; + } + + public reverse() { + return this; + } + + public firstCharacter() { + return new CharacterSet([this], false); + } + + public static fromCharCode(charCode: number) { + if (charCode >= 32 && charCode <= 126) { + return new Raw(String.fromCharCode(charCode)); + } + if (charCode < 0xFF) { + return new Escaped("x", charCode); + } + return new Escaped("u", charCode); + } +} + +export class Dot implements Node { + private constructor( + public readonly includesNewLine: boolean, + ) {} + + public toString() { + return "."; + } + + public reverse() { + return this; + } + + public firstCharacter() { + return new CharacterSet(this.includesNewLine ? [] : [Raw.newLine], true); + } + + public static readonly includingNewLine = new Dot(true); + public static readonly excludingNewLine = new Dot(false); +} + +export class CharacterClass implements Node { + public constructor( + public readonly characterClass: string, + public readonly isNegative: boolean, + ) {} + + public toString() { + return `\\${this.isNegative ? "P" : "p"}{${this.characterClass}}`; + } + + public negate() { + return new CharacterClass(this.characterClass, !this.isNegative); + } + + public reverse() { + return this; + } + + public firstCharacter() { + return new CharacterSet([this], false); + } +} + +const enum AnchorKind { + Start, + End, + Boundary, + NotBoundary, +} + +export class Anchor implements Node { + private constructor( + public readonly kind: Anchor.Kind, + public readonly string: string, + ) {} + + public toString() { + return this.string; + } + + public reverse() { + return this; + } + + public firstCharacter() { + return undefined; + } + + public static readonly start = new Anchor(AnchorKind.Start, "^"); + public static readonly end = new Anchor(AnchorKind.End, "$"); + public static readonly boundary = new Anchor(AnchorKind.Boundary, "\\b"); + public static readonly notBoundary = new Anchor(AnchorKind.NotBoundary, "\\B"); +} + +export class Lookaround extends Disjunction { + public constructor( + alternatives: readonly Sequence[], + public readonly isNegative: boolean, + public readonly isLookbehind: boolean, + ) { + super(alternatives); + } + + protected prefix() { + return "(?" + (this.isLookbehind ? "<" : "") + (this.isNegative ? "!" : "="); + } + + public reverse(state: Node.ReverseState) { + return new Lookaround( + this.alternatives.map((a) => a.reverse(state)), + this.isNegative, + this.isLookbehind, + ); + } +} + +export namespace Anchor { + export type Kind = AnchorKind; +} + +export class Repeat implements Node { + public constructor( + public readonly node: T, + public readonly min?: number, + public readonly max?: number, + public readonly lazy = false, + ) {} + + public get isStar() { + return this.min === undefined && this.max === undefined; + } + + public get isPlus() { + return this.min === 1 && this.max === undefined; + } + + public get isOptional() { + return this.min === undefined && this.max === 1; + } + + public get isNonRepeated() { + return this.min === 1 && this.max === 1 && !this.lazy; + } + + public toString() { + const node = this.node.toString(), + lazy = this.lazy ? "?" : ""; + + if (this.isOptional) { + return node + "?" + lazy; + } + + if (this.isPlus) { + return node + "+" + lazy; + } + + if (this.isStar) { + return node + "*" + lazy; + } + + return `${node}{${this.min ?? ""},${this.max ?? ""}}${lazy}`; + } + + public reverse(state: Node.ReverseState): Repeat> { + const reversed = this.node.reverse(state) as any; + + if (reversed === this.node) { + return this as any; + } + + return new Repeat(reversed, this.min, this.max, this.lazy); + } + + public firstCharacter() { + return this.node.firstCharacter(); + } +} + +export namespace Repeat { + export type Node = Group | Lookaround | CharacterSet | Raw | Escaped | CharacterClass | Dot + | NumericEscape | Backreference; +} + +export class NumericEscape implements Node { + public constructor( + public n: number, + ) { + assert(n > 0); + } + + public toString() { + return "\\" + this.n; + } + + public reverse(state: Node.ReverseState) { + const i = this.n - 1; + + if (i >= state.reversedGroups.length || state.reversedGroups[i] !== undefined) { + return this; + } + + const group = state.expression.groups[i]; + + return state.reversedGroups[i] = new Group( + group.alternatives.map((a) => a.reverse(state)), + group.index, + group.name, + ); + } + + public firstCharacter() { + return undefined; + } +} + +export class Backreference implements Node { + public constructor( + public readonly name: string, + ) {} + + public toString() { + return "\\k<" + this.name + ">"; + } + + public reverse(state: Node.ReverseState) { + const n = state.expression.groups.findIndex((g) => g.name === this.name); + + assert(n !== -1); + + if (state.reversedGroups[n] !== undefined) { + return this; + } + + const group = state.expression.groups[n]; + + return state.reversedGroups[n] = new Group( + group.alternatives.map((a) => a.reverse(state)), + group.index, + group.name, + ); + } + + public firstCharacter() { + return undefined; + } +} + +export class Expression extends Disjunction { + public constructor( + public readonly re: RegExp, + public readonly groups: readonly Group[], + alternatives: readonly Sequence[], + ) { + super(alternatives); + } + + protected prefix() { + return ""; + } + + protected suffix() { + return ""; + } + + public reverse(state?: Node.ReverseState) { + if (state === undefined) { + state = { + expression: this, + reversedGroups: Array.from(this.groups, () => undefined), + }; + } + + const alternatives = this.alternatives.map((a) => a.reverse(state!)), + re = new RegExp(alternatives.join(""), this.re.flags); + + assert(state.reversedGroups.indexOf(undefined) === -1); + + return new Expression(re, state.reversedGroups as Group[], alternatives); + } +} + +export const enum CharCodes { + LF = 10, + Bang = 33, + Dollar = 36, + LParen = 40, + RParen = 41, + Star = 42, + Plus = 43, + Comma = 44, + Minus = 45, + Dot = 46, + Colon = 58, + LAngle = 60, + Eq = 61, + RAngle = 62, + Question = 63, + LBracket = 91, + Backslash = 92, + RBracket = 93, + Caret = 94, + LCurly = 123, + Pipe = 124, + RCurly = 125, +} + +/** + * Returns the AST of the given `RegExp`. + */ +export function parse(re: RegExp) { + const dummyGroup = new Group([], undefined); + + const src = re.source, + groups = [] as Group[]; + let i = 0; + + function repeat(node: T) { + const ch = src.charCodeAt(i); + let min: number | undefined, + max: number | undefined; + + if (ch === CharCodes.Star) { + i++; + min = 0; + } else if (ch === CharCodes.Plus) { + i++; + min = 1; + } else if (ch === CharCodes.LCurly) { + i++; + + const start = i; + + for (;;) { + const ch = src.charCodeAt(i++); + + if (isDigit(ch)) { + continue; + } + + if (ch === CharCodes.RCurly) { + min = max = +src.slice(start, i - 1); + break; + } + + if (ch === CharCodes.Comma) { + min = +src.slice(start, i - 1); + + const end = i; + + for (;;) { + const ch = src.charCodeAt(i++); + + if (isDigit(ch)) { + continue; + } + + if (ch === CharCodes.RCurly) { + max = +src.slice(end, i - 1); + break; + } + + i = start - 1; + + return new Repeat(node, 1, 1, false); + } + + break; + } + + i = start - 1; + + return new Repeat(node, 1, 1, false); + } + } else if (ch === CharCodes.Question) { + i++; + min = 0; + max = 1; + } else { + return new Repeat(node, 1, 1, false); + } + + let lazy = false; + + if (src.charCodeAt(i) === CharCodes.Question) { + lazy = true; + i++; + } + + return new Repeat(node, min, max, lazy); + } + + function escapedCharacter( + inCharSet: InCharSet, + ): Raw | CharacterSet | Escaped | CharacterClass + | (InCharSet extends true ? never : NumericEscape | Backreference) { + switch (src.charCodeAt(i++)) { + case 110: // n + return new Raw("\n"); + case 114: // r + return new Raw("\r"); + case 116: // t + return new Raw("\t"); + case 102: // f + return new Raw("\f"); + case 118: // v + return new Raw("\n"); + + case 119: // w + return CharacterSet.word; + case 87: // W + return CharacterSet.notWord; + + case 100: // d + return CharacterSet.digit; + case 68: // D + return CharacterSet.notDigit; + + case 115: // s + return CharacterSet.whitespace; + case 83: // S + return CharacterSet.notWhitespace; + + case 99: // c + const controlCh = src.charCodeAt(i), + isUpper = 65 <= controlCh && controlCh <= 90, + offset = (isUpper ? 65 /* A */ : 107 /* a */) - 1, + value = controlCh - offset; + + i++; + return new Escaped("c", value); + + case 48: // 0 + if (isRange(src, i, 2, 48 /* 0 */, 55 /* 7 */)) { + const value = parseInt(src.substr(i, 2), 8); + + i += 2; + return new Escaped("0", value); + } else { + return new Raw("\0"); + } + + case 120: // x + if (isHex(src, i, 2)) { + const value = parseInt(src.substr(i, 2), 16); + + i += 2; + return new Escaped("x", value); + } else { + return new Raw("x"); + } + + case 117: // u + if (src.charCodeAt(i) === CharCodes.LCurly) { + const end = src.indexOf("}", i + 1); + + assert(end !== -1); + + const value = parseInt(src.slice(i + 1, end), 16); + + i = end + 1; + return new Escaped("u", value); + } else if (isHex(src, i, 4)) { + const value = parseInt(src.substr(i, 4), 16); + + i += 4; + return new Escaped("u", value); + } else { + return new Raw("u"); + } + + case 112: // p + case 80: // P + assert(src.charCodeAt(i) === CharCodes.LCurly); + + const start = i + 1, + end = src.indexOf("}", start); + + assert(end > start); + + i = end + 1; + return new CharacterClass(src.slice(start, end), src.charCodeAt(start - 2) === 80); + + default: + if (!inCharSet && isDigit(src.charCodeAt(i - 1))) { + const start = i - 1; + + while (isDigit(src.charCodeAt(i))) { + i++; + } + + return new NumericEscape(+src.slice(start, i)) as any; + } + + if (!inCharSet && src.charCodeAt(i - 1) === 107 /* k */) { + assert(src.charCodeAt(i) === CharCodes.LAngle); + + const start = i + 1, + end = src.indexOf(">", start); + + assert(end > start); + + i = end + 1; + return new Backreference(src.slice(start, end)) as any; + } + + return new Raw(src[i - 1]); + } + } + + function characterSet() { + const alternatives: CharacterSet.Alternative[] = [], + isNegated = src.charCodeAt(i) === CharCodes.Caret; + + if (isNegated) { + i++; + } + + while (i < src.length) { + switch (src.charCodeAt(i++)) { + case CharCodes.RBracket: + return new CharacterSet(alternatives, isNegated); + + case CharCodes.Backslash: + const escaped = escapedCharacter(/* inCharSet= */ true); + + if (escaped instanceof CharacterSet) { + assert(!escaped.isNegated); + + alternatives.push(...escaped.alternatives); + } else { + alternatives.push(escaped); + } + break; + + case CharCodes.Minus: + if (alternatives.length === 0) { + alternatives.push(new Raw("-")); + continue; + } + + const start = alternatives[alternatives.length - 1]; + + if (Array.isArray(start) || start instanceof CharacterClass) { + alternatives.push(new Raw("-")); + continue; + } + + switch (src.charCodeAt(i++)) { + case CharCodes.RBracket: + alternatives.push(new Raw("-")); + return new CharacterSet(alternatives, isNegated); + + default: + const end = src.charCodeAt(i - 1) === CharCodes.Backslash + ? escapedCharacter(/* inCharSet= */ true) + : new Raw(src[i - 1]); + + if (end instanceof CharacterSet) { + assert(!end.isNegated); + + alternatives.push(new Raw("-"), ...end.alternatives); + } else if (end instanceof CharacterClass) { + alternatives.push(new Raw("-"), end); + } else { + alternatives.push([start, end]); + } + break; + } + break; + + default: + alternatives.push(new Raw(src[i - 1])); + break; + } + } + + assert(false); + } + + function group(): readonly Sequence[] { + const alternatives: Sequence[] = [], + sequence: Sequence.Node[] = []; + + while (i < src.length) { + switch (src.charCodeAt(i)) { + case CharCodes.RParen: + i++; + return alternatives; + + case CharCodes.Pipe: + i++; + alternatives.push(new Sequence(sequence.splice(0))); + break; + + case CharCodes.LParen: + if (src.charCodeAt(i + 1) === CharCodes.Question) { + const next = src.charCodeAt(i + 2); + + if (next === CharCodes.Colon) { + i += 3; + sequence.push(repeat(new Group(group()))); + } else if (next === CharCodes.Eq) { + i += 3; + sequence.push(repeat(new Lookaround(group(), false, false))); + } else if (next === CharCodes.Bang) { + i += 3; + sequence.push(repeat(new Lookaround(group(), true, false))); + } else if (next === CharCodes.LAngle) { + if (src.charCodeAt(i) === CharCodes.Eq) { + i += 4; + sequence.push(repeat(new Lookaround(group(), false, true))); + } else if (src.charCodeAt(i + 3) === CharCodes.Bang) { + i += 4; + sequence.push(repeat(new Lookaround(group(), true, true))); + } else { + i += 3; + + const start = i, + n = groups.push(dummyGroup); + + while (src.charCodeAt(i) !== CharCodes.RAngle) { + i++; + } + + i++; + sequence.push(repeat(groups[n - 1] = new Group(group(), n, src.slice(start, i - 2)))); + } + } else { + assert(false); + } + } else { + const n = groups.push(dummyGroup); + + sequence.push(repeat(groups[n - 1] = new Group(group(), n) as any) as Repeat); + } + break; + + case CharCodes.Backslash: + i++; + sequence.push(repeat(escapedCharacter(/* inCharSet= */ false))); + break; + + case CharCodes.LBracket: + i++; + sequence.push(repeat(characterSet())); + break; + + case CharCodes.Dot: + i++; + sequence.push(repeat(re.dotAll ? Dot.includingNewLine : Dot.excludingNewLine)); + break; + + case CharCodes.Caret: + i++; + sequence.push(Anchor.start); + break; + + case CharCodes.Dollar: + i++; + sequence.push(Anchor.end); + break; + + default: + if (sequence.length > 0 + && (sequence[sequence.length - 1] as Repeat).node instanceof Raw + && (sequence[sequence.length - 1] as Repeat).isNonRepeated) { + const prev = (sequence[sequence.length - 1] as Repeat).node; + + sequence[sequence.length - 1] = repeat(new Raw(prev.string + src[i])); + } else { + sequence.push(repeat(new Raw(src[i]))); + } + break; + } + } + + alternatives.push(new Sequence(sequence)); + + return alternatives; + } + + return new Expression(re, groups, group()); +} + +/** + * Returns the last `RegExp` match in the given text. + */ +export function execLast(re: RegExp, text: string) { + if (text.length > 10_000) { + // Execute reversed text on reversed regular expression. + const reverseRe = parse(re).reverse().re, + match = reverseRe.exec([...text].reverse().join("")); + + if (match === null) { + return null; + } + + // Update match index and input. + match.index = text.length - match.index - match[0].length; + match.input = text; + + // Reverse all matched groups so that they go back to their original text. + match[0] = text.substr(match.index, match[0].length); + + for (let i = 1; i < match.length; i++) { + match[i] = [...match[i]].reverse().join(""); + } + + if (match.groups !== undefined) { + for (const name in match.groups) { + match.groups[name] = [...match.groups[name]].reverse().join(""); + } + } + + return match; + } + + let lastMatch: RegExpExecArray | undefined, + lastMatchIndex = 0; + + for (;;) { + const match = re.exec(text); + + if (match === null) { + break; + } + + if (match[0].length === 0) { + throw new Error("RegExp returned empty result"); + } + + lastMatchIndex += match.index + (lastMatch?.[0].length ?? 0); + lastMatch = match; + text = text.slice(match.index + match[0].length); + } + + if (lastMatch === undefined) { + return null; + } + + lastMatch.index = lastMatchIndex; + + return lastMatch; +} + +/** + * Parses a RegExp string with a possible replacement. + */ +export function parseRegExpWithReplacement(regexp: string) { + if (regexp.length < 2 || regexp[0] !== "/") { + throw new Error("invalid RegExp"); + } + + let pattern = "", + replacement: string | undefined = undefined, + flags: string | undefined = undefined; + + for (let i = 1; i < regexp.length; i++) { + const ch = regexp[i]; + + if (flags !== undefined) { + // Parse flags + if (!"miguys".includes(ch)) { + throw new Error(`unknown RegExp flag "${ch}"`); + } + + flags += ch; + } else if (replacement !== undefined) { + // Parse replacement string + if (ch === "/") { + flags = ""; + } else if (ch === "\\") { + if (i === regexp.length - 1) { + throw new Error("unexpected end of RegExp"); + } + + replacement += ch + regexp[++i]; + } else { + replacement += ch; + } + } else { + // Parse pattern + if (ch === "/") { + replacement = ""; + } else if (ch === "\\") { + if (i === regexp.length - 1) { + throw new Error("unexpected end of RegExp"); + } + + pattern += ch + regexp[++i]; + } else { + pattern += ch; + } + } + } + + if ((flags === undefined || flags === "") && /^[miguys]+$/.test(replacement ?? "")) { + flags = replacement; + replacement = undefined; + } + + try { + return [new RegExp(pattern, flags), replacement] as const; + } catch { + throw new Error("invalid RegExp"); + } +} + +/** + * Returns a valid RegExp source allowing a `RegExp` to match the given string. + */ +export function escapeForRegExp(text: string) { + // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** + * Returns a `RegExp` that matches if any of the two given `RegExp`s does, and + * the index of the "marker group" that will be equal to `""` if the `RegExp` + * that matched the input is `b`. + */ +export function anyRegExp(a: RegExp, b: RegExp) { + const flags = [...new Set([...a.flags, ...b.flags])].join(""), + aGroups = new RegExp("|" + a.source, a.flags).exec("")!.length - 1, + bGroups = new RegExp("|" + b.source, b.flags).exec("")!.length - 1; + + // Update backreferences in `b` to ensure they point to the right indices. + const bSource = replaceUnlessEscaped(b.source, /\\(\d+)/g, (text, n) => { + if (n[0] === "0" || +n > bGroups) { + return text; + } + + return "\\" + (+n + aGroups); + }); + + return [new RegExp(`(?:${a.source})|(?:${bSource})()`, flags), aGroups + bGroups + 1] as const; +} + +/** + * Same as `text.replace(...args)`, but does not replaced escaped characters. + */ +export function replaceUnlessEscaped(text: string, re: RegExp, replace: (...args: any) => string) { + return text.replace(re, (...args) => { + const offset = args[args.length - 2], + text = args[args.length - 1]; + + if (isEscaped(text, offset)) { + return args[0]; + } + + return replace(...args); + }); +} + +/** + * Same as `text.replace(...args)`, but does not replaced escaped characters. + */ +export function matchUnlessEscaped(text: string, re: RegExp) { + assert(re.global); + + for (let match = re.exec(text); match !== null; match = re.exec(text)) { + if (!isEscaped(text, match.index)) { + return match; + } + } + + return null; +} + +function isEscaped(text: string, offset: number) { + if (offset === 0) { + return false; + } + + let isEscaped = false; + + for (let i = offset - 1; i >= 0; i--) { + if (text[i] === "\\") { + isEscaped = !isEscaped; + } else { + return isEscaped; + } + } + + return isEscaped; +} + +/** + * Like `String.prototype.split(RegExp)`, but returns the `[start, end]` + * indices corresponding to each string of the split. + */ +export function splitRange(text: string, re: RegExp) { + const sections: [start: number, end: number][] = []; + + for (let start = 0;;) { + const match = re.exec(text); + + if (match === null || text.length === 0) { + sections.push([start, start + text.length]); + + return sections; + } + + sections.push([start, start + match.index]); + + if (match[0].length === 0) { + text = text.slice(1); + start++; + } else { + text = text.slice(match.index + match[0].length); + start += match.index + match[0].length; + } + + re.lastIndex = 0; + } +} + +/** + * Like `RegExp.prototype.exec()`, but returns the `[start, end]` + * indices corresponding to each matched result. + */ +export function execRange(text: string, re: RegExp) { + const sections: [start: number, end: number, match: RegExpExecArray][] = []; + let diff = 0; + + for (let match = re.exec(text); match !== null && text.length > 0; match = re.exec(text)) { + const start = match.index, + end = start + match[0].length; + + sections.push([diff + start, diff + end, match]); + + text = text.slice(end); + diff += end; + + if (start === end) { + text = text.slice(1); + diff++; + } + } + + return sections; +} diff --git a/src/utils/savedSelection.ts b/src/utils/savedSelection.ts deleted file mode 100644 index fdce14e..0000000 --- a/src/utils/savedSelection.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as vscode from "vscode"; - -/** - * A selection that has been saved, and that is being tracked. - */ -export class SavedSelection { - private _anchorOffset: number; - private _activeOffset: number; - - public constructor(anchorOffset: number, activeOffset: number) { - this._anchorOffset = anchorOffset; - this._activeOffset = activeOffset; - } - - /** The offset of the anchor in its document. */ - public get anchorOffset() { - return this._anchorOffset; - } - - /** The offset of the active position in its document. */ - public get activeOffset() { - return this._activeOffset; - } - - /** Whether the selection is reversed (i.e. `activeOffset < anchorOffset`). */ - public get isReversed() { - return this._activeOffset < this._anchorOffset; - } - - /** - * Returns the anchor of the saved selection. - */ - public anchor(document: vscode.TextDocument) { - return document.positionAt(this._anchorOffset); - } - - /** - * Returns the active position of the saved selection. - */ - public active(document: vscode.TextDocument) { - return document.positionAt(this._activeOffset); - } - - /** - * Returns the saved selection, restored in the given document. - */ - public selection(document: vscode.TextDocument) { - return new vscode.Selection(this.anchor(document), this.active(document)); - } - - /** - * Updates the underlying selection to reflect a change in its document. - */ - public updateAfterDocumentChanged(e: vscode.TextDocumentContentChangeEvent) { - const diff = e.text.length - e.rangeLength, - offset = e.rangeOffset + e.rangeLength; - - if (offset <= this._activeOffset) { - this._activeOffset += diff; - } - - if (offset <= this._anchorOffset) { - this._anchorOffset += diff; - } - } -} diff --git a/src/utils/selectionHelper.ts b/src/utils/selectionHelper.ts deleted file mode 100644 index 76b5b40..0000000 --- a/src/utils/selectionHelper.ts +++ /dev/null @@ -1,538 +0,0 @@ -import * as vscode from "vscode"; - -import { CommandState } from "../commands"; -import { EditorState } from "../state/editor"; -import { SelectionBehavior } from "../state/extension"; - -export const DocumentStart = new vscode.Position(0, 0); - -/** - * Direction of an operation. - */ -export const enum Direction { - /** Forward direction (`1`). */ - Forward = 1, - /** Backward direction (`-1`). */ - Backward = -1, -} - -/** - * Whether or not an operation should expend a selection. - */ -export type ExtendBehavior = boolean; - -/** Forward direction. */ -export const Forward = Direction.Forward; -/** Backward direction. */ -export const Backward = Direction.Backward; - -/** Do extend. */ -export const Extend = true as ExtendBehavior; -/** Do not extend. */ -export const DoNotExtend = false as ExtendBehavior; - -export class SelectionHelper { - public static for(editorState: EditorState): SelectionHelper; - public static for( - editorState: EditorState, - state: State, - ): SelectionHelper; - - public static for(editorState: EditorState, state?: any) { - // TODO: Caching - return new SelectionHelper(editorState, state ?? editorState); - } - - public readonly selectionBehavior = this.state.selectionBehavior; - - /** - * Get the "cursor active" Coord of a selection. - * - * In other words, the basis Coord for moving, etc. of the selection. This - * method handles special cases for non-directional selections correctly. - * @param selection - */ - public activeCoord(selection: vscode.Selection): Coord { - if ( - this.selectionBehavior === SelectionBehavior.Caret - || selection.active.isEqual(DocumentStart) - || selection.isEmpty - || selection.isReversed - ) { - return selection.active; - } - return this.coordAt(this.offsetAt(selection.active) - 1); - } - - /** - * Apply a transformation to each selection. - * - * If all selections are to be removed, an alert will be raised and selections - * are left untouched (or replaced by fallback, if provided). - * - * @param mapper a function takes the old selection and return a new one or - * `{remove: true}` with optional fallback. - * - * @see moveActiveCoord,jumpTo,seekToRange for utilities to create mappers for - * common select operations - */ - public mapEach(mapper: SelectionMapper): void { - const newSelections: vscode.Selection[] = []; - const editor = this.editor; - let acceptFallback = true; - const len = editor.selections.length; - for (let i = 0; i < len; i++) { - const moveResult = mapper(editor.selections[i], this, i); - if (moveResult === RemoveSelection || "remove" in moveResult) { - if (acceptFallback) { - newSelections.push(moveResult.fallback || editor.selections[i]); - } - } else { - if (acceptFallback) { - newSelections.length = 0; // Clear all fallback selections so far. - acceptFallback = false; - } - newSelections.push(moveResult); - } - } - if (acceptFallback) { - throw new Error("no selections remaining"); - } - editor.selections = newSelections; - } - - /** - * Return a selection that spans from and to (both inclusive). - * @param from the coordinate of the starting symbol to include - * @param to the coordinate of the ending symbol to include - * @param singleCharDirection the direction of selection when from equals to - */ - public selectionBetween( - from: Coord, - to: Coord, - singleCharDirection: Direction = Forward, - ): vscode.Selection { - // TODO: Remove this.coordAt and this.offsetAt. Use lineAt for shifting to - // the next position. - if (from.isBefore(to) || (from.isEqual(to) && singleCharDirection === Forward)) { - // Forward: 0123456 ==select(1, 4)=> 0<1234|56 - // Need to increment `to` to include the last character. - const active = this.coordAt(this.offsetAt(to) + 1); - return new vscode.Selection(from, active); - } else { - // Reverse: 0123456 ==select(4, 1)=> 0|1234>56 - // Need to increment `from` to include the last character. - const anchor = this.coordAt(this.offsetAt(from) + 1); - return new vscode.Selection(anchor, to); - } - } - - public extend(oldSelection: vscode.Selection, to: Coord): vscode.Selection { - // TODO: Remove this.coordAt and this.offsetAt. Use lineAt for shifting to - // the next / previous position. - let { anchor } = oldSelection; - if (anchor.isBeforeOrEqual(to)) { - // The resulting selection will be forward-facing. - if (this.selectionBehavior === SelectionBehavior.Character && oldSelection.isReversed) { - // Flipping selections in the opposite direction: 0|1>2345 to 0<12345| - // Need to push anchor backwards so that the first symbol is included. - anchor = this.coordAt(this.offsetAt(anchor) - 1); - } - if ( - this.selectionBehavior === SelectionBehavior.Character - || to.isAfterOrEqual(oldSelection.active) - ) { - // Moving forward (or non-directional): 01|23>456 ==(to 4)==> 01|234>56 - // Need to increment to include the the symbol at `to`. - const active = this.coordAt(this.offsetAt(to) + 1); - return new vscode.Selection(anchor, active); - } else { - // Moving backwards: 01|23>456 ==(to 2)==> 01|>23456 - // The symbol at `to` is excluded in this case, since we're - // "deselecting" that character. - return new vscode.Selection(anchor, to); - } - } else { - // The resulting selection will be (most likely) backwards-facing. - const afterTo = this.coordAt(this.offsetAt(to) + 1); - if (this.selectionBehavior === SelectionBehavior.Character) { - if ( - (!oldSelection.isReversed && oldSelection.anchor.isEqual(to)) - || (oldSelection.isReversed && oldSelection.anchor.isEqual(afterTo)) - ) { - // Special case one character selections to face forward: - return new vscode.Selection(to, afterTo); - } else if (!oldSelection.isReversed) { - // Flipping selections in the opposite direction: 0123<4|5 to |01234>5 - // Need to push anchor forward so that the last symbol is included. - anchor = this.coordAt(this.offsetAt(anchor) + 1); - } - } - if ( - this.selectionBehavior === SelectionBehavior.Character - || to.isBeforeOrEqual(oldSelection.active) - ) { - // Moving backward (or non-directional): 012<34|5 ==(to 2)==> 01<234|5 - // Include the symbol at `to`. - return new vscode.Selection(anchor, to); - } else { - // Moving forward (or non-directional): 012<34|5 ==(to 4)==> 01234<|5 - // The symbol at `to` is excluded in this case, since we're - // "deselecting" that character. - return new vscode.Selection(anchor, afterTo); - } - } - } - - /** Get the next position in document. */ - public nextPos(pos: vscode.Position): vscode.Position { - // TODO: Optimize - return this.coordAt(this.offsetAt(pos) + 1); - } - - /** Get the previous position in document. */ - public prevPos(pos: vscode.Position): vscode.Position { - // TODO: Optimize - return this.coordAt(this.offsetAt(pos) - 1); - } - - /** - * Get the line and character of an offset in the document. - * - * This should be used instead of `this.coordAt` to make - * the intention clear and also allow future optimization like caching. - * @param coordOffset the nth symbol (or line break) in the document - */ - public coordAt(coordOffset: number): Coord { - // TODO: Caching - return this.editor.document.positionAt(coordOffset); - } - - /** - * Get the total sequence number of symbol (or line break) in the document. - * - * This should be used instead of `this.offsetAt` to make - * the intention clear and also allow future optimization like caching. - * @param coord the line and character of the symbol in the document - */ - public offsetAt(coord: Coord): CoordOffset { - // TODO: Caching - return this.editor.document.offsetAt(coord); - } - - public endLine(selection: vscode.Selection) { - if (selection.end === selection.active && selection.end.character === 0) { - return selection.end.line - 1; - } else { - return selection.end.line; - } - } - - public activeLine(selection: vscode.Selection) { - if (selection.end === selection.active && selection.end.character === 0) { - return selection.active.line - 1; - } else { - return selection.active.line; - } - } - - public endCharacter(selection: vscode.Selection, forPositionConstructor = false) { - if ( - this.selectionBehavior === SelectionBehavior.Character - && selection.end === selection.active - && selection.end.character === 0 - ) { - return forPositionConstructor - ? Number.MAX_SAFE_INTEGER - : this.editor.document.lineAt(selection.end.line - 1).text.length + 1; - } else { - return selection.end.character; - } - } - - public activeCharacter(selection: vscode.Selection, forPositionConstructor = false) { - if ( - this.selectionBehavior === SelectionBehavior.Character - && selection.end === selection.active - && selection.active.character === 0 - ) { - return forPositionConstructor - ? Number.MAX_SAFE_INTEGER - : this.editor.document.lineAt(selection.active.line - 1).text.length + 1; - } else { - return selection.active.character; - } - } - - public isSingleLine(selection: vscode.Selection) { - return selection.isSingleLine || selection.start.line === this.endLine(selection); - } - - public isEntireLine(selection: vscode.Selection) { - return ( - selection.start.character === 0 - && selection.end.character === 0 - && selection.start.line === selection.end.line - 1 - ); - } - - public isEntireLines(selection: vscode.Selection) { - return ( - selection.start.character === 0 - && selection.end.character === 0 - && selection.start.line !== selection.end.line - ); - } - - /** - * The selection length of the given selection, as defined by - * `offsetAt(active) - offsetAt(anchor)`. - * - * If the selection is reversed, the selection length is negative. - */ - public selectionLength(selection: vscode.Selection) { - if (selection.isSingleLine) { - return selection.active.character - selection.anchor.character; - } - - return this.offsetAt(selection.active) - this.offsetAt(selection.anchor); - } - - public selectionFromLength(anchorCoord: Coord, length: number) { - return new vscode.Selection(anchorCoord, this.coordAt(this.offsetAt(anchorCoord) + length)); - } - - public lastCoord(): Coord { - const document = this.editor.document; - const lastLineText = document.lineAt(document.lineCount - 1).text; - if (lastLineText.length > 0) { - return new Coord(document.lineCount - 1, lastLineText.length - 1); - } - if (document.lineCount >= 2) { - return new Coord(document.lineCount - 2, document.lineAt(document.lineCount - 2).text.length); - } - return DocumentStart; - } - - /** DEBUG ONLY method to visualize an offset. DO NOT leave calls in code. */ - public _visualizeOffset(offset: number): string { - const position = this.coordAt(offset); - return this._visualizePosition(position); - } - - /** DEBUG ONLY method to visualize a position. DO NOT leave calls in code. */ - public _visualizePosition(position: vscode.Position): string { - const text = this.editor.document.lineAt(position.line).text; - return ( - position.line - + ": " - + text.substr(0, position.character) - + "|" - + text.substr(position.character) - ); - } - - /** DEBUG ONLY method to visualize a Coord. DO NOT leave calls in code. */ - public _visualizeCoord(coord: Coord): string { - const text = this.editor.document.lineAt(coord.line).text; - let visualLine; - if (coord.character === text.length) { - visualLine = text + "⏎"; - } else { - visualLine - = text.substr(0, coord.character) - + `[${text[coord.character]}]` - + text.substr(coord.character + 1); - } - return `L${coord.line}: ${visualLine}`; - } - - public readonly editor: vscode.TextEditor; - - private constructor(public readonly editorState: EditorState, public readonly state: State) { - this.editor = editorState.editor; - } -} - -/** - * A coordinate (line, character) in the document. It stands for the c-th - * text symbol at the l-th line in the document (both zero-indexed). - * - * Note that a Coord should always be interpreted to be pointed at a text symbol - * or the line break at the end of the current line (e.g. the 'b' in 'abc'). - * It should not be mistaken for a Position between symbols (a|bc in 'abc'). - */ -export interface Coord extends vscode.Position { - // Dummy interface so that it shows as its type in generated docs, IDE hover - // tooltips, etc. Do not add fields here. -} -export const Coord = vscode.Position; // To allow sugar like `new Coord(1, 2)`. -export type CoordOffset = number; - -export type SelectionMapper< - State extends { selectionBehavior: SelectionBehavior } = CommandState -> = ( - selection: vscode.Selection, - helper: SelectionHelper, - i: number, -) => vscode.Selection | { remove: true; fallback?: vscode.Selection }; -export type CoordMapper = ( - oldActive: Coord, - helper: SelectionHelper, - i: number, -) => Coord | typeof RemoveSelection; -export type SeekFunc = ( - oldActive: Coord, - helper: SelectionHelper, - i: number, -) => [Coord, Coord] | { remove: true; fallback?: [Coord | undefined, Coord] }; - -export const RemoveSelection: { remove: true } = { remove: true }; - -/** - * Create a SelectionMapper that moves selection's active to a new coordinate. - * - * @summary The mapper created is useful for commands that "drags" to a - * character such as `f` (selectTo) without moving anchor. - * - * When extending, the new selection will keep the same anchor caret / - * character (depending on `this.selectionBehavior`). When not extending, the - * new selection anchors to the old active caret / character. Either way, - * the new active will always sweep over the symbol at the coordinate - * returned, selecting or deselecting it as appropriate. - * - * @param activeMapper a function that takes an old active coordinate and return - * the Coord of the symbol to move to or `RemoveSelection` - * @param extend if Extend, the new selection will keep the anchor caret / - * character. Otherwise, it is anchored to old active. - * @returns a SelectionMapper that is suitable for SelectionHelper#mapEach - */ -export function moveActiveCoord( - activeMapper: CoordMapper, - extend: ExtendBehavior, -): SelectionMapper { - return (selection, helper, i) => { - const oldActive = helper.activeCoord(selection); - const newActive = activeMapper(oldActive, helper, i); - if ("remove" in newActive) { - return RemoveSelection; - } - - if (extend) { - return helper.extend(selection, newActive); - } - - if (helper.selectionBehavior === SelectionBehavior.Caret) { - // TODO: Optimize to avoid coordAt / offsetAt. - const activePos = selection.active.isBeforeOrEqual(newActive) - ? helper.coordAt(helper.offsetAt(newActive) + 1) - : newActive; - return new vscode.Selection(selection.active, activePos); - } - return helper.selectionBetween(oldActive, newActive); - }; -} - -/** - * Create a SelectionMapper that jump / extend the selection to a new position - * (caret or coordinate, depending on `this.selectionBehavior`). - * - * @summary The mapper created is useful for commands that "jump" to a place - * without dragging to it, such as `gg` (gotoFirstLine) or arrows (`hjkl`). - * - * When `this.selectionBehavior` is Caret, the new active will be at the caret - * at the result Position. When not extending, the anchor is moved there as - * well, creating an empty selection on that spot. - * - * When `this.selectionBehavior` is Character and extending, new active always - * include the symbol at Position. When not extending, the new selection will - * contain exactly one symbol under the result Position. - * - * @param activeMapper a function that takes an old active coordinate and return - * the caret / character move to or `RemoveSelection`. - * @param extend if Extend, the new selection will keep the anchor caret / - * character. Otherwise, it is anchored to old active. - * @returns a SelectionMapper that is suitable for SelectionHelper#mapEach - */ -export function jumpTo(activeMapper: CoordMapper, extend: ExtendBehavior): SelectionMapper { - return (selection, helper, i) => { - const oldActive = helper.activeCoord(selection); - const newActive = activeMapper(oldActive, helper, i); - if ("remove" in newActive) { - return RemoveSelection; - } - - if (helper.selectionBehavior === SelectionBehavior.Caret) { - return new vscode.Selection(extend ? selection.anchor : newActive, newActive); - } - - if (extend) { - return helper.extend(selection, newActive); - } - - return helper.selectionBetween(newActive, newActive); - }; -} - -/** - * Create a SelectionMapper that change / extend the selection to a new - * character range (including starting and ending characters). - * - * @summary The mapper created is useful for commands that "seeks" to a range of - * characters and re-anchor, such as `w` (selectWord) or `m` (selectMatching). - * - * The seek function returns two Coords, start and end, both inclusive. If not - * extending, a new selection will be created that includes both symbols under - * start and end. (The selection will be reversed if start is after end). When - * extending, start is ignored and the old extension is extended to sweep over - * the end symbol, selecting or deselecting it as appropriate. - * - * @param seekFunc a function that takes an old active coordinate and return - * the range ([start, end]) to seek to, or remove. - * @param extend if Extend, the new selection will keep the anchor caret / - * character. Otherwise, the new selection is anchored to start. - * @param singleCharDirection if specified, caret-based selection that only has - * one character will face that direction - * @returns a SelectionMapper that is suitable for SelectionHelper#mapEach - */ -export function seekToRange( - seek: SeekFunc, - extend: ExtendBehavior, - singleCharDirection = Forward, -): SelectionMapper { - return (selection, helper, i) => { - const oldActive = helper.activeCoord(selection); - const seekResult = seek(oldActive, helper, i); - let remove = false; - let start: Coord | undefined, end: Coord; - - if ("remove" in seekResult) { - if (!seekResult.fallback) { - return RemoveSelection; - } - // Avoid array destructuring which is not fully optimized yet in V8. - // See: http://bit.ly/array-destructuring-for-multi-value-returns - start = seekResult.fallback[0]; - end = seekResult.fallback[1]; - remove = true; - } else { - start = seekResult[0]; - end = seekResult[1]; - } - - let newSelection; - if (!start || extend) { - newSelection = helper.extend(selection, end); - } else if (helper.selectionBehavior === SelectionBehavior.Caret) { - newSelection = helper.selectionBetween(start, end, singleCharDirection); - } else { - newSelection = helper.selectionBetween(start, end); - } - - if (remove) { - return { remove, fallback: newSelection }; - } else { - return newSelection; - } - }; -} diff --git a/src/utils/settings-validator.ts b/src/utils/settings-validator.ts new file mode 100644 index 0000000..2e773b8 --- /dev/null +++ b/src/utils/settings-validator.ts @@ -0,0 +1,71 @@ +import * as vscode from "vscode"; + +/** + * A class used to validate settings. + */ +export class SettingsValidator { + public readonly path: string[] = []; + public readonly errors: string[] = []; + + public constructor(...path: string[]) { + this.path.push(...path); + } + + public enter(property: string) { + this.path.push(property); + } + + public leave() { + this.path.pop(); + } + + public forProperty(property: string, f: (validator: this) => T) { + this.enter(property); + + try { + return f(this); + } finally { + this.leave(); + } + } + + public reportInvalidSetting(message: string, name?: string) { + const suffix = name === undefined ? "" : "." + name; + + this.errors.push(`${this.path.join(".")}${suffix}: ${message}`); + } + + public displayErrorIfNeeded() { + const errors = this.errors; + + if (errors.length === 0) { + return; + } + + return vscode.window.showErrorMessage("Invalid settings: " + errors.join(" — ")); + } + + public throwErrorIfNeeded() { + const errors = this.errors; + + if (errors.length === 0) { + return; + } + + throw new Error("Invalid settings: " + errors.join(" — ")); + } + + public static displayErrorIfNeeded(path: string, f: (validator: SettingsValidator) => T) { + const validator = new SettingsValidator(path), + result = f(validator); + validator.displayErrorIfNeeded(); + return result; + } + + public static throwErrorIfNeeded(path: string, f: (validator: SettingsValidator) => T) { + const validator = new SettingsValidator(path), + result = f(validator); + validator.throwErrorIfNeeded(); + return result; + } +} diff --git a/src/utils/tracked-selection.ts b/src/utils/tracked-selection.ts new file mode 100644 index 0000000..d594a40 --- /dev/null +++ b/src/utils/tracked-selection.ts @@ -0,0 +1,331 @@ +import * as vscode from "vscode"; +import { ArgumentError } from "../api"; +import { PerEditorState } from "../state/editors"; + +export namespace TrackedSelection { + /** + * Flags passed to `TrackedSelection.updateAfterDocumentChanged`. + */ + export const enum Flags { + Inclusive = 0, + + StrictStart = 0b01, + StrictEnd = 0b10, + + Strict = 0b11, + } + + /** + * An array of tracked selections selections. + */ + export interface Array extends Iterable { + [index: number]: number; + readonly length: number; + } + + /** + * Creates a new `TrackedSelection.Array`, reading offsets from the given + * selections in the given document. + */ + export function fromArray( + selections: readonly vscode.Selection[], + document: vscode.TextDocument, + ): Array { + const trackedSelections = [] as number[]; + + for (let i = 0, len = selections.length; i < len; i++) { + const selection = selections[i], + anchor = selection.anchor, + active = selection.active; + + if (anchor.line === active.line) { + const anchorOffset = document.offsetAt(anchor), + activeOffset = anchorOffset + active.character - anchor.character; + + trackedSelections.push(anchorOffset, activeOffset); + } else { + trackedSelections.push(document.offsetAt(anchor), document.offsetAt(active)); + } + } + + return trackedSelections; + } + + export function restore(array: Array, index: number, document: vscode.TextDocument) { + const anchor = document.positionAt(array[index << 1]), + active = document.positionAt(array[(index << 1) | 1]); + + return new vscode.Selection(anchor, active); + } + + export function anchorOffset(array: Array, index: number) { + return array[index << 1]; + } + + export function activeOffset(array: Array, index: number) { + return array[(index << 1) | 1]; + } + + export function startOffset(array: Array, index: number) { + return Math.min(anchorOffset(array, index), activeOffset(array, index)); + } + + export function endOffset(array: Array, index: number) { + return Math.max(anchorOffset(array, index), activeOffset(array, index)); + } + + export function length(array: Array, index: number) { + return Math.abs(anchorOffset(array, index) - activeOffset(array, index)); + } + + export function setAnchorOffset(array: Array, index: number, offset: number) { + array[index << 1] = offset; + } + + export function setActiveOffset(array: Array, index: number, offset: number) { + array[(index << 1) | 1] = offset; + } + + export function activeIsStart(array: Array, index: number) { + return activeOffset(array, index) <= anchorOffset(array, index); + } + + export function setLength(array: Array, index: number, length: number) { + const active = activeOffset(array, index), + anchor = anchorOffset(array, index); + + if (active < anchor) { + setAnchorOffset(array, index, active + length); + } else { + setActiveOffset(array, index, anchor + length); + } + } + + export function setStartEnd( + array: Array, + index: number, + startOffset: number, + endOffset: number, + startIsActive: boolean, + ) { + if (startIsActive) { + setActiveOffset(array, index, startOffset); + setAnchorOffset(array, index, endOffset); + } else { + setActiveOffset(array, index, endOffset); + setAnchorOffset(array, index, startOffset); + } + } + + /** + * Returns the saved selections, restored in the given document. + */ + export function restoreArray(array: Array, document: vscode.TextDocument) { + const selections = [] as vscode.Selection[]; + + for (let i = 0, len = array.length >> 1; i < len; i++) { + selections.push(restore(array, i, document)); + } + + return selections; + } + + /** + * Returns the saved selections, restored in the given document, skipping + * empty selections. + */ + export function restoreNonEmpty(array: Array, document: vscode.TextDocument) { + const selections = [] as vscode.Selection[]; + + for (let i = 0, len = array.length >> 1; i < len; i++) { + const anchorOffset = array[i << 1], + activeOffset = array[(i << 1) | 1]; + + if (anchorOffset === activeOffset) { + continue; + } + + selections.push( + new vscode.Selection(document.positionAt(anchorOffset), document.positionAt(activeOffset)), + ); + } + + return selections; + } + + /** + * Updates the underlying selection to reflect a change in its document. + */ + export function updateAfterDocumentChanged( + array: Array, + changes: readonly vscode.TextDocumentContentChangeEvent[], + flags: TrackedSelection.Flags, + ) { + for (let i = 0, len = array.length; i < len; i += 2) { + let anchorOffset = array[i], + activeOffset = array[i + 1]; + + const activeIsStart = activeOffset <= anchorOffset, + anchorIsStart = activeOffset >= anchorOffset, + inclusiveStart = (flags & Flags.StrictStart) === 0, + inclusiveEnd = (flags & Flags.StrictEnd) === 0, + inclusiveActive = activeIsStart ? !inclusiveStart : inclusiveEnd, + inclusiveAnchor = anchorIsStart ? !inclusiveStart : inclusiveEnd; + + for (let i = 0, len = changes.length; i < len; i++) { + const change = changes[i], + diff = change.text.length - change.rangeLength, + offset = change.rangeOffset + change.rangeLength; + + if (offset < activeOffset || (inclusiveActive && offset === activeOffset)) { + activeOffset += diff; + } + + if (offset < anchorOffset || (inclusiveAnchor && offset === anchorOffset)) { + anchorOffset += diff; + } + } + + array[i] = anchorOffset; + array[i + 1] = activeOffset; + } + } + + /** + * A set of `TrackedSelection`s. + */ + export class Set implements vscode.Disposable { + private readonly _onDisposed = new vscode.EventEmitter(); + private readonly _selections: Array; + private readonly _onDidChangeTextDocumentSubscription: vscode.Disposable; + + public get onDisposed() { + return this._onDisposed.event; + } + + public constructor( + selections: Array, + public readonly document: vscode.TextDocument, + public readonly flags = Flags.Inclusive, + ) { + ArgumentError.validate("selections", selections.length > 0, "selections cannot be empty"); + + this._selections = selections; + this._onDidChangeTextDocumentSubscription = + vscode.workspace.onDidChangeTextDocument(this.updateAfterDocumentChanged, this); + } + + public addArray(array: Array) { + (this._selections as number[]).push(...array); + + return this; + } + + public addSelections(selections: readonly vscode.Selection[]) { + return this.addArray(TrackedSelection.fromArray(selections, this.document)); + } + + public addSelection(selection: vscode.Selection) { + return this.addArray(TrackedSelection.fromArray([selection], this.document)); + } + + /** + * Updates the tracked selections to reflect a change in their document. + * + * @returns whether the change was applied. + */ + protected updateAfterDocumentChanged(e: vscode.TextDocumentChangeEvent) { + if (e.document !== this.document || e.contentChanges.length === 0) { + return false; + } + + updateAfterDocumentChanged(this._selections, e.contentChanges, this.flags); + + return true; + } + + public restore() { + return restoreArray(this._selections, this.document); + } + + public restoreNonEmpty() { + return restoreNonEmpty(this._selections, this.document); + } + + public dispose() { + this._onDidChangeTextDocumentSubscription.dispose(); + } + } + + /** + * A `TrackedSelection.Set` that displays active selections using some given + * style. + */ + export class StyledSet extends Set { + private readonly _decorationType: vscode.TextEditorDecorationType; + private readonly _onDidEditorVisibilityChangeSubscription: vscode.Disposable; + + public constructor( + selections: Array, + public readonly editorState: PerEditorState, + renderOptions: vscode.DecorationRenderOptions, + ) { + super( + selections, editorState.editor.document, rangeBehaviorToFlags(renderOptions.rangeBehavior)); + + this._decorationType = vscode.window.createTextEditorDecorationType(renderOptions); + this._onDidEditorVisibilityChangeSubscription = editorState.onVisibilityDidChange((e) => + e.isVisible && this._updateDecorations()); + this._updateDecorations(); + } + + public addArray(selections: Array) { + super.addArray(selections); + + for (let i = 0, len = selections.length; i < len; i += 2) { + if (selections[i] !== selections[i + 1]) { + this._updateDecorations(); + break; + } + } + + return this; + } + + protected updateAfterDocumentChanged(e: vscode.TextDocumentChangeEvent) { + if (!super.updateAfterDocumentChanged(e)) { + return false; + } + + this._updateDecorations(); + + return true; + } + + public dispose() { + super.dispose(); + + this._decorationType.dispose(); + } + + private _updateDecorations() { + this.editorState.editor.setDecorations(this._decorationType, this.restoreNonEmpty()); + } + } +} + +function rangeBehaviorToFlags(rangeBehavior: vscode.DecorationRangeBehavior | undefined) { + switch (rangeBehavior) { + case vscode.DecorationRangeBehavior.ClosedOpen: + return TrackedSelection.Flags.StrictStart; + + case vscode.DecorationRangeBehavior.OpenClosed: + return TrackedSelection.Flags.StrictEnd; + + case vscode.DecorationRangeBehavior.ClosedClosed: + return TrackedSelection.Flags.Strict; + + default: + return TrackedSelection.Flags.Inclusive; + } +} diff --git a/test/README.md b/test/README.md index c71e788..dce1576 100644 --- a/test/README.md +++ b/test/README.md @@ -1,75 +1,282 @@ -# Writing command tests +# Dance tests -Command tests are plain text files that are separated in several sections. +There are currently two types of tests for Dance: [API tests](#api-tests) and +[command tests](#command-tests). Both of these tests use code to represent +what documents should look like before and after performing changes, as well as +what selections should be on the given document. This syntax is specified [at +the end of this document](#syntax-of-expected-documents). -### Sections +## API tests -Each section has a name, which is any string that has no whitespace. +API tests are processed by [`api.test.ts`](./suite/api.test.ts), which reads +all files under the [`api`](../src/api) directory and (roughly) parses their +documentation comments. When these comments specify examples, these examples +are tested. -Except for the first section (implicitly named `0` or `root`), each section -is associated with some transition that consists of several Dance commands to run. +These examples are specified in Markdown, and must have a format matching the +following example. -For instance, let's look at the following code: +
+ Example with "before" / "after" sections -``` -... +### Example -//== 0 > 1 -//= dance.select.line -... +```js +const anchor = new vscode.Position(0, 0), + active = new vscode.Position(0, 3), + selection = new vscode.Selection(anchor, active); -//== 1 > 2 -//= dance.select.line.extend -... - -//== 1 > 3 -//= dance.select.line -//= dance.select.line.extend -... +await updateSelections([selection]); ``` -It defines three sections: - -- `1`, which is reached after executing `dance.select.line` from section `0`. -- `2`, which is reached after executing `dance.select.line.extend` from section `1`. -- `3`, which is reached after executing `dance.select.line` and then `dance.select.line.extend` from section `1`. - -As you can see, several sections can depend on the same parent section. Do note that -sections must be defined in order; that is, a section `a` cannot depend on a section `b` -if section `b` is defined after `a`. - -### Section content - -Each section has content (the `...` in the example above). That content is plain text to which -one or more selections must be added using a `{...}` / `|{...}` syntax, where `...` is a number. - -`{0}` represents the anchor of the 1st selection, and `|{2}` represents the active position of the 3rd selection. - -Selections can be given in any order, but must be complete; that is, if a selection `3` is given, then the -selections `0`, `1`, and `2` must be defined at some point too. The anchor can be omitted, and will default to -the active position. - -### Tests generation - -For each transition, a test will be generated making sure that executing the corresponding commands -will lead to some document with selections at some locations. - -Let's look at the following code: - +Before: ``` -{0}f|{0}oo - -//== 0 > 1 -//= dance.right -f{0}o|{0}o - -//== 1 > 2 -//= dance.delete.yank -f{0}o|{0} +foo bar + ^^^ 0 ``` -The first generated test asserts that calling `dance.right` in the document `foo` where `f` is the main selection -leads to a document `foo` with the first `o` selected. +After: +``` +foo bar +^^^ 0 +``` +
-The second generated test asserts that calling `dance.delete.yank` in the document `foo` where the first `o` is -the main selection leads to a document `fo` with `o` selected. +
+ Example with "with" section + +### Example + +```js +expect( + text(Selections.current[0]), + "to be", + "bar", +); +``` + +With: +``` +foo bar + ^^^ 0 +``` +
+ +
+ Example with no section + +### Example + +```js +expect( + map( + new vscode.Range(Positions.at(0, 0), Positions.at(0, 5)), + (p) => p.translate(1), + ), + "to satisfy", + { + start: expect.it("to be at coords", 0, 0), + end: expect.it("to be at coords", 0, 5), + }, +) +``` +
+ +### Debugging + +While viewing a file in the [`api`](../src/api) directory, select "Run tests in +this file" to debug all doc tests in the file being viewed. Select "Run test on +this line" to debug the test of the function under the cursor. + +## Command tests + +Command tests are processed by [`commands.test.ts`](./suite/commands.test.ts), +which reads all files under the [`commands`](./suite/commands) directory and +parses them into several sections. + +Each section must start with a Markdown `# heading`, which represents the title +of the section. Except for initial sections, all sections must also link to +the section from which they transition with a Markdown `[link](#heading)`. + +Sections can then specify a set of [flags](#available-flags) that may alter the +behavior of their test using Markdown `> quotes`. + +Then, the commands to execute are specified as Markdown `- lists`. These +commands may specify arguments in JSON. + +Finally, the expected document after changes can be specified in a Markdown +code block. + +
+ Example + +# 1 + +``` +foo bar + ^ 0 +``` + +## 1 search-b +[up](#1) + +- .search { input: "b" } + +``` +foo bar + ^ 0 +``` + +
+ +### Available flags + +- `debug`: Inserts a `debugger` statement at the beginning of the test. +- `behavior <- character`: Sets selection behavior of normal mode to `character` + for the duration of the test. The mode will be reset to `caret` at the end of + the test. Tests that depend on a test with character behavior `character` will + default to having that same behavior. Use `behavior <- caret` to undo this. +- `///`: Replaces the given pattern by the given + replacement string in all sections that inherit from the current section. + +When executing Dance commands, you may also pass an `$expect` argument with a +`RegExp` to check that the error throws an error whose message matches the +regular expression. + +### Naming and organization + +To make it easier to navigate and understand tests, tests must have be named +this way: +- Initial sections should be top-level headers (have a single `#` character), + and be a single non-whitespace word (e.g. `1`, `empty-document`). +- Non-initial sections have names split in several parts separated by spaces. + If a test essentially moves to the left, it should be named ` left`, with + `` the name of the section from which it transitions. It should also have + as many `#` characters as parts (with a upper bound of 6). + * Names cannot contain whitespace. Names should be in snake-case. If a count + is associated with the test, it should be the last part of the test. If a + test repeats exactly what its previous test does, it should be named `x`. + * If a test performs multiple logically different actions, they should be + separated by `-then-` in the title of the test. + +Finally, sections should always be nested under the section from which they +transition. + +### Debugging + +While viewing a Markdown file in the [`commands`](./suite/commands) directory, +select "Run tests in this file" to debug all tests defined by the Markdown file. +Select "Run test on this line" to debug the test defined in the section under +the cursor. + +## Syntax of expected documents + +Expected documents are regular strings where some lines specify where +selections should be set. These selections are represented using carets, +numbers and pipes. + +These specifiers are replaced by spaces in the output document; if a line is +empty after removing specifiers, it is completely erased. + +By default, specifiers face forward (the anchor position comes before the +active position). + +
+ Examples + +> The following examples are also tested in [`utils.test.ts`]( + ./suite/utils.test.ts). + +1. Equivalent to [0:0 → 0:3]: + ``` + foo bar + ^^^ 0 + ``` +2. Equivalent to [0:0 → 0:3]: + ``` + foo bar + ^^| 0 + ``` +3. Equivalent to [0:3 → 0:0]: + ``` + foo bar + |^^ 0 + ``` +4. Equivalent to [0:0 → 0:3, 0:4 → 0:7]: + ``` + foo bar + ^^^ 0 + ^^^ 1 + ``` +5. Equivalent to [0:4 → 0:7, 0:0 → 0:3]: + ``` + foo bar + ^^^ 1 + ^^^ 0 + ``` +6. Equivalent to [0:0 → 0:1, 0:5 → 0:5]: + ``` + foo bar + ^ 0 | 1 + ``` +7. Equivalent to [0:0 → 2:4]: + ``` + foo + ^ 0 + bar + baz + ^ 0 + ``` +8. Equivalent to [0:0 → 2:4]: + ``` + foo + ^ 0 + bar + baz + | 0 + ``` +9. Equivalent to [2:4 → 0:0]: + ``` + foo + | 0 + bar + baz + ^ 0 + ``` +10. Equivalent to [2:4 → 0:0]: + ``` + foo + |^^ 0 + bar + baz + ^^^^ 0 + ``` +11. Equivalent to [0:0 → 1:4]: + ``` + + ^ 0 + abcd + ^ 0 + ``` +12. Equivalent to [0:3 → 0:3]: + ``` + foo + | 0 + bar + ``` +13. Equivalent to [0:0 → 1:0, 1:0 → 1:3]: + ``` + abc + ^^^^ 0 + def + ^^^ 1 + ``` +14. Equivalent to [1:2 → 0:3]: + ``` + abc + | 0 + def + ^^ 0 + ``` + +
diff --git a/test/suite/api.test.build.ts b/test/suite/api.test.build.ts new file mode 100644 index 0000000..4f2dea0 --- /dev/null +++ b/test/suite/api.test.build.ts @@ -0,0 +1,92 @@ +import * as assert from "assert"; + +import { Builder, unindent } from "../../meta"; +import { stringifyExpectedDocument } from "./build-utils"; + +export async function build(builder: Builder) { + const modules = await builder.getApiModules(); + + return modules.map((module) => { + const examples = module.functions.flatMap((f) => f.examples.map((example) => { + const match = exampleRe.exec(example); + + assert(match !== null, "Example does not have valid format"); + + const [_, flags, code, before, after] = match; + + return { + functionName: f.name, + flags, + code, + before, + after, + } as Example; + })); + + if (examples.length === 0) { + return ""; + } + + const examplesPerFunction = new Map(); + + // Note: we use the name starting with ./ below to make sure the filename is + // clickeable. + return unindent(4, ` + suite(${JSON.stringify(module.path.replace(/^dance/, "."))}, function () { + ${examples.map((example) => { + let testName = `function ${example.functionName}`; + const examplesForFunction = examplesPerFunction.get(example.functionName); + + if (examplesForFunction === undefined) { + examplesPerFunction.set(example.functionName, 1); + } else { + testName += `#${examplesForFunction}`; + examplesPerFunction.set(example.functionName, examplesForFunction + 1); + } + + const decls = [ + "editorState = extension.editors.getState(editor)!", + "context = new Context(editorState, cancellationToken)", + ]; + + for (const name of ["before", "after"] as const) { + const code = example[name]; + + if (code !== undefined) { + decls.push(`${name} = ExpectedDocument.parseIndented(${ + stringifyExpectedDocument(code, 22, 14)})`); + } + } + + return unindent(4, ` + test("${testName}", async function () { + const ${decls.join(",\n ")}; + + ${example.before !== undefined + ? "await before.apply(editor);" + : "// No setup needed."} + + await context.runAsync(async () => {${"\n" + + example.code.replace(/^/gm, " ".repeat(16)).trimEnd()} + }); + + ${example.after !== undefined + ? "after.assertEquals(editor);" + : "// No expected end document."} + }); + `); + }).join("")} + }); + `); + }).join("") + "});\n"; +} + +interface Example { + functionName: string; + flags: string; + code: string; + before?: string; + after?: string; +} + +const exampleRe = /^```(.+)\n([\s\S]+?)^```\n+(?:^(?:Before|With):\n^```\n([\s\S]*?)^```\n+(?:^After:\n^```\n([\s\S]+?)^```)?)?/m; diff --git a/test/suite/api.test.ts b/test/suite/api.test.ts new file mode 100644 index 0000000..0a9bae9 --- /dev/null +++ b/test/suite/api.test.ts @@ -0,0 +1,1353 @@ +/* eslint-disable require-await */ +import * as assert from "assert"; +import * as vscode from "vscode"; + +import { expect, ExpectedDocument } from "./utils"; +import { Context, deindentLines, EmptySelectionsError, filterSelections, indentLines, insert, isPosition, isRange, isSelection, joinLines, mergeOverlappingSelections, moveWhile, NotASelectionError, Positions, replace, rotate, rotateSelections, search, Select, Selections, selectionsLines, setSelections, text, updateSelections } from "../../src/api"; +import { Extension } from "../../src/state/extension"; +import { SelectionBehavior } from "../../src/state/modes"; + +function setSelectionBehavior(selectionBehavior: SelectionBehavior) { + Context.current.mode.update("_selectionBehavior", selectionBehavior); +} + +function resetNormalMode(extension: Extension) { + extension.modes.get("normal")?.update("_selectionBehavior", SelectionBehavior.Caret); +} + +suite("API tests", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor, + extension: Extension; + const cancellationToken = new vscode.CancellationTokenSource().token; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + extension = (await vscode.extensions.getExtension("gregoire.dance")!.activate()).extension; + }); + + this.beforeEach(() => resetNormalMode(extension)); + + this.afterAll(async () => { + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + }); + + // + // Content below this line was auto-generated by api.test.build.ts. Do not edit manually. + + suite("./src/api/context.ts", function () { + + test("function text", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + foo bar + `); + + await before.apply(editor); + + await context.runAsync(async () => { + const start = new vscode.Position(0, 0), + end = new vscode.Position(0, 3); + + expect( + text(new vscode.Range(start, end)), + "to be", + "foo", + ); + }); + + // No expected end document. + }); + + test("function text#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + foo bar + `); + + await before.apply(editor); + + await context.runAsync(async () => { + const start1 = new vscode.Position(0, 0), + end1 = new vscode.Position(0, 3), + start2 = new vscode.Position(0, 4), + end2 = new vscode.Position(0, 7); + + expect( + text([new vscode.Range(start1, end1), new vscode.Range(start2, end2)]), + "to equal", + ["foo", "bar"], + ); + }); + + // No expected end document. + }); + + }); + + suite("./src/api/functional.ts", function () { + + test("function isPosition", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken); + + // No setup needed. + + await context.runAsync(async () => { + const position = new vscode.Position(0, 0), + range = new vscode.Range(position, position), + selection = new vscode.Selection(position, position); + + assert(isPosition(position)); + assert(!isPosition(range)); + assert(!isPosition(selection)); + }); + + // No expected end document. + }); + + test("function isRange", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken); + + // No setup needed. + + await context.runAsync(async () => { + const position = new vscode.Position(0, 0), + range = new vscode.Range(position, position), + selection = new vscode.Selection(position, position); + + assert(!isRange(position)); + assert(isRange(range)); + assert(!isRange(selection)); + }); + + // No expected end document. + }); + + test("function isSelection", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken); + + // No setup needed. + + await context.runAsync(async () => { + const position = new vscode.Position(0, 0), + range = new vscode.Range(position, position), + selection = new vscode.Selection(position, position); + + assert(!isSelection(position)); + assert(!isSelection(range)); + assert(isSelection(selection)); + }); + + // No expected end document. + }); + + }); + + suite("./src/api/edit/index.ts", function () { + + test("function insert", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + 1 2 3 + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + 2 4 6 + ^ 0 + ^ 1 + ^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + Selections.set(await insert(insert.Replace, (x) => `${+x * 2}`)); + }); + + after.assertEquals(editor); + }); + + test("function byIndex", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + 1a 2b 3c + ^ 0 + ^ 1 + ^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + Selections.set(await insert.byIndex(insert.Start, (i) => `${i + 1}`)); + }); + + after.assertEquals(editor); + }); + + test("function byIndex#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + 1a 2b 3c + ^ 0 + ^ 1 + ^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + Selections.set(await insert.byIndex(insert.Start | insert.Select, (i) => `${i + 1}`)); + }); + + after.assertEquals(editor); + }); + + test("function byIndex#2", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + 1a 2b 3c + ^^ 0 + ^^ 1 + ^^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + Selections.set(await insert.byIndex(insert.Start | insert.Extend, (i) => `${i + 1}`)); + }); + + after.assertEquals(editor); + }); + + test("function byIndex#3", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a1 b2 c3 + ^ 0 + ^ 1 + ^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + Selections.set(await insert.byIndex(insert.End, (i) => `${i + 1}`)); + }); + + after.assertEquals(editor); + }); + + test("function byIndex#4", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a1 b2 c3 + ^ 0 + ^ 1 + ^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + Selections.set(await insert.byIndex(insert.End | insert.Select, (i) => `${i + 1}`)); + }); + + after.assertEquals(editor); + }); + + test("function byIndex#5", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a1 b2 c3 + ^^ 0 + ^^ 1 + ^^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + Selections.set(await insert.byIndex(insert.End | insert.Extend, (i) => `${i + 1}`)); + }); + + after.assertEquals(editor); + }); + + test("function replace", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + 1 2 3 + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + 2 4 6 + ^ 0 + ^ 1 + ^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await replace((x) => `${+x * 2}`); + }); + + after.assertEquals(editor); + }); + + test("function byIndex#6", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + 1 2 3 + ^ 0 + ^ 1 + ^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await replace.byIndex((i) => `${i + 1}`); + }); + + after.assertEquals(editor); + }); + + test("function rotate", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + b c a + ^ 1 + ^ 2 + ^ 0 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await rotate(1); + }); + + after.assertEquals(editor); + }); + + test("function selectionsOnly", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 0 + ^ 1 + ^ 2 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a b c + ^ 1 + ^ 2 + ^ 0 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + rotate.selectionsOnly(1); + }); + + after.assertEquals(editor); + }); + + }); + + suite("./src/api/search/index.ts", function () { + + test("function backward", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abc + `); + + await before.apply(editor); + + await context.runAsync(async () => { + const [p1, [t1]] = search.backward(/\w/, new vscode.Position(0, 1))!; + + assert.deepStrictEqual(p1, new vscode.Position(0, 0)); + assert.strictEqual(t1, "a"); + + const [p2, [t2]] = search.backward(/\w/, new vscode.Position(0, 2))!; + + assert.deepStrictEqual(p2, new vscode.Position(0, 1)); + assert.strictEqual(t2, "b"); + + const [p3, [t3]] = search.backward(/\w+/, new vscode.Position(0, 2))!; + + assert.deepStrictEqual(p3, new vscode.Position(0, 0)); + assert.strictEqual(t3, "ab"); + + assert.strictEqual( + search.backward(/\w/, new vscode.Position(0, 0)), + undefined, + ); + }); + + // No expected end document. + }); + + test("function forward", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abc + `); + + await before.apply(editor); + + await context.runAsync(async () => { + const [p1, [t1]] = search.forward(/\w/, new vscode.Position(0, 0))!; + + assert.deepStrictEqual(p1, new vscode.Position(0, 0)); + assert.strictEqual(t1, "a"); + + const [p2, [t2]] = search.forward(/\w/, new vscode.Position(0, 1))!; + + assert.deepStrictEqual(p2, new vscode.Position(0, 1)); + assert.strictEqual(t2, "b"); + + const [p3, [t3]] = search.forward(/\w+/, new vscode.Position(0, 1))!; + + assert.deepStrictEqual(p3, new vscode.Position(0, 1)); + assert.strictEqual(t3, "bc"); + + assert.strictEqual( + search.forward(/\w/, new vscode.Position(0, 3)), + undefined, + ); + }); + + // No expected end document. + }); + + }); + + suite("./src/api/edit/linewise.ts", function () { + + test("function indentLines", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a + + c + d + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a + + c + d + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await indentLines([0, 1, 3]); + }); + + after.assertEquals(editor); + }); + + test("function indentLines#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await indentLines([0], 2); + }); + + after.assertEquals(editor); + }); + + test("function indentLines#2", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a + + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a + ·· + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await indentLines([0, 1], 1, true); + }); + + after.assertEquals(editor); + }); + + test("function deindentLines", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a + ·· + c + d + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a + + c + d + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await deindentLines([0, 1, 3]); + }); + + after.assertEquals(editor); + }); + + test("function deindentLines#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a + ·· + c + d + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a + + c + d + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await deindentLines([0, 1, 3], 2); + }); + + after.assertEquals(editor); + }); + + test("function joinLines", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b + c d + e f + g h + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a b c d + e f + g h + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await joinLines([0]); + }); + + after.assertEquals(editor); + }); + + test("function joinLines#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b + c d + e f + g h + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a b c d + e f + g h + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await joinLines([0, 1]); + }); + + after.assertEquals(editor); + }); + + test("function joinLines#2", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b + c d + e f + g h + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a b c d + e f g h + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await joinLines([0, 2]); + }); + + after.assertEquals(editor); + }); + + test("function joinLines#3", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a b + c d + e f + g h + `), + after = ExpectedDocument.parseIndented(14, String.raw` + a b + c d e f + g h + `); + + await before.apply(editor); + + await context.runAsync(async () => { + await joinLines([1], " "); + }); + + after.assertEquals(editor); + }); + + }); + + suite("./src/api/search/move.ts", function () { + + test("function backward", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abc + `); + + await before.apply(editor); + + await context.runAsync(async () => { + assert.deepStrictEqual( + moveWhile.backward((c) => /\w/.test(c), new vscode.Position(0, 3)), + new vscode.Position(0, 0), + ); + + assert.deepStrictEqual( + moveWhile.backward((c) => c === "c", new vscode.Position(0, 3)), + new vscode.Position(0, 2), + ); + + assert.deepStrictEqual( + moveWhile.backward((c) => c === "b", new vscode.Position(0, 3)), + new vscode.Position(0, 3), + ); + }); + + // No expected end document. + }); + + test("function forward", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abc + `); + + await before.apply(editor); + + await context.runAsync(async () => { + assert.deepStrictEqual( + moveWhile.forward((c) => /\w/.test(c), new vscode.Position(0, 0)), + new vscode.Position(0, 3), + ); + + assert.deepStrictEqual( + moveWhile.forward((c) => c === "a", new vscode.Position(0, 0)), + new vscode.Position(0, 1), + ); + + assert.deepStrictEqual( + moveWhile.forward((c) => c === "b", new vscode.Position(0, 0)), + new vscode.Position(0, 0), + ); + }); + + // No expected end document. + }); + + }); + + suite("./src/api/selections.ts", function () { + + test("function setSelections", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + hello world + ^ 0 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + hello world + ^^^^^ 0 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + const start = new vscode.Position(0, 6), + end = new vscode.Position(0, 11); + + setSelections([new vscode.Selection(start, end)]); + }); + + after.assertEquals(editor); + }); + + test("function setSelections#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken); + + // No setup needed. + + await context.runAsync(async () => { + assert.throws(() => setSelections([]), EmptySelectionsError); + assert.throws(() => setSelections([1 as any]), NotASelectionError); + }); + + // No expected end document. + }); + + test("function filterSelections", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + foo 123 + ^^^ 0 + ^^^ 1 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + const atChar = (character: number) => new vscode.Position(0, character); + + assert.deepStrictEqual( + filterSelections((text) => !isNaN(+text)), + [new vscode.Selection(atChar(4), atChar(7))], + ); + }); + + // No expected end document. + }); + + test("function filterSelections#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + foo 123 + ^^^ 0 + ^^^ 1 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + const atChar = (character: number) => new vscode.Position(0, character); + + assert.deepStrictEqual( + await filterSelections(async (text) => !isNaN(+text)), + [new vscode.Selection(atChar(4), atChar(7))], + ); + }); + + // No expected end document. + }); + + test("function updateSelections", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + foo 123 + ^^^ 0 + ^^^ 1 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + foo 123 + |^^ 0 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + const reverseUnlessNumber = (text: string, sel: vscode.Selection) => + isNaN(+text) ? new vscode.Selection(sel.active, sel.anchor) : undefined; + + updateSelections(reverseUnlessNumber); + }); + + after.assertEquals(editor); + }); + + test("function updateSelections#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + foo 123 + ^^^ 0 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + assert.throws(() => updateSelections(() => undefined), EmptySelectionsError); + }); + + // No expected end document. + }); + + test("function updateSelections#2", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + foo 123 + ^^^ 0 + ^^^ 1 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + foo 123 + |^^ 0 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + const reverseIfNumber = async (text: string, sel: vscode.Selection) => + !isNaN(+text) ? new vscode.Selection(sel.active, sel.anchor) : undefined; + + await updateSelections(reverseIfNumber); + }); + + after.assertEquals(editor); + }); + + test("function rotateSelections", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + foo bar baz + ^^^ 0 ^^^ 2 + ^^^ 1 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + foo bar baz + ^^^ 1 ^^^ 0 + ^^^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + setSelections(rotateSelections(1)); + }); + + after.assertEquals(editor); + }); + + test("function rotateSelections#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + foo bar baz + ^^^ 0 ^^^ 2 + ^^^ 1 + `), + after = ExpectedDocument.parseIndented(14, String.raw` + foo bar baz + ^^^ 2 ^^^ 1 + ^^^ 0 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + setSelections(rotateSelections(-1)); + }); + + after.assertEquals(editor); + }); + + test("function selectionsLines", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + ab + ^^ 0 + cd + ^ 1 + ef + gh + ^ 2 + ^ 3 + ij + ^ 3 + kl + | 4 + mn + ^^ 5 + op + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(selectionsLines(), "to only contain", 0, 1, 3, 4, 5, 6); + }); + + // No expected end document. + }); + + test("function mergeOverlappingSelections", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abcd + ^^ 0 + ^^ 1 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(mergeOverlappingSelections(Selections.current), "to equal", [Selections.current[0]]); + }); + + // No expected end document. + }); + + test("function mergeOverlappingSelections#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abcd + | 0 + | 1 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(mergeOverlappingSelections(Selections.current), "to equal", [Selections.current[0]]); + }); + + // No expected end document. + }); + + test("function mergeOverlappingSelections#2", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abcd + ^^^ 0 + ^^^ 1 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(mergeOverlappingSelections(Selections.current), "to satisfy", [ + expect.it("to start at coords", 0, 0).and("to end at coords", 0, 4), + ]); + }); + + // No expected end document. + }); + + test("function mergeOverlappingSelections#3", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abcd + ^^ 0 + ^^ 1 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.mergeOverlapping(Selections.current), "to equal", Selections.current); + + expect(Selections.mergeConsecutive(Selections.current), "to satisfy", [ + expect.it("to start at coords", 0, 0).and("to end at coords", 0, 4), + ]); + }); + + // No expected end document. + }); + + test("function mergeOverlappingSelections#4", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abcd + ^^ 1 + ^^ 0 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.mergeOverlapping(Selections.current), "to equal", Selections.current); + + expect(Selections.mergeConsecutive(Selections.current), "to satisfy", [ + expect.it("to start at coords", 0, 0).and("to end at coords", 0, 4), + ]); + }); + + // No expected end document. + }); + + test("function shift", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken); + + // No setup needed. + + await context.runAsync(async () => { + const s1 = Selections.empty(0, 0), + shifted1 = Selections.shift(s1, Positions.at(0, 4), Select); + + expect(shifted1, "to have anchor at coords", 0, 0).and("to have cursor at coords", 0, 4); + }); + + // No expected end document. + }); + + test("function shift#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken); + + // No setup needed. + + await context.runAsync(async () => { + setSelectionBehavior(SelectionBehavior.Character); + }); + + // No expected end document. + }); + + test("function isEntireLine", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abc + ^^^^ 0 + + def + ^^^ 1 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.isEntireLine(Selections.current[0]), "to be true"); + expect(Selections.isEntireLine(Selections.current[1]), "to be false"); + }); + + // No expected end document. + }); + + test("function isEntireLine#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abc + ^^^^ 0 + def + ^^^^ 0 + + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.isEntireLine(Selections.current[0]), "to be false"); + }); + + // No expected end document. + }); + + test("function isEntireLines", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abc + ^^^^ 0 + def + ^^^^ 0 + ghi + ^^^^ 1 + jkl + ^^^^ 2 + mno + ^^^ 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.isEntireLines(Selections.current[0]), "to be true"); + expect(Selections.isEntireLines(Selections.current[1]), "to be true"); + expect(Selections.isEntireLines(Selections.current[2]), "to be false"); + }); + + // No expected end document. + }); + + test("function length", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + abc + ^^^^ 0 + def + ^^^ 0 + ghi + ^ 1 + | 2 + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.length(Selections.current[0]), "to be", 7); + expect(Selections.length(Selections.current[1]), "to be", 1); + expect(Selections.length(Selections.current[2]), "to be", 0); + }); + + // No expected end document. + }); + + test("function fromStartEnd", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken); + + // No setup needed. + + await context.runAsync(async () => { + const p0 = new vscode.Position(0, 0), + p1 = new vscode.Position(0, 1); + + expect(Selections.fromStartEnd(p0, p1, false), "to satisfy", { + start: p0, + end: p1, + anchor: p0, + active: p1, + isReversed: false, + }); + + expect(Selections.fromStartEnd(p0, p1, true), "to satisfy", { + start: p0, + end: p1, + anchor: p1, + active: p0, + isReversed: true, + }); + }); + + // No expected end document. + }); + + test("function toCharacterMode", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a + b + `); + + await before.apply(editor); + + await context.runAsync(async () => { + // One-character selection becomes empty. + expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 0, 1)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 0), + ]); + + // One-character selection becomes empty (at line break). + expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 1, 0)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 1), + ]); + + // Forward-facing selection becomes shorter. + expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 1, 1)]), "to satisfy", [ + expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 1, 0), + ]); + + // One-character selection becomes empty (reversed). + expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 0, 0)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 0), + ]); + + // One-character selection becomes empty (reversed, at line break). + expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 0, 0, 1)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 1), + ]); + + // Reversed selection stays as-is. + expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 1, 0, 0)]), "to satisfy", [ + expect.it("to have anchor at coords", 1, 1).and("to have cursor at coords", 0, 0), + ]); + }); + + // No expected end document. + }); + + test("function fromCharacterMode", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 0), + ]); + }); + + // No expected end document. + }); + + test("function fromCharacterMode#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a + b + + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ + expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 0, 1), + ]); + + // At the end of the line, it selects the line ending: + expect(Selections.fromCharacterMode([Selections.empty(0, 1)]), "to satisfy", [ + expect.it("to have anchor at coords", 0, 1).and("to have cursor at coords", 1, 0), + ]); + + // But it does nothing at the end of the document: + expect(Selections.fromCharacterMode([Selections.empty(2, 0)]), "to satisfy", [ + expect.it("to be empty at coords", 2, 0), + ]); + }); + + // No expected end document. + }); + + }); +}); diff --git a/test/suite/build-utils.ts b/test/suite/build-utils.ts new file mode 100644 index 0000000..a585034 --- /dev/null +++ b/test/suite/build-utils.ts @@ -0,0 +1,24 @@ +import * as assert from "assert"; + +export function stringifyExpectedDocument(code: string, codeIndent: number, indent: number) { + code = code.replace(/`/g, "\\`").replace(/^/gm, " ".repeat(codeIndent)); + code = code.slice(0, code.length - 2); // De-indent end line. + + return `${indent}, String.raw\`\n${code}\``; +} + +export function longestStringLength(f: (v: T) => string, values: readonly T[]) { + return values.reduce((longest, curr) => Math.max(longest, f(curr).length), 0); +} + +export function execAll(re: RegExp, contents: string) { + assert(re.global); + + const matches = [] as RegExpExecArray[]; + + for (let match = re.exec(contents); match !== null; match = re.exec(contents)) { + matches.push(match); + } + + return matches; +} diff --git a/test/suite/commands.build.ts b/test/suite/commands.build.ts new file mode 100644 index 0000000..9236374 --- /dev/null +++ b/test/suite/commands.build.ts @@ -0,0 +1,275 @@ +import * as assert from "assert"; +import * as fs from "fs/promises"; +import * as path from "path"; + +import { unindent } from "../../meta"; +import { execAll, stringifyExpectedDocument } from "./build-utils"; + +export async function build() { + const commandsDir = path.join(__dirname, "commands"), + fileNames = await fs.readdir(commandsDir); + + for (const file of fileNames.filter((f) => f.endsWith(".md"))) { + const filePath = path.resolve(commandsDir, file), + contents = await fs.readFile(filePath, "utf-8"), + { tests } = parseMarkdownTests(contents); + + await fs.writeFile(filePath.replace(/\.md$/, ".test.ts"), unindent(6, `\ + import * as vscode from "vscode"; + + import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + + suite("./test/suite/commands/${path.basename(file)}", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + ${tests.map((test) => { + return unindent(4, ` + test("${test.titleParts.join(" > ")}", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, ${replaceInTest( + test.comesAfter, stringifyExpectedDocument(test.comesAfter.code, 16, 6))}); + + // Perform all operations.${"\n" + + stringifyOperations(test).replace(/^/gm, " ".repeat(14))} + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/${ + path.basename(file)}:${test.line + 1}:1", ${replaceInTest( + test, stringifyExpectedDocument(test.code, 16, 6))}); + });`); + }).join("\n")} + + groupTestsByParentName(this); + }); + `)); + } +} + +interface TestOperation { + command: string; + args?: string; +} + +interface Section { + line: number; + title: string; + code: string; + debug?: boolean; + replacements?: [RegExp, string][]; + behavior: "caret" | "character"; +} + +interface Test extends Section { + titleParts: string[]; + comesAfter: Section; + operations: TestOperation[]; +} + +interface InitialDocument extends Section { +} + +function parseMarkdownTests(contents: string) { + const re = /^#+ (.+)\n(?:\[.+?\]\(#(.+?)\)\n)?([\s\S]+?)^```\n([\s\S]+?)^```\n/gm, + opre = /^- *([\w.:]+)( +.+)?$|^> *(.+)$/gm, + initial = [] as InitialDocument[], + tests = [] as Test[], + all = new Map(), + lines = contents.split("\n"); + let currentLine = 0; + + for (const [text, badTitle, comesAfterTitle, operationsText, after] of execAll(re, contents)) { + const title = badTitle.replace(/\s/g, "-"); + + assert(!all.has(title), `document state "${title}" is defined multiple times`); + + const titleParts = badTitle.split(" "), + nesting = /^#+/.exec(text)![0].length; + + if (titleParts.length !== nesting && titleParts.length <= 6) { + console.warn(`section "${title}" has ${titleParts} parts but a nesting of ${nesting}`); + } + + const line = lines.indexOf(text.slice(0, text.indexOf("\n")), currentLine); + currentLine = line + 1; + + if (comesAfterTitle === undefined) { + if (nesting !== 1) { + console.warn("an initial section should have a top-level header"); + } + + const flags = execAll(/^> *(.+)$/gm, operationsText).map(([_, flag]) => flag), + behavior = getBehavior(flags) ?? "caret", + data: InitialDocument = { title, code: after, behavior, line }; + + applyFlags(flags, data); + + initial.push(data); + all.set(title, data); + continue; + } + + const lastTitlePart = /\S+$/.exec(badTitle)![0], + expectedTitle = comesAfterTitle + "-" + lastTitlePart; + + if (title !== expectedTitle) { + console.warn(`section "${title}" should be called "${expectedTitle}"`); + } + + if (!/^([a-z]+)(-([a-z]+|\d+))*$/.test(lastTitlePart)) { + console.warn(`section "${title}" has an invalid name`); + } + + const operations = [] as TestOperation[], + flags = [] as string[]; + + for (const [_, command, args, flag] of execAll(opre, operationsText)) { + if (flag) { + flags.push(flag); + } else { + operations.push({ command, args }); + } + } + + const comesAfter = all.get(comesAfterTitle); + + assert(comesAfter !== undefined, `test "${title}" depends on unknown test "${comesAfterTitle}"`); + + const behavior = getBehavior(flags) ?? comesAfter.behavior, + data: Test = { title, comesAfter, code: after, operations, behavior, titleParts, line }; + + applyFlags(flags, data); + + tests.push(data); + all.set(title, data); + } + + assert.strictEqual( + execAll(/^#+ /gm, contents).length, + tests.length + initial.length, + "not all tests were parsed", + ); + + return { setups: initial, tests }; +} + +function applyFlags(flags: readonly string[], section: Section) { + for (const flag of flags) { + if (flag.startsWith("/")) { + const split = flag.slice(1).split(/(? setTimeout(() => executeCommand("type", { text: ${ + JSON.stringify(text)} }).then(resolve), ${promises.length * 20}))`, + ); + } + + if (promises.length === 1) { + text += `await ${promises[0]};\n`; + } else { + text += `await Promise.all([${promises.map((x) => `\n ${x},`)}]);\n`; + } + } + + return replaceInTest(test, text + textEnd); +} diff --git a/test/suite/commands.test.ts b/test/suite/commands.test.ts deleted file mode 100644 index cd7b768..0000000 --- a/test/suite/commands.test.ts +++ /dev/null @@ -1,343 +0,0 @@ -// Enhance stack traces with the TypeScript source pos instead of compiled JS. -// This is only included in tests to avoid introducing a production dependency. -import "source-map-support/register"; - -import * as assert from "assert"; -import * as fs from "fs"; -import * as path from "path"; -import * as vscode from "vscode"; - -import { Command } from "../../commands"; -import { CommandDescriptor } from "../../src/commands"; -import { extensionState } from "../../src/extension"; -import { SelectionBehavior } from "../../src/state/extension"; - -export namespace testCommands { - export interface Mutation { - readonly commands: readonly (string | { readonly command: string; readonly args: any[] })[]; - readonly contentAfterMutation: string; - } - - export interface Options { - readonly initialContent: string; - readonly mutations: readonly Mutation[]; - readonly selectionBehavior: SelectionBehavior; - readonly allowErrors: boolean; - } -} - -/** - * Used to indicate the anchor ("{0}") and active ("|{0}") carets of each - * selection in the document. e.g. "a{0}bcd|{0}ef" indicates "bcd" selected. - */ -const selectionMarkerRegexp = /(\|)?{(\d+)}/g; - -/* - * Used to mark the end of line, which helps to indicate trailing whitespace - * on a line or (trailing) empty lines (if placed by itself on a line). - */ -const eolMarkerRegexp = /{EOL}/g; - -function getPlainContent(templatedContent: string) { - return templatedContent - .trimRight() - .replace(eolMarkerRegexp, "") - .replace(selectionMarkerRegexp, ""); -} - -function getSelections(document: vscode.TextDocument, templatedContent: string) { - const anchorPositions = [] as vscode.Position[]; - const activePositions = [] as vscode.Position[]; - - let match: RegExpExecArray | null = null; - let diff = 0; - - const contentAndSelections = templatedContent.trimRight().replace(eolMarkerRegexp, ""); - - while ((match = selectionMarkerRegexp.exec(contentAndSelections))) { - const index = +match[2]; - - if (match[1] === "|") { - activePositions[index] = document.positionAt(match.index - diff); - - if (anchorPositions[index] === undefined) { - anchorPositions[index] = activePositions[index]; - } - } else { - anchorPositions[index] = document.positionAt(match.index - diff); - } - - diff += match[0].length; - } - - return Array.from(anchorPositions, (anchor, i) => { - if (!anchor) { - throw new Error(`Selection ${i} is not specified.`); - } - return new vscode.Selection(anchor, activePositions[i]); - }); -} - -function stringifySelection(document: vscode.TextDocument, selection: vscode.Selection) { - const content = document.getText(); - const startOffset = document.offsetAt(selection.start), - endOffset = document.offsetAt(selection.end), - [startString, endString] = selection.isReversed ? ["|", "<"] : [">", "|"]; - - if (selection.isEmpty) { - return content.substring(0, startOffset) + "|" + content.substring(startOffset); - } else { - return ( - content.substring(0, startOffset) - + startString - + content.substring(startOffset, endOffset) - + endString - + content.substring(endOffset) - ); - } -} - -async function testCommands( - editor: vscode.TextEditor, - { initialContent, mutations, selectionBehavior, allowErrors }: testCommands.Options, -) { - // @ts-ignore - extensionState._selectionBehavior = selectionBehavior; - CommandDescriptor.throwOnError = !allowErrors; - - const content = getPlainContent(initialContent); - const document = editor.document; - - await editor.edit((builder) => - builder.replace( - new vscode.Range( - new vscode.Position(0, 0), - document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end, - ), - content, - ), - ); - - // Set up initial selections. - const initialSelections = getSelections(document, initialContent); - - editor.selections = initialSelections; - - // For each mutation... - let mutationIndex = 1; - - for (const { commands, contentAfterMutation } of mutations) { - // Execute commands. - for (let i = 0; i < commands.length; i++) { - const command = commands[i]; - const { command: commandName, args } - = typeof command === "string" ? { command, args: [] } : command; - - const promise = vscode.commands.executeCommand(commandName, ...args); - - while ( - i < commands.length - 1 - && typeof commands[i + 1] === "string" - && (commands[i + 1] as string).startsWith("type:") - ) { - await new Promise((resolve) => { - setTimeout(() => { - vscode.commands - .executeCommand("type", { text: (commands[i + 1] as string)[5] }) - .then(resolve); - }, 20); - }); - - i++; - } - - await promise; - } - - // Ensure resulting text is valid. - const prefix = mutations.length === 1 ? "" : `After ${mutationIndex} mutation(s):\n `; - - const expectedContent = getPlainContent(contentAfterMutation); - - assert.strictEqual( - document.getText(), - expectedContent, - `${prefix}Document text is not as expected.`, - ); - - // Set up expected selections. - const expectedSelections = getSelections(document, contentAfterMutation); - - // Ensure resulting selections are right. - assert.strictEqual( - editor.selections.length, - expectedSelections.length, - `${prefix}Expected ${expectedSelections.length} selection(s), ` - + `but had ${editor.selections.length}.`, - ); - - for (let i = 0; i < expectedSelections.length; i++) { - if (editor.selections[i].isEqual(expectedSelections[i])) { - continue; - } - - const expected = stringifySelection(document, expectedSelections[i]); - const actual = stringifySelection(document, editor.selections[i]); - - assert.strictEqual( - actual, - expected, - `${prefix}Expected Selection #${i} to match ('>' is anchor, '|' is cursor).`, - ); - // If stringified results are the same, throw message using strict equal. - assert.deepStrictEqual( - editor.selections[i], - expectedSelections[i], - `(Actual Selection #${i} is at same spots in document as expected, ` - + `but with different numbers)`, - ); - assert.fail(); - } - - mutationIndex++; - } -} - -suite("Running commands", function () { - let document: vscode.TextDocument; - let editor: vscode.TextEditor; - - this.beforeAll(async () => { - document = await vscode.workspace.openTextDocument(); - editor = await vscode.window.showTextDocument(document); - }); - - test("mutation tests work correctly", async function () { - await testCommands(editor, { - initialContent: `{0}f|{0}oo`, - mutations: [{ contentAfterMutation: `{0}fo|{0}o`, commands: [Command.rightExtend] }], - selectionBehavior: SelectionBehavior.Character, - allowErrors: false, - }); - }); - - test("mutation tests catch errors correctly", async function () { - try { - await testCommands(editor, { - initialContent: `|{0}foo`, - mutations: [{ contentAfterMutation: `|{0}foo`, commands: [Command.rightExtend] }], - selectionBehavior: SelectionBehavior.Character, - allowErrors: false, - }); - } catch (err) { - if ( - err instanceof Error - && err.message === `Expected Selection #0 to match ('>' is anchor, '|' is cursor).` - ) { - return; - } - - throw err; - } - - assert.fail(`Expected error.`); - }); - - const basedir = this.file!.replace("\\out\\", "\\").replace("/out/", "/").replace(".test.js", ""), - fileNames = fs.readdirSync(basedir), - longestFileName = fileNames.reduce((longest, curr) => - curr.length > longest.length ? curr : longest, - ), - fileNamePadding = longestFileName.length; - - for (const file of fileNames) { - const fullPath = path.join(basedir, file.padEnd(fileNamePadding)); - const friendlyPath = fullPath.substr(/dance.test.suite/.exec(fullPath)!.index); - const selectionBehavior = file.endsWith(".caret") - ? SelectionBehavior.Caret - : SelectionBehavior.Character; - - const content = fs - .readFileSync(fullPath.trimRight(), { encoding: "utf8" }) - .replace(/^\/\/[^=].*\n/gm, ""); // Remove //-comments. - const sections = content.split(/(^\/\/== [\w.]+(?: > [\w.]+)?$\n(?:^\/\/= .+$\n)*)/gm); - const nodes = new Map(); - const results = new Map>(); - const initialContent = sections[0].trim() + "\n"; - - nodes.set("root", initialContent); - nodes.set("0", initialContent); - results.set("root", Promise.resolve(true)); - results.set("0", Promise.resolve(true)); - - // Find longest section name for padding. - let longestSectionNameLength = 0; - - for (let i = 1; i < sections.length; i += 2) { - const [_, sectionIn, sectionOut] = /^\/\/== ([\w.]+)(?: > ([\w.]+))?$/m.exec(sections[i])!; - - longestSectionNameLength = Math.max( - longestSectionNameLength, - sectionIn.length, - sectionOut?.length ?? 0, - ); - } - - // Run all tests in the file. - for (let i = 1; i < sections.length; i += 2) { - const metadata = sections[i], - content = sections[i + 1]; - - const [full, from, to] = /^\/\/== ([\w.]+)(?: > ([\w.]+))?$/m.exec(metadata)!; - const commands = metadata - .substr(full.length) - .split("\n") - .map((x) => x.substr(3).trim()) - .filter((x) => x) - .map((str) => (str[0] === "{" ? JSON.parse(str) : str)); - const contentAfterMutation = content; - const initialContent = nodes.get(from)!; - - if (to === undefined) { - assert(commands.length === 0, `Cannot define commands in base section.`); - - nodes.set(from, contentAfterMutation); - results.set(from, Promise.resolve(true)); - - continue; - } - - assert(typeof initialContent === "string"); - assert(!nodes.has(to)); - - let setSuccess: (success: boolean) => void; - - nodes.set(to, contentAfterMutation); - results.set(to, new Promise((resolve) => (setSuccess = resolve))); - - test(`${friendlyPath}: mutation ${from.padEnd(longestSectionNameLength)} > ${to.padEnd( - longestSectionNameLength, - )} is applied correctly`, async function () { - if (!(await results.get(from)!)) { - setSuccess(false); - this.skip(); - } - - let success = false; - - try { - await testCommands(editor, { - initialContent, - mutations: [{ contentAfterMutation, commands }], - selectionBehavior, - allowErrors: true, - }); - - success = true; - } finally { - setSuccess(success); - } - }); - } - } -}); diff --git a/test/suite/commands/edit-deindent.md b/test/suite/commands/edit-deindent.md new file mode 100644 index 0000000..f184a0d --- /dev/null +++ b/test/suite/commands/edit-deindent.md @@ -0,0 +1,38 @@ +# 1 + +``` +foo +|^^ 0 + bar + baz + quux + ^ 0 +``` + +## 1 deindent +[up](#1) + +- .edit.deindent.withIncomplete + +``` +foo +|^^ 0 +bar +baz + quux + ^ 0 +``` + +## 1 deindent-alt +[up](#1) + +- .edit.deindent + +``` +foo +|^^ 0 + bar +baz + quux + ^ 0 +``` diff --git a/test/suite/commands/edit-deindent.test.ts b/test/suite/commands/edit-deindent.test.ts new file mode 100644 index 0000000..7be5259 --- /dev/null +++ b/test/suite/commands/edit-deindent.test.ts @@ -0,0 +1,72 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/edit-deindent.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > deindent", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + |^^ 0 + bar + baz + quux + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.edit.deindent.withIncomplete"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-deindent.md:12:1", 6, String.raw` + foo + |^^ 0 + bar + baz + quux + ^ 0 + `); + }); + + test("1 > deindent-alt", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + |^^ 0 + bar + baz + quux + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.edit.deindent"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-deindent.md:26:1", 6, String.raw` + foo + |^^ 0 + bar + baz + quux + ^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/edit-indent.md b/test/suite/commands/edit-indent.md new file mode 100644 index 0000000..105336d --- /dev/null +++ b/test/suite/commands/edit-indent.md @@ -0,0 +1,130 @@ +# 1 + +``` +The quick brown fox +^ 0 +jumps over the lazy + ^ 0 +dog. +``` + +## 1 indent +[up](#1) + +- .edit.indent + +``` + The quick brown fox + ^ 0 + jumps over the lazy + ^ 0 +dog. +``` + +# 2 + +``` +The quick brown fox +^ 0 +jumps over the lazy + ^ 0 +dog. +``` + +## 2 indent +[up](#2) + +- .edit.indent + +``` + The quick brown fox + ^ 0 + jumps over the lazy + ^ 0 +dog. +``` + +# 3 + +``` +The quick brown fox +^^^^^^^^^^^^^^^ 0 +jumps over the lazy +dog. +``` + +## 3 indent +[up](#3) + +- .edit.indent + +``` + The quick brown fox + ^^^^^^^^^^^^^^^ 0 +jumps over the lazy +dog. +``` + +# 4 + +``` +The quick brown fox +|^^^^^^^^^^^^^^ 0 +jumps over the lazy +dog. +``` + +## 4 indent +[up](#4) + +- .edit.indent + +``` + The quick brown fox + |^^^^^^^^^^^^^^ 0 +jumps over the lazy +dog. +``` + +# 5 + +``` +The quick brown fox +^^^^^^^^^^^^^^^^^^^^ 0 +jumps over the lazy +dog. +``` + +## 5 indent +[up](#5) + +- .edit.indent + +``` + The quick brown fox + ^^^^^^^^^^^^^^^^^^^^ 0 +jumps over the lazy +dog. +``` + +# 6 +No character is selected at all, so we just indent the active line. + +``` +The quick brown fox +| 0 +jumps over the lazy +dog. +``` + +## 6 indent +[up](#6) + +- .edit.indent + +``` + The quick brown fox + | 0 +jumps over the lazy +dog. +``` diff --git a/test/suite/commands/edit-indent.test.ts b/test/suite/commands/edit-indent.test.ts new file mode 100644 index 0000000..b11b24c --- /dev/null +++ b/test/suite/commands/edit-indent.test.ts @@ -0,0 +1,152 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/edit-indent.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > indent", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^ 0 + jumps over the lazy + ^ 0 + dog. + `); + + // Perform all operations. + await executeCommand("dance.edit.indent"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-indent.md:11:1", 6, String.raw` + The quick brown fox + ^ 0 + jumps over the lazy + ^ 0 + dog. + `); + }); + + test("2 > indent", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^ 0 + jumps over the lazy + ^ 0 + dog. + `); + + // Perform all operations. + await executeCommand("dance.edit.indent"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-indent.md:34:1", 6, String.raw` + The quick brown fox + ^ 0 + jumps over the lazy + ^ 0 + dog. + `); + }); + + test("3 > indent", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^^^^^^^^^^^^^ 0 + jumps over the lazy + dog. + `); + + // Perform all operations. + await executeCommand("dance.edit.indent"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-indent.md:56:1", 6, String.raw` + The quick brown fox + ^^^^^^^^^^^^^^^ 0 + jumps over the lazy + dog. + `); + }); + + test("4 > indent", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + |^^^^^^^^^^^^^^ 0 + jumps over the lazy + dog. + `); + + // Perform all operations. + await executeCommand("dance.edit.indent"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-indent.md:77:1", 6, String.raw` + The quick brown fox + |^^^^^^^^^^^^^^ 0 + jumps over the lazy + dog. + `); + }); + + test("5 > indent", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^^^^^^^^^^^^^^^^^^ 0 + jumps over the lazy + dog. + `); + + // Perform all operations. + await executeCommand("dance.edit.indent"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-indent.md:98:1", 6, String.raw` + The quick brown fox + ^^^^^^^^^^^^^^^^^^^^ 0 + jumps over the lazy + dog. + `); + }); + + test("6 > indent", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the lazy + dog. + `); + + // Perform all operations. + await executeCommand("dance.edit.indent"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-indent.md:120:1", 6, String.raw` + The quick brown fox + | 0 + jumps over the lazy + dog. + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/edit-join.md b/test/suite/commands/edit-join.md new file mode 100644 index 0000000..e5baf2b --- /dev/null +++ b/test/suite/commands/edit-join.md @@ -0,0 +1,71 @@ +# 1 + +``` +a b +^^^ 0 +c d +^^^ 0 +e f +^^^ 0 +g h +``` + +## 1 join +[up](#1) + +- .edit.join + +``` +a b c d e f +^^^^^^^^^^^ 0 +g h +``` + +## 1 join-select +[up](#1) + +- .edit.join.select + +``` +a b c d e f + ^ 0 ^ 1 +g h +``` + +# 2 + +``` +a b + ^ 0 +c d +e f + ^ 1 +g h +i j +``` + +## 2 join +[up](#2) + +- .edit.join + +``` +a b c d + ^ 0 +e f g h + ^ 1 +i j +``` + +## 2 join-select +[up](#2) + +- .edit.join.select + +``` +a b c d + ^ 0 +e f g h + ^ 1 +i j +``` diff --git a/test/suite/commands/edit-join.test.ts b/test/suite/commands/edit-join.test.ts new file mode 100644 index 0000000..4477204 --- /dev/null +++ b/test/suite/commands/edit-join.test.ts @@ -0,0 +1,118 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/edit-join.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > join", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b + ^^^ 0 + c d + ^^^ 0 + e f + ^^^ 0 + g h + `); + + // Perform all operations. + await executeCommand("dance.edit.join"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-join.md:13:1", 6, String.raw` + a b c d e f + ^^^^^^^^^^^ 0 + g h + `); + }); + + test("1 > join-select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b + ^^^ 0 + c d + ^^^ 0 + e f + ^^^ 0 + g h + `); + + // Perform all operations. + await executeCommand("dance.edit.join.select"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-join.md:24:1", 6, String.raw` + a b c d e f + ^ 0 ^ 1 + g h + `); + }); + + test("2 > join", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b + ^ 0 + c d + e f + ^ 1 + g h + i j + `); + + // Perform all operations. + await executeCommand("dance.edit.join"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-join.md:47:1", 6, String.raw` + a b c d + ^ 0 + e f g h + ^ 1 + i j + `); + }); + + test("2 > join-select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b + ^ 0 + c d + e f + ^ 1 + g h + i j + `); + + // Perform all operations. + await executeCommand("dance.edit.join.select"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-join.md:60:1", 6, String.raw` + a b c d + ^ 0 + e f g h + ^ 1 + i j + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/edit-paste-before.md b/test/suite/commands/edit-paste-before.md new file mode 100644 index 0000000..4ebd0e5 --- /dev/null +++ b/test/suite/commands/edit-paste-before.md @@ -0,0 +1,27 @@ +# 1 + +``` +hello world + ^^^^ 0 +``` + +## 1 paste +[up](#1) + +- .selections.saveText +- .edit.paste.before + +``` +helloello world + ^^^^ 0 +``` + +## 1 paste-select +[up](#1) + +- .edit.paste.before.select + +``` +helloello world + ^^^^ 0 +``` diff --git a/test/suite/commands/edit-paste-before.test.ts b/test/suite/commands/edit-paste-before.test.ts new file mode 100644 index 0000000..7f735ff --- /dev/null +++ b/test/suite/commands/edit-paste-before.test.ts @@ -0,0 +1,57 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/edit-paste-before.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > paste", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello world + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.selections.saveText"); + await executeCommand("dance.edit.paste.before"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-paste-before.md:8:1", 6, String.raw` + helloello world + ^^^^ 0 + `); + }); + + test("1 > paste-select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello world + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.edit.paste.before.select"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-paste-before.md:19:1", 6, String.raw` + helloello world + ^^^^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/edit-paste.md b/test/suite/commands/edit-paste.md new file mode 100644 index 0000000..036cd0c --- /dev/null +++ b/test/suite/commands/edit-paste.md @@ -0,0 +1,60 @@ +# 1 + +``` +foo +^^^^ 0 +bar +``` + +## 1 paste +[up](#1) + +- .selections.saveText +- .edit.paste.after + +``` +foo +^^^^ 0 +foo +bar +``` + +### 1 paste x +[up](#1-paste) + +- .edit.paste.after + +``` +foo +^^^^ 0 +foo +foo +bar +``` + +## 1 move-then-paste +[up](#1) + +- .select.left.jump +- .edit.paste.after + +``` +foo + | 0 +foo +bar +``` + +### 1 move-then-paste move-2-then-paste +[up](#1-move-then-paste) + +- .select.left.extend { count: 2 } +- .selections.saveText +- .edit.paste.after + +``` +foooo + ^^ 0 +foo +bar +``` diff --git a/test/suite/commands/edit-paste.test.ts b/test/suite/commands/edit-paste.test.ts new file mode 100644 index 0000000..6b2cb2d --- /dev/null +++ b/test/suite/commands/edit-paste.test.ts @@ -0,0 +1,109 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/edit-paste.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > paste", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^^^^ 0 + bar + `); + + // Perform all operations. + await executeCommand("dance.selections.saveText"); + await executeCommand("dance.edit.paste.after"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-paste.md:9:1", 6, String.raw` + foo + ^^^^ 0 + foo + bar + `); + }); + + test("1 > paste > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^^^^ 0 + foo + bar + `); + + // Perform all operations. + await executeCommand("dance.edit.paste.after"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-paste.md:22:1", 6, String.raw` + foo + ^^^^ 0 + foo + foo + bar + `); + }); + + test("1 > move-then-paste", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^^^^ 0 + bar + `); + + // Perform all operations. + await executeCommand("dance.select.left.jump"); + await executeCommand("dance.edit.paste.after"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-paste.md:35:1", 6, String.raw` + foo + | 0 + foo + bar + `); + }); + + test("1 > move-then-paste > move-2-then-paste", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + | 0 + foo + bar + `); + + // Perform all operations. + await executeCommand("dance.select.left.extend", { count: 2 }); + await executeCommand("dance.selections.saveText"); + await executeCommand("dance.edit.paste.after"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/edit-paste.md:48:1", 6, String.raw` + foooo + ^^ 0 + foo + bar + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/indent b/test/suite/commands/indent deleted file mode 100644 index be7fac8..0000000 --- a/test/suite/commands/indent +++ /dev/null @@ -1,56 +0,0 @@ -// Note that only two full lines are considered to be selected -- the last line -// ("dog") is not. -{0}The quick brown fox -jumps over the lazy -|{0}dog. - -//== 0 > 0.indent -//= dance.indent - {0}The quick brown fox - jumps over the lazy -|{0}dog. - -//== 1 -{0}The quick brown fox -jumps over the lazy|{0} -dog. - -//== 1 > 1.indent -//= dance.indent - {0}The quick brown fox - jumps over the lazy|{0} -dog. - -//== 2 -{0}The quick brown|{0} fox -jumps over the lazy -dog. - -//== 2 > 2.indent -//= dance.indent - {0}The quick brown|{0} fox -jumps over the lazy -dog. - -//== 3 -|{0}The quick brown{0} fox -jumps over the lazy -dog. - -//== 3 > 3.indent -//= dance.indent - |{0}The quick brown{0} fox -jumps over the lazy -dog. - -//== 4 -// The "jumps" line is not considered to be selected. -|{0}The quick brown fox -{0}jumps over the lazy -dog. - -//== 4 > 4.indent -//= dance.indent - |{0}The quick brown fox -{0}jumps over the lazy -dog. diff --git a/test/suite/commands/indent.caret b/test/suite/commands/indent.caret deleted file mode 100644 index 557adb8..0000000 --- a/test/suite/commands/indent.caret +++ /dev/null @@ -1,68 +0,0 @@ -// Note that only two full lines are considered to be selected -- the last line -// ("dog") is not. -{0}The quick brown fox -jumps over the lazy -|{0}dog. - -//== 0 > 0.indent -//= dance.indent - {0}The quick brown fox - jumps over the lazy -|{0}dog. - -//== 1 -{0}The quick brown fox -jumps over the lazy|{0} -dog. - -//== 1 > 1.indent -//= dance.indent - {0}The quick brown fox - jumps over the lazy|{0} -dog. - -//== 2 -{0}The quick brown|{0} fox -jumps over the lazy -dog. - -//== 2 > 2.indent -//= dance.indent - {0}The quick brown|{0} fox -jumps over the lazy -dog. - -//== 3 -|{0}The quick brown{0} fox -jumps over the lazy -dog. - -//== 3 > 3.indent -//= dance.indent - |{0}The quick brown{0} fox -jumps over the lazy -dog. - -//== 4 -// The "jumps" line is not considered to be selected. -|{0}The quick brown fox -{0}jumps over the lazy -dog. - -//== 4 > 4.indent -//= dance.indent - |{0}The quick brown fox -{0}jumps over the lazy -dog. - -//== 5 -// No character is selected at all, so we just indent the active line. -|{0}The quick brown fox -jumps over the lazy -dog. - -//== 5 > 5.indent -//= dance.indent - |{0}The quick brown fox -jumps over the lazy -dog. diff --git a/test/suite/commands/join b/test/suite/commands/join deleted file mode 100644 index 848368a..0000000 --- a/test/suite/commands/join +++ /dev/null @@ -1,14 +0,0 @@ -{0}a b -c d -e f|{0} -g h - -//== 0 > 1 -//= dance.join -{0}a b c d e f|{0} -g h - -//== 0 > 2 -//= dance.join.select -a b{0} |{0}c d{1} |{1}e f -g h diff --git a/test/suite/commands/join.1 b/test/suite/commands/join.1 deleted file mode 100644 index f7371a1..0000000 --- a/test/suite/commands/join.1 +++ /dev/null @@ -1,17 +0,0 @@ -a {0}b|{0} -c d -e f{1} -|{1}g h -i j - -//== 0 > 1 -//= dance.join -a {0}b|{0} c d -e f{1} |{1}g h -i j - -//== 0 > 2 -//= dance.join.select -a b{0} |{0}c d -e f{1} |{1}g h -i j diff --git a/test/suite/commands/move.lineend b/test/suite/commands/move.lineend deleted file mode 100644 index 3371324..0000000 --- a/test/suite/commands/move.lineend +++ /dev/null @@ -1,66 +0,0 @@ -foo -bar{0} -|{0}baz -quxxx - -//== 0 > 1 -//= dance.left -foo -ba{0}r|{0} -baz -quxxx - -//== 0 > 2 -//= dance.right -foo -bar -{0}b|{0}az -quxxx - -//== 0 > 3 -//= dance.up -// When at line end, moving to a different line will always select the last -// character instead of line end. "Desired column" is set to len+1 (=4) though. -fo{0}o|{0} -bar -baz -quxxx - -//== 0 > 4 -//= dance.down -// Same reason as test case 0 > 3 above, "desired column"=4 so select last char. -foo -bar -ba{0}z|{0} -quxxx - -//== 4 > 5 -//= dance.down -// As explained above, 4th character should be selected because it's on the -// desired column. -foo -bar -baz -qux{0}x|{0}x - -//== blank.0 -foo - -bar{0} -|{0} - -//== blank.0 > blank.1 -//= dance.up -// The second line is blank, so select its line break. -// This is the only case where up/down will select line breaks. -foo -{0} -|{0}bar -{EOL} - -//== blank.1 > blank.2 -//= dance.up -fo{0}o|{0} - -bar -{EOL} diff --git a/test/suite/commands/objects b/test/suite/commands/objects deleted file mode 100644 index 7f01c7b..0000000 --- a/test/suite/commands/objects +++ /dev/null @@ -1,132 +0,0 @@ -if {0}(|{0}ok) { - f|{1}oo ={1} a+(b{2}+(|{2}c+(d)+e)+f)+g; -} else { - {3}for (var i = (foo + bar)|{3}; i < 1000; i++) { - getAction(i{4})|{4}(); - } -} - -//== 0 > 0.toEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "selectToEnd"}]} -// Old selection 1 is removed because it is not in a parens block. -// Old selection 4 is removed because it is not in a parens block (the ')' it is -// on does not count, and the next '(' starts a NEW parens block.) -if {0}(ok)|{0} { - foo = a+(b+{1}(c+(d)+e)|{1}+f)+g; -} else { - for (var i = (foo + bar{2}); i < 1000; i++)|{2} { - getAction(i)(); - } -} - -//== 0 > 0.toEnd.extend -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "selectToEnd", "extend": true}]} -// Old selection 1 is removed because it is not in a parens block. -// Old selection 4 is removed because it is not in a parens block (the ')' it is -// on does not count, and the next '(' starts a NEW parens block.) -if {0}(ok)|{0} { - foo = a+(b{1}+(c+(d)+e)|{1}+f)+g; -} else { - {2}for (var i = (foo + bar); i < 1000; i++)|{2} { - getAction(i)(); - } -} - -//== 0 > 0.toEnd.inner -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "selectToEnd", "inner": true}]} -// Old selection 1 is removed because it is not in a parens block. -// Old selection 4 is removed because it is not in a parens block (the ')' it is -// on does not count, and the next '(' starts a NEW parens block.) -if {0}(ok|{0}) { - foo = a+(b+{1}(c+(d)+e|{1})+f)+g; -} else { - for (var i = (foo + bar{2}); i < 1000; i++|{2}) { - getAction(i)(); - } -} - -//== 0 > 0.toEnd.inner.extend -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "selectToEnd", "inner": true, "extend": true}]} -// Old selection 1 is removed because it is not in a parens block. -// Old selection 4 is removed because it is not in a parens block (the ')' it is -// on does not count, and the next '(' starts a NEW parens block.) -if {0}(ok|{0}) { - foo = a+(b{1}+(c+(d)+e|{1})+f)+g; -} else { - {2}for (var i = (foo + bar); i < 1000; i++|{2}) { - getAction(i)(); - } -} - -//== 0 > 0.toStart -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "selectToStart"}]} -// Old selection 0 is removed because it is not in a parens block. (the '(' it -// is on does not count. -// Note: Old selection 1 is removed because it is not in a parens block. -if (ok) { - foo = a+|{0}(b+({0}c+(d)+e)+f)+g; -} else { - for (var i = |{1}(foo + bar){1}; i < 1000; i++) { - getAction|{2}(i){2}(); - } -} - -//== 0 > 0.toStart.extend -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "selectToStart", "extend": true}]} -// Old selection 0 is removed because it is not in a parens block. (the '(' it -// is on does not count. -// Note: Old selection 1 is removed because it is not in a parens block. -if (ok) { - foo = a+|{0}(b+{0}(c+(d)+e)+f)+g; -} else { - {1}for (var i = (|{1}foo + bar); i < 1000; i++) { - getAction|{2}(i){2}(); - } -} - -//== 0 > 0.toStart.inner -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "selectToStart", "inner": true}]} -// Old selection 0 is removed because it is not in a parens block. (the '(' it -// is on does not count. -// Note: Old selection 1 is removed because it is not in a parens block. -if (ok) { - foo = a+(|{0}b+({0}c+(d)+e)+f)+g; -} else { - for (var i = (|{1}foo + bar){1}; i < 1000; i++) { - getAction(|{2}i){2}(); - } -} - -//== 0 > 0.toStart.inner.extend -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "selectToStart", "inner": true, "extend": true}]} -// Old selection 0 is removed because it is not in a parens block. (the '(' it -// is on does not count. -// Note: Old selection 1 is removed because it is not in a parens block. -if (ok) { - foo = a+(|{0}b+{0}(c+(d)+e)+f)+g; -} else { - {1}for (var i = (f|{1}oo + bar); i < 1000; i++) { - getAction(|{2}i){2}(); - } -} - -//== 0 > 0.select -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "select"}]} -// Note: Old selection 1 is removed because it is not in a parens block. -if {0}(ok)|{0} { - foo = a+(b+{1}(c+(d)+e)|{1}+f)+g; -} else { - for (var i = {2}(foo + bar)|{2}; i < 1000; i++) { - getAction{3}(i)|{3}(); - } -} - -//== 0 > 0.select.inner -//= {"command": "dance.objects.performSelection", "args": [{"object": "parens", "action": "select", "inner": true}]} -if ({0}ok|{0}) { - foo = a+(b+({1}c+(d)+e|{1})+f)+g; -} else { - for (var i = ({2}foo + bar|{2}); i < 1000; i++) { - getAction({3}i|{3})(); - } -} diff --git a/test/suite/commands/objects.paragraph b/test/suite/commands/objects.paragraph deleted file mode 100644 index 8c1f45d..0000000 --- a/test/suite/commands/objects.paragraph +++ /dev/null @@ -1,99 +0,0 @@ -{0}f|{0}oo{1} -|{1}bar{2} -|{2}{3} -|{3}{4}b|{4}az - -{5} -|{5} - -qux - -//== 0 > 0.toStart -//= {"command": "dance.objects.performSelection", "args": [{"object": "paragraph", "action": "selectToStart"}]} -// Selection 4 skipped to first sentence start because it was active at the very -// beginning of next sentence. Notice how anchor is moved. -// Similarly, Selection 5 reanchored to one line above and then selected last. -{0}|{1}|{2}|{3}|{4}f|{0}oo -{1}bar -{2} -{3}{4}|{5}baz - -{5} - - -qux - -//== 0 > 0.toEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "paragraph", "action": "selectToEnd"}]} -// Note: Paragraph outer end includes all trailing line breaks. -// If a selection is on an empty line, it is always reanchored to the next line. -{0}foo{1} -bar{2} - -|{0}|{1}|{2}{3}{4}baz - - -{5} - -|{3}|{4}|{5}qux - -//== 0 > 0.toInnerEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "paragraph", "action": "selectToEnd", "inner": true}]} -// Paragraph inner end does not include blank lines (but includes the last line -// break before blank lines). Special cases are same as above. -{0}foo{1} -bar{2} -|{0}|{1}|{2} -{3}{4}baz -|{3}|{4} - -{5} -|{5} -qux - -//== 0 > 0.select -//= {"command": "dance.objects.performSelection", "args": [{"object": "paragraph", "action": "select"}]} -// Do not skip over the current character when finding paragraph start. -{0}{1}{2}foo -bar - -|{0}|{1}|{2}{3}{4}{5}baz - - - - -|{3}|{4}|{5}qux - -//== 1 -// Special cases regarding blank lines and next paragraph. -paragraph 1{0} -|{0}{1} -|{1}{2} -|{2}{3} -|{3}{4} -|{4}paragraph 2 - -//== 1 > 1.select -//= {"command": "dance.objects.performSelection", "args": [{"object": "paragraph", "action": "select", "inner": true}]} -// The only special case for select: when active line is blank and the next line -// is not, select the NEXT paragraph instead. This applied to Selection 4. -// Note that it only looks one line ahead, so Selection 0-3 were not affected. -{0}{1}{2}{3}paragraph 1 -|{0}|{1}|{2}|{3}|{3} - - - -{4}paragraph 2|{4} - -//== 1 > 1.toInnerEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "paragraph", "action": "selectToEnd", "inner": true}]} -// In Kakoune, if a selection is on an empty line (L), it always reanchor to the -// start of the next line (L+1). Then if L+1 is non-empty or L+2 is non-empty, -// it selects to the end of the paragraph. (Applied to Selection 3 and 4 here.) -// Selection 1-3 were only reanchored. Selection 0 was at the end of 1st line. -paragraph 1{0} -|{0} -{1} -|{1}{2} -|{2}{3} -{4}paragraph 2|{4}|{3} diff --git a/test/suite/commands/objects.sentence b/test/suite/commands/objects.sentence deleted file mode 100644 index 53dabea..0000000 --- a/test/suite/commands/objects.sentence +++ /dev/null @@ -1,214 +0,0 @@ -{0}A|{0} sentence starts with a non-blank character or a line break. <== It ends with a -punctuation mark like the previous {1}o|{1}ne, or two consecutive line breaks like this - -|{2}An outer sentence{2} also contains the trailing blank characters (but never line -breaks) like this. {3} |{3} <== The white spaces before this sentence belongs to -the outer previou{4}s|{4} sentence. - <- |{5}White spaces here and {5}the line break before them belongs to this sentence, -not the previous one, since the previous trailing cannot contain line breaks. - -//== 0 > 0.toEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToEnd"}]} -{0}A sentence starts with a non-blank character or a line break. |{0}<== It ends with a -punctuation mark like the previous {1}one, or two consecutive line breaks like this -|{1} -{2}An outer sentence also contains the trailing blank characters (but never line -breaks) like this. {3} |{2}<== The white spaces before this sentence belongs to -the outer previou{4}s sentence.|{3}|{4} - <- {5}White spaces here and the line break before them belongs to this sentence, -not the previous one, since the previous trailing cannot contain line breaks.|{5} - -//== 0 > 0.toStart -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToStart"}]} -{0}A|{0} sentence starts with a non-blank character or a line break. |{1}<== It ends with a -punctuation mark like the previous o{1}ne, or two consecutive line breaks like this - -{2}|{3}A|{2}n outer sentence also contains the trailing blank characters (but never line -breaks) like this.{3} |{4}<== The white spaces before this sentence belongs to -the outer previous{4} sentence.|{5} - <- W{5}hite spaces here and the line break before them belongs to this sentence, -not the previous one, since the previous trailing cannot contain line breaks. - -//== 0 > 0.selectInner -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "select", "inner": true}]} -{0}A sentence starts with a non-blank character or a line break.|{0} {1}<== It ends with a -punctuation mark like the previous one, or two consecutive line breaks like this -|{1} -{2}{3}An outer sentence also contains the trailing blank characters (but never line -breaks) like this.|{2}|{3} {4}<== The white spaces before this sentence belongs to -the outer previous sentence.|{4}{5} - <- White spaces here and the line break before them belongs to this sentence, -not the previous one, since the previous trailing cannot contain line breaks.|{5} - -// And here comes some edge cases: - -//== 1 - {0} |{0} {1}I|{1}'m a sen|{2}tenc{2}e . I'm another sentence. -// ++++++++++main part++++++++++trailing -// In this case since the leading blank chars are at document start, they do not -// belong to any sentence. First sentence starts at "I". - -//== 1 > 1.toStart -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToStart"}]} - {0} {1}|{2}I|{0}|{1}'m a sent{2}ence . I'm another sentence. - -//== 1 > 1.toEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToEnd"}]} - {0} {1}I'm a sen{2}tence . |{0}|{1}|{2}I'm another sentence. - -//== 1 > 1.select -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "select"}]} - {0}{1}{2}I'm a sentence . |{0}|{1}|{2}I'm another sentence. - -//== 2 -I'm a previous sent|{3}ence{3}. {4} |{4} {5} -|{5} {0} |{0} {1}I|{1}'m a sen|{2}tenc{2}e . I'm another sentence. -//<-----leading---- +++++main part+++++++++++++++trailing -// In this case, the leading blank chars and the line break before it belongs to -// current sentence (outer & inner) because the previous sentence's inner end is -// the previous period and it's outer end can only cover trailing blank chars -// but not the line break (or anything after the line break). - -//== 2 > 2.select -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "select", "inner": true}]} -// This one is actually pretty easy -- it only depends on which sentence each -// selection was active on. Just remember that the previous sentence ends -// BEFORE the line break and the current sentence starts AT the line break. -{3}{4}I'm a previous sentence.|{3}|{4} {0}{1}{2}{5} - I'm a sentence .|{0}|{1}|{2}|{5} I'm another sentence. - -//== 2 > 2.toStart -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToStart"}]} -|{0}|{1}|{3}|{4}|{5}I'm a previous sente{3}nce.{0}{1}{4}{5} |{2} - I'm a sent{2}ence . I'm another sentence. -// Note that Selection 0, 1, 5 are sent to the PREVIOUS sentence since they are -// active at the leading blank chars or first nonblank char. As a special case, -// their anchors were set to the INNER end of the previous sentence (instead of -// old active). Similarly, Selection 4 is also re-anchored because old active -// was on trailing blank chars. - -//== 2 > 2.toStartInner -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToStart", "inner": true}]} -// This is exactly the same as above, because leading blank chars are also part -// of the inner sentence. -|{0}|{1}|{3}|{4}|{5}I'm a previous sente{3}nce.{0}{1}{4}{5} |{2} - I'm a sent{2}ence . I'm another sentence. - -//== 2 > 2.toEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToEnd"}]} -I'm a previous sent{3}ence. {4} |{3}{5} - {0} {1}I'm a sen{2}tence . |{0}|{1}|{2}|{4}|{5}I'm another sentence. -// Selection 5 was active on the line break, which is also part of the following -// sentence. Selection 4 was on trailing of previous sentence so it seeks to -// the current sentence. Worth noting that toEnd does not have special -// treatment for anchor so Selection 4 does not get re-anchored to the current -// sentence start. Similarly, Selection 0 still anchors to the leading blank. - -//== 3 -//<-------Sentence A-----++++++++++++++Sentence B++++++++++++++ -I'm a s{0}ente|{0}nce{1}.|{1}{2}I|{2}'m anoth{3}e|{3}r sentence{4} -|{4} - -//== 3 > 3.select -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "select", "inner": true}]} -{0}{1}I'm a sentence.|{0}|{1}{2}{3}{4}I'm another sentence -|{2}|{3}|{4} -// The last line break is the terminating character of Sentence B and is also -// considered to be inner sentence. There is no trailing for either sentence. - -//== 3 > 3.toStart -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToStart"}]} -|{0}|{1}|{2}I'm a sente{0}nce.{1}{2}|{3}|{4}I'm anothe{3}r sentence -{4} -// Selection 2 seeks to the previous sentence because it was active at the first -// character of Sentence B and it's anchor was set to the end of last sentence -// instead of old active. That's right: toStart has tons of special cases. - -//== 3 > 3.toEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToEnd"}]} -I'm a sent{0}ence{1}.|{0}|{1}{2}I'm anoth{3}er sentence{4} -|{2}|{3}|{4} -// toEnd has way less edge cases and it will NOT seek to the next sentence on -// from the current sentence inner end, so Selection 1 did not move. -// (Trailing whitespace is a different story though, covered by cases above.) - -//== 4 -I'm a sentence ter|{0}minate{0}d by two line breaks{1} -|{1}{2} -|{2} {3} |{3} I'm anoth|{4}er sen{4}tence -// The first sentence includes the first line break as both inner and outer. -// There is no "trailing" whitespace here (i.e. inner end === outer end). -// The empty line and the blank characters on the next line does not belong to -// either sentence. - -//== 4 > 4.select -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "select"}]} -{0}{1}I'm a sentence terminated by two line breaks -|{0}|{1} - {2}{3}{4}I'm another sentence|{2}|{3}|{4} - -//== 4 > 4.toStart -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToStart"}]} -|{0}|{1}I'm a sentence term{0}inated by two line breaks -{1}{2} - {3} |{4}I|{2}|{3}'m anothe{4}r sentence -// More special cases: Selection 2 was on an empty line so it does not belong -// to any sentence, and it actually scanned to the NEXT sentence start. - -//== 4 > 4.toEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToEnd"}]} -I'm a sentence ter{0}minated by two line breaks{1} -|{0}|{1}{2} - {3} I'm anoth{4}er sentence|{2}|{3}|{4} -// Selection 1 was exactly at the end of the first sentence and did not move. -// Selection 2 was on the empty line and scanned to the next sentence end. -// Again, no special treatment for anchors of Selection 2 and 3. - -//== 5 -// These test cases before document the Kakoune behavior in some minor corner -// cases regarding trailing blank lines. Note that these may or may not make -// sense in VSCode where the last line does NOT have a line break attached. -I'm a sentence ter|{0}minate{0}d by two line breaks plus one more{1} -|{1}{2} -|{2}{3} -|{3} - -//== 5 > 5.toStart -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToStart"}]} -|{0}|{1}I'm a sentence term{0}inated by two line breaks plus one more -{1}{2} -{3} -|{2}|{3} - -//== 5 > 5.toEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToEnd"}]} -I'm a sentence ter{0}minated by two line breaks plus one more{1} -|{0}|{1}{2} -|{2}{3} -|{3} - -//== 5 > 5.select -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "select"}]} -{0}{1}I'm a sentence terminated by two line breaks plus one more -|{0}|{1}{2} -{3} -|{2}|{3} - -//== 6 -I'm a sentence at end of document -{0}|{0} - -//== 6 > 6.toStart -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToStart"}]} -|{0}I'm a sentence at end of document -{0} - -//== 6 > 6.toEnd -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "selectToEnd"}]} -I'm a sentence at end of document{0} -|{0} - -//== 6 > 6.select -//= {"command": "dance.objects.performSelection", "args": [{"object": "sentence", "action": "select"}]} -|{0}I'm a sentence at end of document -{0} diff --git a/test/suite/commands/objects.sentence.caret b/test/suite/commands/objects.sentence.caret deleted file mode 100644 index 1869276..0000000 --- a/test/suite/commands/objects.sentence.caret +++ /dev/null @@ -1,5 +0,0 @@ -// TODO: Write tests. Ideas below: -// If caret is between two sentences, we should arbitrarily lean to: -// 1. Direction of current selection (if not empty). -// 2. Direction of movement (if to inner/out start/end) -// 3. The NEXT one by default diff --git a/test/suite/commands/objects.word.caret b/test/suite/commands/objects.word.caret deleted file mode 100644 index 43903bd..0000000 --- a/test/suite/commands/objects.word.caret +++ /dev/null @@ -1,9 +0,0 @@ -he|{0}l{0}lo world - -//== 0 > 1 -//= {"command": "dance.objects.performSelection", "args": [{"object": "word", "action": "select", "inner": true}]} -{0}hello|{0} world - -//== 1 > 2 -//= {"command": "dance.objects.performSelection", "args": [{"object": "word", "action": "select", "inner": true}]} -{0}hello|{0} world diff --git a/test/suite/commands/paste b/test/suite/commands/paste deleted file mode 100644 index 70cb009..0000000 --- a/test/suite/commands/paste +++ /dev/null @@ -1,23 +0,0 @@ -{0}foo -|{0}bar - -//== 0 > 1 -//= dance.yank -//= dance.paste.after -{0}foo -|{0}foo -bar - -//== 1 > 2 -//= dance.paste.after -{0}foo -|{0}foo -foo -bar - -//== 0 > 3 -//= dance.left -//= dance.paste.after -fo{0}o|{0} -foo -bar diff --git a/test/suite/commands/search b/test/suite/commands/search deleted file mode 100644 index 9b1a44f..0000000 --- a/test/suite/commands/search +++ /dev/null @@ -1,179 +0,0 @@ -Th{0}e q|{0}uick brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search -//= {"command": "dance.search", "args": [{"input": "brown"}]} -The quick {0}brown|{0} fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.repeat -//= dance.count.2 -//= {"command": "dance.search", "args": [{"input": "o"}]} -The quick brown f{0}o|{0}x -jumps over the -lazy dog quickly. - -//== 0 > 0.search.start -//= {"command": "dance.search", "args": [{"input": "quick"}]} -// Search starts AFTER the selection so the first "quick" is not matched. -The quick brown fox -jumps over the -lazy dog {0}quick|{0}ly. - -//== 0 > 0.search.start.wrap -//= {"command": "dance.search", "args": [{"input": "quick "}]} -// Search starts AFTER the selection, but wraps over to find "quick ". -The {0}quick |{0}brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.wrap -//= {"command": "dance.search", "args": [{"input": "Th"}]} -{0}Th|{0}e quick brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.notfound -//= {"command": "dance.search", "args": [{"input": "pig"}]} -// No matches found. Selection left untouched because otherwise there will be -// no selection left. -Th{0}e q|{0}uick brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.backwards -//= {"command": "dance.search.backwards", "args": [{"input": "Th"}]} -// Note: Selection always face forward when not extending. -{0}Th|{0}e quick brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.backwards.start.wrap -//= {"command": "dance.search.backwards", "args": [{"input": "he"}]} -// Search starts BEFORE the selection and wraps around to find the last "he". -The quick brown fox -jumps over t{0}he|{0} -lazy dog quickly. - -//== 0 > 0.search.backwards.start.wrap2 -//= {"command": "dance.search.backwards", "args": [{"input": "he q"}]} -// Search starts BEFORE the selection "q" but wraps around to find "he q". -T{0}he q|{0}uick brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.backwards.notfound -//= {"command": "dance.search.backwards", "args": [{"input": "pig"}]} -// No matches found. Selection left untouched because otherwise there will be -// no selection left. -Th{0}e q|{0}uick brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.extend -//= {"command": "dance.search.extend", "args": [{"input": "quick"}]} -Th{0}e quick brown fox -jumps over the -lazy dog quick|{0}ly. - -//== 0 > 0.search.extend.wrap -//= {"command": "dance.search.extend", "args": [{"input": "T"}]} -// When extending, a selection is deleted if it would require wrapping to find -// the next match. In this case, the only main selection is left untouched -// because otherwise there will be no selection left. -Th{0}e q|{0}uick brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.backwards.extend -//= {"command": "dance.search.backwards.extend", "args": [{"input": "T"}]} -// Note: 'e' is included in character-selections because it is the anchor. -// When extending, the resulting selection may face backwards. -|{0}The{0} quick brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.backwards.extend2 -//= {"command": "dance.search.backwards.extend", "args": [{"input": "Th"}]} -// Note: 'e' is included in character-selections because it is the anchor. -// When extending, the resulting selection may face backwards. -|{0}The{0} quick brown fox -jumps over the -lazy dog quickly. - -//== 0 > 0.search.backwards.extend.wrap -//= {"command": "dance.search.backwards.extend", "args": [{"input": "lazy"}]} -// When extending, a selection is deleted if it would require wrapping to find -// the next match. In this case, the only main selection is left untouched -// because otherwise there will be no selection left. -Th{0}e q|{0}uick brown fox -jumps over the -lazy dog quickly. - -//== 1 -// Remember that search will start at the start / end of the selection, so -// anything that is (partially) covered by the current selection cannot be -// found without wrapping first. The following cases show some consequences. -The quick b|{0}rown fox -jumps over the -laz{0}y dog quickly. - -//== 1 > 1.search -//= {"command": "dance.search", "args": [{"input": "o"}]} -// Forward search starts at "y" and finds "d[o]g" instead of "br[o]wn". -The quick brown fox -jumps over the -lazy d{0}o|{0}g quickly. - -//== 1 > 1.search.extend -//= {"command": "dance.search.extend", "args": [{"input": "o"}]} -// Same, but extends instead of replaces. -The quick brown fox -jumps over the -la{0}zy do|{0}g quickly. - -//== 1 > 1.search.wrap -//= {"command": "dance.search", "args": [{"input": "he"}]} -// Forward search starts at "y" and wraps to "T[he]" instead of "t[he]". -T|{0}he{0} quick brown fox -jumps over the -lazy dog quickly. - -//== 1 > 1.search.extend.wrap -//= {"command": "dance.search.extend", "args": [{"input": "he"}]} -// When extending, it should not wrap around document edges to find "T[he]". -// "t[he]" is not considered at all. No-op due to no selections remaining. -The quick b|{0}rown fox -jumps over the -laz{0}y dog quickly. - -//== 1 > 1.search.backwards -//= {"command": "dance.search.backwards", "args": [{"input": "u"}]} -// Backward search starts at "b" and finds "q[u]ick" instead of "j[u]mps". -The q{0}u|{0}ick brown fox -jumps over the -lazy dog quickly. - -//== 1 > 1.search.backwards.extend -//= {"command": "dance.search.backwards.extend", "args": [{"input": "u"}]} -// Same, but extends instead of replaces. -The q|{0}uick brown fox -jumps over the -laz{0}y dog quickly. - -//== 1 > 1.search.backwards.wrap -//= {"command": "dance.search.backwards", "args": [{"input": "o"}]} -// Backward search starts at "b" and wraps to "d[o]g" instead of "br[o]wn". -The quick brown fox -jumps over the -lazy d{0}o|{0}g quickly. - -//== 1 > 1.search.backwards.extend.wrap -//= {"command": "dance.search.backwards.extend", "args": [{"input": "o"}]} -// When extending, it should not wrap around document edges to find "d[o]g". -// "br[o]wn" is not considered at all. No-op due to no selections remaining. -The quick b|{0}rown fox -jumps over the -laz{0}y dog quickly. diff --git a/test/suite/commands/search-next.md b/test/suite/commands/search-next.md new file mode 100644 index 0000000..9a2a8e7 --- /dev/null +++ b/test/suite/commands/search-next.md @@ -0,0 +1,159 @@ +# 1 + +``` +apple pineapple pear +^ 0 +pear pineapple apple +kiwi orange kiwi +``` + +## 1 search-apple +[up](#1) + +- .search { input: "apple" } + +``` +apple pineapple pear + ^^^^^ 0 +pear pineapple apple +kiwi orange kiwi +``` + +### 1 search-apple next +[up](#1-search-apple) + +- .search.next + +``` +apple pineapple pear +pear pineapple apple + ^^^^^ 0 +kiwi orange kiwi +``` + +### 1 search-apple next-add +[up](#1-search-apple) + +- .search.next.add + +``` +apple pineapple pear + ^^^^^ 1 +pear pineapple apple + ^^^^^ 0 +kiwi orange kiwi +``` + +### 1 search-apple next-3 +[up](#1-search-apple) + +- .search.next { count: 3 } + +Main selection search will wrap around: + +``` +apple pineapple pear +^^^^^ 0 +pear pineapple apple +kiwi orange kiwi +``` + +### 1 search-apple next-add-3 +[up](#1-search-apple) + +- .search.next.add { count: 3 } + +Main selection search will wrap around: + +``` +apple pineapple pear +^^^^^ 0 ^^^^^ 3 +pear pineapple apple + ^^^^^ 2 + ^^^^^ 1 +kiwi orange kiwi +``` + +### 1 search-apple next-4 +[up](#1-search-apple) + +- .search.next { count: 4 } + +Main selection search will wrap around and hit the second "apple" again: + +``` +apple pineapple pear + ^^^^^ 0 +pear pineapple apple +kiwi orange kiwi +``` + +### 1 search-apple next-add-4 +[up](#1-search-apple) + +- .search.next.add { count: 4 } + +Main selection search will wrap around and hit the second "apple" again, and VS +Code will then merge the selections 0 and 4 automatically: + +``` +apple pineapple pear +^^^^^ 1 ^^^^^ 0 +pear pineapple apple + ^^^^^ 3 + ^^^^^ 2 +kiwi orange kiwi +``` + +### 1 search-apple previous +[up](#1-search-apple) + +- .search.previous + +``` +apple pineapple pear +^^^^^ 0 +pear pineapple apple +kiwi orange kiwi +``` + +### 1 search-apple previous-add +[up](#1-search-apple) + +- .search.previous.add + +``` +apple pineapple pear +^^^^^ 0 ^^^^^ 1 +pear pineapple apple +kiwi orange kiwi +``` + +### 1 search-apple previous-2 +[up](#1-search-apple) + +- .search.previous { count: 2 } + +Main selection search will wrap around: + +``` +apple pineapple pear +pear pineapple apple + ^^^^^ 0 +kiwi orange kiwi +``` + +### 1 search-apple previous-add-2 +[up](#1-search-apple) + +- .search.previous.add { count: 2 } + +Main selection search will wrap around: + +``` +apple pineapple pear +^^^^^ 1 ^^^^^ 2 +pear pineapple apple + ^^^^^ 0 +kiwi orange kiwi +``` diff --git a/test/suite/commands/search-next.test.ts b/test/suite/commands/search-next.test.ts new file mode 100644 index 0000000..c2fd4ea --- /dev/null +++ b/test/suite/commands/search-next.test.ts @@ -0,0 +1,259 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/search-next.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > search-apple", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "apple" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:10:1", 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + }); + + test("1 > search-apple > next", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.next"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:22:1", 6, String.raw` + apple pineapple pear + pear pineapple apple + ^^^^^ 0 + kiwi orange kiwi + `); + }); + + test("1 > search-apple > next-add", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.next.add"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:34:1", 6, String.raw` + apple pineapple pear + ^^^^^ 1 + pear pineapple apple + ^^^^^ 0 + kiwi orange kiwi + `); + }); + + test("1 > search-apple > next-3", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.next", { count: 3 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:47:1", 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + }); + + test("1 > search-apple > next-add-3", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.next.add", { count: 3 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:61:1", 6, String.raw` + apple pineapple pear + ^^^^^ 0 ^^^^^ 3 + pear pineapple apple + ^^^^^ 2 + ^^^^^ 1 + kiwi orange kiwi + `); + }); + + test("1 > search-apple > next-4", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.next", { count: 4 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:77:1", 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + }); + + test("1 > search-apple > next-add-4", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.next.add", { count: 4 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:91:1", 6, String.raw` + apple pineapple pear + ^^^^^ 1 ^^^^^ 0 + pear pineapple apple + ^^^^^ 3 + ^^^^^ 2 + kiwi orange kiwi + `); + }); + + test("1 > search-apple > previous", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.previous"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:108:1", 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + }); + + test("1 > search-apple > previous-add", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.previous.add"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:120:1", 6, String.raw` + apple pineapple pear + ^^^^^ 0 ^^^^^ 1 + pear pineapple apple + kiwi orange kiwi + `); + }); + + test("1 > search-apple > previous-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.previous", { count: 2 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:132:1", 6, String.raw` + apple pineapple pear + pear pineapple apple + ^^^^^ 0 + kiwi orange kiwi + `); + }); + + test("1 > search-apple > previous-add-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + apple pineapple pear + ^^^^^ 0 + pear pineapple apple + kiwi orange kiwi + `); + + // Perform all operations. + await executeCommand("dance.search.previous.add", { count: 2 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search-next.md:146:1", 6, String.raw` + apple pineapple pear + ^^^^^ 1 ^^^^^ 2 + pear pineapple apple + ^^^^^ 0 + kiwi orange kiwi + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/search.md b/test/suite/commands/search.md new file mode 100644 index 0000000..2ac79f2 --- /dev/null +++ b/test/suite/commands/search.md @@ -0,0 +1,412 @@ +# easy + +``` +foo bar + ^ 0 +``` + +## easy search-b +[up](#easy) + +- .search { input: "b" } + +``` +foo bar + ^ 0 +``` + +# 1 + +``` +The quick brown fox + ^^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search +[up](#1) + +- .search { input: "brown" } + +``` +The quick brown fox + ^^^^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-repeat +[up](#1) + +- .search { input: "o", count: 2 } + +``` +The quick brown fox + ^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-start +[up](#1) + +- .search { input: "quick" } + +Search starts **after** the selection so the first "quick" is not matched. + +``` +The quick brown fox +jumps over the +lazy dog quickly. + ^^^^^ 0 +``` + +## 1 search-start-wrap +[up](#1) + +- .search { input: "quick " } + +Search starts **after** the selection, but wraps over to find "quick ". + +``` +The quick brown fox + ^^^^^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-wrap +[up](#1) + +- .search { input: "Th" } + +``` +The quick brown fox +^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-not-found +[up](#1) + +- .search { input: "pig", $expect: /^no selections remain$/ } + +No matches found. Selection is left untouched because otherwise there would be +no selection left. + +``` +The quick brown fox + ^^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-backward +[up](#1) + +- .search { input: "Th", direction: -1 } + +Note: Selection always faces forward (except when extending). + +``` +The quick brown fox +^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-backward-wrap +[up](#1) + +- .search { input: "he", direction: -1 } + +Search starts **before** the selection and wraps around to find the last "he". + +``` +The quick brown fox +jumps over the + ^^ 0 +lazy dog quickly. +``` + +## 1 search-backward-wrap-other +[up](#1) + +- .search { input: "he q", direction: -1 } + +Search starts **before** the selection "q" but wraps around to find "he q". + +``` +The quick brown fox + ^^^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-backward-not-found +[up](#1) + +- .search { input: "pig", direction: -1, $expect: /^no selections remain$/ } + +No matches found. Selection is left untouched because otherwise there would be +no selection left. + +``` +The quick brown fox + ^^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-extend +[up](#1) + +- .search { input: "quick", shift: "extend" } + +``` +The quick brown fox + ^ 0 +jumps over the +lazy dog quickly. + ^ 0 +``` + +## 1 search-extend-wrap +[up](#1) + +- .search { input: "T", shift: "extend", $expect: /^no selections remain$/ } + +When extending, a selection is deleted if it would require wrapping to find the +next match. In this case, the (only) main selection is left untouched because +otherwise there would be no selection left. + +``` +The quick brown fox + ^^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-backward-extend +[up](#1) + +- .search { input: "T", direction: -1, shift: "extend" } + +When extending, the resulting selection may face backward. + +``` +The quick brown fox +|^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-backward-extend-character +[up](#1) + +> behavior <- character + +- .search { input: "T", direction: -1, shift: "extend" } + +Note: "e" is included in character-selections because it is the anchor. + +``` +The quick brown fox +|^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-backward-extend-other +[up](#1) + +- .search { input: "Th", direction: -1, shift: "extend" } + +When extending, the resulting selection may face backward. + +``` +The quick brown fox +|^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-backward-extend-character-other +[up](#1) + +> behavior <- character + +- .search { input: "Th", direction: -1, shift: "extend" } + +Note: "e" is included in character-selections because it is the anchor. + +``` +The quick brown fox +|^^ 0 +jumps over the +lazy dog quickly. +``` + +## 1 search-backward-extend-wrap +[up](#1) + +- .search { input: "lazy", direction: -1, shift: "extend", $expect: /^no selections remain$/ } + +When extending, a selection is deleted if it would require wrapping to find the +next match. In this case, the (only) main selection is left untouched because +otherwise there would be no selection left. + +``` +The quick brown fox + ^^^ 0 +jumps over the +lazy dog quickly. +``` + +# 2 +Remember that the search will start at the start or end of the selection, so +anything that is (partially) covered by the current selection cannot be +found without wrapping first. The following cases show some consequences. + +``` +The quick brown fox + | 0 +jumps over the +lazy dog quickly. + ^ 0 +``` + +## 2 search +[up](#2) + +- .search { input: "o" } + +Forward search starts at "y" and finds "d**o**g" instead of "br**o**wn". + +``` +The quick brown fox +jumps over the +lazy dog quickly. + ^ 0 +``` + +## 2 search-extend +[up](#2) + +- .search { input: "o", shift: "extend" } + +Same, but extends instead of jumping. + +``` +The quick brown fox +jumps over the +lazy dog quickly. + ^^^^ 0 +``` + +## 2 search-extend-character +[up](#2) + +> behavior <- character + +- .search { input: "o", shift: "extend" } + +Same, but extends instead of jumping. + +``` +The quick brown fox +jumps over the +lazy dog quickly. + ^^^^^ 0 +``` + +## 2 search-wrap +[up](#2) + +- .search { input: "he" } + +Forward search starts at "y" and wraps to "T**he**" instead of "t**he**". + +``` +The quick brown fox + |^ 0 +jumps over the +lazy dog quickly. +``` + +## 2 search-extend-wrap +[up](#2) + +- .search { input: "he", shift: "extend", $expect: /^no selections remain$/ } + +When extending, Dance should not wrap around document edges to find "T**he**". +"t**he**" is not considered at all. No-op due to no selections remaining. + +``` +The quick brown fox + | 0 +jumps over the +lazy dog quickly. + ^ 0 +``` + +## 2 search-backward +[up](#2) + +- .search { input: "u", direction: -1 } + +Backward search starts at "b" and finds "q**u**ick" instead of "j**u**mps". + +``` +The quick brown fox + ^ 0 +jumps over the +lazy dog quickly. +``` + +## 2 search-backward-extend +[up](#2) + +- .search { input: "u", direction: -1, shift: "extend" } + +Same, but extends instead of jumping. + +``` +The quick brown fox + | 0 +jumps over the +lazy dog quickly. + ^ 0 +``` + +## 2 search-backward-wrap +[up](#2) + +- .search { input: "o", direction: -1 } + +Backward search starts at "b" and wraps to "d**o**g" instead of "br**o**wn". + +``` +The quick brown fox +jumps over the +lazy dog quickly. + ^ 0 +``` + +## 2 search-backward-extend-wrap +[up](#2) + +- .search { input: "o", direction: -1, shift: "extend", $expect: /^no selections remain$/ } + +When extending, Dance should not wrap around document edges to find "d**o**g". +"br**o**wn" is not considered at all. No-op due to no selections remaining. + +``` +The quick brown fox + | 0 +jumps over the +lazy dog quickly. + ^ 0 +``` diff --git a/test/suite/commands/search.next b/test/suite/commands/search.next deleted file mode 100644 index ade35aa..0000000 --- a/test/suite/commands/search.next +++ /dev/null @@ -1,83 +0,0 @@ -{0}a|{0}pple pineapple pear -pear pineapple apple -kiwi orange kiwi - -//== 0 > 0.search -//= {"command": "dance.search", "args": [{"input": "apple"}]} -apple pine{0}apple|{0} pear -pear pineapple apple -kiwi orange kiwi - -//== 0.search > 0.search.next -//= dance.search.next -apple pineapple pear -pear pine{0}apple|{0} apple -kiwi orange kiwi - -//== 0.search > 0.search.next.add -//= dance.search.next.add -apple pine{1}apple|{1} pear -pear pine{0}apple|{0} apple -kiwi orange kiwi - -//== 0.search > 0.search.3next -//= dance.count.3 -//= dance.search.next -// Main selection search wrapped around buffer. -{0}apple|{0} pineapple pear -pear pineapple apple -kiwi orange kiwi - -//== 0.search > 0.search.3next.add -//= dance.count.3 -//= dance.search.next.add -// Main selection search wrapped around buffer. -{0}apple|{0} pine{3}apple|{3} pear -pear pine{2}apple|{2} {1}apple|{1} -kiwi orange kiwi - -//== 0.search > 0.search.4next -//= dance.count.4 -//= dance.search.next -// Main selection search wrapped around buffer and hits the second "apple" -// again. -apple pine{0}apple|{0} pear -pear pineapple apple -kiwi orange kiwi - -//== 0.search > 0.search.4next.add -//= dance.count.4 -//= dance.search.next.add -// Main selection search wrapped around buffer and hits the second "apple" -// again. VSCode will then merge Selection 0 and 4 automatically. -{1}apple|{1} pine{0}{4}apple|{0}|{4} pear -pear pine{3}apple|{3} {2}apple|{2} -kiwi orange kiwi - -//== 0.search > 0.search.previous -//= dance.search.previous -{0}apple|{0} pineapple pear -pear pineapple apple -kiwi orange kiwi - -//== 0.search > 0.search.previous.add -//= dance.search.previous.add -{0}apple|{0} pine{1}apple|{1} pear -pear pineapple apple -kiwi orange kiwi - -//== 0.search > 0.search.2previous -//= dance.count.2 -//= dance.search.previous -// Main selection search wrapped around buffer. -apple pineapple pear -pear pineapple {0}apple|{0} -kiwi orange kiwi - -//== 0.search > 0.search.2previous.add -//= dance.count.2 -//= dance.search.previous.add -// Main selection search wrapped around buffer. -{1}apple|{1} pine{2}apple|{2} pear -pear pineapple {0}apple|{0} -kiwi orange kiwi diff --git a/test/suite/commands/search.test.ts b/test/suite/commands/search.test.ts new file mode 100644 index 0000000..a86f269 --- /dev/null +++ b/test/suite/commands/search.test.ts @@ -0,0 +1,604 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/search.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("easy > search-b", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo bar + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "b" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:8:1", 6, String.raw` + foo bar + ^ 0 + `); + }); + + test("1 > search", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "brown" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:27:1", 6, String.raw` + The quick brown fox + ^^^^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-repeat", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "o", count: 2 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:39:1", 6, String.raw` + The quick brown fox + ^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "quick" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:51:1", 6, String.raw` + The quick brown fox + jumps over the + lazy dog quickly. + ^^^^^ 0 + `); + }); + + test("1 > search-start-wrap", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "quick " }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:65:1", 6, String.raw` + The quick brown fox + ^^^^^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-wrap", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "Th" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:79:1", 6, String.raw` + The quick brown fox + ^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-not-found", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "pig", $expect: /^no selections remain$/ }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:91:1", 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "Th", direction: -1 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:106:1", 6, String.raw` + The quick brown fox + ^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-backward-wrap", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "he", direction: -1 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:120:1", 6, String.raw` + The quick brown fox + jumps over the + ^^ 0 + lazy dog quickly. + `); + }); + + test("1 > search-backward-wrap-other", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "he q", direction: -1 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:134:1", 6, String.raw` + The quick brown fox + ^^^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-backward-not-found", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "pig", direction: -1, $expect: /^no selections remain$/ }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:148:1", 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "quick", shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:163:1", 6, String.raw` + The quick brown fox + ^ 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + }); + + test("1 > search-extend-wrap", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "T", shift: "extend", $expect: /^no selections remain$/ }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:176:1", 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-backward-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "T", direction: -1, shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:192:1", 6, String.raw` + The quick brown fox + |^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-backward-extend-character", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.search", { input: "T", direction: -1, shift: "extend" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:206:1", 6, String.raw` + The quick brown fox + |^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-backward-extend-other", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "Th", direction: -1, shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:222:1", 6, String.raw` + The quick brown fox + |^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-backward-extend-character-other", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.search", { input: "Th", direction: -1, shift: "extend" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:236:1", 6, String.raw` + The quick brown fox + |^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("1 > search-backward-extend-wrap", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "lazy", direction: -1, shift: "extend", $expect: /^no selections remain$/ }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:252:1", 6, String.raw` + The quick brown fox + ^^^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("2 > search", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "o" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:281:1", 6, String.raw` + The quick brown fox + jumps over the + lazy dog quickly. + ^ 0 + `); + }); + + test("2 > search-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "o", shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:295:1", 6, String.raw` + The quick brown fox + jumps over the + lazy dog quickly. + ^^^^ 0 + `); + }); + + test("2 > search-extend-character", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.search", { input: "o", shift: "extend" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:309:1", 6, String.raw` + The quick brown fox + jumps over the + lazy dog quickly. + ^^^^^ 0 + `); + }); + + test("2 > search-wrap", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "he" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:325:1", 6, String.raw` + The quick brown fox + |^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("2 > search-extend-wrap", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "he", shift: "extend", $expect: /^no selections remain$/ }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:339:1", 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + }); + + test("2 > search-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "u", direction: -1 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:355:1", 6, String.raw` + The quick brown fox + ^ 0 + jumps over the + lazy dog quickly. + `); + }); + + test("2 > search-backward-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "u", direction: -1, shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:369:1", 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + }); + + test("2 > search-backward-wrap", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "o", direction: -1 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:384:1", 6, String.raw` + The quick brown fox + jumps over the + lazy dog quickly. + ^ 0 + `); + }); + + test("2 > search-backward-extend-wrap", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.search", { input: "o", direction: -1, shift: "extend", $expect: /^no selections remain$/ }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/search.md:398:1", 6, String.raw` + The quick brown fox + | 0 + jumps over the + lazy dog quickly. + ^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek-enclosing.md b/test/suite/commands/seek-enclosing.md new file mode 100644 index 0000000..33b4da5 --- /dev/null +++ b/test/suite/commands/seek-enclosing.md @@ -0,0 +1,94 @@ +# 1 + +``` +{ hello: 1, + world: { + foo: [ + [ 1, 2, 3, ], + ^ 0 + ], + bar: (42), + ^^ 1 + }, +} +``` + +## 1 enclosing +[up](#1) + +- .seek.enclosing + +Since the active of selection #0 is not on a brace / bracket character, Dance +will find the next bracket (`]`) and then match from there, moving the active to +the previous matching `[`, selecting the text during the move (i.e. backwards +from `]` to `[`). Same for selection #1. + +``` +{ hello: 1, + world: { + foo: [ + [ 1, 2, 3, ], + |^^^^^^^^^^^ 0 + ], + bar: (42), + |^^^ 1 + }, +} +``` + +### 1 enclosing x +[up](#1-enclosing) + +Since the active position was at the opening square bracket (`[`), `m` again +should keep the same selection but forwards, so the active position is at the +closing bracket (`]`). + +- .seek.enclosing + +``` +{ hello: 1, + world: { + foo: [ + [ 1, 2, 3, ], + ^^^^^^^^^^^^ 0 + ], + bar: (42), + ^^^^ 1 + }, +} +``` + +# 2 + +``` +{ hello: 1, +^ 0 + world: { + foo: [ + [ 1, 2, 3, ], + ], + bar: (42), + }, +} +``` + +## 2 enclosing +[up](#2) + +- .seek.enclosing + +Current active was already on {, so no need to seek. Directly move to } and +select the text along the way. + +``` +{ hello: 1, +^ 0 + world: { + foo: [ + [ 1, 2, 3, ], + ], + bar: (42), + }, +} +^ 0 +``` diff --git a/test/suite/commands/seek-enclosing.test.ts b/test/suite/commands/seek-enclosing.test.ts new file mode 100644 index 0000000..329f61b --- /dev/null +++ b/test/suite/commands/seek-enclosing.test.ts @@ -0,0 +1,120 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek-enclosing.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > enclosing", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + { hello: 1, + world: { + foo: [ + [ 1, 2, 3, ], + ^ 0 + ], + bar: (42), + ^^ 1 + }, + } + `); + + // Perform all operations. + await executeCommand("dance.seek.enclosing"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-enclosing.md:16:1", 6, String.raw` + { hello: 1, + world: { + foo: [ + [ 1, 2, 3, ], + |^^^^^^^^^^^ 0 + ], + bar: (42), + |^^^ 1 + }, + } + `); + }); + + test("1 > enclosing > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + { hello: 1, + world: { + foo: [ + [ 1, 2, 3, ], + |^^^^^^^^^^^ 0 + ], + bar: (42), + |^^^ 1 + }, + } + `); + + // Perform all operations. + await executeCommand("dance.seek.enclosing"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-enclosing.md:39:1", 6, String.raw` + { hello: 1, + world: { + foo: [ + [ 1, 2, 3, ], + ^^^^^^^^^^^^ 0 + ], + bar: (42), + ^^^^ 1 + }, + } + `); + }); + + test("2 > enclosing", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + { hello: 1, + ^ 0 + world: { + foo: [ + [ 1, 2, 3, ], + ], + bar: (42), + }, + } + `); + + // Perform all operations. + await executeCommand("dance.seek.enclosing"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-enclosing.md:75:1", 6, String.raw` + { hello: 1, + ^ 0 + world: { + foo: [ + [ 1, 2, 3, ], + ], + bar: (42), + }, + } + ^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek-object-between.md b/test/suite/commands/seek-object-between.md new file mode 100644 index 0000000..4611666 --- /dev/null +++ b/test/suite/commands/seek-object-between.md @@ -0,0 +1,234 @@ +# 1 + +> behavior <- character +> /\$object/"\\((?#inner)\\)"/g + +``` +if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 +} else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } +} +``` + +## 1 to-end +[up](#1) + +- .seek.object { input: $object, where: "end" } + +Old selection #1 is removed because it is not in a parens block. +Old selection #4 is removed because it is not in a parens block (the `)` it is +on does not count, and the next `(` starts a NEW parens block). + +``` +if (ok) { + ^^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^^ 1 +} else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^ 2 + getAction(i)(); + } +} +``` + +## 1 to-end-extend +[up](#1) + +- .seek.object { input: $object, where: "end", shift: "extend" } + +Old selection #1 is removed because it is not in a parens block. +Old selection #4 is removed because it is not in a parens block (the `)` it is +on does not count, and the next `(` starts a NEW parens block). + +``` +if (ok) { + ^^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^^^ 1 +} else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 + getAction(i)(); + } +} +``` + +## 1 to-end-inner +[up](#1) + +- .seek.object { input: $object, where: "end", inner: true } + +Old selection #1 is removed because it is not in a parens block. +Old selection #4 is removed because it is not in a parens block (the `)` it is +on does not count, and the next `(` starts a NEW parens block). + +``` +if (ok) { + ^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^ 1 +} else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^ 2 + getAction(i)(); + } +} +``` + +## 1 to-end-inner-extend +[up](#1) + +- .seek.object { input: $object, where: "end", inner: true, shift: "extend" } + +Old selection #1 is removed because it is not in a parens block. +Old selection #4 is removed because it is not in a parens block (the `)` it is +on does not count, and the next `(` starts a NEW parens block). + +``` +if (ok) { + ^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^^ 1 +} else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 + getAction(i)(); + } +} +``` + +## 1 to-start +[up](#1) + +- .seek.object { input: $object, where: "start" } + +Old selection #0 is removed because it is not in a parens block (the `(` it is +is on does not count). +Old selection #1 is removed because it is not in a parens block. + +``` +if (ok) { + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 0 +} else { + for (var i = (foo + bar); i < 1000; i++) { + |^^^^^^^^^^ 1 + getAction(i)(); + |^^ 2 + } +} +``` + +## 1 to-start-extend +[up](#1) + +- .seek.object { input: $object, where: "start", shift: "extend" } + +Old selection #0 is removed because it is not in a parens block (the `(` it is +is on does not count). +Old selection #1 is removed because it is not in a parens block. + +``` +if (ok) { + foo = a+(b+(c+(d)+e)+f)+g; + |^^ 0 +} else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^ 1 + getAction(i)(); + |^^ 2 + } +} +``` + +## 1 to-start-inner +[up](#1) + +- .seek.object { input: $object, where: "start", inner: true } + +Old selection #0 is removed because it is not in a parens block (the `(` it is +on does not count). +Old selection #1 is removed because it is not in a parens block. + +``` +if (ok) { + foo = a+(b+(c+(d)+e)+f)+g; + |^^ 0 +} else { + for (var i = (foo + bar); i < 1000; i++) { + |^^^^^^^^^ 1 + getAction(i)(); + |^ 2 + } +} +``` + +## 1 to-start-inner-extend +[up](#1) + +- .seek.object { input: $object, where: "start", inner: true, shift: "extend" } + +Old selection #0 is removed because it is not in a parens block (the `(` it is +on does not count). +Old selection #1 is removed because it is not in a parens block. + +``` +if (ok) { + foo = a+(b+(c+(d)+e)+f)+g; + |^ 0 +} else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^ 1 + getAction(i)(); + |^ 2 + } +} +``` + +## 1 select +[up](#1) + +- .seek.object { input: $object } + +Old selection #1 is removed because it is not in a parens block. + +``` +if (ok) { + ^^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^^ 1 +} else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^ 2 + getAction(i)(); + ^^^ 3 + } +} +``` + +## 1 select-inner +[up](#1) + +- .seek.object { input: $object, inner: true } + +``` +if (ok) { + ^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^ 1 +} else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^ 2 + getAction(i)(); + ^ 3 + } +} +``` diff --git a/test/suite/commands/seek-object-between.test.ts b/test/suite/commands/seek-object-between.test.ts new file mode 100644 index 0000000..0bfbfba --- /dev/null +++ b/test/suite/commands/seek-object-between.test.ts @@ -0,0 +1,384 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek-object-between.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > to-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)", where: "end" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:20:1", 6, String.raw` + if (ok) { + ^^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^^ 1 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^ 2 + getAction(i)(); + } + } + `); + }); + + test("1 > to-end-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)", where: "end", shift: "extend" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:42:1", 6, String.raw` + if (ok) { + ^^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^^^ 1 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 + getAction(i)(); + } + } + `); + }); + + test("1 > to-end-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)", where: "end", inner: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:64:1", 6, String.raw` + if (ok) { + ^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^ 1 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^ 2 + getAction(i)(); + } + } + `); + }); + + test("1 > to-end-inner-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)", where: "end", inner: true, shift: "extend" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:86:1", 6, String.raw` + if (ok) { + ^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^^ 1 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 + getAction(i)(); + } + } + `); + }); + + test("1 > to-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)", where: "start" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:108:1", 6, String.raw` + if (ok) { + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 0 + } else { + for (var i = (foo + bar); i < 1000; i++) { + |^^^^^^^^^^ 1 + getAction(i)(); + |^^ 2 + } + } + `); + }); + + test("1 > to-start-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)", where: "start", shift: "extend" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:130:1", 6, String.raw` + if (ok) { + foo = a+(b+(c+(d)+e)+f)+g; + |^^ 0 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^ 1 + getAction(i)(); + |^^ 2 + } + } + `); + }); + + test("1 > to-start-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)", where: "start", inner: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:152:1", 6, String.raw` + if (ok) { + foo = a+(b+(c+(d)+e)+f)+g; + |^^ 0 + } else { + for (var i = (foo + bar); i < 1000; i++) { + |^^^^^^^^^ 1 + getAction(i)(); + |^ 2 + } + } + `); + }); + + test("1 > to-start-inner-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)", where: "start", inner: true, shift: "extend" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:174:1", 6, String.raw` + if (ok) { + foo = a+(b+(c+(d)+e)+f)+g; + |^ 0 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^ 1 + getAction(i)(); + |^ 2 + } + } + `); + }); + + test("1 > select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:196:1", 6, String.raw` + if (ok) { + ^^^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^^^ 1 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^ 2 + getAction(i)(); + ^^^ 3 + } + } + `); + }); + + test("1 > select-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + if (ok) { + ^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + |^^^ 1 ^^ 2 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^^^^^^^^^^^^^^^^ 3 + getAction(i)(); + ^ 4 + } + } + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "\\((?#inner)\\)", inner: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-between.md:217:1", 6, String.raw` + if (ok) { + ^^ 0 + foo = a+(b+(c+(d)+e)+f)+g; + ^^^^^^^ 1 + } else { + for (var i = (foo + bar); i < 1000; i++) { + ^^^^^^^^^ 2 + getAction(i)(); + ^ 3 + } + } + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek-object-charset.md b/test/suite/commands/seek-object-charset.md new file mode 100644 index 0000000..a70c46f --- /dev/null +++ b/test/suite/commands/seek-object-charset.md @@ -0,0 +1,27 @@ +# 1 + +> /\$object/\/[\p{L}]+(?[^\S\n]+)\/u.source/g + +``` +hello world + ^ 0 +``` + +## 1 select-inner +[up](#1) + +- .seek.object { input: $object, inner: true } +``` +hello world +^^^^^ 0 +``` + +### 1 select-inner x +[up](#1-select-inner) + +- .seek.object { input: $object, inner: true } + +``` +hello world +^^^^^ 0 +``` diff --git a/test/suite/commands/seek-object-charset.test.ts b/test/suite/commands/seek-object-charset.test.ts new file mode 100644 index 0000000..4f1c391 --- /dev/null +++ b/test/suite/commands/seek-object-charset.test.ts @@ -0,0 +1,56 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek-object-charset.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > select-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello world + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: /[\p{L}]+(?[^\S\n]+)/u.source, inner: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-charset.md:10:1", 6, String.raw` + hello world + ^^^^^ 0 + `); + }); + + test("1 > select-inner > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello world + ^^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: /[\p{L}]+(?[^\S\n]+)/u.source, inner: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-charset.md:19:1", 6, String.raw` + hello world + ^^^^^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek-object-paragraph.md b/test/suite/commands/seek-object-paragraph.md new file mode 100644 index 0000000..519956e --- /dev/null +++ b/test/suite/commands/seek-object-paragraph.md @@ -0,0 +1,197 @@ +# 1 + +> /\$object/"(?#predefined=paragraph)"/g + +``` +foo +| 0 + ^ 1 +bar + ^ 2 + +^ 3 +baz +^ 4 + + +^ 5 + + +qux +``` + +## 1 to-start +[up](#1) + +- .seek.object { input: $object, where: "start" } + +Selection #4 skipped to first sentence start because it was active at the very +beginning of next sentence. Notice how anchor is moved. +Similarly, selection #5 re-anchored to one line above and then selected last. + +``` +foo +^^^^ 0 +bar +^^^^ 0 + +^ 0 +baz +|^^^ 1 + +^ 1 + +^ 1 + + +qux +``` + +## 1 to-end +[up](#1) + +- .seek.object { input: $object, where: "end" } + +Paragraph outer end includes all trailing line breaks. +If a selection is on an empty line, it is always re-anchored to the next line. + +``` +foo +^^^^ 0 +bar +^^^^ 0 + +^ 0 +baz +^^^^ 1 + +^ 1 + +^ 1 + +^ 1 + +^ 1 +qux +``` + +## 1 to-end-inner +[up](#1) + +> behavior <- character + +- .seek.object { input: $object, where: "end", inner: true } + +Paragraph inner end does not include blank lines (but includes the last line +break before blank lines). Special cases are same as above. + +``` +foo +^^^^ 0 +bar +^^^^ 0 + +^ 1 +baz +^^^^ 1 + + +^ 2 + + +qux +``` + +## 1 select +[up](#1) + +- .seek.object { input: $object } + +Do not skip over the current character when finding paragraph start. + +``` +foo +^^^^ 0 +bar +^^^^ 0 + +^ 0 +baz +^^^^ 1 + +^ 1 + +^ 1 + +^ 1 + +^ 1 +qux +``` + +# 2 + +Special cases regarding blank lines and next paragraph. + +> /\$object/"(?#predefined=paragraph)"/g + +``` +paragraph 1 +^^^^^^^^^^^^ 0 + +^ 1 + +^ 2 + +^ 3 + +^ 4 +paragraph 2 +``` + +## 2 select +[up](#2) + +- .seek.object { input: $object, inner: true } + +The only special case for select: when active line is blank and the next line +is not, select the **next** paragraph instead. This applied to selection #4. +Note that it only looks one line ahead, so selections #0-3 were not affected. + +``` +paragraph 1 +^^^^^^^^^^^^ 0 + + + + +paragraph 2 +^^^^^^^^^^^ 1 +``` + +## 2 to-end-inner +[up](#2) + +> behavior <- character + +- .seek.object { input: $object, where: "end", inner: true } + +In Kakoune, if a selection is on an empty line (L), it always re-anchors to the +start of the next line (L+1). Then if L+1 is non-empty or L+2 is non-empty, +it selects to the end of the paragraph (applied to selections #3 and #4 here). +Selections #1-3 were only reanchored. Selection #0 was at the end of 1st line. + +``` +paragraph 1 + ^ 0 + +^ 1 + +^ 2 + +^ 3 + +^ 4 +paragraph 2 +^^^^^^^^^^^ 4 +``` diff --git a/test/suite/commands/seek-object-paragraph.test.ts b/test/suite/commands/seek-object-paragraph.test.ts new file mode 100644 index 0000000..23301b0 --- /dev/null +++ b/test/suite/commands/seek-object-paragraph.test.ts @@ -0,0 +1,269 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek-object-paragraph.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > to-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + | 0 + ^ 1 + bar + ^ 2 + + ^ 3 + baz + ^ 4 + + + ^ 5 + + + qux + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=paragraph)", where: "start" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-paragraph.md:23:1", 6, String.raw` + foo + ^^^^ 0 + bar + ^^^^ 0 + + ^ 0 + baz + |^^^ 1 + + ^ 1 + + ^ 1 + + + qux + `); + }); + + test("1 > to-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + | 0 + ^ 1 + bar + ^ 2 + + ^ 3 + baz + ^ 4 + + + ^ 5 + + + qux + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=paragraph)", where: "end" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-paragraph.md:50:1", 6, String.raw` + foo + ^^^^ 0 + bar + ^^^^ 0 + + ^ 0 + baz + ^^^^ 1 + + ^ 1 + + ^ 1 + + ^ 1 + + ^ 1 + qux + `); + }); + + test("1 > to-end-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + | 0 + ^ 1 + bar + ^ 2 + + ^ 3 + baz + ^ 4 + + + ^ 5 + + + qux + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "(?#predefined=paragraph)", where: "end", inner: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-paragraph.md:78:1", 6, String.raw` + foo + ^^^^ 0 + bar + ^^^^ 0 + + ^ 1 + baz + ^^^^ 1 + + + ^ 2 + + + qux + `); + }); + + test("1 > select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + | 0 + ^ 1 + bar + ^ 2 + + ^ 3 + baz + ^ 4 + + + ^ 5 + + + qux + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=paragraph)" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-paragraph.md:105:1", 6, String.raw` + foo + ^^^^ 0 + bar + ^^^^ 0 + + ^ 0 + baz + ^^^^ 1 + + ^ 1 + + ^ 1 + + ^ 1 + + ^ 1 + qux + `); + }); + + test("2 > select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + paragraph 1 + ^^^^^^^^^^^^ 0 + + ^ 1 + + ^ 2 + + ^ 3 + + ^ 4 + paragraph 2 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=paragraph)", inner: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-paragraph.md:152:1", 6, String.raw` + paragraph 1 + ^^^^^^^^^^^^ 0 + + + + + paragraph 2 + ^^^^^^^^^^^ 1 + `); + }); + + test("2 > to-end-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + paragraph 1 + ^^^^^^^^^^^^ 0 + + ^ 1 + + ^ 2 + + ^ 3 + + ^ 4 + paragraph 2 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "(?#predefined=paragraph)", where: "end", inner: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-paragraph.md:172:1", 6, String.raw` + paragraph 1 + ^ 0 + + ^ 1 + + ^ 2 + + ^ 3 + + ^ 4 + paragraph 2 + ^^^^^^^^^^^ 4 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek-object-quoted.md b/test/suite/commands/seek-object-quoted.md new file mode 100644 index 0000000..6f04f0f --- /dev/null +++ b/test/suite/commands/seek-object-quoted.md @@ -0,0 +1,31 @@ +# 1 + +> /\$object/"(?#noescape)\"(?#inner)(?#noescape)\""/g + +Note that escaped characters are handled. There are two backslashes at the end, +so the string ends on the second-to-last character. + +``` +hello world "inside a quote, there can be escaped \" characters! also\"\\\"\\"" + ^ 0 +``` + +## 1 select +[up](#1) + +- .seek.object { input: $object } + +``` +hello world "inside a quote, there can be escaped \" characters! also\"\\\"\\"" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 +``` + +## 1 select-inner +[up](#1) + +- .seek.object { input: $object, inner: true } + +``` +hello world "inside a quote, there can be escaped \" characters! also\"\\\"\\"" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 +``` diff --git a/test/suite/commands/seek-object-quoted.test.ts b/test/suite/commands/seek-object-quoted.test.ts new file mode 100644 index 0000000..0da425d --- /dev/null +++ b/test/suite/commands/seek-object-quoted.test.ts @@ -0,0 +1,56 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek-object-quoted.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello world "inside a quote, there can be escaped \" characters! also\"\\\"\\"" + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#noescape)\"(?#inner)(?#noescape)\"" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-quoted.md:13:1", 6, String.raw` + hello world "inside a quote, there can be escaped \" characters! also\"\\\"\\"" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + `); + }); + + test("1 > select-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello world "inside a quote, there can be escaped \" characters! also\"\\\"\\"" + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#noescape)\"(?#inner)(?#noescape)\"", inner: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-quoted.md:23:1", 6, String.raw` + hello world "inside a quote, there can be escaped \" characters! also\"\\\"\\"" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek-object-sentence.md b/test/suite/commands/seek-object-sentence.md new file mode 100644 index 0000000..e2fe8f9 --- /dev/null +++ b/test/suite/commands/seek-object-sentence.md @@ -0,0 +1,464 @@ +# 1 + +> /\$object/"(?#predefined=sentence)"/g + +``` +A sentence starts with a non-blank character or a line break. <== It ends with a +| 0 +punctuation mark like the previous one, or two consecutive line breaks like this + | 1 + +An outer sentence also contains the trailing blank characters (but never line +|^^^^^^^^^^^^^^^^ 2 +breaks) like this. <== The white spaces before this sentence belongs to + ^ 3 +the outer previous sentence. + ^ 4 + <- White spaces here and the line break before them belongs to this sentence, + |^^^^^^^^^^^^^^^^^^^^^ 5 +not the previous one, since the previous trailing cannot contain line breaks. +``` + +## 1 to-end +[up](#1) + +- .seek.object { input: $object, where: "end" } + +``` +A sentence starts with a non-blank character or a line break. <== It ends with a +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 +punctuation mark like the previous one, or two consecutive line breaks like this + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1 + +An outer sentence also contains the trailing blank characters (but never line +^ 2 +breaks) like this. <== The white spaces before this sentence belongs to +the outer previous sentence. + ^ 2 + <- White spaces here and the line break before them belongs to this sentence, + ^ 3 +not the previous one, since the previous trailing cannot contain line breaks. + ^ 3 +``` + +## 1 to-start +[up](#1) + +- .seek.object { input: $object, where: "start" } + +``` +A sentence starts with a non-blank character or a line break. <== It ends with a +| 0 |^^^^^^^^^^^^^^^^^^ 1 +punctuation mark like the previous one, or two consecutive line breaks like this +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1 + +An outer sentence also contains the trailing blank characters (but never line +|^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 +breaks) like this. <== The white spaces before this sentence belongs to +^^^^^^^^^^^^^^^^^^^^^^ 2 |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 3 +the outer previous sentence. +^^^^^^^^^^^^^^^^^^ 3 | 4 + <- White spaces here and the line break before them belongs to this sentence, +^^^^^^ 4 +not the previous one, since the previous trailing cannot contain line breaks. +``` + +## 1 select-inner +[up](#1) + +- .seek.object { input: $object, inner: true } + +``` +A sentence starts with a non-blank character or a line break. <== It ends with a +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + ^^^^^^^^^^^^^^^^^^^ 1 +punctuation mark like the previous one, or two consecutive line breaks like this +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1 + +An outer sentence also contains the trailing blank characters (but never line +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 +breaks) like this. <== The white spaces before this sentence belongs to +^^^^^^^^^^^^^^^^^ 2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 3 +the outer previous sentence. +^^^^^^^^^^^^^^^^^^^^^^^^^^^ 3 + ^ 4 + <- White spaces here and the line break before them belongs to this sentence, +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 +not the previous one, since the previous trailing cannot contain line breaks. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 +``` + +And now, some edge cases... + +# 2 + +> /\$object/"(?#predefined=sentence)"/g + +``` + I'm a sentence . I'm another sentence. + | 0 ^ 1 |^^^ 2 +``` + +```` + <--- main part --><-------- trailing ---------> +```` + +In this case since the leading blank chars are at document start, they do not +belong to any sentence. First sentence starts at "I". + +## 2 to-start +[up](#2) + +- .seek.object { input: $object, where: "start" } + +``` + I'm a sentence . I'm another sentence. + ^^^^ 0 + ^^^^^^^^^ 1 +``` + +## 2 to-end +[up](#2) + +- .seek.object { input: $object, where: "end" } + +``` + I'm a sentence . I'm another sentence. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 +``` + +## 2 select +[up](#2) + +- .seek.object { input: $object } + +``` + I'm a sentence . I'm another sentence. + ^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 +``` + +# 3 + +> /\$object/"(?#predefined=sentence)"/g + +``` +I'm a previous sentence. + |^^^ 3 ^ 4 + ^ 5 + I'm a sentence . I'm another sentence. + ^ 0 ^ 1 |^^^ 2 +``` + +```` +<----- leading ---><------ main part -------------><---- trailing -------------> +```` + +In this case, the leading blank chars and the line break before it belongs to +current sentence (outer and inner) because the previous sentence's inner end is +the previous period and it's outer end can only cover trailing blank chars +but not the line break (or anything after the line break). + +## 3 select-inner +[up](#3) + +- .seek.object { input: $object, inner: true } + +This one is actually pretty easy -- it only depends on which sentence each +selection was active on. Just remember that the previous sentence ends +**before** the line break and the current sentence starts **at** the line break. + +``` +I'm a previous sentence. +^^^^^^^^^^^^^^^^^^^^^^^ 0 ^ 1 + I'm a sentence . I'm another sentence. +^^^^^^^^^^^^^^^^^^^^^^^^^ 1 +``` + +## 3 to-start +[up](#3) + +- .seek.object { input: $object, where: "start" } + +``` +I'm a previous sentence. +|^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + I'm a sentence . I'm another sentence. +^^^^^^^^^^^^^^^^^ 0 +``` + +Note that selections #0, #1, #5 are sent to the **previous** sentence since they +are active at the leading blank chars or first nonblank char. As a special case, +their anchors were set to the **inner** end of the previous sentence (instead of +old active). Similarly, selection #4 is also re-anchored because old active +was on trailing blank chars. + +## 3 to-start-inner +[up](#3) + +- .seek.object { input: $object, where: "start", inner: true } + +This is exactly the same as above, because leading blank chars are also part of +the inner sentence. + +``` +I'm a previous sentence. +|^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + I'm a sentence . I'm another sentence. +^^^^^^^^^^^^^^^^^ 0 +``` + +## 3 to-end +[up](#3) + +- .seek.object { input: $object, where: "end" } + +``` +I'm a previous sentence. + ^^^^^^^^^^ 0 + I'm a sentence . I'm another sentence. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 +``` + +Selection #5 was active on the line break, which is also part of the following +sentence. Selection #4 was on trailing of previous sentence so it seeks to the +current sentence. Worth noting that `to-end` does not have special treatment for +anchors so selection #4 does not get re-anchored to the current sentence start. +Similarly, selection #0 still anchors to the leading blank. + +# 4 + +> /\$object/"(?#predefined=sentence)"/g + +``` +I'm a sentence.I'm another sentence + ^^^^ 0 ^ 1 ^ 3 | 4 + ^ 2 + +``` + +```` +<----- sentence A ------><------------ sentence B ---------------> +```` + +## 4 select +[up](#4) + +- .seek.object { input: $object } + +``` +I'm a sentence.I'm another sentence +^^^^^^^^^^^^^^^ 0 + ^^^^^^^^^^^^^^^^^^^^^ 1 + +``` + +The last line break is the terminating character of sentence B and is also +considered to be inner sentence. There is no trailing for either sentence. + +## 4 to-start +[up](#4) + +- .seek.object { input: $object, where: "start" } + +``` +I'm a sentence.I'm another sentence +|^^^^^^^^^^^^^^ 0 + |^^^^^^^^^^^^^^^^^^^ 1 + +``` + +Selection #2 seeks to the previous sentence because it was active at the first +character of sentence B and it's anchor was set to the end of last sentence +instead of old active. That's right: `to-start` has tons of special cases. + +## 4 to-end +[up](#4) + +- .seek.object { input: $object, where: "end" } + +``` +I'm a sentence.I'm another sentence + ^^^^ 0 + ^^^^^^^^^^^^^^^^^^^^^ 1 + +``` + +`to-end` has fewer edge cases and it will **not** seek to the next sentence on +from the current sentence inner end, so selection #1 did not move. +(Trailing whitespace is a different story though, covered by cases above.) + +# 5 + +> /\$object/"(?#predefined=sentence)"/g + +``` +I'm a sentence terminated by two line breaks + |^^^^^ 0 ^ 1 + +^ 2 + I'm another sentence + ^ 3 |^^^^^ 4 +``` + +The first sentence includes the first line break as both inner and outer. +There is no "trailing" whitespace here (i.e. inner end === outer end). +The empty line and the blank characters on the next line does not belong to +either sentence. + +## 5 select +[up](#5) + +- .seek.object { input: $object } + +``` +I'm a sentence terminated by two line breaks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + I'm another sentence + ^^^^^^^^^^^^^^^^^^^^ 1 +``` + +## 5 to-start +[up](#5) + +- .seek.object { input: $object, where: "start" } + +``` +I'm a sentence terminated by two line breaks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + I'm another sentence +^^^^ 1 + ^^^^^^^^^ 2 +``` + +More special cases: selection #2 was on an empty line so it does not belong to +any sentence, and it actually scanned to the **next** sentence start. + +## 5 to-end +[up](#5) + +- .seek.object { input: $object, where: "end" } + +``` +I'm a sentence terminated by two line breaks + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + +^ 1 + I'm another sentence +^^^^^^^^^^^^^^^^^^^^^^^^ 1 +``` + +Selection #1 was exactly at the end of the first sentence and did not move. +Selection #2 was on the empty line and scanned to the next sentence end. +Again, no special treatment for the anchors of selections #2 and #3. + +# 6 + +> /\$object/"(?#predefined=sentence)"/g + +These test cases before document the Kakoune behavior in some minor corner +cases regarding trailing blank lines. Note that these may or may not make +sense in VSCode where the last line does NOT have a line break attached. + +``` +I'm a sentence terminated by two line breaks plus one more + |^^^^^ 0 ^ 1 + +^ 2 + +| 3 +``` + +## 6 to-start +[up](#6) + +- .seek.object { input: $object, where: "start" } + +``` +I'm a sentence terminated by two line breaks plus one more +|^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + +| 1 +``` + +## 6 to-end +[up](#6) + +- .seek.object { input: $object, where: "end" } + +``` +I'm a sentence terminated by two line breaks plus one more + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + +| 1 + +| 2 +``` + +## 6 select +[up](#6) + +- .seek.object { input: $object } + +``` +I'm a sentence terminated by two line breaks plus one more +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + +| 1 +``` + +# 7 + +> behavior <- character +> /\$object/"(?#predefined=sentence)"/g + +``` +I'm a sentence at end of document + +| 0 +``` + +## 7 to-start +[up](#7) + +- .seek.object { input: $object, where: "start" } + +``` +I'm a sentence at end of document +|^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + +``` + +## 7 to-end +[up](#7) + +- .seek.object { input: $object, where: "end" } + +``` +I'm a sentence at end of document + ^ 0 + +``` + +## 7 select +[up](#7) + +- .seek.object { input: $object } + +``` +I'm a sentence at end of document +|^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + +``` + + diff --git a/test/suite/commands/seek-object-sentence.test.ts b/test/suite/commands/seek-object-sentence.test.ts new file mode 100644 index 0000000..35537a6 --- /dev/null +++ b/test/suite/commands/seek-object-sentence.test.ts @@ -0,0 +1,561 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek-object-sentence.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > to-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + A sentence starts with a non-blank character or a line break. <== It ends with a + | 0 + punctuation mark like the previous one, or two consecutive line breaks like this + | 1 + + An outer sentence also contains the trailing blank characters (but never line + |^^^^^^^^^^^^^^^^ 2 + breaks) like this. <== The white spaces before this sentence belongs to + ^ 3 + the outer previous sentence. + ^ 4 + <- White spaces here and the line break before them belongs to this sentence, + |^^^^^^^^^^^^^^^^^^^^^ 5 + not the previous one, since the previous trailing cannot contain line breaks. + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "end" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:22:1", 6, String.raw` + A sentence starts with a non-blank character or a line break. <== It ends with a + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + punctuation mark like the previous one, or two consecutive line breaks like this + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1 + + An outer sentence also contains the trailing blank characters (but never line + ^ 2 + breaks) like this. <== The white spaces before this sentence belongs to + the outer previous sentence. + ^ 2 + <- White spaces here and the line break before them belongs to this sentence, + ^ 3 + not the previous one, since the previous trailing cannot contain line breaks. + ^ 3 + `); + }); + + test("1 > to-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + A sentence starts with a non-blank character or a line break. <== It ends with a + | 0 + punctuation mark like the previous one, or two consecutive line breaks like this + | 1 + + An outer sentence also contains the trailing blank characters (but never line + |^^^^^^^^^^^^^^^^ 2 + breaks) like this. <== The white spaces before this sentence belongs to + ^ 3 + the outer previous sentence. + ^ 4 + <- White spaces here and the line break before them belongs to this sentence, + |^^^^^^^^^^^^^^^^^^^^^ 5 + not the previous one, since the previous trailing cannot contain line breaks. + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "start" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:44:1", 6, String.raw` + A sentence starts with a non-blank character or a line break. <== It ends with a + | 0 |^^^^^^^^^^^^^^^^^^ 1 + punctuation mark like the previous one, or two consecutive line breaks like this + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1 + + An outer sentence also contains the trailing blank characters (but never line + |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 + breaks) like this. <== The white spaces before this sentence belongs to + ^^^^^^^^^^^^^^^^^^^^^^ 2 |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 3 + the outer previous sentence. + ^^^^^^^^^^^^^^^^^^ 3 | 4 + <- White spaces here and the line break before them belongs to this sentence, + ^^^^^^ 4 + not the previous one, since the previous trailing cannot contain line breaks. + `); + }); + + test("1 > select-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + A sentence starts with a non-blank character or a line break. <== It ends with a + | 0 + punctuation mark like the previous one, or two consecutive line breaks like this + | 1 + + An outer sentence also contains the trailing blank characters (but never line + |^^^^^^^^^^^^^^^^ 2 + breaks) like this. <== The white spaces before this sentence belongs to + ^ 3 + the outer previous sentence. + ^ 4 + <- White spaces here and the line break before them belongs to this sentence, + |^^^^^^^^^^^^^^^^^^^^^ 5 + not the previous one, since the previous trailing cannot contain line breaks. + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", inner: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:66:1", 6, String.raw` + A sentence starts with a non-blank character or a line break. <== It ends with a + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + ^^^^^^^^^^^^^^^^^^^ 1 + punctuation mark like the previous one, or two consecutive line breaks like this + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1 + + An outer sentence also contains the trailing blank characters (but never line + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 + breaks) like this. <== The white spaces before this sentence belongs to + ^^^^^^^^^^^^^^^^^ 2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 3 + the outer previous sentence. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 3 + ^ 4 + <- White spaces here and the line break before them belongs to this sentence, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 + not the previous one, since the previous trailing cannot contain line breaks. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 + `); + }); + + test("2 > to-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence . I'm another sentence. + | 0 ^ 1 |^^^ 2 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "start" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:109:1", 6, String.raw` + I'm a sentence . I'm another sentence. + ^^^^ 0 + ^^^^^^^^^ 1 + `); + }); + + test("2 > to-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence . I'm another sentence. + | 0 ^ 1 |^^^ 2 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "end" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:120:1", 6, String.raw` + I'm a sentence . I'm another sentence. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + `); + }); + + test("2 > select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence . I'm another sentence. + | 0 ^ 1 |^^^ 2 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:130:1", 6, String.raw` + I'm a sentence . I'm another sentence. + ^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + `); + }); + + test("3 > select-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a previous sentence. + |^^^ 3 ^ 4 + ^ 5 + I'm a sentence . I'm another sentence. + ^ 0 ^ 1 |^^^ 2 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", inner: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:161:1", 6, String.raw` + I'm a previous sentence. + ^^^^^^^^^^^^^^^^^^^^^^^ 0 ^ 1 + I'm a sentence . I'm another sentence. + ^^^^^^^^^^^^^^^^^^^^^^^^^ 1 + `); + }); + + test("3 > to-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a previous sentence. + |^^^ 3 ^ 4 + ^ 5 + I'm a sentence . I'm another sentence. + ^ 0 ^ 1 |^^^ 2 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "start" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:177:1", 6, String.raw` + I'm a previous sentence. + |^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + I'm a sentence . I'm another sentence. + ^^^^^^^^^^^^^^^^^ 0 + `); + }); + + test("3 > to-start-inner", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a previous sentence. + |^^^ 3 ^ 4 + ^ 5 + I'm a sentence . I'm another sentence. + ^ 0 ^ 1 |^^^ 2 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "start", inner: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:195:1", 6, String.raw` + I'm a previous sentence. + |^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + I'm a sentence . I'm another sentence. + ^^^^^^^^^^^^^^^^^ 0 + `); + }); + + test("3 > to-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a previous sentence. + |^^^ 3 ^ 4 + ^ 5 + I'm a sentence . I'm another sentence. + ^ 0 ^ 1 |^^^ 2 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "end" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:210:1", 6, String.raw` + I'm a previous sentence. + ^^^^^^^^^^ 0 + I'm a sentence . I'm another sentence. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + `); + }); + + test("4 > select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence.I'm another sentence + ^^^^ 0 ^ 1 ^ 3 | 4 + ^ 2 + + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:243:1", 6, String.raw` + I'm a sentence.I'm another sentence + ^^^^^^^^^^^^^^^ 0 + ^^^^^^^^^^^^^^^^^^^^^ 1 + + `); + }); + + test("4 > to-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence.I'm another sentence + ^^^^ 0 ^ 1 ^ 3 | 4 + ^ 2 + + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "start" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:258:1", 6, String.raw` + I'm a sentence.I'm another sentence + |^^^^^^^^^^^^^^ 0 + |^^^^^^^^^^^^^^^^^^^ 1 + + `); + }); + + test("4 > to-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence.I'm another sentence + ^^^^ 0 ^ 1 ^ 3 | 4 + ^ 2 + + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "end" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:274:1", 6, String.raw` + I'm a sentence.I'm another sentence + ^^^^ 0 + ^^^^^^^^^^^^^^^^^^^^^ 1 + + `); + }); + + test("5 > select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence terminated by two line breaks + |^^^^^ 0 ^ 1 + + ^ 2 + I'm another sentence + ^ 3 |^^^^^ 4 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:308:1", 6, String.raw` + I'm a sentence terminated by two line breaks + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + I'm another sentence + ^^^^^^^^^^^^^^^^^^^^ 1 + `); + }); + + test("5 > to-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence terminated by two line breaks + |^^^^^ 0 ^ 1 + + ^ 2 + I'm another sentence + ^ 3 |^^^^^ 4 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "start" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:321:1", 6, String.raw` + I'm a sentence terminated by two line breaks + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + I'm another sentence + ^^^^ 1 + ^^^^^^^^^ 2 + `); + }); + + test("5 > to-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence terminated by two line breaks + |^^^^^ 0 ^ 1 + + ^ 2 + I'm another sentence + ^ 3 |^^^^^ 4 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "end" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:338:1", 6, String.raw` + I'm a sentence terminated by two line breaks + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + ^ 1 + I'm another sentence + ^^^^^^^^^^^^^^^^^^^^^^^^ 1 + `); + }); + + test("6 > to-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence terminated by two line breaks plus one more + |^^^^^ 0 ^ 1 + + ^ 2 + + | 3 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "start" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:373:1", 6, String.raw` + I'm a sentence terminated by two line breaks plus one more + |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + + | 1 + `); + }); + + test("6 > to-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence terminated by two line breaks plus one more + |^^^^^ 0 ^ 1 + + ^ 2 + + | 3 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "end" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:386:1", 6, String.raw` + I'm a sentence terminated by two line breaks plus one more + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + | 1 + + | 2 + `); + }); + + test("6 > select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence terminated by two line breaks plus one more + |^^^^^ 0 ^ 1 + + ^ 2 + + | 3 + `); + + // Perform all operations. + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:400:1", 6, String.raw` + I'm a sentence terminated by two line breaks plus one more + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + + | 1 + `); + }); + + test("7 > to-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence at end of document + + | 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "start" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:424:1", 6, String.raw` + I'm a sentence at end of document + |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + `); + }); + + test("7 > to-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence at end of document + + | 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)", where: "end" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:435:1", 6, String.raw` + I'm a sentence at end of document + ^ 0 + + `); + }); + + test("7 > select", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + I'm a sentence at end of document + + | 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.object", { input: "(?#predefined=sentence)" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-object-sentence.md:446:1", 6, String.raw` + I'm a sentence at end of document + |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 0 + + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek-word-edge.md b/test/suite/commands/seek-word-edge.md new file mode 100644 index 0000000..e7181db --- /dev/null +++ b/test/suite/commands/seek-word-edge.md @@ -0,0 +1,283 @@ +# 1 + +``` +the quick brown fox +|^ 0 +``` + +## 1 word-start-backward +[up](#1) + +- .seek.word.backward + +No more selections remaining, just keep the last one. + +``` +the quick brown fox +|^ 0 +``` + +## 1 word-start-4 +[up](#1) + +- .seek.word { count: 4 } + +``` +the quick brown fox + ^^^ 0 +``` + +### 1 word-start-4 word-start +[up](#1-word-start-4) + +- .seek.word + +No more selections remaining, just keep the last one. + +``` +the quick brown fox + ^^^ 0 +``` + +#### 1 word-start-4 word-start x +[up](#1-word-start-4-word-start) + +- .seek.word + +No more selections remaining, do not change. + +``` +the quick brown fox + ^^^ 0 +``` + +### 1 word-start-4 word-start-backward +[up](#1-word-start-4) + +- .seek.word.backward + +``` +the quick brown fox + |^^ 0 +``` + +### 1 word-start-4 word-start-backward-2 +[up](#1-word-start-4) + +- .seek.word.backward { count: 2 } + +``` +the quick brown fox + |^^^^^ 0 +``` + +### 1 word-start-4 word-start-backward-5 +[up](#1-word-start-4) + +- .seek.word.backward { count: 5 } + +Move 4 times, but don't move again (no more selections remaining otherwise). + +``` +the quick brown fox +|^^^ 0 +``` + +## 1 word-start-5 +[up](#1) + +- .seek.word { count: 5 } + +Move 4 times, but don't move again (no more selections remaining otherwise). + +``` +the quick brown fox + ^^^ 0 +``` + +# 2 + +``` +foo bar + ^ 0 +baz +``` + +## 2 word-start-backward +[up](#2) + +- .seek.word.backward + +``` +foo bar + |^^ 0 +baz +``` + +# 3 + +> behavior <- character + +``` +the quick brown fox +|^^^ 0 + |^ 1 +``` + +## 3 word-start-backward +[up](#3) + +- .seek.word.backward + +Selection #0 overflowed and was removed. Selection #1 moved. + +``` +the quick brown fox + |^^ 0 +``` + +## 3 word-start-backward-9 +[up](#3) + +- .seek.word.backward { count: 9 } + +Both overflowed and both falled back to the selection below. + +``` +the quick brown fox +|^^^ 0 +``` + +## 3 word-end-4 +[up](#3) + +- .seek.wordEnd { count: 4 } + +Selection #1 overflowed and was removed. Selection #0 moved. + +``` +the quick brown fox + ^^^^ 0 +``` + +## 3 word-end-5 +[up](#3) + +- .seek.wordEnd { count: 5 } + +Both overflowed and both falled back to the selection below. + +``` +the quick brown fox + ^^^^ 0 +``` + +# 4 + +> behavior <- character + +``` + +there is a blank line before me +|^^^^ 0 +``` + +## 4 word-start-backward +[up](#4) + +- .seek.word.backward + +Special case in Kakoune: anchor is moved to beginning of document and active is +moved to the first character of the second line. + +``` + +^ 0 +there is a blank line before me +^ 0 +``` + +### 4 word-start-backward x +[up](#4-word-start-backward) + +- .seek.word.backward + +Going to previous again will just keep the selection the same. + +``` + +^ 0 +there is a blank line before me +^ 0 +``` + +## 4 word-start-backward-4 +[up](#4) + +- .seek.word.backward { count: 9 } + +Similarly, more repetitions won't do anything either. + +``` + +^ 0 +there is a blank line before me +^ 0 +``` + +# 5 + +> behavior <- character + +``` + + +there are two blank lines before me +|^^^^ 0 +``` + +## 5 word-start-backward +[up](#5) + +- .seek.word.backward + +Special case in Kak: anchor is moved to beginning of document and active is +moved to the first character (line break in this case) of the second line. + +``` + +^ 0 + +^ 0 +there are two blank lines before me +``` + +### 5 word-start-backward x +[up](#5-word-start-backward) + +- .seek.word.backward + +Going to previous again will just keep the selection the same. + +``` + +^ 0 + +^ 0 +there are two blank lines before me +``` + +## 5 word-start-backward-9 +[up](#5) + +- .seek.word.backward { count: 9 } + +``` + +^ 0 + +^ 0 +there are two blank lines before me +``` + +TODO: Write tests for document with trailing empty lines. diff --git a/test/suite/commands/seek-word-edge.test.ts b/test/suite/commands/seek-word-edge.test.ts new file mode 100644 index 0000000..4883e3a --- /dev/null +++ b/test/suite/commands/seek-word-edge.test.ts @@ -0,0 +1,397 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek-word-edge.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + |^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.word.backward"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:8:1", 6, String.raw` + the quick brown fox + |^ 0 + `); + }); + + test("1 > word-start-4", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + |^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.word", { count: 4 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:20:1", 6, String.raw` + the quick brown fox + ^^^ 0 + `); + }); + + test("1 > word-start-4 > word-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.word"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:30:1", 6, String.raw` + the quick brown fox + ^^^ 0 + `); + }); + + test("1 > word-start-4 > word-start > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.word"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:42:1", 6, String.raw` + the quick brown fox + ^^^ 0 + `); + }); + + test("1 > word-start-4 > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.word.backward"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:54:1", 6, String.raw` + the quick brown fox + |^^ 0 + `); + }); + + test("1 > word-start-4 > word-start-backward-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.word.backward", { count: 2 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:64:1", 6, String.raw` + the quick brown fox + |^^^^^ 0 + `); + }); + + test("1 > word-start-4 > word-start-backward-5", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.word.backward", { count: 5 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:74:1", 6, String.raw` + the quick brown fox + |^^^ 0 + `); + }); + + test("1 > word-start-5", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + |^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.word", { count: 5 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:86:1", 6, String.raw` + the quick brown fox + ^^^ 0 + `); + }); + + test("2 > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo bar + ^ 0 + baz + `); + + // Perform all operations. + await executeCommand("dance.seek.word.backward"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:106:1", 6, String.raw` + foo bar + |^^ 0 + baz + `); + }); + + test("3 > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + |^^^ 0 + |^ 1 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:127:1", 6, String.raw` + the quick brown fox + |^^ 0 + `); + }); + + test("3 > word-start-backward-9", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + |^^^ 0 + |^ 1 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward", { count: 9 }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:139:1", 6, String.raw` + the quick brown fox + |^^^ 0 + `); + }); + + test("3 > word-end-4", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + |^^^ 0 + |^ 1 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd", { count: 4 }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:151:1", 6, String.raw` + the quick brown fox + ^^^^ 0 + `); + }); + + test("3 > word-end-5", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + |^^^ 0 + |^ 1 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd", { count: 5 }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:163:1", 6, String.raw` + the quick brown fox + ^^^^ 0 + `); + }); + + test("4 > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + + there is a blank line before me + |^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:185:1", 6, String.raw` + + ^ 0 + there is a blank line before me + ^ 0 + `); + }); + + test("4 > word-start-backward > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + + ^ 0 + there is a blank line before me + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:200:1", 6, String.raw` + + ^ 0 + there is a blank line before me + ^ 0 + `); + }); + + test("4 > word-start-backward-4", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + + there is a blank line before me + |^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward", { count: 9 }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:214:1", 6, String.raw` + + ^ 0 + there is a blank line before me + ^ 0 + `); + }); + + test("5 > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + + + there are two blank lines before me + |^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:239:1", 6, String.raw` + + ^ 0 + + ^ 0 + there are two blank lines before me + `); + }); + + test("5 > word-start-backward > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + + ^ 0 + + ^ 0 + there are two blank lines before me + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:255:1", 6, String.raw` + + ^ 0 + + ^ 0 + there are two blank lines before me + `); + }); + + test("5 > word-start-backward-9", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + + + there are two blank lines before me + |^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward", { count: 9 }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-edge.md:270:1", 6, String.raw` + + ^ 0 + + ^ 0 + there are two blank lines before me + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek-word-end.md b/test/suite/commands/seek-word-end.md new file mode 100644 index 0000000..2315bbd --- /dev/null +++ b/test/suite/commands/seek-word-end.md @@ -0,0 +1,36 @@ +# 1 + +``` +private String foo; + | 0 +``` + +## 1 word-end +[up](#1) + +- .seek.wordEnd + +``` +private String foo; + ^^^^ 0 +``` + +### 1 word-end x +[up](#1-word-end) + +- .seek.wordEnd + +``` +private String foo; + ^^^^^^^ 0 +``` + +## 1 word-end-2 +[up](#1) + +- .seek.wordEnd { count: 2 } + +``` +private String foo; + ^^^^^^^ 0 +``` diff --git a/test/suite/commands/seek-word-end.test.ts b/test/suite/commands/seek-word-end.test.ts new file mode 100644 index 0000000..7f1e171 --- /dev/null +++ b/test/suite/commands/seek-word-end.test.ts @@ -0,0 +1,73 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek-word-end.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > word-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + private String foo; + | 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.wordEnd"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-end.md:8:1", 6, String.raw` + private String foo; + ^^^^ 0 + `); + }); + + test("1 > word-end > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + private String foo; + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.wordEnd"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-end.md:18:1", 6, String.raw` + private String foo; + ^^^^^^^ 0 + `); + }); + + test("1 > word-end-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + private String foo; + | 0 + `); + + // Perform all operations. + await executeCommand("dance.seek.wordEnd", { count: 2 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word-end.md:28:1", 6, String.raw` + private String foo; + ^^^^^^^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek-word.md b/test/suite/commands/seek-word.md new file mode 100644 index 0000000..a3d5237 --- /dev/null +++ b/test/suite/commands/seek-word.md @@ -0,0 +1,338 @@ +# 1 + +> behavior <- character + +``` +console.log() +^ 0 +``` + +## 1 word-end +[up](#1) + +- .seek.wordEnd + +``` +console.log() +^^^^^^^ 0 +``` + +### 1 word-end x +[up](#1-word-end) + +- .seek.wordEnd + +``` +console.log() + ^ 0 +``` + +#### 1 word-end x x +[up](#1-word-end-x) + +- .seek.wordEnd + +``` +console.log() + ^^^ 0 +``` + +##### 1 word-end x x word-start-backward +[up](#1-word-end-x-x) + +- .seek.word.backward + +``` +console.log() + |^^ 0 +``` + +###### 1 word-end x x word-start-backward x +[up](#1-word-end-x-x-word-start-backward) + +- .seek.word.backward + +``` +console.log() + ^ 0 +``` + +###### 1 word-end x x word-start-backward x x +[up](#1-word-end-x-x-word-start-backward-x) + +- .seek.word.backward + +``` +console.log() +|^^^^^^ 0 +``` + +## 1 word-end-extend +[up](#1) + +- .seek.wordEnd.extend + +``` +console.log() +^^^^^^^ 0 +``` + +### 1 word-end-extend x +[up](#1-word-end-extend) + +- .seek.wordEnd.extend + +``` +console.log() +^^^^^^^^ 0 +``` + +#### 1 word-end-extend x x +[up](#1-word-end-extend-x) + +- .seek.wordEnd.extend + +``` +console.log() +^^^^^^^^^^^ 0 +``` + +#### 1 word-end-extend x word-end +[up](#1-word-end-extend-x) + +- .seek.wordEnd + +``` +console.log() + ^^^ 0 +``` + +# 2 + +> behavior <- character + +``` +foo + +bar +^ 0 +``` + +## 2 word-start-backward +[up](#2) + +- .seek.word.backward + +``` +foo +|^^ 0 + +bar +``` + +# 3 + +Now with spaces. + +> behavior <- character + +``` +aaa bbb ccc ddd + ^ 0 +``` + +## 3 word-end +[up](#3) + +- .seek.wordEnd + +``` +aaa bbb ccc ddd + ^^^^ 0 +``` + +### 3 word-end x +[up](#3-word-end) + +- .seek.wordEnd + +``` +aaa bbb ccc ddd + ^^^^ 0 +``` + +#### 3 word-end x word-start-backward +[up](#3-word-end-x) + +- .seek.word.backward + +``` +aaa bbb ccc ddd + |^^ 0 +``` + +#### 3 word-end x word-start-backward-2 +[up](#3-word-end-x) + +- .seek.word.backward { count: 2 } + +``` +aaa bbb ccc ddd + |^^^ 0 +``` + +# 4 + +``` +aaa bbb + ^ 0 + ccc +dd +``` + +## 4 word-start +[up](#4) + +- .seek.word + +``` +aaa bbb + ^^^ 0 + ccc +dd +``` + +### 4 word-start word-end +[up](#4-word-start) + +- .seek.wordEnd + +``` +aaa bbb + ccc +^^^^^ 0 +dd +``` + +# 5 + +> behavior <- character + +``` +foo x bar.baz ex +^ 0 +la +``` + +## 5 word-end +[up](#5) + +- .seek.wordEnd + +``` +foo x bar.baz ex +^^^ 0 +la +``` + +### 5 word-end x +[up](#5-word-end) + +- .seek.wordEnd + +``` +foo x bar.baz ex + ^^ 0 +la +``` + +#### 5 word-end x x +[up](#5-word-end-x) + +- .seek.wordEnd + +``` +foo x bar.baz ex + ^^^^ 0 +la +``` + +##### 5 word-end x x x +[up](#5-word-end-x-x) + +- .seek.wordEnd + +``` +foo x bar.baz ex + ^ 0 +la +``` + +# 6 + +> behavior <- character + +``` +a b c d + ^ 0 +``` + +## 6 word-end +[up](#6) + +- .seek.wordEnd + +``` +a b c d + ^^ 0 +``` + +## 6 word-start +[up](#6) + +- .seek.word + +``` +a b c d + ^ 0 +``` + +## 6 word-start-backward +[up](#6) + +- .seek.word.backward + +``` +a b c d +|^ 0 +``` + +## 6 word-end-extend +[up](#6) + +- .seek.wordEnd.extend + +``` +a b c d + ^^^ 0 +``` + +## 6 word-start-extend +[up](#6) + +- .seek.word.extend + +``` +a b c d + ^^ 0 +``` + +## 6 word-start-extend-backward +[up](#6) + +- .seek.word.extend.backward + +``` +a b c d +|^^ 0 +``` diff --git a/test/suite/commands/seek-word.test.ts b/test/suite/commands/seek-word.test.ts new file mode 100644 index 0000000..91533dc --- /dev/null +++ b/test/suite/commands/seek-word.test.ts @@ -0,0 +1,551 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek-word.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > word-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:10:1", 6, String.raw` + console.log() + ^^^^^^^ 0 + `); + }); + + test("1 > word-end > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + ^^^^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:20:1", 6, String.raw` + console.log() + ^ 0 + `); + }); + + test("1 > word-end > x > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:30:1", 6, String.raw` + console.log() + ^^^ 0 + `); + }); + + test("1 > word-end > x > x > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:40:1", 6, String.raw` + console.log() + |^^ 0 + `); + }); + + test("1 > word-end > x > x > word-start-backward > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + |^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:50:1", 6, String.raw` + console.log() + ^ 0 + `); + }); + + test("1 > word-end > x > x > word-start-backward > x > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:60:1", 6, String.raw` + console.log() + |^^^^^^ 0 + `); + }); + + test("1 > word-end-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd.extend"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:70:1", 6, String.raw` + console.log() + ^^^^^^^ 0 + `); + }); + + test("1 > word-end-extend > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + ^^^^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd.extend"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:80:1", 6, String.raw` + console.log() + ^^^^^^^^ 0 + `); + }); + + test("1 > word-end-extend > x > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + ^^^^^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd.extend"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:90:1", 6, String.raw` + console.log() + ^^^^^^^^^^^ 0 + `); + }); + + test("1 > word-end-extend > x > word-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + console.log() + ^^^^^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:100:1", 6, String.raw` + console.log() + ^^^ 0 + `); + }); + + test("2 > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + bar + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:121:1", 6, String.raw` + foo + |^^ 0 + + bar + `); + }); + + test("3 > word-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + aaa bbb ccc ddd + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:144:1", 6, String.raw` + aaa bbb ccc ddd + ^^^^ 0 + `); + }); + + test("3 > word-end > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + aaa bbb ccc ddd + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:154:1", 6, String.raw` + aaa bbb ccc ddd + ^^^^ 0 + `); + }); + + test("3 > word-end > x > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + aaa bbb ccc ddd + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:164:1", 6, String.raw` + aaa bbb ccc ddd + |^^ 0 + `); + }); + + test("3 > word-end > x > word-start-backward-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + aaa bbb ccc ddd + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward", { count: 2 }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:174:1", 6, String.raw` + aaa bbb ccc ddd + |^^^ 0 + `); + }); + + test("4 > word-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + aaa bbb + ^ 0 + ccc + dd + `); + + // Perform all operations. + await executeCommand("dance.seek.word"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:193:1", 6, String.raw` + aaa bbb + ^^^ 0 + ccc + dd + `); + }); + + test("4 > word-start > word-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + aaa bbb + ^^^ 0 + ccc + dd + `); + + // Perform all operations. + await executeCommand("dance.seek.wordEnd"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:205:1", 6, String.raw` + aaa bbb + ccc + ^^^^^ 0 + dd + `); + }); + + test("5 > word-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo x bar.baz ex + ^ 0 + la + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:227:1", 6, String.raw` + foo x bar.baz ex + ^^^ 0 + la + `); + }); + + test("5 > word-end > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo x bar.baz ex + ^^^ 0 + la + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:238:1", 6, String.raw` + foo x bar.baz ex + ^^ 0 + la + `); + }); + + test("5 > word-end > x > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo x bar.baz ex + ^^ 0 + la + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:249:1", 6, String.raw` + foo x bar.baz ex + ^^^^ 0 + la + `); + }); + + test("5 > word-end > x > x > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo x bar.baz ex + ^^^^ 0 + la + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:260:1", 6, String.raw` + foo x bar.baz ex + ^ 0 + la + `); + }); + + test("6 > word-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b c d + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:280:1", 6, String.raw` + a b c d + ^^ 0 + `); + }); + + test("6 > word-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b c d + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:290:1", 6, String.raw` + a b c d + ^ 0 + `); + }); + + test("6 > word-start-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b c d + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:300:1", 6, String.raw` + a b c d + |^ 0 + `); + }); + + test("6 > word-end-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b c d + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.wordEnd.extend"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:310:1", 6, String.raw` + a b c d + ^^^ 0 + `); + }); + + test("6 > word-start-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b c d + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.extend"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:320:1", 6, String.raw` + a b c d + ^^ 0 + `); + }); + + test("6 > word-start-extend-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + a b c d + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek.word.extend.backward"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek-word.md:330:1", 6, String.raw` + a b c d + |^^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/seek.md b/test/suite/commands/seek.md new file mode 100644 index 0000000..3cf89f5 --- /dev/null +++ b/test/suite/commands/seek.md @@ -0,0 +1,213 @@ +# 1 + +``` +abcabc +| 0 +``` + +## 1 select-to-included +[up](#1) + +- .seek { input: "c", include: true } + +``` +abcabc +^^^ 0 +``` + +### 1 select-to-included select-to +[up](#1-select-to-included) + +- .seek { input: "c" } + +``` +abcabc + ^^ 0 +``` + +### 1 select-to-included select-to-character +[up](#1-select-to-included) + +> behavior <- character + +- .seek { input: "c" } + +``` +abcabc + ^^^ 0 +``` + +## 1 select-to-c-2 +[up](#1) + +- .seek { input: "c", count: 2 } + +``` +abcabc +^^^^^ 0 +``` + +### 1 select-to-c-2 select-to-c +[up](#1-select-to-c-2) + +- .seek { input: "c", $expect: /^no selections remain$/ } + +``` +abcabc +^^^^^ 0 +``` + +#### 1 select-to-c-2 select-to-c select-to-b-backward +[up](#1-select-to-c-2-select-to-c) + +- .seek { input: "b", direction: -1 } + +``` +abcabc + | 0 +``` + +##### 1 select-to-c-2 select-to-c select-to-b-backward select-to-a-backward +[up](#1-select-to-c-2-select-to-c-select-to-b-backward) + +- .seek { input: "a", direction: -1 } + +``` +abcabc + ^ 0 +``` + +### 1 select-to-c-2 select-to-c-character +[up](#1-select-to-c-2) + +> behavior <- character + +- .seek { input: "c", $expect: /^no selections remain$/ } + +``` +abcabc +^^^^^ 0 +``` + +#### 1 select-to-c-2 select-to-c-character select-to-b-character +[up](#1-select-to-c-2-select-to-c-character) + +- .seek { input: "b", direction: -1 } + +``` +abcabc + |^^ 0 +``` + +# 2 + +``` +abcdefghijk + ^^^^ 0 +``` + +## 2 extend-to-e-included-backward +[up](#2) + +- .seek { input: "e", direction: -1, shift: "extend", include: true } + +``` +abcdefghijk + ^^ 0 +``` + +## 2 extend-to-g-included-backward +[up](#2) + +- .seek { input: "g", direction: -1, shift: "extend", include: true } + +Selection left unchanged since it can't find another "g" before this. + +``` +abcdefghijk + ^^^^ 0 +``` + +## 2 extend-to-d-included-backward +[up](#2) + +- .seek { input: "d", direction: -1, shift: "extend", include: true } + +``` +abcdefghijk + ^ 0 +``` + +## 2 extend-to-b-included-backward +[up](#2) + +- .seek { input: "b", direction: -1, shift: "extend", include: true } + +``` +abcdefghijk + |^ 0 +``` + +## 2 extend-to-b-backward-character +[up](#2) + +> behavior <- character + +- .seek { input: "b", direction: -1, shift: "extend", include: true } + +``` +abcdefghijk + |^^ 0 +``` + +## 2 extend-to-g-backward +[up](#2) + +- .seek { input: "g", direction: -1, shift: "extend" } + +Selection left unchanged since it can't find another "g" before this. + +``` +abcdefghijk + ^^^^ 0 +``` + +## 2 extend-to-f-backward +[up](#2) + +- .seek { input: "f", direction: -1, shift: "extend" } + +``` +abcdefghijk + ^^^ 0 +``` + +## 2 extend-to-e-backward +[up](#2) + +- .seek { input: "e", direction: -1, shift: "extend" } + +``` +abcdefghijk + ^^ 0 +``` + +## 2 extend-to-c-backward +[up](#2) + +- .seek { input: "c", direction: -1, shift: "extend" } + +``` +abcdefghijk + | 0 +``` + +## 2 extend-to-b-backward +[up](#2) + +- .seek { input: "b", direction: -1, shift: "extend" } + +``` +abcdefghijk + ^ 0 +``` diff --git a/test/suite/commands/seek.test.ts b/test/suite/commands/seek.test.ts new file mode 100644 index 0000000..4fd0967 --- /dev/null +++ b/test/suite/commands/seek.test.ts @@ -0,0 +1,353 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/seek.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > select-to-included", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcabc + | 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "c", include: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:8:1", 6, String.raw` + abcabc + ^^^ 0 + `); + }); + + test("1 > select-to-included > select-to", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcabc + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "c" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:18:1", 6, String.raw` + abcabc + ^^ 0 + `); + }); + + test("1 > select-to-included > select-to-character", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcabc + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek", { input: "c" }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:28:1", 6, String.raw` + abcabc + ^^^ 0 + `); + }); + + test("1 > select-to-c-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcabc + | 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "c", count: 2 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:40:1", 6, String.raw` + abcabc + ^^^^^ 0 + `); + }); + + test("1 > select-to-c-2 > select-to-c", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcabc + ^^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "c", $expect: /^no selections remain$/ }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:50:1", 6, String.raw` + abcabc + ^^^^^ 0 + `); + }); + + test("1 > select-to-c-2 > select-to-c > select-to-b-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcabc + ^^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "b", direction: -1 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:60:1", 6, String.raw` + abcabc + | 0 + `); + }); + + test("1 > select-to-c-2 > select-to-c > select-to-b-backward > select-to-a-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcabc + | 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "a", direction: -1 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:70:1", 6, String.raw` + abcabc + ^ 0 + `); + }); + + test("1 > select-to-c-2 > select-to-c-character", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcabc + ^^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek", { input: "c", $expect: /^no selections remain$/ }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:80:1", 6, String.raw` + abcabc + ^^^^^ 0 + `); + }); + + test("1 > select-to-c-2 > select-to-c-character > select-to-b-character", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcabc + ^^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek", { input: "b", direction: -1 }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:92:1", 6, String.raw` + abcabc + |^^ 0 + `); + }); + + test("2 > extend-to-e-included-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "e", direction: -1, shift: "extend", include: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:109:1", 6, String.raw` + abcdefghijk + ^^ 0 + `); + }); + + test("2 > extend-to-g-included-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "g", direction: -1, shift: "extend", include: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:119:1", 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + }); + + test("2 > extend-to-d-included-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "d", direction: -1, shift: "extend", include: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:131:1", 6, String.raw` + abcdefghijk + ^ 0 + `); + }); + + test("2 > extend-to-b-included-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "b", direction: -1, shift: "extend", include: true }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:141:1", 6, String.raw` + abcdefghijk + |^ 0 + `); + }); + + test("2 > extend-to-b-backward-character", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.seek", { input: "b", direction: -1, shift: "extend", include: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:151:1", 6, String.raw` + abcdefghijk + |^^ 0 + `); + }); + + test("2 > extend-to-g-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "g", direction: -1, shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:163:1", 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + }); + + test("2 > extend-to-f-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "f", direction: -1, shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:175:1", 6, String.raw` + abcdefghijk + ^^^ 0 + `); + }); + + test("2 > extend-to-e-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "e", direction: -1, shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:185:1", 6, String.raw` + abcdefghijk + ^^ 0 + `); + }); + + test("2 > extend-to-c-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "c", direction: -1, shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:195:1", 6, String.raw` + abcdefghijk + | 0 + `); + }); + + test("2 > extend-to-b-backward", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + abcdefghijk + ^^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.seek", { input: "b", direction: -1, shift: "extend" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/seek.md:205:1", 6, String.raw` + abcdefghijk + ^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/select b/test/suite/commands/select deleted file mode 100644 index 1c9d325..0000000 --- a/test/suite/commands/select +++ /dev/null @@ -1,232 +0,0 @@ -{0}f|{0}oo -bar -baz - -//== 0 > wholebuffer -//= dance.select.buffer -{0}foo -bar -baz|{0} - -//== 0 > 1 -//= dance.select.line -{0}foo -|{0}bar -baz - -//== 0 > 1.x -//= dance.select.line.extend -{0}foo -|{0}bar -baz - -//== 1 > 2 -//= dance.select.line -foo -{0}bar -|{0}baz - -//== 1.x > 2.x -//= dance.select.line.extend -{0}foo -bar -|{0}baz - -//== 4 -hel{0}lo -world - m|{0}y - friends, - and welcome - -//== 4 > 4.select -//= dance.select.line -hello -world -{0} my -|{0} friends, - and welcome - -//== 4 > 4.extend -//= dance.select.line.extend -hel{0}lo -world - my -|{0} friends, - and welcome - -//== 4 > 4.select.withcount -//= dance.count.2 -//= dance.select.line -hello -world - my -{0} friends, -|{0} and welcome - -//== 4 > 4.extend.withcount -//= dance.count.2 -//= dance.select.line.extend -hel{0}lo -world - my - friends, -|{0} and welcome - -//== 5 -he{0}llo -|{0}world - -my -friend - -//== 5 > 5.select.line -//= dance.select.line -// The full line is not yet selected, so select it. -{0}hello -|{0}world - -my -friend - -//== 5 > 5.select.line.twice -//= dance.count.2 -//= dance.select.line -// First select the full first line, then the next line. -hello -{0}world -|{0} -my -friend - -//== 5.select.line.twice > 5.select.line.emptyLine -//= dance.select.line -// An empty line is selected now. -hello -world -{0} -|{0}my -friend - -//== 5.select.line.emptyLine > 5.x -//= dance.select.line -hello -world - -{0}my -|{0}friend - -//== 6 -// The full line is selected, but in reverse direction. -|{0}hello -{0}world - -//== 6 > 6.select.line -//= dance.select.line -// The selection should be reversed without moving on. -{0}hello -|{0}world - -//== 6 > 6.select.line.extend -//= dance.select.line.extend -// Anchor is unchanged but active moved to the line break, thus selecting it. -hello{0} -|{0}world - -//== 6.select.line.extend > 6.select.line.extend.again -//= dance.select.line.extend -// Special case: Anchor (instead of active) is moved to the line start, thus -// selecting the full line in forward direction. -{0}hello -|{0}world - -//== 7 -|{0}hello -w{0}orld - -//== 7 > 7.select.line.extend -//= dance.select.line.extend -// The special case above does not apply if anchor is on a different line. -hello{0} -w|{0}orld - -//== 7.select.line.extend > 7.select.line.extend.again -//= dance.select.line.extend -// So the selection will get stuck there with more select.line.extend commands. -hello{0} -w|{0}orld - -//== 8 -h{0}e|{0}llo -world -m|{1}y dear{1} -friends - -//== 8 > 8.trim -//= dance.trimLines -// Neither selection contains a full line but deleting both would eliminate -// all selections. Thus leave everything unchanged. -h{0}e|{0}llo -world -m|{1}y dear{1} -friends - -//== 8 > 8.expand -//= dance.expandLines -{0}hello -|{0}world -|{1}my dear -{1}friends - -//== 8.expand > 8.expand.again -//= dance.expandLines -// No changes, each selection is already a full line. -{0}hello -|{0}world -|{1}my dear -{1}friends - -//== 9 -h|{0}ello -wo{0}rld -my{1} -dear -fri|{1}ends - -//== 9 > 9.expand -//= dance.expandLines -// Selection 1 is at document end since there is no trailing line break. -// VSCode will take care of merging the two selections next. -|{0}hello -world -{0}{1}my -dear -friends|{1} - -//== 9 > 9.trim -//= dance.trimLines -// Old Selection 0 disappears because it contains no full lines. -hello -world -my -{0}dear -|{0}friends - -//== line.0 -the quick {0}bro|{0}wn fox - -//== line.0 > line.1 -//= dance.select.toLineBegin -|{0}the quick bro{0}wn fox - -//== line.0 > line.2 -//= dance.select.toLineBegin.extend -|{0}the quick b{0}rown fox - -//== line.0 > line.3 -//= dance.select.toLineEnd -the quick br{0}own fox|{0} - -//== line.0 > line.4 -//= dance.select.toLineEnd.extend -the quick {0}brown fox|{0} diff --git a/test/suite/commands/select-lateral.md b/test/suite/commands/select-lateral.md new file mode 100644 index 0000000..dbdff4d --- /dev/null +++ b/test/suite/commands/select-lateral.md @@ -0,0 +1,258 @@ +# 1 + +> behavior <- character + +``` +foo +bar + ^ 0 +baz +quxxx +``` + +## 1 left +[up](#1) + +- .select.left.jump + +``` +foo +bar + ^ 0 +baz +quxxx +``` + +## 1 right +[up](#1) + +- .select.right.jump + +``` +foo +bar +baz +^ 0 +quxxx +``` + +## 1 up +[up](#1) + +- .select.up.jump + +``` +foo + ^ 0 +bar +baz +quxxx +``` + +## 1 up-skip-eol +[up](#1) + +- .select.up.jump { avoidEol: true } + +When at line end, moving to a different line will always select the last +character instead of line end. "Desired column" is set to `len + 1` (= 4) +though. + +``` +foo + ^ 0 +bar +baz +quxxx +``` + +## 1 down +[up](#1) + +- .select.down.jump + +``` +foo +bar +baz + ^ 0 +quxxx +``` + +## 1 down-skip-eol +[up](#1) + +- .select.down.jump { avoidEol: true } + +Similarly to the [test case above](#1-up-skip-eol), "desired column" is 4 so we +select the last character. + +``` +foo +bar +baz + ^ 0 +quxxx +``` + +## 1 down-skip-eol-2 +[up](#1) + +- .select.down.jump { count: 2, avoidEol: true } + +As explained above, the 4th character should be selected because it's on the +desired column. + +``` +foo +bar +baz +quxxx + ^ 0 +``` + +# 2 + +> behavior <- character + +``` +foo + +bar + ^ 0 + +``` + +## 2 up +[up](#2) + +- .select.up.jump + +The second line is blank, so we will select its line break. + +``` +foo + +^ 0 +bar + +``` + +## 2 up-skip-eol-2 +[up](#2) + +- .select.up.jump { count: 2, avoidEol: true } + +``` +foo + ^ 0 + +bar + +``` + +# 3 + +> behavior <- character + +``` +foo + +^ 0 +bar +baz +``` + +## 3 left +[up](#3) + +- .select.left.jump + +``` +foo + ^ 0 + +bar +baz +``` + +## 3 right +[up](#3) + +- .select.right.jump + +``` +foo + +bar +^ 0 +baz +``` + +## 3 up +[up](#3) + +- .select.up.jump + +``` +foo +^ 0 + +bar +baz +``` + +## 3 down +[up](#3) + +- .select.down.jump + +``` +foo + +bar +^ 0 +baz +``` + +### 3 down up +[up](#3-down) + +- .select.up.jump + +``` +foo + +^ 0 +bar +baz +``` + +### 3 down up-extend +[up](#3-down) + +- .select.up.extend + +``` +foo + +| 0 +bar +^ 0 +baz +``` + +#### 3 down up-extend x +[up](#3-down-up-extend) + +- .select.up.extend + +``` +foo +| 0 + +^ 0 +bar +^ 0 +baz +``` diff --git a/test/suite/commands/select-lateral.test.ts b/test/suite/commands/select-lateral.test.ts new file mode 100644 index 0000000..e807ad2 --- /dev/null +++ b/test/suite/commands/select-lateral.test.ts @@ -0,0 +1,426 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/select-lateral.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > left", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + bar + ^ 0 + baz + quxxx + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.left.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:13:1", 6, String.raw` + foo + bar + ^ 0 + baz + quxxx + `); + }); + + test("1 > right", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + bar + ^ 0 + baz + quxxx + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.right.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:26:1", 6, String.raw` + foo + bar + baz + ^ 0 + quxxx + `); + }); + + test("1 > up", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + bar + ^ 0 + baz + quxxx + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.up.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:39:1", 6, String.raw` + foo + ^ 0 + bar + baz + quxxx + `); + }); + + test("1 > up-skip-eol", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + bar + ^ 0 + baz + quxxx + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.up.jump", { avoidEol: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:52:1", 6, String.raw` + foo + ^ 0 + bar + baz + quxxx + `); + }); + + test("1 > down", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + bar + ^ 0 + baz + quxxx + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.down.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:69:1", 6, String.raw` + foo + bar + baz + ^ 0 + quxxx + `); + }); + + test("1 > down-skip-eol", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + bar + ^ 0 + baz + quxxx + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.down.jump", { avoidEol: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:82:1", 6, String.raw` + foo + bar + baz + ^ 0 + quxxx + `); + }); + + test("1 > down-skip-eol-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + bar + ^ 0 + baz + quxxx + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.down.jump", { count: 2, avoidEol: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:98:1", 6, String.raw` + foo + bar + baz + quxxx + ^ 0 + `); + }); + + test("2 > up", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + bar + ^ 0 + + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.up.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:126:1", 6, String.raw` + foo + + ^ 0 + bar + + `); + }); + + test("2 > up-skip-eol-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + bar + ^ 0 + + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.up.jump", { count: 2, avoidEol: true }); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:141:1", 6, String.raw` + foo + ^ 0 + + bar + + `); + }); + + test("3 > left", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + ^ 0 + bar + baz + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.left.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:166:1", 6, String.raw` + foo + ^ 0 + + bar + baz + `); + }); + + test("3 > right", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + ^ 0 + bar + baz + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.right.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:179:1", 6, String.raw` + foo + + bar + ^ 0 + baz + `); + }); + + test("3 > up", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + ^ 0 + bar + baz + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.up.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:192:1", 6, String.raw` + foo + ^ 0 + + bar + baz + `); + }); + + test("3 > down", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + ^ 0 + bar + baz + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.down.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:205:1", 6, String.raw` + foo + + bar + ^ 0 + baz + `); + }); + + test("3 > down > up", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + bar + ^ 0 + baz + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.up.jump"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:218:1", 6, String.raw` + foo + + ^ 0 + bar + baz + `); + }); + + test("3 > down > up-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + bar + ^ 0 + baz + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.up.extend"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:231:1", 6, String.raw` + foo + + | 0 + bar + ^ 0 + baz + `); + }); + + test("3 > down > up-extend > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + + | 0 + bar + ^ 0 + baz + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.up.extend"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lateral.md:245:1", 6, String.raw` + foo + | 0 + + ^ 0 + bar + ^ 0 + baz + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/select-line-end.md b/test/suite/commands/select-line-end.md new file mode 100644 index 0000000..a86801e --- /dev/null +++ b/test/suite/commands/select-line-end.md @@ -0,0 +1,70 @@ +# 1 + +``` +the quick brown fox + ^^^ 0 +``` + +## 1 line-start +[up](#1) + +- .select.lineStart + +``` +the quick brown fox +|^^^^^^^^^^^^ 0 +``` + +## 1 line-start-extend +[up](#1) + +- .select.lineStart.extend + +``` +the quick brown fox +|^^^^^^^^^ 0 +``` + +## 1 line-start-extend-character +[up](#1) + +> behavior <- character + +- .select.lineStart.extend + +``` +the quick brown fox +|^^^^^^^^^^ 0 +``` + +## 1 line-end +[up](#1) + +- .select.lineEnd + +``` +the quick brown fox + ^^^^^^ 0 +``` + +## 1 line-end-character +[up](#1) + +> behavior <- character + +- .select.lineEnd + +``` +the quick brown fox + ^^^^^^^ 0 +``` + +## 1 line-end-extend +[up](#1) + +- .select.lineEnd.extend + +``` +the quick brown fox + ^^^^^^^^^ 0 +``` diff --git a/test/suite/commands/select-line-end.test.ts b/test/suite/commands/select-line-end.test.ts new file mode 100644 index 0000000..2015760 --- /dev/null +++ b/test/suite/commands/select-line-end.test.ts @@ -0,0 +1,128 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/select-line-end.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > line-start", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.select.lineStart"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-line-end.md:8:1", 6, String.raw` + the quick brown fox + |^^^^^^^^^^^^ 0 + `); + }); + + test("1 > line-start-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.select.lineStart.extend"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-line-end.md:18:1", 6, String.raw` + the quick brown fox + |^^^^^^^^^ 0 + `); + }); + + test("1 > line-start-extend-character", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.lineStart.extend"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-line-end.md:28:1", 6, String.raw` + the quick brown fox + |^^^^^^^^^^ 0 + `); + }); + + test("1 > line-end", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.select.lineEnd"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-line-end.md:40:1", 6, String.raw` + the quick brown fox + ^^^^^^ 0 + `); + }); + + test("1 > line-end-character", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "character" }); + await executeCommand("dance.select.lineEnd"); + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-line-end.md:50:1", 6, String.raw` + the quick brown fox + ^^^^^^^ 0 + `); + }); + + test("1 > line-end-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + the quick brown fox + ^^^ 0 + `); + + // Perform all operations. + await executeCommand("dance.select.lineEnd.extend"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-line-end.md:62:1", 6, String.raw` + the quick brown fox + ^^^^^^^^^ 0 + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/select-lines.md b/test/suite/commands/select-lines.md new file mode 100644 index 0000000..f017418 --- /dev/null +++ b/test/suite/commands/select-lines.md @@ -0,0 +1,350 @@ +# 1 + +``` +foo +^ 0 +bar +baz +``` + +## 1 whole-buffer +[up](#1) + +- .select.buffer + +``` +foo +^^^^ 0 +bar +^^^^ 0 +baz +^^^ 0 +``` + +## 1 select-line +[up](#1) + +- .select.line.below + +``` +foo +^^^^ 0 +bar +baz +``` + +### 1 select-line x +[up](#1-select-line) + +- .select.line.below + +``` +foo +bar +^^^^ 0 +baz +``` + +## 1 extend-line +[up](#1) + +- .select.line.below.extend + +``` +foo +^^^^ 0 +bar +baz +``` + +### 1 extend-line x +[up](#1-extend-line) + +- .select.line.below.extend + +``` +foo +^^^^ 0 +bar +^^^^ 0 +baz +``` + +# 2 + +``` +hello + ^^^ 0 +world +^^^^^^ 0 + my +^^^ 0 + friends, + and welcome +``` + +## 2 line +[up](#2) + +- .select.line.below + +``` +hello +world + my +^^^^^ 0 + friends, + and welcome +``` + +## 2 line-extend +[up](#2) + +- .select.line.below.extend + +``` +hello + ^^^ 0 +world +^^^^^^ 0 + my +^^^^^ 0 + friends, + and welcome +``` + +## 2 line-2 +[up](#2) + +- .select.line.below { count: 2 } + +``` +hello +world + my + friends, +^^^^^^^^^^^^^ 0 + and welcome +``` + +## 2 line-extend-2 +[up](#2) + +- .select.line.below.extend { count: 2 } + +``` +hello + ^^^ 0 +world +^^^^^^ 0 + my +^^^^^ 0 + friends, +^^^^^^^^^^^^^ 0 + and welcome +``` + +# 3 + +``` +hello + ^^^^ 0 +world + +my +friend +``` + +## 3 line +[up](#3) + +- .select.line.below + +The full line is not yet selected, so select it. + +``` +hello +^^^^^^ 0 +world + +my +friend +``` + +## 3 line-2 +[up](#3) + +- .select.line.below { count: 2 } + +First select the full first line, then the next line. + +``` +hello +world +^^^^^^ 0 + +my +friend +``` + +### 3 line-2 line +[up](#3-line-2) + +- .select.line.below + +An empty line is selected now. + +``` +hello +world + +^ 0 +my +friend +``` + +#### 3 line-2 line x +[up](#3-line-2-line) + +- .select.line.below + +``` +hello +world + +my +^^^ 0 +friend +``` + +# 4 + +The full line is selected, but in reverse direction. + +``` +hello +|^^^^^ 0 +world +``` + +## 4 line +[up](#4) + +- .select.line.below + +The selection should be reversed without moving on. + +``` +hello +^^^^^^ 0 +world +``` + +# 5 + +``` +hello +|^^^^^ 0 +world +^ 0 +``` + +## 5 line-extend +[up](#5) + +- .select.line.below.extend + +The special case above does not apply if anchor is on a different line. + +``` +hello +world +^ 0 +``` + +### 5 line-extend x +[up](#5-line-extend) + +- .select.line.below.extend + +``` +hello +world +^^^^^ 0 +``` + +# 6 + +``` +foo + | 0 +bar +baz +quux +``` + +## 6 line +[up](#6) + +- .select.line.below + +``` +foo +^^^^ 0 +bar +baz +quux +``` + +### 6 line x +[up](#6-line) + +- .select.line.below + +``` +foo +bar +^^^^ 0 +baz +quux +``` + +#### 6 line x x +[up](#6-line-x) + +- .select.line.below + +``` +foo +bar +baz +^^^^ 0 +quux +``` + +### 6 line line-extend +[up](#6-line) + +- .select.line.below.extend + +``` +foo +^^^^ 0 +bar +^^^^ 0 +baz +quux +``` + +## 6 line-extend +[up](#6) + +- .select.line.below.extend + +``` +foo +^^^^ 0 +bar +baz +quux +``` diff --git a/test/suite/commands/select-lines.test.ts b/test/suite/commands/select-lines.test.ts new file mode 100644 index 0000000..dc5c0e3 --- /dev/null +++ b/test/suite/commands/select-lines.test.ts @@ -0,0 +1,517 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/select-lines.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > whole-buffer", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^ 0 + bar + baz + `); + + // Perform all operations. + await executeCommand("dance.select.buffer"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:10:1", 6, String.raw` + foo + ^^^^ 0 + bar + ^^^^ 0 + baz + ^^^ 0 + `); + }); + + test("1 > select-line", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^ 0 + bar + baz + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:24:1", 6, String.raw` + foo + ^^^^ 0 + bar + baz + `); + }); + + test("1 > select-line > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^^^^ 0 + bar + baz + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:36:1", 6, String.raw` + foo + bar + ^^^^ 0 + baz + `); + }); + + test("1 > extend-line", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^ 0 + bar + baz + `); + + // Perform all operations. + await executeCommand("dance.select.line.below.extend"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:48:1", 6, String.raw` + foo + ^^^^ 0 + bar + baz + `); + }); + + test("1 > extend-line > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^^^^ 0 + bar + baz + `); + + // Perform all operations. + await executeCommand("dance.select.line.below.extend"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:60:1", 6, String.raw` + foo + ^^^^ 0 + bar + ^^^^ 0 + baz + `); + }); + + test("2 > line", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + ^^^ 0 + world + ^^^^^^ 0 + my + ^^^ 0 + friends, + and welcome + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:86:1", 6, String.raw` + hello + world + my + ^^^^^ 0 + friends, + and welcome + `); + }); + + test("2 > line-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + ^^^ 0 + world + ^^^^^^ 0 + my + ^^^ 0 + friends, + and welcome + `); + + // Perform all operations. + await executeCommand("dance.select.line.below.extend"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:100:1", 6, String.raw` + hello + ^^^ 0 + world + ^^^^^^ 0 + my + ^^^^^ 0 + friends, + and welcome + `); + }); + + test("2 > line-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + ^^^ 0 + world + ^^^^^^ 0 + my + ^^^ 0 + friends, + and welcome + `); + + // Perform all operations. + await executeCommand("dance.select.line.below", { count: 2 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:116:1", 6, String.raw` + hello + world + my + friends, + ^^^^^^^^^^^^^ 0 + and welcome + `); + }); + + test("2 > line-extend-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + ^^^ 0 + world + ^^^^^^ 0 + my + ^^^ 0 + friends, + and welcome + `); + + // Perform all operations. + await executeCommand("dance.select.line.below.extend", { count: 2 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:130:1", 6, String.raw` + hello + ^^^ 0 + world + ^^^^^^ 0 + my + ^^^^^ 0 + friends, + ^^^^^^^^^^^^^ 0 + and welcome + `); + }); + + test("3 > line", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + ^^^^ 0 + world + + my + friend + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:158:1", 6, String.raw` + hello + ^^^^^^ 0 + world + + my + friend + `); + }); + + test("3 > line-2", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + ^^^^ 0 + world + + my + friend + `); + + // Perform all operations. + await executeCommand("dance.select.line.below", { count: 2 }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:174:1", 6, String.raw` + hello + world + ^^^^^^ 0 + + my + friend + `); + }); + + test("3 > line-2 > line", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + world + ^^^^^^ 0 + + my + friend + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:190:1", 6, String.raw` + hello + world + + ^ 0 + my + friend + `); + }); + + test("3 > line-2 > line > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + world + + ^ 0 + my + friend + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:206:1", 6, String.raw` + hello + world + + my + ^^^ 0 + friend + `); + }); + + test("4 > line", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + |^^^^^ 0 + world + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:230:1", 6, String.raw` + hello + ^^^^^^ 0 + world + `); + }); + + test("5 > line-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + |^^^^^ 0 + world + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.select.line.below.extend"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:252:1", 6, String.raw` + hello + world + ^ 0 + `); + }); + + test("5 > line-extend > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + world + ^ 0 + `); + + // Perform all operations. + await executeCommand("dance.select.line.below.extend"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:265:1", 6, String.raw` + hello + world + ^^^^^ 0 + `); + }); + + test("6 > line", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + | 0 + bar + baz + quux + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:286:1", 6, String.raw` + foo + ^^^^ 0 + bar + baz + quux + `); + }); + + test("6 > line > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^^^^ 0 + bar + baz + quux + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:299:1", 6, String.raw` + foo + bar + ^^^^ 0 + baz + quux + `); + }); + + test("6 > line > x > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + bar + ^^^^ 0 + baz + quux + `); + + // Perform all operations. + await executeCommand("dance.select.line.below"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:312:1", 6, String.raw` + foo + bar + baz + ^^^^ 0 + quux + `); + }); + + test("6 > line > line-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^^^^ 0 + bar + baz + quux + `); + + // Perform all operations. + await executeCommand("dance.select.line.below.extend"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:325:1", 6, String.raw` + foo + ^^^^ 0 + bar + ^^^^ 0 + baz + quux + `); + }); + + test("6 > line-extend", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + | 0 + bar + baz + quux + `); + + // Perform all operations. + await executeCommand("dance.select.line.below.extend"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/select-lines.md:339:1", 6, String.raw` + foo + ^^^^ 0 + bar + baz + quux + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/select.copy b/test/suite/commands/select.copy deleted file mode 100644 index f629097..0000000 --- a/test/suite/commands/select.copy +++ /dev/null @@ -1,80 +0,0 @@ -{0}f|{0}oo -bar -baz -qux - -//== 0 > 1 -//= dance.select.copy -{0}f|{0}oo -{1}b|{1}ar -baz -qux - -//== 1 > 2 -//= dance.select.copy -{0}f|{0}oo -{1}b|{1}ar -{2}b|{2}az -qux - -//== 10 -aaa aaa aaa - b{0}b|{0} bb bb {1}bb|{1} - cc cc cc cc - ddd - ee - f - gg gg gg gg gg - -//== 10 > 11 -//= dance.select.copy -// Basic copy with multiple selections. -aaa aaa aaa - b{0}b|{0} bb bb {1}bb|{1} - {2} |{2}cc cc c{3}c |{3}cc - ddd - ee - f - gg gg gg gg gg - -//== 11 > 12 -//= dance.select.copy -// Skip a line because it's too short. -aaa aaa aaa - b{0}b|{0} bb bb {1}bb|{1} - {2} |{2}cc cc c{3}c |{3}cc - {4} |{4} ddd - ee - f - gg gg gg {5}gg|{5} gg - -//== 12 > 13 -//= dance.select.copy -// Do not add selections after the end of the document. -aaa aaa aaa - b{0}b|{0} bb bb {1}bb|{1} - {2} |{2}cc cc c{3}c |{3}cc - {4} |{4} ddd - {6} |{6} ee - f - gg gg gg {5}gg|{5} gg - -//== 14 -ab{0} -|{0}cd -efg -hi - -//== 14 > 15 -//= dance.select.copy -ab{0} -|{0}cd{1} -|{1}efg -hi - -//== 15 > 16 -//= dance.select.copy -ab{0} -|{0}cd{1} -|{1}ef{2}g|{2} -hi diff --git a/test/suite/commands/select.enclosing b/test/suite/commands/select.enclosing deleted file mode 100644 index 32223da..0000000 --- a/test/suite/commands/select.enclosing +++ /dev/null @@ -1,58 +0,0 @@ -{ hello: 1, - world: { - foo: [ - [ {0}1|{0}, 2, 3, ], - ], - bar: ({1}42|{1}), - }, -} - -//== 0 > 1 -//= dance.select.enclosing -// Since 0.active is not on a brace/bracket char, find the next bracket (]) and -// then match from there, moving active to the previous matching [, selecting -// the text during the move (i.e. backwards from ] to [). Same for 1. -{ hello: 1, - world: { - foo: [ - |{0}[ 1, 2, 3, ]{0}, - ], - bar: |{1}(42){1}, - }, -} - -//== 1 > 2 -// Since active was at the opening square bracket ([), `m` again should -// keep the same selection but forwards, so active is at closing bracket (]). -//= dance.select.enclosing -{ hello: 1, - world: { - foo: [ - {0}[ 1, 2, 3, ]|{0}, - ], - bar: {1}(42)|{1}, - }, -} - -//== 3 -{0}{|{0} hello: 1, - world: { - foo: [ - [ 1, 2, 3, ], - ], - bar: (42), - }, -} - -//== 3 > 4 -//= dance.select.enclosing -// Current active was already on {, so no need to seek. Directly move to } and -// select the text along the way. -{0}{ hello: 1, - world: { - foo: [ - [ 1, 2, 3, ], - ], - bar: (42), - }, -}|{0} diff --git a/test/suite/commands/select.line.caret b/test/suite/commands/select.line.caret deleted file mode 100644 index 3b1898c..0000000 --- a/test/suite/commands/select.line.caret +++ /dev/null @@ -1,39 +0,0 @@ -f|{0}oo -bar -baz -quux - -//== 0 > 1 -//= dance.select.line -{0}foo -|{0}bar -baz -quux - -//== 0 > 1.extend -//= dance.select.line.extend -{0}foo -|{0}bar -baz -quux - -//== 1 > 2 -//= dance.select.line -foo -{0}bar -|{0}baz -quux - -//== 1 > 2.extend -//= dance.select.line.extend -{0}foo -bar -|{0}baz -quux - -//== 2 > 3 -//= dance.select.line -foo -bar -{0}baz -|{0}quux diff --git a/test/suite/commands/select.lineend.empty b/test/suite/commands/select.lineend.empty deleted file mode 100644 index b49d534..0000000 --- a/test/suite/commands/select.lineend.empty +++ /dev/null @@ -1,54 +0,0 @@ -// Initial case: The line break on an empty line is selected. -foo -{0} -|{0}bar -baz - -//== 0 > 1 -//= dance.left -foo{0} -|{0} -bar -baz - -//== 0 > 2 -//= dance.right -foo - -{0}b|{0}ar -baz - -//== 0 > 3 -//= dance.up -{0}f|{0}oo - -bar -baz - -//== 0 > 4 -//= dance.down -foo - -{0}b|{0}ar -baz - -//== 4 > 5 -//= dance.up -foo -{0} -|{0}bar -baz - -//== 4 > 6 -//= dance.up.extend -foo -|{0} -b{0}ar -baz - -//== 6 > 7 -//= dance.up.extend -|{0}foo - -b{0}ar -baz diff --git a/test/suite/commands/select.to b/test/suite/commands/select.to deleted file mode 100644 index edc1bfe..0000000 --- a/test/suite/commands/select.to +++ /dev/null @@ -1,83 +0,0 @@ -{0}a|{0}bcabc - -//== 0 > 1 -//= dance.select.to.included -//= type:c -{0}abc|{0}abc - -//== 1 > 2 -//= dance.select.to.excluded -//= type:c -ab{0}cab|{0}c - -//== 0 > 3 -//= dance.count.2 -//= dance.select.to.excluded -//= type:c -{0}abcab|{0}c - -//== 3 > 4 -//= dance.select.to.excluded -//= type:c -abca{0}b|{0}c - -//== 4 > 0.back -//= dance.select.to.excluded.backwards -//= type:b -ab|{0}cab{0}c - -//== 0.back > 1.back -//= dance.select.to.included.backwards -//= type:a -|{0}abc{0}abc - -//== 100 -abc{0}defg|{0}hijk - -//== 100 > 101 -//= dance.select.to.included.extend.backwards -//= type:e -abc{0}de|{0}fghijk - -//== 100 > 102 -//= dance.select.to.included.extend.backwards -//= type:g -// Selection left unchanged since it can't find another 'g' before this. -abc{0}defg|{0}hijk - -//== 100 > 103 -//= dance.select.to.included.extend.backwards -//= type:d -abc{0}d|{0}efghijk - -//== 100 > 104 -//= dance.select.to.included.extend.backwards -//= type:b -a|{0}bcd{0}efghijk - -//== 100 > 105 -//= dance.select.to.excluded.extend.backwards -//= type:g -// Selection left unchanged since it can't find another 'g' before this. -abc{0}defg|{0}hijk - -//== 100 > 106 -//= dance.select.to.excluded.extend.backwards -//= type:f -// 'g' is already the character after 'f'. No changes. -abc{0}defg|{0}hijk - -//== 100 > 107 -//= dance.select.to.excluded.extend.backwards -//= type:e -abc{0}def|{0}ghijk - -//== 100 > 108 -//= dance.select.to.excluded.extend.backwards -//= type:c -abc{0}d|{0}efghijk - -//== 100 > 109 -//= dance.select.to.excluded.extend.backwards -//= type:b -ab|{0}cd{0}efghijk diff --git a/test/suite/commands/select.word b/test/suite/commands/select.word deleted file mode 100644 index 21fc603..0000000 --- a/test/suite/commands/select.word +++ /dev/null @@ -1,89 +0,0 @@ -|{0}c{0}onsole.log() - -//== 0 > 1 -//= dance.select.word.end -{0}console|{0}.log() - -//== 0 > 2 -//= dance.select.word.end.extend -{0}console|{0}.log() - -//== 1 > 3 -//= dance.select.word.end -console{0}.|{0}log() - -//== 2 > 4 -//= dance.select.word.end.extend -{0}console.|{0}log() - -//== 3 > 5 -//= dance.select.word.end -console.{0}log|{0}() - -//== 4 > 6 -//= dance.select.word.end.extend -{0}console.log|{0}() - -//== 4 > 7 -//= dance.select.word.end -console.{0}log|{0}() - -//== 5 > prev.0 -//= dance.select.word.previous -console.|{0}log{0}() - -//== prev.0 > prev.1 -//= dance.select.word.previous -console|{0}.{0}log() - -//== prev.1 > prev.2 -//= dance.select.word.previous -|{0}console{0}.log() - -//== prev.3 -foo - -{0}b|{0}ar - -//== prev.3 > prev.4 -//= dance.select.word.previous -|{0}foo{0} - -bar - -//== spaced.0 -aaa bb{0}b|{0} ccc ddd - -//== spaced.0 > spaced.1 -//= dance.select.word.end -aaa bbb{0} ccc|{0} ddd - -//== spaced.1 > spaced.2 -//= dance.select.word.end -aaa bbb ccc{0} ddd|{0} - -//== spaced.2 > spaced.3 -//= dance.select.word.previous -aaa bbb ccc |{0}ddd{0} - -//== spaced.2 > spaced.4 -//= dance.count.2 -//= dance.select.word.previous -aaa bbb |{0}ccc {0}ddd - -//== break.0 -aaa{0} |{0}bbb - ccc -dd - -//== break.0 > break.1 -//= dance.select.word -aaa {0}bbb|{0} - ccc -dd - -//== break.1 > break.2 -//= dance.select.word.end -aaa bbb -{0} ccc|{0} -dd diff --git a/test/suite/commands/select.word.1 b/test/suite/commands/select.word.1 deleted file mode 100644 index 7837c65..0000000 --- a/test/suite/commands/select.word.1 +++ /dev/null @@ -1,22 +0,0 @@ -{0}f|{0}oo x bar.baz ex -la - -//== 0 > 1 -//= dance.select.word.end -{0}foo|{0} x bar.baz ex -la - -//== 1 > 2 -//= dance.select.word.end -foo{0} x|{0} bar.baz ex -la - -//== 2 > 3 -//= dance.select.word.end -foo x{0} bar|{0}.baz ex -la - -//== 3 > 4 -//= dance.select.word.end -foo x bar{0}.|{0}baz ex -la diff --git a/test/suite/commands/select.word.2 b/test/suite/commands/select.word.2 deleted file mode 100644 index 40804b8..0000000 --- a/test/suite/commands/select.word.2 +++ /dev/null @@ -1,25 +0,0 @@ -a {0}b|{0} c d - -//== 0 > 1 -//= dance.select.word.end -a b{0} c|{0} d - -//== 0 > 2 -//= dance.select.word -a b{0} |{0}c d - -//== 0 > 3 -//= dance.select.word.previous -|{0}a {0}b c d - -//== 0 > 4 -//= dance.select.word.end.extend -a {0}b c|{0} d - -//== 0 > 5 -//= dance.select.word.extend -a {0}b |{0}c d - -//== 0 > 6 -//= dance.select.word.previous.extend -|{0}a b{0} c d diff --git a/test/suite/commands/select.word.caret b/test/suite/commands/select.word.caret deleted file mode 100644 index eb27355..0000000 --- a/test/suite/commands/select.word.caret +++ /dev/null @@ -1,14 +0,0 @@ -pri|{0}vate String foo; - -//== 0 > 1 -//= dance.select.word.end -pri{0}vate|{0} String foo; - -//== 1 > 2 -//= dance.select.word.end -private{0} String|{0} foo; - -//== 0 > 3 -//= dance.count.2 -//= dance.select.word.end -private{0} String|{0} foo; diff --git a/test/suite/commands/select.word.edge b/test/suite/commands/select.word.edge deleted file mode 100644 index 54df2ea..0000000 --- a/test/suite/commands/select.word.edge +++ /dev/null @@ -1,130 +0,0 @@ -|{0}th{0}e quick brown fox - -//== 0 > 1 -//= dance.select.word.previous -// No more selections remaining, just keep the last one. -|{0}th{0}e quick brown fox - -//== 0 > 2 -//= dance.count.4 -//= dance.select.word -the quick brown {0}fox|{0} - -//== 2 > 3 -//= dance.select.word -// No more selections remaining, just keep the last one. -the quick brown {0}fox|{0} - -//== 3 > 3.repeat -//= dance.select.word -// No more selections remaining, do not change. -the quick brown {0}fox|{0} - -//== 0 > 4 -//= dance.count.5 -//= dance.select.word -// Move 4 times, but don't move again (no more selections remaining otherwise). -the quick brown {0}fox|{0} - -//== 2 > 5 -//= dance.count.4 -//= dance.select.word.previous -|{0}the {0}quick brown fox - -//== 2 > 6 -//= dance.count.5 -//= dance.select.word.previous -// Move 4 times, but don't move again (no more selections remaining otherwise). -|{0}the {0}quick brown fox - -//== 100 -foo bar{0} -|{0}baz - -//== 100 > 101 -//= dance.select.word.previous -foo |{0}bar{0} -baz - -//== multi.0 -|{0}the {0}qu|{1}ic{1}k brown fox - -//== multi.0 > multi.1 -//= dance.select.word.previous -// Old Selection 0 overflowed and was removed. Old Selection 1 moved. -the |{0}qui{0}ck brown fox - -//== multi.0 > multi.2 -//= dance.count.9 -//= dance.select.word.previous -// Both overflowed and both falled back to the selection below. -// VSCode will then automatically merge the two selections since they overlap. -|{0}|{1}the {0}{1}quick brown fox - -//== multi.0 > multi.3 -//= dance.count.4 -//= dance.select.word.end -// Old Selection 1 overflowed and was removed. Old Selection 0 moved. -the quick brown{0} fox|{0} - -//== multi.0 > multi.4 -//= dance.count.5 -//= dance.select.word.end -// Both overflowed and both falled back to the selection below. -// VSCode will then automatically merge the two selections since they overlap. -the quick brown{0}{1} fox|{0}|{1} - -//== blank.start.0 - -|{0}there{0} is a blank line before me - -//== blank.start.0 > blank.start.1 -//= dance.select.word.previous -// Special case in Kak: anchor is moved to beginning of document and active is -// moved to the first character of the second line. -{0} -t|{0}here is a blank line before me - -//== blank.start.1 > blank.start.2 -//= dance.select.word.previous -// Going to previous again will just keep the selection the same. -{0} -t|{0}here is a blank line before me - -//== blank.start.0 > blank.start.3 -//= dance.count.9 -//= dance.select.word.previous -// Similarly, more repetitions won't do anything either. -{0} -t|{0}here is a blank line before me - -//== blank.start.4 - - -|{0}there{0} are two blank lines before me - -//== blank.start.4 > blank.start.5 -//= dance.select.word.previous -// Special case in Kak: anchor is moved to beginning of document and active is -// moved to the first character (line break in this case) of the second line. -{0} - -|{0}there are two blank lines before me - -//== blank.start.5 > blank.start.6 -//= dance.select.word.previous -// Going to previous again will just keep the selection the same. -{0} - -|{0}there are two blank lines before me - -//== blank.start.4 > blank.start.7 -//= dance.count.9 -//= dance.select.word.previous -// Similarly, more repetitions won't do anything either. -{0} - -|{0}there are two blank lines before me - -// TODO: Write tests for blank lines in the end. -// Note that {EOL} can be used to create trailing blank lines. diff --git a/test/suite/commands/selections-copy.md b/test/suite/commands/selections-copy.md new file mode 100644 index 0000000..3d5e83f --- /dev/null +++ b/test/suite/commands/selections-copy.md @@ -0,0 +1,152 @@ +# 1 + +``` +foo +^ 0 +bar +baz +qux +``` + +## 1 copy +[up](#1) + +- .selections.copy + +``` +foo +^ 1 +bar +^ 0 +baz +qux +``` + +### 1 copy x +[up](#1-copy) + +- .selections.copy + +``` +foo +^ 2 +bar +^ 1 +baz +^ 0 +qux +``` + +# 2 + +``` +aaa aaa aaa + bb bb bb bb + ^ 0 ^^ 1 + cc cc cc cc + ddd + ee + f + gg gg gg gg gg +``` + +## 2 copy +[up](#2) + +- .selections.copy + +Basic copy with multiple selections. + +``` +aaa aaa aaa + bb bb bb bb + ^ 2 ^^ 3 + cc cc cc cc + ^ 0 ^^ 1 + ddd + ee + f + gg gg gg gg gg +``` + +### 2 copy x +[up](#2-copy) + +- .selections.copy + +Skip a line because it's too short. + +``` +aaa aaa aaa + bb bb bb bb + ^ 4 ^^ 5 + cc cc cc cc + ^ 2 ^^ 3 + ddd + ^ 0 + ee + f + gg gg gg gg gg + ^^ 1 +``` + +#### 2 copy x x +[up](#2-copy-x) + +- .selections.copy + +Do not add selections after the end of the document. + +``` +aaa aaa aaa + bb bb bb bb + ^ 5 ^^ 6 + cc cc cc cc + ^ 3 ^^ 4 + ddd + ^ 1 + ee + ^ 0 + f + gg gg gg gg gg + ^^ 2 +``` + +# 3 + +``` +ab + ^ 0 +cd +efg +hi +``` + +## 3 copy +[up](#3) + +- .selections.copy + +``` +ab + ^ 1 +cd + ^ 0 +efg +hi +``` + +### 3 copy x +[up](#3-copy) + +- .selections.copy + +``` +ab + ^ 2 +cd + ^ 1 +efg + ^ 0 +hi +``` diff --git a/test/suite/commands/selections-copy.test.ts b/test/suite/commands/selections-copy.test.ts new file mode 100644 index 0000000..e1d5591 --- /dev/null +++ b/test/suite/commands/selections-copy.test.ts @@ -0,0 +1,221 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/selections-copy.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > copy", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^ 0 + bar + baz + qux + `); + + // Perform all operations. + await executeCommand("dance.selections.copy"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-copy.md:11:1", 6, String.raw` + foo + ^ 1 + bar + ^ 0 + baz + qux + `); + }); + + test("1 > copy > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + foo + ^ 1 + bar + ^ 0 + baz + qux + `); + + // Perform all operations. + await executeCommand("dance.selections.copy"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-copy.md:25:1", 6, String.raw` + foo + ^ 2 + bar + ^ 1 + baz + ^ 0 + qux + `); + }); + + test("2 > copy", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + aaa aaa aaa + bb bb bb bb + ^ 0 ^^ 1 + cc cc cc cc + ddd + ee + f + gg gg gg gg gg + `); + + // Perform all operations. + await executeCommand("dance.selections.copy"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-copy.md:53:1", 6, String.raw` + aaa aaa aaa + bb bb bb bb + ^ 2 ^^ 3 + cc cc cc cc + ^ 0 ^^ 1 + ddd + ee + f + gg gg gg gg gg + `); + }); + + test("2 > copy > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + aaa aaa aaa + bb bb bb bb + ^ 2 ^^ 3 + cc cc cc cc + ^ 0 ^^ 1 + ddd + ee + f + gg gg gg gg gg + `); + + // Perform all operations. + await executeCommand("dance.selections.copy"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-copy.md:72:1", 6, String.raw` + aaa aaa aaa + bb bb bb bb + ^ 4 ^^ 5 + cc cc cc cc + ^ 2 ^^ 3 + ddd + ^ 0 + ee + f + gg gg gg gg gg + ^^ 1 + `); + }); + + test("2 > copy > x > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + aaa aaa aaa + bb bb bb bb + ^ 4 ^^ 5 + cc cc cc cc + ^ 2 ^^ 3 + ddd + ^ 0 + ee + f + gg gg gg gg gg + ^^ 1 + `); + + // Perform all operations. + await executeCommand("dance.selections.copy"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-copy.md:93:1", 6, String.raw` + aaa aaa aaa + bb bb bb bb + ^ 5 ^^ 6 + cc cc cc cc + ^ 3 ^^ 4 + ddd + ^ 1 + ee + ^ 0 + f + gg gg gg gg gg + ^^ 2 + `); + }); + + test("3 > copy", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + ab + ^ 0 + cd + efg + hi + `); + + // Perform all operations. + await executeCommand("dance.selections.copy"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-copy.md:125:1", 6, String.raw` + ab + ^ 1 + cd + ^ 0 + efg + hi + `); + }); + + test("3 > copy > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + ab + ^ 1 + cd + ^ 0 + efg + hi + `); + + // Perform all operations. + await executeCommand("dance.selections.copy"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-copy.md:139:1", 6, String.raw` + ab + ^ 2 + cd + ^ 1 + efg + ^ 0 + hi + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/selections-trim.md b/test/suite/commands/selections-trim.md new file mode 100644 index 0000000..6fb6e1a --- /dev/null +++ b/test/suite/commands/selections-trim.md @@ -0,0 +1,143 @@ +# 1 + +``` + +^ 0 +there are two blank lines before me + ^ 0 | 1 + some whitespaces around me + ^ 1 +and some more words +^^^^^^^^^^^^^ 2 +finally a selection + ^^^ 3 + that contains only whitespace +^^^| 3 +``` + +## 1 trim-whitespace +[up](#1) + +- .selections.trimWhitespace + +``` + +there are two blank lines before me +^^^^^^^^^ 0 + some whitespaces around me + |^^^^^^^^^^^^^^^^^^^^^^^^^ 1 +and some more words +^^^^^^^^^^^^^ 2 +finally a selection + that contains only whitespace +``` + +# 2 + +``` +hello + ^ 0 +world +my dear + |^^^^^ 1 +friends +``` + +## 2 trim +[up](#2) + +- .selections.trimLines { $expect: /^no selections remain$/ } + +Neither selection contains a full line but deleting both would eliminate all +selections. Thus leave everything unchanged. + +``` +hello + ^ 0 +world +my dear + |^^^^^ 1 +friends +``` + +## 2 expand +[up](#2) + +- .selections.expandToLines + +``` +hello +^^^^^^ 0 +world +my dear +|^^^^^^^ 1 +friends +``` + +### 2 expand x +[up](#2-expand) + +- .selections.expandToLines + +No changes, each selection is already a full line. + +``` +hello +^^^^^^ 0 +world +my dear +|^^^^^^^ 1 +friends +``` + +# 3 + +``` +hello + |^^^^ 0 +world +^^ 0 +my + ^ 1 +dear +^^^^^ 1 +friends +^^^ 1 +``` + +## 3 expand +[up](#3) + +- .selections.expandToLines + +Selection #1 is at document end since there is no trailing line break. +VS Code will take care of merging the two selections next. + +``` +hello +|^^^^^ 0 +world +^^^^^^ 0 +my +^^^ 1 +dear +^^^^^ 1 +friends +^^^^^^^ 1 +``` + +## 3 trim +[up](#3) + +- .selections.trimLines + +Old selection #0 disappears because it contains no full lines. + +``` +hello +world +my +dear +^^^^^ 0 +friends +``` diff --git a/test/suite/commands/selections-trim.test.ts b/test/suite/commands/selections-trim.test.ts new file mode 100644 index 0000000..912bb3f --- /dev/null +++ b/test/suite/commands/selections-trim.test.ts @@ -0,0 +1,193 @@ +import * as vscode from "vscode"; + +import { executeCommand, ExpectedDocument, groupTestsByParentName } from "../utils"; + +suite("./test/suite/commands/selections-trim.md", function () { + // Set up document. + let document: vscode.TextDocument, + editor: vscode.TextEditor; + + this.beforeAll(async () => { + document = await vscode.workspace.openTextDocument(); + editor = await vscode.window.showTextDocument(document); + + await executeCommand("dance.dev.setSelectionBehavior", { mode: "normal", value: "caret" }); + }); + + this.afterAll(async () => { + await executeCommand("workbench.action.closeActiveEditor"); + }); + + test("1 > trim-whitespace", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + + ^ 0 + there are two blank lines before me + ^ 0 | 1 + some whitespaces around me + ^ 1 + and some more words + ^^^^^^^^^^^^^ 2 + finally a selection + ^^^ 3 + that contains only whitespace + ^^^| 3 + `); + + // Perform all operations. + await executeCommand("dance.selections.trimWhitespace"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-trim.md:18:1", 6, String.raw` + + there are two blank lines before me + ^^^^^^^^^ 0 + some whitespaces around me + |^^^^^^^^^^^^^^^^^^^^^^^^^ 1 + and some more words + ^^^^^^^^^^^^^ 2 + finally a selection + that contains only whitespace + `); + }); + + test("2 > trim", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + ^ 0 + world + my dear + |^^^^^ 1 + friends + `); + + // Perform all operations. + await executeCommand("dance.selections.trimLines", { $expect: /^no selections remain$/ }); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-trim.md:46:1", 6, String.raw` + hello + ^ 0 + world + my dear + |^^^^^ 1 + friends + `); + }); + + test("2 > expand", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + ^ 0 + world + my dear + |^^^^^ 1 + friends + `); + + // Perform all operations. + await executeCommand("dance.selections.expandToLines"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-trim.md:63:1", 6, String.raw` + hello + ^^^^^^ 0 + world + my dear + |^^^^^^^ 1 + friends + `); + }); + + test("2 > expand > x", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + ^^^^^^ 0 + world + my dear + |^^^^^^^ 1 + friends + `); + + // Perform all operations. + await executeCommand("dance.selections.expandToLines"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-trim.md:77:1", 6, String.raw` + hello + ^^^^^^ 0 + world + my dear + |^^^^^^^ 1 + friends + `); + }); + + test("3 > expand", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + |^^^^ 0 + world + ^^ 0 + my + ^ 1 + dear + ^^^^^ 1 + friends + ^^^ 1 + `); + + // Perform all operations. + await executeCommand("dance.selections.expandToLines"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-trim.md:108:1", 6, String.raw` + hello + |^^^^^ 0 + world + ^^^^^^ 0 + my + ^^^ 1 + dear + ^^^^^ 1 + friends + ^^^^^^^ 1 + `); + }); + + test("3 > trim", async function () { + // Set-up document to be in expected initial state. + await ExpectedDocument.apply(editor, 6, String.raw` + hello + |^^^^ 0 + world + ^^ 0 + my + ^ 1 + dear + ^^^^^ 1 + friends + ^^^ 1 + `); + + // Perform all operations. + await executeCommand("dance.selections.trimLines"); + + // Ensure document is as expected. + ExpectedDocument.assertEquals(editor, "./test/suite/commands/selections-trim.md:129:1", 6, String.raw` + hello + world + my + dear + ^^^^^ 0 + friends + `); + }); + + groupTestsByParentName(this); +}); diff --git a/test/suite/commands/trim b/test/suite/commands/trim deleted file mode 100644 index 6365b8c..0000000 --- a/test/suite/commands/trim +++ /dev/null @@ -1,18 +0,0 @@ -{0} - -there are |{0}two blank lines before me|{1} - some whitespaces around me {1} -{2}and some more|{2} words -finally a selection {3} {EOL} - |{3} that contains only whitespace - -//== 0 > 1 -//= dance.trimSelections - - -{0}there are|{0} two blank lines before me - |{1}some whitespaces around me{1} {EOL} -{2}and some more|{2} words -finally a selection {EOL} - that contains only whitespace -// which is now gone :) diff --git a/test/suite/index.ts b/test/suite/index.ts index 3a4e2e0..08a3bb7 100644 --- a/test/suite/index.ts +++ b/test/suite/index.ts @@ -1,25 +1,89 @@ -import * as path from "path"; +import * as glob from "glob"; import * as Mocha from "mocha"; -import * as glob from "glob"; +import * as path from "path"; +import { promises as fs } from "fs"; -export function run(testsRoot: string, cb: (error: any, failures?: number) => void): void { - // Create the mocha test - const mocha = new Mocha({ ui: "tdd", color: true, timeout: 0 }); +export async function run(testsRoot: string) { + // Create the mocha test. + const currentFile = (process.env.CURRENT_FILE ?? "").replace(/\\/g, "/"), + currentLine = process.env.CURRENT_LINE ? +process.env.CURRENT_LINE - 1 : undefined, + rootPath = path.join(__dirname, "../../.."), + mocha = new Mocha({ ui: "tdd", color: true, timeout: 0 }); - glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { - if (err) { - return cb(err); - } + let files = await new Promise((resolve, reject) => { + glob("**/**.test.js", { cwd: testsRoot }, (err, matches) => { + if (err) { + return reject(err); + } - // Add files to the test suite - files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); - - try { - // Run the mocha test - mocha.run((failures) => cb(null, failures)); - } catch (err) { - console.error(err); - cb(err); - } + return resolve(matches); + }); }); + + if (currentFile === "test/README.md") { + mocha.grep("ExpectedDocument#parse"); + + files = ["utils.test.js"]; + } else if (currentFile.startsWith("test/suite/commands/")) { + if (currentFile.endsWith(".md")) { + files = [path.join("commands", path.basename(currentFile, ".md") + ".test.js")]; + + if (currentLine !== undefined) { + const filePath = path.join(rootPath, "test/suite/commands", path.basename(currentFile)), + contents = await fs.readFile(filePath, "utf-8"), + lines = contents.split("\n"); + + for (let i = currentLine; i >= 0; i--) { + const line = lines[i], + match = /^#+ (.+)$/.exec(line); + + if (match !== null) { + mocha.grep(match[1] + "$"); + break; + } + } + } + } else if (currentFile.endsWith(".test.ts")) { + files = [path.join("commands", path.basename(currentFile, ".ts") + ".js")]; + } + } else if (currentFile.includes(".test.")) { + const currentFileAsJs = path.basename(currentFile).replace(/\.ts$/, ".js"); + + files = files.filter((f) => f.endsWith(currentFileAsJs)); + } else if (currentFile.startsWith("src/api/")) { + mocha.grep(currentFile.slice(8)); + + files = ["api.test.js"]; + + if (currentLine !== undefined) { + const filePath = path.join(rootPath, "src/api", currentFile.slice(8)), + contents = await fs.readFile(filePath, "utf-8"), + lines = contents.split("\n"); + let direction = -1; + + if (/^[ /]+\*/.test(lines[currentLine])) { + direction = 1; + } + + for (let i = currentLine; i >= 0 && i < lines.length; i += direction) { + const line = lines[i], + match = /^ *export function (\w+)/.exec(line); + + if (match !== null) { + mocha.grep(currentFile.slice(8) + ".+" + match[1]); + break; + } + } + } + } else if (currentFile.startsWith("src/commands")) { + files = files.filter((f) => f.startsWith("commands/")); + } else if (currentFile.length > 0) { + files = []; + } + + // Add files to the test suite. + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); + + // Run the mocha test. + await new Promise((resolve) => mocha.run(resolve)); } diff --git a/test/suite/utils.test.ts b/test/suite/utils.test.ts new file mode 100644 index 0000000..758885d --- /dev/null +++ b/test/suite/utils.test.ts @@ -0,0 +1,34 @@ +import * as assert from "assert"; +import * as fs from "fs"; +import * as vscode from "vscode"; + +import { execAll } from "./build-utils"; +import { ExpectedDocument, resolve } from "./utils"; + +function stringifySelection(selection: vscode.Selection) { + return `${selection.anchor.line}:${selection.anchor.character} → ` + + `${selection.active.line}:${selection.active.character}`; +} + +suite("Testing utilities tests", function () { + suite("ExpectedDocument#parse", function () { + const readmePath = resolve("test/README.md"), + readmeContents = fs.readFileSync(readmePath, "utf-8"), + re = /^(\d+)\..*?\[(.+?)\].*?:\n( +)```\n([\s\S]+?)\n\3```/gm; + + for (const [_, n, selectionsString, indent, indentedCode] of execAll(re, readmeContents)) { + const expectedSelections = selectionsString.split(", "), + code = indentedCode.replace(new RegExp("^" + indent, "gm"), ""); + + test(`example ${n}`, function () { + const document = ExpectedDocument.parse(code), + actualSelections = document.selections.map(stringifySelection); + + const actual = actualSelections.map((s, i) => `selection #${i}: ${s}`).join("\n"), + expected = expectedSelections.map((s, i) => `selection #${i}: ${s}`).join("\n"); + + assert.strictEqual(actual, expected); + }); + } + }); +}); diff --git a/test/suite/utils.ts b/test/suite/utils.ts new file mode 100644 index 0000000..077d5ab --- /dev/null +++ b/test/suite/utils.ts @@ -0,0 +1,519 @@ +// Enhance stack traces with the TypeScript source pos instead of compiled JS. +// This is only included in tests to avoid introducing a production dependency. +import "source-map-support/register"; + +import * as assert from "assert"; +import * as Mocha from "mocha"; +import * as path from "path"; +// @ts-expect-error +import * as unexpected from "unexpected"; +import * as vscode from "vscode"; + +import { Context, Selections } from "../../src/api"; +import { SelectionBehavior } from "../../src/state/modes"; +import { Extension } from "../../src/state/extension"; + +interface Expect { + (subject: T, assertion: string, ...args: readonly any[]): { + readonly and: Expect.Continuation; + }; + + readonly it: Expect.Continuation; + + addAssertion( + pattern: string, + handler: (expect: Expect, subject: T, ...args: readonly any[]) => void, + ): void; + + addType(typeDefinition: { + name: string; + identify: (value: unknown) => boolean; + + base?: string; + inspect?: (value: T, depth: number, output: Expect.Output, inspect: Expect.Inspect) => any; + }): void; +} + +namespace Expect { + export interface Continuation { + (assertion: string, ...args: readonly any[]): { readonly and: Continuation }; + } + + export interface Inspect { + (value: any, depth: number): any; + } + + export interface Output { + text(text: string): Output; + append(_: any): Output; + } +} + +/** + * Resolves a path starting at the root of the Git repository. + */ +export function resolve(subpath: string) { + // Current path is dance/out/test/suite/utils + return path.join(__dirname, "../../..", subpath); +} + +/** + * Add depth to command-like suites for nicer reporting. + */ +export function groupTestsByParentName(toplevel: Mocha.Suite) { + for (const test of toplevel.tests) { + const parts = test.title.split(" > "), + testName = parts.pop()!, + suiteName = parts.join(" "), + suite = toplevel.suites.find((s) => s.title === suiteName) + ?? Mocha.Suite.create(toplevel, suiteName); + + suite.addTest(test); + test.title = testName; + } + + toplevel.tests.splice(0); +} + +/** + * Executes a VS Code command, attempting to better recover errors. + */ +export async function executeCommand(command: string, ...args: readonly any[]) { + const runPromiseSafely = Extension.prototype.runPromiseSafely; + + Extension.prototype.runPromiseSafely = async (f) => { + try { + return await f(); + } catch (e) { + error = e; + throw e; + } + }; + + let result: unknown, + error: unknown; + + try { + result = await vscode.commands.executeCommand(command, ...args); + } catch (e) { + if (error === undefined + || !(e instanceof Error + && e.message.startsWith("Running the contributed command") + && e.message.endsWith("failed."))) { + error = e; + } + } finally { + Extension.prototype.runPromiseSafely = runPromiseSafely; + } + + if (command.startsWith("dance") && args.length === 1 && args[0].$expect instanceof RegExp) { + assert.notStrictEqual(error, undefined, "an error was expected, but no error was raised"); + + const pattern = args[0].$expect, + message = "" + ((error as any)?.message ?? error); + + assert.match( + message, + pattern, + `error ${JSON.stringify(message)} does not match expected pattern ${pattern}`, + ); + } else if (error !== undefined) { + throw error; + } + + return result; +} + +export const expect: Expect = unexpected.clone(); + +const shortPos = (p: vscode.Position) => `${p.line}:${p.character}`; + +expect.addType({ + name: "position", + identify: (v) => v instanceof vscode.Position, + base: "object", + + inspect: (value, _, output) => { + output + .text("Position(") + .text(shortPos(value)) + .text(")"); + }, +}); + +expect.addType({ + name: "range", + identify: (v) => v instanceof vscode.Range, + base: "object", + + inspect: (value, _, output) => { + output + .text("Range(") + .text(shortPos(value.start)) + .text(" -> ") + .text(shortPos(value.end)) + .text(")"); + }, +}); + +expect.addType({ + name: "selection", + identify: (v) => v instanceof vscode.Selection, + base: "range", + + inspect: (value, _, output) => { + output + .text("Selection(") + .text(shortPos(value.anchor)) + .text(" -> ") + .text(shortPos(value.active)) + .text(")"); + }, +}); + +expect.addAssertion( + " [not] to (have|be at) coords ", + (expect, subject, line: number, character: number) => { + expect(subject, "[not] to satisfy", { line, character }); + }, +); + +expect.addAssertion( + " [not] to be empty at coords ", + (expect, subject, line: number, character: number) => { + expect(subject, "[not] to start at", new vscode.Position(line, character)) + .and("[not] to be empty"); + }, +); + +expect.addAssertion( + " [not] to be empty", + (expect, subject) => { + expect(subject, "[not] to satisfy", { isEmpty: true }); + }, +); + +expect.addAssertion( + " [not] to start at ", + (expect, subject, position: vscode.Position) => { + expect(subject, "[not] to satisfy", { start: position }); + }, +); + +expect.addAssertion( + " [not] to end at ", + (expect, subject, position: vscode.Position) => { + expect(subject, "[not] to satisfy", { end: position }); + }, +); + +expect.addAssertion( + " [not] to start at coords ", + (expect, subject, line: number, character: number) => { + expect(subject, "[not] to start at", new vscode.Position(line, character)); + }, +); + +expect.addAssertion( + " [not] to end at coords ", + (expect, subject, line: number, character: number) => { + expect(subject, "[not] to end at", new vscode.Position(line, character)); + }, +); + +expect.addAssertion( + " [not] to be reversed", + (expect, subject) => { + expect(subject, "[not] to satisfy", { isReversed: true }); + }, +); + +expect.addAssertion( + " [not] to (have anchor|be anchored) at ", + (expect, subject, position: vscode.Position) => { + expect(subject, "[not] to satisfy", { anchor: position }); + }, +); + +expect.addAssertion( + " [not] to (have cursor|be active) at ", + (expect, subject, position: vscode.Position) => { + expect(subject, "[not] to satisfy", { active: position }); + }, +); + +expect.addAssertion( + " [not] to (have anchor|be anchored) at coords ", + (expect, subject, line: number, character: number) => { + expect(subject, "[not] to be anchored at", new vscode.Position(line, character)); + }, +); + +expect.addAssertion( + " [not] to (have cursor|be active) at coords ", + (expect, subject, line: number, character: number) => { + expect(subject, "[not] to be active at", new vscode.Position(line, character)); + }, +); + +function stringifySelection(document: vscode.TextDocument, selection: vscode.Selection) { + const content = document.getText(), + startOffset = document.offsetAt(selection.start); + + if (selection.isEmpty) { + return content.slice(0, startOffset) + "|" + content.slice(startOffset); + } + + let endOffset = document.offsetAt(selection.end), + endString = selection.isReversed ? "<" : "|"; + const startString = selection.isReversed ? "|" : ">"; + + if (selection.end.character === 0) { + // Selection ends at line break. + endString = "↵" + endString; + endOffset--; + } + + return ( + content.slice(0, startOffset) + + startString + + content.slice(startOffset, endOffset) + + endString + + content.slice(endOffset) + ); +} + +export class ExpectedDocument { + public constructor( + public readonly text: string, + public readonly selections: vscode.Selection[] = [], + ) { + const lineCount = text.split("\n").length; + + for (const selection of selections) { + expect(selection.end.line, "to be less than", lineCount); + } + } + + public static apply(editor: vscode.TextEditor, indent: number, text: string) { + return this.parseIndented(indent, text).apply(editor); + } + + public static assertEquals( + editor: vscode.TextEditor, + message: string | undefined, + indent: number, + text: string, + ) { + return this.parseIndented(indent, text).assertEquals(editor, message); + } + + public static parseIndented(indent: number, text: string) { + if (text.length < indent) { + // Empty document. + return new ExpectedDocument(""); + } + + // Remove first line break. + text = text.slice(1); + + // Remove final line break (indent - (two spaces) + line break). + text = text.slice(0, text.length - (indent - 2 + 1)); + + // Remove indentation. + text = text.replace(new RegExp(`^ {${indent}}`, "gm"), ""); + + return ExpectedDocument.parse(text); + } + + public static parse(text: string) { + text = text.replace(/·/g, " "); + + const selections = [] as vscode.Selection[], + lines = [] as string[]; + let previousLineStart = 0; + + for (let line of text.split("\n")) { + let hasIndicator = false; + + line = line.replace(/([|^]+) *(\d+)|(\d+) *([|^]+)/g, (match, c1, n1, n2, c2, offset) => { + const carets = (c1 ?? c2) as string, + selectionIndex = +(n1 ?? n2), + prevSelection = selections[selectionIndex], + empty = carets === "|" && prevSelection === undefined, + start = new vscode.Position(lines.length - 1, offset), + end = offset + carets.length === lines[lines.length - 1].length + 1 && !empty + ? new vscode.Position(lines.length, 0) // Select end of line character. + : new vscode.Position(lines.length - 1, offset + (empty ? 0 : carets.length)); + + if (prevSelection === undefined) { + selections[selectionIndex] = carets[0] === "|" + ? new vscode.Selection(end, start) + : new vscode.Selection(start, end); + } else { + selections[selectionIndex] = prevSelection.isEmpty || prevSelection.isReversed + ? new vscode.Selection(end, prevSelection.start) + : new vscode.Selection(prevSelection.start, end); + } + hasIndicator = true; + + return " ".repeat(match.length); + }); + + if (hasIndicator && /^ +$/.test(line)) { + continue; + } + + if (lines.length > 0) { + previousLineStart += lines[lines.length - 1].length + 1; + // Accounting for the newline character. ^^^ + } + + lines.push(line); + } + + return new this(lines.join("\n"), selections); + } + + public async apply(editor: vscode.TextEditor) { + await editor.edit((builder) => { + const start = new vscode.Position(0, 0), + end = editor.document.lineAt(editor.document.lineCount - 1).rangeIncludingLineBreak.end; + + builder.replace(new vscode.Range(start, end), this.text); + }); + + if (this.selections.length > 0) { + editor.selections = this.selections; + } + } + + public assertEquals(editor: vscode.TextEditor, message = "") { + const document = editor.document; + + assert.strictEqual( + document.getText(), + this.text, + message + (message ? "\n" : "") + `Document text is not as expected.`, + ); + + const expectedSelections = this.selections.slice() as (vscode.Selection | undefined)[]; + + if (expectedSelections.length === 0) { + return; + } + + expect(editor.selections, "to have items satisfying", expect.it("to satisfy", { + end: expect.it("to satisfy", { + line: expect.it("to be less than", document.lineCount), + }), + })); + + // Ensure resulting selections are right. + let mergedSelections = Selections.mergeOverlapping(editor.selections).slice(); + + if (Context.currentOrUndefined?.selectionBehavior === SelectionBehavior.Character) { + mergedSelections = Selections.toCharacterMode(mergedSelections, document); + } + + const actualSelections = mergedSelections.slice() as (vscode.Selection | undefined)[]; + + // First, we set correct selections to `undefined` to ignore them in the + // checks below. + let hasUnexpectedSelection = false; + + for (let i = 0; i < expectedSelections.length && i < actualSelections.length; i++) { + if (expectedSelections[i]!.isEqual(actualSelections[i]!)) { + expectedSelections[i] = actualSelections[i] = undefined; + } else { + hasUnexpectedSelection = true; + } + } + + if (!hasUnexpectedSelection && expectedSelections.length === actualSelections.length) { + return; + } + + const commonText: string[] = [message === "" ? "Selections are not as expected." : message], + expectedText: string[] = [], + actualText: string[] = []; + + // Then, we report selections that are correct, but have the wrong index. + for (let i = 0; i < expectedSelections.length; i++) { + const expectedSelection = expectedSelections[i]; + + if (expectedSelection === undefined) { + continue; + } + + for (let j = 0; j < actualSelections.length; j++) { + const actualSelection = actualSelections[j]; + + if (actualSelection === undefined) { + continue; + } + + if (expectedSelection.isEqual(actualSelection)) { + commonText.push(`Expected selection found at index #${j} to be at index #${i}.`); + expectedSelections[i] = actualSelections[j] = undefined; + break; + } + } + } + + // Then, we report selections that are expected and not found, and those + // that were found but were not expected. + const sortedExpectedSelections = expectedSelections + .map((x, i) => [i, x!] as const) + .filter((x) => x[1] !== undefined) + .sort((a, b) => a[1].start.compareTo(b[1].start)); + const sortedActualSelections = actualSelections + .map((x, i) => [i, x!] as const) + .filter((x) => x[1] !== undefined) + .sort((a, b) => a[1].start.compareTo(b[1].start)); + + for (let i = sortedActualSelections.length; i < sortedExpectedSelections.length; i++) { + const [index, expectedSelection] = sortedExpectedSelections[i]; + + expectedText.push( + `Missing selection #${index}:\n${ + stringifySelection(document, expectedSelection).replace(/^(?=.)/gm, " ")}`); + actualText.push(`Missing selection #${index}:\n`); + } + + for (let i = sortedExpectedSelections.length; i < sortedActualSelections.length; i++) { + const [index, actualSelection] = sortedActualSelections[i]; + + actualText.push( + `Unexpected selection #${index}:\n${ + stringifySelection(document, actualSelection).replace(/^(?=.)/gm, " ")}`); + expectedText.push(`Unexpected selection #${index}:\n`); + } + + // Finally, we diff selections that exist in both arrays. + for (let i = 0; i < sortedExpectedSelections.length && i < sortedActualSelections.length; i++) { + const [expectedIndex, expectedSelection] = sortedExpectedSelections[i], + [actualIndex, actualSelection] = sortedActualSelections[i]; + + const error = actualIndex === expectedIndex + ? `Selection #${actualIndex} is not as expected:` + : `Actual selection #${actualIndex} differs from expected selection #${expectedIndex}:`; + + actualText.push(error); + expectedText.push(error); + + actualText.push(stringifySelection(document, actualSelection).replace(/^/gm, " ")); + expectedText.push(stringifySelection(document, expectedSelection).replace(/^/gm, " ")); + } + + assert.strictEqual( + actualText.join("\n"), + expectedText.join("\n"), + commonText.join("\n"), + ); + + // Sometimes the error messages end up being the same; ensure this isn't the + // case below. + assert.fail(commonText.join("\n") + "\n" + actualText.join("\n")); + } +} diff --git a/tsconfig.json b/tsconfig.json index 33820da..2f0c018 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,12 @@ { "compilerOptions": { - "module": "commonjs", + "module": "CommonJS", "target": "ES2018", "outDir": "out", - "lib": ["es6"], + "lib": ["ES2019", "ESNext.WeakRef"], "sourceMap": true, "strict": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "experimentalDecorators": true }, - "exclude": ["node_modules", ".vscode-test", "package.ts"] + "exclude": ["node_modules", ".vscode-test", "src/commands/old-*.ts"], } diff --git a/yarn.lock b/yarn.lock index e12755e..94ca25b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,17 +9,17 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-identifier@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" + integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A== "@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf" + integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" + "@babel/helper-validator-identifier" "^7.14.0" chalk "^2.0.0" js-tokens "^4.0.0" @@ -38,31 +38,31 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@nodelib/fs.scandir@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" - integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== +"@nodelib/fs.scandir@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" + integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== dependencies: - "@nodelib/fs.stat" "2.0.3" + "@nodelib/fs.stat" "2.0.4" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" - integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" + integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== "@nodelib/fs.walk@^1.2.3": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" - integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== dependencies: - "@nodelib/fs.scandir" "2.1.3" + "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== "@types/glob@^7.1.1": version "7.1.3" @@ -72,43 +72,43 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/js-yaml@^3.12.3": - version "3.12.5" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb" - integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== - "@types/json-schema@^7.0.3": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== "@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" + integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== "@types/mocha@^8.0.3": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.0.3.tgz#51b21b6acb6d1b923bbdc7725c38f9f455166402" - integrity sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg== + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.2.tgz#91daa226eb8c2ff261e6a8cbf8c7304641e095e0" + integrity sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw== -"@types/node@*", "@types/node@^14.6.0": - version "14.6.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499" - integrity sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA== +"@types/node@*": + version "15.0.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.1.tgz#ef34dea0881028d11398be5bf4e856743e3dc35a" + integrity sha512-TMkXt0Ck1y0KKsGr9gJtWGjttxlZnnvDtphxUOSd0bfaR6Q1jle+sPvrzNR1urqYTWMinoKvjKfXUGsumaO1PA== + +"@types/node@^14.6.0": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/vscode@^1.44.0": - version "1.48.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.48.0.tgz#c1841ccf80086d53b35a9d7f2eb3b4d949bd2d2f" - integrity sha512-sZJKzsJz1gSoFXcOJWw3fnKl2sseUgZmvB4AJZS+Fea+bC/jfGPVhmFL/FfQHld/TKtukVONsmoD3Pkyx9iadg== + version "1.55.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.55.0.tgz#58cfbebbd32b3e374e07e7858b1fd0e92b1a1d2b" + integrity sha512-49hysH7jneTQoSC8TWbAi7nKK9Lc5osQNjmDHVosrcU8o3jecD9GrK0Qyul8q4aGPSXRfNGqIp9CBdb13akETg== "@typescript-eslint/eslint-plugin@^4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.18.0.tgz#50fbce93211b5b690895d20ebec6fe8db48af1f6" - integrity sha512-Lzkc/2+7EoH7+NjIWLS2lVuKKqbEmJhtXe3rmfA8cyiKnZm3IfLf51irnBcmow8Q/AptVV0XBZmBJKuUJTe6cQ== + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz#3d5f29bb59e61a9dba1513d491b059e536e16dbc" + integrity sha512-U8SP9VOs275iDXaL08Ln1Fa/wLXfj5aTr/1c0t0j6CdbOnxh+TruXu1p4I0NAvdPBQgoPjHsgKn28mOi0FzfoA== dependencies: - "@typescript-eslint/experimental-utils" "4.18.0" - "@typescript-eslint/scope-manager" "4.18.0" + "@typescript-eslint/experimental-utils" "4.22.0" + "@typescript-eslint/scope-manager" "4.22.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" lodash "^4.17.15" @@ -116,66 +116,66 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.18.0.tgz#ed6c955b940334132b17100d2917449b99a91314" - integrity sha512-92h723Kblt9JcT2RRY3QS2xefFKar4ZQFVs3GityOKWQYgtajxt/tuXIzL7sVCUlM1hgreiV5gkGYyBpdOwO6A== +"@typescript-eslint/experimental-utils@4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.0.tgz#68765167cca531178e7b650a53456e6e0bef3b1f" + integrity sha512-xJXHHl6TuAxB5AWiVrGhvbGL8/hbiCQ8FiWwObO3r0fnvBdrbWEDy1hlvGQOAWc6qsCWuWMKdVWlLAEMpxnddg== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.18.0" - "@typescript-eslint/types" "4.18.0" - "@typescript-eslint/typescript-estree" "4.18.0" + "@typescript-eslint/scope-manager" "4.22.0" + "@typescript-eslint/types" "4.22.0" + "@typescript-eslint/typescript-estree" "4.22.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" "@typescript-eslint/parser@^4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.18.0.tgz#a211edb14a69fc5177054bec04c95b185b4dde21" - integrity sha512-W3z5S0ZbecwX3PhJEAnq4mnjK5JJXvXUDBYIYGoweCyWyuvAKfGHvzmpUzgB5L4cRBb+cTu9U/ro66dx7dIimA== + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.22.0.tgz#e1637327fcf796c641fe55f73530e90b16ac8fe8" + integrity sha512-z/bGdBJJZJN76nvAY9DkJANYgK3nlRstRRi74WHm3jjgf2I8AglrSY+6l7ogxOmn55YJ6oKZCLLy+6PW70z15Q== dependencies: - "@typescript-eslint/scope-manager" "4.18.0" - "@typescript-eslint/types" "4.18.0" - "@typescript-eslint/typescript-estree" "4.18.0" + "@typescript-eslint/scope-manager" "4.22.0" + "@typescript-eslint/types" "4.22.0" + "@typescript-eslint/typescript-estree" "4.22.0" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.18.0.tgz#d75b55234c35d2ff6ac945758d6d9e53be84a427" - integrity sha512-olX4yN6rvHR2eyFOcb6E4vmhDPsfdMyfQ3qR+oQNkAv8emKKlfxTWUXU5Mqxs2Fwe3Pf1BoPvrwZtwngxDzYzQ== +"@typescript-eslint/scope-manager@4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz#ed411545e61161a8d702e703a4b7d96ec065b09a" + integrity sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q== dependencies: - "@typescript-eslint/types" "4.18.0" - "@typescript-eslint/visitor-keys" "4.18.0" + "@typescript-eslint/types" "4.22.0" + "@typescript-eslint/visitor-keys" "4.22.0" -"@typescript-eslint/types@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.18.0.tgz#bebe323f81f2a7e2e320fac9415e60856267584a" - integrity sha512-/BRociARpj5E+9yQ7cwCF/SNOWwXJ3qhjurMuK2hIFUbr9vTuDeu476Zpu+ptxY2kSxUHDGLLKy+qGq2sOg37A== +"@typescript-eslint/types@4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.22.0.tgz#0ca6fde5b68daf6dba133f30959cc0688c8dd0b6" + integrity sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA== -"@typescript-eslint/typescript-estree@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.18.0.tgz#756d3e61da8c16ab99185532c44872f4cd5538cb" - integrity sha512-wt4xvF6vvJI7epz+rEqxmoNQ4ZADArGQO9gDU+cM0U5fdVv7N+IAuVoVAoZSOZxzGHBfvE3XQMLdy+scsqFfeg== +"@typescript-eslint/typescript-estree@4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.0.tgz#b5d95d6d366ff3b72f5168c75775a3e46250d05c" + integrity sha512-TkIFeu5JEeSs5ze/4NID+PIcVjgoU3cUQUIZnH3Sb1cEn1lBo7StSV5bwPuJQuoxKXlzAObjYTilOEKRuhR5yg== dependencies: - "@typescript-eslint/types" "4.18.0" - "@typescript-eslint/visitor-keys" "4.18.0" + "@typescript-eslint/types" "4.22.0" + "@typescript-eslint/visitor-keys" "4.22.0" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.18.0.tgz#4e6fe2a175ee33418318a029610845a81e2ff7b6" - integrity sha512-Q9t90JCvfYaN0OfFUgaLqByOfz8yPeTAdotn/XYNm5q9eHax90gzdb+RJ6E9T5s97Kv/UHWKERTmqA0jTKAEHw== +"@typescript-eslint/visitor-keys@4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.0.tgz#169dae26d3c122935da7528c839f42a8a42f6e47" + integrity sha512-nnMu4F+s4o0sll6cBSsTeVsT4cwxB7zECK3dFxzEjPBii9xLpq4yqqsy/FU5zMfan6G60DKZSCXAa3sHJZrcYw== dependencies: - "@typescript-eslint/types" "4.18.0" + "@typescript-eslint/types" "4.22.0" eslint-visitor-keys "^2.0.0" -acorn-jsx@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== acorn-jsx@^5.3.1: version "5.3.1" @@ -187,24 +187,14 @@ acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -agent-base@4, agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: - es6-promisify "^5.0.0" + debug "4" -ajv@^6.10.0: - version "6.12.4" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" - integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.12.4: +ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -214,10 +204,10 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^7.0.2: - version "7.2.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.1.tgz#a5ac226171912447683524fa2f1248fcf8bac83d" - integrity sha512-+nu0HDv7kNSOua9apAVc979qd932rrZeb3WOvoiD31A/p1mIE5/9bN2027pE2rOPYEdS3UHzsvof4hY+lM9/WQ== +ajv@^8.0.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.2.0.tgz#c89d3380a784ce81b2085f48811c4c101df4c602" + integrity sha512-WSNGFuyWd//XO8n/m/EaOlNLtO0yL8EXT/74LqT4khdhpZjP7lkj/kT5uwRmGitKEVp/Oj7ZUHeGfPtgHhQ5CA== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -234,42 +224,34 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== -ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-styles@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.0.0.tgz#432b26162fea1b63c878896abc8cc5548f25063e" + integrity sha1-QysmFi/qG2PIeIlqvIzFVI8lBj4= + +ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" -ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== - dependencies: - "@types/color-name" "^1.1.1" - color-convert "^2.0.1" - anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -286,47 +268,82 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-changes-async@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/array-changes-async/-/array-changes-async-3.0.1.tgz#7103962ae9d954be12e3f0d844e1ac7021f65688" + integrity sha512-WNHLhMOTzntixkBxNm/MiWCNKuC4FMYXk6DKuzZUbkWXAe0Xomwv40SEUicfOuHHtW7Ue661Mc5AJA0AOfqApg== + dependencies: + arraydiff-async "0.2.0" + +array-changes@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/array-changes/-/array-changes-3.0.1.tgz#691aab4eb9fc6ae400446f1b0257cdb0fdd35dd6" + integrity sha512-UYXV+qUaTKJO3GUBVfD6b9Mu7wUzDvpfovZKtbxNJApwRUifgrJMidvE+/rbqV3wCffly5HXcbOW3/7shmmEag== + dependencies: + arraydiff-papandreou "0.1.1-patch1" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.map@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.2.tgz#9a4159f416458a23e9483078de1106b2ef68f8ec" - integrity sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.4" +arraydiff-async@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/arraydiff-async/-/arraydiff-async-0.2.0.tgz#bb0528b98e814b702f01f496bc73bec3e57f4086" + integrity sha1-uwUouY6BS3AvAfSWvHO+w+V/QIY= + +arraydiff-papandreou@0.1.1-patch1: + version "0.1.1-patch1" + resolved "https://registry.yarnpkg.com/arraydiff-papandreou/-/arraydiff-papandreou-0.1.1-patch1.tgz#0290270bfdbb4b2efad8b7482638ad5a72109210" + integrity sha1-ApAnC/27Sy762LdIJjitWnIQkhA= astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -azure-devops-node-api@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-7.2.0.tgz#131d4e01cf12ebc6e45569b5e0c5c249e4114d6d" - integrity sha512-pMfGJ6gAQ7LRKTHgiRF+8iaUUeGAI0c8puLaqHLc7B8AR7W6GJLozK9RFeUHFjEGybC9/EB3r67WPd7e46zQ8w== +azure-devops-node-api@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-10.2.2.tgz#9f557e622dd07bbaa9bd5e7e84e17c761e2151b2" + integrity sha512-4TVv2X7oNStT0vLaEfExmy3J4/CzfuXolEcQl/BRUmvGySqKStTG2O55/hUQ0kM7UJlZBLgniM0SBq4d/WkKow== dependencies: - os "0.1.1" - tunnel "0.0.4" - typed-rest-client "1.2.0" - underscore "1.8.3" + tunnel "0.0.6" + typed-rest-client "^1.8.4" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +big-integer@^1.6.17: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -boolbase@~1.0.0: +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk= + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM= + +boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= @@ -361,15 +378,40 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== + +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= + dependencies: + traverse ">=0.3.0 <0.4" chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" @@ -381,29 +423,40 @@ chalk@^2.0.0, chalk@^2.4.2: supports-color "^5.3.0" chalk@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" -cheerio@^1.0.0-rc.1: - version "1.0.0-rc.3" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" - integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== +cheerio-select@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.4.0.tgz#3a16f21e37a2ef0f211d6d1aa4eff054bb22cdc9" + integrity sha512-sobR3Yqz27L553Qa7cK6rtJlMDbiKPdNywtR95Sj/YgfpLfy0u6CGJuaBKe5YE/vTc23SCRKxWSdlon/w6I/Ew== dependencies: - css-select "~1.2.0" - dom-serializer "~0.1.1" - entities "~1.1.1" - htmlparser2 "^3.9.1" - lodash "^4.15.0" - parse5 "^3.0.1" + css-select "^4.1.2" + css-what "^5.0.0" + domelementtype "^2.2.0" + domhandler "^4.2.0" + domutils "^2.6.0" -chokidar@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" - integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== +cheerio@^1.0.0-rc.1: + version "1.0.0-rc.6" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.6.tgz#a5ae81ab483aeefa1280c325543c601145506240" + integrity sha512-hjx1XE1M/D5pAtMgvWwE21QClmAEeGHOIDfycgmndisdNgI6PE1cGRQkMGBcsbUbmEQyWu5PJLUcAOjtQS8DWw== + dependencies: + cheerio-select "^1.3.0" + dom-serializer "^1.3.1" + domhandler "^4.1.0" + htmlparser2 "^6.1.0" + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter "^6.0.1" + +chokidar@3.5.1, chokidar@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -411,18 +464,18 @@ chokidar@3.3.1: is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.3.0" + readdirp "~3.5.0" optionalDependencies: - fsevents "~2.1.2" + fsevents "~2.3.1" -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" color-convert@^1.9.0: version "1.9.3" @@ -438,6 +491,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-diff@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/color-diff/-/color-diff-0.1.7.tgz#6db78cd9482a8e459d40821eaf4b503283dcb8e2" + integrity sha1-bbeM2UgqjkWdQIIer0tQMoPcuOI= + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -458,6 +516,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -472,65 +535,59 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -css-select@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= +css-select@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286" + integrity sha512-nu5ye2Hg/4ISq4XqdLY2bEatAcLIdt3OYGFc9Tm9n7VSlFBcfRv0gBNksHRgSdUDQGtN3XrZ94ztW+NfzkFSUw== dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" + boolbase "^1.0.0" + css-what "^5.0.0" + domhandler "^4.2.0" + domutils "^2.6.0" + nth-check "^2.0.0" -css-what@2.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== +css-what@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.0.tgz#f0bf4f8bac07582722346ab243f6a35b512cfc47" + integrity sha512-qxyKHQvgKwzwDWC/rGbT821eJalfupxYW2qbSJSAtdSTimsr/MlaGONoNLllaUPZWf8QnbcKM/kPVYUQuEKAFA== -debug@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== +debug@4, debug@4.3.1, debug@^4.0.1, debug@^4.1.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: - ms "2.0.0" + ms "2.1.2" -debug@3.2.6, debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -debug@^4.0.1, debug@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -define-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - denodeify@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" integrity sha1-OjYof1A05pnnV3kBBSwubJQlFjE= -diff@4.0.2, diff@^4.0.1: +detect-indent@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-3.0.1.tgz#9dc5e5ddbceef8325764b9451b02bc6d54084f75" + integrity sha1-ncXl3bzu+DJXZLlFGwK8bVQIT3U= + dependencies: + get-stdin "^4.0.1" + minimist "^1.1.0" + repeating "^1.1.0" + +diff@5.0.0, diff@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== @@ -549,59 +606,42 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== +dom-serializer@^1.0.1, dom-serializer@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.1.tgz#d845a1565d7c041a95e5dab62184ab41e3a519be" + integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q== dependencies: domelementtype "^2.0.1" + domhandler "^4.0.0" entities "^2.0.0" -dom-serializer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" - integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^4.0.0, domhandler@^4.1.0, domhandler@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" + integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== dependencies: - domelementtype "^1.3.0" - entities "^1.1.1" + domelementtype "^2.2.0" -domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" - integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== +domutils@^2.5.2, domutils@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7" + integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA== dependencies: - domelementtype "1" + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + readable-stream "^2.0.2" emoji-regex@^8.0.0: version "8.0.0" @@ -615,86 +655,32 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" -entities@^1.1.1, entities@~1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^2.0.0, entities@~2.0.0: +entities@~2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== -es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" - object-keys "^1.1.1" - object.assign "^4.1.0" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -es-array-method-boxes-properly@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" - integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -es-get-iterator@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" - integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== - dependencies: - es-abstract "^1.17.4" - has-symbols "^1.0.1" - is-arguments "^1.0.4" - is-map "^2.0.1" - is-set "^2.0.1" - is-string "^1.0.5" - isarray "^2.0.5" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -eslint-scope@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-scope@^5.1.1: +eslint-scope@^5.0.0, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -720,9 +706,9 @@ eslint-visitor-keys@^2.0.0: integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== eslint@^7.22.0: - version "7.22.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.22.0.tgz#07ecc61052fec63661a2cab6bd507127c07adc6f" - integrity sha512-3VawOtjSJUQiiqac8MQc+w457iGLfuNGLFn8JmF051tTKbh5/x/0vlcEj8OgDCaw7Ysa2Jn8paGshV7x2abKXg== + version "7.25.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.25.0.tgz#1309e4404d94e676e3e831b3a3ad2b050031eb67" + integrity sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw== dependencies: "@babel/code-frame" "7.12.11" "@eslint/eslintrc" "^0.4.0" @@ -762,16 +748,7 @@ eslint@^7.22.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" - integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== - dependencies: - acorn "^7.4.0" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.3.0" - -espree@^7.3.1: +espree@^7.3.0, espree@^7.3.1: version "7.3.1" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== @@ -792,13 +769,6 @@ esquery@^1.4.0: dependencies: estraverse "^5.1.0" -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== - dependencies: - estraverse "^4.1.0" - esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -806,7 +776,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.0, estraverse@^4.1.1: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -827,9 +797,9 @@ fast-deep-equal@^3.1.1: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.1.1: - version "3.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" - integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -849,9 +819,9 @@ fast-levenshtein@^2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" - integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== dependencies: reusify "^1.0.4" @@ -876,21 +846,14 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-up@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: - locate-path "^5.0.0" + locate-path "^6.0.0" path-exists "^4.0.0" -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -899,12 +862,10 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" -flat@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" - integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== - dependencies: - is-buffer "~2.0.3" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.1.0: version "3.1.1" @@ -916,10 +877,20 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" function-bind@^1.1.1: version "1.1.1" @@ -931,15 +902,29 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -get-caller-file@^2.0.1: +get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" @@ -963,16 +948,16 @@ globals@^12.1.0: type-fest "^0.8.1" globals@^13.6.0: - version "13.7.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.7.0.tgz#aed3bcefd80ad3ec0f0be2cf0c895110c0591795" - integrity sha512-Aipsz6ZKRxa/xQkZhNg0qIWXT6x6rD46f6x/PCnBomlttdIyAPak4YD9jTmKpZ72uROSMU87qJtcgpgHaVchiA== + version "13.8.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.8.0.tgz#3e20f504810ce87a8d72e55aecf8435b50f4c1b3" + integrity sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q== dependencies: type-fest "^0.20.2" globby@^11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" - integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + version "11.0.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" + integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" @@ -981,6 +966,16 @@ globby@^11.0.1: merge2 "^1.3.0" slash "^3.0.0" +graceful-fs@^4.1.2, graceful-fs@^4.2.2: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + +greedy-interval-packer@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/greedy-interval-packer/-/greedy-interval-packer-1.2.0.tgz#d6da11f3661bb797812da78a1ea510678a2a8739" + integrity sha1-1toR82Ybt5eBLaeKHqUQZ4oqhzk= + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -996,10 +991,10 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.0, has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== has@^1.0.3: version "1.0.3" @@ -1013,33 +1008,32 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -htmlparser2@^3.9.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" -http-proxy-agent@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" - integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== dependencies: - agent-base "4" - debug "3.1.0" + "@tootallnate/once" "1" + agent-base "6" + debug "4" -https-proxy-agent@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== dependencies: - agent-base "^4.3.0" - debug "^3.1.0" + agent-base "6" + debug "4" ignore@^4.0.6: version "4.0.6" @@ -1052,9 +1046,9 @@ ignore@^5.1.4: integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -1072,16 +1066,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3: +inherits@2, inherits@~2.0.0, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== - is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1089,26 +1078,16 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-buffer@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" - integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== - -is-callable@^1.1.4, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= +is-finite@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" @@ -1126,85 +1105,42 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" - integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-plain-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== -is-regex@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-set@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" - integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== - -is-string@^1.0.4, is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -iterate-iterator@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6" - integrity sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw== - -iterate-value@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" - integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== - dependencies: - es-get-iterator "^1.0.2" - iterate-iterator "^1.0.1" - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== +js-yaml@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" + integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== dependencies: - argparse "^1.0.7" - esprima "^4.0.0" + argparse "^2.0.1" -js-yaml@^3.13.0, js-yaml@^3.13.1: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -1244,37 +1180,59 @@ linkify-it@^2.0.0: dependencies: uc.micro "^1.0.1" -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc= + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" + p-locate "^5.0.0" -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= -lodash@^4.15.0, lodash@^4.17.15: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= -lodash@^4.17.20, lodash@^4.17.21: +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + +lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== +log-symbols@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== dependencies: - chalk "^2.4.2" + chalk "^4.0.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magicpen@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/magicpen/-/magicpen-6.2.2.tgz#8069cb5d9d9b4d47350001465f30f2f541215a9d" + integrity sha512-B+PWeKs3OUry0S5iGR7EKVRxif6Pfy1S/C1VJ/a2pFnJbScWJHvrulhss3rIfiHMCt5HOxB2v2LX+un3Z+W5/g== + dependencies: + ansi-styles "2.0.0" + color-diff "0.1.7" make-error@^1.1.1: version "1.3.6" @@ -1303,12 +1261,12 @@ merge2@^1.3.0: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: braces "^3.0.1" - picomatch "^2.0.5" + picomatch "^2.2.3" mime@^1.3.4: version "1.6.0" @@ -1322,52 +1280,69 @@ minimatch@3.0.4, minimatch@^3.0.3, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -mocha@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.1.tgz#1de1ba4e9a2c955d96b84e469d7540848223592d" - integrity sha512-p7FuGlYH8t7gaiodlFreseLxEmxTgvyG9RgPHODFPySNhwUehu8NIb0vdSt3WFckSneswZ0Un5typYcWElk7HQ== +minimist@^1.1.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +"mkdirp@>=0.5 0": + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: + minimist "^1.2.5" + +mocha@^8.1.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.3.2.tgz#53406f195fa86fbdebe71f8b1c6fb23221d69fcc" + integrity sha512-UdmISwr/5w+uXLPKspgoV7/RXZwKRTiTjJ2/AC5ZiEztIoOYdfKb19+9jNmEInzx5pBsCyJQzarAxqIGBNYJhg== + dependencies: + "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" - chokidar "3.3.1" - debug "3.2.6" - diff "4.0.2" - escape-string-regexp "1.0.5" - find-up "4.1.0" + chokidar "3.5.1" + debug "4.3.1" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" glob "7.1.6" growl "1.10.5" he "1.2.0" - js-yaml "3.13.1" - log-symbols "3.0.0" + js-yaml "4.0.0" + log-symbols "4.0.0" minimatch "3.0.4" - ms "2.1.2" - object.assign "4.1.0" - promise.allsettled "1.0.2" - serialize-javascript "4.0.0" - strip-json-comments "3.0.1" - supports-color "7.1.0" + ms "2.1.3" + nanoid "3.1.20" + serialize-javascript "5.0.1" + strip-json-comments "3.1.1" + supports-color "8.1.1" which "2.0.2" wide-align "1.1.3" - workerpool "6.0.0" - yargs "13.3.2" - yargs-parser "13.1.2" - yargs-unparser "1.6.1" + workerpool "6.1.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.2, ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +nanoid@3.1.20: + version "3.1.20" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" + integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -1378,32 +1353,17 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -nth-check@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== +nth-check@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" + integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== dependencies: - boolbase "~1.0.0" + boolbase "^1.0.0" -object-inspect@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== - -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@4.1.0, object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" +object-inspect@^1.9.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.2.tgz#b6385a3e2b7cae0b5eafcf90cddf85d128767f30" + integrity sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA== once@^1.3.0: version "1.4.0" @@ -1434,11 +1394,6 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.1: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -os@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/os/-/os-0.1.1.tgz#208845e89e193ad4d971474b93947736a56d13f3" - integrity sha1-IIhF6J4ZOtTZcUdLk5R3NqVtE/M= - osenv@^0.1.3: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" @@ -1447,31 +1402,19 @@ osenv@^0.1.3: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: - p-try "^2.0.0" + yocto-queue "^0.1.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + p-limit "^3.0.2" parent-module@^1.0.0: version "1.0.1" @@ -1487,17 +1430,17 @@ parse-semver@^1.1.1: dependencies: semver "^5.1.0" -parse5@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" - integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== +parse5-htmlparser2-tree-adapter@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== dependencies: - "@types/node" "*" + parse5 "^6.0.1" -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== path-exists@^4.0.0: version "4.0.0" @@ -1524,37 +1467,43 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" + integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -promise.allsettled@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" - integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg== - dependencies: - array.prototype.map "^1.0.1" - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - iterate-value "^1.0.0" - punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@^6.9.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -1569,27 +1518,38 @@ read@^1.0.7: dependencies: mute-stream "~0.0.4" -readable-stream@^3.1.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== +readable-stream@^2.0.2, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" -readdirp@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" - integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== dependencies: - picomatch "^2.0.7" + picomatch "^2.2.1" regexpp@^3.0.0, regexpp@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +repeating@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" + integrity sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw= + dependencies: + is-finite "^1.0.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -1600,11 +1560,6 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -1615,7 +1570,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^2.6.3: +rimraf@2: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -1630,36 +1585,45 @@ rimraf@^3.0.2: glob "^7.1.3" run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" -safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@^5.1.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + semver@^5.1.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== semver@^7.2.1, semver@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" -serialize-javascript@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== +serialize-javascript@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== dependencies: randombytes "^2.1.0" -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +setimmediate@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= shebang-command@^2.0.0: version "2.0.0" @@ -1673,6 +1637,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -1713,16 +1686,7 @@ sprintf-js@~1.0.2: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== @@ -1731,28 +1695,12 @@ string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" + safe-buffer "~5.1.0" strip-ansi@^4.0.0: version "4.0.0" @@ -1761,13 +1709,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -1775,20 +1716,15 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-json-comments@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" - integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@7.1.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" @@ -1799,15 +1735,25 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -table@^6.0.4: - version "6.0.7" - resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" - integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: - ajv "^7.0.2" - lodash "^4.17.20" + has-flag "^4.0.0" + +table@^6.0.4: + version "6.6.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.6.0.tgz#905654b79df98d9e9a973de1dd58682532c40e8e" + integrity sha512-iZMtp5tUvcnAdtHpZTWLPF0M7AgiQsURR2DwmxnJwSy8I3+cY+ozzVvYha3BOLG2TB+L0CqjIz+91htuj6yCXg== + dependencies: + ajv "^8.0.1" + lodash.clonedeep "^4.5.0" + lodash.flatten "^4.4.0" + lodash.truncate "^4.4.2" slice-ansi "^4.0.0" string-width "^4.2.0" + strip-ansi "^6.0.0" text-table@^0.2.0: version "0.2.0" @@ -1828,6 +1774,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= + ts-node@^9.1.1: version "9.1.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" @@ -1841,21 +1792,21 @@ ts-node@^9.1.1: yn "3.1.1" tslib@^1.8.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tsutils@^3.17.1: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" -tunnel@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.4.tgz#2d3785a158c174c9a16dc2c046ec5fc5f1742213" - integrity sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM= +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -1874,33 +1825,74 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -typed-rest-client@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.2.0.tgz#723085d203f38d7d147271e5ed3a75488eb44a02" - integrity sha512-FrUshzZ1yxH8YwGR29PWWnfksLEILbWJydU7zfIRkyH7kAEzB62uMAl2WY6EyolWpLpVHeJGgQm45/MaruaHpw== +typed-rest-client@^1.8.4: + version "1.8.4" + resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.8.4.tgz#ba3fb788e5b9322547406392533f12d660a5ced6" + integrity sha512-MyfKKYzk3I6/QQp6e1T50py4qg+c+9BzOEl2rBmQIpStwNUoqQ73An+Tkfy9YuV7O+o2mpVVJpe+fH//POZkbg== dependencies: - tunnel "0.0.4" - underscore "1.8.3" + qs "^6.9.1" + tunnel "0.0.6" + underscore "^1.12.1" -typescript@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" - integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== +typescript@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" + integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== -underscore@1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" - integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= +ukkonen@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ukkonen/-/ukkonen-1.4.0.tgz#594629234ceccbf7e44187ad9fedbeb4866a2e5f" + integrity sha512-g8SLGxflI0/VNH2C8j66KcfJXrU5StJglRQBYPNiChXFlOrqqYM1icOykOAAUgTeBpktaEuCm9hjpPinQ080PA== + +underscore@^1.12.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" + integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== + +unexpected-bluebird@2.9.34-longstack2: + version "2.9.34-longstack2" + resolved "https://registry.yarnpkg.com/unexpected-bluebird/-/unexpected-bluebird-2.9.34-longstack2.tgz#49acac753b0556ded6025210ee96182307d2b2c9" + integrity sha1-SaysdTsFVt7WAlIQ7pYYIwfSssk= + +unexpected@^12.0.0: + version "12.0.1" + resolved "https://registry.yarnpkg.com/unexpected/-/unexpected-12.0.1.tgz#6d74769d8063cdcbdd6fb8561851113a26b1e428" + integrity sha512-JTAUzmFK0ufM3o7EHpF7eTPnWdCDiIIVg2M9q20dZ6I8e8jJKAkA/9yQy2txhsOLtmEejbuenlnleIBTHKQKhA== + dependencies: + array-changes "3.0.1" + array-changes-async "3.0.1" + detect-indent "3.0.1" + diff "^5.0.0" + greedy-interval-packer "1.2.0" + magicpen "^6.2.2" + ukkonen "^1.4.0" + unexpected-bluebird "2.9.34-longstack2" + +unzipper@^0.10.11: + version "0.10.11" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e" + integrity sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + graceful-fs "^4.2.2" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" @@ -1909,22 +1901,22 @@ url-join@^1.1.0: resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" integrity sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg= -util-deprecate@^1.0.1: +util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= v8-compile-cache@^2.0.3: - version "2.1.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" - integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== vsce@^1.87.0: - version "1.87.0" - resolved "https://registry.yarnpkg.com/vsce/-/vsce-1.87.0.tgz#d592142c8781c984bc49dec60be940f9d26528e6" - integrity sha512-7Ow05XxIM4gHBq/Ho3hefdmiZG0fddHtu0M0XJ1sojyZBvxPxTHaMuBsRnfnMzgCqxDTFI5iLr94AgiwQnhOMQ== + version "1.88.0" + resolved "https://registry.yarnpkg.com/vsce/-/vsce-1.88.0.tgz#748dc9f75996d97a5953408848c56c4c1b4dca6b" + integrity sha512-FS5ou3G+WRnPPr/tWVs8b/jVzeDacgZHy/y7/QQW7maSPFEAmRt2bFGUJtJVEUDLBqtDm/3VGMJ7D31cF2U1tw== dependencies: - azure-devops-node-api "^7.2.0" + azure-devops-node-api "^10.2.2" chalk "^2.4.2" cheerio "^1.0.0-rc.1" commander "^6.1.0" @@ -1940,24 +1932,20 @@ vsce@^1.87.0: read "^1.0.7" semver "^5.1.0" tmp "0.0.29" - typed-rest-client "1.2.0" + typed-rest-client "^1.8.4" url-join "^1.1.0" yauzl "^2.3.1" yazl "^2.2.2" -vscode-test@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-1.4.0.tgz#a56f73c1667b4d37ba6baa6765f233a19d4ffbfe" - integrity sha512-Jt7HNGvSE0+++Tvtq5wc4hiXLIr2OjDShz/gbAfM/mahQpy4rKBnmOK33D+MR67ATWviQhl+vpmU3p/qwSH/Pg== +vscode-test@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-1.5.2.tgz#d9ec3cab1815afae1d7d81923e3c685d13d32303" + integrity sha512-x9PVfKxF6EInH9iSFGQi0V8H5zIW1fC7RAer6yNQR6sy3WyOwlWkuT3I+wf75xW/cO53hxMi1aj/EvqQfDFOAg== dependencies: - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.4" - rimraf "^2.6.3" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + rimraf "^3.0.2" + unzipper "^0.10.11" which@2.0.2, which@^2.0.1: version "2.0.2" @@ -1978,89 +1966,67 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -workerpool@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" - integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== +workerpool@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b" + integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg== -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -y18n@^4.0.0: +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@13.1.2, yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== -yargs-parser@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3" - integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.7" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" + integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== -yargs-unparser@1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.1.tgz#bd4b0ee05b4c94d058929c32cb09e3fce71d3c5f" - integrity sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA== +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== dependencies: - camelcase "^5.3.1" - decamelize "^1.2.0" - flat "^4.1.0" - is-plain-obj "^1.1.0" - yargs "^14.2.3" + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" -yargs@13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yargs@^14.2.3: - version "14.2.3" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" - integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== - dependencies: - cliui "^5.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^15.0.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" yauzl@^2.3.1: version "2.10.0" @@ -2081,3 +2047,8 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==