Repository: VSCodeVim/Vim Branch: master Commit: 1fe317a6552b Files: 311 Total size: 2.4 MB Directory structure: gitextract_6p9ldwqz/ ├── .eslintrc.js ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── copilot-instructions.md │ └── workflows/ │ ├── build.yml │ ├── pull_request.yml │ └── release.yml ├── .gitignore ├── .husky/ │ ├── .gitignore │ └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── .yarnrc ├── CHANGELOG.OLD.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── build/ │ └── Dockerfile ├── extension.ts ├── extensionBase.ts ├── extensionWeb.ts ├── gulpfile.js ├── language-configuration.json ├── package.json ├── renovate.json ├── src/ │ ├── actions/ │ │ ├── base.ts │ │ ├── baseMotion.ts │ │ ├── commands/ │ │ │ ├── actions.ts │ │ │ ├── commandLine.ts │ │ │ ├── digraphs.ts │ │ │ ├── documentChange.ts │ │ │ ├── file.ts │ │ │ ├── fold.ts │ │ │ ├── incrementDecrement.ts │ │ │ ├── insert.ts │ │ │ ├── join.ts │ │ │ ├── macro.ts │ │ │ ├── navigate.ts │ │ │ ├── put.ts │ │ │ ├── replace.ts │ │ │ ├── scroll.ts │ │ │ ├── search.ts │ │ │ ├── undo.ts │ │ │ ├── visual.ts │ │ │ └── window.ts │ │ ├── include-main.ts │ │ ├── include-plugins.ts │ │ ├── languages/ │ │ │ └── python/ │ │ │ └── motion.ts │ │ ├── motion.ts │ │ ├── operator.ts │ │ ├── plugins/ │ │ │ ├── camelCaseMotion.ts │ │ │ ├── easymotion/ │ │ │ │ ├── easymotion.cmd.ts │ │ │ │ ├── easymotion.ts │ │ │ │ ├── markerGenerator.ts │ │ │ │ ├── registerMoveActions.ts │ │ │ │ └── types.ts │ │ │ ├── imswitcher.ts │ │ │ ├── pluginDefaultMappings.ts │ │ │ ├── replaceWithRegister.ts │ │ │ ├── sneak.ts │ │ │ ├── surround.ts │ │ │ └── targets/ │ │ │ ├── lastNextObjectHelper.ts │ │ │ ├── lastNextObjects.ts │ │ │ ├── searchUtils.ts │ │ │ ├── smartQuotes.ts │ │ │ ├── smartQuotesMatcher.ts │ │ │ ├── targets.ts │ │ │ └── targetsConfig.ts │ │ ├── types.d.ts │ │ └── wrapping.ts │ ├── cmd_line/ │ │ ├── commandLine.ts │ │ └── commands/ │ │ ├── ascii.ts │ │ ├── bang.ts │ │ ├── breakpoints.ts │ │ ├── bufferDelete.ts │ │ ├── change.ts │ │ ├── close.ts │ │ ├── copy.ts │ │ ├── delete.ts │ │ ├── digraph.ts │ │ ├── echo.ts │ │ ├── eval.ts │ │ ├── explore.ts │ │ ├── file.ts │ │ ├── fileInfo.ts │ │ ├── goto.ts │ │ ├── gotoLine.ts │ │ ├── grep.ts │ │ ├── history.ts │ │ ├── jumps.ts │ │ ├── leftRightCenter.ts │ │ ├── let.ts │ │ ├── marks.ts │ │ ├── move.ts │ │ ├── nohl.ts │ │ ├── normal.ts │ │ ├── only.ts │ │ ├── print.ts │ │ ├── put.ts │ │ ├── pwd.ts │ │ ├── quit.ts │ │ ├── read.ts │ │ ├── redo.ts │ │ ├── register.ts │ │ ├── retab.ts │ │ ├── set.ts │ │ ├── sh.ts │ │ ├── shift.ts │ │ ├── smile.ts │ │ ├── sort.ts │ │ ├── substitute.ts │ │ ├── tab.ts │ │ ├── terminal.ts │ │ ├── undo.ts │ │ ├── vscode.ts │ │ ├── wall.ts │ │ ├── write.ts │ │ ├── writequit.ts │ │ ├── writequitall.ts │ │ └── yank.ts │ ├── common/ │ │ ├── matching/ │ │ │ ├── matcher.ts │ │ │ ├── quoteMatcher.ts │ │ │ └── tagMatcher.ts │ │ ├── motion/ │ │ │ ├── cursor.ts │ │ │ └── position.ts │ │ └── number/ │ │ └── numericString.ts │ ├── completion/ │ │ └── lineCompletionProvider.ts │ ├── configuration/ │ │ ├── configuration.ts │ │ ├── configurationValidator.ts │ │ ├── decoration.ts │ │ ├── iconfiguration.ts │ │ ├── iconfigurationValidator.ts │ │ ├── langmap.ts │ │ ├── notation.ts │ │ ├── remapper.ts │ │ ├── validators/ │ │ │ ├── inputMethodSwitcherValidator.ts │ │ │ ├── neovimValidator.ts │ │ │ ├── remappingValidator.ts │ │ │ └── vimrcValidator.ts │ │ ├── vimrc.ts │ │ └── vimrcKeyRemappingBuilder.ts │ ├── error.ts │ ├── globals.ts │ ├── history/ │ │ ├── historyFile.ts │ │ └── historyTracker.ts │ ├── jumps/ │ │ ├── jump.ts │ │ └── jumpTracker.ts │ ├── mode/ │ │ ├── internalSelectionsTracker.ts │ │ ├── mode.ts │ │ ├── modeData.ts │ │ ├── modeHandler.ts │ │ └── modeHandlerMap.ts │ ├── neovim/ │ │ └── neovim.ts │ ├── platform/ │ │ ├── browser/ │ │ │ ├── constants.ts │ │ │ ├── fs.ts │ │ │ └── history.ts │ │ └── node/ │ │ ├── constants.ts │ │ ├── fs.ts │ │ └── history.ts │ ├── register/ │ │ └── register.ts │ ├── state/ │ │ ├── compositionState.ts │ │ ├── globalState.ts │ │ ├── recordedState.ts │ │ ├── remapState.ts │ │ ├── replaceState.ts │ │ ├── searchState.ts │ │ ├── substituteState.ts │ │ └── vimState.ts │ ├── statusBar.ts │ ├── taskQueue.ts │ ├── textEditor.ts │ ├── textobject/ │ │ ├── paragraph.ts │ │ ├── sentence.ts │ │ ├── textobject.ts │ │ ├── util.ts │ │ └── word.ts │ ├── transformations/ │ │ ├── execute.ts │ │ ├── transformations.ts │ │ └── transformer.ts │ ├── util/ │ │ ├── child_process.ts │ │ ├── clipboard.ts │ │ ├── decorationUtils.ts │ │ ├── externalCommand.ts │ │ ├── logger.ts │ │ ├── os.ts │ │ ├── path.ts │ │ ├── selections.ts │ │ ├── specialKeys.ts │ │ ├── statusBarTextUtils.ts │ │ ├── util.ts │ │ └── vscodeContext.ts │ └── vimscript/ │ ├── exCommand.ts │ ├── exCommandParser.ts │ ├── expression/ │ │ ├── build.ts │ │ ├── displayValue.ts │ │ ├── evaluate.ts │ │ ├── parser.ts │ │ └── types.ts │ ├── lineRange.ts │ ├── parserUtils.ts │ └── pattern.ts ├── syntaxes/ │ └── vimscript.tmLanguage.json ├── test/ │ ├── actions/ │ │ ├── baseAction.test.ts │ │ ├── insertLine.test.ts │ │ ├── languages/ │ │ │ └── python/ │ │ │ └── motion.test.ts │ │ └── markMovement.test.ts │ ├── cmd_line/ │ │ ├── bang.test.ts │ │ ├── breakpoints.test.ts │ │ ├── bufferDelete.test.ts │ │ ├── change.test.ts │ │ ├── command.test.ts │ │ ├── cursorLocation.test.ts │ │ ├── delete.test.ts │ │ ├── grep.test.ts │ │ ├── historyFile.test.ts │ │ ├── move.test.ts │ │ ├── normal.test.ts │ │ ├── only.test.ts │ │ ├── put.test.ts │ │ ├── redo.test.ts │ │ ├── retab.test.ts │ │ ├── smile.test.ts │ │ ├── sort.test.ts │ │ ├── split.test.ts │ │ ├── substitute.test.ts │ │ ├── tab.test.ts │ │ ├── tabCompletion.test.ts │ │ ├── undo.test.ts │ │ ├── vsplit.test.ts │ │ ├── write.test.ts │ │ ├── writequit.test.ts │ │ └── yank.test.ts │ ├── completion/ │ │ └── lineCompletion.test.ts │ ├── configuration/ │ │ ├── configuration.test.ts │ │ ├── langmap.test.ts │ │ ├── notation.test.ts │ │ ├── remapper.test.ts │ │ ├── remaps.test.ts │ │ ├── validators/ │ │ │ ├── neovimValidator.test.ts │ │ │ └── remappingValidator.test.ts │ │ ├── vimrc.test.ts │ │ └── vimrcKeyRemappingBuilder.test.ts │ ├── extension.test.ts │ ├── historyTracker.test.ts │ ├── index.ts │ ├── jumpTracker.test.ts │ ├── macro.test.ts │ ├── marks.test.ts │ ├── mode/ │ │ ├── modeHandler.test.ts │ │ ├── modeInsert.test.ts │ │ ├── modeNormal.test.ts │ │ ├── modeReplace.test.ts │ │ ├── modeVisual.test.ts │ │ ├── modeVisualBlock.test.ts │ │ ├── modeVisualLine.test.ts │ │ └── normalModeTests/ │ │ ├── commands.test.ts │ │ ├── dot.test.ts │ │ ├── matchingBracket.test.ts │ │ ├── motionMatchpairs.test.ts │ │ ├── motions.test.ts │ │ └── undo.test.ts │ ├── motion.test.ts │ ├── motionLineWrapping.test.ts │ ├── multicursor.test.ts │ ├── number/ │ │ ├── incrementDecrement.test.ts │ │ └── numericString.test.ts │ ├── operator/ │ │ ├── comment.test.ts │ │ ├── filter.test.ts │ │ ├── format.test.ts │ │ ├── put.test.ts │ │ ├── rot13.test.ts │ │ ├── shift.test.ts │ │ └── surrogate.test.ts │ ├── plugins/ │ │ ├── camelCaseMotion.test.ts │ │ ├── easymotion.test.ts │ │ ├── imswitcher.test.ts │ │ ├── lastNextObject.test.ts │ │ ├── replaceWithRegister.test.ts │ │ ├── smartQuotes.test.ts │ │ ├── sneak.test.ts │ │ └── surround.test.ts │ ├── register/ │ │ ├── register.test.ts │ │ └── repeatableMovement.test.ts │ ├── runTest.ts │ ├── search/ │ │ ├── motionIncSearch.test.ts │ │ ├── search.test.ts │ │ └── searchTextObject.test.ts │ ├── sentenceMotion.test.ts │ ├── state/ │ │ └── vimState.test.ts │ ├── testConfiguration.ts │ ├── testSimplifier.ts │ ├── testUtils.ts │ ├── util/ │ │ └── path.test.ts │ └── vimscript/ │ ├── exCommandParse.test.ts │ ├── expression.test.ts │ ├── lineRangeParse.test.ts │ ├── lineRangeResolve.test.ts │ └── searchOffset.test.ts ├── tsconfig.json ├── webpack.config.js └── webpack.dev.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ module.exports = { env: { es6: true, node: true, }, extends: [ 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', 'prettier', ], parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', sourceType: 'module', }, plugins: ['eslint-plugin-jsdoc', 'eslint-plugin-prefer-arrow', '@typescript-eslint'], root: true, ignorePatterns: ['*.js'], rules: { '@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/array-type': [ 'error', { default: 'array-simple', }, ], '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/ban-ts-comment': 'error', '@typescript-eslint/consistent-type-assertions': 'error', '@typescript-eslint/dot-notation': 'error', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/naming-convention': [ 'error', { selector: 'variable', format: ['camelCase', 'UPPER_CASE', 'PascalCase'], leadingUnderscore: 'allow', trailingUnderscore: 'forbid', }, ], '@typescript-eslint/no-array-constructor': 'error', // ignore the rule to conform to current code '@typescript-eslint/no-base-to-string': 'off', '@typescript-eslint/no-duplicate-enum-values': 'error', '@typescript-eslint/no-duplicate-type-constituents': 'error', '@typescript-eslint/no-empty-function': 'error', '@typescript-eslint/no-empty-interface': 'error', // ignore the rule to conform to current code '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-for-in-array': 'error', '@typescript-eslint/no-implied-eval': 'error', '@typescript-eslint/no-loss-of-precision': 'error', '@typescript-eslint/no-misused-new': 'error', '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: false, // TODO }, ], '@typescript-eslint/no-namespace': 'error', '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', '@typescript-eslint/parameter-properties': 'error', '@typescript-eslint/no-redundant-type-constituents': 'error', '@typescript-eslint/no-shadow': [ 'error', { hoist: 'all', }, ], '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/no-unnecessary-type-constraint': 'error', '@typescript-eslint/no-unsafe-argument': 'error', '@typescript-eslint/no-unsafe-assignment': 'error', '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-unused-expressions': 'error', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-var-requires': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/prefer-namespace-keyword': 'error', // ignore the rule to conform to current code '@typescript-eslint/require-await': 'off', '@typescript-eslint/restrict-plus-operands': 'error', // ignore the rule to conform to current code '@typescript-eslint/restrict-template-expressions': 'off', '@typescript-eslint/triple-slash-reference': [ 'error', { path: 'always', types: 'prefer-import', lib: 'always', }, ], '@typescript-eslint/typedef': 'off', '@typescript-eslint/unbound-method': 'error', '@typescript-eslint/unified-signatures': 'error', complexity: 'off', 'constructor-super': 'error', 'dot-notation': 'off', eqeqeq: ['error', 'smart'], 'guard-for-in': 'error', 'id-denylist': [ 'error', 'any', 'Number', 'number', 'String', 'string', 'Boolean', 'boolean', 'Undefined', 'undefined', ], 'id-match': 'error', 'jsdoc/check-alignment': 'error', // ignore the rule to conform to current code 'jsdoc/check-indentation': 'off', 'max-classes-per-file': 'off', 'new-parens': 'error', 'no-array-constructor': 'off', 'no-bitwise': 'error', 'no-caller': 'error', 'no-cond-assign': 'error', 'no-console': [ 'error', { allow: [ 'log', 'warn', 'dir', 'timeLog', 'assert', 'clear', 'count', 'countReset', 'group', 'groupEnd', 'table', 'dirxml', 'error', 'groupCollapsed', 'Console', 'profile', 'profileEnd', 'timeStamp', 'context', ], }, ], 'no-debugger': 'error', 'no-empty': 'error', 'no-empty-function': 'off', 'no-eval': 'error', 'no-fallthrough': 'error', 'no-implied-eval': 'off', 'no-invalid-this': 'off', 'no-loss-of-precision': 'off', 'no-new-wrappers': 'error', 'no-return-await': 'error', 'no-shadow': 'off', 'no-throw-literal': 'error', 'no-trailing-spaces': 'error', 'no-undef-init': 'error', 'no-underscore-dangle': 'off', 'no-unsafe-finally': 'error', 'no-unused-expressions': 'off', 'no-unused-labels': 'error', 'no-unused-vars': 'off', 'no-use-before-define': 'off', 'no-var': 'error', 'object-shorthand': 'error', 'one-var': ['error', 'never'], 'prefer-arrow/prefer-arrow-functions': [ 'error', { allowStandaloneDeclarations: true, }, ], 'prefer-const': [ 'error', { destructuring: 'all', }, ], radix: 'error', 'require-await': 'off', 'spaced-comment': [ 'error', 'always', { markers: ['/'], }, ], 'use-isnan': 'error', 'valid-typeof': 'off', }, }; ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contribution Guide This document offers a set of guidelines for contributing to VSCodeVim. These are just guidelines, not rules; use your best judgment and feel free to propose changes to this document. If you need help, drop by on [GitHub Discussions](https://github.com/VSCodeVim/Vim/discussions) or [Slack](https://vscodevim.slack.com/). Thanks for helping us in making VSCodeVim better! :clap: ## Submitting Issues The [GitHub issue tracker](https://github.com/VSCodeVim/Vim/issues) is the preferred channel for tracking bugs and enhancement suggestions. When creating a new bug report do: - Search against existing issues to check if somebody else has already reported your problem or requested your idea - Fill out the issue template. ### Improve Existing Issues - Try to replicate bugs and describe the method if you're able to. - Search for [duplicate issues](https://github.com/VSCodeVim/Vim/issues?q=is%3Aissue+is%3Aopen+cursor). See which thread(s) are more mature, and recommend the duplicate be closed, or just provide links to related issues. - Find [old issues](https://github.com/VSCodeVim/Vim/issues?page=25&q=is%3Aissue+is%3Aopen) and test them in the latest version of VSCodeVim. If the issue has been resolved, comment & recommend OP to close (or provide more information if not resolved). - Give thumbs up / thumbs down to existing issues, to indicate your support (or not) ## Submitting Pull Requests Pull requests are _awesome_. If you're looking to raise a PR for something which doesn't have an open issue, consider creating an issue first. When submitting a PR, please fill out the template that is presented by GitHub when a PR is opened. ## First Time Setup 1. Install prerequisites: - [Visual Studio Code](https://code.visualstudio.com/), latest stable or insiders - [Node.js](https://nodejs.org/) v22.x or higher - [Yarn](https://classic.yarnpkg.com/) v1.x - _Optional_: [Docker Community Edition](https://store.docker.com/search?type=edition&offering=community) 🐋 2. Fork and clone repository: ```bash git clone git@github.com:/Vim.git cd Vim ``` 3. Build extension: ```bash # Install the dependencies yarn install # Open in VS Code code . # Build with one of these... yarn build-dev # Fast build for development yarn build # Slow build for release yarn watch # Fast build whenever a file changes ``` 4. Run extension using VS Code's "Run and Debug" menu 5. Run tests: ```bash # If Docker is installed and running: npx gulp test # Run tests inside Docker container npx gulp test --grep # Run only tests/suites matching inside Docker container # Otherwise, build and run the tests locally: yarn build # Build yarn build-test # Build tests yarn test # Test (must close all instances of VS Code) ``` 6. Package and install extension: ```bash # Package extension into `vim-...vsix` # (This can be opened and inspected like a .zip file) yarn package # Install packaged extension to your local VS Code installation code --install-extension vim-...vsix --force ``` ## Code Architecture The code is split into two parts: - ModeHandler - Vim state machine - Actions - 'actions' which modify the state ### Actions Actions are all currently stuffed into actions.ts (sorry!). There are: - `BaseAction` - the base Action type that all Actions derive from. - `BaseMovement` - A movement (e.g.`w`, `h`, `{`, etc.) _ONLY_ updates the cursor position or returns an `IMovement`, which indicates a start and stop. This is used for movements like `aw` which may actually start before the cursor. - `BaseCommand` - Anything which is not just a movement is a Command. That includes motions which also update the state of Vim in some way, like `*`. Actions should, when possible, avoid side effects other than modifying `vimState`. ### The Vim State Machine Consists of two data structures: - `VimState` - this is the state of Vim, scoped to a `TextEditor`. It's what actions update. - `RecordedState` - this is temporary state that will reset at the end of a change. #### How it works 1. `handleKeyEventHelper` is called with the most recent keypress. 2. `Actions.getRelevantAction` determines if all the keys pressed so far uniquely specify any action in actions.ts. If not, we continue waiting for keypresses. 3. `runAction` runs the action that was matched. Movements, Commands and Operators all have separate functions that dictate how to run them - `executeMovement`, `handleCommand`, and `executeOperator` respectively. 4. Now that we've updated `VimState`, we run `updateView` with the new VimState to "redraw" VS Code to the new state. #### `vscode.window.onDidChangeTextEditorSelection` This is my hack to simulate a click event based API in an IDE that doesn't have them (yet?). I check the selection that just came in to see if it's the same as what I thought I previously set the selection to the last time the state machine updated. If it's not, the user _probably_ clicked. (But she also could have tab completed!) ## Release Before you push a release, be sure to make sure the changelog is updated! To push a release: ```bash npx gulp release --semver [SEMVER] git push --follow-tags ``` The above Gulp command will: 1. Bump the package version based off the semver supplied. Supported values: patch, minor, major. 2. Create a Git commit with the above changes. 3. Create a Git tag using the new package version. In addition to building and testing the extension, when a tag is applied to the commit, the CI server will also create a GitHub release and publish the new version to the Visual Studio marketplace. ## Troubleshooting ### VS Code Slowdown If you notice a slowdown and have ever run `yarn test` in the past instead of running tests through VSCode, you might find a `.vscode-test/` folder, which VSCode is continually consuming CPU cycles to index. Long story short, you can speed up VSCode by: ```bash rm -rf .vscode-test/ ``` ================================================ FILE: .github/FUNDING.yml ================================================ github: [J-Fields] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. If remapping-related, please attach log output: https://github.com/VSCodeVim/Vim#debugging-remappings. **Environment (please complete the following information):** - Extension (VsCodeVim) version: - VSCode version: - OS: **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ **What this PR does / why we need it**: **Which issue(s) this PR fixes** **Special notes for your reviewer**: ================================================ FILE: .github/copilot-instructions.md ================================================ # Copilot Instructions for VSCodeVim ## Project Overview - **VSCodeVim** is a complex VS Code extension that emulates Vim behavior within VS Code. It is written in TypeScript and targets both desktop and web environments. - The codebase is organized by Vim concepts: actions, motions, operators, modes, state, configuration, and plugin emulation. See `src/` for main logic, with subfolders for each concept. - The extension supports advanced features like `.vimrc` parsing, Neovim integration, multi-cursor, and emulated Vim plugins (see `README.md` for the full list). ## Key Architectural Patterns - **Mode Handler:** Each open document is managed by a `ModeHandler` (`src/mode/`), which can be compared to an instance of Vim. State transitions and command dispatch are centralized here. - **State Management:** Each `ModeHandler` has a `VimState` (`src/state/vimState.ts`) which tracks the current mode, cursor, registers, and more. State is passed through most command flows. - **Action/Motion/Operator Classes:** User input is parsed into actions, motions, and operators (see `src/actions/`). These are composed to implement Vim commands. - **Configuration:** Settings are loaded from VS Code config, `.vimrc`, and defaults, in that order. See `src/configuration/` and `README.md` for details. - **Plugin Emulation:** Many Vim plugins are emulated natively (see `src/actions/plugins/` and `README.md` > Emulated Plugins). - **Neovim Integration:** If enabled, some Ex-commands are delegated to a Neovim process (`src/neovim/`). ## Developer Workflows - **Build:** Use `yarn build-dev` (or the VS Code task) to build the extension. See `package.json` and `gulpfile.js` for all tasks. - **Test:** Run tests with `yarn build-test` then `yarn test`. Tests are in `test/` and mirror the structure of `src/`. - **Debug:** Launch the extension in the Extension Development Host via VS Code's debugger. Use breakpoints in TypeScript files. - **Release:** Versioning and release tasks are managed via Gulp (`gulpfile.js`). ## Project-Specific Conventions - **Remapping:** Key remapping is handled via configuration and `.vimrc`. Only remaps are supported in `.vimrc` (see `README.md`). - **Settings Precedence:** Settings are loaded in this order: Ex-commands, user/workspace settings, VS Code settings, then defaults. - **Plugin Emulation:** Emulated plugins are implemented as native TypeScript, not as Vimscript or external scripts. - **Testing:** Tests are colocated with the feature under test (e.g., `test/actions/`, `test/mode/`). - **Cross-Platform:** Some features (e.g., input method switching) require platform-specific configuration (see `README.md`). ## Integration Points - **VS Code API:** Extension entry point is `extension.ts`. All VS Code API usage is centralized here and in `extensionBase.ts`/`extensionWeb.ts`. - **Neovim:** Integration is optional and controlled by settings. See `src/neovim/`. - **Status Bar:** Status bar updates are handled in `src/statusBar.ts`. ## References - See `README.md` for user-facing documentation, settings, and plugin support. - See `gulpfile.js` for build/test/release automation. - See `src/` for main extension logic, organized by Vim concept. - See `test/` for tests, which mirror the structure of `src/`. --- If you are unsure about a pattern or workflow, check the `README.md` or the relevant subdirectory in `src/` or `test/`. For new features, follow the structure and conventions of existing code. ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: push: branches: - 'master' jobs: build: strategy: matrix: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout VSCodeVim uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: yarn - name: Install dependencies run: yarn install --frozen-lockfile - name: Prettier if: matrix.os != 'windows-latest' run: yarn prettier:check - name: Lint if: matrix.os != 'windows-latest' run: yarn lint - name: Build run: yarn build - name: Test on ubuntu-latest if: matrix.os != 'windows-latest' run: | yarn build-test xvfb-run -a yarn test - name: Test on windows-latest if: matrix.os == 'windows-latest' run: | yarn build-test yarn test ================================================ FILE: .github/workflows/pull_request.yml ================================================ name: Pull Request on: pull_request: branches: - '**' jobs: build: strategy: matrix: os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout VSCodeVim uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: yarn - name: Install dependencies run: yarn install --frozen-lockfile - name: Prettier run: yarn prettier:check - name: Lint run: yarn lint - name: Build run: yarn build - name: Test run: | yarn build-test xvfb-run -a yarn test ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - v1.[0-9]+.[0-9]+ jobs: release: runs-on: ubuntu-latest steps: - name: Checkout VSCodeVim uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: yarn - name: Install dependencies run: yarn install --frozen-lockfile - name: Lint run: yarn lint - name: Build run: yarn build - name: Test run: | yarn build-test xvfb-run -a yarn test - name: Build extension package id: build_vsix run: | yarn package; echo ::set-output name=vsix_path::$(ls *.vsix); - name: Create release on GitHub id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: false prerelease: false - name: Upload .vsix as release asset to GitHub uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ${{ steps.build_vsix.outputs.vsix_path }} asset_name: ${{ steps.build_vsix.outputs.vsix_path }} asset_content_type: application/zip - name: Publish to VSCode Extension Marketplace run: yarn run vsce publish --yarn env: VSCE_PAT: ${{ secrets.VSCE_PAT }} - name: Publish to Open VSX Registry uses: HaaLeo/publish-vscode-extension@v1 id: publishToOpenVSX with: pat: ${{ secrets.OPEN_VSX_TOKEN }} ================================================ FILE: .gitignore ================================================ out testing node_modules *.sw? .vscode-test .DS_Store *.vsix *.log .cache typings/* !typings/custom/ testing ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/pre-commit ================================================ npx lint-staged ================================================ FILE: .prettierignore ================================================ .vscode-test out ================================================ FILE: .prettierrc ================================================ { "printWidth": 100, "singleQuote": true, "plugins": ["prettier-plugin-organize-imports"] } ================================================ FILE: .vscode/extensions.json ================================================ { // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp // List of extensions which should be recommended for users of this workspace. "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] } ================================================ FILE: .vscode/launch.json ================================================ // A launch configuration that compiles the extension and then opens it inside a new window { "version": "0.2.0", "configurations": [ { "name": "Build, Run Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}"], "stopOnEntry": false, "sourceMaps": true, "outFiles": ["${workspaceRoot}/{out, node_modules}/**/*.js"], "preLaunchTask": "gulp: build-dev", "internalConsoleOptions": "openOnSessionStart" }, { "name": "Run Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}"], "stopOnEntry": false, "sourceMaps": true, "outFiles": ["${workspaceRoot}/{out, node_modules}/**/*.js"] }, { "name": "Run Tests", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], "stopOnEntry": false, "sourceMaps": true, "outFiles": ["${workspaceRoot}/{out, node_modules}/**/*.js"], "preLaunchTask": "gulp: prepare-test", "internalConsoleOptions": "openOnSessionStart" }, { "name": "Run Web Extension", "type": "pwa-extensionHost", "debugWebWorkerHost": true, "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionDevelopmentKind=web"], "outFiles": ["${workspaceRoot}/{out, node_modules}/**/*.js"] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "files.trimTrailingWhitespace": true, "files.exclude": { "out": false // set this to true to hide the "out" folder with the compiled JS files }, "search.exclude": { "out": true // set this to false to include "out" folder in search results }, "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version "editor.tabSize": 2, "editor.insertSpaces": true } ================================================ FILE: .vscode/tasks.json ================================================ // Available variables which can be used inside of strings. // ${workspaceRoot}: the root folder of the team // ${file}: the current opened file // ${fileBasename}: the current opened file's basename // ${fileDirname}: the current opened file's dirname // ${fileExtname}: the current opened file's extension // ${cwd}: the current working directory of the spawned process { "version": "2.0.0", "tasks": [ { "type": "gulp", "task": "default", "problemMatcher": "$tsc-watch" }, { "type": "gulp", "task": "build-dev", "problemMatcher": [] }, { "type": "gulp", "task": "prepare-test", "problemMatcher": [] } ] } ================================================ FILE: .vscodeignore ================================================ .github/** .husky/** .yarn/** .vscode/** .vscode-test/** **/*.ts *.yml src/** build/** test/** typings/** out/src/** out/test/** out/*.map node_modules/** images/design/** .gitignore .prettierignore tsconfig.json webpack.*.js gulpfile.js renovate.json ================================================ FILE: .yarnrc ================================================ ignore-engines true ================================================ FILE: CHANGELOG.OLD.md ================================================ # Change Log ## **_NOTE: For versions 1.23.0 and newer, include the lastest changes; please see [CHANGELOG.md](CHANGELOG.md)._** ## [v1.22.2](https://github.com/vscodevim/vim/tree/v1.22.2) (2022-02-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.22.1...v1.22.2) **Fixed Bugs:** - Failed to handle key `j`: Cannot read property 'substring' of undefined [\#7512](https://github.com/VSCodeVim/Vim/issues/7512) - 1.22 broken for browser [\#7469](https://github.com/VSCodeVim/Vim/issues/7469) - Tab completion of file names should be case insensitive on Windows [\#7160](https://github.com/VSCodeVim/Vim/issues/7160) **Merged pull requests:** - Fix extension for web [\#7520](https://github.com/VSCodeVim/Vim/pull/7520) ([jeanp413](https://github.com/jeanp413)) - fix bugs with: Failed to handle key ... Cannot read property 'substring' of undefined [\#7513](https://github.com/VSCodeVim/Vim/pull/7513) ([elazarcoh](https://github.com/elazarcoh)) - Tab completion of file names is case insensitive on Windows [\#7471](https://github.com/VSCodeVim/Vim/pull/7471) ([elazarcoh](https://github.com/elazarcoh)) ## [v1.22.1](https://github.com/vscodevim/vim/tree/v1.22.1) (2022-02-08) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.22.0...v1.22.1) **Fixed Bugs:** - `\#` does not work with `visualstar` enabled [\#7463](https://github.com/VSCodeVim/Vim/issues/7463) - `\*` does not reliably update search highlights [\#7462](https://github.com/VSCodeVim/Vim/issues/7462) **Merged pull requests:** - Added documentation for complex keyboard shortcuts [\#6944](https://github.com/VSCodeVim/Vim/pull/6944) ([w-cantin](https://github.com/w-cantin)) ## [v1.22.0](https://github.com/vscodevim/vim/tree/v1.22.0) (2022-02-07) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.10...v1.22.0) **Enhancements:** - Vim filter with WSL2 is unavailable [\#7100](https://github.com/VSCodeVim/Vim/issues/7100) **Fixed Bugs:** - `O` does not properly preserve indentation [\#7423](https://github.com/VSCodeVim/Vim/issues/7423) - `:marks` to list marks doesn't work [\#7367](https://github.com/VSCodeVim/Vim/issues/7367) - `cgn` fails when the match is one character long [\#7291](https://github.com/VSCodeVim/Vim/issues/7291) - Notebook-cells duplicate marks [\#7280](https://github.com/VSCodeVim/Vim/issues/7280) - Command line `move` command throws `E488: Trailing characters` [\#7207](https://github.com/VSCodeVim/Vim/issues/7207) - Opening search in multiple editors leads to invalid state [\#7038](https://github.com/VSCodeVim/Vim/issues/7038) - `:s` does not properly handle capture groups \(`\0`, `\1`, ...\) [\#6963](https://github.com/VSCodeVim/Vim/issues/6963) - Undo doesn't work in Notebook Cell [\#6960](https://github.com/VSCodeVim/Vim/issues/6960) - Cannot paste with `\` in command-line mode on OSX. Works with `\` [\#6922](https://github.com/VSCodeVim/Vim/issues/6922) - Exclude `:w`, `:q`, and `:wq` from dot command [\#6829](https://github.com/VSCodeVim/Vim/issues/6829) **Closed issues:** - why sneak doesn't highlight matching results using different characters as expected? [\#7429](https://github.com/VSCodeVim/Vim/issues/7429) - gh to show variable value in debug mode [\#7409](https://github.com/VSCodeVim/Vim/issues/7409) - Vimrc file won't reload after saving file with vimrcPath's home environment variable [\#7359](https://github.com/VSCodeVim/Vim/issues/7359) - vim.editVimrc command failed with vimrcPath's home environment variable [\#7358](https://github.com/VSCodeVim/Vim/issues/7358) - Keep selection when copying text [\#7352](https://github.com/VSCodeVim/Vim/issues/7352) - Type lower case "j" in insert mode has strange cursor behavior. [\#7351](https://github.com/VSCodeVim/Vim/issues/7351) - `vim.mode == 'Normal'` in key binding is triggering in replace mode [\#7256](https://github.com/VSCodeVim/Vim/issues/7256) - Use a separate color for the current match while searching [\#7212](https://github.com/VSCodeVim/Vim/issues/7212) - Can't install in Azure Data Studio from VSIX package [\#7079](https://github.com/VSCodeVim/Vim/issues/7079) **Merged pull requests:** - Fixed a bug where insertLineAbove would leave extra whitespace. [\#7450](https://github.com/VSCodeVim/Vim/pull/7450) ([half-potato](https://github.com/half-potato)) - add sentence when currentChar is undefined [\#7439](https://github.com/VSCodeVim/Vim/pull/7439) ([monjara](https://github.com/monjara)) - Exclude :w, :q, and :wq from dot command [\#7428](https://github.com/VSCodeVim/Vim/pull/7428) ([justalmill](https://github.com/justalmill)) - Fixed insertLineBefore indent behavior [\#7424](https://github.com/VSCodeVim/Vim/pull/7424) ([half-potato](https://github.com/half-potato)) - Implement `inccommand` [\#7416](https://github.com/VSCodeVim/Vim/pull/7416) ([adrsm108](https://github.com/adrsm108)) - added enable key-repeating doc for Codium Exploration Users [\#7408](https://github.com/VSCodeVim/Vim/pull/7408) ([AMMAR-62](https://github.com/AMMAR-62)) - Disable other extensions while running tests for avoiding unexpected side effect [\#7376](https://github.com/VSCodeVim/Vim/pull/7376) ([waynewaynetsai](https://github.com/waynewaynetsai)) - Fix \ override system-clipboard issue for macOS users [\#7375](https://github.com/VSCodeVim/Vim/pull/7375) ([waynewaynetsai](https://github.com/waynewaynetsai)) - fix typo in README [\#7365](https://github.com/VSCodeVim/Vim/pull/7365) ([ambiguous48](https://github.com/ambiguous48)) - Fix .vimrc file's issues with vimrcPath's home environment variable [\#7360](https://github.com/VSCodeVim/Vim/pull/7360) ([waynewaynetsai](https://github.com/waynewaynetsai)) - Update README.md [\#7311](https://github.com/VSCodeVim/Vim/pull/7311) ([xerosanyam](https://github.com/xerosanyam)) - Silence failing tests on Windows and add Windows build step [\#7293](https://github.com/VSCodeVim/Vim/pull/7293) ([tagniam](https://github.com/tagniam)) - Add `vim.shell` setting for custom `!` shell [\#7255](https://github.com/VSCodeVim/Vim/pull/7255) ([tagniam](https://github.com/tagniam)) - Add silent option to key remappings [\#7253](https://github.com/VSCodeVim/Vim/pull/7253) ([mly32](https://github.com/mly32)) - Refactor `externalCommand.ts` to not use temporary files [\#7252](https://github.com/VSCodeVim/Vim/pull/7252) ([tagniam](https://github.com/tagniam)) - fix \#6922: paste with \ in command-line mode [\#7227](https://github.com/VSCodeVim/Vim/pull/7227) ([Injae-Lee](https://github.com/Injae-Lee)) - Improve incremental search [\#7224](https://github.com/VSCodeVim/Vim/pull/7224) ([adrsm108](https://github.com/adrsm108)) - search operator `\%V` [\#7215](https://github.com/VSCodeVim/Vim/pull/7215) ([elazarcoh](https://github.com/elazarcoh)) - Fix a typo in the bug report template [\#7205](https://github.com/VSCodeVim/Vim/pull/7205) ([brettcannon](https://github.com/brettcannon)) - Fix release date error [\#7178](https://github.com/VSCodeVim/Vim/pull/7178) ([oo6](https://github.com/oo6)) - Added documentation for argument text objects [\#6942](https://github.com/VSCodeVim/Vim/pull/6942) ([w-cantin](https://github.com/w-cantin)) ## [v1.21.10](https://github.com/vscodevim/vim/tree/v1.21.10) (2021-10-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.9...v1.21.10) **Fixed Bugs:** - `:tabo\[nly\]` and `:tabc\[lose\]` throw `E488` in version 1.21.9 [\#7171](https://github.com/VSCodeVim/Vim/issues/7171) ## [v1.21.9](https://github.com/vscodevim/vim/tree/v1.21.9) (2021-10-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.8...v1.21.9) **Fixed Bugs:** - /pattern/s/.../.../ doesn't work [\#7151](https://github.com/VSCodeVim/Vim/issues/7151) - 1.21.8 does not work in web worker anymore [\#7150](https://github.com/VSCodeVim/Vim/issues/7150) - `\*` throws an error when `wordSeparators` doesn't have `/` [\#7135](https://github.com/VSCodeVim/Vim/issues/7135) - `iskeyword` doesn't work for multiple languages [\#7123](https://github.com/VSCodeVim/Vim/issues/7123) - Ex "copy" command with `.`, `-`, or `+` \(current, previous, or next line\) at end of command stopped working [\#7058](https://github.com/VSCodeVim/Vim/issues/7058) **Closed issues:** - README.md missing installation item: linux setup [\#7080](https://github.com/VSCodeVim/Vim/issues/7080) **Merged pull requests:** - Load process polyfill automatically, required by util [\#7156](https://github.com/VSCodeVim/Vim/pull/7156) ([jeanp413](https://github.com/jeanp413)) - Add pane resize keybindings [\#7138](https://github.com/VSCodeVim/Vim/pull/7138) ([tagniam](https://github.com/tagniam)) - Update ROADMAP.ZH.md [\#7137](https://github.com/VSCodeVim/Vim/pull/7137) ([hellorayza](https://github.com/hellorayza)) - iskeyword is evaluated when a command is called \(\#7123\) [\#7126](https://github.com/VSCodeVim/Vim/pull/7126) ([shinichy](https://github.com/shinichy)) - Fix bang command with ranges [\#7122](https://github.com/VSCodeVim/Vim/pull/7122) ([tagniam](https://github.com/tagniam)) - \#6553: capture file mode and restore it after force write [\#7092](https://github.com/VSCodeVim/Vim/pull/7092) ([joecrop](https://github.com/joecrop)) ## [v1.21.8](https://github.com/vscodevim/vim/tree/v1.21.8) (2021-09-29) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.7...v1.21.8) **Enhancements:** - Support `:substitute`'s `n` flag \(count matches without substituting\) [\#7081](https://github.com/VSCodeVim/Vim/issues/7081) - Support `\['` and `\]'` \(move to nearby lowercase mark\) commands [\#7041](https://github.com/VSCodeVim/Vim/issues/7041) **Closed issues:** - Inconsistent indentation? [\#7107](https://github.com/VSCodeVim/Vim/issues/7107) - Cannot change to normal mode. [\#7106](https://github.com/VSCodeVim/Vim/issues/7106) - Simple movement like HJKL should not be recorded in jump history for Ctrl-O and Ctrl-I [\#7102](https://github.com/VSCodeVim/Vim/issues/7102) **Merged pull requests:** - fix ROADMAP.md typo [\#7066](https://github.com/VSCodeVim/Vim/pull/7066) ([mly32](https://github.com/mly32)) - make vim strict ui extension [\#7049](https://github.com/VSCodeVim/Vim/pull/7049) ([sandy081](https://github.com/sandy081)) - Added documentation for all Vim Modes [\#6945](https://github.com/VSCodeVim/Vim/pull/6945) ([w-cantin](https://github.com/w-cantin)) ## [v1.21.7](https://github.com/vscodevim/vim/tree/v1.21.7) (2021-08-31) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.6...v1.21.7) **Enhancements:** - `:delete` and `:yank` should support `{count}` argument [\#6995](https://github.com/VSCodeVim/Vim/issues/6995) **Fixed Bugs:** - Failed to handle key=\. Cannot read property 'end' of undefined [\#7027](https://github.com/VSCodeVim/Vim/issues/7027) - Failed to handle key=\. e.getTransformation is not a function [\#7009](https://github.com/VSCodeVim/Vim/issues/7009) **Closed issues:** - Why vim-surround command csw" \(word surround\) is not working now? [\#7003](https://github.com/VSCodeVim/Vim/issues/7003) - Allow for appending to \[a-z\] registers [\#6965](https://github.com/VSCodeVim/Vim/issues/6965) **Merged pull requests:** - Show command and search when showmodename is disabled [\#7021](https://github.com/VSCodeVim/Vim/pull/7021) ([BlakeWilliams](https://github.com/BlakeWilliams)) - Adds count argument to `:yank` and `:delete` commands [\#7007](https://github.com/VSCodeVim/Vim/pull/7007) ([DevinLeamy](https://github.com/DevinLeamy)) - fix: \ behavior in replace mode [\#6997](https://github.com/VSCodeVim/Vim/pull/6997) ([Komar0ff](https://github.com/Komar0ff)) - Append to \[a-z\] registers [\#6971](https://github.com/VSCodeVim/Vim/pull/6971) ([DevinLeamy](https://github.com/DevinLeamy)) ## [v1.21.6](https://github.com/vscodevim/vim/tree/v1.21.6) (2021-08-11) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.5...v1.21.6) **Fixed Bugs:** - Backslashes must be duplicated in :s substitution [\#6890](https://github.com/VSCodeVim/Vim/issues/6890) - Failed to handle key=\. Overlapping ranges are not allowed! [\#6888](https://github.com/VSCodeVim/Vim/issues/6888) - Failed to handle key=:. No cursor index - this should never ever happen! [\#6887](https://github.com/VSCodeVim/Vim/issues/6887) - `:marks` show error position when focusing on another file [\#6886](https://github.com/VSCodeVim/Vim/issues/6886) - Failed to handle key=.. Illegal argument: line must be non-negative [\#6870](https://github.com/VSCodeVim/Vim/issues/6870) - Repeating with `.` does not play nice with auto-matching quotes [\#6819](https://github.com/VSCodeVim/Vim/issues/6819) **Closed issues:** - s [\#6959](https://github.com/VSCodeVim/Vim/issues/6959) - Make "gd" Open definition to the side in Search Editor [\#6921](https://github.com/VSCodeVim/Vim/issues/6921) - Failed to handle key=\. Could NOT open editor for "file:///home/fabrice/CRIStAL/Speed/examples/train_example.py". [\#6868](https://github.com/VSCodeVim/Vim/issues/6868) - Failed to handle key=2. Cannot read property 'length' of undefined [\#6861](https://github.com/VSCodeVim/Vim/issues/6861) - Failed to handle key=.. Overlapping ranges are not allowed! [\#6840](https://github.com/VSCodeVim/Vim/issues/6840) **Merged pull requests:** - Fix history navigation in VS Code interactive window [\#6980](https://github.com/VSCodeVim/Vim/pull/6980) ([rebornix](https://github.com/rebornix)) - Remove look behind for Safari [\#6937](https://github.com/VSCodeVim/Vim/pull/6937) ([rebornix](https://github.com/rebornix)) - Fix /\\c by requiring odd number of \'s before c for case \(in\)sensitivity [\#6900](https://github.com/VSCodeVim/Vim/pull/6900) ([edemaine](https://github.com/edemaine)) - Fix escaping in :s substitutions [\#6891](https://github.com/VSCodeVim/Vim/pull/6891) ([edemaine](https://github.com/edemaine)) - Argument text object documentation [\#6857](https://github.com/VSCodeVim/Vim/pull/6857) ([w-cantin](https://github.com/w-cantin)) - Add debuggingForeground to colorCustomizations [\#6852](https://github.com/VSCodeVim/Vim/pull/6852) ([lmlorca](https://github.com/lmlorca)) ## [v1.21.5](https://github.com/vscodevim/vim/tree/v1.21.5) (2021-07-06) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.4...v1.21.5) **Fixed Bugs:** - :sort u merges two duplicates [\#6825](https://github.com/VSCodeVim/Vim/issues/6825) - setting space as leader key does not work anymore [\#6824](https://github.com/VSCodeVim/Vim/issues/6824) - Problems with \ key and remapping in the latest version [\#6821](https://github.com/VSCodeVim/Vim/issues/6821) **Merged pull requests:** - Fix sort unique bug [\#6835](https://github.com/VSCodeVim/Vim/pull/6835) ([sixskys](https://github.com/sixskys)) ## [v1.21.4](https://github.com/vscodevim/vim/tree/v1.21.4) (2021-07-02) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.3...v1.21.4) **Fixed Bugs:** - `2i"` should act like `a"`, but exclude whitespace before/after the quotes [\#6806](https://github.com/VSCodeVim/Vim/issues/6806) ## [v1.21.3](https://github.com/vscodevim/vim/tree/v1.21.3) (2021-06-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.2...v1.21.3) ## [v1.21.2](https://github.com/vscodevim/vim/tree/v1.21.2) (2021-06-11) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.1...v1.21.2) ## [v1.21.1](https://github.com/vscodevim/vim/tree/v1.21.1) (2021-06-10) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.21.0...v1.21.1) ## [v1.21.0](https://github.com/vscodevim/vim/tree/v1.21.0) (2021-06-09) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.20.3...v1.21.0) ## [v1.20.3](https://github.com/vscodevim/vim/tree/v1.20.3) (2021-05-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.20.2...v1.20.3) ## [v1.20.2](https://github.com/vscodevim/vim/tree/v1.20.2) (2021-04-30) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.20.1...v1.20.2) ## [v1.20.1](https://github.com/vscodevim/vim/tree/v1.20.1) (2021-04-25) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.20.0...v1.20.1) ## [v1.20.0](https://github.com/vscodevim/vim/tree/v1.20.0) (2021-04-17) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.19.3...v1.20.0) ## [v1.19.3](https://github.com/vscodevim/vim/tree/v1.19.3) (2021-03-29) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.19.2...v1.19.3) ## [v1.19.2](https://github.com/vscodevim/vim/tree/v1.19.2) (2021-03-24) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.19.1...v1.19.2) ## [v1.19.1](https://github.com/vscodevim/vim/tree/v1.19.1) (2021-03-21) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.19.0...v1.19.1) ## [v1.19.0](https://github.com/vscodevim/vim/tree/v1.19.0) (2021-03-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.18.9...v1.19.0) ## [v1.18.9](https://github.com/vscodevim/vim/tree/v1.18.9) (2021-02-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.18.8...v1.18.9) ## [v1.18.8](https://github.com/vscodevim/vim/tree/v1.18.8) (2021-02-02) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.18.7...v1.18.8) ## [v1.18.7](https://github.com/vscodevim/vim/tree/v1.18.7) (2021-02-01) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.18.5...v1.18.7) ## [v1.18.5](https://github.com/vscodevim/vim/tree/v1.18.5) (2020-12-10) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.18.4...v1.18.5) ## [v1.18.4](https://github.com/vscodevim/vim/tree/v1.18.4) (2020-12-07) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.18.3...v1.18.4) ## [v1.18.3](https://github.com/vscodevim/vim/tree/v1.18.3) (2020-12-07) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.18.2...v1.18.3) ## [v1.18.2](https://github.com/vscodevim/vim/tree/v1.18.2) (2020-12-07) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.18.0...v1.18.2) ## [v1.18.0](https://github.com/vscodevim/vim/tree/v1.18.0) (2020-12-06) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.17.1...v1.18.0) ## [v1.17.1](https://github.com/vscodevim/vim/tree/v1.17.1) (2020-09-25) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.17.0...v1.17.1) ## [v1.17.0](https://github.com/vscodevim/vim/tree/v1.17.0) (2020-09-22) [Full Changelog](https://github.com/vscodevim/vim/compare/beta...v1.17.0) ## [beta](https://github.com/vscodevim/vim/tree/beta) (2020-09-21) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.16.0...beta) ## [v1.11.0](https://github.com/vscodevim/vim/tree/v1.11.0) (2019-09-28) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.10.2...v1.11.0) **Enhancements:** - Support VSCode's View: Toggle Panel in vim mode. [\#4103](https://github.com/VSCodeVim/Vim/issues/4103) - Store subparsers in terms of abbreviation and full command [\#4094](https://github.com/VSCodeVim/Vim/issues/4094) - directories are un-completable with tab-completion [\#4085](https://github.com/VSCodeVim/Vim/issues/4085) - Command mode status bar is too small [\#4077](https://github.com/VSCodeVim/Vim/issues/4077) - set cursorcolumn [\#4076](https://github.com/VSCodeVim/Vim/issues/4076) - Support for whichwarp [\#4068](https://github.com/VSCodeVim/Vim/issues/4068) - Command line does not support Ctrl-W [\#4027](https://github.com/VSCodeVim/Vim/issues/4027) - Add setting to swap ; with : in Easymotion [\#4020](https://github.com/VSCodeVim/Vim/issues/4020) - Allow for placeholders in rebindings [\#4012](https://github.com/VSCodeVim/Vim/issues/4012) - Support :his\[tory\] [\#3949](https://github.com/VSCodeVim/Vim/issues/3949) - Support gdefault option [\#3594](https://github.com/VSCodeVim/Vim/issues/3594) **Fixed Bugs:** - Find and replace all occurances in current line does not work [\#4067](https://github.com/VSCodeVim/Vim/issues/4067) - Commentary does not work in visual block mode [\#4036](https://github.com/VSCodeVim/Vim/issues/4036) - Change operator doesn't behave linewise when appropriate [\#4024](https://github.com/VSCodeVim/Vim/issues/4024) - \$ command takes newline in visual mode [\#3970](https://github.com/VSCodeVim/Vim/issues/3970) - Text reflow doesn't respect tabs [\#3929](https://github.com/VSCodeVim/Vim/issues/3929) - commands \(d, y, c...\) don't work with the smart selection [\#3850](https://github.com/VSCodeVim/Vim/issues/3850) - :split Can't Open Files With Names That Include Spaces [\#3824](https://github.com/VSCodeVim/Vim/issues/3824) - Unexpected jumping after deleting a line with 'd-d' [\#3804](https://github.com/VSCodeVim/Vim/issues/3804) - jk doesn't respect tab size [\#3796](https://github.com/VSCodeVim/Vim/issues/3796) - 'dd' followed by any character jumps cursor to end of file. [\#3713](https://github.com/VSCodeVim/Vim/issues/3713) - In ctrl v mode, c doesn't change all instances [\#3601](https://github.com/VSCodeVim/Vim/issues/3601) **Closed issues:** - gf doesn't work for files not from current directory [\#4099](https://github.com/VSCodeVim/Vim/issues/4099) - ViM extension makes VSCode super slow, typing is almost impossible. [\#4088](https://github.com/VSCodeVim/Vim/issues/4088) - mapping control-something to escape in insert doesn't work [\#4062](https://github.com/VSCodeVim/Vim/issues/4062) - When Overtype extension presents, VSCodeVim stops working. [\#4046](https://github.com/VSCodeVim/Vim/issues/4046) - \ in search mode doesn't respect cursor position [\#4044](https://github.com/VSCodeVim/Vim/issues/4044) - Tests for special keys on command line [\#4040](https://github.com/VSCodeVim/Vim/issues/4040) - Cannot find module 'winston-transport' [\#4029](https://github.com/VSCodeVim/Vim/issues/4029) - How to re-map ":e" to ":w"? [\#4026](https://github.com/VSCodeVim/Vim/issues/4026) - Ctrl+h ignores useCtrlKeys and handleKeys binds [\#4019](https://github.com/VSCodeVim/Vim/issues/4019) - It is possible to scroll the cursor out of screen [\#3846](https://github.com/VSCodeVim/Vim/issues/3846) - ModeHandler messages not coming through debug console [\#3828](https://github.com/VSCodeVim/Vim/issues/3828) - :o fails in remote SSH [\#3815](https://github.com/VSCodeVim/Vim/issues/3815) - Being able to disable VIM on startup [\#3783](https://github.com/VSCodeVim/Vim/issues/3783) - Autocomplete feature [\#3570](https://github.com/VSCodeVim/Vim/issues/3570) **Merged pull requests:** - Use command abbreviations [\#4106](https://github.com/VSCodeVim/Vim/pull/4106) ([J-Fields](https://github.com/J-Fields)) - Update dependency @types/node to v12.7.8 [\#4100](https://github.com/VSCodeVim/Vim/pull/4100) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/node to v12.7.7 [\#4097](https://github.com/VSCodeVim/Vim/pull/4097) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency sinon to v7.5.0 [\#4095](https://github.com/VSCodeVim/Vim/pull/4095) ([renovate[bot]](https://github.com/apps/renovate)) - Tests for special keys on the command line [\#4090](https://github.com/VSCodeVim/Vim/pull/4090) ([J-Fields](https://github.com/J-Fields)) - Add shift+tab support for cmd line [\#4089](https://github.com/VSCodeVim/Vim/pull/4089) ([stevenguh](https://github.com/stevenguh)) - Update dependency ts-loader to v6.1.2 [\#4087](https://github.com/VSCodeVim/Vim/pull/4087) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency ts-loader to v6.1.1 [\#4084](https://github.com/VSCodeVim/Vim/pull/4084) ([renovate[bot]](https://github.com/apps/renovate)) - Add missing `to` in CONTRIBUTING.md [\#4080](https://github.com/VSCodeVim/Vim/pull/4080) ([caleywoods](https://github.com/caleywoods)) - Fix incorrect position when editing the same file in 2 splits [\#4074](https://github.com/VSCodeVim/Vim/pull/4074) ([uHOOCCOOHu](https://github.com/uHOOCCOOHu)) - Smile command [\#4070](https://github.com/VSCodeVim/Vim/pull/4070) ([caleywoods](https://github.com/caleywoods)) - Update dependency @types/node to v12.7.5 [\#4066](https://github.com/VSCodeVim/Vim/pull/4066) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency ts-loader to v6.1.0 [\#4065](https://github.com/VSCodeVim/Vim/pull/4065) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency typescript to v3.6.3 [\#4064](https://github.com/VSCodeVim/Vim/pull/4064) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency tslint to v5.20.0 [\#4060](https://github.com/VSCodeVim/Vim/pull/4060) ([renovate[bot]](https://github.com/apps/renovate)) - Don't use lodash for things ES6 supports natively [\#4056](https://github.com/VSCodeVim/Vim/pull/4056) ([J-Fields](https://github.com/J-Fields)) - Pin dependencies [\#4051](https://github.com/VSCodeVim/Vim/pull/4051) ([renovate[bot]](https://github.com/apps/renovate)) - Fix gq to handle tab indentation [\#4050](https://github.com/VSCodeVim/Vim/pull/4050) ([orn688](https://github.com/orn688)) - Add flag to replace `f` with a single-character sneak [\#4048](https://github.com/VSCodeVim/Vim/pull/4048) ([J-Fields](https://github.com/J-Fields)) - \ doesn't respect the cursor in search mode [\#4045](https://github.com/VSCodeVim/Vim/pull/4045) ([stevenguh](https://github.com/stevenguh)) - Fix dependencies [\#4037](https://github.com/VSCodeVim/Vim/pull/4037) ([J-Fields](https://github.com/J-Fields)) - Update dependency @types/node to v12.7.4 [\#4033](https://github.com/VSCodeVim/Vim/pull/4033) ([renovate[bot]](https://github.com/apps/renovate)) - Refactor the existing file opening and auto completion [\#4032](https://github.com/VSCodeVim/Vim/pull/4032) ([stevenguh](https://github.com/stevenguh)) - Remove word in command line with \ [\#4031](https://github.com/VSCodeVim/Vim/pull/4031) ([stevenguh](https://github.com/stevenguh)) - Update dependency sinon to v7.4.2 [\#4030](https://github.com/VSCodeVim/Vim/pull/4030) ([renovate[bot]](https://github.com/apps/renovate)) - Implement `nowrapscan` [\#4028](https://github.com/VSCodeVim/Vim/pull/4028) ([contrib15](https://github.com/contrib15)) - linewise change operator [\#4025](https://github.com/VSCodeVim/Vim/pull/4025) ([JoshuaRichards](https://github.com/JoshuaRichards)) - Fix gj/gk so it maintains cursor position [\#3890](https://github.com/VSCodeVim/Vim/pull/3890) ([hetmankp](https://github.com/hetmankp)) - WebPack builds for improved loading times [\#3889](https://github.com/VSCodeVim/Vim/pull/3889) ([ianjfrosst](https://github.com/ianjfrosst)) ## [v1.10.2](https://github.com/vscodevim/vim/tree/v1.10.2) (2019-09-01) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.10.1...v1.10.2) **Closed issues:** - Cut release 1.10.1 [\#4022](https://github.com/VSCodeVim/Vim/issues/4022) **Merged pull requests:** - Fix case sensitive sorting [\#4023](https://github.com/VSCodeVim/Vim/pull/4023) ([noslaver](https://github.com/noslaver)) ## [v1.10.1](https://github.com/vscodevim/vim/tree/v1.10.1) (2019-08-31) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.10.0...v1.10.1) **Fixed Bugs:** - ReplaceWithRegister doesn't work in visual mode [\#4015](https://github.com/VSCodeVim/Vim/issues/4015) - \ not working in 1.10.0 [\#4011](https://github.com/VSCodeVim/Vim/issues/4011) - zh/zl/zH/zL not working properly [\#4008](https://github.com/VSCodeVim/Vim/issues/4008) **Closed issues:** - Ctrl-P and Ctrl-N can‘t work in the latest version [\#4017](https://github.com/VSCodeVim/Vim/issues/4017) - d Command Removes Mode Text [\#3781](https://github.com/VSCodeVim/Vim/issues/3781) - Yanking "clears" mode \(or makes it disappear\) from status bar until INSERT mode [\#3488](https://github.com/VSCodeVim/Vim/issues/3488) **Merged pull requests:** - Update dependency @types/node to v12.7.3 [\#4021](https://github.com/VSCodeVim/Vim/pull/4021) ([renovate[bot]](https://github.com/apps/renovate)) - Make ReplaceWithRegister work in visual mode [\#4016](https://github.com/VSCodeVim/Vim/pull/4016) ([stevenguh](https://github.com/stevenguh)) - :w write in background [\#4013](https://github.com/VSCodeVim/Vim/pull/4013) ([stevenguh](https://github.com/stevenguh)) - Update dependency typescript to v3.6.2 [\#4010](https://github.com/VSCodeVim/Vim/pull/4010) ([renovate[bot]](https://github.com/apps/renovate)) ## [v1.10.0](https://github.com/vscodevim/vim/tree/v1.10.0) (2019-08-28) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.9.0...v1.10.0) **Enhancements:** - \ and \ should be equivalent to \ and \ on command line / search bar [\#3995](https://github.com/VSCodeVim/Vim/issues/3995) - Support `when` for contextual keybindings [\#3994](https://github.com/VSCodeVim/Vim/issues/3994) - Del should work on command/search line [\#3992](https://github.com/VSCodeVim/Vim/issues/3992) - Home/End should work on command/search line [\#3991](https://github.com/VSCodeVim/Vim/issues/3991) - `Ctrl-R` should allow pasting from a register when typing a command, as in insert mode [\#3950](https://github.com/VSCodeVim/Vim/issues/3950) - Ctrl-P and Ctrl-N should be equivalent to Up / Down when entering a command or search [\#3942](https://github.com/VSCodeVim/Vim/issues/3942) - Support ignorecase for sort command [\#3939](https://github.com/VSCodeVim/Vim/issues/3939) - Support search offsets [\#3917](https://github.com/VSCodeVim/Vim/issues/3917) - Enhancement: sneak one char jump. [\#3907](https://github.com/VSCodeVim/Vim/issues/3907) - Simple undo command behaviour from vi/vim not implemented [\#3649](https://github.com/VSCodeVim/Vim/issues/3649) **Fixed Bugs:** - Variable highlighting not working [\#3982](https://github.com/VSCodeVim/Vim/issues/3982) - Change side in diff mode [\#3979](https://github.com/VSCodeVim/Vim/issues/3979) - Annoying brackets autoremoving [\#3936](https://github.com/VSCodeVim/Vim/issues/3936) - "Search forward" functionality is not case sensitive [\#3764](https://github.com/VSCodeVim/Vim/issues/3764) - Does not start up with VSCode and no vim commands work [\#3753](https://github.com/VSCodeVim/Vim/issues/3753) **Closed issues:** - `/` is not case sensitive [\#3980](https://github.com/VSCodeVim/Vim/issues/3980) - Will VIM extension be compatible with python interactive window in the next update? [\#3973](https://github.com/VSCodeVim/Vim/issues/3973) - visual mode block copy/past [\#3971](https://github.com/VSCodeVim/Vim/issues/3971) - range yank does not work [\#3931](https://github.com/VSCodeVim/Vim/issues/3931) - Console warning [\#3926](https://github.com/VSCodeVim/Vim/issues/3926) - :wq does not close window if there are unsaved changes [\#3922](https://github.com/VSCodeVim/Vim/issues/3922) - make easymotion looks exactly the vim-easymotion way [\#3901](https://github.com/VSCodeVim/Vim/issues/3901) - bug to record macro [\#3898](https://github.com/VSCodeVim/Vim/issues/3898) - Faulty link in readme [\#3827](https://github.com/VSCodeVim/Vim/issues/3827) - Navigation in the explorer pane vim way \(j , k\) doesn't work after window reload [\#3760](https://github.com/VSCodeVim/Vim/issues/3760) - Easy motion shows error when jumping to brackets and backslash [\#3685](https://github.com/VSCodeVim/Vim/issues/3685) - I can't continuous movement the cursor ,and copy or delete more line. [\#3634](https://github.com/VSCodeVim/Vim/issues/3634) - Why don't work command mode? [\#3500](https://github.com/VSCodeVim/Vim/issues/3500) - Tab completion for `:vnew` and `:tabnew` [\#3479](https://github.com/VSCodeVim/Vim/issues/3479) - Yank lines in 1 window should be available for pasting in another window [\#3401](https://github.com/VSCodeVim/Vim/issues/3401) **Merged pull requests:** - Update dependency @types/lodash to v4.14.138 [\#4003](https://github.com/VSCodeVim/Vim/pull/4003) ([renovate[bot]](https://github.com/apps/renovate)) - Fix typo in README.md [\#4002](https://github.com/VSCodeVim/Vim/pull/4002) ([jedevc](https://github.com/jedevc)) - Implement single char sneak [\#3999](https://github.com/VSCodeVim/Vim/pull/3999) ([JohnnyUrosevic](https://github.com/JohnnyUrosevic)) - fix :wq in remote [\#3998](https://github.com/VSCodeVim/Vim/pull/3998) ([stevenguh](https://github.com/stevenguh)) - Update dependency tslint to v5.19.0 [\#3987](https://github.com/VSCodeVim/Vim/pull/3987) ([renovate[bot]](https://github.com/apps/renovate)) - Fix console warning [\#3985](https://github.com/VSCodeVim/Vim/pull/3985) ([huww98](https://github.com/huww98)) - Fix duplicated command added in c542b42 [\#3984](https://github.com/VSCodeVim/Vim/pull/3984) ([huww98](https://github.com/huww98)) - Update dependency @types/lodash to v4.14.137 [\#3983](https://github.com/VSCodeVim/Vim/pull/3983) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/node to v12.7.2 [\#3981](https://github.com/VSCodeVim/Vim/pull/3981) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/node to v12.7.1 [\#3967](https://github.com/VSCodeVim/Vim/pull/3967) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/node to v12.7.0 [\#3964](https://github.com/VSCodeVim/Vim/pull/3964) ([renovate[bot]](https://github.com/apps/renovate)) - Disallow all forms of :help [\#3962](https://github.com/VSCodeVim/Vim/pull/3962) ([J-Fields](https://github.com/J-Fields)) - Be clear in package.json that vim.statusBarColorControl reduces performance [\#3961](https://github.com/VSCodeVim/Vim/pull/3961) ([J-Fields](https://github.com/J-Fields)) - Update dependency sinon to v7.4.1 [\#3958](https://github.com/VSCodeVim/Vim/pull/3958) ([renovate[bot]](https://github.com/apps/renovate)) - Implement `q/` and `q?` [\#3956](https://github.com/VSCodeVim/Vim/pull/3956) ([J-Fields](https://github.com/J-Fields)) - When the `c` \(confirm\) flag is used in a `:s` command, don't use neovim [\#3955](https://github.com/VSCodeVim/Vim/pull/3955) ([J-Fields](https://github.com/J-Fields)) - `\` shows command history when pressed on command line [\#3954](https://github.com/VSCodeVim/Vim/pull/3954) ([J-Fields](https://github.com/J-Fields)) - Fix `gC` in visual mode [\#3948](https://github.com/VSCodeVim/Vim/pull/3948) ([J-Fields](https://github.com/J-Fields)) - Roll back dependency sinon to 7.3.2 [\#3947](https://github.com/VSCodeVim/Vim/pull/3947) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency sinon to v7.4.0 [\#3944](https://github.com/VSCodeVim/Vim/pull/3944) ([renovate[bot]](https://github.com/apps/renovate)) - Allow \ and \ to be used as prev/next when entering a command or search [\#3943](https://github.com/VSCodeVim/Vim/pull/3943) ([J-Fields](https://github.com/J-Fields)) - Respect `editor.autoClosingBrackets` and `editor.autoClosingQuotes` when deleting a bracket/quote [\#3941](https://github.com/VSCodeVim/Vim/pull/3941) ([J-Fields](https://github.com/J-Fields)) - added option to ignore case when sorting [\#3938](https://github.com/VSCodeVim/Vim/pull/3938) ([noslaver](https://github.com/noslaver)) - Update dependency @types/node to v12.6.9 [\#3937](https://github.com/VSCodeVim/Vim/pull/3937) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency vscode to v1.1.36 [\#3933](https://github.com/VSCodeVim/Vim/pull/3933) ([renovate[bot]](https://github.com/apps/renovate)) - Implement search offsets [\#3918](https://github.com/VSCodeVim/Vim/pull/3918) ([J-Fields](https://github.com/J-Fields)) ## [v1.9.0](https://github.com/vscodevim/vim/tree/v1.9.0) (2019-07-29) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.8.2...v1.9.0) **Enhancements:** - Support ampersand \("&"\) action in normal mode [\#3808](https://github.com/VSCodeVim/Vim/issues/3808) **Fixed Bugs:** - At beginning of line with all spaces, backspace causes error [\#3915](https://github.com/VSCodeVim/Vim/issues/3915) - Go to Line Using \[line\]+gg Throws Exception [\#3845](https://github.com/VSCodeVim/Vim/issues/3845) - Easymotion uses RegExp [\#3844](https://github.com/VSCodeVim/Vim/issues/3844) - `%` doesn't ignore unmatched `\>` [\#3807](https://github.com/VSCodeVim/Vim/issues/3807) - Regression: full path to nvim is now required [\#3754](https://github.com/VSCodeVim/Vim/issues/3754) **Closed issues:** - Mapping s in Visual Mode causes strange mistake [\#3788](https://github.com/VSCodeVim/Vim/issues/3788) **Merged pull requests:** - Make `C` work with registers [\#3927](https://github.com/VSCodeVim/Vim/pull/3927) ([J-Fields](https://github.com/J-Fields)) - Implement ampersand \(&\) action [\#3925](https://github.com/VSCodeVim/Vim/pull/3925) ([J-Fields](https://github.com/J-Fields)) - Move prettier configuration to .prettierrc [\#3921](https://github.com/VSCodeVim/Vim/pull/3921) ([kizza](https://github.com/kizza)) - Handle backspace on first character of all-space line correctly [\#3916](https://github.com/VSCodeVim/Vim/pull/3916) ([J-Fields](https://github.com/J-Fields)) - Fix f/F/t/T with \ [\#3914](https://github.com/VSCodeVim/Vim/pull/3914) ([J-Fields](https://github.com/J-Fields)) - Make `%` skip over characters such as '\>' [\#3913](https://github.com/VSCodeVim/Vim/pull/3913) ([J-Fields](https://github.com/J-Fields)) - Do not treat easymotion input as regex unless it's a letter [\#3911](https://github.com/VSCodeVim/Vim/pull/3911) ([J-Fields](https://github.com/J-Fields)) - fix\(deps\): update dependency lodash to v4.17.15 [\#3906](https://github.com/VSCodeVim/Vim/pull/3906) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency mocha to v6.2.0 [\#3905](https://github.com/VSCodeVim/Vim/pull/3905) ([renovate[bot]](https://github.com/apps/renovate)) - Fixes \#3754. Don't require full path to neovim [\#3903](https://github.com/VSCodeVim/Vim/pull/3903) ([notskm](https://github.com/notskm)) - chore\(deps\): update dependency @types/node to v12.6.8 [\#3902](https://github.com/VSCodeVim/Vim/pull/3902) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/node to v12.6.6 [\#3897](https://github.com/VSCodeVim/Vim/pull/3897) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/node to v12.6.4 [\#3896](https://github.com/VSCodeVim/Vim/pull/3896) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/node to v12.6.3 [\#3893](https://github.com/VSCodeVim/Vim/pull/3893) ([renovate[bot]](https://github.com/apps/renovate)) - Add ReplaceWithRegister plugin [\#3887](https://github.com/VSCodeVim/Vim/pull/3887) ([kizza](https://github.com/kizza)) ## [v1.8.2](https://github.com/vscodevim/vim/tree/v1.8.2) (2019-07-15) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.8.1...v1.8.2) **Fixed Bugs:** - GoToDefinition make invalid history when use C\# extension [\#3865](https://github.com/VSCodeVim/Vim/issues/3865) - Invisible "WORD" in roadmap [\#3823](https://github.com/VSCodeVim/Vim/issues/3823) **Closed issues:** - Identifier highlights do not appear with keyboard movement [\#3885](https://github.com/VSCodeVim/Vim/issues/3885) - Cursor width when indent using tabs [\#3856](https://github.com/VSCodeVim/Vim/issues/3856) - cw without yank [\#3836](https://github.com/VSCodeVim/Vim/issues/3836) - Frozen in 'Activating Extensions' [\#3826](https://github.com/VSCodeVim/Vim/issues/3826) - How can we make a normal-mode shift-enter mapping? [\#3814](https://github.com/VSCodeVim/Vim/issues/3814) - Input response is too slow after updating vsc to the latest version\(1.34.0\) [\#3810](https://github.com/VSCodeVim/Vim/issues/3810) - Yank + motion only working partially [\#3794](https://github.com/VSCodeVim/Vim/issues/3794) - vim mode does not work after upgrading to 1.8.1 [\#3791](https://github.com/VSCodeVim/Vim/issues/3791) - Save File Using leader leader [\#3790](https://github.com/VSCodeVim/Vim/issues/3790) - space + tab transforme to solo tab [\#3789](https://github.com/VSCodeVim/Vim/issues/3789) - Unable to replace single quotes surrounding string with double quotes like I can in Vim [\#3657](https://github.com/VSCodeVim/Vim/issues/3657) - cannot bind "," [\#3565](https://github.com/VSCodeVim/Vim/issues/3565) **Merged pull requests:** - fix\(deps\): update dependency lodash to v4.17.14 [\#3884](https://github.com/VSCodeVim/Vim/pull/3884) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/node to v12.6.2 [\#3882](https://github.com/VSCodeVim/Vim/pull/3882) ([renovate[bot]](https://github.com/apps/renovate)) - fix\(deps\): update dependency lodash to v4.17.13 [\#3881](https://github.com/VSCodeVim/Vim/pull/3881) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency typescript to v3.5.3 [\#3878](https://github.com/VSCodeVim/Vim/pull/3878) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/lodash to v4.14.136 [\#3877](https://github.com/VSCodeVim/Vim/pull/3877) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/node to v12.6.1 [\#3876](https://github.com/VSCodeVim/Vim/pull/3876) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency tslint to v5.18.0 [\#3874](https://github.com/VSCodeVim/Vim/pull/3874) ([renovate[bot]](https://github.com/apps/renovate)) - fix: fix build break [\#3873](https://github.com/VSCodeVim/Vim/pull/3873) ([jpoon](https://github.com/jpoon)) - chore: fix URL for input method setting [\#3870](https://github.com/VSCodeVim/Vim/pull/3870) ([AndersDJohnson](https://github.com/AndersDJohnson)) - Assign activeTextEditor to local variable first. [\#3866](https://github.com/VSCodeVim/Vim/pull/3866) ([yaegaki](https://github.com/yaegaki)) - Update dependency @types/node to v12.0.12 [\#3862](https://github.com/VSCodeVim/Vim/pull/3862) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/node to v12.0.11 [\#3861](https://github.com/VSCodeVim/Vim/pull/3861) ([renovate[bot]](https://github.com/apps/renovate)) - fix log message for 'vim.debug.silent' [\#3859](https://github.com/VSCodeVim/Vim/pull/3859) ([stfnwp](https://github.com/stfnwp)) - Update dependency @types/node to v12.0.10 [\#3858](https://github.com/VSCodeVim/Vim/pull/3858) ([renovate-bot](https://github.com/renovate-bot)) - Fix build per microsoft/vscode\#75873 [\#3857](https://github.com/VSCodeVim/Vim/pull/3857) ([octref](https://github.com/octref)) - Update dependency vscode to v1.1.35 [\#3855](https://github.com/VSCodeVim/Vim/pull/3855) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/lodash to v4.14.135 [\#3854](https://github.com/VSCodeVim/Vim/pull/3854) ([renovate[bot]](https://github.com/apps/renovate)) - pull request to fix the issue \#3845 [\#3853](https://github.com/VSCodeVim/Vim/pull/3853) ([zhuzisheng](https://github.com/zhuzisheng)) - upgrade pkgs [\#3843](https://github.com/VSCodeVim/Vim/pull/3843) ([jpoon](https://github.com/jpoon)) - Fix broken links in README.md [\#3842](https://github.com/VSCodeVim/Vim/pull/3842) ([aquova](https://github.com/aquova)) - Update dependency typescript to v3.5.2 [\#3834](https://github.com/VSCodeVim/Vim/pull/3834) ([renovate[bot]](https://github.com/apps/renovate)) - Fix WORD wrapped in pipes [\#3829](https://github.com/VSCodeVim/Vim/pull/3829) ([scebotari66](https://github.com/scebotari66)) - Update dependency prettier to v1.18.2 [\#3819](https://github.com/VSCodeVim/Vim/pull/3819) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency prettier to v1.18.0 [\#3818](https://github.com/VSCodeVim/Vim/pull/3818) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/lodash to v4.14.134 [\#3817](https://github.com/VSCodeVim/Vim/pull/3817) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/lodash to v4.14.133 [\#3802](https://github.com/VSCodeVim/Vim/pull/3802) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency tslint to v5.17.0 [\#3801](https://github.com/VSCodeVim/Vim/pull/3801) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/mocha to v5.2.7 [\#3800](https://github.com/VSCodeVim/Vim/pull/3800) ([renovate[bot]](https://github.com/apps/renovate)) - Consolidate documentation for visual modes [\#3799](https://github.com/VSCodeVim/Vim/pull/3799) ([max-sixty](https://github.com/max-sixty)) - Update dependency typescript to v3.5.1 [\#3798](https://github.com/VSCodeVim/Vim/pull/3798) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/sinon to v7.0.12 [\#3795](https://github.com/VSCodeVim/Vim/pull/3795) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/lodash to v4.14.132 [\#3792](https://github.com/VSCodeVim/Vim/pull/3792) ([renovate[bot]](https://github.com/apps/renovate)) ## [v1.8.1](https://github.com/vscodevim/vim/tree/v1.8.1) (2019-05-22) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.8.0...v1.8.1) **Fixed Bugs:** - Vim extension UI "blocks" on remote development save [\#3777](https://github.com/VSCodeVim/Vim/issues/3777) - Cancelling a search should not undo :noh [\#3748](https://github.com/VSCodeVim/Vim/issues/3748) - \ and \ don't cancel search [\#3668](https://github.com/VSCodeVim/Vim/issues/3668) - \/\ don't move cursor if the first line is visible [\#3648](https://github.com/VSCodeVim/Vim/issues/3648) - vim.statusBarColors.normal reports type error [\#3607](https://github.com/VSCodeVim/Vim/issues/3607) **Closed issues:** - Copy inside of words after typing ci" [\#3758](https://github.com/VSCodeVim/Vim/issues/3758) **Merged pull requests:** - Update dependency @types/lodash to v4.14.130 [\#3784](https://github.com/VSCodeVim/Vim/pull/3784) ([renovate[bot]](https://github.com/apps/renovate)) - Update ROADMAP.ZH.md [\#3782](https://github.com/VSCodeVim/Vim/pull/3782) ([sxlwar](https://github.com/sxlwar)) - Make the write command non-blocking on remote files [\#3778](https://github.com/VSCodeVim/Vim/pull/3778) ([suo](https://github.com/suo)) - Fix MoveHalfPageUp \(\\) when first line is visible. [\#3776](https://github.com/VSCodeVim/Vim/pull/3776) ([faldah](https://github.com/faldah)) - Update dependency @types/lodash to v4.14.129 [\#3771](https://github.com/VSCodeVim/Vim/pull/3771) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/lodash to v4.14.127 [\#3770](https://github.com/VSCodeVim/Vim/pull/3770) ([renovate[bot]](https://github.com/apps/renovate)) - Fix statusBarColors linting in vscode user settings. [\#3767](https://github.com/VSCodeVim/Vim/pull/3767) ([faldah](https://github.com/faldah)) - Update dependency prettier to v1.17.1 [\#3765](https://github.com/VSCodeVim/Vim/pull/3765) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/lodash to v4.14.126 [\#3755](https://github.com/VSCodeVim/Vim/pull/3755) ([renovate[bot]](https://github.com/apps/renovate)) - Make sure :noh disables hlsearch until the next search is done [\#3749](https://github.com/VSCodeVim/Vim/pull/3749) ([J-Fields](https://github.com/J-Fields)) ## [v1.8.0](https://github.com/vscodevim/vim/tree/v1.8.0) (2019-05-10) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.7.1...v1.8.0) **Enhancements:** - :reg should show multiple registers if given multiple arguments [\#3610](https://github.com/VSCodeVim/Vim/issues/3610) - :reg should not show the \_ \(black hole\) register [\#3606](https://github.com/VSCodeVim/Vim/issues/3606) - Implement the % \(file name\) and : \(last executed command\) registers [\#3605](https://github.com/VSCodeVim/Vim/issues/3605) - The . \(last inserted text\) register should be read-only [\#3604](https://github.com/VSCodeVim/Vim/issues/3604) **Fixed Bugs:** - Backspace in command line mode should return to normal mode if the command is empty [\#3729](https://github.com/VSCodeVim/Vim/issues/3729) **Closed issues:** - Tab to spaces setting in vscode not applying when extension is enabled [\#3732](https://github.com/VSCodeVim/Vim/issues/3732) - %d/string/d" does not work [\#3709](https://github.com/VSCodeVim/Vim/issues/3709) - Extension issue [\#3615](https://github.com/VSCodeVim/Vim/issues/3615) - Support the / register [\#3542](https://github.com/VSCodeVim/Vim/issues/3542) **Merged pull requests:** - Show search results in the overview ruler [\#3750](https://github.com/VSCodeVim/Vim/pull/3750) ([J-Fields](https://github.com/J-Fields)) - Update dependency @types/lodash to v4.14.125 [\#3747](https://github.com/VSCodeVim/Vim/pull/3747) ([renovate[bot]](https://github.com/apps/renovate)) - \ and \ should terminate search mode [\#3746](https://github.com/VSCodeVim/Vim/pull/3746) ([hkleynhans](https://github.com/hkleynhans)) - Update dependency vscode to v1.1.34 [\#3739](https://github.com/VSCodeVim/Vim/pull/3739) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency gulp to v4.0.2 [\#3738](https://github.com/VSCodeVim/Vim/pull/3738) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/lodash to v4.14.124 [\#3737](https://github.com/VSCodeVim/Vim/pull/3737) ([renovate[bot]](https://github.com/apps/renovate)) - Fix replace character \(`r`\) behavior with newline [\#3735](https://github.com/VSCodeVim/Vim/pull/3735) ([J-Fields](https://github.com/J-Fields)) - Show `match {x} of {y}` in the status bar when searching [\#3734](https://github.com/VSCodeVim/Vim/pull/3734) ([J-Fields](https://github.com/J-Fields)) - Keymapping bindings inconsistently cased \#3012 [\#3731](https://github.com/VSCodeVim/Vim/pull/3731) ([ObliviousJamie](https://github.com/ObliviousJamie)) - Return to normal mode after hitting \ on empty command line [\#3730](https://github.com/VSCodeVim/Vim/pull/3730) ([J-Fields](https://github.com/J-Fields)) - Various improvements to registers [\#3728](https://github.com/VSCodeVim/Vim/pull/3728) ([J-Fields](https://github.com/J-Fields)) - Add tab completion on vim command line [\#3639](https://github.com/VSCodeVim/Vim/pull/3639) ([keith-ferney](https://github.com/keith-ferney)) ## [v1.7.1](https://github.com/vscodevim/vim/tree/v1.7.1) (2019-05-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.7.0...v1.7.1) **Enhancements:** - Set extensionKind in package.json to support Remote Development [\#3720](https://github.com/VSCodeVim/Vim/issues/3720) - gf doesn't work with filepath:linenumber format [\#3710](https://github.com/VSCodeVim/Vim/issues/3710) - Hive ctrl+G show which file is editing been supported? [\#3700](https://github.com/VSCodeVim/Vim/issues/3700) **Fixed Bugs:** - Replace \(:%s\) confirm text is wrong [\#3715](https://github.com/VSCodeVim/Vim/issues/3715) **Merged pull requests:** - Update dependency untildify to v4 [\#3725](https://github.com/VSCodeVim/Vim/pull/3725) ([renovate[bot]](https://github.com/apps/renovate)) - Add searches from \* and \# to the search history [\#3724](https://github.com/VSCodeVim/Vim/pull/3724) ([J-Fields](https://github.com/J-Fields)) - Implement Ctrl+G and :file [\#3723](https://github.com/VSCodeVim/Vim/pull/3723) ([J-Fields](https://github.com/J-Fields)) - Correct replacement confirmation text [\#3722](https://github.com/VSCodeVim/Vim/pull/3722) ([J-Fields](https://github.com/J-Fields)) - Set "extensionKind": "ui" to support remote development [\#3721](https://github.com/VSCodeVim/Vim/pull/3721) ([mjbvz](https://github.com/mjbvz)) ## [v1.7.0](https://github.com/vscodevim/vim/tree/v1.7.0) (2019-04-30) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.4.0...v1.7.0) **Fixed Bugs:** - vim.debug.suppress invalid [\#3703](https://github.com/VSCodeVim/Vim/issues/3703) - cw, dw, vw doesn't work with non-ascii char earlier in line [\#3680](https://github.com/VSCodeVim/Vim/issues/3680) - Word seperate doesn't works well [\#3665](https://github.com/VSCodeVim/Vim/issues/3665) - catastrophic performance [\#3654](https://github.com/VSCodeVim/Vim/issues/3654) **Closed issues:** - Ctrl keys can not be remapped in insert mode [\#3697](https://github.com/VSCodeVim/Vim/issues/3697) - Surround: Implement whitespace configuration [\#3681](https://github.com/VSCodeVim/Vim/issues/3681) - :\[line number\]d causes type error [\#3678](https://github.com/VSCodeVim/Vim/issues/3678) - How to fit VIM search on IDE footer with long git branch name? [\#3652](https://github.com/VSCodeVim/Vim/issues/3652) - cannot open or close directories with L key in file navigation [\#3576](https://github.com/VSCodeVim/Vim/issues/3576) - VsCodeVim makes workbench.tree.indent not effective [\#3561](https://github.com/VSCodeVim/Vim/issues/3561) - Ex command 'copy' throws "failed to handle key=.undefined" error [\#3505](https://github.com/VSCodeVim/Vim/issues/3505) - All mappings in Visual mode do not work when you just enter Visual mod by pressing v [\#3503](https://github.com/VSCodeVim/Vim/issues/3503) **Merged pull requests:** - Fix reverse selecting in normal mode. [\#3712](https://github.com/VSCodeVim/Vim/pull/3712) ([kroton](https://github.com/kroton)) - chore\(deps\): update dependency typescript to v3.4.5 [\#3701](https://github.com/VSCodeVim/Vim/pull/3701) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency gulp to v4.0.1 [\#3698](https://github.com/VSCodeVim/Vim/pull/3698) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency typescript to v3.4.4 [\#3690](https://github.com/VSCodeVim/Vim/pull/3690) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency mocha to v6.1.4 [\#3689](https://github.com/VSCodeVim/Vim/pull/3689) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency sinon to v7.3.2 [\#3686](https://github.com/VSCodeVim/Vim/pull/3686) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency tslint to v5.16.0 [\#3683](https://github.com/VSCodeVim/Vim/pull/3683) ([renovate[bot]](https://github.com/apps/renovate)) - docs: update slackin link [\#3679](https://github.com/VSCodeVim/Vim/pull/3679) ([khoitd1997](https://github.com/khoitd1997)) - Update dependency typescript to v3.4.3 [\#3677](https://github.com/VSCodeVim/Vim/pull/3677) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency prettier to v1.17.0 [\#3676](https://github.com/VSCodeVim/Vim/pull/3676) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency mocha to v6.1.3 [\#3675](https://github.com/VSCodeVim/Vim/pull/3675) ([renovate[bot]](https://github.com/apps/renovate)) - Add note about unsupported motions [\#3670](https://github.com/VSCodeVim/Vim/pull/3670) ([karlhorky](https://github.com/karlhorky)) - Fix word separation [\#3667](https://github.com/VSCodeVim/Vim/pull/3667) ([ajalab](https://github.com/ajalab)) - Update dependency typescript to v3.4.2 [\#3664](https://github.com/VSCodeVim/Vim/pull/3664) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency mocha to v6.1.2 [\#3663](https://github.com/VSCodeVim/Vim/pull/3663) ([renovate[bot]](https://github.com/apps/renovate)) - Fixes \#2754. Ctrl+d/u pull cursor along when screen moves past cursor [\#3658](https://github.com/VSCodeVim/Vim/pull/3658) ([mayhewluke](https://github.com/mayhewluke)) - Implement \ s [\#3563](https://github.com/VSCodeVim/Vim/pull/3563) ([aminroosta](https://github.com/aminroosta)) ## [v1.4.0](https://github.com/vscodevim/vim/tree/v1.4.0) (2019-04-08) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.3.0...v1.4.0) **Fixed Bugs:** - Performance degradation of word motions in v1.3.0 [\#3660](https://github.com/VSCodeVim/Vim/issues/3660) **Closed issues:** - Adding vim style 'Go to Symbol in Workspace' shortcut [\#3624](https://github.com/VSCodeVim/Vim/issues/3624) **Merged pull requests:** - Improve performance of word motions [\#3662](https://github.com/VSCodeVim/Vim/pull/3662) ([ajalab](https://github.com/ajalab)) - Update dependency tslint to v5.15.0 [\#3647](https://github.com/VSCodeVim/Vim/pull/3647) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency @types/mocha to v5.2.6 [\#3646](https://github.com/VSCodeVim/Vim/pull/3646) ([renovate[bot]](https://github.com/apps/renovate)) - Document display line movement best practices [\#3623](https://github.com/VSCodeVim/Vim/pull/3623) ([karlhorky](https://github.com/karlhorky)) - Only use regex lookbehind where supported [\#3525](https://github.com/VSCodeVim/Vim/pull/3525) ([JKillian](https://github.com/JKillian)) ## [v1.3.0](https://github.com/vscodevim/vim/tree/v1.3.0) (2019-04-02) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.2.0...v1.3.0) **Enhancements:** - Better non-ASCII character support in word motions [\#3612](https://github.com/VSCodeVim/Vim/issues/3612) **Fixed Bugs:** - Preview file from explorer is not tracked as jump [\#3507](https://github.com/VSCodeVim/Vim/issues/3507) - ‘W’ and 'w' shortcut keys do not support Chinese characters! [\#3439](https://github.com/VSCodeVim/Vim/issues/3439) **Closed issues:** - emmet with vscode vim [\#3644](https://github.com/VSCodeVim/Vim/issues/3644) - How do I insert a linebreak where the cursor is without entering into insert mode in VSCodeVim? [\#3636](https://github.com/VSCodeVim/Vim/issues/3636) - Hitting backspace with an empty search should return to normal mode [\#3619](https://github.com/VSCodeVim/Vim/issues/3619) - Search state should not change until a new search command is completed [\#3616](https://github.com/VSCodeVim/Vim/issues/3616) - Jumping to a mark that is off-screen should center the view around the mark [\#3609](https://github.com/VSCodeVim/Vim/issues/3609) - The original vim's redo command \(Ctrl+Shift+R\) doesn't work [\#3608](https://github.com/VSCodeVim/Vim/issues/3608) - vim-surround does not work with multiple cursors [\#3600](https://github.com/VSCodeVim/Vim/issues/3600) - digraphs cannot be inputted in different order [\#3599](https://github.com/VSCodeVim/Vim/issues/3599) - gU/gu does not work in visual mode [\#3491](https://github.com/VSCodeVim/Vim/issues/3491) - Error when executing 'View Latex PDF'-command from latex-workshop-plugin [\#3484](https://github.com/VSCodeVim/Vim/issues/3484) **Merged pull requests:** - chore\(deps\): update dependency vscode to v1.1.33 [\#3643](https://github.com/VSCodeVim/Vim/pull/3643) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency typescript to v3.4.1 [\#3642](https://github.com/VSCodeVim/Vim/pull/3642) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/sinon to v7.0.11 [\#3641](https://github.com/VSCodeVim/Vim/pull/3641) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/diff to v4.0.2 [\#3640](https://github.com/VSCodeVim/Vim/pull/3640) ([renovate[bot]](https://github.com/apps/renovate)) - Digraphs: Allow input in reverse order \(fixes \#3599\) [\#3635](https://github.com/VSCodeVim/Vim/pull/3635) ([jbaiter](https://github.com/jbaiter)) - Assign lastClosedModeHandler when onDidCloseTextDocument. [\#3630](https://github.com/VSCodeVim/Vim/pull/3630) ([yaegaki](https://github.com/yaegaki)) - When backspace is hit on an empty search, cancel the search [\#3626](https://github.com/VSCodeVim/Vim/pull/3626) ([J-Fields](https://github.com/J-Fields)) - Mark several features that have been implemented as complete in ROADMAP.md [\#3620](https://github.com/VSCodeVim/Vim/pull/3620) ([J-Fields](https://github.com/J-Fields)) - When a search is cancelled, revert to previous search state [\#3617](https://github.com/VSCodeVim/Vim/pull/3617) ([J-Fields](https://github.com/J-Fields)) - Support word motions for non-ASCII characters [\#3614](https://github.com/VSCodeVim/Vim/pull/3614) ([ajalab](https://github.com/ajalab)) - Support for gU and gu in visual mode [\#3603](https://github.com/VSCodeVim/Vim/pull/3603) ([J-Fields](https://github.com/J-Fields)) - Chinese translation of ROADMAP.MD [\#3597](https://github.com/VSCodeVim/Vim/pull/3597) ([sxlwar](https://github.com/sxlwar)) - fix\(deps\): update dependency neovim to v4.5.0 [\#3555](https://github.com/VSCodeVim/Vim/pull/3555) ([renovate[bot]](https://github.com/apps/renovate)) ## [v1.2.0](https://github.com/vscodevim/vim/tree/v1.2.0) (2019-03-17) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.1.0...v1.2.0) **Enhancements:** - The small delete register "- doesn't work [\#3492](https://github.com/VSCodeVim/Vim/issues/3492) **Closed issues:** - Extension causes high cpu load [\#3587](https://github.com/VSCodeVim/Vim/issues/3587) - Custom keybind breaks search [\#3558](https://github.com/VSCodeVim/Vim/issues/3558) - vim-auto-save [\#3550](https://github.com/VSCodeVim/Vim/issues/3550) - Extension causes high cpu load [\#3546](https://github.com/VSCodeVim/Vim/issues/3546) - Extension causes high cpu load [\#3533](https://github.com/VSCodeVim/Vim/issues/3533) - The extension don't work with Java Extension Pack [\#3526](https://github.com/VSCodeVim/Vim/issues/3526) - command 'toggleVim' not found. [\#3524](https://github.com/VSCodeVim/Vim/issues/3524) - Error when upgraded to 1.1.0 [\#3521](https://github.com/VSCodeVim/Vim/issues/3521) - TaskQueue: Error running task. Invalid regular expression: [\#3519](https://github.com/VSCodeVim/Vim/issues/3519) - Chinese i18n support? [\#3497](https://github.com/VSCodeVim/Vim/issues/3497) **Merged pull requests:** - Add yank highlighting \(REBASED\) [\#3593](https://github.com/VSCodeVim/Vim/pull/3593) ([epeli](https://github.com/epeli)) - chore\(deps\): update dependency tslint to v5.14.0 [\#3586](https://github.com/VSCodeVim/Vim/pull/3586) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency gulp-typescript to v5.0.1 [\#3585](https://github.com/VSCodeVim/Vim/pull/3585) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/sinon to v7.0.10 [\#3583](https://github.com/VSCodeVim/Vim/pull/3583) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/lodash to v4.14.123 [\#3582](https://github.com/VSCodeVim/Vim/pull/3582) ([renovate[bot]](https://github.com/apps/renovate)) - Fix TOC [\#3574](https://github.com/VSCodeVim/Vim/pull/3574) ([mtsmfm](https://github.com/mtsmfm)) - chore\(deps\): update dependency @types/sinon to v7.0.9 [\#3568](https://github.com/VSCodeVim/Vim/pull/3568) ([renovate[bot]](https://github.com/apps/renovate)) - Bump minimum VSCode version to 1.31.0 [\#3567](https://github.com/VSCodeVim/Vim/pull/3567) ([JKillian](https://github.com/JKillian)) - docs: remove outdated notes on splits from roadmap [\#3564](https://github.com/VSCodeVim/Vim/pull/3564) ([JKillian](https://github.com/JKillian)) - chore\(deps\): update dependency @types/lodash to v4.14.122 [\#3557](https://github.com/VSCodeVim/Vim/pull/3557) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency sinon to v7.2.7 [\#3554](https://github.com/VSCodeVim/Vim/pull/3554) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency sinon to v7.2.6 [\#3552](https://github.com/VSCodeVim/Vim/pull/3552) ([renovate[bot]](https://github.com/apps/renovate)) - Add small deletions to small delete register [\#3544](https://github.com/VSCodeVim/Vim/pull/3544) ([rickythefox](https://github.com/rickythefox)) - chore\(deps\): update dependency tslint to v5.13.1 [\#3541](https://github.com/VSCodeVim/Vim/pull/3541) ([renovate[bot]](https://github.com/apps/renovate)) - Mod:change sneak sneakUseIgnorecaseAndSmartcase default value explana… [\#3540](https://github.com/VSCodeVim/Vim/pull/3540) ([duguanyue](https://github.com/duguanyue)) - Fix links in README [\#3534](https://github.com/VSCodeVim/Vim/pull/3534) ([yorinasub17](https://github.com/yorinasub17)) - chore\(deps\): update dependency mocha to v6.0.2 [\#3529](https://github.com/VSCodeVim/Vim/pull/3529) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/sinon to v7.0.8 [\#3528](https://github.com/VSCodeVim/Vim/pull/3528) ([renovate[bot]](https://github.com/apps/renovate)) ## [v1.1.0](https://github.com/vscodevim/vim/tree/v1.1.0) (2019-02-24) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.0.8...v1.1.0) **Fixed Bugs:** - vim.searchHighlightColor does not work [\#3489](https://github.com/VSCodeVim/Vim/issues/3489) - Error when jumping to undefined mark [\#3468](https://github.com/VSCodeVim/Vim/issues/3468) **Closed issues:** - \[Feature request\]: Add the ability to copy the current query into clipboard. [\#3493](https://github.com/VSCodeVim/Vim/issues/3493) - Not working on vscode 1.31.0 [\#3473](https://github.com/VSCodeVim/Vim/issues/3473) - Extension causes high cpu load [\#3471](https://github.com/VSCodeVim/Vim/issues/3471) - Error when using the `\> motion [\#3452](https://github.com/VSCodeVim/Vim/issues/3452) - Show mark label like VIM in visual studio [\#3406](https://github.com/VSCodeVim/Vim/issues/3406) **Merged pull requests:** - Fixes vim.searchHighlightColor [\#3517](https://github.com/VSCodeVim/Vim/pull/3517) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency tslint to v5.13.0 [\#3516](https://github.com/VSCodeVim/Vim/pull/3516) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency vscode to v1.1.30 [\#3513](https://github.com/VSCodeVim/Vim/pull/3513) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency typescript to v3.3.3333 [\#3512](https://github.com/VSCodeVim/Vim/pull/3512) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency mocha to v6.0.1 [\#3511](https://github.com/VSCodeVim/Vim/pull/3511) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency gulp-tslint to v8.1.4 [\#3510](https://github.com/VSCodeVim/Vim/pull/3510) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency mocha to v6 [\#3499](https://github.com/VSCodeVim/Vim/pull/3499) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency gulp-sourcemaps to v2.6.5 [\#3498](https://github.com/VSCodeVim/Vim/pull/3498) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/node to v10.12.27 [\#3496](https://github.com/VSCodeVim/Vim/pull/3496) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/lodash to v4.14.121 [\#3487](https://github.com/VSCodeVim/Vim/pull/3487) ([renovate[bot]](https://github.com/apps/renovate)) - Add CamelCaseMotion plugin [\#3483](https://github.com/VSCodeVim/Vim/pull/3483) ([JKillian](https://github.com/JKillian)) - chore\(deps\): update dependency @types/node to v9.6.42 [\#3478](https://github.com/VSCodeVim/Vim/pull/3478) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency vscode to v1.1.29 [\#3476](https://github.com/VSCodeVim/Vim/pull/3476) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency typescript to v3.3.3 [\#3475](https://github.com/VSCodeVim/Vim/pull/3475) ([renovate[bot]](https://github.com/apps/renovate)) - Set \< and \> marks when yanking in visual mode. [\#3472](https://github.com/VSCodeVim/Vim/pull/3472) ([rickythefox](https://github.com/rickythefox)) - Fixes \#3468 [\#3469](https://github.com/VSCodeVim/Vim/pull/3469) ([hnefatl](https://github.com/hnefatl)) - chore\(deps\): update dependency prettier to v1.16.4 [\#3465](https://github.com/VSCodeVim/Vim/pull/3465) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency gulp-git to v2.9.0 [\#3464](https://github.com/VSCodeVim/Vim/pull/3464) ([renovate[bot]](https://github.com/apps/renovate)) - Digraph support [\#3407](https://github.com/VSCodeVim/Vim/pull/3407) ([jbaiter](https://github.com/jbaiter)) ## [v1.0.8](https://github.com/vscodevim/vim/tree/v1.0.8) (2019-02-07) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.0.7...v1.0.8) **Fixed Bugs:** - Cursor jumps after building with CMake [\#3462](https://github.com/VSCodeVim/Vim/issues/3462) - Illegal Value for Line using any input mode while WallabyJs || Quokka is running [\#3459](https://github.com/VSCodeVim/Vim/issues/3459) - Cursor jumps up to the beginning of a file after saving. [\#3444](https://github.com/VSCodeVim/Vim/issues/3444) **Merged pull requests:** - fix: cursor jumps when selection changes to output window [\#3463](https://github.com/VSCodeVim/Vim/pull/3463) ([jpoon](https://github.com/jpoon)) - feat: configuration validators [\#3451](https://github.com/VSCodeVim/Vim/pull/3451) ([jpoon](https://github.com/jpoon)) - fix: de-dupe cursors [\#3449](https://github.com/VSCodeVim/Vim/pull/3449) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/diff to v4.0.1 [\#3448](https://github.com/VSCodeVim/Vim/pull/3448) ([renovate[bot]](https://github.com/apps/renovate)) - v1.0.7 [\#3447](https://github.com/VSCodeVim/Vim/pull/3447) ([jpoon](https://github.com/jpoon)) - refactor: no need for so many different ways to create a position object [\#3446](https://github.com/VSCodeVim/Vim/pull/3446) ([jpoon](https://github.com/jpoon)) ## [v1.0.7](https://github.com/vscodevim/vim/tree/v1.0.7) (2019-02-02) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.0.6...v1.0.7) **Fixed Bugs:** - Illegal value for line error using command-mode range deletion [\#3441](https://github.com/VSCodeVim/Vim/issues/3441) - Extension crash or hangs when failing to call nvim [\#3433](https://github.com/VSCodeVim/Vim/issues/3433) **Merged pull requests:** - \[Bugfix\] - sentences backward [\#3445](https://github.com/VSCodeVim/Vim/pull/3445) ([esetnik](https://github.com/esetnik)) - refactor: rename cursorPositionJustBeforeAnythingHappened to cursorsInitialState [\#3443](https://github.com/VSCodeVim/Vim/pull/3443) ([jpoon](https://github.com/jpoon)) - fix: ensure cursor is in bounds. closes \#3441 [\#3442](https://github.com/VSCodeVim/Vim/pull/3442) ([jpoon](https://github.com/jpoon)) - fix: validate that remappings are string arrays [\#3440](https://github.com/VSCodeVim/Vim/pull/3440) ([jpoon](https://github.com/jpoon)) - v1.0.6 [\#3438](https://github.com/VSCodeVim/Vim/pull/3438) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency typescript to v3.3.1 [\#3436](https://github.com/VSCodeVim/Vim/pull/3436) ([renovate[bot]](https://github.com/apps/renovate)) - Adopt latest list navigation support [\#3432](https://github.com/VSCodeVim/Vim/pull/3432) ([joaomoreno](https://github.com/joaomoreno)) ## [v1.0.6](https://github.com/vscodevim/vim/tree/v1.0.6) (2019-02-01) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.0.5...v1.0.6) **Fixed Bugs:** - Bad interaction between 1.0.5 and jscode-java-pack [\#3431](https://github.com/VSCodeVim/Vim/issues/3431) - Release 1.0.4 doesn't contain listed changes [\#3429](https://github.com/VSCodeVim/Vim/issues/3429) **Merged pull requests:** - fix: check neovim configurations and timeout on nvim attach [\#3437](https://github.com/VSCodeVim/Vim/pull/3437) ([jpoon](https://github.com/jpoon)) - fix: revert back to previous non-async code when syncing cursor [\#3435](https://github.com/VSCodeVim/Vim/pull/3435) ([jpoon](https://github.com/jpoon)) - feat: output commit hash. closes \#3429 [\#3430](https://github.com/VSCodeVim/Vim/pull/3430) ([jpoon](https://github.com/jpoon)) ## [v1.0.5](https://github.com/vscodevim/vim/tree/v1.0.5) (2019-01-31) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.0.4...v1.0.5) **Merged pull requests:** - chore\(deps\): update dependency prettier to v1.16.3 [\#3428](https://github.com/VSCodeVim/Vim/pull/3428) ([renovate[bot]](https://github.com/apps/renovate)) - v1.0.4 [\#3427](https://github.com/VSCodeVim/Vim/pull/3427) ([jpoon](https://github.com/jpoon)) ## [v1.0.4](https://github.com/vscodevim/vim/tree/v1.0.4) (2019-01-31) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.0.3...v1.0.4) **Fixed Bugs:** - "Delete surrounding quotes" doesn't work in certain cases [\#3415](https://github.com/VSCodeVim/Vim/issues/3415) - 'gd' is working correctly, but an error occurs. [\#3387](https://github.com/VSCodeVim/Vim/issues/3387) **Closed issues:** - Extension causes high cpu load [\#3400](https://github.com/VSCodeVim/Vim/issues/3400) **Merged pull requests:** - fix ds" with nested quotes and add some tests - fixes \#3415 [\#3426](https://github.com/VSCodeVim/Vim/pull/3426) ([esetnik](https://github.com/esetnik)) - chore\(deps\): update dependency @types/diff to v4 [\#3425](https://github.com/VSCodeVim/Vim/pull/3425) ([renovate[bot]](https://github.com/apps/renovate)) - fix: single-key remappings were being ignored [\#3424](https://github.com/VSCodeVim/Vim/pull/3424) ([jpoon](https://github.com/jpoon)) - fix\(deps\): update dependency winston to v3.2.1 [\#3423](https://github.com/VSCodeVim/Vim/pull/3423) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency prettier to v1.16.2 [\#3422](https://github.com/VSCodeVim/Vim/pull/3422) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/sinon to v7.0.5 [\#3421](https://github.com/VSCodeVim/Vim/pull/3421) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/diff to v3.5.3 [\#3420](https://github.com/VSCodeVim/Vim/pull/3420) ([renovate[bot]](https://github.com/apps/renovate)) - fix: validate configurations once, instead of every key press [\#3418](https://github.com/VSCodeVim/Vim/pull/3418) ([jpoon](https://github.com/jpoon)) - Run `closeMarkersNavigation` on ESC. Fix \#3367 [\#3416](https://github.com/VSCodeVim/Vim/pull/3416) ([octref](https://github.com/octref)) - chore\(deps\): update dependency vscode to v1.1.28 [\#3412](https://github.com/VSCodeVim/Vim/pull/3412) ([renovate-bot](https://github.com/renovate-bot)) - refactor: make globalstate singleton class [\#3411](https://github.com/VSCodeVim/Vim/pull/3411) ([jpoon](https://github.com/jpoon)) - Misc async fixes - new revision [\#3410](https://github.com/VSCodeVim/Vim/pull/3410) ([xconverge](https://github.com/xconverge)) - fix: closes \#3157 [\#3409](https://github.com/VSCodeVim/Vim/pull/3409) ([jpoon](https://github.com/jpoon)) - fix \#3157: register single onDidChangeTextDocument handler and delegate to appropriate mode handler [\#3408](https://github.com/VSCodeVim/Vim/pull/3408) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency prettier to v1.16.1 [\#3405](https://github.com/VSCodeVim/Vim/pull/3405) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency vscode to v1.1.27 [\#3403](https://github.com/VSCodeVim/Vim/pull/3403) ([renovate-bot](https://github.com/renovate-bot)) - fix address 'gf' bug. `replace file://` method [\#3402](https://github.com/VSCodeVim/Vim/pull/3402) ([pikulev](https://github.com/pikulev)) - bump version [\#3399](https://github.com/VSCodeVim/Vim/pull/3399) ([jpoon](https://github.com/jpoon)) ## [v1.0.3](https://github.com/vscodevim/vim/tree/v1.0.3) (2019-01-20) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.0.2...v1.0.3) **Merged pull requests:** - fix rangeerror. action buttons on log messages. [\#3398](https://github.com/VSCodeVim/Vim/pull/3398) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency prettier to v1.16.0 [\#3397](https://github.com/VSCodeVim/Vim/pull/3397) ([renovate-bot](https://github.com/renovate-bot)) - fix: gf over a 'file://...' path and \#3310 issue \(v2\) [\#3396](https://github.com/VSCodeVim/Vim/pull/3396) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency sinon to v7.2.3 [\#3394](https://github.com/VSCodeVim/Vim/pull/3394) ([renovate-bot](https://github.com/renovate-bot)) - fix: 3350 [\#3393](https://github.com/VSCodeVim/Vim/pull/3393) ([jpoon](https://github.com/jpoon)) - docs: change slackin host [\#3392](https://github.com/VSCodeVim/Vim/pull/3392) ([jpoon](https://github.com/jpoon)) - Update dependency @types/lodash to v4.14.120 [\#3385](https://github.com/VSCodeVim/Vim/pull/3385) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency typescript to v3.2.4 [\#3384](https://github.com/VSCodeVim/Vim/pull/3384) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/sinon to v7.0.4 [\#3383](https://github.com/VSCodeVim/Vim/pull/3383) ([renovate-bot](https://github.com/renovate-bot)) - Fixes \#3378 [\#3381](https://github.com/VSCodeVim/Vim/pull/3381) ([xconverge](https://github.com/xconverge)) - fixes \#3374 [\#3380](https://github.com/VSCodeVim/Vim/pull/3380) ([xconverge](https://github.com/xconverge)) ## [v1.0.2](https://github.com/vscodevim/vim/tree/v1.0.2) (2019-01-16) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.0.1...v1.0.2) ## [v1.0.1](https://github.com/vscodevim/vim/tree/v1.0.1) (2019-01-06) [Full Changelog](https://github.com/vscodevim/vim/compare/v1.0.0...v1.0.1) ## [v1.0.0](https://github.com/vscodevim/vim/tree/v1.0.0) (2019-01-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.17.3...v1.0.0) The first commit to this project was a little over 3 years ago, and what a journey it's been. To celebrate the new year, we are pushing out v1.0.0 of VSCodeVim! In addition to this project reaching such an amazing milestone, but in my personal life, I'll soon be celebrating the birth of my first-born. With that in mind, over the last few weeks I've tried to close out as many issues as I could before all my spare time is filled with diapers and bottles. Thanks to amazing team of maintainers, contributors, and users that have brought us to where we are today and where we'll go tomorrow. **Breaking Change:** - `vim.debug.loggingLevel` has been removed. In it's place we now have `vim.debug.loggingLevelForConsole`. For full details, see the [settings section of our README](https://github.com/VSCodeVim/Vim#vscodevim-settings). **Enhancements:** - feat: change debug configurations to loggingLevelForConsole, loggingLevelForAlert [\#3325](https://github.com/VSCodeVim/Vim/pull/3325) ([jpoon](https://github.com/jpoon)) **Fixed Bugs:** - Status Bar Color did not changed with the mode [\#3316](https://github.com/VSCodeVim/Vim/issues/3316) - Error when remapping to commands with name starting with "extension." [\#3307](https://github.com/VSCodeVim/Vim/issues/3307) **Closed issues:** - gf: 'try to find it with the same extension'-code doesn't work [\#3309](https://github.com/VSCodeVim/Vim/issues/3309) - Extension causes high cpu load [\#3289](https://github.com/VSCodeVim/Vim/issues/3289) - The Vim plugin can not edit except i/a/s [\#3270](https://github.com/VSCodeVim/Vim/issues/3270) - Keyboard stops working with VSCode when indenting multiline \[MacOS Mojave\] [\#3206](https://github.com/VSCodeVim/Vim/issues/3206) - ctrl o shortcut not work sometimes [\#3074](https://github.com/VSCodeVim/Vim/issues/3074) **Merged pull requests:** - fix: closes \#3316 [\#3321](https://github.com/VSCodeVim/Vim/pull/3321) ([jpoon](https://github.com/jpoon)) - fix: Actually fix \#3295. [\#3320](https://github.com/VSCodeVim/Vim/pull/3320) ([jpoon](https://github.com/jpoon)) - refactor: disableExtension configuration should follow pattern of rest of configs [\#3318](https://github.com/VSCodeVim/Vim/pull/3318) ([jpoon](https://github.com/jpoon)) - feat: show vim errors in vscode informational window [\#3315](https://github.com/VSCodeVim/Vim/pull/3315) ([jpoon](https://github.com/jpoon)) - fix: log warning if remapped command does not exist. closes \#3307 [\#3314](https://github.com/VSCodeVim/Vim/pull/3314) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/sinon to v7.0.3 [\#3313](https://github.com/VSCodeVim/Vim/pull/3313) ([renovate-bot](https://github.com/renovate-bot)) - v0.17.3 [\#3306](https://github.com/VSCodeVim/Vim/pull/3306) ([jpoon](https://github.com/jpoon)) ## [v0.17.3](https://github.com/vscodevim/vim/tree/v0.17.3) (2018-12-30) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.17.2...v0.17.3) **Enhancements:** - :on is not an editor command [\#3286](https://github.com/VSCodeVim/Vim/issues/3286) - editor.wordSeparators setting is ignored [\#3166](https://github.com/VSCodeVim/Vim/issues/3166) - save \(:w or :wq\) with SSHFS and LiveShare guest don't work properly [\#2956](https://github.com/VSCodeVim/Vim/issues/2956) **Fixed Bugs:** - \ jumps back to wrong location after 'gd' [\#3277](https://github.com/VSCodeVim/Vim/issues/3277) **Closed issues:** - Either slash or colon not working [\#3291](https://github.com/VSCodeVim/Vim/issues/3291) - s and S Key Commands Not Working [\#3274](https://github.com/VSCodeVim/Vim/issues/3274) - Extension Host is unresponsive [\#3056](https://github.com/VSCodeVim/Vim/issues/3056) - Vim mode randomly not functional - show warning [\#2725](https://github.com/VSCodeVim/Vim/issues/2725) - Is hanging. [\#2629](https://github.com/VSCodeVim/Vim/issues/2629) **Merged pull requests:** - fix: sync editor.wordSeparators and vim.iskeyword. closes \#3166 [\#3305](https://github.com/VSCodeVim/Vim/pull/3305) ([jpoon](https://github.com/jpoon)) - feat: add on as alias for only [\#3303](https://github.com/VSCodeVim/Vim/pull/3303) ([jpoon](https://github.com/jpoon)) - fix: \#3277 [\#3302](https://github.com/VSCodeVim/Vim/pull/3302) ([jpoon](https://github.com/jpoon)) - fix saving remote file error [\#3281](https://github.com/VSCodeVim/Vim/pull/3281) ([zhuzisheng](https://github.com/zhuzisheng)) ## [v0.17.2](https://github.com/vscodevim/vim/tree/v0.17.2) (2018-12-28) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.17.1...v0.17.2) **Fixed Bugs:** - v0.17.1 prints `\` string for every tab keystroke [\#3298](https://github.com/VSCodeVim/Vim/issues/3298) **Merged pull requests:** - fix: v0.17.1 regression [\#3299](https://github.com/VSCodeVim/Vim/pull/3299) ([jpoon](https://github.com/jpoon)) - v0.17.0-\>v0.17.1 [\#3297](https://github.com/VSCodeVim/Vim/pull/3297) ([jpoon](https://github.com/jpoon)) ## [v0.17.1](https://github.com/vscodevim/vim/tree/v0.17.1) (2018-12-28) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.17.0...v0.17.1) **Fixed Bugs:** - Keybindings reset on invalid command [\#3295](https://github.com/VSCodeVim/Vim/issues/3295) **Closed issues:** - For easy motion plugin, allow user to remap leader key. [\#3244](https://github.com/VSCodeVim/Vim/issues/3244) - after opening user settings, all Vim keybindings are disabled [\#3029](https://github.com/VSCodeVim/Vim/issues/3029) **Merged pull requests:** - fix: ignore remappings with non-existent commands. fixes \#3295 [\#3296](https://github.com/VSCodeVim/Vim/pull/3296) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update node.js to v8.15 [\#3294](https://github.com/VSCodeVim/Vim/pull/3294) ([renovate-bot](https://github.com/renovate-bot)) - fix: slightly improve perf by caching vscode context [\#3293](https://github.com/VSCodeVim/Vim/pull/3293) ([jpoon](https://github.com/jpoon)) - fix: disable nvim shada [\#3288](https://github.com/VSCodeVim/Vim/pull/3288) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/sinon to v7.0.2 [\#3279](https://github.com/VSCodeVim/Vim/pull/3279) ([renovate-bot](https://github.com/renovate-bot)) - refactor: status bar [\#3276](https://github.com/VSCodeVim/Vim/pull/3276) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/node to v9.6.41 [\#3275](https://github.com/VSCodeVim/Vim/pull/3275) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency tslint to v5.12.0 [\#3272](https://github.com/VSCodeVim/Vim/pull/3272) ([renovate-bot](https://github.com/renovate-bot)) - Release [\#3271](https://github.com/VSCodeVim/Vim/pull/3271) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency typescript to v3.2.2 [\#3234](https://github.com/VSCodeVim/Vim/pull/3234) ([renovate-bot](https://github.com/renovate-bot)) ## [v0.17.0](https://github.com/vscodevim/vim/tree/v0.17.0) (2018-12-17) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.14...v0.17.0) **Fixed Bugs:** - Running :reg when clipboard is empty causes an error [\#2898](https://github.com/VSCodeVim/Vim/issues/2898) **Merged pull requests:** - Change to use native vscode clipboard [\#3261](https://github.com/VSCodeVim/Vim/pull/3261) ([xconverge](https://github.com/xconverge)) - chore\(deps\): update dependency @types/sinon to v7 [\#3259](https://github.com/VSCodeVim/Vim/pull/3259) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency sinon to v7.2.1 [\#3258](https://github.com/VSCodeVim/Vim/pull/3258) ([renovate-bot](https://github.com/renovate-bot)) - v0.16.13 -\> v0.16.14 [\#3257](https://github.com/VSCodeVim/Vim/pull/3257) ([jpoon](https://github.com/jpoon)) ## [v0.16.14](https://github.com/vscodevim/vim/tree/v0.16.14) (2018-12-11) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.13...v0.16.14) **Enhancements:** - Add support for new grid layout with splits [\#2696](https://github.com/VSCodeVim/Vim/issues/2696) **Fixed Bugs:** - It seems % command is not treated like a motion [\#3138](https://github.com/VSCodeVim/Vim/issues/3138) **Closed issues:** - vim.normalModeKeyBindingsNonRecursive do not work [\#3247](https://github.com/VSCodeVim/Vim/issues/3247) - Status bar in zen mode [\#3245](https://github.com/VSCodeVim/Vim/issues/3245) - When closing a window with `:q` VS Code now selects the tab "before" the one you were previously on [\#2984](https://github.com/VSCodeVim/Vim/issues/2984) **Merged pull requests:** - chore\(deps\): update dependency vscode to v1.1.26 [\#3256](https://github.com/VSCodeVim/Vim/pull/3256) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency sinon to v7.2.0 [\#3255](https://github.com/VSCodeVim/Vim/pull/3255) ([renovate-bot](https://github.com/renovate-bot)) - Format operator fixes and tests [\#3254](https://github.com/VSCodeVim/Vim/pull/3254) ([watsoncj](https://github.com/watsoncj)) - Added common example for key remapping for £ [\#3250](https://github.com/VSCodeVim/Vim/pull/3250) ([ycmjason](https://github.com/ycmjason)) - chore\(deps\): update dependency @types/lodash to v4.14.119 [\#3246](https://github.com/VSCodeVim/Vim/pull/3246) ([renovate-bot](https://github.com/renovate-bot)) - Re-implement `` and '' with jumpTracker [\#3242](https://github.com/VSCodeVim/Vim/pull/3242) ([dsschnau](https://github.com/dsschnau)) - chore\(deps\): update dependency gulp-typescript to v5 [\#3240](https://github.com/VSCodeVim/Vim/pull/3240) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency prettier to v1.15.3 [\#3236](https://github.com/VSCodeVim/Vim/pull/3236) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/node to v9.6.40 [\#3235](https://github.com/VSCodeVim/Vim/pull/3235) ([renovate-bot](https://github.com/renovate-bot)) - fix typo [\#3230](https://github.com/VSCodeVim/Vim/pull/3230) ([fourcels](https://github.com/fourcels)) - chore\(deps\): update node.js to v8.14 [\#3228](https://github.com/VSCodeVim/Vim/pull/3228) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency vscode to v1.1.24 [\#3224](https://github.com/VSCodeVim/Vim/pull/3224) ([renovate-bot](https://github.com/renovate-bot)) - Fix \#2984: wrong tab selected after :quit [\#3170](https://github.com/VSCodeVim/Vim/pull/3170) ([ohjames](https://github.com/ohjames)) ## [v0.16.13](https://github.com/vscodevim/vim/tree/v0.16.13) (2018-11-27) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.12...v0.16.13) **Fixed Bugs:** - Finding with `?` renders `/` in the status bar instead of `?` [\#3211](https://github.com/VSCodeVim/Vim/issues/3211) - Test docker - debconf enforces interactive during build [\#3168](https://github.com/VSCodeVim/Vim/issues/3168) **Closed issues:** - Problem with insert mode after highlighting in visual mode [\#3174](https://github.com/VSCodeVim/Vim/issues/3174) - Recursive mapping V key [\#3173](https://github.com/VSCodeVim/Vim/issues/3173) - Code Action not working when using Vim mappings [\#3160](https://github.com/VSCodeVim/Vim/issues/3160) **Merged pull requests:** - v0.16.13 [\#3223](https://github.com/VSCodeVim/Vim/pull/3223) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update node.js to v8.13 [\#3222](https://github.com/VSCodeVim/Vim/pull/3222) ([renovate-bot](https://github.com/renovate-bot)) - display '?' or '/' in status bar in search mode [\#3218](https://github.com/VSCodeVim/Vim/pull/3218) ([dsschnau](https://github.com/dsschnau)) - fix: upgrade sinon 5.0.5-\>5.0.7. prettier 1.14.3-\>1.15.2 [\#3217](https://github.com/VSCodeVim/Vim/pull/3217) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/node to v9.6.39 [\#3215](https://github.com/VSCodeVim/Vim/pull/3215) ([renovate-bot](https://github.com/renovate-bot)) - Fix \#1287: CJK characters\(korean\) overlap each other in insert mode [\#3214](https://github.com/VSCodeVim/Vim/pull/3214) ([Injae-Lee](https://github.com/Injae-Lee)) - chore\(deps\): update dependency @types/node to v9.6.37 [\#3204](https://github.com/VSCodeVim/Vim/pull/3204) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/lodash to v4.14.118 [\#3196](https://github.com/VSCodeVim/Vim/pull/3196) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update node.js to v8.12 [\#3194](https://github.com/VSCodeVim/Vim/pull/3194) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/diff to v3.5.2 [\#3193](https://github.com/VSCodeVim/Vim/pull/3193) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency typescript to v3.1.6 [\#3188](https://github.com/VSCodeVim/Vim/pull/3188) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/node to v9.6.36 [\#3187](https://github.com/VSCodeVim/Vim/pull/3187) ([renovate-bot](https://github.com/renovate-bot)) - docs: update roadmap for split and new [\#3184](https://github.com/VSCodeVim/Vim/pull/3184) ([jpoon](https://github.com/jpoon)) - fix: automerge renovate minor/patch [\#3183](https://github.com/VSCodeVim/Vim/pull/3183) ([jpoon](https://github.com/jpoon)) - Update dependency typescript to v3.1.5 [\#3182](https://github.com/VSCodeVim/Vim/pull/3182) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency typescript to v3.1.4 [\#3175](https://github.com/VSCodeVim/Vim/pull/3175) ([renovate-bot](https://github.com/renovate-bot)) - Issue \#3168 - Ubuntu tests [\#3169](https://github.com/VSCodeVim/Vim/pull/3169) ([pschoffer](https://github.com/pschoffer)) - v0.16.12 [\#3165](https://github.com/VSCodeVim/Vim/pull/3165) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency sinon to v7.1.1 [\#3162](https://github.com/VSCodeVim/Vim/pull/3162) ([renovate-bot](https://github.com/renovate-bot)) - Convert synchronous funcs to async [\#3123](https://github.com/VSCodeVim/Vim/pull/3123) ([kylecarbs](https://github.com/kylecarbs)) ## [v0.16.12](https://github.com/vscodevim/vim/tree/v0.16.12) (2018-10-26) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.11...v0.16.12) **Fixed Bugs:** - Gulp test with Docker fails to launch [\#3152](https://github.com/VSCodeVim/Vim/issues/3152) - The link to \*Multi-Cursor\* mode in \_\_Table of content\_\_ doesn't work \(in repo\) [\#3149](https://github.com/VSCodeVim/Vim/issues/3149) - Multi-Cursor + insertModeKeyBinding jk -\> \ [\#2752](https://github.com/VSCodeVim/Vim/issues/2752) **Merged pull requests:** - Add more Docker documentation [\#3156](https://github.com/VSCodeVim/Vim/pull/3156) ([westim](https://github.com/westim)) - Fix 3152: Upgrade Docker prerequisite libgtk from 2.0 to 3.0 [\#3153](https://github.com/VSCodeVim/Vim/pull/3153) ([westim](https://github.com/westim)) - Fix \#3149: broken table of contents links [\#3151](https://github.com/VSCodeVim/Vim/pull/3151) ([westim](https://github.com/westim)) - Fix for \#2752 [\#3131](https://github.com/VSCodeVim/Vim/pull/3131) ([donald93](https://github.com/donald93)) ## [v0.16.11](https://github.com/vscodevim/vim/tree/v0.16.11) (2018-10-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.10...v0.16.11) **Closed issues:** - Version 0.16.10 stuck in insert mode [\#3143](https://github.com/VSCodeVim/Vim/issues/3143) - fold code block bug [\#3140](https://github.com/VSCodeVim/Vim/issues/3140) - Escape key stopped being registered so can't exit insert mode [\#3139](https://github.com/VSCodeVim/Vim/issues/3139) **Merged pull requests:** - Prevent error on loading search history if no active editor on startup [\#3146](https://github.com/VSCodeVim/Vim/pull/3146) ([shawnaxsom](https://github.com/shawnaxsom)) - v0.16.10 [\#3137](https://github.com/VSCodeVim/Vim/pull/3137) ([jpoon](https://github.com/jpoon)) ## [v0.16.10](https://github.com/vscodevim/vim/tree/v0.16.10) (2018-10-14) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.9...v0.16.10) **Enhancements:** - Previous searches are not saved across sessions [\#3098](https://github.com/VSCodeVim/Vim/issues/3098) - substitution statefulness [\#3067](https://github.com/VSCodeVim/Vim/issues/3067) - feat: implement 'changeWordIncludesWhitespace' option [\#2964](https://github.com/VSCodeVim/Vim/pull/2964) ([darfink](https://github.com/darfink)) **Fixed Bugs:** - Wrong cursor position after using same file in two panels [\#2688](https://github.com/VSCodeVim/Vim/issues/2688) - Search and replace doesn't work with current line \(.\) and relative lines [\#2384](https://github.com/VSCodeVim/Vim/issues/2384) **Closed issues:** - Broken on Insiders build [\#3119](https://github.com/VSCodeVim/Vim/issues/3119) - Cannot bind \ [\#3072](https://github.com/VSCodeVim/Vim/issues/3072) - CTRL-\[ does not quit the command-line editing mode [\#3019](https://github.com/VSCodeVim/Vim/issues/3019) **Merged pull requests:** - chore\(deps\): update dependency sinon to v7 [\#3135](https://github.com/VSCodeVim/Vim/pull/3135) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency typescript to v3.1.3 [\#3130](https://github.com/VSCodeVim/Vim/pull/3130) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency typescript to v3.1.2 [\#3122](https://github.com/VSCodeVim/Vim/pull/3122) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/node to v9.6.35 [\#3121](https://github.com/VSCodeVim/Vim/pull/3121) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/lodash to v4.14.117 [\#3120](https://github.com/VSCodeVim/Vim/pull/3120) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/sinon to v5.0.5 [\#3118](https://github.com/VSCodeVim/Vim/pull/3118) ([renovate-bot](https://github.com/renovate-bot)) - Save search history to a file like commandline history [\#3116](https://github.com/VSCodeVim/Vim/pull/3116) ([xconverge](https://github.com/xconverge)) - fix \(simpler\) - cursor whenever changing editors - closes \#2688 [\#3103](https://github.com/VSCodeVim/Vim/pull/3103) ([captaincaius](https://github.com/captaincaius)) - feature: relative, plus/minus ranges. closes \#2384 [\#3071](https://github.com/VSCodeVim/Vim/pull/3071) ([captaincaius](https://github.com/captaincaius)) - Adding state to substitution command [\#3068](https://github.com/VSCodeVim/Vim/pull/3068) ([captaincaius](https://github.com/captaincaius)) ## [v0.16.9](https://github.com/vscodevim/vim/tree/v0.16.9) (2018-10-08) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.8...v0.16.9) **Fixed Bugs:** - Repeating command \(`.`\) after doing vim-easymotion find character command doesn't work. [\#3111](https://github.com/VSCodeVim/Vim/issues/3111) - Incrementing / Decrementing numbers doesn't work when it's after a minus sign and a word [\#3057](https://github.com/VSCodeVim/Vim/issues/3057) - Unexpected behavior with easymotion and `.` as repeat command [\#2310](https://github.com/VSCodeVim/Vim/issues/2310) **Merged pull requests:** - support "edit" command [\#3114](https://github.com/VSCodeVim/Vim/pull/3114) ([m59peacemaker](https://github.com/m59peacemaker)) - Minor C-a C-x fix [\#3113](https://github.com/VSCodeVim/Vim/pull/3113) ([xconverge](https://github.com/xconverge)) - Allow dot to repeat after doing any EasyMotion move [\#3112](https://github.com/VSCodeVim/Vim/pull/3112) ([xconverge](https://github.com/xconverge)) ## [v0.16.8](https://github.com/vscodevim/vim/tree/v0.16.8) (2018-10-06) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.7...v0.16.8) **Closed issues:** - \ stopped working this morning [\#3110](https://github.com/VSCodeVim/Vim/issues/3110) - version 0.16.6 cause \ key insert string for unknown reason [\#3096](https://github.com/VSCodeVim/Vim/issues/3096) - yank in visual mode doesn't update register 0 [\#3065](https://github.com/VSCodeVim/Vim/issues/3065) - Paste the yanked text with "0p does no work [\#2554](https://github.com/VSCodeVim/Vim/issues/2554) - Surround: Keep HTML attributes when changing tags [\#1938](https://github.com/VSCodeVim/Vim/issues/1938) **Merged pull requests:** - Fix issues with keybindings when changing to an editor in different mode [\#3108](https://github.com/VSCodeVim/Vim/pull/3108) ([shawnaxsom](https://github.com/shawnaxsom)) - README cleanup [\#3107](https://github.com/VSCodeVim/Vim/pull/3107) ([xconverge](https://github.com/xconverge)) - Update readme based on new feature for surround with attributes [\#3106](https://github.com/VSCodeVim/Vim/pull/3106) ([xconverge](https://github.com/xconverge)) - fixes \#1938 Allow to retain attributes when using surround [\#3105](https://github.com/VSCodeVim/Vim/pull/3105) ([xconverge](https://github.com/xconverge)) - Multiline yank writes to 0 register; fixes \#1214 [\#3087](https://github.com/VSCodeVim/Vim/pull/3087) ([JKillian](https://github.com/JKillian)) ## [v0.16.7](https://github.com/vscodevim/vim/tree/v0.16.7) (2018-10-06) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.6...v0.16.7) **Merged pull requests:** - Update dependency @types/sinon to v5.0.4 [\#3104](https://github.com/VSCodeVim/Vim/pull/3104) ([renovate-bot](https://github.com/renovate-bot)) - Cleanup gt count command [\#3097](https://github.com/VSCodeVim/Vim/pull/3097) ([xconverge](https://github.com/xconverge)) - Update dependency @types/sinon to v5.0.3 [\#3093](https://github.com/VSCodeVim/Vim/pull/3093) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/node to v9.6.34 [\#3092](https://github.com/VSCodeVim/Vim/pull/3092) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency sinon to v6.3.5 [\#3091](https://github.com/VSCodeVim/Vim/pull/3091) ([renovate-bot](https://github.com/renovate-bot)) - Remappings not applying with operators that enter insert mode [\#3090](https://github.com/VSCodeVim/Vim/pull/3090) ([shawnaxsom](https://github.com/shawnaxsom)) - v0.16.6 [\#3085](https://github.com/VSCodeVim/Vim/pull/3085) ([jpoon](https://github.com/jpoon)) - Add support for grid layout [\#2697](https://github.com/VSCodeVim/Vim/pull/2697) ([rodcloutier](https://github.com/rodcloutier)) ## [v0.16.6](https://github.com/vscodevim/vim/tree/v0.16.6) (2018-10-02) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.5...v0.16.6) **Fixed Bugs:** - Confirm-Replace works incorrectly with global substitute for certain types of replace patterns [\#2950](https://github.com/VSCodeVim/Vim/issues/2950) - Remapping `d` to always delete to black-hole [\#2672](https://github.com/VSCodeVim/Vim/issues/2672) **Closed issues:** - Visual Block Mode when not using Ctrl keys [\#3042](https://github.com/VSCodeVim/Vim/issues/3042) - Investigate reducing startup activation time [\#2947](https://github.com/VSCodeVim/Vim/issues/2947) **Merged pull requests:** - Feature/fix black hole operator mappings [\#3081](https://github.com/VSCodeVim/Vim/pull/3081) ([shawnaxsom](https://github.com/shawnaxsom)) - Feature/insert mode optimizations [\#3078](https://github.com/VSCodeVim/Vim/pull/3078) ([shawnaxsom](https://github.com/shawnaxsom)) - Update dependency typescript to v3.1.1 [\#3077](https://github.com/VSCodeVim/Vim/pull/3077) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/node to v9.6.32 [\#3066](https://github.com/VSCodeVim/Vim/pull/3066) ([renovate-bot](https://github.com/renovate-bot)) - Fix substitute with gc flag [\#3055](https://github.com/VSCodeVim/Vim/pull/3055) ([tomotg](https://github.com/tomotg)) ## [v0.16.5](https://github.com/vscodevim/vim/tree/v0.16.5) (2018-09-21) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.4...v0.16.5) **Fixed Bugs:** - keybinding \ overwrite vscode's default behavior [\#3050](https://github.com/VSCodeVim/Vim/issues/3050) - New Jump Tracker doesn't always handle that isn't left open in a tab [\#3039](https://github.com/VSCodeVim/Vim/issues/3039) - Exiting CommandMode should mimic Vim behavior [\#3035](https://github.com/VSCodeVim/Vim/issues/3035) **Closed issues:** - C-o, C-i strange jumping behavior. [\#3047](https://github.com/VSCodeVim/Vim/issues/3047) - Support vscode's color copy [\#3038](https://github.com/VSCodeVim/Vim/issues/3038) - Possible for `:new` to a open a new editor in the current group without splitting? [\#2911](https://github.com/VSCodeVim/Vim/issues/2911) - Support for ' ' \(Jump to previous cursor position\) [\#2031](https://github.com/VSCodeVim/Vim/issues/2031) **Merged pull requests:** - Update dependency prettier to v1.14.3 [\#3060](https://github.com/VSCodeVim/Vim/pull/3060) ([renovate-bot](https://github.com/renovate-bot)) - fix `\` in 「Insert」mode [\#3051](https://github.com/VSCodeVim/Vim/pull/3051) ([myhere](https://github.com/myhere)) - Support for line completion \(\\\) [\#3048](https://github.com/VSCodeVim/Vim/pull/3048) ([shawnaxsom](https://github.com/shawnaxsom)) - Update dependency lodash to v4.17.11 [\#3045](https://github.com/VSCodeVim/Vim/pull/3045) ([renovate-bot](https://github.com/renovate-bot)) - Fixed Jump Tracker jumps when jumping from a file that auto closes [\#3041](https://github.com/VSCodeVim/Vim/pull/3041) ([shawnaxsom](https://github.com/shawnaxsom)) - Fix: Missing bindings to exit CommandMode. closes \#3035 [\#3036](https://github.com/VSCodeVim/Vim/pull/3036) ([mxlian](https://github.com/mxlian)) ## [v0.16.4](https://github.com/vscodevim/vim/tree/v0.16.4) (2018-09-10) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.3...v0.16.4) **Enhancements:** - \[FEATURE REQUEST\]visual line mode support A or I [\#2167](https://github.com/VSCodeVim/Vim/issues/2167) **Closed issues:** - Moving out of viewport centers the viewport when it shouldn't [\#2998](https://github.com/VSCodeVim/Vim/issues/2998) - docs: all-contributors [\#2645](https://github.com/VSCodeVim/Vim/issues/2645) - Make small movement command not registered to Ctrl+o [\#1933](https://github.com/VSCodeVim/Vim/issues/1933) **Merged pull requests:** - Feature/improved jump list [\#3028](https://github.com/VSCodeVim/Vim/pull/3028) ([shawnaxsom](https://github.com/shawnaxsom)) - I or A in visual/visual line mode creates multiple cursors \#2167 [\#2993](https://github.com/VSCodeVim/Vim/pull/2993) ([shawnaxsom](https://github.com/shawnaxsom)) ## [v0.16.3](https://github.com/vscodevim/vim/tree/v0.16.3) (2018-09-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.2...v0.16.3) **Enhancements:** - Add activationEvent 'onCommand:type' to avoid missing keystrokes [\#3016](https://github.com/VSCodeVim/Vim/issues/3016) - va{a{ doesn't work [\#2506](https://github.com/VSCodeVim/Vim/issues/2506) **Closed issues:** - Expand selection with inner tag selection command [\#2907](https://github.com/VSCodeVim/Vim/issues/2907) **Merged pull requests:** - fix: re-enable relativelinenumbers. closes \#3020 [\#3025](https://github.com/VSCodeVim/Vim/pull/3025) ([jpoon](https://github.com/jpoon)) - fix: add activationevent onCommand type. closes \#3016 [\#3023](https://github.com/VSCodeVim/Vim/pull/3023) ([jpoon](https://github.com/jpoon)) - Update dependency winston to v3.1.0 [\#3021](https://github.com/VSCodeVim/Vim/pull/3021) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency diff-match-patch to v1.0.4 [\#3018](https://github.com/VSCodeVim/Vim/pull/3018) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/node to v9.6.31 [\#3011](https://github.com/VSCodeVim/Vim/pull/3011) ([renovate-bot](https://github.com/renovate-bot)) - Fix multiple issues with expand selection commands and pair/block movement [\#2921](https://github.com/VSCodeVim/Vim/pull/2921) ([xmbhasin](https://github.com/xmbhasin)) ## [v0.16.2](https://github.com/vscodevim/vim/tree/v0.16.2) (2018-08-30) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.1...v0.16.2) **Closed issues:** - Intermediate cursor shape to show that a command is being entered [\#2999](https://github.com/VSCodeVim/Vim/issues/2999) **Merged pull requests:** - Revert "Center cursor vertically on movement out of viewport" [\#3009](https://github.com/VSCodeVim/Vim/pull/3009) ([hhu94](https://github.com/hhu94)) - Update dependency typescript to v3.0.3 [\#3008](https://github.com/VSCodeVim/Vim/pull/3008) ([renovate-bot](https://github.com/renovate-bot)) - Update vim.searchHighlightColor in README.md [\#3007](https://github.com/VSCodeVim/Vim/pull/3007) ([ytang](https://github.com/ytang)) - v0.16.1 [\#2997](https://github.com/VSCodeVim/Vim/pull/2997) ([jpoon](https://github.com/jpoon)) ## [v0.16.1](https://github.com/vscodevim/vim/tree/v0.16.1) (2018-08-27) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.16.0...v0.16.1) **Fixed Bugs:** - `:vsp file\_name` cannot open file_name, although this file does exist [\#2983](https://github.com/VSCodeVim/Vim/issues/2983) - `gf` \(go to file under cursor\) produces the "Vim: The file ... does not exist." error, even though file clearly exists [\#2966](https://github.com/VSCodeVim/Vim/issues/2966) - Open File with :e deletes file content [\#2963](https://github.com/VSCodeVim/Vim/issues/2963) **Closed issues:** - "before": \["\", "C-s\>"\] not work. [\#2949](https://github.com/VSCodeVim/Vim/issues/2949) - VSCodeVim airline affecting color scheme [\#2948](https://github.com/VSCodeVim/Vim/issues/2948) - \[Feature Request\] : ReplaceWithRegister [\#2937](https://github.com/VSCodeVim/Vim/issues/2937) - % should match on strings & chars [\#2935](https://github.com/VSCodeVim/Vim/issues/2935) - Throw away the mouse [\#2922](https://github.com/VSCodeVim/Vim/issues/2922) - Wried cursor behavior with INSERT MULTI CURSOR mode [\#2910](https://github.com/VSCodeVim/Vim/issues/2910) **Merged pull requests:** - Lazy Load Neovim [\#2992](https://github.com/VSCodeVim/Vim/pull/2992) ([jpoon](https://github.com/jpoon)) - Update dependency @types/node to v9.6.30 [\#2987](https://github.com/VSCodeVim/Vim/pull/2987) ([renovate-bot](https://github.com/renovate-bot)) - Fix type in ROADMAP.md [\#2980](https://github.com/VSCodeVim/Vim/pull/2980) ([nickebbitt](https://github.com/nickebbitt)) - Fix emulated plugins link in README [\#2977](https://github.com/VSCodeVim/Vim/pull/2977) ([jjt](https://github.com/jjt)) - Fix `gf` showing error for files which exist [\#2969](https://github.com/VSCodeVim/Vim/pull/2969) ([arussellk](https://github.com/arussellk)) - Fix Typo in ROADMAP [\#2967](https://github.com/VSCodeVim/Vim/pull/2967) ([AdrieanKhisbe](https://github.com/AdrieanKhisbe)) - Center cursor vertically on movement out of viewport [\#2962](https://github.com/VSCodeVim/Vim/pull/2962) ([hhu94](https://github.com/hhu94)) - chore\(deps\): update dependency vscode to v1.1.21 [\#2958](https://github.com/VSCodeVim/Vim/pull/2958) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/node to v9.6.28 [\#2952](https://github.com/VSCodeVim/Vim/pull/2952) ([renovate-bot](https://github.com/renovate-bot)) ## [v0.16.0](https://github.com/vscodevim/vim/tree/v0.16.0) (2018-08-09) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.15.7...v0.16.0) **Enhancements:** - Reenable change that minimized the calls to setContext [\#2900](https://github.com/VSCodeVim/Vim/pull/2900) ([xconverge](https://github.com/xconverge)) **Fixed Bugs:** - Cannot create files with extensions using :e\[dit\] {file} [\#2923](https://github.com/VSCodeVim/Vim/issues/2923) - :tablast broken with vscode 1.25.0 [\#2813](https://github.com/VSCodeVim/Vim/issues/2813) - 2gt not goes to the right tab [\#2789](https://github.com/VSCodeVim/Vim/issues/2789) **Closed issues:** - "commandlineinprogress": "underline" causes issues [\#2896](https://github.com/VSCodeVim/Vim/issues/2896) - Quote macro sometimes doubling in Python [\#2662](https://github.com/VSCodeVim/Vim/issues/2662) - easy motion mapping key problem [\#1894](https://github.com/VSCodeVim/Vim/issues/1894) **Merged pull requests:** - bump version [\#2946](https://github.com/VSCodeVim/Vim/pull/2946) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency prettier to v1.14.2 [\#2943](https://github.com/VSCodeVim/Vim/pull/2943) ([renovate-bot](https://github.com/renovate-bot)) - docs: move configs to tables for readability [\#2941](https://github.com/VSCodeVim/Vim/pull/2941) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/node to v9.6.26 [\#2940](https://github.com/VSCodeVim/Vim/pull/2940) ([renovate-bot](https://github.com/renovate-bot)) - docs: clean-up readme [\#2931](https://github.com/VSCodeVim/Vim/pull/2931) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/lodash to v4.14.116 [\#2930](https://github.com/VSCodeVim/Vim/pull/2930) ([renovate-bot](https://github.com/renovate-bot)) - fix: files with extensions not being auto-created. closes \#2923. [\#2928](https://github.com/VSCodeVim/Vim/pull/2928) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/node to v9.6.25 [\#2927](https://github.com/VSCodeVim/Vim/pull/2927) ([renovate-bot](https://github.com/renovate-bot)) - Fix :tablast breaking in vscode 1.25 \#2813 [\#2926](https://github.com/VSCodeVim/Vim/pull/2926) ([Roshanjossey](https://github.com/Roshanjossey)) - chore\(deps\): update dependency typescript to v3 [\#2920](https://github.com/VSCodeVim/Vim/pull/2920) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency gulp-git to v2.8.0 [\#2919](https://github.com/VSCodeVim/Vim/pull/2919) ([renovate-bot](https://github.com/renovate-bot)) - Fix Emulated Plugins TOC link in README [\#2918](https://github.com/VSCodeVim/Vim/pull/2918) ([jjt](https://github.com/jjt)) - fix: use full path for configs [\#2915](https://github.com/VSCodeVim/Vim/pull/2915) ([jpoon](https://github.com/jpoon)) - fix: enable prettier for md [\#2909](https://github.com/VSCodeVim/Vim/pull/2909) ([jpoon](https://github.com/jpoon)) - Update dependency prettier to v1.14.0 [\#2908](https://github.com/VSCodeVim/Vim/pull/2908) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/node to v9.6.24 [\#2906](https://github.com/VSCodeVim/Vim/pull/2906) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/lodash to v4.14.115 [\#2905](https://github.com/VSCodeVim/Vim/pull/2905) ([renovate-bot](https://github.com/renovate-bot)) - Add --grep flag to gulp test [\#2904](https://github.com/VSCodeVim/Vim/pull/2904) ([xmbhasin](https://github.com/xmbhasin)) - Update dependency @types/lodash to v4.14.114 [\#2901](https://github.com/VSCodeVim/Vim/pull/2901) ([renovate-bot](https://github.com/renovate-bot)) - Fix gt tab navigation with count prefix [\#2899](https://github.com/VSCodeVim/Vim/pull/2899) ([xconverge](https://github.com/xconverge)) - Updating README FAQ [\#2894](https://github.com/VSCodeVim/Vim/pull/2894) ([augustnmonteiro](https://github.com/augustnmonteiro)) - refactor baseaction [\#2892](https://github.com/VSCodeVim/Vim/pull/2892) ([jpoon](https://github.com/jpoon)) - Revert "fix: use ferrarimarco's image instead of my fork to generate changelog" [\#2891](https://github.com/VSCodeVim/Vim/pull/2891) ([jpoon](https://github.com/jpoon)) - Integrate SmartIM to VSCodeVim [\#2643](https://github.com/VSCodeVim/Vim/pull/2643) ([daipeihust](https://github.com/daipeihust)) ## [v0.15.7](https://github.com/vscodevim/vim/tree/v0.15.7) (2018-07-25) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.15.6...v0.15.7) **Enhancements:** - Please use vscode's config folder for .cmdline_history [\#2799](https://github.com/VSCodeVim/Vim/issues/2799) - Improve neovim command execution status reporting in status bar [\#2878](https://github.com/VSCodeVim/Vim/pull/2878) ([xconverge](https://github.com/xconverge)) **Fixed Bugs:** - 'r' in insert mode not entered when typed quickly [\#2888](https://github.com/VSCodeVim/Vim/issues/2888) - Vim extension stops working [\#2873](https://github.com/VSCodeVim/Vim/issues/2873) **Closed issues:** - hjkl keys as arrow keys in intellisense contextual menu do not work [\#2885](https://github.com/VSCodeVim/Vim/issues/2885) **Merged pull requests:** - Fix issue with incorrectly finding and triggering certain remappings [\#2890](https://github.com/VSCodeVim/Vim/pull/2890) ([xconverge](https://github.com/xconverge)) - Move commandline history to XDG_CACHE_HOME or %APPDATA% [\#2889](https://github.com/VSCodeVim/Vim/pull/2889) ([xconverge](https://github.com/xconverge)) - fix: use ferrarimarco's image instead of my fork to generate changelog [\#2884](https://github.com/VSCodeVim/Vim/pull/2884) ([jpoon](https://github.com/jpoon)) - fix: use map to search for relevant actions. \#2021 [\#2883](https://github.com/VSCodeVim/Vim/pull/2883) ([jpoon](https://github.com/jpoon)) - fix: handle non-string remapped key. closes \#2873 [\#2881](https://github.com/VSCodeVim/Vim/pull/2881) ([jpoon](https://github.com/jpoon)) ## [v0.15.6](https://github.com/vscodevim/vim/tree/v0.15.6) (2018-07-24) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.15.5...v0.15.6) **Merged pull requests:** - Fix regression with setContext in modeHandler [\#2880](https://github.com/VSCodeVim/Vim/pull/2880) ([xconverge](https://github.com/xconverge)) ## [v0.15.5](https://github.com/vscodevim/vim/tree/v0.15.5) (2018-07-24) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.15.4...v0.15.5) **Merged pull requests:** - Neovim integration show errors when using commandline at correct times [\#2877](https://github.com/VSCodeVim/Vim/pull/2877) ([xconverge](https://github.com/xconverge)) - Improve error reporting with neovim commandline [\#2876](https://github.com/VSCodeVim/Vim/pull/2876) ([xconverge](https://github.com/xconverge)) - chore\(deps\): update dependency @types/lodash to v4.14.113 [\#2875](https://github.com/VSCodeVim/Vim/pull/2875) ([renovate-bot](https://github.com/renovate-bot)) ## [v0.15.4](https://github.com/vscodevim/vim/tree/v0.15.4) (2018-07-24) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.15.3...v0.15.4) **Enhancements:** - Moving down at a fold that's at the end of the file causes an infinite loop [\#1855](https://github.com/VSCodeVim/Vim/issues/1855) **Fixed Bugs:** - Long key chords does not trigger configured action. [\#2735](https://github.com/VSCodeVim/Vim/issues/2735) - Cursor jumps erratically before moving vertically [\#2163](https://github.com/VSCodeVim/Vim/issues/2163) **Closed issues:** - ^f stopped working after 1.25.1 update [\#2865](https://github.com/VSCodeVim/Vim/issues/2865) - Switching escape and capslock [\#2859](https://github.com/VSCodeVim/Vim/issues/2859) **Merged pull requests:** - fix: add missing wrapkeys to test configuration [\#2871](https://github.com/VSCodeVim/Vim/pull/2871) ([jpoon](https://github.com/jpoon)) - Improve foldfix performance and potentially fix some bugs\(\#1855 \#2163\) [\#2867](https://github.com/VSCodeVim/Vim/pull/2867) ([xmbhasin](https://github.com/xmbhasin)) - Roadmap doc fix for visual mode case switching [\#2866](https://github.com/VSCodeVim/Vim/pull/2866) ([pjlangley](https://github.com/pjlangley)) - Add whichwrap [\#2864](https://github.com/VSCodeVim/Vim/pull/2864) ([davidmfoley](https://github.com/davidmfoley)) - docs: add section on debugging remappings [\#2862](https://github.com/VSCodeVim/Vim/pull/2862) ([jpoon](https://github.com/jpoon)) - Cache mode so that calls to setContext is minimized [\#2861](https://github.com/VSCodeVim/Vim/pull/2861) ([xconverge](https://github.com/xconverge)) - Workaround surround bug [\#2830](https://github.com/VSCodeVim/Vim/pull/2830) ([reujab](https://github.com/reujab)) - Add unit test for long user configured chords. [\#2736](https://github.com/VSCodeVim/Vim/pull/2736) ([regiontog](https://github.com/regiontog)) ## [v0.15.3](https://github.com/vscodevim/vim/tree/v0.15.3) (2018-07-20) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.15.2...v0.15.3) **Fixed Bugs:** - :\$ requires additional enter to go to end of buffer [\#2858](https://github.com/VSCodeVim/Vim/issues/2858) **Merged pull requests:** - Fixes \$ and % commands [\#2860](https://github.com/VSCodeVim/Vim/pull/2860) ([xconverge](https://github.com/xconverge)) - fixed buggy interactive substitute replacements [\#2857](https://github.com/VSCodeVim/Vim/pull/2857) ([kevintighe](https://github.com/kevintighe)) ## [v0.15.2](https://github.com/vscodevim/vim/tree/v0.15.2) (2018-07-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.15.1...v0.15.2) **Fixed Bugs:** - Change surround tag with tag including a dot [\#2850](https://github.com/VSCodeVim/Vim/issues/2850) - Delete using \('d' + 'number' + '+/-'\) \(e.g. d5+\) doesn't work like expected. [\#2846](https://github.com/VSCodeVim/Vim/issues/2846) **Merged pull requests:** - fixes \#2850 [\#2856](https://github.com/VSCodeVim/Vim/pull/2856) ([xconverge](https://github.com/xconverge)) - fix: don't run test when launching through vscode [\#2854](https://github.com/VSCodeVim/Vim/pull/2854) ([jpoon](https://github.com/jpoon)) - v0.15.1 [\#2853](https://github.com/VSCodeVim/Vim/pull/2853) ([jpoon](https://github.com/jpoon)) - Interactive Substitute [\#2851](https://github.com/VSCodeVim/Vim/pull/2851) ([kevintighe](https://github.com/kevintighe)) ## [v0.15.1](https://github.com/vscodevim/vim/tree/v0.15.1) (2018-07-17) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.15.0...v0.15.1) **Enhancements:** - Option case-insensitive for vim-sneak [\#2829](https://github.com/VSCodeVim/Vim/issues/2829) - "x" operation far too cpu-hungry [\#1581](https://github.com/VSCodeVim/Vim/issues/1581) **Fixed Bugs:** - ctrl+v no longer pastes in insert mode [\#2646](https://github.com/VSCodeVim/Vim/issues/2646) **Merged pull requests:** - fix: upgrade winston to 3.0 [\#2852](https://github.com/VSCodeVim/Vim/pull/2852) ([jpoon](https://github.com/jpoon)) - update tslint and fix radix linting [\#2849](https://github.com/VSCodeVim/Vim/pull/2849) ([xconverge](https://github.com/xconverge)) - Update dependency @types/mocha to v5.2.5 [\#2847](https://github.com/VSCodeVim/Vim/pull/2847) ([renovate-bot](https://github.com/renovate-bot)) - gulp release [\#2841](https://github.com/VSCodeVim/Vim/pull/2841) ([jpoon](https://github.com/jpoon)) - Update dependency @types/lodash to v4.14.112 [\#2839](https://github.com/VSCodeVim/Vim/pull/2839) ([renovate-bot](https://github.com/renovate-bot)) - Add config option for sneak to use smartcase and ignorecase [\#2837](https://github.com/VSCodeVim/Vim/pull/2837) ([xconverge](https://github.com/xconverge)) ## [v0.15.0](https://github.com/vscodevim/vim/tree/v0.15.0) (2018-07-12) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.14.2...v0.15.0) **Enhancements:** - TypeError shown on invalid search command. [\#2823](https://github.com/VSCodeVim/Vim/issues/2823) - Allow registering keybindings commands using strings [\#2806](https://github.com/VSCodeVim/Vim/issues/2806) **Fixed Bugs:** - Keybindings not triggering [\#2833](https://github.com/VSCodeVim/Vim/issues/2833) - Macro doesn't memoryize `delete` key. [\#2702](https://github.com/VSCodeVim/Vim/issues/2702) - VimError's does not show up on the status bar [\#2525](https://github.com/VSCodeVim/Vim/issues/2525) **Merged pull requests:** - Add "cursor" to commandline entry [\#2836](https://github.com/VSCodeVim/Vim/pull/2836) ([xconverge](https://github.com/xconverge)) - Update issue templates [\#2825](https://github.com/VSCodeVim/Vim/pull/2825) ([jpoon](https://github.com/jpoon)) - Cache the mode for updating status bar colors [\#2822](https://github.com/VSCodeVim/Vim/pull/2822) ([xconverge](https://github.com/xconverge)) - chore\(deps\): update dependency @types/lodash to v4.14.111 [\#2821](https://github.com/VSCodeVim/Vim/pull/2821) ([renovate-bot](https://github.com/renovate-bot)) - Fix quickpick commandline [\#2816](https://github.com/VSCodeVim/Vim/pull/2816) ([xconverge](https://github.com/xconverge)) - Added ability to register commands using simple strings \(fixes \#2806\) [\#2807](https://github.com/VSCodeVim/Vim/pull/2807) ([6A](https://github.com/6A)) ## [v0.14.2](https://github.com/vscodevim/vim/tree/v0.14.2) (2018-07-06) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.14.1...v0.14.2) **Enhancements:** - \ doesn't behave as expected in insert mode [\#2804](https://github.com/VSCodeVim/Vim/issues/2804) - \(feature\) Add an option to bring commandline back to old place [\#2773](https://github.com/VSCodeVim/Vim/issues/2773) **Fixed Bugs:** - 2gt not goes to the right tab [\#2789](https://github.com/VSCodeVim/Vim/issues/2789) - Repeating a VISUAL LINE indentation is inconsistent with native vim behaviour [\#2606](https://github.com/VSCodeVim/Vim/issues/2606) - ngt/ngT for tab switching is broken [\#2580](https://github.com/VSCodeVim/Vim/issues/2580) **Closed issues:** - editor.cursorStyle not being respected [\#2809](https://github.com/VSCodeVim/Vim/issues/2809) **Merged pull requests:** - Make gt work correctly like gT [\#2812](https://github.com/VSCodeVim/Vim/pull/2812) ([xconverge](https://github.com/xconverge)) - chore\(deps\): update dependency @types/node to v9.6.23 [\#2811](https://github.com/VSCodeVim/Vim/pull/2811) ([renovate-bot](https://github.com/renovate-bot)) - feat: Update \ insert mode behavior [\#2805](https://github.com/VSCodeVim/Vim/pull/2805) ([mrwest808](https://github.com/mrwest808)) - bump version [\#2797](https://github.com/VSCodeVim/Vim/pull/2797) ([jpoon](https://github.com/jpoon)) - fixes \#2606 [\#2790](https://github.com/VSCodeVim/Vim/pull/2790) ([xconverge](https://github.com/xconverge)) - Allow for quickpick commandline usage [\#2781](https://github.com/VSCodeVim/Vim/pull/2781) ([xconverge](https://github.com/xconverge)) ## [v0.14.1](https://github.com/vscodevim/vim/tree/v0.14.1) (2018-06-30) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.14.0...v0.14.1) **Fixed Bugs:** - Remapping \> to editor.fold [\#2774](https://github.com/VSCodeVim/Vim/issues/2774) - Bug: Remapping Numbers \(0-9\) [\#2759](https://github.com/VSCodeVim/Vim/issues/2759) - At a certain point VSCodeVim "forgets" all remappings for every new tab opened [\#2271](https://github.com/VSCodeVim/Vim/issues/2271) **Closed issues:** - 0.14.0 doesn't work on Fedora 28, but 0.13.1 works. [\#2780](https://github.com/VSCodeVim/Vim/issues/2780) - \[neovim\] Inconsistent behaviour when clicking files in the file tree [\#2770](https://github.com/VSCodeVim/Vim/issues/2770) **Merged pull requests:** - doc: emojify readme [\#2796](https://github.com/VSCodeVim/Vim/pull/2796) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/mocha to v5.2.4 [\#2795](https://github.com/VSCodeVim/Vim/pull/2795) ([renovate-bot](https://github.com/renovate-bot)) - fix: enable remapping of numbers [\#2793](https://github.com/VSCodeVim/Vim/pull/2793) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency prettier to v1.13.7 [\#2786](https://github.com/VSCodeVim/Vim/pull/2786) ([renovate-bot](https://github.com/renovate-bot)) - refactor: simplify normalizekey\(\) by using existing map [\#2782](https://github.com/VSCodeVim/Vim/pull/2782) ([jpoon](https://github.com/jpoon)) - fix: fixes bug where null arguments to vscode executecommand would fail [\#2776](https://github.com/VSCodeVim/Vim/pull/2776) ([jpoon](https://github.com/jpoon)) ## [v0.14.0](https://github.com/vscodevim/vim/tree/v0.14.0) (2018-06-26) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.13.1...v0.14.0) **Fixed Bugs:** - Surround aliases not working as targets [\#2769](https://github.com/VSCodeVim/Vim/issues/2769) - Ctrl+D stuck on top of the window on visual mode [\#2766](https://github.com/VSCodeVim/Vim/issues/2766) - Cut two characters but only paste one. [\#2760](https://github.com/VSCodeVim/Vim/issues/2760) - Paste with CTRL+V while in edit mode does not work [\#2706](https://github.com/VSCodeVim/Vim/issues/2706) - Can't bind leader key shortcuts to some vscode methods [\#2674](https://github.com/VSCodeVim/Vim/issues/2674) - Searching forward / backward ignores count [\#2664](https://github.com/VSCodeVim/Vim/issues/2664) **Closed issues:** - Yanking/deleting multiline into default register then pasting over other multiline text copies that overwritten multiline text, instead of retaining original yanked text. [\#2717](https://github.com/VSCodeVim/Vim/issues/2717) - "S" \(capital s\) does not behave properly when on prefixing whitespace [\#2240](https://github.com/VSCodeVim/Vim/issues/2240) - Bug: Can't navigate in autocompletion with "Ctrl+j" and "Ctrl+k". [\#1980](https://github.com/VSCodeVim/Vim/issues/1980) - Backwards delete using "X" doesn't allow count prefixes [\#1780](https://github.com/VSCodeVim/Vim/issues/1780) **Merged pull requests:** - fixes \#2769 [\#2772](https://github.com/VSCodeVim/Vim/pull/2772) ([xconverge](https://github.com/xconverge)) - Fix \#2766. [\#2771](https://github.com/VSCodeVim/Vim/pull/2771) ([rebornix](https://github.com/rebornix)) - Update dependency prettier to v1.13.6 [\#2768](https://github.com/VSCodeVim/Vim/pull/2768) ([renovate-bot](https://github.com/renovate-bot)) - fixes \#2766 [\#2767](https://github.com/VSCodeVim/Vim/pull/2767) ([xconverge](https://github.com/xconverge)) - fixes \#1980 [\#2765](https://github.com/VSCodeVim/Vim/pull/2765) ([xconverge](https://github.com/xconverge)) - Fixes \#1780 [\#2764](https://github.com/VSCodeVim/Vim/pull/2764) ([xconverge](https://github.com/xconverge)) - fixes \#2664 and removes unused variable [\#2763](https://github.com/VSCodeVim/Vim/pull/2763) ([xconverge](https://github.com/xconverge)) - fixes \#2706 [\#2762](https://github.com/VSCodeVim/Vim/pull/2762) ([xconverge](https://github.com/xconverge)) - fixes \#2760 [\#2761](https://github.com/VSCodeVim/Vim/pull/2761) ([xconverge](https://github.com/xconverge)) - Move commandline to status bar to allow history navigation [\#2758](https://github.com/VSCodeVim/Vim/pull/2758) ([xconverge](https://github.com/xconverge)) - chore\(deps\): update dependency @types/mocha to v5.2.3 [\#2757](https://github.com/VSCodeVim/Vim/pull/2757) ([renovate-bot](https://github.com/renovate-bot)) - v0.13.1 [\#2753](https://github.com/VSCodeVim/Vim/pull/2753) ([jpoon](https://github.com/jpoon)) ## [v0.13.1](https://github.com/vscodevim/vim/tree/v0.13.1) (2018-06-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.13.0...v0.13.1) **Closed issues:** - Remapping ESC in insert mode with CR or Space does work via settings [\#2584](https://github.com/VSCodeVim/Vim/issues/2584) **Merged pull requests:** - fix: closes \#1472. insertModeKeyBindings apply to insert and replace modes [\#2749](https://github.com/VSCodeVim/Vim/pull/2749) ([jpoon](https://github.com/jpoon)) - fix: closes \#2390. enables remapping using '\' [\#2748](https://github.com/VSCodeVim/Vim/pull/2748) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/lodash to v4.14.110 [\#2745](https://github.com/VSCodeVim/Vim/pull/2745) ([renovate-bot](https://github.com/renovate-bot)) - Update visualModeKeyBindingsNonRecursive example [\#2744](https://github.com/VSCodeVim/Vim/pull/2744) ([chibicode](https://github.com/chibicode)) - Fix \#1348. ctrl+D/U correct position [\#2723](https://github.com/VSCodeVim/Vim/pull/2723) ([rebornix](https://github.com/rebornix)) ## [v0.13.0](https://github.com/vscodevim/vim/tree/v0.13.0) (2018-06-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.12.0...v0.13.0) **Breaking changes:** - Add normalModeKeyBindings and visualModeKeyBindings, remove otherModesKeyBindings [\#2726](https://github.com/VSCodeVim/Vim/pull/2726) ([chibicode](https://github.com/chibicode)) **Enhancements:** - Allow remappings from mocked configurations during testing. [\#2732](https://github.com/VSCodeVim/Vim/issues/2732) - use vscode task api [\#2731](https://github.com/VSCodeVim/Vim/issues/2731) - Add visualModeKeyBindings, in addition to otherModesKeyBindings [\#2705](https://github.com/VSCodeVim/Vim/issues/2705) - \[FEATURE REQUEST\] "q:" command [\#2617](https://github.com/VSCodeVim/Vim/issues/2617) - How to make a keybinding only work in visual mode? [\#1805](https://github.com/VSCodeVim/Vim/issues/1805) - Allow simplified keybinding syntax in settings.json [\#1667](https://github.com/VSCodeVim/Vim/issues/1667) **Fixed Bugs:** - gf creates files when the given file does not exist [\#2683](https://github.com/VSCodeVim/Vim/issues/2683) - Change/Delete/Yank combined with next unmatched bracket/parenthesis not behaving correctly [\#2670](https://github.com/VSCodeVim/Vim/issues/2670) - \[Bug report\]: 'c' key in multi-cursor mode removes additional cursors [\#2668](https://github.com/VSCodeVim/Vim/issues/2668) **Closed issues:** - Keybindings with Alt modifier. [\#2713](https://github.com/VSCodeVim/Vim/issues/2713) - Commands cc and S do not respect indent level if executed before the first character [\#2497](https://github.com/VSCodeVim/Vim/issues/2497) - Toggling Vim Mode using keybindings is broken [\#2381](https://github.com/VSCodeVim/Vim/issues/2381) - Searching finds nothing when pasting from cmd [\#2362](https://github.com/VSCodeVim/Vim/issues/2362) - Evil mode [\#2328](https://github.com/VSCodeVim/Vim/issues/2328) - different key bindings for normal and visual mode [\#2205](https://github.com/VSCodeVim/Vim/issues/2205) - need support for alt+x key mapping [\#2061](https://github.com/VSCodeVim/Vim/issues/2061) - Keybindings with space don't seem to work [\#2039](https://github.com/VSCodeVim/Vim/issues/2039) - \[Not Sure\] Copy using Windows Clipboard looses CR/LF [\#2022](https://github.com/VSCodeVim/Vim/issues/2022) - "TypeError: Cannot read property 'isEqual' of undefined" while debugging an extension with vim enabled [\#2019](https://github.com/VSCodeVim/Vim/issues/2019) - :m command doesn't work [\#2010](https://github.com/VSCodeVim/Vim/issues/2010) - pane switching is broken in newest vscode-insiders [\#1973](https://github.com/VSCodeVim/Vim/issues/1973) - \[Bug\] Copy text destroys special characters [\#1825](https://github.com/VSCodeVim/Vim/issues/1825) **Merged pull requests:** - fix: handle when commandLineHistory is empty [\#2741](https://github.com/VSCodeVim/Vim/pull/2741) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/node to v9.6.22 [\#2739](https://github.com/VSCodeVim/Vim/pull/2739) ([renovate-bot](https://github.com/renovate-bot)) - fix: use explicit configuration for logginglevel [\#2738](https://github.com/VSCodeVim/Vim/pull/2738) ([jpoon](https://github.com/jpoon)) - fix: remove duplicate UT [\#2734](https://github.com/VSCodeVim/Vim/pull/2734) ([jpoon](https://github.com/jpoon)) - Don't ignore mocked configurations' remaps during testing. [\#2733](https://github.com/VSCodeVim/Vim/pull/2733) ([regiontog](https://github.com/regiontog)) - chore\(deps\): update dependency typescript to v2.9.2 [\#2730](https://github.com/VSCodeVim/Vim/pull/2730) ([renovate-bot](https://github.com/renovate-bot)) - Fix autoindent on cc/S \#2497 [\#2729](https://github.com/VSCodeVim/Vim/pull/2729) ([dqsully](https://github.com/dqsully)) - chore\(deps\): update dependency @types/mocha to v5.2.2 [\#2724](https://github.com/VSCodeVim/Vim/pull/2724) ([renovate-bot](https://github.com/renovate-bot)) - fix: revert our workaround cursor toggle as this has been fixed in vscode [\#2720](https://github.com/VSCodeVim/Vim/pull/2720) ([jpoon](https://github.com/jpoon)) - feat: use winston for logging [\#2719](https://github.com/VSCodeVim/Vim/pull/2719) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency prettier to v1.13.5 [\#2718](https://github.com/VSCodeVim/Vim/pull/2718) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/node to v9.6.21 [\#2715](https://github.com/VSCodeVim/Vim/pull/2715) ([renovate-bot](https://github.com/renovate-bot)) - Update prettier dependency [\#2712](https://github.com/VSCodeVim/Vim/pull/2712) ([xconverge](https://github.com/xconverge)) - chore\(deps\): update dependency @types/mocha to v5.2.1 [\#2704](https://github.com/VSCodeVim/Vim/pull/2704) ([renovate-bot](https://github.com/renovate-bot)) - fix gf to be like issue \#2683 [\#2701](https://github.com/VSCodeVim/Vim/pull/2701) ([SuyogSoti](https://github.com/SuyogSoti)) - chore\(deps\): update dependency typescript to v2.9.1 [\#2698](https://github.com/VSCodeVim/Vim/pull/2698) ([renovate-bot](https://github.com/renovate-bot)) - Fix vim-commentary description in README [\#2694](https://github.com/VSCodeVim/Vim/pull/2694) ([Ran4](https://github.com/Ran4)) - chore\(deps\): update dependency @types/node to v9.6.20 [\#2691](https://github.com/VSCodeVim/Vim/pull/2691) ([renovate-bot](https://github.com/renovate-bot)) - fix: fix 'no-use-before-declare' requires type information lint warning [\#2679](https://github.com/VSCodeVim/Vim/pull/2679) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency gulp-git to v2.7.0 [\#2678](https://github.com/VSCodeVim/Vim/pull/2678) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency vscode to v1.1.18 [\#2676](https://github.com/VSCodeVim/Vim/pull/2676) ([renovate-bot](https://github.com/renovate-bot)) - Fixed difference in behavior for \]\) and \]} when combined with certain operators [\#2671](https://github.com/VSCodeVim/Vim/pull/2671) ([willcassella](https://github.com/willcassella)) - fix\(deps\): update dependency untildify to v3.0.3 [\#2669](https://github.com/VSCodeVim/Vim/pull/2669) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency mocha to v5.2.0 [\#2666](https://github.com/VSCodeVim/Vim/pull/2666) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/node to v9.6.16 [\#2644](https://github.com/VSCodeVim/Vim/pull/2644) ([renovate-bot](https://github.com/renovate-bot)) ## [v0.12.0](https://github.com/vscodevim/vim/tree/v0.12.0) (2018-05-16) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.11.6...v0.12.0) - Fix development problems on win [\#2651](https://github.com/VSCodeVim/Vim/pull/2651) ([KamikazeZirou](https://github.com/KamikazeZirou)) - Fixes \#2632 [\#2641](https://github.com/VSCodeVim/Vim/pull/2641) ([xconverge](https://github.com/xconverge)) - Revert "\[Fix\] Restore 'when' conditions in \, \, \" [\#2640](https://github.com/VSCodeVim/Vim/pull/2640) ([jpoon](https://github.com/jpoon)) - fix\(deps\): update dependency diff-match-patch to v1.0.1 [\#2631](https://github.com/VSCodeVim/Vim/pull/2631) ([renovate-bot](https://github.com/renovate-bot)) - Update dependency @types/node to v9.6.14 [\#2630](https://github.com/VSCodeVim/Vim/pull/2630) ([renovate-bot](https://github.com/renovate-bot)) - \[Fix\] Restore 'when' conditions in \, \, \ [\#2628](https://github.com/VSCodeVim/Vim/pull/2628) ([tyru](https://github.com/tyru)) - Link to Linux setup [\#2627](https://github.com/VSCodeVim/Vim/pull/2627) ([gggauravgandhi](https://github.com/gggauravgandhi)) - fix: immediately exit travis on build error [\#2626](https://github.com/VSCodeVim/Vim/pull/2626) ([jpoon](https://github.com/jpoon)) - fix: immediately exit if there is an error on ts [\#2625](https://github.com/VSCodeVim/Vim/pull/2625) ([jpoon](https://github.com/jpoon)) - feat: log to outputChannel [\#2623](https://github.com/VSCodeVim/Vim/pull/2623) ([jpoon](https://github.com/jpoon)) - Implement "q:" command [\#2618](https://github.com/VSCodeVim/Vim/pull/2618) ([KamikazeZirou](https://github.com/KamikazeZirou)) ## [v0.11.6](https://github.com/vscodevim/vim/tree/v0.11.6) (2018-05-07) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.11.5...v0.11.6) - chore\(deps\): update dependency @types/node to v9.6.12 [\#2615](https://github.com/VSCodeVim/Vim/pull/2615) ([renovate-bot](https://github.com/renovate-bot)) - \[Fix\] \* command highlights extra content [\#2611](https://github.com/VSCodeVim/Vim/pull/2611) ([tyru](https://github.com/tyru)) - \[Fix\] p in visual line appends unnecessary newline [\#2609](https://github.com/VSCodeVim/Vim/pull/2609) ([tyru](https://github.com/tyru)) - chore\(deps\): update dependency tslint to v5.10.0 [\#2605](https://github.com/VSCodeVim/Vim/pull/2605) ([renovate-bot](https://github.com/renovate-bot)) - Add o command in visual block mode [\#2604](https://github.com/VSCodeVim/Vim/pull/2604) ([tyru](https://github.com/tyru)) - \[Fix\] p in visual-mode should update register content [\#2602](https://github.com/VSCodeVim/Vim/pull/2602) ([tyru](https://github.com/tyru)) - \[Fix\] p won't work in linewise visual-mode at the end of document [\#2601](https://github.com/VSCodeVim/Vim/pull/2601) ([tyru](https://github.com/tyru)) - Add missing window keys \(\\\) [\#2600](https://github.com/VSCodeVim/Vim/pull/2600) ([tyru](https://github.com/tyru)) - fix: fail on ts transpile errors by setting noEmitOnErrors [\#2599](https://github.com/VSCodeVim/Vim/pull/2599) ([jpoon](https://github.com/jpoon)) - add easymotion-lineforward and easymotion-linebackward [\#2596](https://github.com/VSCodeVim/Vim/pull/2596) ([hy950831](https://github.com/hy950831)) - Fix description in 🔢 % command [\#2595](https://github.com/VSCodeVim/Vim/pull/2595) ([Ding-Fan](https://github.com/Ding-Fan)) - \[Fix\] \ should work as same as \ in search mode [\#2593](https://github.com/VSCodeVim/Vim/pull/2593) ([tyru](https://github.com/tyru)) - \[Fix\] aW doesn't work at the end of lines [\#2591](https://github.com/VSCodeVim/Vim/pull/2591) ([tyru](https://github.com/tyru)) - Implement gn,gN command [\#2589](https://github.com/VSCodeVim/Vim/pull/2589) ([tyru](https://github.com/tyru)) - \[Fix\] p in visual-mode should save last selection [\#2588](https://github.com/VSCodeVim/Vim/pull/2588) ([tyru](https://github.com/tyru)) - \[Fix\] Transition between v,V,\ is different with original Vim behavior [\#2581](https://github.com/VSCodeVim/Vim/pull/2581) ([tyru](https://github.com/tyru)) - \[Fix\] Don't add beginning newline of linewise put in visual-mode [\#2579](https://github.com/VSCodeVim/Vim/pull/2579) ([tyru](https://github.com/tyru)) - fix: Manually dispose ModeHandler when no longer needed [\#2577](https://github.com/VSCodeVim/Vim/pull/2577) ([BinaryKhaos](https://github.com/BinaryKhaos)) - chore\(deps\): update dependency vscode to v1.1.16 [\#2575](https://github.com/VSCodeVim/Vim/pull/2575) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/node to v9.6.7 [\#2573](https://github.com/VSCodeVim/Vim/pull/2573) ([renovate-bot](https://github.com/renovate-bot)) - Fixes \#2569. Fix vi{ for nested braces. [\#2572](https://github.com/VSCodeVim/Vim/pull/2572) ([Shadaraman](https://github.com/Shadaraman)) - Fixed neovim spawning in invalid directories [\#2570](https://github.com/VSCodeVim/Vim/pull/2570) ([Chillee](https://github.com/Chillee)) - chore\(deps\): update dependency @types/lodash to v4.14.108 [\#2565](https://github.com/VSCodeVim/Vim/pull/2565) ([renovate-bot](https://github.com/renovate-bot)) - Hopefully fixing the rest of our undo issues [\#2559](https://github.com/VSCodeVim/Vim/pull/2559) ([Chillee](https://github.com/Chillee)) ## [v0.11.5](https://github.com/vscodevim/vim/tree/v0.11.5) (2018-04-23) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.11.4...v0.11.5) - chore\(deps\): update dependency gulp-bump to v3.1.1 [\#2556](https://github.com/VSCodeVim/Vim/pull/2556) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency typescript to v2.8.3 [\#2553](https://github.com/VSCodeVim/Vim/pull/2553) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/node to v9.6.6 [\#2551](https://github.com/VSCodeVim/Vim/pull/2551) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/mocha to v5.2.0 [\#2550](https://github.com/VSCodeVim/Vim/pull/2550) ([renovate-bot](https://github.com/renovate-bot)) - Fixed undo issue given in \#2545 [\#2547](https://github.com/VSCodeVim/Vim/pull/2547) ([Chillee](https://github.com/Chillee)) - chore\(deps\): update dependency mocha to v5.1.1 [\#2546](https://github.com/VSCodeVim/Vim/pull/2546) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency prettier to v1.12.1 [\#2543](https://github.com/VSCodeVim/Vim/pull/2543) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/lodash to v4.14.107 [\#2540](https://github.com/VSCodeVim/Vim/pull/2540) ([renovate-bot](https://github.com/renovate-bot)) ## [v0.11.4](https://github.com/vscodevim/vim/tree/v0.11.4) (2018-04-14) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.11.3...v0.11.4) - fix: don't call prettier when no files updated [\#2539](https://github.com/VSCodeVim/Vim/pull/2539) ([jpoon](https://github.com/jpoon)) - chore\(dep\): upgrade gulp-bump, gulp-git, gulp-typescript, prettier, typescript, vscode [\#2538](https://github.com/VSCodeVim/Vim/pull/2538) ([jpoon](https://github.com/jpoon)) - chore\(deps\): update dependency @types/node to v9.6.5 [\#2535](https://github.com/VSCodeVim/Vim/pull/2535) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency mocha to v5.1.0 [\#2534](https://github.com/VSCodeVim/Vim/pull/2534) ([renovate-bot](https://github.com/renovate-bot)) - docs: update readme to indicate restart of vscode needed [\#2530](https://github.com/VSCodeVim/Vim/pull/2530) ([jdhines](https://github.com/jdhines)) - chore\(deps\): update dependency @types/node to v9.6.4 [\#2528](https://github.com/VSCodeVim/Vim/pull/2528) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/diff to v3.5.1 [\#2527](https://github.com/VSCodeVim/Vim/pull/2527) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency @types/diff to v3.5.0 [\#2523](https://github.com/VSCodeVim/Vim/pull/2523) ([renovate-bot](https://github.com/renovate-bot)) - bug: Neovim not spawned in appropriate directory \(fixes \#2482\) [\#2522](https://github.com/VSCodeVim/Vim/pull/2522) ([Chillee](https://github.com/Chillee)) - bug: fixes behaviour of search when using \* and \# \(fixes \#2517\) [\#2518](https://github.com/VSCodeVim/Vim/pull/2518) ([clamb](https://github.com/clamb)) - chore\(deps\): update dependency @types/node to v9.6.2 [\#2509](https://github.com/VSCodeVim/Vim/pull/2509) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update node docker tag to v8.11 [\#2496](https://github.com/VSCodeVim/Vim/pull/2496) ([renovate-bot](https://github.com/renovate-bot)) - chore\(deps\): update dependency mocha to v5.0.5 [\#2490](https://github.com/VSCodeVim/Vim/pull/2490) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency gulp-tslint to v8.1.3 [\#2489](https://github.com/VSCodeVim/Vim/pull/2489) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): update dependency @types/lodash to v4.14.106 [\#2485](https://github.com/VSCodeVim/Vim/pull/2485) ([renovate[bot]](https://github.com/apps/renovate)) - chore\(deps\): pin dependencies [\#2483](https://github.com/VSCodeVim/Vim/pull/2483) ([renovate[bot]](https://github.com/apps/renovate)) - Configure Renovate [\#2480](https://github.com/VSCodeVim/Vim/pull/2480) ([renovate[bot]](https://github.com/apps/renovate)) - Add jumptoanywhere command for easymotion [\#2454](https://github.com/VSCodeVim/Vim/pull/2454) ([jsonMartin](https://github.com/jsonMartin)) ## [v0.11.3](https://github.com/vscodevim/vim/tree/v0.11.3) (2018-03-29) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.11.2...v0.11.3) - docs: add documentation for installing xsel. fixes \#2071 [\#2476](https://github.com/VSCodeVim/Vim/pull/2476) ([jpoon](https://github.com/jpoon)) - Respect vim.visualstar configuration \(fixes \#2469\) [\#2470](https://github.com/VSCodeVim/Vim/pull/2470) ([ytang](https://github.com/ytang)) - feat: Added \= keybind [\#2453](https://github.com/VSCodeVim/Vim/pull/2453) ([844196](https://github.com/844196)) - neovim.ts: typo in log [\#2451](https://github.com/VSCodeVim/Vim/pull/2451) ([prakashdanish](https://github.com/prakashdanish)) - await openEditorAtIndex1 command [\#2442](https://github.com/VSCodeVim/Vim/pull/2442) ([arussellk](https://github.com/arussellk)) ## [v0.11.2](https://github.com/vscodevim/vim/tree/v0.11.2) (2018-03-09) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.11.1...v0.11.2) - Readds vimState.lastClickWasPastEOL. Fixes \#2404 [\#2433](https://github.com/VSCodeVim/Vim/pull/2433) ([Chillee](https://github.com/Chillee)) - fix: selection in search in visual mode \#2406 [\#2418](https://github.com/VSCodeVim/Vim/pull/2418) ([shortheron](https://github.com/shortheron)) ## [v0.11.1](https://github.com/vscodevim/vim/tree/v0.11.1) (2018-03-08) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.11.0...v0.11.1) - Set the timeout to 0 for waitforcursorupdatestopropagate [\#2428](https://github.com/VSCodeVim/Vim/pull/2428) ([Chillee](https://github.com/Chillee)) - fix: use 'fsPath'. closes \#2422 [\#2426](https://github.com/VSCodeVim/Vim/pull/2426) ([jpoon](https://github.com/jpoon)) - fix: don't overwrite file if file exists. fixes \#2408 [\#2409](https://github.com/VSCodeVim/Vim/pull/2409) ([jpoon](https://github.com/jpoon)) - Fix :tabm to use moveActiveEditor command [\#2405](https://github.com/VSCodeVim/Vim/pull/2405) ([arussellk](https://github.com/arussellk)) ## [v0.11.0](https://github.com/vscodevim/vim/tree/v0.11.0) (2018-02-26) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.13...v0.11.0) - Fix :tabe {file} only relative to current file \(\#1162\) [\#2400](https://github.com/VSCodeVim/Vim/pull/2400) ([arussellk](https://github.com/arussellk)) - fix: clean-up neovim processes. closes \#2038 [\#2395](https://github.com/VSCodeVim/Vim/pull/2395) ([jpoon](https://github.com/jpoon)) - refactor: no need to set current mode twice [\#2394](https://github.com/VSCodeVim/Vim/pull/2394) ([jpoon](https://github.com/jpoon)) - feat: create file if file does not exist. closes \#2274 [\#2392](https://github.com/VSCodeVim/Vim/pull/2392) ([jpoon](https://github.com/jpoon)) - fix: status bar when configuration.showcmd is set \(fixes \#2365\) [\#2386](https://github.com/VSCodeVim/Vim/pull/2386) ([jpoon](https://github.com/jpoon)) - `jj` cursor position fix for \#1418 [\#2366](https://github.com/VSCodeVim/Vim/pull/2366) ([prog666](https://github.com/prog666)) - fix: actually run prettier [\#2359](https://github.com/VSCodeVim/Vim/pull/2359) ([jpoon](https://github.com/jpoon)) - feat: implements usage of `insert` to toggle between modes \(as per \#1787\) [\#2356](https://github.com/VSCodeVim/Vim/pull/2356) ([jpoon](https://github.com/jpoon)) - Build Improvements [\#2351](https://github.com/VSCodeVim/Vim/pull/2351) ([jpoon](https://github.com/jpoon)) - Possibility to set statusBar foreground color [\#2350](https://github.com/VSCodeVim/Vim/pull/2350) ([mgor](https://github.com/mgor)) - Fixes \#2346 [\#2347](https://github.com/VSCodeVim/Vim/pull/2347) ([Chillee](https://github.com/Chillee)) - Improve Test Infrastructure [\#2335](https://github.com/VSCodeVim/Vim/pull/2335) ([jpoon](https://github.com/jpoon)) - fix typo in README [\#2327](https://github.com/VSCodeVim/Vim/pull/2327) ([hayley](https://github.com/hayley)) - Sneak plugin [\#2307](https://github.com/VSCodeVim/Vim/pull/2307) ([jpotterm](https://github.com/jpotterm)) ## [v0.10.13](https://github.com/vscodevim/vim/tree/v0.10.13) (2018-01-23) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.12...v0.10.13) - fix: bad jason. fix bad release. [\#2324](https://github.com/VSCodeVim/Vim/pull/2324) ([jpoon](https://github.com/jpoon)) ## [v0.10.12](https://github.com/vscodevim/vim/tree/v0.10.12) (2018-01-23) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.11...v0.10.12) - fix: closes \#730. setcontext when switching active text editors [\#2320](https://github.com/VSCodeVim/Vim/pull/2320) ([jpoon](https://github.com/jpoon)) - Update README for Mac key repeat [\#2316](https://github.com/VSCodeVim/Vim/pull/2316) ([puradox](https://github.com/puradox)) - Default to vim behaviour for Ctrl+D [\#2314](https://github.com/VSCodeVim/Vim/pull/2314) ([Graham42](https://github.com/Graham42)) - Left shift fix 2299 [\#2300](https://github.com/VSCodeVim/Vim/pull/2300) ([jessewmc](https://github.com/jessewmc)) ## [v0.10.11](https://github.com/vscodevim/vim/tree/v0.10.11) (2018-01-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.10...v0.10.11) - fix: status bar not updating properly when recording macros. fixes \#2296. [\#2304](https://github.com/VSCodeVim/Vim/pull/2304) ([jpoon](https://github.com/jpoon)) ## [v0.10.10](https://github.com/vscodevim/vim/tree/v0.10.10) (2018-01-16) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.9...v0.10.10) - fix: add tests for compareKeyPressSequence [\#2289](https://github.com/VSCodeVim/Vim/pull/2289) ([jpoon](https://github.com/jpoon)) - Fix BaseAction.couldActionApply to work with two-dimensional keys array [\#2288](https://github.com/VSCodeVim/Vim/pull/2288) ([jpotterm](https://github.com/jpotterm)) - refactor: move modehandlermap to own class [\#2285](https://github.com/VSCodeVim/Vim/pull/2285) ([jpoon](https://github.com/jpoon)) - fix: status bar not updating following toggle [\#2283](https://github.com/VSCodeVim/Vim/pull/2283) ([jpoon](https://github.com/jpoon)) - Fix: Warnings when retrieving configurations w/o resource [\#2282](https://github.com/VSCodeVim/Vim/pull/2282) ([jpoon](https://github.com/jpoon)) - fix: \ remapping disabled by default. functionality controlled by "handleKeys" [\#2269](https://github.com/VSCodeVim/Vim/pull/2269) ([Arxzin](https://github.com/Arxzin)) ## [v0.10.9](https://github.com/vscodevim/vim/tree/v0.10.9) (2018-01-11) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.8...v0.10.9) - feature: "h", "l" keybindings for sidebar [\#2290](https://github.com/VSCodeVim/Vim/pull/2290) ([Nodman](https://github.com/Nodman)) - fix: no need to change cursor if there is no active editor. closes \#2273 [\#2278](https://github.com/VSCodeVim/Vim/pull/2278) ([jpoon](https://github.com/jpoon)) - fix: fixes circular dependency between notation and configuration [\#2277](https://github.com/VSCodeVim/Vim/pull/2277) ([jpoon](https://github.com/jpoon)) - fix: show cmd-line errors in status bar. add new E492 error [\#2272](https://github.com/VSCodeVim/Vim/pull/2272) ([jpoon](https://github.com/jpoon)) - refactor: normalize keys when loading configuration [\#2268](https://github.com/VSCodeVim/Vim/pull/2268) ([jpoon](https://github.com/jpoon)) ## [v0.10.8](https://github.com/vscodevim/vim/tree/v0.10.8) (2018-01-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.7...v0.10.8) - fix\(2162\): handleKeys was previously only handling negation [\#2267](https://github.com/VSCodeVim/Vim/pull/2267) ([jpoon](https://github.com/jpoon)) - fix\(2264\): go-to-line [\#2266](https://github.com/VSCodeVim/Vim/pull/2266) ([jpoon](https://github.com/jpoon)) - fix\(2261\): change status bar text for search-in-progress to be more l… [\#2263](https://github.com/VSCodeVim/Vim/pull/2263) ([jpoon](https://github.com/jpoon)) - fix\(2261\): fix regression. show search string in status bar [\#2262](https://github.com/VSCodeVim/Vim/pull/2262) ([jpoon](https://github.com/jpoon)) ## [v0.10.7](https://github.com/vscodevim/vim/tree/v0.10.7) (2018-01-04) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.6...v0.10.7) - Stop Silently Failing [\#2250](https://github.com/VSCodeVim/Vim/pull/2250) ([jpoon](https://github.com/jpoon)) - Misc Bug Fixes and Refactoring [\#2243](https://github.com/VSCodeVim/Vim/pull/2243) ([jpoon](https://github.com/jpoon)) - fix\(2184\): handle situation when no document is opened [\#2237](https://github.com/VSCodeVim/Vim/pull/2237) ([jpoon](https://github.com/jpoon)) ## [v0.10.6](https://github.com/vscodevim/vim/tree/v0.10.6) (2017-12-15) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.5...v0.10.6) - update\(package.json\) [\#2225](https://github.com/VSCodeVim/Vim/pull/2225) ([jpoon](https://github.com/jpoon)) - Add C-\[ to Replace Mode escape [\#2223](https://github.com/VSCodeVim/Vim/pull/2223) ([deybhayden](https://github.com/deybhayden)) - Do not open open file dialog when calling `:e!` [\#2215](https://github.com/VSCodeVim/Vim/pull/2215) ([squgeim](https://github.com/squgeim)) - Update `list.\*` command keybindings [\#2213](https://github.com/VSCodeVim/Vim/pull/2213) ([joaomoreno](https://github.com/joaomoreno)) - moar clean-up [\#2208](https://github.com/VSCodeVim/Vim/pull/2208) ([jpoon](https://github.com/jpoon)) - Fix cursor position of \ command in insertmode [\#2206](https://github.com/VSCodeVim/Vim/pull/2206) ([hy950831](https://github.com/hy950831)) - refactor\(modehandler-updateview\): use map and remove unused context [\#2197](https://github.com/VSCodeVim/Vim/pull/2197) ([jpoon](https://github.com/jpoon)) - Integrate TravisBuddy [\#2191](https://github.com/VSCodeVim/Vim/pull/2191) ([bluzi](https://github.com/bluzi)) - Fix \#2168: Surround offset [\#2171](https://github.com/VSCodeVim/Vim/pull/2171) ([westim](https://github.com/westim)) - Fix \#1945 \$ in VisualBlock works on ragged lines [\#2096](https://github.com/VSCodeVim/Vim/pull/2096) ([Strafos](https://github.com/Strafos)) ## [v0.10.5](https://github.com/vscodevim/vim/tree/v0.10.5) (2017-11-21) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.4...v0.10.5) - Fixed incorrect styling of 'fake' cursors [\#2161](https://github.com/VSCodeVim/Vim/pull/2161) ([Chillee](https://github.com/Chillee)) - Fix \#2155, Fix \#2133: escape delimiter substitute [\#2159](https://github.com/VSCodeVim/Vim/pull/2159) ([westim](https://github.com/westim)) - Fix \#2148: vertical split command [\#2158](https://github.com/VSCodeVim/Vim/pull/2158) ([westim](https://github.com/westim)) - fix\(1673\): re-enable some tests [\#2152](https://github.com/VSCodeVim/Vim/pull/2152) ([jpoon](https://github.com/jpoon)) - keep workbench color customizations when using status bar color [\#2122](https://github.com/VSCodeVim/Vim/pull/2122) ([rodrigo-garcia-leon](https://github.com/rodrigo-garcia-leon)) ## [v0.10.4](https://github.com/vscodevim/vim/tree/v0.10.4) (2017-11-14) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.3...v0.10.4) - fix\(2145\): reverse logic [\#2147](https://github.com/VSCodeVim/Vim/pull/2147) ([jpoon](https://github.com/jpoon)) ## [v0.10.3](https://github.com/vscodevim/vim/tree/v0.10.3) (2017-11-13) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.2...v0.10.3) - Fix release [\#2142](https://github.com/VSCodeVim/Vim/pull/2142) ([jpoon](https://github.com/jpoon)) - Code Cleanup [\#2138](https://github.com/VSCodeVim/Vim/pull/2138) ([jpoon](https://github.com/jpoon)) - Fixed typo in README [\#2137](https://github.com/VSCodeVim/Vim/pull/2137) ([Nonoctis](https://github.com/Nonoctis)) - fix\(travis\): use lts/carbon \(v8.9.1\) for travis [\#2129](https://github.com/VSCodeVim/Vim/pull/2129) ([jpoon](https://github.com/jpoon)) - Fix ^, \$, add case sensitivity override in search [\#2123](https://github.com/VSCodeVim/Vim/pull/2123) ([parkovski](https://github.com/parkovski)) - fix vscode launch/tasks [\#2121](https://github.com/VSCodeVim/Vim/pull/2121) ([jpoon](https://github.com/jpoon)) - Fix remapping keys to actions with "mustBeFirstKey", fixes \#2216 [\#2117](https://github.com/VSCodeVim/Vim/pull/2117) ([ohjames](https://github.com/ohjames)) - Fixes \#2113: Start in Disabled mode configuration. [\#2115](https://github.com/VSCodeVim/Vim/pull/2115) ([westim](https://github.com/westim)) - fix\(line-endings\): change all files to lf [\#2111](https://github.com/VSCodeVim/Vim/pull/2111) ([jpoon](https://github.com/jpoon)) - fix\(build\): position does not exist for replacetexttransformation [\#2105](https://github.com/VSCodeVim/Vim/pull/2105) ([jpoon](https://github.com/jpoon)) - Use 'editor.unfold' with direction: 'down' [\#2104](https://github.com/VSCodeVim/Vim/pull/2104) ([aeschli](https://github.com/aeschli)) - Pesky penguin CHANGELOG.md update. [\#2091](https://github.com/VSCodeVim/Vim/pull/2091) ([westim](https://github.com/westim)) - Added unit tests for movement commands. [\#2088](https://github.com/VSCodeVim/Vim/pull/2088) ([westim](https://github.com/westim)) - Fix \#2080 [\#2087](https://github.com/VSCodeVim/Vim/pull/2087) ([Strafos](https://github.com/Strafos)) - Update Contributors [\#2083](https://github.com/VSCodeVim/Vim/pull/2083) ([mcsosa121](https://github.com/mcsosa121)) - Fixes \#1974: U command [\#2081](https://github.com/VSCodeVim/Vim/pull/2081) ([westim](https://github.com/westim)) - Fix \#2063 [\#2079](https://github.com/VSCodeVim/Vim/pull/2079) ([Strafos](https://github.com/Strafos)) - Fix \#1852 surround issue at end of line [\#2077](https://github.com/VSCodeVim/Vim/pull/2077) ([Strafos](https://github.com/Strafos)) - added `showOpenDialog` when typing emtpy e [\#2067](https://github.com/VSCodeVim/Vim/pull/2067) ([DanEEStar](https://github.com/DanEEStar)) - Fix gj/gk in visual block mode [\#2046](https://github.com/VSCodeVim/Vim/pull/2046) ([orn688](https://github.com/orn688)) ## [v0.10.2](https://github.com/vscodevim/vim/tree/v0.10.2) (2017-10-14) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.1...v0.10.2) - Update ROADMAP.md [\#2073](https://github.com/VSCodeVim/Vim/pull/2073) ([xconverge](https://github.com/xconverge)) - Change ignoreFocusOut to false for the command line [\#2072](https://github.com/VSCodeVim/Vim/pull/2072) ([gadkadosh](https://github.com/gadkadosh)) - Upgrade packages [\#2070](https://github.com/VSCodeVim/Vim/pull/2070) ([jpoon](https://github.com/jpoon)) - fixes \#1576 and showcmd configuration option [\#2069](https://github.com/VSCodeVim/Vim/pull/2069) ([xconverge](https://github.com/xconverge)) - removed code which is not needed anymore due to \#2062 [\#2065](https://github.com/VSCodeVim/Vim/pull/2065) ([DanEEStar](https://github.com/DanEEStar)) - An option to show the colon at the start of the command line box [\#2064](https://github.com/VSCodeVim/Vim/pull/2064) ([gadkadosh](https://github.com/gadkadosh)) - Bugfix \#1951: text selection in insert mode [\#2062](https://github.com/VSCodeVim/Vim/pull/2062) ([DanEEStar](https://github.com/DanEEStar)) - Dispose modehandler if NO documents match the modehandler document anymore [\#2058](https://github.com/VSCodeVim/Vim/pull/2058) ([xconverge](https://github.com/xconverge)) - Fixes \#2050 Allow custom cursor styles per mode [\#2054](https://github.com/VSCodeVim/Vim/pull/2054) ([xconverge](https://github.com/xconverge)) - Fixes \#1824: g; and g, commands. [\#2040](https://github.com/VSCodeVim/Vim/pull/2040) ([westim](https://github.com/westim)) - Fixes \#1248: support for '., `., and gi commands. [\#2037](https://github.com/VSCodeVim/Vim/pull/2037) ([westim](https://github.com/westim)) - Fix for issue \#1860, visual multicursor movement. [\#2036](https://github.com/VSCodeVim/Vim/pull/2036) ([westim](https://github.com/westim)) - Fix a typo [\#2028](https://github.com/VSCodeVim/Vim/pull/2028) ([joonro](https://github.com/joonro)) ## [v0.10.1](https://github.com/vscodevim/vim/tree/v0.10.1) (2017-09-16) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.10.0...v0.10.1) - Fixing travis issues [\#2024](https://github.com/VSCodeVim/Vim/pull/2024) ([Chillee](https://github.com/Chillee)) - Correct behavior of mouseSelectionGoesIntoVisualMode [\#2020](https://github.com/VSCodeVim/Vim/pull/2020) ([nguymin4](https://github.com/nguymin4)) - Easymotion improvements [\#2017](https://github.com/VSCodeVim/Vim/pull/2017) ([MaxfieldWalker](https://github.com/MaxfieldWalker)) - fix \#2009 [\#2012](https://github.com/VSCodeVim/Vim/pull/2012) ([MaxfieldWalker](https://github.com/MaxfieldWalker)) - Fix deref of undefined race on startup. [\#2002](https://github.com/VSCodeVim/Vim/pull/2002) ([brandonbloom](https://github.com/brandonbloom)) - Use Go To Def & history absent a tag stack. [\#2001](https://github.com/VSCodeVim/Vim/pull/2001) ([brandonbloom](https://github.com/brandonbloom)) - Fix\#1981 [\#1997](https://github.com/VSCodeVim/Vim/pull/1997) ([MaxfieldWalker](https://github.com/MaxfieldWalker)) - Improvements to paragraph text objects. [\#1996](https://github.com/VSCodeVim/Vim/pull/1996) ([brandonbloom](https://github.com/brandonbloom)) - Implement '' and ``. [\#1993](https://github.com/VSCodeVim/Vim/pull/1993) ([brandonbloom](https://github.com/brandonbloom)) ## [v0.10.0](https://github.com/vscodevim/vim/tree/v0.10.0) (2017-08-30) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.9.0...v0.10.0) - Make prettier work on Windows [\#1987](https://github.com/VSCodeVim/Vim/pull/1987) ([MaxfieldWalker](https://github.com/MaxfieldWalker)) - Remove flaky tests [\#1982](https://github.com/VSCodeVim/Vim/pull/1982) ([Chillee](https://github.com/Chillee)) - Fixed iW on beginning of word \(\#1935\) [\#1977](https://github.com/VSCodeVim/Vim/pull/1977) ([Ghust1995](https://github.com/Ghust1995)) - Easymotion new features [\#1967](https://github.com/VSCodeVim/Vim/pull/1967) ([MaxfieldWalker](https://github.com/MaxfieldWalker)) - Trying to fix the travis issues with neovim [\#1958](https://github.com/VSCodeVim/Vim/pull/1958) ([Chillee](https://github.com/Chillee)) - Fixes \#1941: Action repetition with Ctrl-\[ [\#1953](https://github.com/VSCodeVim/Vim/pull/1953) ([tagniam](https://github.com/tagniam)) - Fixes \#1950: counter for \$ [\#1952](https://github.com/VSCodeVim/Vim/pull/1952) ([tagniam](https://github.com/tagniam)) - Makes all tests pass on Windows [\#1939](https://github.com/VSCodeVim/Vim/pull/1939) ([philipmat](https://github.com/philipmat)) - Update tests due to VSCode PR 28238 [\#1926](https://github.com/VSCodeVim/Vim/pull/1926) ([philipmat](https://github.com/philipmat)) - fix `z O` unfoldRecursively [\#1924](https://github.com/VSCodeVim/Vim/pull/1924) ([VincentBel](https://github.com/VincentBel)) - Renamed test to reflect purpose [\#1913](https://github.com/VSCodeVim/Vim/pull/1913) ([philipmat](https://github.com/philipmat)) - Ctrl-C should copy to clipboard in visual mode - fix for \#1896 [\#1912](https://github.com/VSCodeVim/Vim/pull/1912) ([philipmat](https://github.com/philipmat)) - Substitute global flag \(like Vim's `gdefault`\) [\#1909](https://github.com/VSCodeVim/Vim/pull/1909) ([philipmat](https://github.com/philipmat)) - Fixes \#1871: Adds configuration option to go into visual mode upon clicking in insert mode [\#1898](https://github.com/VSCodeVim/Vim/pull/1898) ([Chillee](https://github.com/Chillee)) - Fixes \#1886: indent repeat doesn't work in visual mode [\#1890](https://github.com/VSCodeVim/Vim/pull/1890) ([Chillee](https://github.com/Chillee)) - Formattted everything with prettier [\#1879](https://github.com/VSCodeVim/Vim/pull/1879) ([Chillee](https://github.com/Chillee)) ## [v0.9.0](https://github.com/vscodevim/vim/tree/v0.9.0) (2017-06-24) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.8.7...v0.9.0) - fixes \#1861 [\#1868](https://github.com/VSCodeVim/Vim/pull/1868) ([xconverge](https://github.com/xconverge)) - Fix off by one error in visual mode [\#1862](https://github.com/VSCodeVim/Vim/pull/1862) ([Chillee](https://github.com/Chillee)) ## [v0.8.7](https://github.com/vscodevim/vim/tree/v0.8.7) (2017-06-23) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.8.6...v0.8.7) - Added :only command and corresponding shortcuts [\#1882](https://github.com/VSCodeVim/Vim/pull/1882) ([LeonB](https://github.com/LeonB)) - Select in visual mode when scrolling [\#1859](https://github.com/VSCodeVim/Vim/pull/1859) ([Chillee](https://github.com/Chillee)) - Fixes \#1857: P not creating an undo stop [\#1858](https://github.com/VSCodeVim/Vim/pull/1858) ([Chillee](https://github.com/Chillee)) - Fixes \#979: Adds q! to close without saving [\#1854](https://github.com/VSCodeVim/Vim/pull/1854) ([Chillee](https://github.com/Chillee)) - Update README.md \(minor\) [\#1851](https://github.com/VSCodeVim/Vim/pull/1851) ([BlueDrink9](https://github.com/BlueDrink9)) - fixes \#1843 A and I preceded by count [\#1846](https://github.com/VSCodeVim/Vim/pull/1846) ([xconverge](https://github.com/xconverge)) - WIP Fixes \#754: Adds j,k,o,\, gg, G, ctrl+d, and ctrl+u commands for navigating inside the file explorer [\#1718](https://github.com/VSCodeVim/Vim/pull/1718) ([Chillee](https://github.com/Chillee)) ## [v0.8.6](https://github.com/vscodevim/vim/tree/v0.8.6) (2017-06-15) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.8.5...v0.8.6) - Removed solid block cursor [\#1842](https://github.com/VSCodeVim/Vim/pull/1842) ([Chillee](https://github.com/Chillee)) - Fix yiw cursor pos [\#1837](https://github.com/VSCodeVim/Vim/pull/1837) ([xconverge](https://github.com/xconverge)) - Fixes \#1794: Undo not undoing all changes [\#1833](https://github.com/VSCodeVim/Vim/pull/1833) ([Chillee](https://github.com/Chillee)) - Fixes \#1827: Autocomplete fails when any lines are wrapped/folded [\#1832](https://github.com/VSCodeVim/Vim/pull/1832) ([Chillee](https://github.com/Chillee)) - Fixes \#1826: Jump to line with neovim disabled doesn't work [\#1831](https://github.com/VSCodeVim/Vim/pull/1831) ([Chillee](https://github.com/Chillee)) ## [v0.8.5](https://github.com/vscodevim/vim/tree/v0.8.5) (2017-06-11) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.8.4...v0.8.5) - Fixes \#1814: Undo history getting deleted when file changes [\#1820](https://github.com/VSCodeVim/Vim/pull/1820) ([Chillee](https://github.com/Chillee)) - Fixes \#1200: :e doesn't expand tildes [\#1819](https://github.com/VSCodeVim/Vim/pull/1819) ([Chillee](https://github.com/Chillee)) - Fixes \#1786: Adds relative line ranges [\#1810](https://github.com/VSCodeVim/Vim/pull/1810) ([Chillee](https://github.com/Chillee)) - Fixed \#1803: zc automatically reopens folds if the fold is performed in the middle. [\#1809](https://github.com/VSCodeVim/Vim/pull/1809) ([Chillee](https://github.com/Chillee)) - Vertical split shortcut keys [\#1795](https://github.com/VSCodeVim/Vim/pull/1795) ([beefsack](https://github.com/beefsack)) ## [v0.8.4](https://github.com/vscodevim/vim/tree/v0.8.4) (2017-05-29) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.8.3...v0.8.4) - Fixes \#1743: Fixed pasting over visual mode with named register overwriting the named register [\#1777](https://github.com/VSCodeVim/Vim/pull/1777) ([Chillee](https://github.com/Chillee)) - Fixes \#1760: Deindenting not working properly with neovim ex-commands [\#1770](https://github.com/VSCodeVim/Vim/pull/1770) ([Chillee](https://github.com/Chillee)) - Fixes \#1768: Backspace deletes more than one tab when tabs are mandated by language specific settings [\#1769](https://github.com/VSCodeVim/Vim/pull/1769) ([Chillee](https://github.com/Chillee)) - More v8 patches [\#1766](https://github.com/VSCodeVim/Vim/pull/1766) ([Chillee](https://github.com/Chillee)) - fixed \#1027 maybe? [\#1740](https://github.com/VSCodeVim/Vim/pull/1740) ([Chillee](https://github.com/Chillee)) ## [v0.8.3](https://github.com/vscodevim/vim/tree/v0.8.3) (2017-05-26) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.8.2...v0.8.3) ## [v0.8.2](https://github.com/vscodevim/vim/tree/v0.8.2) (2017-05-26) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.8.1...v0.8.2) - Fixes \#1750: gq doesn't work for JSDoc type comments [\#1759](https://github.com/VSCodeVim/Vim/pull/1759) ([Chillee](https://github.com/Chillee)) - Some patches for v0.8.0 [\#1757](https://github.com/VSCodeVim/Vim/pull/1757) ([Chillee](https://github.com/Chillee)) ## [v0.8.1](https://github.com/vscodevim/vim/tree/v0.8.1) (2017-05-26) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.8.0...v0.8.1) - Fixes \#1752: Tab Completion [\#1753](https://github.com/VSCodeVim/Vim/pull/1753) ([Chillee](https://github.com/Chillee)) ## [v0.8.0](https://github.com/vscodevim/vim/tree/v0.8.0) (2017-05-25) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.7.1...v0.8.0) - Fixes \#1749: \ in insert mode doesn't work when the word isn't by itself [\#1748](https://github.com/VSCodeVim/Vim/pull/1748) ([Chillee](https://github.com/Chillee)) - Added automatic changelog generator [\#1747](https://github.com/VSCodeVim/Vim/pull/1747) ([Chillee](https://github.com/Chillee)) - Actually readded \ and \ [\#1730](https://github.com/VSCodeVim/Vim/pull/1730) ([Chillee](https://github.com/Chillee)) - Revert "Unfixes \#1720" [\#1729](https://github.com/VSCodeVim/Vim/pull/1729) ([Chillee](https://github.com/Chillee)) - Unfixes \#1720 [\#1728](https://github.com/VSCodeVim/Vim/pull/1728) ([Chillee](https://github.com/Chillee)) - Embedding Neovim for Ex commands [\#1725](https://github.com/VSCodeVim/Vim/pull/1725) ([Chillee](https://github.com/Chillee)) - Fixes \#1720: Removed unused \ bindings from package.json [\#1722](https://github.com/VSCodeVim/Vim/pull/1722) ([Chillee](https://github.com/Chillee)) - Fixes \#1376: \ doesn't work correctly when a word has more than 1 number [\#1721](https://github.com/VSCodeVim/Vim/pull/1721) ([Chillee](https://github.com/Chillee)) - Fixes \#1715: Adds multicursor paste [\#1717](https://github.com/VSCodeVim/Vim/pull/1717) ([Chillee](https://github.com/Chillee)) - Fixes \#1534, \#1518, \#1716, \#1618, \#1450: Refactored repeating motions [\#1712](https://github.com/VSCodeVim/Vim/pull/1712) ([Chillee](https://github.com/Chillee)) - Fixes \#1520: search in visual/visualLine/visualBlock mode [\#1710](https://github.com/VSCodeVim/Vim/pull/1710) ([Chillee](https://github.com/Chillee)) - Fixes \#1403: VisualBlock doesn't respect keybindings. [\#1709](https://github.com/VSCodeVim/Vim/pull/1709) ([Chillee](https://github.com/Chillee)) - Fixes \#1655: Extends gf to line numbers [\#1708](https://github.com/VSCodeVim/Vim/pull/1708) ([Chillee](https://github.com/Chillee)) - Fixes \#1436: extension prevents 'find all references' pop-up from closing through \ if it's empty. [\#1707](https://github.com/VSCodeVim/Vim/pull/1707) ([Chillee](https://github.com/Chillee)) - Fixes \#1668: Self closing tags not properly handled. [\#1702](https://github.com/VSCodeVim/Vim/pull/1702) ([Chillee](https://github.com/Chillee)) - Fixes \#1674: repeating . with characters like " or \) leaves cursor in wrong place [\#1700](https://github.com/VSCodeVim/Vim/pull/1700) ([Chillee](https://github.com/Chillee)) - remove system clipboard hack for UTF-8 [\#1695](https://github.com/VSCodeVim/Vim/pull/1695) ([xconverge](https://github.com/xconverge)) - Fixes \#1684: Fixed gq spacing issues [\#1686](https://github.com/VSCodeVim/Vim/pull/1686) ([Chillee](https://github.com/Chillee)) - Fixed some regressions I introduced [\#1681](https://github.com/VSCodeVim/Vim/pull/1681) ([Chillee](https://github.com/Chillee)) - feat\(surround\): support complex tags surround [\#1680](https://github.com/VSCodeVim/Vim/pull/1680) ([admosity](https://github.com/admosity)) - Fixes \#1400, \#612, \#1632, \#1634, \#1531, \#1458: Tab isn't handled properly for insert and visualblockinsert modes [\#1663](https://github.com/VSCodeVim/Vim/pull/1663) ([Chillee](https://github.com/Chillee)) - Fixes \#792: Selecting range before Ex-commands highlights initial text [\#1659](https://github.com/VSCodeVim/Vim/pull/1659) ([Chillee](https://github.com/Chillee)) - Cobbweb/more readme fixes [\#1656](https://github.com/VSCodeVim/Vim/pull/1656) ([cobbweb](https://github.com/cobbweb)) - Fixes \#1256 and \#394: Fixes delete key and adds functionality [\#1644](https://github.com/VSCodeVim/Vim/pull/1644) ([Chillee](https://github.com/Chillee)) - Fixes \#1196, \#1197: d}/y} not working correctly [\#1621](https://github.com/VSCodeVim/Vim/pull/1621) ([Chillee](https://github.com/Chillee)) - Fixing the automatic fold expansion \(\#1004\) [\#1552](https://github.com/VSCodeVim/Vim/pull/1552) ([Chillee](https://github.com/Chillee)) - Fix visual mode bugs\#1304to\#1308 [\#1322](https://github.com/VSCodeVim/Vim/pull/1322) ([xlaech](https://github.com/xlaech)) ## [v0.7.1](https://github.com/vscodevim/vim/tree/v0.7.1) (2017-05-10) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.7.0...v0.7.1) - Changes tabs to navigate inside the same split [\#1677](https://github.com/VSCodeVim/Vim/pull/1677) ([vinicio](https://github.com/vinicio)) - clean up tests. increase timeout [\#1672](https://github.com/VSCodeVim/Vim/pull/1672) ([jpoon](https://github.com/jpoon)) - Fixes \#1585: Added \ j and \ k [\#1666](https://github.com/VSCodeVim/Vim/pull/1666) ([Chillee](https://github.com/Chillee)) - Add :close support based on :quit [\#1665](https://github.com/VSCodeVim/Vim/pull/1665) ([mspaulding06](https://github.com/mspaulding06)) - Fixes \#1280: Pasting over selection doesn't yank deleted section [\#1651](https://github.com/VSCodeVim/Vim/pull/1651) ([Chillee](https://github.com/Chillee)) - Fixes \#1535, \#1467, \#1311: D-d doesn't work in insert mode [\#1631](https://github.com/VSCodeVim/Vim/pull/1631) ([Chillee](https://github.com/Chillee)) ## [v0.7.0](https://github.com/vscodevim/vim/tree/v0.7.0) (2017-05-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.20...v0.7.0) - Join HTML on single line to prevent extraneous \s [\#1643](https://github.com/VSCodeVim/Vim/pull/1643) ([cobbweb](https://github.com/cobbweb)) - Refactor [\#1642](https://github.com/VSCodeVim/Vim/pull/1642) ([rebornix](https://github.com/rebornix)) - Fixes \#1637, \#1638: z- and z\ movements [\#1640](https://github.com/VSCodeVim/Vim/pull/1640) ([Chillee](https://github.com/Chillee)) - Fixes \#1503: Undo history isn't kept when switching tabs [\#1629](https://github.com/VSCodeVim/Vim/pull/1629) ([Chillee](https://github.com/Chillee)) - Fixes \#1441: Ctrl-c dropping a character when selecting from right to left in insert mode [\#1628](https://github.com/VSCodeVim/Vim/pull/1628) ([Chillee](https://github.com/Chillee)) - Fixes \#1300: Fixed bug with recently submitted tag PR [\#1625](https://github.com/VSCodeVim/Vim/pull/1625) ([Chillee](https://github.com/Chillee)) - Fixes \#1137: i\_\ deletes through whitespace at beginning of line [\#1624](https://github.com/VSCodeVim/Vim/pull/1624) ([Chillee](https://github.com/Chillee)) - Further work on tag matching \(based off of \#1454\) [\#1620](https://github.com/VSCodeVim/Vim/pull/1620) ([Chillee](https://github.com/Chillee)) - Toggle vim [\#1619](https://github.com/VSCodeVim/Vim/pull/1619) ([rebornix](https://github.com/rebornix)) - Fixes \#1588: \ does wrong things if cursor is to the right of a number \(and there's a number on the next line\) [\#1617](https://github.com/VSCodeVim/Vim/pull/1617) ([Chillee](https://github.com/Chillee)) - Visualstar [\#1616](https://github.com/VSCodeVim/Vim/pull/1616) ([mikew](https://github.com/mikew)) - outfiles needs to be globbed [\#1615](https://github.com/VSCodeVim/Vim/pull/1615) ([jpoon](https://github.com/jpoon)) - Upgrade typescript 2.2.1-\>2.3.2. tslint 3.10.2-\>2.3.2. Fix errors [\#1614](https://github.com/VSCodeVim/Vim/pull/1614) ([jpoon](https://github.com/jpoon)) - Fix warning [\#1613](https://github.com/VSCodeVim/Vim/pull/1613) ([jpoon](https://github.com/jpoon)) - Stopped getLineMaxColumn from erroring on line 0 [\#1610](https://github.com/VSCodeVim/Vim/pull/1610) ([Chillee](https://github.com/Chillee)) - use editor from event fixes \#1607 [\#1608](https://github.com/VSCodeVim/Vim/pull/1608) ([brandoncc](https://github.com/brandoncc)) - Fixes \#1532: gd doesn't set desiredColumn properly [\#1605](https://github.com/VSCodeVim/Vim/pull/1605) ([Chillee](https://github.com/Chillee)) - Fixes \#1594: \ drops the first and last line when selecting in visual line mode from the bottom up [\#1604](https://github.com/VSCodeVim/Vim/pull/1604) ([Chillee](https://github.com/Chillee)) - Fixes \#1575: Adds support for searching for strings with newlines [\#1603](https://github.com/VSCodeVim/Vim/pull/1603) ([Chillee](https://github.com/Chillee)) - Fix status bar color when change mode [\#1602](https://github.com/VSCodeVim/Vim/pull/1602) ([zelphir](https://github.com/zelphir)) - Made command line persistent when switching windows [\#1601](https://github.com/VSCodeVim/Vim/pull/1601) ([Chillee](https://github.com/Chillee)) - Fixes \#890, \#1377: Selection \(both visual/visualline\) is very wonky with gj and gk [\#1600](https://github.com/VSCodeVim/Vim/pull/1600) ([Chillee](https://github.com/Chillee)) - Fixes \#1251: gq always adds an extra space to beginning of block. [\#1596](https://github.com/VSCodeVim/Vim/pull/1596) ([Chillee](https://github.com/Chillee)) - Fixes \#1599: dot command doesn't work in macros [\#1595](https://github.com/VSCodeVim/Vim/pull/1595) ([Chillee](https://github.com/Chillee)) - Fixes \#1369: Change on a selection where endpoint was at beginning of line misses last character [\#1560](https://github.com/VSCodeVim/Vim/pull/1560) ([Chillee](https://github.com/Chillee)) - Add support for indent objects [\#1550](https://github.com/VSCodeVim/Vim/pull/1550) ([mikew](https://github.com/mikew)) - Navigate between view [\#1504](https://github.com/VSCodeVim/Vim/pull/1504) ([lyup](https://github.com/lyup)) ## [v0.6.20](https://github.com/vscodevim/vim/tree/v0.6.20) (2017-04-26) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.19...v0.6.20) ## [v0.6.19](https://github.com/vscodevim/vim/tree/v0.6.19) (2017-04-26) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.18...v0.6.19) - Fixes \#1573: Backspace at beginning of file causes subsequent operation to nop [\#1577](https://github.com/VSCodeVim/Vim/pull/1577) ([Chillee](https://github.com/Chillee)) - Fix logo src so logo displays inside VSCode [\#1572](https://github.com/VSCodeVim/Vim/pull/1572) ([cobbweb](https://github.com/cobbweb)) - fixes \#1449 [\#1571](https://github.com/VSCodeVim/Vim/pull/1571) ([squedd](https://github.com/squedd)) - fixes \#1252 [\#1569](https://github.com/VSCodeVim/Vim/pull/1569) ([xconverge](https://github.com/xconverge)) - fixes \#1486 :wqa command [\#1568](https://github.com/VSCodeVim/Vim/pull/1568) ([xconverge](https://github.com/xconverge)) - fixes \#1357 [\#1567](https://github.com/VSCodeVim/Vim/pull/1567) ([xconverge](https://github.com/xconverge)) - Fix surround aliases [\#1564](https://github.com/VSCodeVim/Vim/pull/1564) ([xconverge](https://github.com/xconverge)) ## [v0.6.18](https://github.com/vscodevim/vim/tree/v0.6.18) (2017-04-24) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.17...v0.6.18) - update clipboardy library with windows utf-8 fix [\#1559](https://github.com/VSCodeVim/Vim/pull/1559) ([xconverge](https://github.com/xconverge)) - Fixes \#1539: Displaying values in register stops displaying anything after the newline [\#1558](https://github.com/VSCodeVim/Vim/pull/1558) ([Chillee](https://github.com/Chillee)) - Fixes \#1539: Viewing register value displays incorrectly for macros [\#1557](https://github.com/VSCodeVim/Vim/pull/1557) ([Chillee](https://github.com/Chillee)) - Fixes \#1554, \#1553: Fixed daW bugs [\#1555](https://github.com/VSCodeVim/Vim/pull/1555) ([Chillee](https://github.com/Chillee)) - Fixes \#1193, \#1350, \#967: Fixes daw bugs [\#1549](https://github.com/VSCodeVim/Vim/pull/1549) ([Chillee](https://github.com/Chillee)) - Allow users to use VSCode keybinding for remapping [\#1548](https://github.com/VSCodeVim/Vim/pull/1548) ([rebornix](https://github.com/rebornix)) - README enhancements [\#1547](https://github.com/VSCodeVim/Vim/pull/1547) ([cobbweb](https://github.com/cobbweb)) - Fixes \#1533: \ not activating when \ is pressed [\#1542](https://github.com/VSCodeVim/Vim/pull/1542) ([Chillee](https://github.com/Chillee)) - Fixes \#1528: daw on end of word doesn't delete properly [\#1536](https://github.com/VSCodeVim/Vim/pull/1536) ([Chillee](https://github.com/Chillee)) - Fixes \#1513: Backspace on middle of whitespace only line fails [\#1514](https://github.com/VSCodeVim/Vim/pull/1514) ([Chillee](https://github.com/Chillee)) ## [v0.6.17](https://github.com/vscodevim/vim/tree/v0.6.17) (2017-04-20) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.16...v0.6.17) - Allow user to change status bar color based on mode [\#1529](https://github.com/VSCodeVim/Vim/pull/1529) ([xconverge](https://github.com/xconverge)) - Fix README description for `af` [\#1522](https://github.com/VSCodeVim/Vim/pull/1522) ([esturcke](https://github.com/esturcke)) - fixes \#1519 [\#1521](https://github.com/VSCodeVim/Vim/pull/1521) ([xconverge](https://github.com/xconverge)) - make surround repeatable with dot [\#1515](https://github.com/VSCodeVim/Vim/pull/1515) ([xconverge](https://github.com/xconverge)) - \[WIP\] change system clipboard library to a newer more maintained library [\#1487](https://github.com/VSCodeVim/Vim/pull/1487) ([xconverge](https://github.com/xconverge)) ## [v0.6.16](https://github.com/vscodevim/vim/tree/v0.6.16) (2017-04-16) [Full Changelog](https://github.com/vscodevim/vim/compare/0.6.15...v0.6.16) - added cmd_line commands to remapper [\#1516](https://github.com/VSCodeVim/Vim/pull/1516) ([xconverge](https://github.com/xconverge)) - fixes \#1507 and removes workspace settings that should not be there [\#1509](https://github.com/VSCodeVim/Vim/pull/1509) ([xconverge](https://github.com/xconverge)) - Add line comment operator [\#1506](https://github.com/VSCodeVim/Vim/pull/1506) ([fiedler](https://github.com/fiedler)) - Add 5i= or 4a- so that the previously inserted text is repeated upon exiting to normal mode [\#1495](https://github.com/VSCodeVim/Vim/pull/1495) ([xconverge](https://github.com/xconverge)) - Add ability to turn surround plugin off [\#1494](https://github.com/VSCodeVim/Vim/pull/1494) ([xconverge](https://github.com/xconverge)) - Added new style settings \(color, size, etc.\) for easymotion markers [\#1493](https://github.com/VSCodeVim/Vim/pull/1493) ([edasaki](https://github.com/edasaki)) - fixes \#1475 [\#1485](https://github.com/VSCodeVim/Vim/pull/1485) ([xconverge](https://github.com/xconverge)) - fix for double clicking a word with mouse not showing selection properly [\#1484](https://github.com/VSCodeVim/Vim/pull/1484) ([xconverge](https://github.com/xconverge)) - fix easymotion j and k [\#1474](https://github.com/VSCodeVim/Vim/pull/1474) ([xconverge](https://github.com/xconverge)) ## [0.6.15](https://github.com/vscodevim/vim/tree/0.6.15) (2017-04-07) [Full Changelog](https://github.com/vscodevim/vim/compare/0.6.14...0.6.15) ## [0.6.14](https://github.com/vscodevim/vim/tree/0.6.14) (2017-04-07) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.13...0.6.14) - Fix tables in roadmap [\#1469](https://github.com/VSCodeVim/Vim/pull/1469) ([xconverge](https://github.com/xconverge)) - Fix visual block mode not updating multicursor selection [\#1468](https://github.com/VSCodeVim/Vim/pull/1468) ([xconverge](https://github.com/xconverge)) - Fix type suggestion for handleKeys object [\#1465](https://github.com/VSCodeVim/Vim/pull/1465) ([abhiranjankumar00](https://github.com/abhiranjankumar00)) ## [v0.6.13](https://github.com/vscodevim/vim/tree/v0.6.13) (2017-04-04) [Full Changelog](https://github.com/vscodevim/vim/compare/0.6.12...v0.6.13) - fixes \#1448 [\#1462](https://github.com/VSCodeVim/Vim/pull/1462) ([xconverge](https://github.com/xconverge)) - fix multi line in 'at' and 'it' commands [\#1454](https://github.com/VSCodeVim/Vim/pull/1454) ([jrenton](https://github.com/jrenton)) ## [0.6.12](https://github.com/vscodevim/vim/tree/0.6.12) (2017-04-04) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.11...0.6.12) - fixes \#1432 [\#1434](https://github.com/VSCodeVim/Vim/pull/1434) ([xconverge](https://github.com/xconverge)) - fixes \#1312 [\#1433](https://github.com/VSCodeVim/Vim/pull/1433) ([xconverge](https://github.com/xconverge)) - Change easymotion decoration colors to use searchHighlight colors [\#1431](https://github.com/VSCodeVim/Vim/pull/1431) ([xconverge](https://github.com/xconverge)) - minor cleanup to improve leader usage with \ [\#1429](https://github.com/VSCodeVim/Vim/pull/1429) ([xconverge](https://github.com/xconverge)) - gUU and guu [\#1428](https://github.com/VSCodeVim/Vim/pull/1428) ([xconverge](https://github.com/xconverge)) - Allowing user to selectively disable some key combos [\#1425](https://github.com/VSCodeVim/Vim/pull/1425) ([xconverge](https://github.com/xconverge)) - Remapper cleanup key history [\#1416](https://github.com/VSCodeVim/Vim/pull/1416) ([xconverge](https://github.com/xconverge)) - fix undo points when moving around in insert with mouse or arrow keys [\#1413](https://github.com/VSCodeVim/Vim/pull/1413) ([xconverge](https://github.com/xconverge)) - update readme for plugins [\#1411](https://github.com/VSCodeVim/Vim/pull/1411) ([xconverge](https://github.com/xconverge)) - Allow users to use their own cursor style for insert from editor.cursorStyle [\#1399](https://github.com/VSCodeVim/Vim/pull/1399) ([xconverge](https://github.com/xconverge)) ## [v0.6.11](https://github.com/vscodevim/vim/tree/v0.6.11) (2017-03-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.10...v0.6.11) - Fix comment syntax for shell commands. [\#1408](https://github.com/VSCodeVim/Vim/pull/1408) ([frewsxcv](https://github.com/frewsxcv)) - Increase timeout for some test cases in mocha [\#1379](https://github.com/VSCodeVim/Vim/pull/1379) ([xconverge](https://github.com/xconverge)) ## [v0.6.10](https://github.com/vscodevim/vim/tree/v0.6.10) (2017-03-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.9...v0.6.10) ## [v0.6.9](https://github.com/vscodevim/vim/tree/v0.6.9) (2017-03-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.8...v0.6.9) ## [v0.6.8](https://github.com/vscodevim/vim/tree/v0.6.8) (2017-03-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.7...v0.6.8) ## [v0.6.7](https://github.com/vscodevim/vim/tree/v0.6.7) (2017-03-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.6...v0.6.7) - fix bracket motion behavior for use with % and a count, or \[\( and a c… [\#1406](https://github.com/VSCodeVim/Vim/pull/1406) ([xconverge](https://github.com/xconverge)) - fix for cursor not changing correctly, workaround for vscode issue [\#1402](https://github.com/VSCodeVim/Vim/pull/1402) ([xconverge](https://github.com/xconverge)) ## [v0.6.6](https://github.com/vscodevim/vim/tree/v0.6.6) (2017-03-17) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.5...v0.6.6) - Use block cursor in visual & underline in replace [\#1394](https://github.com/VSCodeVim/Vim/pull/1394) ([net](https://github.com/net)) - Perform remapped commands when prefix by a number [\#1359](https://github.com/VSCodeVim/Vim/pull/1359) ([bdauria](https://github.com/bdauria)) ## [v0.6.5](https://github.com/vscodevim/vim/tree/v0.6.5) (2017-03-12) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.4...v0.6.5) ## [v0.6.4](https://github.com/vscodevim/vim/tree/v0.6.4) (2017-03-12) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.3...v0.6.4) - Update README.md [\#1390](https://github.com/VSCodeVim/Vim/pull/1390) ([xconverge](https://github.com/xconverge)) - fixes \#1385 % motion with a count [\#1387](https://github.com/VSCodeVim/Vim/pull/1387) ([xconverge](https://github.com/xconverge)) - fixes \#1382 [\#1386](https://github.com/VSCodeVim/Vim/pull/1386) ([xconverge](https://github.com/xconverge)) ## [v0.6.3](https://github.com/vscodevim/vim/tree/v0.6.3) (2017-03-11) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.6.0...v0.6.3) - fixes \#1373 [\#1374](https://github.com/VSCodeVim/Vim/pull/1374) ([xconverge](https://github.com/xconverge)) - Remove log file. [\#1368](https://github.com/VSCodeVim/Vim/pull/1368) ([frewsxcv](https://github.com/frewsxcv)) - Remove our modified older typings [\#1367](https://github.com/VSCodeVim/Vim/pull/1367) ([xconverge](https://github.com/xconverge)) - \[WIP\] fix travis due to double digit version numbers [\#1366](https://github.com/VSCodeVim/Vim/pull/1366) ([xconverge](https://github.com/xconverge)) - Fixed numbered registered macros from overwriting themselves [\#1362](https://github.com/VSCodeVim/Vim/pull/1362) ([xconverge](https://github.com/xconverge)) - Update config options without restarting [\#1361](https://github.com/VSCodeVim/Vim/pull/1361) ([xconverge](https://github.com/xconverge)) - Index fixes [\#1190](https://github.com/VSCodeVim/Vim/pull/1190) ([xconverge](https://github.com/xconverge)) ## [v0.6.0](https://github.com/vscodevim/vim/tree/v0.6.0) (2017-03-03) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.5.3...v0.6.0) - Fix clipboard copy [\#1349](https://github.com/VSCodeVim/Vim/pull/1349) ([johnfn](https://github.com/johnfn)) - regex match [\#1346](https://github.com/VSCodeVim/Vim/pull/1346) ([rebornix](https://github.com/rebornix)) - Add limited support for :sort [\#1342](https://github.com/VSCodeVim/Vim/pull/1342) ([jordan-heemskerk](https://github.com/jordan-heemskerk)) - Override VSCode copy command. \#1337, \#616. [\#1339](https://github.com/VSCodeVim/Vim/pull/1339) ([johnfn](https://github.com/johnfn)) - Fix \#1318 [\#1338](https://github.com/VSCodeVim/Vim/pull/1338) ([rebornix](https://github.com/rebornix)) - Fix \#1329 failing build by removing undefined in configuration.ts [\#1332](https://github.com/VSCodeVim/Vim/pull/1332) ([misoguy](https://github.com/misoguy)) - fixes \#1327 [\#1331](https://github.com/VSCodeVim/Vim/pull/1331) ([xconverge](https://github.com/xconverge)) - fixes \#1320 [\#1325](https://github.com/VSCodeVim/Vim/pull/1325) ([xconverge](https://github.com/xconverge)) - fixes \#1313 [\#1324](https://github.com/VSCodeVim/Vim/pull/1324) ([xconverge](https://github.com/xconverge)) - Add ctrl-w q action to quit current window. [\#1317](https://github.com/VSCodeVim/Vim/pull/1317) ([tail](https://github.com/tail)) - Fix lint issue. [\#1316](https://github.com/VSCodeVim/Vim/pull/1316) ([tail](https://github.com/tail)) - Fix c on line beginning\#1302 [\#1303](https://github.com/VSCodeVim/Vim/pull/1303) ([xlaech](https://github.com/xlaech)) - fixes travis with minor hack used in tests [\#1301](https://github.com/VSCodeVim/Vim/pull/1301) ([xconverge](https://github.com/xconverge)) - D in visual mode behaves like d [\#1297](https://github.com/VSCodeVim/Vim/pull/1297) ([xlaech](https://github.com/xlaech)) - Fix for \#1293 [\#1296](https://github.com/VSCodeVim/Vim/pull/1296) ([xlaech](https://github.com/xlaech)) - Update readme for some clarity on using settings [\#1295](https://github.com/VSCodeVim/Vim/pull/1295) ([xconverge](https://github.com/xconverge)) - fixes \#1290, visual block still has the same issue though [\#1291](https://github.com/VSCodeVim/Vim/pull/1291) ([xconverge](https://github.com/xconverge)) - More surround fixes [\#1289](https://github.com/VSCodeVim/Vim/pull/1289) ([xconverge](https://github.com/xconverge)) ## [v0.5.3](https://github.com/vscodevim/vim/tree/v0.5.3) (2017-02-12) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.5.0...v0.5.3) - fixes \#1258 [\#1286](https://github.com/VSCodeVim/Vim/pull/1286) ([xconverge](https://github.com/xconverge)) - avoid using user remapping in test mode [\#1278](https://github.com/VSCodeVim/Vim/pull/1278) ([rufusroflpunch](https://github.com/rufusroflpunch)) - Support exact and inexact current word search [\#1277](https://github.com/VSCodeVim/Vim/pull/1277) ([rhys-vdw](https://github.com/rhys-vdw)) - fixes \#1271 [\#1274](https://github.com/VSCodeVim/Vim/pull/1274) ([xconverge](https://github.com/xconverge)) - fixes \#1199 easymotion in visual mode [\#1273](https://github.com/VSCodeVim/Vim/pull/1273) ([xconverge](https://github.com/xconverge)) - fixes \#1145 [\#1272](https://github.com/VSCodeVim/Vim/pull/1272) ([xconverge](https://github.com/xconverge)) - Delete matching bracket upon backspace [\#1267](https://github.com/VSCodeVim/Vim/pull/1267) ([rufusroflpunch](https://github.com/rufusroflpunch)) - Clearing commandList for remapped commands [\#1263](https://github.com/VSCodeVim/Vim/pull/1263) ([rufusroflpunch](https://github.com/rufusroflpunch)) - Added tag text to status bar in surround mode [\#1254](https://github.com/VSCodeVim/Vim/pull/1254) ([xconverge](https://github.com/xconverge)) - Fix autoindent when opening a line above [\#1249](https://github.com/VSCodeVim/Vim/pull/1249) ([inejge](https://github.com/inejge)) - Fixes README spelling mistake [\#1246](https://github.com/VSCodeVim/Vim/pull/1246) ([eastwood](https://github.com/eastwood)) ## [v0.5.0](https://github.com/vscodevim/vim/tree/v0.5.0) (2017-01-23) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.5.1...v0.5.0) ## [v0.5.1](https://github.com/vscodevim/vim/tree/v0.5.1) (2017-01-23) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.10...v0.5.1) - Surround [\#1238](https://github.com/VSCodeVim/Vim/pull/1238) ([johnfn](https://github.com/johnfn)) - Support "gf" in es6 import statements by adding the file extension [\#1227](https://github.com/VSCodeVim/Vim/pull/1227) ([aminroosta](https://github.com/aminroosta)) - fixes \#1214 [\#1217](https://github.com/VSCodeVim/Vim/pull/1217) ([Platzer](https://github.com/Platzer)) ## [v0.4.10](https://github.com/vscodevim/vim/tree/v0.4.10) (2016-12-22) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.9...v0.4.10) - fixes \#1132 [\#1187](https://github.com/VSCodeVim/Vim/pull/1187) ([xconverge](https://github.com/xconverge)) - fixes \#1173 [\#1186](https://github.com/VSCodeVim/Vim/pull/1186) ([xconverge](https://github.com/xconverge)) - Fixed register tests breaking due to \#1183 [\#1185](https://github.com/VSCodeVim/Vim/pull/1185) ([vikramthyagarajan](https://github.com/vikramthyagarajan)) - fixes \#1180 [\#1183](https://github.com/VSCodeVim/Vim/pull/1183) ([xconverge](https://github.com/xconverge)) - Adds documentation for adding leader bindings [\#1182](https://github.com/VSCodeVim/Vim/pull/1182) ([eastwood](https://github.com/eastwood)) - Implements Global state [\#1179](https://github.com/VSCodeVim/Vim/pull/1179) ([vikramthyagarajan](https://github.com/vikramthyagarajan)) - fixes \#1176 [\#1177](https://github.com/VSCodeVim/Vim/pull/1177) ([xconverge](https://github.com/xconverge)) - Select inner vi\( fix [\#1175](https://github.com/VSCodeVim/Vim/pull/1175) ([xconverge](https://github.com/xconverge)) - fixes \#1170 [\#1174](https://github.com/VSCodeVim/Vim/pull/1174) ([xconverge](https://github.com/xconverge)) - Fixes travis [\#1169](https://github.com/VSCodeVim/Vim/pull/1169) ([xconverge](https://github.com/xconverge)) - control key bindings respect the useCtrlKey setting [\#1151](https://github.com/VSCodeVim/Vim/pull/1151) ([xwvvvvwx](https://github.com/xwvvvvwx)) - fixes \#657 implements search history [\#1147](https://github.com/VSCodeVim/Vim/pull/1147) ([xconverge](https://github.com/xconverge)) - More click past eol o no [\#1146](https://github.com/VSCodeVim/Vim/pull/1146) ([xconverge](https://github.com/xconverge)) - Reselect visual implemented \(gv\) [\#1141](https://github.com/VSCodeVim/Vim/pull/1141) ([xconverge](https://github.com/xconverge)) - fixes \#1136 [\#1139](https://github.com/VSCodeVim/Vim/pull/1139) ([xconverge](https://github.com/xconverge)) - minor fixes for \# and \* after using :nohl [\#1134](https://github.com/VSCodeVim/Vim/pull/1134) ([xconverge](https://github.com/xconverge)) - Updated useCtrlKeys default value [\#1126](https://github.com/VSCodeVim/Vim/pull/1126) ([Mxbonn](https://github.com/Mxbonn)) - fixes \#1063 [\#1124](https://github.com/VSCodeVim/Vim/pull/1124) ([xconverge](https://github.com/xconverge)) - Fixed "d" and "D" in multicursor mode [\#1029](https://github.com/VSCodeVim/Vim/pull/1029) ([Platzer](https://github.com/Platzer)) ## [v0.4.9](https://github.com/vscodevim/vim/tree/v0.4.9) (2016-12-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.8...v0.4.9) ## [v0.4.8](https://github.com/vscodevim/vim/tree/v0.4.8) (2016-12-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.7...v0.4.8) - Update readme for easymotion [\#1114](https://github.com/VSCodeVim/Vim/pull/1114) ([xconverge](https://github.com/xconverge)) ## [v0.4.7](https://github.com/vscodevim/vim/tree/v0.4.7) (2016-12-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.6...v0.4.7) - Fix minor typo [\#1113](https://github.com/VSCodeVim/Vim/pull/1113) ([xconverge](https://github.com/xconverge)) - \[WIP\] initial leader fixes [\#1112](https://github.com/VSCodeVim/Vim/pull/1112) ([xconverge](https://github.com/xconverge)) - Added more aliases for nohl [\#1111](https://github.com/VSCodeVim/Vim/pull/1111) ([xconverge](https://github.com/xconverge)) - Turns highlighting back on after nohl if you try to go to a new searc… [\#1110](https://github.com/VSCodeVim/Vim/pull/1110) ([xconverge](https://github.com/xconverge)) ## [v0.4.6](https://github.com/vscodevim/vim/tree/v0.4.6) (2016-12-04) [Full Changelog](https://github.com/vscodevim/vim/compare/0.4.5...v0.4.6) ## [0.4.5](https://github.com/vscodevim/vim/tree/0.4.5) (2016-12-04) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.5...0.4.5) - \[WIP\] gq [\#1106](https://github.com/VSCodeVim/Vim/pull/1106) ([johnfn](https://github.com/johnfn)) ## [v0.4.5](https://github.com/vscodevim/vim/tree/v0.4.5) (2016-12-02) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.4...v0.4.5) - Override home key \(for pressing home in visual for example\) [\#1100](https://github.com/VSCodeVim/Vim/pull/1100) ([xconverge](https://github.com/xconverge)) - avoid syncing style back to config [\#1099](https://github.com/VSCodeVim/Vim/pull/1099) ([rebornix](https://github.com/rebornix)) - Implement open file command - Issue \#801 [\#1098](https://github.com/VSCodeVim/Vim/pull/1098) ([jamirvin](https://github.com/jamirvin)) ## [v0.4.4](https://github.com/vscodevim/vim/tree/v0.4.4) (2016-11-29) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.3...v0.4.4) - Removed debug print [\#1083](https://github.com/VSCodeVim/Vim/pull/1083) ([xconverge](https://github.com/xconverge)) - Update roadmap for ctrl-o [\#1082](https://github.com/VSCodeVim/Vim/pull/1082) ([xconverge](https://github.com/xconverge)) - fixes \#1076 [\#1077](https://github.com/VSCodeVim/Vim/pull/1077) ([xconverge](https://github.com/xconverge)) - fixes \#1073 [\#1074](https://github.com/VSCodeVim/Vim/pull/1074) ([xconverge](https://github.com/xconverge)) - fixes \#1065 [\#1071](https://github.com/VSCodeVim/Vim/pull/1071) ([xconverge](https://github.com/xconverge)) - fixes \#1023 [\#1069](https://github.com/VSCodeVim/Vim/pull/1069) ([xconverge](https://github.com/xconverge)) ## [v0.4.3](https://github.com/vscodevim/vim/tree/v0.4.3) (2016-11-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.2...v0.4.3) - fixes \#1034 [\#1068](https://github.com/VSCodeVim/Vim/pull/1068) ([xconverge](https://github.com/xconverge)) - fixes \#1035 [\#1067](https://github.com/VSCodeVim/Vim/pull/1067) ([xconverge](https://github.com/xconverge)) - fixes \#1064 [\#1066](https://github.com/VSCodeVim/Vim/pull/1066) ([xconverge](https://github.com/xconverge)) - How can I fix travis failure [\#1062](https://github.com/VSCodeVim/Vim/pull/1062) ([rebornix](https://github.com/rebornix)) ## [v0.4.2](https://github.com/vscodevim/vim/tree/v0.4.2) (2016-11-17) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.1...v0.4.2) - Visual block fixes to cursor position and tests [\#1044](https://github.com/VSCodeVim/Vim/pull/1044) ([xconverge](https://github.com/xconverge)) - Hide the info line in issue template [\#1037](https://github.com/VSCodeVim/Vim/pull/1037) ([octref](https://github.com/octref)) - Implemented EasyMotion plugin functionality [\#993](https://github.com/VSCodeVim/Vim/pull/993) ([Metamist](https://github.com/Metamist)) ## [v0.4.1](https://github.com/vscodevim/vim/tree/v0.4.1) (2016-10-31) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.4.0...v0.4.1) - fixes \#1013 [\#1014](https://github.com/VSCodeVim/Vim/pull/1014) ([xconverge](https://github.com/xconverge)) - Update Readme [\#1012](https://github.com/VSCodeVim/Vim/pull/1012) ([jpoon](https://github.com/jpoon)) - fixes \#983 [\#1008](https://github.com/VSCodeVim/Vim/pull/1008) ([xconverge](https://github.com/xconverge)) - Make create-multicursor commands repeatable [\#1007](https://github.com/VSCodeVim/Vim/pull/1007) ([Platzer](https://github.com/Platzer)) - fix mouse clicking past EOL [\#1006](https://github.com/VSCodeVim/Vim/pull/1006) ([xconverge](https://github.com/xconverge)) - fixes \#1000 and a minor replace issue [\#1005](https://github.com/VSCodeVim/Vim/pull/1005) ([xconverge](https://github.com/xconverge)) - Update "r" for visual modes on roadmap [\#1002](https://github.com/VSCodeVim/Vim/pull/1002) ([xconverge](https://github.com/xconverge)) - fixes \#998 [\#1001](https://github.com/VSCodeVim/Vim/pull/1001) ([xconverge](https://github.com/xconverge)) - Remove fix-whitespace gulp command. [\#999](https://github.com/VSCodeVim/Vim/pull/999) ([jpoon](https://github.com/jpoon)) - Improved performance of visual block replace by a lot [\#997](https://github.com/VSCodeVim/Vim/pull/997) ([xconverge](https://github.com/xconverge)) - fixes \#663 [\#996](https://github.com/VSCodeVim/Vim/pull/996) ([xconverge](https://github.com/xconverge)) - No need for "i" flag on a numerical only regex [\#995](https://github.com/VSCodeVim/Vim/pull/995) ([stefanoio](https://github.com/stefanoio)) - SearchState - Fixed isRegex not set on search [\#994](https://github.com/VSCodeVim/Vim/pull/994) ([Metamist](https://github.com/Metamist)) - fix \#985 [\#992](https://github.com/VSCodeVim/Vim/pull/992) ([rebornix](https://github.com/rebornix)) - Run all tests [\#990](https://github.com/VSCodeVim/Vim/pull/990) ([xconverge](https://github.com/xconverge)) - Fix for visual line behaving funky when going from bottom up [\#989](https://github.com/VSCodeVim/Vim/pull/989) ([xconverge](https://github.com/xconverge)) - Add Keymaps category [\#987](https://github.com/VSCodeVim/Vim/pull/987) ([waderyan](https://github.com/waderyan)) - fix \#982 [\#984](https://github.com/VSCodeVim/Vim/pull/984) ([rebornix](https://github.com/rebornix)) - fix \#977 [\#981](https://github.com/VSCodeVim/Vim/pull/981) ([rebornix](https://github.com/rebornix)) - fix \#689 [\#980](https://github.com/VSCodeVim/Vim/pull/980) ([rebornix](https://github.com/rebornix)) - Numbered, upper case and multicursor register [\#974](https://github.com/VSCodeVim/Vim/pull/974) ([Platzer](https://github.com/Platzer)) - remove leading spaces when \... is pressed \#685 [\#962](https://github.com/VSCodeVim/Vim/pull/962) ([Zzzen](https://github.com/Zzzen)) - Fix replace in visual, visual line, and visual block mode [\#953](https://github.com/VSCodeVim/Vim/pull/953) ([xconverge](https://github.com/xconverge)) - Add some tests and fix some exceptions during the tests [\#914](https://github.com/VSCodeVim/Vim/pull/914) ([xconverge](https://github.com/xconverge)) ## [v0.4.0](https://github.com/vscodevim/vim/tree/v0.4.0) (2016-10-24) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.3.8...v0.4.0) - fix \#528 [\#966](https://github.com/VSCodeVim/Vim/pull/966) ([rebornix](https://github.com/rebornix)) - fix \#693 [\#964](https://github.com/VSCodeVim/Vim/pull/964) ([rebornix](https://github.com/rebornix)) - fix \#922 [\#960](https://github.com/VSCodeVim/Vim/pull/960) ([rebornix](https://github.com/rebornix)) - fix \#939 [\#958](https://github.com/VSCodeVim/Vim/pull/958) ([rebornix](https://github.com/rebornix)) - Add a command is `D` in visual block mode. [\#957](https://github.com/VSCodeVim/Vim/pull/957) ([Kooooya](https://github.com/Kooooya)) - Add commands is `s` and `S` in visual block mode. [\#954](https://github.com/VSCodeVim/Vim/pull/954) ([Kooooya](https://github.com/Kooooya)) - fix \#808 [\#952](https://github.com/VSCodeVim/Vim/pull/952) ([rebornix](https://github.com/rebornix)) - fix \#484 [\#951](https://github.com/VSCodeVim/Vim/pull/951) ([rebornix](https://github.com/rebornix)) - fix \#921 [\#950](https://github.com/VSCodeVim/Vim/pull/950) ([rebornix](https://github.com/rebornix)) - make tab sequence feel right [\#949](https://github.com/VSCodeVim/Vim/pull/949) ([rebornix](https://github.com/rebornix)) - stop revealing cursor when not necessary [\#948](https://github.com/VSCodeVim/Vim/pull/948) ([rebornix](https://github.com/rebornix)) - add gh hover command [\#945](https://github.com/VSCodeVim/Vim/pull/945) ([will-wow](https://github.com/will-wow)) - New increment without separators [\#944](https://github.com/VSCodeVim/Vim/pull/944) ([xconverge](https://github.com/xconverge)) - fix \#937 [\#943](https://github.com/VSCodeVim/Vim/pull/943) ([rebornix](https://github.com/rebornix)) - fixes \#878 [\#942](https://github.com/VSCodeVim/Vim/pull/942) ([xconverge](https://github.com/xconverge)) - Support num registers macros [\#941](https://github.com/VSCodeVim/Vim/pull/941) ([xconverge](https://github.com/xconverge)) - Test enhancement [\#938](https://github.com/VSCodeVim/Vim/pull/938) ([rebornix](https://github.com/rebornix)) - fix \#845 [\#911](https://github.com/VSCodeVim/Vim/pull/911) ([rebornix](https://github.com/rebornix)) ## [v0.3.8](https://github.com/vscodevim/vim/tree/v0.3.8) (2016-10-18) [Full Changelog](https://github.com/vscodevim/vim/compare/0.3.7...v0.3.8) - fixes \#879 [\#933](https://github.com/VSCodeVim/Vim/pull/933) ([xconverge](https://github.com/xconverge)) - fixes \#905 [\#932](https://github.com/VSCodeVim/Vim/pull/932) ([xconverge](https://github.com/xconverge)) - fixes \#652 [\#931](https://github.com/VSCodeVim/Vim/pull/931) ([xconverge](https://github.com/xconverge)) - Update internal cursor position when necessary [\#927](https://github.com/VSCodeVim/Vim/pull/927) ([rebornix](https://github.com/rebornix)) - Draw multicursor correctly in Visual Mode [\#920](https://github.com/VSCodeVim/Vim/pull/920) ([Platzer](https://github.com/Platzer)) - update internal cursor position per Code selection change [\#919](https://github.com/VSCodeVim/Vim/pull/919) ([rebornix](https://github.com/rebornix)) - display register value in reg-cmd, fix \#830 [\#915](https://github.com/VSCodeVim/Vim/pull/915) ([Platzer](https://github.com/Platzer)) - \[Post 1.0\] Two way syncing of Vim and Code's configuration [\#913](https://github.com/VSCodeVim/Vim/pull/913) ([rebornix](https://github.com/rebornix)) - Macro [\#894](https://github.com/VSCodeVim/Vim/pull/894) ([rebornix](https://github.com/rebornix)) ## [0.3.7](https://github.com/vscodevim/vim/tree/0.3.7) (2016-10-12) [Full Changelog](https://github.com/vscodevim/vim/compare/0.3.6...0.3.7) - fixes \#888 [\#902](https://github.com/VSCodeVim/Vim/pull/902) ([xconverge](https://github.com/xconverge)) - fixes \#882 [\#900](https://github.com/VSCodeVim/Vim/pull/900) ([xconverge](https://github.com/xconverge)) ## [0.3.6](https://github.com/vscodevim/vim/tree/0.3.6) (2016-10-12) [Full Changelog](https://github.com/vscodevim/vim/compare/0.3.5...0.3.6) - allow remapping of ctrl-j and ctrl-k in settings.json [\#891](https://github.com/VSCodeVim/Vim/pull/891) ([xwvvvvwx](https://github.com/xwvvvvwx)) - Fix visual block x [\#861](https://github.com/VSCodeVim/Vim/pull/861) ([xconverge](https://github.com/xconverge)) ## [0.3.5](https://github.com/vscodevim/vim/tree/0.3.5) (2016-10-10) [Full Changelog](https://github.com/vscodevim/vim/compare/0.3.4...0.3.5) ## [0.3.4](https://github.com/vscodevim/vim/tree/0.3.4) (2016-10-10) [Full Changelog](https://github.com/vscodevim/vim/compare/0.3.3...0.3.4) - Remove unused modehandlers when tabs are closed [\#865](https://github.com/VSCodeVim/Vim/pull/865) ([xconverge](https://github.com/xconverge)) - Insert Previous text [\#768](https://github.com/VSCodeVim/Vim/pull/768) ([rebornix](https://github.com/rebornix)) ## [0.3.3](https://github.com/vscodevim/vim/tree/0.3.3) (2016-10-08) [Full Changelog](https://github.com/vscodevim/vim/compare/0.3.2...0.3.3) ## [0.3.2](https://github.com/vscodevim/vim/tree/0.3.2) (2016-10-08) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.3.1...0.3.2) ## [v0.3.1](https://github.com/vscodevim/vim/tree/v0.3.1) (2016-10-08) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.3.0...v0.3.1) - Unnecessary quit check on untitled files [\#855](https://github.com/VSCodeVim/Vim/pull/855) ([xconverge](https://github.com/xconverge)) - Add new logo icon [\#852](https://github.com/VSCodeVim/Vim/pull/852) ([kevincoleman](https://github.com/kevincoleman)) - Fixes arrow navigation to EOL while in insert [\#838](https://github.com/VSCodeVim/Vim/pull/838) ([xconverge](https://github.com/xconverge)) - fixes \#832 [\#837](https://github.com/VSCodeVim/Vim/pull/837) ([xconverge](https://github.com/xconverge)) - \[WIP\] Use new transformation style in delete and paste [\#835](https://github.com/VSCodeVim/Vim/pull/835) ([johnfn](https://github.com/johnfn)) - X eats eol [\#827](https://github.com/VSCodeVim/Vim/pull/827) ([xconverge](https://github.com/xconverge)) - Fix to allow A while in visual mode [\#816](https://github.com/VSCodeVim/Vim/pull/816) ([xconverge](https://github.com/xconverge)) - Fix issue where could not use I while in visual mode [\#815](https://github.com/VSCodeVim/Vim/pull/815) ([xconverge](https://github.com/xconverge)) - fixes \#784 [\#814](https://github.com/VSCodeVim/Vim/pull/814) ([xconverge](https://github.com/xconverge)) ## [v0.3.0](https://github.com/vscodevim/vim/tree/v0.3.0) (2016-10-03) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.2.0...v0.3.0) - Show debug console when session launches [\#821](https://github.com/VSCodeVim/Vim/pull/821) ([xconverge](https://github.com/xconverge)) - zz in visual, visualline, and visual block mode [\#820](https://github.com/VSCodeVim/Vim/pull/820) ([xconverge](https://github.com/xconverge)) - Fixes \#817 [\#819](https://github.com/VSCodeVim/Vim/pull/819) ([xconverge](https://github.com/xconverge)) - Clean up typings [\#818](https://github.com/VSCodeVim/Vim/pull/818) ([jpoon](https://github.com/jpoon)) - Updated documentation for linux system clipboard use [\#813](https://github.com/VSCodeVim/Vim/pull/813) ([xconverge](https://github.com/xconverge)) - Multi-Cursor Mode v 2.0 [\#811](https://github.com/VSCodeVim/Vim/pull/811) ([johnfn](https://github.com/johnfn)) - Fix docs [\#807](https://github.com/VSCodeVim/Vim/pull/807) ([jpoon](https://github.com/jpoon)) - Fix bug joining lines with whitespace only next line [\#799](https://github.com/VSCodeVim/Vim/pull/799) ([mleech](https://github.com/mleech)) - Add autoindent to README, fix hlsearch default [\#796](https://github.com/VSCodeVim/Vim/pull/796) ([srenatus](https://github.com/srenatus)) - Support "+ system clipboard register \(\#780\) [\#782](https://github.com/VSCodeVim/Vim/pull/782) ([bdchauvette](https://github.com/bdchauvette)) - fixes \#739 [\#767](https://github.com/VSCodeVim/Vim/pull/767) ([xconverge](https://github.com/xconverge)) ## [v0.2.0](https://github.com/vscodevim/vim/tree/v0.2.0) (2016-09-21) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.11...v0.2.0) ## [v0.1.11](https://github.com/vscodevim/vim/tree/v0.1.11) (2016-09-20) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.10...v0.1.11) - Release Pipeline [\#788](https://github.com/VSCodeVim/Vim/pull/788) ([jpoon](https://github.com/jpoon)) - Fix delete line with CRLF \(\#743\) [\#770](https://github.com/VSCodeVim/Vim/pull/770) ([jgoz](https://github.com/jgoz)) - fixes \#740 [\#766](https://github.com/VSCodeVim/Vim/pull/766) ([xconverge](https://github.com/xconverge)) - fixes \#764 [\#765](https://github.com/VSCodeVim/Vim/pull/765) ([xconverge](https://github.com/xconverge)) - fixes \#759 [\#760](https://github.com/VSCodeVim/Vim/pull/760) ([xconverge](https://github.com/xconverge)) - Register info [\#756](https://github.com/VSCodeVim/Vim/pull/756) ([rebornix](https://github.com/rebornix)) - build on extension/test launch [\#755](https://github.com/VSCodeVim/Vim/pull/755) ([jpoon](https://github.com/jpoon)) - fixes \#750 [\#752](https://github.com/VSCodeVim/Vim/pull/752) ([xconverge](https://github.com/xconverge)) - clean gulpfile [\#748](https://github.com/VSCodeVim/Vim/pull/748) ([jpoon](https://github.com/jpoon)) - Substitute marks [\#744](https://github.com/VSCodeVim/Vim/pull/744) ([rebornix](https://github.com/rebornix)) - Read command [\#736](https://github.com/VSCodeVim/Vim/pull/736) ([domgee](https://github.com/domgee)) - Doc for enabling repeating j/k for Insider build [\#733](https://github.com/VSCodeVim/Vim/pull/733) ([octref](https://github.com/octref)) - Add autoindent setting [\#726](https://github.com/VSCodeVim/Vim/pull/726) ([octref](https://github.com/octref)) - Disable Vim Mode in Debug Repl [\#723](https://github.com/VSCodeVim/Vim/pull/723) ([rebornix](https://github.com/rebornix)) - \[WIP\] Roadmap update [\#717](https://github.com/VSCodeVim/Vim/pull/717) ([rebornix](https://github.com/rebornix)) - Editor Scroll [\#681](https://github.com/VSCodeVim/Vim/pull/681) ([rebornix](https://github.com/rebornix)) - Implement :wa\[ll\] command \(write all\) [\#671](https://github.com/VSCodeVim/Vim/pull/671) ([mleech](https://github.com/mleech)) - Special keys in Insert Mode [\#615](https://github.com/VSCodeVim/Vim/pull/615) ([rebornix](https://github.com/rebornix)) ## [v0.1.10](https://github.com/vscodevim/vim/tree/v0.1.10) (2016-09-06) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.9...v0.1.10) - Align Screen Line commands with latest Code API [\#724](https://github.com/VSCodeVim/Vim/pull/724) ([rebornix](https://github.com/rebornix)) - Visual block tests [\#722](https://github.com/VSCodeVim/Vim/pull/722) ([xconverge](https://github.com/xconverge)) - Remapper fixes [\#721](https://github.com/VSCodeVim/Vim/pull/721) ([jpoon](https://github.com/jpoon)) - fixes \#718 A and I have cursor in right position now [\#720](https://github.com/VSCodeVim/Vim/pull/720) ([xconverge](https://github.com/xconverge)) - fixes \#696 [\#715](https://github.com/VSCodeVim/Vim/pull/715) ([xconverge](https://github.com/xconverge)) - fix \#690 and other toggle case issues [\#698](https://github.com/VSCodeVim/Vim/pull/698) ([xconverge](https://github.com/xconverge)) ## [v0.1.9](https://github.com/vscodevim/vim/tree/v0.1.9) (2016-09-05) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.8...v0.1.9) - Update README.md [\#714](https://github.com/VSCodeVim/Vim/pull/714) ([jpoon](https://github.com/jpoon)) - Add vim.\* settings to readme. Fixes \#503 [\#713](https://github.com/VSCodeVim/Vim/pull/713) ([jpoon](https://github.com/jpoon)) - Set diff timeout to 1 second. [\#712](https://github.com/VSCodeVim/Vim/pull/712) ([johnfn](https://github.com/johnfn)) - Inserts repeated with . would add many undo points; fix this. [\#711](https://github.com/VSCodeVim/Vim/pull/711) ([johnfn](https://github.com/johnfn)) - Hotfix remapping [\#710](https://github.com/VSCodeVim/Vim/pull/710) ([johnfn](https://github.com/johnfn)) - Tiny change to issue template. [\#709](https://github.com/VSCodeVim/Vim/pull/709) ([johnfn](https://github.com/johnfn)) ## [v0.1.8](https://github.com/vscodevim/vim/tree/v0.1.8) (2016-09-04) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.7...v0.1.8) - Fix race condition with switching active text editor. [\#705](https://github.com/VSCodeVim/Vim/pull/705) ([johnfn](https://github.com/johnfn)) - Fix bug with undo on untitled files. [\#704](https://github.com/VSCodeVim/Vim/pull/704) ([johnfn](https://github.com/johnfn)) - clear history when content from disk is changed [\#703](https://github.com/VSCodeVim/Vim/pull/703) ([aminroosta](https://github.com/aminroosta)) - Fix `\#` and `\*` Behaviour [\#702](https://github.com/VSCodeVim/Vim/pull/702) ([jpoon](https://github.com/jpoon)) - Fix error when \ at beginning of document [\#691](https://github.com/VSCodeVim/Vim/pull/691) ([jpoon](https://github.com/jpoon)) - Handle Ns and fix \#684 [\#688](https://github.com/VSCodeVim/Vim/pull/688) ([octref](https://github.com/octref)) - Use Angle Bracket Notation \(Fixes \#64\) [\#683](https://github.com/VSCodeVim/Vim/pull/683) ([jpoon](https://github.com/jpoon)) - Implement ; and , [\#674](https://github.com/VSCodeVim/Vim/pull/674) ([aminroosta](https://github.com/aminroosta)) - Some visual block fixes [\#667](https://github.com/VSCodeVim/Vim/pull/667) ([xconverge](https://github.com/xconverge)) - implement useSystemClipboard command [\#665](https://github.com/VSCodeVim/Vim/pull/665) ([aminroosta](https://github.com/aminroosta)) - Document autoindent option [\#664](https://github.com/VSCodeVim/Vim/pull/664) ([sectioneight](https://github.com/sectioneight)) - fix \#510 [\#659](https://github.com/VSCodeVim/Vim/pull/659) ([xconverge](https://github.com/xconverge)) - fix \#654 [\#656](https://github.com/VSCodeVim/Vim/pull/656) ([xconverge](https://github.com/xconverge)) - fix \#652 [\#655](https://github.com/VSCodeVim/Vim/pull/655) ([xconverge](https://github.com/xconverge)) - improves bracket undo behavior when vscode autocloses brackets [\#649](https://github.com/VSCodeVim/Vim/pull/649) ([xconverge](https://github.com/xconverge)) - Fix missleading readme instruction [\#647](https://github.com/VSCodeVim/Vim/pull/647) ([AntonAderum](https://github.com/AntonAderum)) - Undo behavior when position changes using arrows or mouse [\#646](https://github.com/VSCodeVim/Vim/pull/646) ([xconverge](https://github.com/xconverge)) - fix for extra character when double click mouse selection [\#645](https://github.com/VSCodeVim/Vim/pull/645) ([xconverge](https://github.com/xconverge)) - fix \#639 visual block mode minor issues [\#640](https://github.com/VSCodeVim/Vim/pull/640) ([xconverge](https://github.com/xconverge)) - Ctrl+a and Ctrl+x now create undo points correctly and can be repeate… [\#636](https://github.com/VSCodeVim/Vim/pull/636) ([xconverge](https://github.com/xconverge)) - fix \#501 some more to include 'k' [\#635](https://github.com/VSCodeVim/Vim/pull/635) ([xconverge](https://github.com/xconverge)) - updating the undo tree when using bracket operators slightly [\#634](https://github.com/VSCodeVim/Vim/pull/634) ([xconverge](https://github.com/xconverge)) - fix \#501 [\#632](https://github.com/VSCodeVim/Vim/pull/632) ([xconverge](https://github.com/xconverge)) - Fix bug \#613 ":wq command dows not work" [\#630](https://github.com/VSCodeVim/Vim/pull/630) ([Platzer](https://github.com/Platzer)) - Respect indentation on cc and S [\#629](https://github.com/VSCodeVim/Vim/pull/629) ([sectioneight](https://github.com/sectioneight)) - Fix tag markup in roadmap [\#628](https://github.com/VSCodeVim/Vim/pull/628) ([sectioneight](https://github.com/sectioneight)) - Allow regex in / search [\#627](https://github.com/VSCodeVim/Vim/pull/627) ([sectioneight](https://github.com/sectioneight)) - Synonyms [\#621](https://github.com/VSCodeVim/Vim/pull/621) ([rebornix](https://github.com/rebornix)) - Implement tag movements [\#619](https://github.com/VSCodeVim/Vim/pull/619) ([sectioneight](https://github.com/sectioneight)) ## [v0.1.7](https://github.com/vscodevim/vim/tree/v0.1.7) (2016-08-14) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.6...v0.1.7) - Add support Y in visual mode [\#597](https://github.com/VSCodeVim/Vim/pull/597) ([shotaAkasaka](https://github.com/shotaAkasaka)) - Sentence selection [\#592](https://github.com/VSCodeVim/Vim/pull/592) ([rebornix](https://github.com/rebornix)) - fix C or cc kill the empty line [\#591](https://github.com/VSCodeVim/Vim/pull/591) ([shotaAkasaka](https://github.com/shotaAkasaka)) - Added Non-Recursive mapping capability. Fixes issue \#408 [\#589](https://github.com/VSCodeVim/Vim/pull/589) ([somkun](https://github.com/somkun)) - Vim Settings [\#508](https://github.com/VSCodeVim/Vim/pull/508) ([rebornix](https://github.com/rebornix)) ## [v0.1.6](https://github.com/vscodevim/vim/tree/v0.1.6) (2016-08-09) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.5...v0.1.6) - \[WIP\] Visual block mode [\#469](https://github.com/VSCodeVim/Vim/pull/469) ([johnfn](https://github.com/johnfn)) ## [v0.1.5](https://github.com/vscodevim/vim/tree/v0.1.5) (2016-08-09) [Full Changelog](https://github.com/vscodevim/vim/compare/0.1.5...v0.1.5) ## [0.1.5](https://github.com/vscodevim/vim/tree/0.1.5) (2016-08-09) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.4...0.1.5) - Replace mode [\#580](https://github.com/VSCodeVim/Vim/pull/580) ([rebornix](https://github.com/rebornix)) - Fix for issue \#571 [\#579](https://github.com/VSCodeVim/Vim/pull/579) ([xconverge](https://github.com/xconverge)) - OS X non-global key repeat fix [\#577](https://github.com/VSCodeVim/Vim/pull/577) ([jimray](https://github.com/jimray)) - Hack to mitigate \#569 and prevent extension from locking up [\#576](https://github.com/VSCodeVim/Vim/pull/576) ([jpoon](https://github.com/jpoon)) - Fix binding of control-keys [\#575](https://github.com/VSCodeVim/Vim/pull/575) ([sectioneight](https://github.com/sectioneight)) - Fix test regression [\#560](https://github.com/VSCodeVim/Vim/pull/560) ([rebornix](https://github.com/rebornix)) - Fix gt,gT numeric prefix [\#559](https://github.com/VSCodeVim/Vim/pull/559) ([rebornix](https://github.com/rebornix)) - Fix incorrect cursor location after deleting linebreak \(fixes \#550\) [\#551](https://github.com/VSCodeVim/Vim/pull/551) ([thomasboyt](https://github.com/thomasboyt)) - Support gd [\#547](https://github.com/VSCodeVim/Vim/pull/547) ([johnfn](https://github.com/johnfn)) - Add support for S [\#546](https://github.com/VSCodeVim/Vim/pull/546) ([glibsm](https://github.com/glibsm)) - update roadmap [\#545](https://github.com/VSCodeVim/Vim/pull/545) ([rebornix](https://github.com/rebornix)) - Support "{char} registers and clipboard access via "\* register. [\#543](https://github.com/VSCodeVim/Vim/pull/543) ([aminroosta](https://github.com/aminroosta)) - Added CommandGoToOtherEndOfHiglightedText - \#526 [\#539](https://github.com/VSCodeVim/Vim/pull/539) ([Platzer](https://github.com/Platzer)) - Move sections [\#533](https://github.com/VSCodeVim/Vim/pull/533) ([rebornix](https://github.com/rebornix)) - Substitute with no range or marks [\#525](https://github.com/VSCodeVim/Vim/pull/525) ([rebornix](https://github.com/rebornix)) - Correct Fold behavior and update roadmap [\#524](https://github.com/VSCodeVim/Vim/pull/524) ([rebornix](https://github.com/rebornix)) - Make \ repeatable in Normal Mode. Fix \#394 [\#514](https://github.com/VSCodeVim/Vim/pull/514) ([octref](https://github.com/octref)) - Screen lines and characters. [\#486](https://github.com/VSCodeVim/Vim/pull/486) ([rebornix](https://github.com/rebornix)) ## [v0.1.4](https://github.com/vscodevim/vim/tree/v0.1.4) (2016-07-28) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.3...v0.1.4) - Implement increment and decrement operators [\#515](https://github.com/VSCodeVim/Vim/pull/515) ([sectioneight](https://github.com/sectioneight)) - Fix \#502 [\#509](https://github.com/VSCodeVim/Vim/pull/509) ([rebornix](https://github.com/rebornix)) - Add tabs movement and fix tab command with correct counting [\#507](https://github.com/VSCodeVim/Vim/pull/507) ([rebornix](https://github.com/rebornix)) - Omit first word in hash backwards search [\#506](https://github.com/VSCodeVim/Vim/pull/506) ([sectioneight](https://github.com/sectioneight)) - Turn around for cursor problem [\#505](https://github.com/VSCodeVim/Vim/pull/505) ([rebornix](https://github.com/rebornix)) - Fix instructions for setting key bindings [\#499](https://github.com/VSCodeVim/Vim/pull/499) ([positron](https://github.com/positron)) - Code clean-up. Remove dead code. [\#497](https://github.com/VSCodeVim/Vim/pull/497) ([jpoon](https://github.com/jpoon)) - Merge history changes into a single operation. Fixes \#427 [\#496](https://github.com/VSCodeVim/Vim/pull/496) ([infogulch](https://github.com/infogulch)) - Fix \#438 - Limit the number of matches, and try to only recalculate when the searchString changes, or the document changes [\#494](https://github.com/VSCodeVim/Vim/pull/494) ([roblourens](https://github.com/roblourens)) - CommandFold should be available in Normal mode [\#493](https://github.com/VSCodeVim/Vim/pull/493) ([aminroosta](https://github.com/aminroosta)) - Fix % movement when not on opening character [\#490](https://github.com/VSCodeVim/Vim/pull/490) ([sectioneight](https://github.com/sectioneight)) - Suggest npm run compile in CONTRIBUTING page [\#488](https://github.com/VSCodeVim/Vim/pull/488) ([aminroosta](https://github.com/aminroosta)) - Implement quoted text objects [\#483](https://github.com/VSCodeVim/Vim/pull/483) ([sectioneight](https://github.com/sectioneight)) - Fix \#338 - add gt, gT support [\#482](https://github.com/VSCodeVim/Vim/pull/482) ([arussellk](https://github.com/arussellk)) - Set correct cursor and selection after code format. [\#478](https://github.com/VSCodeVim/Vim/pull/478) ([rebornix](https://github.com/rebornix)) - CJK in all modes [\#475](https://github.com/VSCodeVim/Vim/pull/475) ([rebornix](https://github.com/rebornix)) - Fix \#358. [\#399](https://github.com/VSCodeVim/Vim/pull/399) ([rebornix](https://github.com/rebornix)) - Word in visual mode [\#385](https://github.com/VSCodeVim/Vim/pull/385) ([rebornix](https://github.com/rebornix)) ## [v0.1.3](https://github.com/vscodevim/vim/tree/v0.1.3) (2016-07-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.2...v0.1.3) - Fix wrong command for ctrl+f [\#476](https://github.com/VSCodeVim/Vim/pull/476) ([rebornix](https://github.com/rebornix)) - Fix regressions in text objects [\#473](https://github.com/VSCodeVim/Vim/pull/473) ([sectioneight](https://github.com/sectioneight)) - Fix handling of opener for nested text objects [\#472](https://github.com/VSCodeVim/Vim/pull/472) ([sectioneight](https://github.com/sectioneight)) - Implement square-bracket text object [\#467](https://github.com/VSCodeVim/Vim/pull/467) ([sectioneight](https://github.com/sectioneight)) - Add support for failed motions [\#466](https://github.com/VSCodeVim/Vim/pull/466) ([johnfn](https://github.com/johnfn)) - Add test-specific tslint [\#464](https://github.com/VSCodeVim/Vim/pull/464) ([sectioneight](https://github.com/sectioneight)) - Initialize mode and cursor after startup [\#462](https://github.com/VSCodeVim/Vim/pull/462) ([rebornix](https://github.com/rebornix)) - FixTabStops [\#461](https://github.com/VSCodeVim/Vim/pull/461) ([rebornix](https://github.com/rebornix)) - Convert 4 space tab to 2 space tab. [\#460](https://github.com/VSCodeVim/Vim/pull/460) ([rebornix](https://github.com/rebornix)) - Enforce TSLint. Closes \#456 [\#459](https://github.com/VSCodeVim/Vim/pull/459) ([jpoon](https://github.com/jpoon)) - Add back missing control-c registration [\#455](https://github.com/VSCodeVim/Vim/pull/455) ([sectioneight](https://github.com/sectioneight)) - Fix checkmark syntax on roadmap [\#454](https://github.com/VSCodeVim/Vim/pull/454) ([sectioneight](https://github.com/sectioneight)) - Add support for ctrl+w in insert mode [\#453](https://github.com/VSCodeVim/Vim/pull/453) ([sectioneight](https://github.com/sectioneight)) - Implement additional text object commands [\#450](https://github.com/VSCodeVim/Vim/pull/450) ([sectioneight](https://github.com/sectioneight)) - Remove custom keyboard mapping \(fixes \#432\). Fix duplicate definition… [\#447](https://github.com/VSCodeVim/Vim/pull/447) ([jpoon](https://github.com/jpoon)) - Fix \#341 CJK Problem. [\#446](https://github.com/VSCodeVim/Vim/pull/446) ([rebornix](https://github.com/rebornix)) - Fix \#426 [\#445](https://github.com/VSCodeVim/Vim/pull/445) ([arussellk](https://github.com/arussellk)) - Read TextEditor options from active editor [\#444](https://github.com/VSCodeVim/Vim/pull/444) ([rebornix](https://github.com/rebornix)) - \[p, \[p, gp and gP [\#412](https://github.com/VSCodeVim/Vim/pull/412) ([rebornix](https://github.com/rebornix)) - Open file in new window. [\#404](https://github.com/VSCodeVim/Vim/pull/404) ([rebornix](https://github.com/rebornix)) ## [v0.1.2](https://github.com/vscodevim/vim/tree/v0.1.2) (2016-07-13) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1.1...v0.1.2) - Fix spec for otherModesKeyBindings to match insert [\#434](https://github.com/VSCodeVim/Vim/pull/434) ([sectioneight](https://github.com/sectioneight)) - Use TypeScript 2.0 and use strictNullChecks. [\#431](https://github.com/VSCodeVim/Vim/pull/431) ([johnfn](https://github.com/johnfn)) - Ctrl+U and Ctrl+D [\#430](https://github.com/VSCodeVim/Vim/pull/430) ([rebornix](https://github.com/rebornix)) - Fix\#369. `dw` eats EOF. [\#428](https://github.com/VSCodeVim/Vim/pull/428) ([rebornix](https://github.com/rebornix)) - Include vscode typings [\#419](https://github.com/VSCodeVim/Vim/pull/419) ([jpoon](https://github.com/jpoon)) - Fix ctrl+b, ctrl+f [\#418](https://github.com/VSCodeVim/Vim/pull/418) ([jpoon](https://github.com/jpoon)) - Fix \#397. [\#413](https://github.com/VSCodeVim/Vim/pull/413) ([rebornix](https://github.com/rebornix)) - Fix layout mistake in Contributing and gulp typo [\#411](https://github.com/VSCodeVim/Vim/pull/411) ([frederickfogerty](https://github.com/frederickfogerty)) ## [v0.1.1](https://github.com/vscodevim/vim/tree/v0.1.1) (2016-07-08) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.1...v0.1.1) - Fix \#414. [\#415](https://github.com/VSCodeVim/Vim/pull/415) ([rebornix](https://github.com/rebornix)) - Substitute [\#376](https://github.com/VSCodeVim/Vim/pull/376) ([rebornix](https://github.com/rebornix)) ## [v0.1](https://github.com/vscodevim/vim/tree/v0.1) (2016-07-08) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.28...v0.1) - Fix Roadmap link in Readme [\#405](https://github.com/VSCodeVim/Vim/pull/405) ([frederickfogerty](https://github.com/frederickfogerty)) - Fix TS2318 and ignore .vscode-test folder [\#400](https://github.com/VSCodeVim/Vim/pull/400) ([rebornix](https://github.com/rebornix)) - Update window command status [\#398](https://github.com/VSCodeVim/Vim/pull/398) ([rebornix](https://github.com/rebornix)) - `workbench.files.action.closeAllFiles` is deprecated. [\#395](https://github.com/VSCodeVim/Vim/pull/395) ([rebornix](https://github.com/rebornix)) - Basic Key Remapping [\#390](https://github.com/VSCodeVim/Vim/pull/390) ([johnfn](https://github.com/johnfn)) - Use correct API for file open. [\#388](https://github.com/VSCodeVim/Vim/pull/388) ([rebornix](https://github.com/rebornix)) - Use Arrows in Insert Mode. [\#387](https://github.com/VSCodeVim/Vim/pull/387) ([rebornix](https://github.com/rebornix)) - Marks [\#386](https://github.com/VSCodeVim/Vim/pull/386) ([johnfn](https://github.com/johnfn)) - Arrows [\#383](https://github.com/VSCodeVim/Vim/pull/383) ([rebornix](https://github.com/rebornix)) - Edit File [\#372](https://github.com/VSCodeVim/Vim/pull/372) ([rebornix](https://github.com/rebornix)) - Unclosed brackets [\#371](https://github.com/VSCodeVim/Vim/pull/371) ([rebornix](https://github.com/rebornix)) - Manual history tracking [\#370](https://github.com/VSCodeVim/Vim/pull/370) ([johnfn](https://github.com/johnfn)) - Tabs [\#368](https://github.com/VSCodeVim/Vim/pull/368) ([rebornix](https://github.com/rebornix)) - Rebornix switch pane [\#367](https://github.com/VSCodeVim/Vim/pull/367) ([johnfn](https://github.com/johnfn)) - Support `C` [\#366](https://github.com/VSCodeVim/Vim/pull/366) ([rebornix](https://github.com/rebornix)) - Add Ncc support and revise cc behavior [\#365](https://github.com/VSCodeVim/Vim/pull/365) ([rebornix](https://github.com/rebornix)) - Bring Ctrl keys back [\#364](https://github.com/VSCodeVim/Vim/pull/364) ([rebornix](https://github.com/rebornix)) - \[WIP\]: Switch Window [\#363](https://github.com/VSCodeVim/Vim/pull/363) ([rebornix](https://github.com/rebornix)) - Sentence [\#362](https://github.com/VSCodeVim/Vim/pull/362) ([rebornix](https://github.com/rebornix)) - Add config option for nonblinking block cursor. [\#361](https://github.com/VSCodeVim/Vim/pull/361) ([johnfn](https://github.com/johnfn)) - Refactor search [\#357](https://github.com/VSCodeVim/Vim/pull/357) ([johnfn](https://github.com/johnfn)) - WriteQuit [\#354](https://github.com/VSCodeVim/Vim/pull/354) ([srepollock](https://github.com/srepollock)) ## [v0.0.28](https://github.com/vscodevim/vim/tree/v0.0.28) (2016-06-24) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.27...v0.0.28) - Implement \yy [\#351](https://github.com/VSCodeVim/Vim/pull/351) ([rebornix](https://github.com/rebornix)) - Align TextEditorOptions between test code and workspace [\#350](https://github.com/VSCodeVim/Vim/pull/350) ([rebornix](https://github.com/rebornix)) - Uppercase support [\#349](https://github.com/VSCodeVim/Vim/pull/349) ([johnfn](https://github.com/johnfn)) - Add format code support. Fix \#308. [\#348](https://github.com/VSCodeVim/Vim/pull/348) ([rebornix](https://github.com/rebornix)) ## [v0.0.27](https://github.com/vscodevim/vim/tree/v0.0.27) (2016-06-23) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.26...v0.0.27) ## [v0.0.26](https://github.com/vscodevim/vim/tree/v0.0.26) (2016-06-22) [Full Changelog](https://github.com/vscodevim/vim/compare/0.0.26...v0.0.26) ## [0.0.26](https://github.com/vscodevim/vim/tree/0.0.26) (2016-06-22) [Full Changelog](https://github.com/vscodevim/vim/compare/0.0.25...0.0.26) - Star and hash [\#335](https://github.com/VSCodeVim/Vim/pull/335) ([johnfn](https://github.com/johnfn)) - Tilde key toggles case and moves forwards [\#325](https://github.com/VSCodeVim/Vim/pull/325) ([markrendle](https://github.com/markrendle)) - Pressing Enter moves cursor to start of next line [\#324](https://github.com/VSCodeVim/Vim/pull/324) ([markrendle](https://github.com/markrendle)) - Add infrastructure for repeatable commands. [\#322](https://github.com/VSCodeVim/Vim/pull/322) ([johnfn](https://github.com/johnfn)) - Add support for 'U' uppercase [\#312](https://github.com/VSCodeVim/Vim/pull/312) ([rebornix](https://github.com/rebornix)) ## [0.0.25](https://github.com/vscodevim/vim/tree/0.0.25) (2016-06-20) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.25...0.0.25) ## [v0.0.25](https://github.com/vscodevim/vim/tree/v0.0.25) (2016-06-20) [Full Changelog](https://github.com/vscodevim/vim/compare/0.0.24...v0.0.25) - Repeated motions [\#321](https://github.com/VSCodeVim/Vim/pull/321) ([johnfn](https://github.com/johnfn)) ## [0.0.24](https://github.com/vscodevim/vim/tree/0.0.24) (2016-06-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.24...0.0.24) ## [v0.0.24](https://github.com/vscodevim/vim/tree/v0.0.24) (2016-06-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.23...v0.0.24) ## [v0.0.23](https://github.com/vscodevim/vim/tree/v0.0.23) (2016-06-19) [Full Changelog](https://github.com/vscodevim/vim/compare/0.0.23...v0.0.23) ## [0.0.23](https://github.com/vscodevim/vim/tree/0.0.23) (2016-06-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.22...0.0.23) - Add %. [\#319](https://github.com/VSCodeVim/Vim/pull/319) ([johnfn](https://github.com/johnfn)) - @darrenweston's test improvements + more work [\#316](https://github.com/VSCodeVim/Vim/pull/316) ([johnfn](https://github.com/johnfn)) ## [v0.0.22](https://github.com/vscodevim/vim/tree/v0.0.22) (2016-06-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.21...v0.0.22) ## [v0.0.21](https://github.com/vscodevim/vim/tree/v0.0.21) (2016-06-17) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.20...v0.0.21) - Fix visual line selection from bottom to top. [\#307](https://github.com/VSCodeVim/Vim/pull/307) ([johnfn](https://github.com/johnfn)) - Fix autocomplete [\#304](https://github.com/VSCodeVim/Vim/pull/304) ([johnfn](https://github.com/johnfn)) - Select into visual mode [\#302](https://github.com/VSCodeVim/Vim/pull/302) ([johnfn](https://github.com/johnfn)) - Refactor dot [\#294](https://github.com/VSCodeVim/Vim/pull/294) ([johnfn](https://github.com/johnfn)) ## [v0.0.20](https://github.com/vscodevim/vim/tree/v0.0.20) (2016-06-13) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.19...v0.0.20) - Add simpler test mechanism and convert some tests [\#292](https://github.com/VSCodeVim/Vim/pull/292) ([darrenweston](https://github.com/darrenweston)) - Refactor motions [\#288](https://github.com/VSCodeVim/Vim/pull/288) ([johnfn](https://github.com/johnfn)) - Search [\#277](https://github.com/VSCodeVim/Vim/pull/277) ([johnfn](https://github.com/johnfn)) - Tests [\#275](https://github.com/VSCodeVim/Vim/pull/275) ([johnfn](https://github.com/johnfn)) - Add P. [\#262](https://github.com/VSCodeVim/Vim/pull/262) ([johnfn](https://github.com/johnfn)) - Add zz. [\#261](https://github.com/VSCodeVim/Vim/pull/261) ([johnfn](https://github.com/johnfn)) - Added some 'r' tests [\#260](https://github.com/VSCodeVim/Vim/pull/260) ([darrenweston](https://github.com/darrenweston)) - Add r. [\#252](https://github.com/VSCodeVim/Vim/pull/252) ([johnfn](https://github.com/johnfn)) - J [\#251](https://github.com/VSCodeVim/Vim/pull/251) ([johnfn](https://github.com/johnfn)) - Dot key. [\#249](https://github.com/VSCodeVim/Vim/pull/249) ([johnfn](https://github.com/johnfn)) - No longer special case insert mode keys. [\#246](https://github.com/VSCodeVim/Vim/pull/246) ([johnfn](https://github.com/johnfn)) - Use vscode built in support for block cursors [\#245](https://github.com/VSCodeVim/Vim/pull/245) ([Paxxi](https://github.com/Paxxi)) ## [v0.0.19](https://github.com/vscodevim/vim/tree/v0.0.19) (2016-06-07) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.18...v0.0.19) - Add f, F, t and T motions [\#244](https://github.com/VSCodeVim/Vim/pull/244) ([johnfn](https://github.com/johnfn)) - Add visual line mode tests. [\#243](https://github.com/VSCodeVim/Vim/pull/243) ([johnfn](https://github.com/johnfn)) - List keys individually rather than as a string. [\#242](https://github.com/VSCodeVim/Vim/pull/242) ([johnfn](https://github.com/johnfn)) - Fix vims wonky visual eol behavior [\#241](https://github.com/VSCodeVim/Vim/pull/241) ([johnfn](https://github.com/johnfn)) - Add Visual Line mode [\#240](https://github.com/VSCodeVim/Vim/pull/240) ([johnfn](https://github.com/johnfn)) - Move word special case to appropriate place. [\#239](https://github.com/VSCodeVim/Vim/pull/239) ([johnfn](https://github.com/johnfn)) - Cleanup cursor drawing and remove Motion class [\#238](https://github.com/VSCodeVim/Vim/pull/238) ([johnfn](https://github.com/johnfn)) - dd, cc & yy tests [\#237](https://github.com/VSCodeVim/Vim/pull/237) ([johnfn](https://github.com/johnfn)) - Add cc/yy/dd. [\#236](https://github.com/VSCodeVim/Vim/pull/236) ([johnfn](https://github.com/johnfn)) - Add s keybinding [\#235](https://github.com/VSCodeVim/Vim/pull/235) ([Paxxi](https://github.com/Paxxi)) - Refactor commands \[WIP\] [\#234](https://github.com/VSCodeVim/Vim/pull/234) ([johnfn](https://github.com/johnfn)) - Don't use ctrl-c to leave insert mode by default. [\#233](https://github.com/VSCodeVim/Vim/pull/233) ([johnfn](https://github.com/johnfn)) - Add rudimentary register implementation. [\#232](https://github.com/VSCodeVim/Vim/pull/232) ([johnfn](https://github.com/johnfn)) - Rewrite normal mode tests. [\#231](https://github.com/VSCodeVim/Vim/pull/231) ([johnfn](https://github.com/johnfn)) - Rewrite Normal Mode tests to use the ModeHandler interface. [\#230](https://github.com/VSCodeVim/Vim/pull/230) ([johnfn](https://github.com/johnfn)) - Refactor CommandKeyMap [\#228](https://github.com/VSCodeVim/Vim/pull/228) ([jpoon](https://github.com/jpoon)) - Add yank support for Visual mode [\#217](https://github.com/VSCodeVim/Vim/pull/217) ([pjvds](https://github.com/pjvds)) ## [v0.0.18](https://github.com/vscodevim/vim/tree/v0.0.18) (2016-05-19) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.17...v0.0.18) - Install Gulp for Travis [\#225](https://github.com/VSCodeVim/Vim/pull/225) ([jpoon](https://github.com/jpoon)) - Update to vscode 0.10.12 APIs [\#224](https://github.com/VSCodeVim/Vim/pull/224) ([jpoon](https://github.com/jpoon)) ## [v0.0.17](https://github.com/vscodevim/vim/tree/v0.0.17) (2016-05-17) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.16...v0.0.17) - Added basic fold commands zc, zo, zC, zO. [\#222](https://github.com/VSCodeVim/Vim/pull/222) ([geksilla](https://github.com/geksilla)) - keymap configurations only override defaults that are changed [\#221](https://github.com/VSCodeVim/Vim/pull/221) ([adiviness](https://github.com/adiviness)) - Added basic support for rebinding keys. [\#219](https://github.com/VSCodeVim/Vim/pull/219) ([Lindenk](https://github.com/Lindenk)) - waffle.io Badge [\#216](https://github.com/VSCodeVim/Vim/pull/216) ([waffle-iron](https://github.com/waffle-iron)) - Add check mark to D key in README [\#215](https://github.com/VSCodeVim/Vim/pull/215) ([pjvds](https://github.com/pjvds)) ## [v0.0.16](https://github.com/vscodevim/vim/tree/v0.0.16) (2016-05-03) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.15...v0.0.16) - I think this may fix the build failure. [\#209](https://github.com/VSCodeVim/Vim/pull/209) ([edthedev](https://github.com/edthedev)) - Support for copy and p command [\#208](https://github.com/VSCodeVim/Vim/pull/208) ([petegleeson](https://github.com/petegleeson)) - Fix issue / key doesn't search current file [\#205](https://github.com/VSCodeVim/Vim/pull/205) ([tnngo2](https://github.com/tnngo2)) - Fixes Incorrect Cursor Position after Transition into Normal Mode [\#202](https://github.com/VSCodeVim/Vim/pull/202) ([dpbackes](https://github.com/dpbackes)) - Fixes Issue with Cursor Position After 'dw' [\#200](https://github.com/VSCodeVim/Vim/pull/200) ([dpbackes](https://github.com/dpbackes)) ## [v0.0.15](https://github.com/vscodevim/vim/tree/v0.0.15) (2016-03-22) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.14...v0.0.15) - Bug fixes [\#192](https://github.com/VSCodeVim/Vim/pull/192) ([jpoon](https://github.com/jpoon)) ## [v0.0.14](https://github.com/vscodevim/vim/tree/v0.0.14) (2016-03-21) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.13...v0.0.14) - Bug fixes [\#191](https://github.com/VSCodeVim/Vim/pull/191) ([jpoon](https://github.com/jpoon)) - Search '/' in Command Mode [\#190](https://github.com/VSCodeVim/Vim/pull/190) ([jpoon](https://github.com/jpoon)) ## [v0.0.13](https://github.com/vscodevim/vim/tree/v0.0.13) (2016-03-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.12...v0.0.13) - fix appveyor build [\#189](https://github.com/VSCodeVim/Vim/pull/189) ([jpoon](https://github.com/jpoon)) - Fixup/highlight eol char [\#182](https://github.com/VSCodeVim/Vim/pull/182) ([khisakuni](https://github.com/khisakuni)) - c commands and ge motions [\#180](https://github.com/VSCodeVim/Vim/pull/180) ([frarees](https://github.com/frarees)) - add github_token to appveyor/travis [\#178](https://github.com/VSCodeVim/Vim/pull/178) ([jpoon](https://github.com/jpoon)) - Commands can write to status bar [\#177](https://github.com/VSCodeVim/Vim/pull/177) ([frarees](https://github.com/frarees)) - Wait for test files to get written [\#175](https://github.com/VSCodeVim/Vim/pull/175) ([frarees](https://github.com/frarees)) - d{motion} support [\#174](https://github.com/VSCodeVim/Vim/pull/174) ([frarees](https://github.com/frarees)) ## [v0.0.12](https://github.com/vscodevim/vim/tree/v0.0.12) (2016-03-04) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.11...v0.0.12) - Spanish keyboard mappings [\#169](https://github.com/VSCodeVim/Vim/pull/169) ([frarees](https://github.com/frarees)) - Fix visual mode activated on insert mode [\#168](https://github.com/VSCodeVim/Vim/pull/168) ([frarees](https://github.com/frarees)) - Fix lexer unreachable code causing build error [\#165](https://github.com/VSCodeVim/Vim/pull/165) ([frarees](https://github.com/frarees)) - Update Package Dependencies. Remove Ctrl+C [\#163](https://github.com/VSCodeVim/Vim/pull/163) ([jpoon](https://github.com/jpoon)) - Add E \(end of WORD\), and fix up e \(end of word\). [\#160](https://github.com/VSCodeVim/Vim/pull/160) ([tma-isbx](https://github.com/tma-isbx)) - Fix for block cursor in insert mode [\#154](https://github.com/VSCodeVim/Vim/pull/154) ([sWW26](https://github.com/sWW26)) - Move private methods and update readme [\#153](https://github.com/VSCodeVim/Vim/pull/153) ([tma-isbx](https://github.com/tma-isbx)) - Visual Mode + Rudimentary Operators [\#144](https://github.com/VSCodeVim/Vim/pull/144) ([johnfn](https://github.com/johnfn)) ## [v0.0.11](https://github.com/vscodevim/vim/tree/v0.0.11) (2016-02-18) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.10...v0.0.11) - Upgrade to Typings as TSD has been deprecated [\#152](https://github.com/VSCodeVim/Vim/pull/152) ([jpoon](https://github.com/jpoon)) - Convert test to async/await style. [\#150](https://github.com/VSCodeVim/Vim/pull/150) ([johnfn](https://github.com/johnfn)) - Capital W/B word movement [\#147](https://github.com/VSCodeVim/Vim/pull/147) ([tma-isbx](https://github.com/tma-isbx)) - Implement 'X' in normal mode \(backspace\) [\#145](https://github.com/VSCodeVim/Vim/pull/145) ([tma-isbx](https://github.com/tma-isbx)) - Fix b motion. [\#143](https://github.com/VSCodeVim/Vim/pull/143) ([johnfn](https://github.com/johnfn)) - Implement ctrl+f/ctrl+b \(PageDown/PageUp\) [\#142](https://github.com/VSCodeVim/Vim/pull/142) ([tma-isbx](https://github.com/tma-isbx)) - \[\#127\] Fix 'x' behavior at EOL [\#141](https://github.com/VSCodeVim/Vim/pull/141) ([tma-isbx](https://github.com/tma-isbx)) - Implement % to jump to matching brace [\#140](https://github.com/VSCodeVim/Vim/pull/140) ([tma-isbx](https://github.com/tma-isbx)) - Add ctrl-c. [\#139](https://github.com/VSCodeVim/Vim/pull/139) ([johnfn](https://github.com/johnfn)) - Fix word and back-word motions, and fix tests. [\#138](https://github.com/VSCodeVim/Vim/pull/138) ([johnfn](https://github.com/johnfn)) - Convert to ES6, Promises, async and await. [\#137](https://github.com/VSCodeVim/Vim/pull/137) ([johnfn](https://github.com/johnfn)) ## [v0.0.10](https://github.com/vscodevim/vim/tree/v0.0.10) (2016-02-01) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.9...v0.0.10) - Implement % to jump to matching brace [\#134](https://github.com/VSCodeVim/Vim/pull/134) ([tma-isbx](https://github.com/tma-isbx)) - Add paragraph motions [\#133](https://github.com/VSCodeVim/Vim/pull/133) ([johnfn](https://github.com/johnfn)) - Add Swedish keyboard layout [\#130](https://github.com/VSCodeVim/Vim/pull/130) ([AntonAderum](https://github.com/AntonAderum)) ## [v0.0.9](https://github.com/vscodevim/vim/tree/v0.0.9) (2016-01-06) [Full Changelog](https://github.com/vscodevim/vim/compare/0.0.9...v0.0.9) ## [0.0.9](https://github.com/vscodevim/vim/tree/0.0.9) (2016-01-06) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.8...0.0.9) - added danish keyboard layout - fix issue \#124 [\#125](https://github.com/VSCodeVim/Vim/pull/125) ([kedde](https://github.com/kedde)) - Delete Right when user presses x [\#122](https://github.com/VSCodeVim/Vim/pull/122) ([sharpoverride](https://github.com/sharpoverride)) ## [v0.0.8](https://github.com/vscodevim/vim/tree/v0.0.8) (2016-01-03) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.7...v0.0.8) ## [v0.0.7](https://github.com/vscodevim/vim/tree/v0.0.7) (2016-01-03) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.6...v0.0.7) - Block Cursor [\#120](https://github.com/VSCodeVim/Vim/pull/120) ([jpoon](https://github.com/jpoon)) - BugFix: swapped cursor and caret. desired column not updated properly [\#119](https://github.com/VSCodeVim/Vim/pull/119) ([jpoon](https://github.com/jpoon)) - Readme: update with keyboard configuration [\#116](https://github.com/VSCodeVim/Vim/pull/116) ([jpoon](https://github.com/jpoon)) - Tests: Enable all tests to be run in Travis CI [\#115](https://github.com/VSCodeVim/Vim/pull/115) ([jpoon](https://github.com/jpoon)) - Cleanup [\#114](https://github.com/VSCodeVim/Vim/pull/114) ([jpoon](https://github.com/jpoon)) ## [v0.0.6](https://github.com/vscodevim/vim/tree/v0.0.6) (2015-12-30) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.5...v0.0.6) - Cleanup [\#113](https://github.com/VSCodeVim/Vim/pull/113) ([jpoon](https://github.com/jpoon)) - Motion Fixes [\#112](https://github.com/VSCodeVim/Vim/pull/112) ([jpoon](https://github.com/jpoon)) - Fix character position persistence on up/down commands, add : "e", "0", and fix "^" [\#109](https://github.com/VSCodeVim/Vim/pull/109) ([corymickelson](https://github.com/corymickelson)) ## [v0.0.5](https://github.com/vscodevim/vim/tree/v0.0.5) (2015-12-09) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.3...v0.0.5) ## [v0.0.3](https://github.com/vscodevim/vim/tree/v0.0.3) (2015-12-04) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.2...v0.0.3) - Promisify [\#92](https://github.com/VSCodeVim/Vim/pull/92) ([jpoon](https://github.com/jpoon)) - fix cursor position after entering command mode \(again\) [\#91](https://github.com/VSCodeVim/Vim/pull/91) ([kimitake](https://github.com/kimitake)) - Refactor motion. [\#87](https://github.com/VSCodeVim/Vim/pull/87) ([jpoon](https://github.com/jpoon)) - Added CONTRIBUTING doc [\#83](https://github.com/VSCodeVim/Vim/pull/83) ([markrendle](https://github.com/markrendle)) - readme: update more detailed contributing info [\#80](https://github.com/VSCodeVim/Vim/pull/80) ([jpoon](https://github.com/jpoon)) - Created tests for modeInsert [\#79](https://github.com/VSCodeVim/Vim/pull/79) ([benjaminRomano](https://github.com/benjaminRomano)) - gulp: add trim-whitespace task [\#78](https://github.com/VSCodeVim/Vim/pull/78) ([jpoon](https://github.com/jpoon)) - implement correct w,b motion behaviour [\#76](https://github.com/VSCodeVim/Vim/pull/76) ([adriaanp](https://github.com/adriaanp)) - Fix PR builds [\#75](https://github.com/VSCodeVim/Vim/pull/75) ([jpoon](https://github.com/jpoon)) - Tests [\#74](https://github.com/VSCodeVim/Vim/pull/74) ([jpoon](https://github.com/jpoon)) - Add commands support for 'gg' and 'G' [\#71](https://github.com/VSCodeVim/Vim/pull/71) ([liushuping](https://github.com/liushuping)) - fix line end determination for a, A, \$ [\#68](https://github.com/VSCodeVim/Vim/pull/68) ([kimitake](https://github.com/kimitake)) - '\$' and '^' for Moving to beginning and end of line [\#66](https://github.com/VSCodeVim/Vim/pull/66) ([josephliccini](https://github.com/josephliccini)) - support x command [\#65](https://github.com/VSCodeVim/Vim/pull/65) ([kimitake](https://github.com/kimitake)) - Update README.md [\#63](https://github.com/VSCodeVim/Vim/pull/63) ([markrendle](https://github.com/markrendle)) - map keys from US keyboard to other layouts [\#61](https://github.com/VSCodeVim/Vim/pull/61) ([guillermooo](https://github.com/guillermooo)) - fix bug for Cursor class [\#58](https://github.com/VSCodeVim/Vim/pull/58) ([kimitake](https://github.com/kimitake)) - Cursor Motions [\#56](https://github.com/VSCodeVim/Vim/pull/56) ([jpoon](https://github.com/jpoon)) - Add word motion and db [\#53](https://github.com/VSCodeVim/Vim/pull/53) ([adriaanp](https://github.com/adriaanp)) ## [v0.0.2](https://github.com/vscodevim/vim/tree/v0.0.2) (2015-11-29) [Full Changelog](https://github.com/vscodevim/vim/compare/v0.0.1...v0.0.2) - move cursor position after getting normal mode [\#50](https://github.com/VSCodeVim/Vim/pull/50) ([kimitake](https://github.com/kimitake)) ## [v0.0.1](https://github.com/vscodevim/vim/tree/v0.0.1) (2015-11-29) - Implement Redo, Refactor Keybindings [\#46](https://github.com/VSCodeVim/Vim/pull/46) ([jpoon](https://github.com/jpoon)) - reorganize tests; add tests [\#45](https://github.com/VSCodeVim/Vim/pull/45) ([guillermooo](https://github.com/guillermooo)) - fixes; add VimError class [\#43](https://github.com/VSCodeVim/Vim/pull/43) ([guillermooo](https://github.com/guillermooo)) - Refactor cmdline [\#42](https://github.com/VSCodeVim/Vim/pull/42) ([guillermooo](https://github.com/guillermooo)) - ensure user can dismiss global messages with esc [\#41](https://github.com/VSCodeVim/Vim/pull/41) ([guillermooo](https://github.com/guillermooo)) - implement :quit [\#40](https://github.com/VSCodeVim/Vim/pull/40) ([guillermooo](https://github.com/guillermooo)) - Commands: `u` and `dw` [\#38](https://github.com/VSCodeVim/Vim/pull/38) ([jpoon](https://github.com/jpoon)) - Update metadata getting ready for a release [\#37](https://github.com/VSCodeVim/Vim/pull/37) ([jpoon](https://github.com/jpoon)) - rename command mode to normal mode [\#34](https://github.com/VSCodeVim/Vim/pull/34) ([jpoon](https://github.com/jpoon)) - Support `\<\<` and `\>\>` [\#32](https://github.com/VSCodeVim/Vim/pull/32) ([jpoon](https://github.com/jpoon)) - Add Slackin to Readme [\#31](https://github.com/VSCodeVim/Vim/pull/31) ([jpoon](https://github.com/jpoon)) - start code in CI server [\#28](https://github.com/VSCodeVim/Vim/pull/28) ([guillermooo](https://github.com/guillermooo)) - assorted fixes [\#27](https://github.com/VSCodeVim/Vim/pull/27) ([guillermooo](https://github.com/guillermooo)) - travis: turn off email notifications [\#25](https://github.com/VSCodeVim/Vim/pull/25) ([jpoon](https://github.com/jpoon)) - add keys [\#22](https://github.com/VSCodeVim/Vim/pull/22) ([guillermooo](https://github.com/guillermooo)) - Ex mode [\#20](https://github.com/VSCodeVim/Vim/pull/20) ([guillermooo](https://github.com/guillermooo)) - Command/Insert Modes [\#16](https://github.com/VSCodeVim/Vim/pull/16) ([jpoon](https://github.com/jpoon)) - Update tslint to vscode official style guidelines [\#14](https://github.com/VSCodeVim/Vim/pull/14) ([jpoon](https://github.com/jpoon)) - Run Tests a la Gulp [\#11](https://github.com/VSCodeVim/Vim/pull/11) ([jpoon](https://github.com/jpoon)) - assorted fixes [\#10](https://github.com/VSCodeVim/Vim/pull/10) ([guillermooo](https://github.com/guillermooo)) - Assorted fixes [\#7](https://github.com/VSCodeVim/Vim/pull/7) ([guillermooo](https://github.com/guillermooo)) - add gulp + tslint [\#6](https://github.com/VSCodeVim/Vim/pull/6) ([jpoon](https://github.com/jpoon)) - command line mode refactoring [\#5](https://github.com/VSCodeVim/Vim/pull/5) ([guillermooo](https://github.com/guillermooo)) - Navigation mode [\#4](https://github.com/VSCodeVim/Vim/pull/4) ([jpoon](https://github.com/jpoon)) - Add ex mode [\#3](https://github.com/VSCodeVim/Vim/pull/3) ([guillermooo](https://github.com/guillermooo)) \* _This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)_ ================================================ FILE: CHANGELOG.md ================================================ # Change Log ## [v1.32.4](https://github.com/vscodevim/vim/tree/v1.32.4) (2025-12-14) ### Fixed - Improved undo behavior after document is changed by an external process ([@J-Fields](https://github.com/J-Fields)). - Fixed spurious `Already at oldest change` message ([@J-Fields](https://github.com/J-Fields)). ## [v1.32.3](https://github.com/vscodevim/vim/tree/v1.32.3) (2025-12-10) ### Fixed - Fixed jump tracking when document is changed by an external process ([@J-Fields](https://github.com/J-Fields)). - Fixed `<` and `>` marks after backward selection in Visual mode ([@paakmau](https://github.com/paakmau)). ## [v1.32.2](https://github.com/vscodevim/vim/tree/v1.32.2) (2025-11-30) ### Fixed - Improved multi-cursor support ([@J-Fields](https://github.com/J-Fields)). - `j` and `k` no longer force cursors to same column. - Fix recording and executing macros with multi-cursor. - `u` no longer cancels multi-cursor. - Fixed several actions in VisualBlock mode. - Fixed several actions incorrectly being executed once per cursor. - Fix several bugs in `:bd[elete]` ([@J-Fields](https://github.com/J-Fields)). - `bd!` suppresses unsaved changes warning. - `bd {bufname}` closes the specified buffer. - Fixed `bd {N}` with high index. - Better error handling. - Fix cursor rendering when `cursorStylePerMode.visual` is set ([@jheroy](https://github.com/jheroy)). - Fix an infinite loop ([@J-Fields](https://github.com/J-Fields)). ## [v1.32.1](https://github.com/vscodevim/vim/tree/v1.32.1) (2025-11-9) ### Fixed - Fixed clipboard register, which was broken in `v1.32.0` ([@J-Fields](https://github.com/J-Fields)). ## [v1.32.0](https://github.com/vscodevim/vim/tree/v1.32.0) (2025-11-8) ### Added - Improved expression support ([@J-Fields](https://github.com/J-Fields)). - `:let` can now set registers and environment variables ([@J-Fields](https://github.com/J-Fields)). - `:s[ubstitute]` can now replace instances with the value of an expression ([@J-Fields](https://github.com/J-Fields)). ### Fixed - Fixed `:qa!` prompting about unsaved changes ([@hirokiokada77](https://github.com/hirokiokada77)). - Fixed Ex commands being repeatable with `.` ([@coxxny](https://github.com/coxxny)). - Fixed bug that would freeze editor if `.` was pressed twice after a non-repeatable action at startup ([@coxxny](https://github.com/coxxny)). - Fixed some motions like `[{` when executed with a high count ([@brasswood](https://github.com/brasswood)). - Fixed `j` and `k` with multiple cursors and `vim.foldfix` enabled ([@dandn9](https://github.com/dandn9)). - Fixed `showTextDocument` being executed for non-preview tabs on `:w[rite]` ([@mccheesy](https://github.com/mccheesy)). - Fixed various Ex command error messages ([@J-Fields](https://github.com/J-Fields)). ## [v1.31.0](https://github.com/vscodevim/vim/tree/v1.31.0) (2025-10-5) ### Added - Added `:gr[ep]` and `:vim[grep]` ([@AzimovParviz](https://github.com/AzimovParviz)). - Added `:c[hange]` ([@kiing-dom](https://github.com/kiing-dom)). - Added better expression support ([@J-Fields](https://github.com/J-Fields)). - Added unpacking, indexing, and slicing with `:let` ([@J-Fields](https://github.com/J-Fields)). - Added `:unl[et]` ([@J-Fields](https://github.com/J-Fields)). ### Changed - `:w` now disables preview for the saved editor, like VS Code's native `workbench.action.files.save` ([@mangas](https://github.com/mangas)). ### Fixed - Fixed significant delay after confirming IME input ([@s-kai273](https://github.com/s-kai273)). - Fixed `` not closing quick diff ([@jacklee1792](https://github.com/jacklee1792)). - Fixed `Vi{` incorrectly including the ending braces ([@Whiskas101](https://github.com/Whiskas101)). - Fixed small delete register (`"-`) not being updated while recording a macro ([@J-Fields](https://github.com/J-Fields)). ## [v1.30.1](https://github.com/vscodevim/vim/tree/v1.30.1) (2025-05-28) ### Added - Added a few character classes to Vim regexes ([@J-Fields](https://github.com/@J-Fields)). ### Fixed - Fixed an issue where the cursor would jump away after some VS Code navigation commands ([@alythobani](https://github.com/@alythobani)). - Fixed surround with tags containing non-word characters ([@robertmoura](https://github.com/@robertmoura)). - Fixed a performance issue when typing `(` or similar in large files ([@J-Fields](https://github.com/@J-Fields)). ## [v1.30.0](https://github.com/vscodevim/vim/tree/v1.30.0) (2025-05-22) ### Added - Added `:pw[d]` ([@zeevoffen](https://github.com/zeevoffen)). - Added `:ma[rk]` ([@arunchaganty](https://github.com/arunchaganty)). ### Changed - Enabled emulated Vim plugins in web extension ([@joshuali925](https://github.com/joshuali925)). - `gf` now interprets relative paths as relative to current file. If that fails, it tries relative to workspace root ([@J-Fields](https://github.com/J-Fields)). ### Fixed - Fixed Python function motions `[m`/`]m` with `async def` ([@nathan-gage](https://github.com/nathan-gage)). - Fixed `o` in Visual mode when selection starts at line end ([@kajikentaro](https://github.com/kajikentaro)). - Fixed `i(` when cursor is between two pairs of parentheses ([@prakhargupta-jan](https://github.com/prakhargupta-jan)). - Fixed global marks jumping to the wrong position ([@NeedsSoySauce](https://github.com/NeedsSoySauce)). - Fixed global marks messing up your position in the file you came from ([@J-Fields](https://github.com/J-Fields)). - Fixed `gf` with paths containing `..` ([@ekinakkaya](https://github.com/ekinakkaya)). - Fixed cursor position after `:ju[mps]` and `breakl[ist]` ([@J-Fields](https://github.com/J-Fields)) - Fixed jumps going to right file, but wrong line ([@J-Fields](https://github.com/J-Fields)). - Fixed `:norm[al]` with a double quote (`"`) in the argument ([@s-kai273](https://github.com/s-kai273)). ## [v1.29.2](https://github.com/vscodevim/vim/tree/v1.29.2) (2025-05-16) ### Fixed - Revert make tab and escape fix for native vscode keybindings ([@ulugbekna](https://github.com/ulugbekna)). - Dismiss inline suggestion/NES on Escape without interfering with Vim modes ([@ulugbekna](https://github.com/ulugbekna)). ## [v1.29.1](https://github.com/vscodevim/vim/tree/v1.29.1) (2025-05-15) ### Fixed - fix(keybindings): make tab and escape play nicer with native vscode keybindings ([@ulugbekna](https://github.com/ulugbekna)). ## [v1.29.0](https://github.com/vscodevim/vim/tree/v1.29.0) (2024-12-04) ### Added - Support `langmap` ([@Opisek](https://github.com/Opisek)). - Partial support for expressions, `:let`, and `:ec[ho]` ([@J-Fields](https://github.com/J-Fields)). ## [v1.28.1](https://github.com/vscodevim/vim/tree/v1.28.1) (2024-09-07) ### Fixed - Fixed `h` with unicode surrogate pairs ([@semicube](https://github.com/semicube)). ## [v1.28.0](https://github.com/vscodevim/vim/tree/v1.28.0) (2024-08-25) ### Added - Support `:norm[al]` ([@s-kai273](https://github.com/s-kai273)). - Status item click mapped to `toggleVim` command ([@JoeyShapiro](https://github.com/JoeyShapiro)). - Set `vim.command` in `VSCodeContext` on key press ([@raineorshine](https://github.com/raineorshine)). - Support `:b[uffer] {bufname}` ([@ccassise](https://github.com/ccassise)). - Support `` in Replace mode ([@s-kai273](https://github.com/s-kai273)). ### Changed - `:on[ly]` joins groups but does not close editors ([@kopiczko](https://github.com/kopiczko)). ### Fixed - Fixed split pane (`v` and `s`) on VS Code 1.90 ([@HenryTSZ](https://github.com/HenryTSZ)). - Fixed `:tabe[dit]` with relative path ([@iblislin](https://github.com/iblislin)). - Fixed `easymotionDimColor` ([@HenryTSZ](https://github.com/HenryTSZ)). - Fixed escaping multiple forward slashes in `visualstar` search ([@zaneduffield](https://github.com/zaneduffield)). - Fixed `.` with numbered registers ([@SirTomme](https://github.com/SirTomme)). - Fixed `H` and `L` not respecting `scrolloff` ([@rpuhalovich](https://github.com/rpuhalovich)). - Fixed surround with HTML tag attributes ([@Nestastnikos](https://github.com/Nestastnikos)). - Fixed deleting unicode surrogate pairs ([@semicube](https://github.com/semicube)). - Fixed repeating Ex commands with `` in them ([@HenryTSZ](https://github.com/HenryTSZ)). ## [v1.27.3](https://github.com/vscodevim/vim/tree/v1.27.3) (2024-05-20) ### Added - Custom digraphs can be added via `:dig[raphs]` ([@J-Fields](https://github.com/J-Fields)). - `:Ex[plore]` is mapped to `workbench.view.explorer` ([@JaiminBrahmbhatt](https://github.com/JaiminBrahmbhatt)). ### Changed - When used with a count, `` and `` set the `scroll` option to the count ([@ontanj](https://github.com/ontanj)). ### Fixed - Fix `:s[ubstitute]` with the `n` flag moving cursor ([@J-Fields](https://github.com/J-Fields)). - Fix special marks displaying in gutter ([@shinohara-rin](https://github.com/shinohara-rin)). - Fix several edge cases of `` ([@harunou](https://github.com/harunou)). - Fix incorrect digraph mappings ([@mlbykn](https://github.com/mlbykn)). - Fix `gv` after visual selection with mouse or command ([@zyd2001](https://github.com/zyd2001)). - Fix `gv` being unaffected by `m<` and `m>` ([@J-Fields](https://github.com/J-Fields)). ## [v1.27.2](https://github.com/vscodevim/vim/tree/v1.27.2) (2023-12-22) ### Added - Map `:ls` to `workbench.action.quickOpenLeastRecentlyUsedEditorInGroup` ([@riyuejiuzhao](https://github.com/riyuejiuzhao)). ### Fixed - Fix scrolling when `editor.smoothScrolling` is enabled ([@zhuowei](https://github.com/zhuowei)). - Fix cursor movement when `vim.foldfix` is enabled ([@HenryTSZ](https://github.com/HenryTSZ)). - Fix `editor.action.smartSelection.expand` command in VisualLine mode ([@rogeryk](https://github.com/rogeryk)). ## [v1.27.1](https://github.com/vscodevim/vim/tree/v1.27.1) (2023-11-22) ### Fixed - Fix `:sp[lit]` and `:vs[plit]` with no file name ([@bcho](https://github.com/bcho)). ## [v1.27.0](https://github.com/vscodevim/vim/tree/v1.27.0) (2023-11-17) ### Added - Allow `:sp[lit]` and `:vs[plit]` to open non-existing files ([@JLMSC](https://github.com/JLMSC)). - Support changing case via `:s[ubstitute]` with `\L`, `\U`, `\E`, `\u`, and `\l` ([@J-Fields](https://github.com/J-Fields)). - Add border to search and `:s[ubstitute]` decorations, based on the `editor.findMatchBorder` and `editor.findMatchHighlightBorder` ThemeColors ([@bryclee](https://github.com/bryclee)). ### Fixed - Make `gf` interpret non-absolute paths as relative to project root ([@Foo-x](https://github.com/Foo-x)). - Fix `gf` with a line number after the path ([@Foo-x](https://github.com/Foo-x)). - Fix status bar color in VisualLine mode ([@chandradeepdey](https://github.com/chandradeepdey)). ## [v1.26.2](https://github.com/vscodevim/vim/tree/v1.26.2) (2023-10-21) ### Fixed - Fixed illegible text with certain color schemes when `vim.statusBarColorControl` is enabled ([@chandradeepdey](https://github.com/chandradeepdey)). ### Changed - Changed extension's `activationEvents` to include `onStartupFinished` rather than `*`, which may improve startup performance ([@whitphx](https://github.com/whitphx)). ## [v1.26.1](https://github.com/vscodevim/vim/tree/v1.26.1) (2023-10-09) ### Fixed - Fixed several Insert mode bugs caused by a regression in `v1.26.0` ([@nullbus](https://github.com/nullbus)). - Fixed dot repeat (`.`) after `:reg[isters]` ([@dannoe](https://github.com/dannoe)). - Fixed overlapping text in Quick Pick caused by `:reg[isters]` ([@dannoe](https://github.com/dannoe)). - Fixed some uses of `vim.remap` ([@jdanbrown](https://github.com/jdanbrown)). ## [v1.26.0](https://github.com/vscodevim/vim/tree/v1.26.0) (2023-09-09) ### Added - Implemented `:m[ove]` ([@zhanyi22333](https://github.com/zhanyi22333)). - Implemented `:red[o]` ([@hamza-tam](https://github.com/hamza-tam)). - Implemented `:pu[t] =` ([@elazarcoh](https://github.com/elazarcoh)). ### Fixed - Fixed misbehavior when selecting from bottom to top with shift+click ([@lqqyt2423](https://github.com/lqqyt2423)). - Fixed `@@` when used in a different editor ([@J-Fields](https://github.com/J-Fields)). - Fixed race condition in the `c` operator and a few other actions when `vim.autoSwitchInputMethod` is enabled ([@listenerri](https://github.com/listenerri)). - Fixed `when` clause for provided `` key bind to enable it being remapped ([@grosssoftware](https://github.com/grosssoftware)). - Fixed `:sp[lit] [file]` ([@fernando-gap](https://github.com/fernando-gap)). - Fixed `` key bind which was blocking menu navigation in VS Code's find dialog ([@devrelm](https://github.com/devrelm)). - Fixed VSCode's auto-surround functionality in Insert mode ([@Elliot-Roberts](https://github.com/Elliot-Roberts)). - Fixed `` and `` not respecting `[count]` ([@rpuhalovich](https://github.com/rpuhalovich)). ## [v1.25.2](https://github.com/vscodevim/vim/tree/v1.25.2) (2023-03-01) ### Added - Support for `:w ` ([@JLMSC](https://github.com/JLMSC)). ### Changed - Reduced extension bundle size by removing source maps ([@kidonng](https://github.com/kidonng)). - Replaced "Report bug" popup on exceptions with an error log message ([@J-Fields](https://github.com/J-Fields)). ### Fixed - Fixed remaps which pass multiple positional arguments to a command ([@elmar-peise](https://github.com/elmar-peise)). - Fixed cursor position after certain surround actions ([@J-Fields](https://github.com/J-Fields)). ## [v1.25.0](https://github.com/vscodevim/vim/tree/v1.25.0) (2023-02-28) ### Added - Support for `:cw[indow]`, `:lw[indow]`, and related commands ([@mogelbrod](https://github.com/mogelbrod)). ### Changed - Logging is now done to a `LogOutputChannel`. It can be accessed in the `Output` panel and configured using `workbench.action.setLogLevel` ([@J-Fields](https://github.com/J-Fields)). - Scope for settings under `vim.autoSwitchInputMethod.*` is now `machine` ([@Quanuanc](https://github.com/Quanuanc)). ### Fixed - Fixed undo/redo after recent VS Code update ([@J-Fields](https://github.com/J-Fields)). - Fixed `.` after exiting Visual mode or command line with `` ([@wgr45097](https://github.com/wgr45097)). - Fixed ex command line ranges with no explicit start, such as `,5` ([@lazygyu](https://github.com/lazygyu)). ## [v1.24.3](https://github.com/vscodevim/vim/tree/v1.24.3) (2022-11-06) ### Added - Text registers can now be executed as macros with `@` ([@elazarcoh](https://github.com/elazarcoh)). ### Fixed - Fixed some ex commands when repeated with `@:` ([@J-Fields](https://github.com/J-Fields)). - Fixed cursor position after `gp` or `gP` in VisualBlock mode ([@burnsdy](https://github.com/burnsdy)). - Fixed edge case of `i{` and `a{` ([@elazarcoh](https://github.com/elazarcoh)). ## [v1.24.2](https://github.com/vscodevim/vim/tree/v1.24.2) (2022-10-29) ### Added - Support for the `'scrolloff'` option, which is mapped to VS Code's `editor.cursorSurroundingLines` setting ([@LinHeLurking](https://github.com/LinHeLurking)). ### Fixed - Fixed indent (`>`) and outdent (`<`) in VisualBlock mode ([@burnsdy](https://github.com/burnsdy)). - Fixed `cW` when the cursor is on the last character of a word ([@wgr45097](https://github.com/wgr45097)). - Fixed indent textobjects (`ii`, `ai`, and `aI`) in VisualLine mode ([@mogelbrod](https://github.com/mogelbrod)). ## [v1.24.1](https://github.com/vscodevim/vim/tree/v1.24.1) (2022-09-26) ### Fixed - Fixed `gt` and `gT` ([@J-Fields](https://github.com/J-Fields)). ## [v1.24.0](https://github.com/vscodevim/vim/tree/v1.24.0) (2022-09-26) ### Added - Support for `zf`/`zd` commands, which fold/unfold arbitrary ranges ([@elazarcoh](https://github.com/elazarcoh)). - Support for surrounding with function call ([@riccardofano](https://github.com/riccardofano)). - Support for `:sor[t] n`, which sorts lines numerically, rather than lexicographically ([@jan25](https://github.com/jan25)). ### Changed - `P` in Visual modes no longer overwrites the default register with the selection's contents ([@J-Fields](https://github.com/J-Fields)). - Yanking block-wise now pads shorter lines with spaces ([@burnsdy](https://github.com/burnsdy)). - `` now goes to definition, not declaration ([@J-Fields](https://github.com/J-Fields)). - `:tabn[ext] {N}` now goes to the Nth tab, not N tabs forward [@elazarcoh](https://github.com/elazarcoh). ### Fixed - Fixed insertion of surrogate pairs, like emoji 🙂 ([@garzj](https://github.com/garzj)). - Fixed `` and `` when cursor is at start of command line ([@J-Fields](https://github.com/J-Fields)). ## [v1.23.2](https://github.com/vscodevim/vim/tree/v1.23.2) (2022-08-01) ### Fixed - Fix the jump list ([@pitkali](https://github.com/pitkali)). - Make increment/decrement (`` and ``) preserve case of hex numbers ([@smallkirby](https://github.com/smallkirby)). - Fix search highlights on inactive but visible editors ([@J-Fields](https://github.com/J-Fields)). ## [v1.23.1](https://github.com/vscodevim/vim/tree/v1.23.1) (2022-06-28) ### Fixed - Fold commands such as `zo` and `zc` no longer throw error ([@J-Fields](https://github.com/J-Fields)). - Fix regression in VisualBlock `c` ([@J-Fields](https://github.com/J-Fields)). ## [v1.23.0](https://github.com/vscodevim/vim/tree/v1.23.0) (2022-06-27) ### Added - Partial implementation of the [targets.vim](https://github.com/wellle/targets.vim#quote-text-objects) plugin ([@elazarcoh](https://github.com/elazarcoh)). - See the configuration available under `vim.targets`. - Support for `:breaka[dd]`, `:breakd[el]`, and `:breakl[ist]` ([@elazarcoh](https://github.com/elazarcoh)). - Support for `:ret[ab]` ([@ivanmaeder](https://github.com/ivanmaeder)). - Support for `:<` and `:>` ([@J-Fields](https://github.com/J-Fields)). - In the replacement string of `:s[ubstitute]`, `~` stands for previous replace string ([@J-Fields](https://github.com/J-Fields)). ### Changed - Searches now abort after ~1 second, rather than after finding 1000 matches ([@elazarcoh](https://github.com/elazarcoh)). - In the replacement string of `:s[ubstitute]`, `&` (rather than `\&`) stands for entire matched text ([@J-Fields](https://github.com/J-Fields)). - The `vim.textwidth` configuration option can now be set per-language ([@BlakeWilliams](https://github.com/BlakeWilliams)). ### Fixed - Allow space in ex command file names if preceded by a backslash ([@J-Fields](https://github.com/J-Fields)). - Fix Replace mode with multiple cursors ([@J-Fields](https://github.com/J-Fields)). - Fix `` and `` (scroll view) with multiple cursors ([@J-Fields](https://github.com/J-Fields)). - Fix `` (go to alternate file) ([@J-Fields](https://github.com/J-Fields)). - Fix the `CamelCaseMotion` plugin ([@rcywongaa](https://github.com/rcywongaa)) - Fix behavior around surrogate pairs ([@smallkirby](https://github.com/smallkirby)). - Fix `:delm[arks]` ([@chewcw](https://github.com/chewcw)). - Fix `` in Insert mode when tabs are used ([@J-Fields](https://github.com/J-Fields)). - Fix `c` in VisualBlock mode when block extends over short lines ([@J-Fields](https://github.com/J-Fields)). - Make `c`, `s`, and `D` in VisualBlock mode copy to register ([@monjara](https://github.com/monjara)). - Fix several edge cases of `gv` ([@J-Fields](https://github.com/J-Fields)). - Fix `p` in Visual modes with multiple cursors ([@joel1st](https://github.com/joel1st)). - Improve search performance ([@J-Fields](https://github.com/J-Fields)). ### Removed - Remove the following deprecated and unused configuration ([@J-Fields](https://github.com/J-Fields)): - `vim.easymotionMarkerForegroundColorTwoChar` - `vim.easymotionMarkerWidthPerChar` - `vim.easymotionMarkerFontFamily` - `vim.easymotionMarkerFontSize` - `vim.easymotionMarkerMargin` ## **_For versions older than 1.23.0, please see [CHANGELOG.OLD.md](CHANGELOG.OLD.md)_** ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Our Standards Be nice. Please. Everybody contributing to open source contributes out of good will in their own free time. ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 VSCodeVim Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================


VSCodeVim

Vim emulation for Visual Studio Code

VSCodeVim is a Vim emulator for [Visual Studio Code](https://code.visualstudio.com/). - 📃 Our [change log](CHANGELOG.md) outlines the breaking/major/minor updates between releases. - Report missing features/bugs on [GitHub](https://github.com/VSCodeVim/Vim/issues).
Table of Contents (click to expand) - [💾 Installation](#-installation) - [Mac](#mac) - [Windows](#windows) - [⚙️ Settings](#️-settings) - [Quick Example](#quick-example) - [VSCodeVim settings](#vscodevim-settings) - [Neovim Integration](#neovim-integration) - [Key Remapping](#key-remapping) - [`"vim.insertModeKeyBindings"`/`"vim.normalModeKeyBindings"`/`"vim.visualModeKeyBindings"`/`"vim.operatorPendingModeKeyBindings"`](#viminsertmodekeybindingsvimnormalmodekeybindingsvimvisualmodekeybindingsvimoperatorpendingmodekeybindings) - [`"vim.insertModeKeyBindingsNonRecursive"`/`"normalModeKeyBindingsNonRecursive"`/`"visualModeKeyBindingsNonRecursive"`/`"operatorPendingModeKeyBindingsNonRecursive"`](#viminsertmodekeybindingsnonrecursivenormalmodekeybindingsnonrecursivevisualmodekeybindingsnonrecursiveoperatorpendingmodekeybindingsnonrecursive) - [Debugging Remappings](#debugging-remappings) - [Remapping more complex key combinations](#remapping-more-complex-key-combinations) - [Vim modes](#vim-modes) - [Vim settings](#vim-settings) - [.vimrc support](#vimrc-support) - [🖱️ Multi-Cursor Mode](#️-multi-cursor-mode) - [🔌 Emulated Plugins](#-emulated-plugins) - [vim-airline](#vim-airline) - [vim-easymotion](#vim-easymotion) - [vim-surround](#vim-surround) - [vim-commentary](#vim-commentary) - [vim-indent-object](#vim-indent-object) - [vim-sneak](#vim-sneak) - [CamelCaseMotion](#camelcasemotion) - [Input Method](#input-method) - [ReplaceWithRegister](#replacewithregister) - [vim-textobj-entire](#vim-textobj-entire) - [vim-textobj-arguments](#vim-textobj-arguments) - [🎩 VSCodeVim tricks!](#-vscodevim-tricks) - [📚 F.A.Q.](#-faq) - [❤️ Contributing](#️-contributing) - [Special shoutouts to:](#special-shoutouts-to)
## 💾 Installation VSCodeVim can be installed via the VS Code [Marketplace](https://marketplace.visualstudio.com/items?itemName=vscodevim.vim) or the OpenVSX [Marketplace](https://open-vsx.org/extension/vscodevim/vim). ### Mac To enable key-repeating, execute the following in your Terminal, log out and back in, and then restart VS Code: ```sh defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false # For VS Code defaults write com.microsoft.VSCodeInsiders ApplePressAndHoldEnabled -bool false # For VS Code Insider defaults write com.vscodium ApplePressAndHoldEnabled -bool false # For VS Codium defaults write com.microsoft.VSCodeExploration ApplePressAndHoldEnabled -bool false # For VS Codium Exploration users defaults write com.exafunction.windsurf ApplePressAndHoldEnabled -bool false # For Windsurf defaults delete -g ApplePressAndHoldEnabled # If necessary, reset global default ``` We also recommend increasing Key Repeat and Delay Until Repeat settings in _System Settings/Preferences -> Keyboard_. ### Windows Like real vim, VSCodeVim will take over your control keys. This behavior can be adjusted with the [`useCtrlKeys`](#vscodevim-settings) and [`handleKeys`](#vscodevim-settings) settings. ## ⚙️ Settings The settings documented here are a subset of the supported settings; the full list is described in the `FEATURES` -> `Settings` tab of VSCodeVim's [extension details page](https://code.visualstudio.com/docs/editor/extension-marketplace#_extension-details), which can be found in the [extensions view](https://code.visualstudio.com/docs/editor/extension-marketplace) of VS Code. ### Quick Example Below is an example of a [settings.json](https://code.visualstudio.com/Docs/customization/userandworkspace) file with settings relevant to VSCodeVim: ```json { "vim.easymotion": true, "vim.incsearch": true, "vim.useSystemClipboard": true, "vim.useCtrlKeys": true, "vim.hlsearch": true, "vim.insertModeKeyBindings": [ { "before": ["j", "j"], "after": [""] } ], "vim.normalModeKeyBindingsNonRecursive": [ { "before": ["", "d"], "after": ["d", "d"] }, { "before": [""], "commands": [":nohl"] }, { "before": ["K"], "commands": ["lineBreakInsert"], "silent": true } ], "vim.leader": "", "vim.handleKeys": { "": false, "": false }, // To improve performance "extensions.experimental.affinity": { "vscodevim.vim": 1 } } ``` ### VSCodeVim settings These settings are specific to VSCodeVim. | Setting | Description | Type | Default Value | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------- | | vim.changeWordIncludesWhitespace | Include trailing whitespace when changing word. This configures the cw action to act consistently as its siblings (yw and dw) instead of acting as ce. | Boolean | false | | vim.cursorStylePerMode._{Mode}_ | Configure a specific cursor style for _{Mode}_. Omitted modes will use [default cursor type](https://github.com/VSCodeVim/Vim/blob/4a6fde6dbd4d1fac1f204c0dc27c32883651ef1a/src/mode/mode.ts#L34) Supported cursors: line, block, underline, line-thin, block-outline, and underline-thin. | String | None | | vim.digraphs._{shorthand}_ | Set custom digraph shorthands that can override the default ones. Entries should map a two-character shorthand to a descriptive string and one or more UTF16 code points. Example: `"R!": ["🚀", [55357, 56960]]` | Object | `{"R!": ["🚀", [0xD83D, 0xDE80]]` | | vim.disableExtension | Disable VSCodeVim extension. This setting can also be toggled using `toggleVim` command in the Command Palette | Boolean | false | | vim.handleKeys | Delegate configured keys to be handled by VS Code instead of by the VSCodeVim extension. Any key in `keybindings` section of the [package.json](https://github.com/VSCodeVim/Vim/blob/master/package.json) that has a `vim.use` in the `when` argument can be delegated back to VS Code by setting `"": false`. Example: to use `ctrl+f` for find (native VS Code behavior): `"vim.handleKeys": { "": false }`. | String | `"": true`
`"": false`
`"": false` | | vim.overrideCopy | Override VS Code's copy command with our own, which works correctly with VSCodeVim. If cmd-c/ctrl-c is giving you issues, set this to false and complain [here](https://github.com/Microsoft/vscode/issues/217). | Boolean | false | | vim.useSystemClipboard | Use the system clipboard register (`*`) as the default register | Boolean | false | | vim.searchHighlightColor | Background color of non-current search matches | String | `findMatchHighlightBackground` ThemeColor | | vim.searchHighlightTextColor | Foreground color of non-current search matches | String | None | | vim.searchMatchColor | Background color of current search match | String | `findMatchBackground` ThemeColor | | vim.searchMatchTextColor | Foreground color of current search match | String | None | | vim.substitutionColor | Background color of substitution text when `vim.inccommand` is enabled | String | "#50f01080" | | vim.substitutionTextColor | Foreground color of substitution text when `vim.inccommand` is enabled | String | None | | vim.startInInsertMode | Start in Insert mode instead of Normal Mode | Boolean | false | | vim.useCtrlKeys | Enable Vim ctrl keys overriding common VS Code operations such as copy, paste, find, etc. | Boolean | true | | vim.visualstar | In visual mode, start a search with `*` or `#` using the current selection | Boolean | false | | vim.highlightedyank.enable | Enable highlighting when yanking | Boolean | false | | vim.highlightedyank.color | Set the color of yank highlights | String | rgba(250, 240, 170, 0.5) | | vim.highlightedyank.duration | Set the duration of yank highlights | Number | 200 | ### Neovim Integration > :warning: Experimental feature. Please leave feedback on neovim integration [here](https://github.com/VSCodeVim/Vim/issues/1735). To leverage neovim for Ex-commands, 1. Install [neovim](https://github.com/neovim/neovim/wiki/Installing-Neovim) 2. Modify the following configurations: | Setting | Description | Type | Default Value | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | ------------- | | vim.enableNeovim | Enable Neovim | Boolean | false | | vim.neovimPath | Full path to neovim executable. If left empty, PATH environment variable will be automatically checked for neovim path. | String | | | vim.neovimUseConfigFile | If `true`, Neovim will load a config file specified by `vim.neovimConfigPath`. This is necessary if you want Neovim to be able to use its own plugins. | Boolean | false | | vim.neovimConfigPath | Path that Neovim will load as config file. If left blank, Neovim will search in its default location. | String | | Here's some ideas on what you can do with neovim integration: - [The power of g](http://vim.wikia.com/wiki/Power_of_g) - [The :normal command](https://vi.stackexchange.com/questions/4418/execute-normal-command-over-range) - Faster search and replace! ### Key Remapping Custom remappings are defined on a per-mode basis. #### `"vim.insertModeKeyBindings"`/`"vim.normalModeKeyBindings"`/`"vim.visualModeKeyBindings"`/`"vim.operatorPendingModeKeyBindings"` - Keybinding overrides to use for insert, normal, operatorPending and visual modes. - Keybinding overrides can include `"before"`, `"after"`, `"commands"`, and `"silent"`. - Bind `jj` to `` in insert mode: ```json "vim.insertModeKeyBindings": [ { "before": ["j", "j"], "after": [""] } ] ``` - Bind `£` to goto previous whole word under cursor: ```json "vim.normalModeKeyBindings": [ { "before": ["£"], "after": ["#"] } ] ``` - Bind `:` to show the command palette, and don't show the message on the status bar: ```json "vim.normalModeKeyBindings": [ { "before": [":"], "commands": [ "workbench.action.showCommands", ], "silent": true } ] ``` - Bind `m` to add a bookmark and `b` to open the list of all bookmarks (using the [Bookmarks](https://marketplace.visualstudio.com/items?itemName=alefragnani.Bookmarks) extension): ```json "vim.normalModeKeyBindings": [ { "before": ["", "m"], "commands": [ "bookmarks.toggle" ] }, { "before": ["", "b"], "commands": [ "bookmarks.list" ] } ] ``` - Bind `ctrl+n` to turn off search highlighting and `w` to save the current file: ```json "vim.normalModeKeyBindings": [ { "before":[""], "commands": [ ":nohl", ] }, { "before": ["leader", "w"], "commands": [ "workbench.action.files.save", ] } ] ``` - Bind `{` to `w` in operator pending mode makes `y{` and `d{` work like `yw` and `dw` respectively: ```json "vim.operatorPendingModeKeyBindings": [ { "before": ["{"], "after": ["w"] } ] ``` - Bind `L` to `$` and `H` to `^` in operator pending mode makes `yL` and `dH` work like `y$` and `d^` respectively: ```json "vim.operatorPendingModeKeyBindings": [ { "before": ["L"], "after": ["$"] }, { "before": ["H"], "after": ["^"] } ] ``` - Bind `>` and `<` in visual mode to indent/outdent lines (repeatable): ```json "vim.visualModeKeyBindings": [ { "before": [ ">" ], "commands": [ "editor.action.indentLines" ] }, { "before": [ "<" ], "commands": [ "editor.action.outdentLines" ] }, ] ``` - Bind `vim` to clone this repository to the selected location: ```json "vim.visualModeKeyBindings": [ { "before": [ "", "v", "i", "m" ], "commands": [ { "command": "git.clone", "args": [ "https://github.com/VSCodeVim/Vim.git" ] } ] } ] ``` #### `"vim.insertModeKeyBindingsNonRecursive"`/`"normalModeKeyBindingsNonRecursive"`/`"visualModeKeyBindingsNonRecursive"`/`"operatorPendingModeKeyBindingsNonRecursive"` - Non-recursive keybinding overrides to use for insert, normal, and visual modes - _Example:_ Exchange the meaning of two keys like `j` to `k` and `k` to `j` to exchange the cursor up and down commands. Notice that if you attempted this binding normally, the `j` would be replaced with `k` and the `k` would be replaced with `j`, on and on forever. When this happens 'maxmapdepth' times (default 1000) the error message 'E223 Recursive Mapping' will be thrown. Stop this recursive expansion using the NonRecursive variation of the keybindings: ```json "vim.normalModeKeyBindingsNonRecursive": [ { "before": ["j"], "after": ["k"] }, { "before": ["k"], "after": ["j"] } ] ``` - Bind `(` to 'i(' in operator pending mode makes 'y(' and 'c(' work like 'yi(' and 'ci(' respectively: ```json "vim.operatorPendingModeKeyBindingsNonRecursive": [ { "before": ["("], "after": ["i("] } ] ``` - Bind `p` in visual mode to paste without overriding the current register: ```json "vim.visualModeKeyBindingsNonRecursive": [ { "before": [ "p", ], "after": [ "p", "g", "v", "y" ] } ], ``` #### Debugging Remappings 1. Adjust the extension's logging level to 'debug' and open the Output window: 1. Run `Developer: Set Log Level` from the command palette. 2. Select `Vim`, then `Debug` 3. Run `Developer: Reload window` 4. In the bottom panel, open the `Output` tab and select `Vim` from the dropdown selection. 2. Are your configurations correct? As each remapped configuration is loaded, it is logged to the Vim Output panel. Do you see any errors? ```console debug: Remapper: normalModeKeyBindingsNonRecursive. before=0. after=^. debug: Remapper: insertModeKeyBindings. before=j,j. after=. error: Remapper: insertModeKeyBindings. Invalid configuration. Missing 'after' key or 'commands'. before=j,k. ``` Misconfigured configurations are ignored. 3. Does the extension handle the keys you are trying to remap? VSCodeVim explicitly instructs VS Code which key events we care about through the [package.json](https://github.com/VSCodeVim/Vim/blob/9bab33c75d0a53873880a79c5d2de41c8be1bef9/package.json#L62). If the key you are trying to remap is a key in which vim/vscodevim generally does not handle, then it's most likely that this extension does not receive those key events from VS Code. In the Vim Output panel, you should see: ```console debug: ModeHandler: handling key=A. debug: ModeHandler: handling key=l. debug: ModeHandler: handling key=. debug: ModeHandler: handling key=. ``` As you press the key that you are trying to remap, do you see it outputted here? If not, it means we don't subscribe to those key events. It is still possible to remap those keys by using VSCode's [keybindings.json](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference) (see next section: [Remapping more complex key combinations](#remapping-more-complex-key-combinations)). #### Remapping more complex key combinations It is highly recommended to remap keys using vim commands like `"vim.normalModeKeyBindings"` ([see here](#key-remapping)). But sometimes the usual remapping commands are not enough as they do not support every key combinations possible (for example `Alt+key` or `Ctrl+Shift+key`). In this case it is possible to create new keybindings inside [keybindings.json](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference). To do so: open up keybindings.json in VSCode using `CTRL+SHIFT+P` and select `Open keyboard shortcuts (JSON)`. You can then add a new entry to the keybindings like so: ```json { "key": "YOUR_KEY_COMBINATION", "command": "vim.remap", "when": "inputFocus && vim.mode == 'VIM_MODE_YOU_WANT_TO_REBIND'", "args": { "after": ["YOUR_VIM_ACTION"] } } ``` For example, to rebind `ctrl+shift+y` to VSCodeVim's `yy` (yank line) in normal mode, add this to your keybindings.json: ```json { "key": "ctrl+shift+y", "command": "vim.remap", "when": "inputFocus && vim.mode == 'Normal'", "args": { "after": ["y", "y"] } } ``` If keybindings.json is empty the first time you open it, make sure to add opening `[` and closing `]` square brackets to the file as the keybindings should be inside a JSON Array. ### Vim modes Here are all the modes used by VSCodeVim: | Mode | | --------------------- | | Normal | | Insert | | Visual | | VisualBlock | | VisualLine | | SearchInProgressMode | | CommandlineInProgress | | Replace | | EasyMotionMode | | EasyMotionInputMode | | SurroundInputMode | | OperatorPendingMode | | Disabled | When rebinding keys in [keybindings.json](https://code.visualstudio.com/docs/getstarted/keybindings) using ["when clause context"](https://code.visualstudio.com/api/references/when-clause-contexts), it can be useful to know in which mode vim currently is. For example to write a "when clause" that checks if vim is currently in normal mode or visual mode it is possible to write the following: ```json "when": "vim.mode == 'Normal' || vim.mode == 'Visual'", ``` ### Vim settings Configuration settings that have been copied from vim. Vim settings are loaded in the following sequence: 1. `:set {setting}` 2. `vim.{setting}` from user/workspace settings. 3. VS Code settings 4. VSCodeVim default values | Setting | Description | Type | Default Value | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------- | | vim.autoindent | Copy indent from current line when starting a new line | Boolean | true | | vim.gdefault | When on, the `:substitute` flag `g` is default on. This means that all matches in a line are substituted instead of one. When a `g` flag is given to a `:substitute` command, this will toggle the substitution of all or one match. | Boolean | false | | vim.hlsearch | Highlights all text matching current search | Boolean | false | | vim.ignorecase | Ignore case in search patterns | Boolean | true | | vim.incsearch | Show the next match while entering a search | Boolean | true | | vim.inccommand | Show the effects of the `:substitute` command while typing | String | `replace` | | vim.joinspaces | Add two spaces after '.', '?', and '!' when joining or reformatting | Boolean | true | | vim.leader | Defines key for `` to be used in key remappings | String | `\` | | vim.maxmapdepth | Maximum number of times a mapping is done without resulting in a character to be used. This normally catches endless mappings, like ":map x y" with ":map y x". It still does not catch ":map g wg", because the 'w' is used before the next mapping is done. | Number | 1000 | | vim.report | Threshold for reporting number of lines changed. | Number | 2 | | vim.shell | Path to the shell to use for `!` and `:!` commands. | String | `/bin/sh` on Unix, `%COMSPEC%` environment variable on Windows | | vim.showcmd | Show (partial) command in status bar | Boolean | true | | vim.showmodename | Show name of current mode in status bar | Boolean | true | | vim.smartcase | Override the 'ignorecase' setting if search pattern contains uppercase characters | Boolean | true | | vim.textwidth | Width to word-wrap when using `gq` | Number | 80 | | vim.timeout | Timeout in milliseconds for remapped commands | Number | 1000 | | vim.whichwrap | Allow specified keys that move the cursor left/right to move to the previous/next line when the cursor is on the first/last character in the line. See [:help whichwrap](https://vimhelp.org/options.txt.html#%27whichwrap%27). | String | `b,s` | ## .vimrc support > :warning: .vimrc support is currently experimental. Only remaps are supported, and you may experience bugs. Please [report them](https://github.com/VSCodeVim/Vim/issues/new?template=bug_report.md)! Set `vim.vimrc.enable` to `true` and set `vim.vimrc.path` appropriately. ## 🖱️ Multi-Cursor Mode > :warning: Multi-Cursor mode is experimental. Please report issues in our [feedback thread.](https://github.com/VSCodeVim/Vim/issues/824) Enter multi-cursor mode by: - On OSX, `cmd-d`. On Windows, `ctrl-d`. - `gb`, a new shortcut we added which is equivalent to `cmd-d` (OSX) or `ctrl-d` (Windows). It adds another cursor at the next word that matches the word the cursor is currently on. - Running "Add Cursor Above/Below" or the shortcut on any platform. Once you have multiple cursors, you should be able to use Vim commands as you see fit. Most should work; some are unsupported (ref [PR#587](https://github.com/VSCodeVim/Vim/pull/587)). - Each cursor has its own clipboard. - Pressing Escape in Multi-Cursor Visual Mode will bring you to Multi-Cursor Normal mode. Pressing it again will return you to Normal mode. ## 🔌 Emulated Plugins ### vim-airline > :warning: There are performance implications to using this plugin. In order to change the status bar, we override the configurations in your workspace settings.json which results in increased latency and a constant changing diff in your working directory (see [issue#2124](https://github.com/VSCodeVim/Vim/issues/2124)). Change the color of the status bar based on the current mode. Once enabled, configure `"vim.statusBarColors"`. Colors can be defined for each mode either as `string` (background only), or `string[]` (background, foreground). ```json "vim.statusBarColorControl": true, "vim.statusBarColors.normal": ["#8FBCBB", "#434C5E"], "vim.statusBarColors.insert": "#BF616A", "vim.statusBarColors.visual": "#B48EAD", "vim.statusBarColors.visualline": "#B48EAD", "vim.statusBarColors.visualblock": "#A3BE8C", "vim.statusBarColors.replace": "#D08770", "vim.statusBarColors.commandlineinprogress": "#007ACC", "vim.statusBarColors.searchinprogressmode": "#007ACC", "vim.statusBarColors.easymotionmode": "#007ACC", "vim.statusBarColors.easymotioninputmode": "#007ACC", "vim.statusBarColors.surroundinputmode": "#007ACC", ``` ### vim-easymotion Based on [vim-easymotion](https://github.com/easymotion/vim-easymotion) and configured through the following settings: | Setting | Description | Type | Default Value | | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------- | | vim.easymotion | Enable/disable easymotion plugin | Boolean | false | | vim.easymotionMarkerBackgroundColor | The background color of the marker box. | String | '#0000' | | vim.easymotionMarkerForegroundColorOneChar | The font color for one-character markers. | String | '#ff0000' | | vim.easymotionMarkerForegroundColorTwoCharFirst | The font color for the first of two-character markers, used to differentiate from one-character markers. | String | '#ffb400' | | vim.easymotionMarkerForegroundColorTwoCharSecond | The font color for the second of two-character markers, used to differentiate consecutive markers. | String | '#b98300' | | vim.easymotionIncSearchForegroundColor | The font color for the search n-character command, used to highlight the matches. | String | '#7fbf00' | | vim.easymotionDimColor | The font color for the dimmed characters, used when `#vim.easymotionDimBackground#` is set to true. | String | '#777777' | | vim.easymotionDimBackground | Whether to dim other text while markers are visible. | Boolean | true | | vim.easymotionMarkerFontWeight | The font weight used for the marker text. | String | 'bold' | | vim.easymotionKeys | The characters used for jump marker name | String | 'hklyuiopnm,qwertzxcvbasdgjf;' | | vim.easymotionJumpToAnywhereRegex | Custom regex to match for JumpToAnywhere motion (analogous to `Easymotion_re_anywhere`) | String | `\b[A-Za-z0-9]\|[A-Za-z0-9]\b\|_.\|#.\|[a-z][A-Z]` | Once easymotion is active, initiate motions using the following commands. After you initiate the motion, text decorators/markers will be displayed and you can press the keys displayed to jump to that position. `leader` is configurable and is `\` by default. | Motion Command | Description | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------- | | ` s ` | Search character | | ` f ` | Find character forwards | | ` F ` | Find character backwards | | ` t ` | Til character forwards | | ` T ` | Til character backwards | | ` w` | Start of word forwards | | ` b` | Start of word backwards | | ` l` | Matches beginning & ending of word, camelCase, after `_`, and after `#` forwards | | ` h` | Matches beginning & ending of word, camelCase, after `_`, and after `#` backwards | | ` e` | End of word forwards | | ` ge` | End of word backwards | | ` j` | Start of line forwards | | ` k` | Start of line backwards | | ` / ... ` | Search n-character | | ` bdt` | Til character | | ` bdw` | Start of word | | ` bde` | End of word | | ` bdjk` | Start of line | | ` j` | JumpToAnywhere motion; default behavior matches beginning & ending of word, camelCase, after `_` and after `#` | ` (2s|2f|2F|2t|2T) ` and ` bd2t char>` are also available. The difference is character count required for search. For example, ` 2s ` requires two characters, and search by two characters. This mapping is not a standard mapping, so it is recommended to use your custom mapping. ### vim-surround Based on [surround.vim](https://github.com/tpope/vim-surround), the plugin is used to work with surrounding characters like parentheses, brackets, quotes, and XML tags. | Setting | Description | Type | Default Value | | ------------ | --------------------------- | ------- | ------------- | | vim.surround | Enable/disable vim-surround | Boolean | true | `t` or `<` as `` or `` will enter tag entry mode. Using `` instead of `>` to finish changing a tag will preserve any existing attributes. | Surround Command | Description | | -------------------------- | -------------------------------------------------------- | | `y s ` | Add `desired` surround around text defined by `` | | `d s ` | Delete `existing` surround | | `c s ` | Change `existing` surround to `desired` | | `S ` | Surround when in visual modes (surrounds full selection) | Some examples: - `"test"` with cursor inside quotes type `cs"'` to end up with `'test'` - `"test"` with cursor inside quotes type `ds"` to end up with `test` - `"test"` with cursor inside quotes type `cs"t` and enter `123>` to end up with `<123>test` ### vim-commentary Similar to [vim-commentary](https://github.com/tpope/vim-commentary), but uses the VS Code native _Toggle Line Comment_ and _Toggle Block Comment_ features. Usage examples: - `gc` - toggles line comment. For example `gcc` to toggle line comment for current line and `gc2j` to toggle line comments for the current line and the next two lines. - `gC` - toggles block comment. For example `gCi)` to comment out everything within parentheses. ### vim-indent-object Based on [vim-indent-object](https://github.com/michaeljsmith/vim-indent-object), it allows for treating blocks of code at the current indentation level as text objects. Useful in languages that don't use braces around statements (e.g. Python). Provided there is a new line between the opening and closing braces / tag, it can be considered an agnostic `cib`/`ci{`/`ci[`/`cit`. | Command | Description | | -------------- | ---------------------------------------------------------------------------------------------------- | | `ii` | This indentation level | | `ai` | This indentation level and the line above (think `if` statements in Python) | | `aI` | This indentation level, the line above, and the line after (think `if` statements in C/C++/Java/etc) | ### vim-sneak Based on [vim-sneak](https://github.com/justinmk/vim-sneak), it allows for jumping to any location specified by two characters. | Setting | Description | Type | Default Value | | ---------------------------------- | ----------------------------------------------------------- | ------- | ------------- | | vim.sneak | Enable/disable vim-sneak | Boolean | false | | vim.sneakUseIgnorecaseAndSmartcase | Respect `vim.ignorecase` and `vim.smartcase` while sneaking | Boolean | false | Once sneak is active, initiate motions using the following commands. For operators sneak uses `z` instead of `s` because `s` is already taken by the surround plugin. | Motion Command | Description | | ------------------------- | ----------------------------------------------------------------------- | | `s` | Move forward to the first occurrence of `` | | `S` | Move backward to the first occurrence of `` | | `z` | Perform `` forward to the first occurrence of `` | | `Z` | Perform `` backward to the first occurrence of `` | ### CamelCaseMotion Based on [CamelCaseMotion](https://github.com/bkad/CamelCaseMotion), though not an exact emulation. This plugin provides an easier way to move through camelCase and snake_case words. | Setting | Description | Type | Default Value | | -------------------------- | ------------------------------ | ------- | ------------- | | vim.camelCaseMotion.enable | Enable/disable CamelCaseMotion | Boolean | false | Once CamelCaseMotion is enabled, the following motions are available: | Motion Command | Description | | ---------------------- | -------------------------------------------------------------------------- | | `w` | Move forward to the start of the next camelCase or snake_case word segment | | `e` | Move forward to the next end of a camelCase or snake_case word segment | | `b` | Move back to the prior beginning of a camelCase or snake_case word segment | | `iw` | Select/change/delete/etc. the current camelCase or snake_case word segment | By default, `` is mapped to `\`, so for example, `d2i\w` would delete the current and next camelCase word segment. ### Input Method Disable input method when exiting Insert Mode. | Setting | Description | | --------------------------------------- | ------------------------------------------------------------------------------------------------ | | `vim.autoSwitchInputMethod.enable` | Boolean denoting whether autoSwitchInputMethod is on/off. | | `vim.autoSwitchInputMethod.defaultIM` | Default input method. | | `vim.autoSwitchInputMethod.obtainIMCmd` | The full path to command to retrieve the current input method key. | | `vim.autoSwitchInputMethod.switchIMCmd` | The full path to command to switch input method, with `{im}` a placeholder for input method key. | Any third-party program can be used to switch input methods. The following will walkthrough the configuration using [im-select](https://github.com/daipeihust/im-select). 1. Install im-select (see [installation guide](https://github.com/daipeihust/im-select#installation)) 1. Find your default input method key - Mac: Switch your input method to English, and run the following in your terminal: `//im-select` to output your default input method. The table below lists the common English key layouts for MacOS. | Key | Description | | ------------------------------ | ----------- | | com.apple.keylayout.US | U.S. | | com.apple.keylayout.ABC | ABC | | com.apple.keylayout.British | British | | com.apple.keylayout.Irish | Irish | | com.apple.keylayout.Australian | Australian | | com.apple.keylayout.Dvorak | Dvorak | | com.apple.keylayout.Colemak | Colemak | - Windows: Refer to the [im-select guide](https://github.com/daipeihust/im-select#to-get-current-keyboard-locale) on how to discover your input method key. Generally, if your keyboard layout is en_US the input method key is 1033 (the locale ID of en_US). You can also find your locale ID from [this page](https://www.science.co.il/language/Locale-codes.php), where the `LCID Decimal` column is the locale ID. 1. Configure `vim.autoSwitchInputMethod`. - MacOS: Given the input method key of `com.apple.keylayout.US` and `im-select` located at `/usr/local/bin`. The configuration is: ```json "vim.autoSwitchInputMethod.enable": true, "vim.autoSwitchInputMethod.defaultIM": "com.apple.keylayout.US", "vim.autoSwitchInputMethod.obtainIMCmd": "/usr/local/bin/im-select", "vim.autoSwitchInputMethod.switchIMCmd": "/usr/local/bin/im-select {im}" ``` - Windows: Given the input method key of `1033` (en_US) and `im-select.exe` located at `D:/bin`. The configuration is: ```json "vim.autoSwitchInputMethod.enable": true, "vim.autoSwitchInputMethod.defaultIM": "1033", "vim.autoSwitchInputMethod.obtainIMCmd": "D:\\bin\\im-select.exe", "vim.autoSwitchInputMethod.switchIMCmd": "D:\\bin\\im-select.exe {im}" ``` The `{im}` argument above is a command-line option that will be passed to `im-select` denoting the input method to switch to. If using an alternative program to switch input methods, you should add a similar option to the configuration. For example, if the program's usage is `my-program -s imKey` to switch input method, the `vim.autoSwitchInputMethod.switchIMCmd` should be `/path/to/my-program -s {im}`. ### ReplaceWithRegister Based on [ReplaceWithRegister](https://github.com/vim-scripts/ReplaceWithRegister), an easy way to replace existing text with the contents of a register. | Setting | Description | Type | Default Value | | ----------------------- | ---------------------------------- | ------- | ------------- | | vim.replaceWithRegister | Enable/disable ReplaceWithRegister | Boolean | false | Once active, type `gr` (say "go replace") followed by a motion to describe the text you want replaced by the contents of the register. | Motion Command | Description | | ----------------------- | --------------------------------------------------------------------------------------- | | `[count]["a]gr` | Replace the text described by the motion with the contents of the specified register | | `[count]["a]grr` | Replace the \[count\] lines or current line with the contents of the specified register | | `{Visual}["a]gr` | Replace the selection with the contents of the specified register | ### vim-textobj-entire Similar to [vim-textobj-entire](https://github.com/kana/vim-textobj-entire). Adds two useful text-objects: - `ae` which represents the entire content of a buffer. - `ie` which represents the entire content of a buffer without the leading and trailing spaces. Usage examples: - `dae` - delete the whole buffer content. - `yie` - will yank the buffer content except leading and trailing blank lines. - `gUae` - transform the whole buffer to uppercase. ### vim-textobj-arguments Similar to the argument text object in [targets.vim](https://github.com/wellle/targets.vim). It is an easy way to deal with arguments inside functions in most programming languages. | Motion Command | Description | | -------------- | ---------------------------------- | | `ia` | The argument excluding separators. | | `aa` | The argument including separators. | Usage examples: - `cia` - change the argument under the cursor while preserving separators like comma `,`. - `daa` - will delete the whole argument under the cursor and the separators if applicable. | Setting | Description | Type | Default Value | | ----------------------------------- | ---------------------------- | ----------- | ------------- | | vim.argumentObjectOpeningDelimiters | A list of opening delimiters | String list | ["(", "["] | | vim.argumentObjectClosingDelimiters | A list of closing delimiters | String list | [")", "]"] | | vim.argumentObjectSeparators | A list of object separators | String list | [","] | ## 🎩 VSCodeVim tricks! VS Code has a lot of nifty tricks and we try to preserve some of them: - `gd` - jump to definition. - `gq` - on a visual selection reflow and wordwrap blocks of text, preserving commenting style. Great for formatting documentation comments. - `gb` - adds another cursor on the next word it finds which is the same as the word under the cursor. - `af` - visual mode command which selects increasingly large blocks of text. For example, if you had "blah (foo [bar 'ba|z'])" then it would select 'baz' first. If you pressed `af` again, it'd then select [bar 'baz'], and if you did it a third time it would select "(foo [bar 'baz'])". - `gh` - equivalent to hovering your mouse over wherever the cursor is. Handy for seeing types and error messages without reaching for the mouse! ## 📚 F.A.Q. - None of the native Visual Studio Code `ctrl` (e.g. `ctrl+f`, `ctrl+v`) commands work Set the [`useCtrlKeys` setting](#vscodevim-settings) to `false`. - Moving `j`/`k` over folds opens up the folds Try setting `vim.foldfix` to `true`. This is a hack; it works fine, but there are side effects (see [issue#22276](https://github.com/Microsoft/vscode/issues/22276)). - Key repeat doesn't work Are you on a Mac? Did you go through our [mac-setup](#mac) instructions? - There are annoying intellisense/notifications/popups that I can't close with ``! Or I'm in a snippet and I want to close intellisense Press `shift+` to close all of those boxes. - How can I use the commandline when in Zen mode or when the status bar is disabled? This extension exposes a remappable command to show a VS Code style quick-pick version of the commandline, with more limited functionality. This can be remapped as follows in VS Code's keybindings.json settings file. ```json { "key": "shift+;", "command": "vim.showQuickpickCmdLine", "when": "editorTextFocus && vim.mode != 'Insert'" } ``` Or for Zen mode only: ```json { "key": "shift+;", "command": "vim.showQuickpickCmdLine", "when": "inZenMode && vim.mode != 'Insert'" } ``` - How can I move the cursor by each display line with word wrapping? If you have word wrap on and would like the cursor to enter each wrapped line when using j, k, or , set the following in VS Code's keybindings.json settings file. ```json { "key": "up", "command": "cursorUp", "when": "editorTextFocus && vim.active && !inDebugRepl && !suggestWidgetMultipleSuggestions && !suggestWidgetVisible" }, { "key": "down", "command": "cursorDown", "when": "editorTextFocus && vim.active && !inDebugRepl && !suggestWidgetMultipleSuggestions && !suggestWidgetVisible" }, { "key": "k", "command": "cursorUp", "when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Normal' && !suggestWidgetMultipleSuggestions && !suggestWidgetVisible" }, { "key": "j", "command": "cursorDown", "when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Normal' && !suggestWidgetMultipleSuggestions && !suggestWidgetVisible" } ``` **Caveats:** This solution restores the default VS Code behavior for the j and k keys, so motions like `10j` [will not work](https://github.com/VSCodeVim/Vim/pull/3623#issuecomment-481473981). If you need these motions to work, [other, less performant options exist](https://github.com/VSCodeVim/Vim/issues/2924#issuecomment-476121848). - I've swapped Escape and Caps Lock with setxkbmap and VSCodeVim isn't respecting the swap This is a [known issue in VS Code](https://github.com/microsoft/vscode/issues/23991), as a workaround you can set `"keyboard.dispatch": "keyCode"` and restart VS Code. - VSCodeVim is too slow! You can try adding the following [setting](https://github.com/microsoft/vscode/issues/75627#issuecomment-1078827311), and reload/restart VSCode: ```json "extensions.experimental.affinity": { "vscodevim.vim": 1 } ``` **Caveats:** One issue with using the affinity setting is that each time you update your settings file, the Vim plugin will reload, which can take a few seconds. ## ❤️ Contributing This project is maintained by a group of awesome [people](https://github.com/VSCodeVim/Vim/graphs/contributors) and contributions are extremely welcome :heart:. For a quick tutorial on how you can help, see our [contributing guide](/.github/CONTRIBUTING.md). Buy Us A Coffee ### Special shoutouts to: - Thanks to @xconverge for making over 100 commits to the repo. If you're wondering why your least favorite bug packed up and left, it was probably him. - Thanks to @Metamist for implementing EasyMotion! - Thanks to @sectioneight for implementing text objects! - Special props to [Kevin Coleman](http://kevincoleman.io), who created our awesome logo! - Shoutout to @chillee aka Horace He for his contributions and hard work. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability Please _do not_ open an issue for a security vulnerability. Instead, please send an email to jasonfields4@gmail.com with "SECURITY" in the subject. I should get back to you within 48 hours; please follow up with another email if I do not. ================================================ FILE: build/Dockerfile ================================================ FROM node:22 ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y xorg xvfb libxss-dev libgtk-3-0 gconf2 libnss3 libasound2 libsecret-1-0 ENV CXX="g++-4.9" ENV CC="gcc-4.9" ENV DISPLAY=:99.0 WORKDIR /app ENTRYPOINT ["sh", "-c", "(Xvfb $DISPLAY -screen 0 1024x768x16 &) && npm run test"] ================================================ FILE: extension.ts ================================================ /** * Extension.ts is a lightweight wrapper around ModeHandler. It converts key * events to their string names and passes them on to ModeHandler via * handleKeyEvent(). */ import './src/actions/include-main'; import './src/actions/include-plugins'; /** * Load configuration validator */ import './src/configuration/validators/inputMethodSwitcherValidator'; import './src/configuration/validators/neovimValidator'; import './src/configuration/validators/remappingValidator'; import './src/configuration/validators/vimrcValidator'; import * as path from 'path'; import * as vscode from 'vscode'; import { activate as activateFunc, loadConfiguration, registerCommand, registerEventListener, } from './extensionBase'; import { vimrc } from './src/configuration/vimrc'; import { Globals } from './src/globals'; import { Register } from './src/register/register'; import { Logger } from './src/util/logger'; export { getAndUpdateModeHandler } from './extensionBase'; export async function activate(context: vscode.ExtensionContext) { // Set the storage path to be used by history files Globals.extensionStoragePath = context.globalStorageUri.fsPath; await activateFunc(context); registerEventListener(context, vscode.workspace.onDidSaveTextDocument, async (document) => { if (vimrc.vimrcPath && path.relative(document.fileName, vimrc.vimrcPath) === '') { await loadConfiguration(); Logger.info('Sourced new .vimrc'); } }); registerCommand( context, 'vim.editVimrc', async () => { if (vimrc.vimrcPath) { const document = await vscode.workspace.openTextDocument(vimrc.vimrcPath); await vscode.window.showTextDocument(document); } else { await vscode.window.showWarningMessage('No .vimrc found. Please set `vim.vimrc.path`.'); } }, false, ); } export async function deactivate() { await Register.saveToDisk(true); } ================================================ FILE: extensionBase.ts ================================================ import * as vscode from 'vscode'; import { ExCommandLine, SearchCommandLine } from './src/cmd_line/commandLine'; import { configuration } from './src/configuration/configuration'; import { Notation } from './src/configuration/notation'; import { Globals } from './src/globals'; import { Jump } from './src/jumps/jump'; import { Mode } from './src/mode/mode'; import { ModeHandler } from './src/mode/modeHandler'; import { ModeHandlerMap } from './src/mode/modeHandlerMap'; import { Register } from './src/register/register'; import { CompositionState } from './src/state/compositionState'; import { globalState } from './src/state/globalState'; import { StatusBar } from './src/statusBar'; import { taskQueue } from './src/taskQueue'; import { Logger } from './src/util/logger'; import { SpecialKeys } from './src/util/specialKeys'; import { VSCodeContext } from './src/util/vscodeContext'; import { exCommandParser } from './src/vimscript/exCommandParser'; let extensionContext: vscode.ExtensionContext; let previousActiveEditorUri: vscode.Uri | undefined; let lastClosedModeHandler: ModeHandler | null = null; interface ICodeKeybinding { after?: string[]; commands?: Array<{ command: string; args: any[] }>; } export async function getAndUpdateModeHandler( forceSyncAndUpdate = false, ): Promise { const activeTextEditor = vscode.window.activeTextEditor; if (activeTextEditor === undefined || activeTextEditor.document.isClosed) { return undefined; } const [curHandler, isNew] = await ModeHandlerMap.getOrCreate(activeTextEditor); if (isNew) { extensionContext.subscriptions.push(curHandler); } curHandler.vimState.editor = activeTextEditor; if ( forceSyncAndUpdate || !previousActiveEditorUri || previousActiveEditorUri !== activeTextEditor.document.uri ) { // We sync the cursors here because ModeHandler is specific to a document, not an editor, so we // need to update our representation of the cursors when switching between editors for the same document. // This will be unnecessary once #4889 is fixed. curHandler.syncCursors(); curHandler.updateView({ drawSelection: false, revealRange: false }); } previousActiveEditorUri = activeTextEditor.document.uri; if (curHandler.focusChanged) { curHandler.focusChanged = false; if (previousActiveEditorUri) { const prevHandler = ModeHandlerMap.get(previousActiveEditorUri); prevHandler!.focusChanged = true; } } return curHandler; } /** * Loads and validates the user's configuration */ export async function loadConfiguration() { const validatorResults = await configuration.load(); Logger.debug(`${validatorResults.numErrors} errors found with vim configuration`); if (validatorResults.numErrors > 0) { for (const validatorResult of validatorResults.get()) { switch (validatorResult.level) { case 'error': Logger.error(validatorResult.message); break; case 'warning': Logger.warn(validatorResult.message); break; } } } } /** * The extension's entry point */ export async function activate(context: vscode.ExtensionContext, handleLocal: boolean = true) { ExCommandLine.parser = exCommandParser; Logger.init(); // before we do anything else, we need to load the configuration await loadConfiguration(); Logger.debug('Start'); extensionContext = context; extensionContext.subscriptions.push(StatusBar); // Load state Register.loadFromDisk(handleLocal); await Promise.all([ExCommandLine.loadHistory(context), SearchCommandLine.loadHistory(context)]); const document = vscode.window.activeTextEditor?.document; if (document) { const filepathComponents = document.fileName.split(/\\|\//); Register.setReadonlyRegister('%', filepathComponents.at(-1)!); } // workspace events registerEventListener( context, vscode.workspace.onDidChangeConfiguration, async () => { Logger.info('Configuration changed'); await loadConfiguration(); }, false, ); registerEventListener(context, vscode.workspace.onDidChangeTextDocument, async (event) => { if (event.document.uri.scheme === 'output') { // Without this, we'll get an infinite logging loop return; } if (event.contentChanges.length === 0) { // This happens when the document is saved return; } Logger.debug( `${event.contentChanges.length} change(s) to ${event.document.fileName} because ${event.reason}`, ); for (const x of event.contentChanges) { Logger.trace(`\t-${x.rangeLength}, +'${x.text}'`); } for (const change of event.contentChanges) { if (change.rangeLength > 0) { globalState.jumpTracker.handleTextDeleted(event.document, change.range); } if (change.text.length > 0) { globalState.jumpTracker.handleTextAdded(event.document, change.range.start, change.text); } } const mh = ModeHandlerMap.get(event.document.uri); if (mh) { // Change from VSCode editor should set document.isDirty to true but they initially don't! // There is a timing issue in VSCode codebase between when the isDirty flag is set and // when registered callbacks are fired. https://github.com/Microsoft/vscode/issues/11339 if (mh.vimState.currentMode === Mode.Insert) { mh.vimState.historyTracker.currentContentChanges.push(...event.contentChanges); } } }); registerEventListener( context, vscode.workspace.onDidCloseTextDocument, async (closedDocument) => { Logger.info(`${closedDocument.fileName} closed`); // Delete modehandler once all tabs of this document have been closed for (const [uri, modeHandler] of ModeHandlerMap.entries()) { let shouldDelete = false; if (modeHandler == null) { shouldDelete = true; } else { if (!vscode.workspace.textDocuments.includes(modeHandler.vimState.document)) { shouldDelete = true; if (closedDocument === modeHandler.vimState.document) { lastClosedModeHandler = modeHandler; } } } if (shouldDelete) { ModeHandlerMap.delete(uri); } } }, false, ); // window events registerEventListener( context, vscode.window.onDidChangeActiveTextEditor, async (activeTextEditor: vscode.TextEditor | undefined) => { if (activeTextEditor) { Logger.info(`Active editor: ${activeTextEditor.document.uri}`); } else { Logger.debug(`No active editor`); } const mhPrevious: ModeHandler | undefined = previousActiveEditorUri ? ModeHandlerMap.get(previousActiveEditorUri) : undefined; // Track the closed editor so we can use it the next time an open event occurs. // When vscode changes away from a temporary file, onDidChangeActiveTextEditor first twice. // First it fires when leaving the closed editor. Then onDidCloseTextDocument first, and we delete // the old ModeHandler. Then a new editor opens. // // This also applies to files that are merely closed, which allows you to jump back to that file similarly // once a new file is opened. lastClosedModeHandler = mhPrevious || lastClosedModeHandler; const oldFileRegister = (await Register.get('%'))?.text; const relativePath = activeTextEditor ? vscode.workspace.asRelativePath(activeTextEditor.document.uri, false) : ''; if (relativePath !== oldFileRegister) { if (oldFileRegister && oldFileRegister !== '') { Register.setReadonlyRegister('#', oldFileRegister as string); } Register.setReadonlyRegister('%', relativePath); } if (activeTextEditor === undefined) { return; } taskQueue.enqueueTask(async () => { const mh = await getAndUpdateModeHandler(true); if (mh) { globalState.jumpTracker.handleFileJump( lastClosedModeHandler ? Jump.fromStateNow(lastClosedModeHandler.vimState) : undefined, Jump.fromStateNow(mh.vimState), ); } }); }, true, true, ); registerEventListener( context, vscode.window.onDidChangeTextEditorSelection, async (e: vscode.TextEditorSelectionChangeEvent) => { if (e.textEditor.document.uri.scheme === 'output') { // Without this, we can an infinite logging loop return; } if ( vscode.window.activeTextEditor === undefined || e.textEditor.document !== vscode.window.activeTextEditor.document ) { // We don't care if user selection changed in a paneled window (e.g debug console/terminal) return; } const mh = ModeHandlerMap.get(vscode.window.activeTextEditor.document.uri); if (mh === undefined) { // We don't care if there is no active editor return; } if ( e.kind !== vscode.TextEditorSelectionChangeKind.Mouse && mh.internalSelectionsTracker.shouldIgnoreAsInternalSelectionChangeEvent(e) ) { return; } // We may receive changes from other panels when, having selections in them containing the same file // and changing text before the selection in current panel. if (e.textEditor !== mh.vimState.editor) { return; } if (mh.focusChanged) { mh.focusChanged = false; return; } if (mh.vimState.currentMode === Mode.EasyMotionMode) { return; } taskQueue.enqueueTask(() => mh.handleSelectionChange(e)); }, true, false, ); registerEventListener( context, vscode.window.onDidChangeTextEditorVisibleRanges, async (e: vscode.TextEditorVisibleRangesChangeEvent) => { if (e.textEditor !== vscode.window.activeTextEditor) { return; } taskQueue.enqueueTask(async () => { // Scrolling the viewport clears any status bar message, even errors. const mh = await getAndUpdateModeHandler(); if (mh && StatusBar.lastMessageTime) { // TODO: Using the time elapsed works most of the time, but is a bit of a hack const timeElapsed = Date.now() - Number(StatusBar.lastMessageTime); if (timeElapsed > 100) { StatusBar.clear(mh.vimState, true); } } }); }, ); const compositionState = new CompositionState(); // Override VSCode commands overrideCommand(context, 'type', async (args: { text: string }) => { taskQueue.enqueueTask(async () => { const mh = await getAndUpdateModeHandler(); if (mh) { if (compositionState.isInComposition) { compositionState.composingText += args.text; if (mh.vimState.currentMode === Mode.Insert) { compositionState.insertedText = true; void vscode.commands.executeCommand('default:type', { text: args.text }); } } else { await mh.handleKeyEvent(args.text); } } }); }); overrideCommand( context, 'replacePreviousChar', async (args: { replaceCharCnt: number; text: string }) => { taskQueue.enqueueTask(async () => { const mh = await getAndUpdateModeHandler(); if (mh) { if (compositionState.isInComposition) { compositionState.composingText = compositionState.composingText.substr( 0, compositionState.composingText.length - args.replaceCharCnt, ) + args.text; } if (compositionState.insertedText) { await vscode.commands.executeCommand('default:replacePreviousChar', { text: args.text, replaceCharCnt: args.replaceCharCnt, }); mh.vimState.cursorStopPosition = mh.vimState.editor.selection.start; mh.vimState.cursorStartPosition = mh.vimState.editor.selection.start; } } else { await vscode.commands.executeCommand('default:replacePreviousChar', { text: args.text, replaceCharCnt: args.replaceCharCnt, }); } }); }, ); overrideCommand(context, 'compositionStart', async () => { taskQueue.enqueueTask(async () => { compositionState.isInComposition = true; }); }); overrideCommand(context, 'compositionEnd', async () => { taskQueue.enqueueTask(async () => { const mh = await getAndUpdateModeHandler(); if (mh) { if (compositionState.insertedText) { mh.internalSelectionsTracker.startIgnoringIntermediateSelections(); await vscode.commands.executeCommand('default:replacePreviousChar', { text: '', replaceCharCnt: compositionState.composingText.length, }); mh.vimState.cursorStopPosition = mh.vimState.editor.selection.active; mh.vimState.cursorStartPosition = mh.vimState.editor.selection.active; mh.internalSelectionsTracker.stopIgnoringIntermediateSelections(); } const text = compositionState.composingText; if (compositionState.insertedText) { await mh.handleMultipleKeyEvents([text]); } else { await mh.handleMultipleKeyEvents(text.split('')); } } compositionState.reset(); }); }); // Register extension commands registerCommand(context, 'vim.showQuickpickCmdLine', async () => { const mh = await getAndUpdateModeHandler(); if (mh) { const cmd = await vscode.window.showInputBox({ prompt: 'Vim command line', value: '', ignoreFocusOut: false, valueSelection: [0, 0], }); if (cmd) { await new ExCommandLine(cmd, mh.vimState.currentMode).run(mh.vimState); } mh.updateView(); } }); registerCommand(context, 'vim.remap', async (args: ICodeKeybinding) => { taskQueue.enqueueTask(async () => { const mh = await getAndUpdateModeHandler(); if (mh === undefined) { return; } if (!args) { throw new Error( "'args' is undefined. For this remap to work it needs to have 'args' with an '\"after\": string[]' and/or a '\"commands\": { command: string; args: any[] }[]'", ); } if (args.after) { for (const key of args.after) { await mh.handleKeyEvent(Notation.NormalizeKey(key, configuration.leader)); } } if (args.commands) { for (const command of args.commands) { // Check if this is a vim command by looking for : if (command.command.startsWith(':')) { await new ExCommandLine( command.command.slice(1, command.command.length), mh.vimState.currentMode, ).run(mh.vimState); mh.updateView(); } else { await vscode.commands.executeCommand(command.command, command.args); } } } }); }); registerCommand(context, 'toggleVim', async () => { configuration.disableExtension = !configuration.disableExtension; void toggleExtension(configuration.disableExtension, compositionState); }); for (const boundKey of configuration.boundKeyCombinations) { const command = ['', ''].includes(boundKey.key) ? async () => { const mh = await getAndUpdateModeHandler(); if (mh && !(await forceStopRecursiveRemap(mh))) { await mh.handleKeyEvent(`${boundKey.key}`); } } : async () => { const mh = await getAndUpdateModeHandler(); if (mh) { await mh.handleKeyEvent(`${boundKey.key}`); } }; registerCommand(context, boundKey.command, async () => { taskQueue.enqueueTask(command); }); } { // Initialize mode handler for current active Text Editor at startup. const modeHandler = await getAndUpdateModeHandler(); if (modeHandler) { if (!configuration.startInInsertMode) { const vimState = modeHandler.vimState; // Make sure no cursors start on the EOL character (which is invalid in normal mode) // This can happen if we quit last session in insert mode at the end of the line vimState.cursors = vimState.cursors.map((cursor) => { const eolColumn = vimState.document.lineAt(cursor.stop).text.length; if (cursor.stop.character >= eolColumn) { const character = Math.max(eolColumn - 1, 0); return cursor.withNewStop(cursor.stop.with({ character })); } else { return cursor; } }); } // This is called last because getAndUpdateModeHandler() will change cursor void modeHandler.updateView({ drawSelection: true, revealRange: false }); } } // Disable automatic keyboard navigation in lists, so it doesn't interfere // with our list navigation keybindings await VSCodeContext.set('listAutomaticKeyboardNavigation', false); await toggleExtension(configuration.disableExtension, compositionState); Logger.debug('Finish.'); } /** * Toggles the VSCodeVim extension between Enabled mode and Disabled mode. This * function is activated by calling the 'toggleVim' command from the Command Palette. * * @param isDisabled if true, sets VSCodeVim to Disabled mode; else sets to enabled mode */ async function toggleExtension(isDisabled: boolean, compositionState: CompositionState) { await VSCodeContext.set('vim.active', !isDisabled); const mh = await getAndUpdateModeHandler(); if (mh) { if (isDisabled) { await mh.handleKeyEvent(SpecialKeys.ExtensionDisable); compositionState.reset(); ModeHandlerMap.clear(); } else { await mh.handleKeyEvent(SpecialKeys.ExtensionEnable); } } } function overrideCommand( context: vscode.ExtensionContext, command: string, callback: (...args: any[]) => any, ) { const disposable = vscode.commands.registerCommand(command, async (args) => { if (configuration.disableExtension) { return vscode.commands.executeCommand('default:' + command, args); } if (!vscode.window.activeTextEditor) { return; } if ( vscode.window.activeTextEditor.document && vscode.window.activeTextEditor.document.uri.toString() === 'debug:input' ) { return vscode.commands.executeCommand('default:' + command, args); } return callback(args) as vscode.Disposable; }); context.subscriptions.push(disposable); } export function registerCommand( context: vscode.ExtensionContext, command: string, callback: (...args: any[]) => any, requiresActiveEditor: boolean = true, ) { const disposable = vscode.commands.registerCommand(command, async (args) => { if (requiresActiveEditor && !vscode.window.activeTextEditor) { return; } callback(args); }); context.subscriptions.push(disposable); } export function registerEventListener( context: vscode.ExtensionContext, event: vscode.Event, listener: (e: T) => Promise, exitOnExtensionDisable = true, exitOnTests = false, ) { const disposable = event(async (e) => { if (exitOnExtensionDisable && configuration.disableExtension) { return; } if (exitOnTests && Globals.isTesting) { return; } await listener(e); }); context.subscriptions.push(disposable); } /** * @returns true if there was a remap being executed to stop */ async function forceStopRecursiveRemap(mh: ModeHandler): Promise { if (mh.remapState.isCurrentlyPerformingRecursiveRemapping) { mh.remapState.forceStopRecursiveRemapping = true; return true; } return false; } ================================================ FILE: extensionWeb.ts ================================================ /** * Extension.ts is a lightweight wrapper around ModeHandler. It converts key * events to their string names and passes them on to ModeHandler via * handleKeyEvent(). */ import './src/actions/include-main'; import './src/actions/include-plugins'; /** * Load configuration validator */ import './src/configuration/validators/inputMethodSwitcherValidator'; import './src/configuration/validators/remappingValidator'; import * as vscode from 'vscode'; import { activate as activateFunc } from './extensionBase'; export async function activate(context: vscode.ExtensionContext) { void activateFunc(context, false); } ================================================ FILE: gulpfile.js ================================================ const gulp = require('gulp'); const bump = require('gulp-bump'); const tag_version = require('gulp-tag-version'); const ts = require('gulp-typescript'); const PluginError = require('plugin-error'); const minimist = require('minimist'); const path = require('path'); const es = require('event-stream'); const shell = require('gulp-shell'); const releaseOptions = { semver: '', }; function validateArgs(done) { const options = minimist(process.argv.slice(2), releaseOptions); if (!options.semver) { return done( new PluginError('updateVersion', { message: 'Missing `--semver` option. Possible values: patch, minor, major', }), ); } if (!['patch', 'minor', 'major'].includes(options.semver)) { return done( new PluginError('updateVersion', { message: 'Invalid `--semver` option. Possible values: patch, minor, major', }), ); } done(); } function createGitTag() { return gulp.src(['./package.json']).pipe(tag_version()); } function updateVersion(done) { var options = minimist(process.argv.slice(2), releaseOptions); return gulp .src(['./package.json', './yarn.lock']) .pipe(bump({ type: options.semver })) .pipe(gulp.dest('./')) .on('end', () => { done(); }); } function updatePath() { const input = es.through(); const output = input.pipe( es.mapSync((f) => { const contents = f.contents.toString('utf8'); const filePath = f.path; let platformRelativepath = path.relative( path.dirname(filePath), path.resolve(process.cwd(), 'out/src/platform/node'), ); platformRelativepath = platformRelativepath.replace(/\\/g, '/'); f.contents = Buffer.from( contents.replace( /\(\"platform\/([^"]*)\"\)/g, '("' + (platformRelativepath === '' ? './' : platformRelativepath + '/') + '$1")', ), 'utf8', ); return f; }), ); return es.duplex(input, output); } function copyPackageJson() { return gulp.src('./package.json').pipe(gulp.dest('out')); } gulp.task('tsc', function () { var isError = false; var tsProject = ts.createProject('tsconfig.json', { noEmitOnError: true }); var tsResult = tsProject .src() .pipe(tsProject()) .on('error', () => { isError = true; }) .on('finish', () => { isError && process.exit(1); }); return tsResult.js.pipe(updatePath()).pipe(gulp.dest('out')); }); // test gulp.task('run-test', function (done) { // the flag --grep takes js regex as a string and filters by test and test suite names var knownOptions = { string: 'grep', default: { grep: '' }, }; var options = minimist(process.argv.slice(2), knownOptions); var spawn = require('child_process').spawn; const dockerTag = 'vscodevim'; console.log('Building container...'); var dockerBuildCmd = spawn( 'docker', ['build', '-f', './build/Dockerfile', './build/', '-t', dockerTag], { cwd: process.cwd(), stdio: 'inherit', }, ); dockerBuildCmd.on('exit', function (exitCode) { if (exitCode !== 0) { return done( new PluginError('test', { message: 'Docker build failed.', }), ); } const dockerRunArgs = [ 'run', '-it', '--rm', '--env', `MOCHA_GREP=${options.grep}`, '-v', process.cwd() + ':/app', dockerTag, ]; console.log('Running tests inside container...'); var dockerRunCmd = spawn('docker', dockerRunArgs, { cwd: process.cwd(), stdio: 'inherit', }); dockerRunCmd.on('exit', function (exitCode) { done(exitCode); }); }); }); gulp.task('prepare-test', gulp.parallel('tsc', copyPackageJson)); gulp.task('test', gulp.series('prepare-test', 'run-test')); gulp.task( 'release', gulp.series( validateArgs, updateVersion, shell.task('git commit -am "bump version"'), createGitTag, ), ); gulp.task('default', shell.task('yarn build-dev')); ================================================ FILE: language-configuration.json ================================================ { "comments": { "lineComment": "\"" } } ================================================ FILE: package.json ================================================ { "name": "vim", "displayName": "Vim", "description": "Vim emulation for Visual Studio Code", "icon": "images/icon.png", "version": "1.32.4", "publisher": "vscodevim", "sponsor": { "url": "https://github.com/sponsors/J-Fields" }, "galleryBanner": { "color": "#e3f4ff", "theme": "light" }, "license": "MIT", "keywords": [ "vim", "vi", "vscodevim" ], "repository": { "type": "git", "url": "https://github.com/VSCodeVim/Vim.git" }, "homepage": "https://github.com/VSCodeVim/Vim", "bugs": { "url": "https://github.com/VSCodeVim/Vim/issues" }, "engines": { "vscode": "^1.74.0" }, "categories": [ "Other", "Keymaps" ], "extensionKind": [ "ui" ], "sideEffects": false, "activationEvents": [ "onStartupFinished", "onCommand:type" ], "qna": "https://github.com/VSCodeVim/Vim/discussions", "main": "./out/extension", "browser": "./out/extensionWeb", "capabilities": { "untrustedWorkspaces": { "supported": true }, "virtualWorkspaces": true }, "contributes": { "commands": [ { "command": "toggleVim", "title": "Vim: Toggle Vim Mode" }, { "command": "vim.showQuickpickCmdLine", "title": "Vim: Show Command Line" }, { "command": "vim.editVimrc", "enablement": "!isWeb", "title": "Vim: Edit .vimrc" } ], "keybindings": [ { "key": "Escape", "command": "extension.vim_escape", "when": "editorTextFocus && vim.active && !inDebugRepl" }, { "key": "Escape", "command": "notebook.cell.quitEdit", "when": "inputFocus && notebookEditorFocused && !editorHasSelection && !editorHoverVisible && vim.active && vim.mode == 'Normal'" }, { "key": "Home", "command": "extension.vim_home", "when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode != 'Insert'" }, { "key": "ctrl+home", "command": "extension.vim_ctrl+home", "when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode != 'Insert'" }, { "key": "End", "command": "extension.vim_end", "when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode != 'Insert'" }, { "key": "ctrl+end", "command": "extension.vim_ctrl+end", "when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode != 'Insert'" }, { "key": "Insert", "command": "extension.vim_insert", "when": "editorTextFocus && vim.active && !inDebugRepl" }, { "key": "Backspace", "command": "extension.vim_backspace", "when": "editorTextFocus && vim.active && !inDebugRepl" }, { "key": "Delete", "command": "extension.vim_delete", "when": "editorTextFocus && vim.active && !inDebugRepl" }, { "key": "tab", "command": "extension.vim_tab", "when": "editorTextFocus && vim.active && vim.mode != 'Insert' && !inDebugRepl && !inlineEditIsVisible" }, { "key": "shift+tab", "command": "extension.vim_shift+tab", "when": "editorTextFocus && vim.active && vim.mode != 'Insert' && !inDebugRepl" }, { "key": "left", "command": "extension.vim_left", "when": "editorTextFocus && vim.active && !inDebugRepl" }, { "key": "right", "command": "extension.vim_right", "when": "editorTextFocus && vim.active && !inDebugRepl" }, { "key": "up", "command": "extension.vim_up", "when": "editorTextFocus && vim.active && !inDebugRepl && !suggestWidgetVisible && !parameterHintsVisible" }, { "key": "down", "command": "extension.vim_down", "when": "editorTextFocus && vim.active && !inDebugRepl && !suggestWidgetVisible && !parameterHintsVisible" }, { "key": "g g", "command": "list.focusFirst", "when": "listFocus && !inputFocus" }, { "key": "h", "command": "list.collapse", "when": "listFocus && !inputFocus" }, { "key": "j", "command": "list.focusDown", "when": "listFocus && !inputFocus" }, { "key": "k", "command": "list.focusUp", "when": "listFocus && !inputFocus" }, { "key": "l", "command": "list.select", "when": "listFocus && !inputFocus" }, { "key": "o", "command": "list.toggleExpand", "when": "listFocus && !inputFocus" }, { "key": "/", "command": "list.toggleKeyboardNavigation", "when": "listFocus && !inputFocus && listSupportsKeyboardNavigation" }, { "key": "ctrl+a", "command": "extension.vim_ctrl+a", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+b", "command": "extension.vim_ctrl+b", "when": "editorTextFocus && vim.active && vim.use && vim.mode != 'Insert' && !inDebugRepl" }, { "key": "ctrl+c", "command": "extension.vim_ctrl+c", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl && vim.overrideCtrlC" }, { "key": "ctrl+d", "command": "extension.vim_ctrl+d", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+d", "command": "list.focusPageDown", "when": "listFocus && !inputFocus" }, { "key": "ctrl+e", "command": "extension.vim_ctrl+e", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+f", "command": "extension.vim_ctrl+f", "when": "editorTextFocus && vim.active && vim.use && vim.mode != 'Insert' && !inDebugRepl" }, { "key": "ctrl+g", "command": "extension.vim_ctrl+g", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+h", "command": "extension.vim_ctrl+h", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+i", "command": "extension.vim_ctrl+i", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+j", "command": "extension.vim_ctrl+j", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+k", "command": "extension.vim_ctrl+k", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+l", "command": "extension.vim_navigateCtrlL", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+m", "command": "extension.vim_ctrl+m", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl || vim.mode == 'CommandlineInProgress' && vim.active && vim.use && !inDebugRepl || vim.mode == 'SearchInProgressMode' && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+n", "command": "extension.vim_ctrl+n", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl || vim.mode == 'CommandlineInProgress' && vim.active && vim.use && !inDebugRepl || vim.mode == 'SearchInProgressMode' && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+o", "command": "extension.vim_ctrl+o", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+p", "command": "extension.vim_ctrl+p", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl || vim.mode == 'CommandlineInProgress' && vim.active && vim.use && !inDebugRepl || vim.mode == 'SearchInProgressMode' && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+q", "command": "extension.vim_winCtrlQ", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+r", "command": "extension.vim_ctrl+r", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+s", "command": "extension.vim_ctrl+s", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+t", "command": "extension.vim_ctrl+t", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+u", "command": "extension.vim_ctrl+u", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+u", "command": "list.focusPageUp", "when": "listFocus && !inputFocus" }, { "key": "ctrl+v", "command": "extension.vim_ctrl+v", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+w", "command": "extension.vim_ctrl+w", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+x", "command": "extension.vim_ctrl+x", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+y", "command": "extension.vim_ctrl+y", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+z", "command": "extension.vim_ctrl+z", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+6", "command": "extension.vim_ctrl+6", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+^", "command": "extension.vim_ctrl+^", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+[", "command": "extension.vim_ctrl+[", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+]", "command": "extension.vim_ctrl+]", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+shift+2", "command": "extension.vim_ctrl+shift+2", "when": "editorTextFocus && vim.active && vim.use" }, { "key": "ctrl+up", "command": "extension.vim_ctrl+up", "when": "editorTextFocus && vim.active && vim.mode != 'Insert' && !inDebugRepl" }, { "key": "ctrl+down", "command": "extension.vim_ctrl+down", "when": "editorTextFocus && vim.active && vim.mode != 'Insert' && !inDebugRepl" }, { "key": "ctrl+left", "command": "extension.vim_ctrl+left", "when": "editorTextFocus && vim.active && vim.mode != 'Insert' && !inDebugRepl" }, { "key": "ctrl+right", "command": "extension.vim_ctrl+right", "when": "editorTextFocus && vim.active && vim.mode != 'Insert' && !inDebugRepl" }, { "key": "ctrl+pagedown", "command": "extension.vim_ctrl+pagedown", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+pageup", "command": "extension.vim_ctrl+pageup", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "ctrl+space", "command": "extension.vim_ctrl+space", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl && vim.mode != 'Insert'" }, { "key": "shift+G", "command": "list.focusLast", "when": "listFocus && !inputFocus" }, { "key": "ctrl+backspace", "command": "extension.vim_ctrl+backspace", "when": "editorTextFocus && vim.active && vim.use && vim.mode != 'Insert' && !inDebugRepl" }, { "key": "shift+backspace", "command": "extension.vim_shift+backspace", "when": "editorTextFocus && vim.active && vim.use && vim.mode != 'Insert' && !inDebugRepl" }, { "key": "cmd+left", "command": "extension.vim_cmd+left", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl && vim.mode != 'Insert'" }, { "key": "cmd+right", "command": "extension.vim_cmd+right", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl && vim.mode != 'Insert'" }, { "key": "cmd+a", "command": "extension.vim_cmd+a", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl && vim.mode != 'Insert'" }, { "key": "cmd+c", "command": "extension.vim_cmd+c", "when": "editorTextFocus && vim.active && vim.use && vim.overrideCopy && !inDebugRepl" }, { "key": "cmd+d", "command": "extension.vim_cmd+d", "when": "editorTextFocus && vim.active && vim.use && !inDebugRepl" }, { "key": "cmd+v", "command": "extension.vim_cmd+v", "when": "editorTextFocus && vim.active && vim.use && vim.mode == 'CommandlineInProgress' && !inDebugRepl || editorTextFocus && vim.active && vim.use && vim.mode == 'SearchInProgressMode' && !inDebugRepl" }, { "key": "ctrl+alt+down", "linux": "shift+alt+down", "mac": "cmd+alt+down", "command": "extension.vim_cmd+alt+down", "when": "editorTextFocus && vim.active && !inDebugRepl" }, { "key": "ctrl+alt+up", "linux": "shift+alt+up", "mac": "cmd+alt+up", "command": "extension.vim_cmd+alt+up", "when": "editorTextFocus && vim.active && !inDebugRepl" }, { "key": "j", "command": "notebook.focusNextEditor", "when": "vim.mode == 'Normal' && editorTextFocus && inputFocus && notebookEditorFocused && notebookEditorCursorAtBoundary != 'none' && notebookEditorCursorAtBoundary != 'top'" }, { "key": "k", "command": "notebook.focusPreviousEditor", "when": "vim.mode == 'Normal' && editorTextFocus && inputFocus && notebookEditorFocused && notebookEditorCursorAtBoundary != 'bottom' && notebookEditorCursorAtBoundary != 'none'" } ], "configuration": { "title": "Vim", "type": "object", "properties": { "vim.normalModeKeyBindings": { "type": "array", "markdownDescription": "Remapped keys in Normal mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.normalModeKeyBindingsNonRecursive": { "type": "array", "markdownDescription": "Non-recursive remapped keys in Normal mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.operatorPendingModeKeyBindings": { "type": "array", "markdownDescription": "Remapped keys in OperatorPending mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.operatorPendingModeKeyBindingsNonRecursive": { "type": "array", "markdownDescription": "Non-recursive remapped keys in OperatorPending mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.useCtrlKeys": { "type": "boolean", "markdownDescription": "Enable some Vim Ctrl key commands that override otherwise common operations, like `Ctrl+C`.", "default": true }, "vim.leader": { "type": "string", "markdownDescription": "What key should `` map to in remappings?", "default": "\\" }, "vim.searchHighlightColor": { "type": "string", "markdownDescription": "Background color of non-current search matches. The color must not be opaque so as not to hide underlying decorations. Uses `#editor.findMatchHighlightColor#` from current theme if undefined." }, "vim.searchHighlightTextColor": { "type": "string", "markdownDescription": "Foreground color of non-current search matches." }, "vim.searchMatchColor": { "type": "string", "markdownDescription": "Background color of the current match. Uses `#editor.findMatchColor#` from current theme if undefined." }, "vim.searchMatchTextColor": { "type": "string", "markdownDescription": "Foreground color of the current match." }, "vim.substitutionColor": { "type": "string", "markdownDescription": "Background color of substituted text when `#editor.inccommand#` is enabled. Uses `#editor.findMatchColor#` from current theme if undefined." }, "vim.substitutionTextColor": { "type": "string", "markdownDescription": "Foreground color of substituted text when `#editor.inccommand#` is enabled." }, "vim.highlightedyank.enable": { "type": "boolean", "description": "Enable highlighting when yanking.", "default": false }, "vim.highlightedyank.color": { "type": "string", "description": "Background color of yanked text. The color must not be opaque so as not to hide underlying decorations.", "default": "rgba(250, 240, 170, 0.5)" }, "vim.highlightedyank.textColor": { "type": "string", "description": "Foreground color of yanked text." }, "vim.highlightedyank.duration": { "type": "number", "description": "Duration in milliseconds of the yank highlight.", "default": 200, "minimum": 1 }, "vim.useSystemClipboard": { "type": "boolean", "description": "Use system clipboard for unnamed register.", "default": false }, "vim.overrideCopy": { "type": "boolean", "description": "Override VS Code's copy command with our own copy command, which works better with VSCodeVim. Turn this off if copying is not working.", "default": true }, "vim.insertModeKeyBindings": { "type": "array", "markdownDescription": "Remapped keys in Insert mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.insertModeKeyBindingsNonRecursive": { "type": "array", "markdownDescription": "Non-recursive keybinding overrides to use for Insert mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.visualModeKeyBindings": { "type": "array", "markdownDescription": "Remapped keys in Visual mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.visualModeKeyBindingsNonRecursive": { "type": "array", "markdownDescription": "Non-recursive keybinding overrides to use for Visual mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.commandLineModeKeyBindings": { "type": "array", "markdownDescription": "Remapped keys in command line mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.commandLineModeKeyBindingsNonRecursive": { "type": "array", "markdownDescription": "Non-recursive keybinding overrides to use for command line mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.", "scope": "application" }, "vim.textwidth": { "type": "number", "markdownDescription": "Width to word-wrap to when using `gq`.", "default": 80, "scope": "language-overridable", "minimum": 1 }, "vim.timeout": { "type": "number", "description": "Timeout in milliseconds for remapped commands.", "default": 1000, "minimum": 0 }, "vim.maxmapdepth": { "type": "number", "description": "Maximum number of times a mapping is done without resulting in a character to be used.", "default": 1000, "minimum": 0 }, "vim.scroll": { "type": "number", "markdownDescription": "Number of lines to scroll with `Ctrl-U` and `Ctrl-D` commands. Set to 0 to use a half page scroll.", "default": 0, "minimum": 0 }, "vim.showcmd": { "type": "boolean", "description": "Show the text of any command you are in the middle of writing.", "default": true }, "vim.showmodename": { "type": "boolean", "description": "Show the name of the current mode in the statusbar.", "default": true }, "vim.iskeyword": { "type": "string", "markdownDescription": "Keywords contain alphanumeric characters and '_'. If not configured, `#editor.wordSeparators#` is used." }, "vim.ignorecase": { "type": "boolean", "description": "Ignore case in search patterns.", "default": true }, "vim.smartcase": { "type": "boolean", "markdownDescription": "Override the `ignorecase` option if the search pattern contains upper case characters.", "default": true }, "vim.matchpairs": { "type": "string", "markdownDescription": "Characters that form pairs. The % command jumps from one to the other. Only character pairs are allowed that are different, thus you cannot jump between two double quotes. The characters must be separated by a colon. The pairs must be separated by a comma.", "default": "(:),{:},[:]", "pattern": "^(.:.)?(,.:.)*$" }, "vim.camelCaseMotion.enable": { "type": "boolean", "markdownDescription": "Enable the [CamelCaseMotion](https://github.com/bkad/CamelCaseMotion) plugin for Vim.", "default": false }, "vim.easymotion": { "type": "boolean", "markdownDescription": "Enable the [EasyMotion](https://github.com/easymotion/vim-easymotion) plugin for Vim.", "default": false }, "vim.easymotionMarkerBackgroundColor": { "type": "string", "default": "#0000", "description": "Set a custom background color for EasyMotion markers." }, "vim.easymotionMarkerForegroundColorOneChar": { "type": "string", "default": "#ff0000", "description": "Set a custom color for the text on one character long markers." }, "vim.easymotionMarkerForegroundColorTwoCharFirst": { "type": "string", "default": "#ffb400", "description": "Set a custom color for the first character on two character long markers." }, "vim.easymotionMarkerForegroundColorTwoCharSecond": { "type": "string", "default": "#b98300", "description": "Set a custom color for the second character on two character long markers." }, "vim.easymotionIncSearchForegroundColor": { "type": "string", "default": "#7fbf00", "markdownDescription": "Set a custom color for the easymotion search n-character (default `/`)." }, "vim.easymotionDimColor": { "type": "string", "default": "#777777", "markdownDescription": "Set a custom color for the easymotion dimmed characters when `#vim.easymotionDimBackground#` is set to true." }, "vim.easymotionDimBackground": { "type": "boolean", "description": "Whether to dim other text while markers are visible.", "default": true }, "vim.easymotionMarkerFontWeight": { "type": "string", "description": "Set the font weight of the marker text.", "default": "bold" }, "vim.easymotionKeys": { "type": "string", "description": "Set the characters used for jump marker name.", "default": "hklyuiopnm,qwertzxcvbasdgjf;" }, "vim.easymotionJumpToAnywhereRegex": { "type": "string", "description": "Regex matches for JumpToAnywhere motion.", "default": "\\b[A-Za-z0-9]|[A-Za-z0-9]\\b|_.|#.|[a-z][A-Z]" }, "vim.replaceWithRegister": { "type": "boolean", "markdownDescription": "Enable the [ReplaceWithRegister](https://github.com/vim-scripts/ReplaceWithRegister) plugin for Vim.", "default": false }, "vim.smartRelativeLine": { "type": "boolean", "markdownDescription": "`#editor.lineNumbers#` is determined by the active Vim mode, absolute when in insert or disabled mode, relative otherwise.", "default": false }, "vim.targets.enable": { "type": "boolean", "markdownDescription": "Enable [targets.vim](https://github.com/wellle/targets.vim#quote-text-objects) plugin (not fully implemented yet).", "default": false }, "vim.targets.bracketObjects.enable": { "type": "boolean", "markdownDescription": "Enable last/next movements for bracket objects.", "default": true }, "vim.targets.smartQuotes.enable": { "type": "boolean", "markdownDescription": "Enable the smart quotes movements from [targets.vim](https://github.com/wellle/targets.vim#quote-text-objects).", "default": true }, "vim.targets.smartQuotes.breakThroughLines": { "type": "boolean", "markdownDescription": "Whether to break through lines when using [n]ext/[l]ast motion, see [targets.vim#next-and-last-quote](https://github.com/wellle/targets.vim#next-and-last-quote).", "default": true }, "vim.targets.smartQuotes.aIncludesSurroundingSpaces": { "type": "boolean", "markdownDescription": "Whether to use default Vim behavior when using `a` (e.g. `da'`) which include surrounding spaces, or not, as for other text objects.", "default": true }, "vim.sneak": { "type": "boolean", "markdownDescription": "Enable the [Sneak](https://github.com/justinmk/vim-sneak) plugin for Vim.", "default": false }, "vim.sneakUseIgnorecaseAndSmartcase": { "type": "boolean", "markdownDescription": "Case sensitivity is determined by `#vim.ignorecase#` and `#vim.smartcase#`.", "default": false }, "vim.sneakReplacesF": { "type": "boolean", "markdownDescription": "Use single-character [Sneak](https://github.com/justinmk/vim-sneak) instead of Vim's native `f`.", "default": false }, "vim.surround": { "type": "boolean", "markdownDescription": "Enable the [Surround](https://github.com/tpope/vim-surround) plugin for Vim.", "default": true }, "vim.argumentObjectSeparators": { "type": "array", "items": { "type": "string" }, "markdownDescription": "Set separators for the argument text object.", "default": [ "," ] }, "vim.argumentObjectOpeningDelimiters": { "type": "array", "items": { "type": "string" }, "markdownDescription": "Set opening delimiters for the argument text object.", "default": [ "(", "[" ] }, "vim.argumentObjectClosingDelimiters": { "type": "array", "items": { "type": "string" }, "markdownDescription": "Set closing delimiters for the argument text object.", "default": [ ")", "]" ] }, "vim.hlsearch": { "type": "boolean", "description": "Show all matches of the most recent search pattern.", "default": false }, "vim.inccommand": { "type": "string", "markdownDescription": "Show the effects of the `:substitute` command as you type.", "default": "replace", "enum": [ "", "append", "replace" ], "enumDescriptions": [ "Don't show substitutions", "Show substitutions after matched text", "Replace matched text with substitutions" ] }, "vim.incsearch": { "type": "boolean", "markdownDescription": "Show where a `/` or `?` search matches as you type it.", "default": true }, "vim.history": { "type": "number", "description": "How much search or command history should be remembered.", "default": 50, "minimum": 1, "maximum": 10000 }, "vim.autoindent": { "type": "boolean", "description": "Indent code automatically.", "default": true }, "vim.joinspaces": { "type": "boolean", "description": "Add two spaces after '.', '?', and '!' when joining or reformatting.", "default": true }, "vim.startInInsertMode": { "type": "boolean", "description": "Start in Insert mode." }, "vim.startInInsertModeSchemes": { "type": "array", "items": { "type": "string" }, "default": [ "comment" ], "markdownDescription": "List of document URI schemes that should automatically start in Insert mode. For example, use `[\"comment\"]` for GitHub PR comment editors, or `[\"comment\", \"gitlens\"]` for multiple schemes." }, "vim.handleKeys": { "type": "object", "description": "Delegate certain key combinations back to VS Code to be handled natively.", "default": { "": true, "": false, "": false } }, "vim.statusBarColorControl": { "type": "boolean", "markdownDescription": "Allow VSCodeVim to change status bar color based on mode. **NOTE:** Using this feature will have a negative impact on performance.", "default": false }, "vim.statusBarColors.normal": { "type": [ "string", "array" ], "description": "Status bar color when in Normal mode.", "default": [ "#005f5f", "#ffffff" ] }, "vim.statusBarColors.insert": { "type": [ "string", "array" ], "description": "Status bar color when in Insert mode.", "default": [ "#5f0000", "#ffffff" ] }, "vim.statusBarColors.visual": { "type": [ "string", "array" ], "description": "Status bar color when in Visual mode.", "default": [ "#5f00af", "#ffffff" ] }, "vim.statusBarColors.visualline": { "type": [ "string", "array" ], "description": "Status bar color when in VisualLine mode.", "default": [ "#005f87", "#ffffff" ] }, "vim.statusBarColors.visualblock": { "type": [ "string", "array" ], "description": "Status bar color when in VisualBlock mode.", "default": [ "#86592d", "#ffffff" ] }, "vim.statusBarColors.replace": { "type": [ "string", "array" ], "description": "Status bar color when in Replace mode.", "default": [ "#00000", "#ffffff" ] }, "vim.statusBarColors.commandlineinprogress": { "type": [ "string", "array" ], "description": "Status bar color when in CommandLineInProgress mode.", "default": [ "#007acc", "#ffffff" ] }, "vim.statusBarColors.searchinprogressmode": { "type": [ "string", "array" ], "description": "Status bar color when in SearchInProgress mode.", "default": [ "#007acc", "#ffffff" ] }, "vim.statusBarColors.easymotionmode": { "type": [ "string", "array" ], "description": "Status bar color when in EasyMotion mode.", "default": [ "#007acc", "#ffffff" ] }, "vim.statusBarColors.easymotioninputmode": { "type": [ "string", "array" ], "description": "Status bar color when in EasyMotionInput mode.", "default": [ "#007acc", "#ffffff" ] }, "vim.statusBarColors.surroundinputmode": { "type": [ "string", "array" ], "description": "Status bar color when in SurroundInput mode.", "default": [ "#007acc", "#ffffff" ] }, "vim.visualstar": { "type": "boolean", "markdownDescription": "In Visual mode, start a search with `*` or `#` using the current selection.", "default": false }, "vim.changeWordIncludesWhitespace": { "type": "boolean", "description": "Includes trailing whitespace when changing word.", "default": false }, "vim.foldfix": { "type": "boolean", "description": "Uses a hack to move around folds properly.", "default": false }, "vim.mouseSelectionGoesIntoVisualMode": { "type": "boolean", "description": "If enabled, dragging with the mouse activates Visual mode.", "default": true }, "vim.disableExtension": { "type": "boolean", "description": "Disables the VSCodeVim extension. The extension will continue to be loaded and activated, but Vim functionality will be disabled.", "default": false }, "vim.enableNeovim": { "type": "boolean", "markdownDescription": "Use Neovim to execute Ex commands. You should restart VS Code after enabling/disabling this for the changes to take effect. **NOTE:** Neovim version 0.2.0 or greater must be installed, and if the executable is not on your PATH, `#vim.neovimPath#` must be set.", "default": false }, "vim.neovimPath": { "type": "string", "markdownDescription": "Path to Neovim executable. For example, `/usr/bin/nvim`, or `C:\\Program Files\\Neovim\\bin\\nvim.exe`.", "default": "", "scope": "machine" }, "vim.neovimUseConfigFile": { "type": "boolean", "markdownDescription": "Use a config file for Neovim, specified by `#vim.neovimConfigPath#`.", "default": false }, "vim.neovimConfigPath": { "type": "string", "markdownDescription": "Path to Neovim configuration file. `#vim.neovimUseConfigFile#` must be enabled. If path is left blank, Neovim will use its default config path, i.e. `~/.config/nvim/init.vim` or 'C:\\Users\\USERNAME\\AppData\\Local\\nvim\\init.vim'.", "default": "", "scope": "machine" }, "vim.vimrc.enable": { "type": "boolean", "markdownDescription": "Use key mappings from a `.vimrc` file.", "default": false }, "vim.vimrc.path": { "type": "string", "markdownDescription": "Path to a Vim configuration file. If unset, it will check for `$HOME/.vscodevimrc`, `$HOME/.vimrc`, `$HOME/_vimrc`, and `$HOME/.config/nvim/init.vim`, in that order." }, "vim.substituteGlobalFlag": { "type": "boolean", "markdownDescription": "Automatically apply the global flag, `/g`, to substitute commands. When set to true, use `/g` to mean only first match should be replaced.", "default": false, "markdownDeprecationMessage": "**Deprecated**: Please use `#vim.gdefault#` instead.", "deprecationMessage": "Deprecated: Please use vim.gdefault instead." }, "vim.gdefault": { "type": "boolean", "markdownDescription": "Automatically apply the global flag, `/g`, to substitute commands. When set to true, use `/g` to mean only first match should be replaced.", "default": false }, "vim.cursorStylePerMode.normal": { "type": "string", "description": "Cursor style for Normal mode.", "enum": [ "", "block", "block-outline", "line", "line-thin", "underline", "underline-thin" ] }, "vim.cursorStylePerMode.insert": { "type": "string", "description": "Cursor style for Insert mode.", "enum": [ "", "block", "block-outline", "line", "line-thin", "underline", "underline-thin" ] }, "vim.cursorStylePerMode.replace": { "type": "string", "description": "Cursor style for Replace mode.", "enum": [ "", "block", "block-outline", "line", "line-thin", "underline", "underline-thin" ] }, "vim.cursorStylePerMode.visual": { "type": "string", "description": "Cursor style for Visual mode.", "enum": [ "", "block", "block-outline", "line", "line-thin", "underline", "underline-thin" ] }, "vim.cursorStylePerMode.visualline": { "type": "string", "description": "Cursor style for VisualLine mode.", "enum": [ "", "block", "block-outline", "line", "line-thin", "underline", "underline-thin" ] }, "vim.cursorStylePerMode.visualblock": { "type": "string", "description": "Cursor style for VisualBlock mode.", "enum": [ "", "block", "block-outline", "line", "line-thin", "underline", "underline-thin" ] }, "vim.autoSwitchInputMethod.enable": { "type": "boolean", "description": "If enabled, the input method switches automatically when the mode changes.", "default": false }, "vim.autoSwitchInputMethod.defaultIM": { "type": "string", "markdownDescription": "The input method for your normal mode, find more information [here](https://github.com/VSCodeVim/Vim#input-method).", "default": "", "scope": "machine" }, "vim.autoSwitchInputMethod.switchIMCmd": { "type": "string", "description": "The shell command to switch input method.", "default": "/path/to/im-select {im}", "scope": "machine" }, "vim.autoSwitchInputMethod.obtainIMCmd": { "type": "string", "description": "The shell command to get current input method.", "default": "/path/to/im-select", "scope": "machine" }, "vim.whichwrap": { "type": "string", "description": "Comma-separated list of motion keys that should wrap to next/previous line.", "default": "b,s" }, "vim.report": { "type": "number", "description": "Threshold for reporting number of lines changed.", "default": 2, "minimum": 1 }, "vim.digraphs": { "type": "object", "description": "Custom digraph shortcuts for inserting special characters, expressed as UTF16 code points.", "default": {} }, "vim.wrapscan": { "type": "boolean", "description": "Searches wrap around the end of the file.", "default": true }, "vim.startofline": { "type": "boolean", "markdownDescription": "When `true` the commands listed below move the cursor to the first non-blank of the line. When `false` the cursor is kept in the same column (if possible). This applies to the commands: ``, ``, ``, ``, `G`, `H`, `M`, `L`, `gg`, and to the commands `d`, `<<` and `>>` with a linewise operator.", "default": true }, "vim.showMarksInGutter": { "type": "boolean", "description": "Show the currently set mark(s) in the gutter.", "default": false }, "vim.shell": { "type": "string", "description": "Path to the shell to use for `!` and `:!` commands.", "default": "" }, "vim.langmap": { "type": "string", "description": "Language map for alternate keyboard layouts. When you are typing text in Insert (or Replace, etc.) mode, the characters are inserted derectly. Otherwise, they are translated based on the provided map." } } }, "languages": [ { "id": "Vimscript", "extensions": [ ".vim", ".vimrc" ], "configuration": "./language-configuration.json" } ], "grammars": [ { "language": "Vimscript", "scopeName": "source.vimscript", "path": "./syntaxes/vimscript.tmLanguage.json" } ] }, "scripts": { "vscode:prepublish": "yarn build", "build": "webpack -c webpack.config.js && npm run commit-hash", "build-dev": "webpack -c webpack.dev.js && npm run commit-hash", "build-test": "gulp prepare-test", "commit-hash": "git rev-parse HEAD > out/version.txt", "test": "node ./out/test/runTest.js", "lint": "eslint .", "lint:fix": "eslint . --fix", "prettier": "npx prettier --write .", "prettier:check": "npx prettier --check .", "watch": "webpack -c webpack.dev.js --watch", "package": "yarn run vsce package --yarn --allow-star-activation", "prepare": "husky" }, "dependencies": { "diff-match-patch": "1.0.5", "lodash": "^4.17.21", "neovim": "5.4.0", "os-browserify": "0.3.0", "parsimmon": "^1.18.0", "path-browserify": "1.0.1", "process": "0.11.10", "queue": "^6.0.2", "untildify": "4.0.0", "util": "0.12.5" }, "devDependencies": { "@types/diff-match-patch": "1.0.36", "@types/lodash": "4.17.23", "@types/minimatch": "5.1.2", "@types/mocha": "10.0.10", "@types/node": "22.19.1", "@types/parsimmon": "1.10.9", "@types/sinon": "17.0.4", "@types/vscode": "1.74.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vscode/test-electron": "2.5.2", "@vscode/vsce": "^3.6.2", "circular-dependency-plugin": "^5.2.2", "clean-webpack-plugin": "4.0.0", "eslint": "^8.52.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-jsdoc": "^62.0.0", "eslint-plugin-prefer-arrow": "^1.2.3", "event-stream": "4.0.1", "fork-ts-checker-webpack-plugin": "9.1.0", "gulp": "5.0.1", "gulp-bump": "3.2.0", "gulp-shell": "^0.8.0", "gulp-tag-version": "1.3.1", "gulp-typescript": "5.0.1", "husky": "^9.0.0", "lint-staged": "^16.0.0", "minimist": "1.2.8", "mocha": "11.7.5", "plugin-error": "2.0.1", "prettier": "3.8.1", "prettier-plugin-organize-imports": "^4.3.0", "sinon": "20.0.0", "ts-loader": "9.5.4", "typescript": "5.9.3", "webpack": "5.105.4", "webpack-cli": "7.0.2", "webpack-merge": "6.0.1" }, "lint-staged": { "*.{ts,js,json,md,yml}": "prettier --write", "*.ts": "eslint --fix" } } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:base", "default:pinDigestsDisabled"], "automerge": false, "ignoreDeps": ["@types/vscode"], "labels": ["pr/dependency"] } ================================================ FILE: src/actions/base.ts ================================================ import { Position } from 'vscode'; import { Cursor } from '../common/motion/cursor'; import { isLiteralMode, unmapLiteral } from '../configuration/langmap'; import { Notation } from '../configuration/notation'; import { isTextTransformation } from '../transformations/transformations'; import { configuration } from './../configuration/configuration'; import { Mode } from './../mode/mode'; import { VimState } from './../state/vimState'; import { ActionType, IBaseAction } from './types'; export abstract class BaseAction implements IBaseAction { abstract readonly actionType: ActionType; public readonly name: string | undefined; /** * If true, the cursor position will be added to the jump list on completion. */ public readonly isJump: boolean = false; /** * If true, the action will create an undo point. */ public readonly createsUndoPoint: boolean = false; /** * If this is being run in multi cursor mode, the index of the cursor * this action is being applied to. */ public multicursorIndex: number | undefined; /** * Whether we should change `vimState.desiredColumn` */ public readonly preservesDesiredColumn: boolean = false; /** * Modes that this action can be run in. */ public abstract readonly modes: readonly Mode[]; /** * The sequence of keys you use to trigger the action, or a list of such sequences. */ public abstract keys: readonly string[] | readonly string[][]; /** * The keys pressed at the time that this action was triggered. */ // TODO: make readonly public keysPressed: string[] = []; private static readonly isSingleNumber: RegExp = /^[0-9]$/; private static readonly isSingleAlpha: RegExp = /^[a-zA-Z]$/; private static readonly isMacroRegister: RegExp = /^[0-9a-zA-Z]$/; /** * Is this action valid in the current Vim state? */ public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { if ( vimState.currentModeIncludingPseudoModes === Mode.OperatorPendingMode && this.actionType === 'command' ) { return false; } return ( this.modes.includes(vimState.currentMode) && BaseAction.CompareKeypressSequence(this.keys, keysPressed) ); } /** * Could the user be in the process of doing this action. */ public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { if ( vimState.currentModeIncludingPseudoModes === Mode.OperatorPendingMode && this.actionType === 'command' ) { return false; } if (!this.modes.includes(vimState.currentMode)) { return false; } const keys2D = BaseAction.is2DArray(this.keys) ? this.keys : [this.keys]; const keysSlice = keys2D.map((x) => x.slice(0, keysPressed.length)); if (!BaseAction.CompareKeypressSequence(keysSlice, keysPressed)) { return false; } return true; } public static CompareKeypressSequence( one: readonly string[] | readonly string[][], two: readonly string[], ): boolean { if (BaseAction.is2DArray(one)) { for (const sequence of one) { if (BaseAction.CompareKeypressSequence(sequence, two)) { return true; } } return false; } if (one.length !== two.length) { return false; } for (let i = 0, j = 0; i < one.length; i++, j++) { const left = one[i]; const right = two[j]; if (left === right && right !== configuration.leader) { continue; } else if (left === '') { continue; } else if (left === '' && right === configuration.leader) { continue; } else if (left === '' && this.isSingleNumber.test(right)) { continue; } else if (left === '' && this.isSingleAlpha.test(right)) { continue; } else if (left === '' && this.isMacroRegister.test(right)) { continue; } else if (['', ''].includes(left) && !Notation.IsControlKey(right)) { continue; } else { return false; } } return true; } public toString(): string { return this.keys.join(''); } private static is2DArray(x: readonly T[] | readonly T[][]): x is readonly T[][] { return Array.isArray(x[0]); } } /** * A command is something like , :, v, i, etc. */ export abstract class BaseCommand extends BaseAction { override actionType: ActionType = 'command' as const; /** * If isCompleteAction is true, then triggering this command is a complete action - * that means that we'll go and try to run it. */ public isCompleteAction = true; /** * In multi-cursor mode, do we run this command for every cursor, or just once? */ public runsOnceForEveryCursor(): boolean { return true; } /** * If true, exec() will get called N times where N is the count. * * If false, exec() will only be called once, and you are expected to * handle count prefixes (e.g. the 3 in 3w) yourself. */ public readonly runsOnceForEachCountPrefix: boolean = false; /** * Run the command a single time. */ public async exec(position: Position, vimState: VimState): Promise { throw new Error('Not implemented!'); } /** * Run the command the number of times VimState wants us to. */ public async execCount(position: Position, vimState: VimState): Promise { const timesToRepeat = this.runsOnceForEachCountPrefix ? vimState.recordedState.count || 1 : 1; if (!this.runsOnceForEveryCursor()) { for (let i = 0; i < timesToRepeat; i++) { await this.exec(position, vimState); } for (const transformation of vimState.recordedState.transformer.transformations) { if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) { transformation.cursorIndex = 0; } } return; } const cursorsToIterateOver = [...vimState.cursors].sort((a, b) => a.start.line > b.start.line || (a.start.line === b.start.line && a.start.character > b.start.character) ? 1 : -1, ); const resultingCursors: Cursor[] = []; for (const [index, { start, stop }] of cursorsToIterateOver.entries()) { this.multicursorIndex = index; vimState.cursorStopPosition = stop; vimState.cursorStartPosition = start; for (let j = 0; j < timesToRepeat; j++) { await this.exec(stop, vimState); } resultingCursors.push(new Cursor(vimState.cursorStartPosition, vimState.cursorStopPosition)); for (const transformation of vimState.recordedState.transformer.transformations) { if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) { transformation.cursorIndex = this.multicursorIndex; } } } vimState.cursors = resultingCursors; } } export enum KeypressState { WaitingOnKeys, NoPossibleMatch, } /** * Every Vim action will be added here with the @RegisterAction decorator. */ const actionMap = new Map BaseAction>>(); /** * Gets the action that should be triggered given a key sequence. * * If there is a definitive action that matched, returns that action. * * If an action could potentially match if more keys were to be pressed, returns `KeyPressState.WaitingOnKeys` * (e.g. you pressed "g" and are about to press "g" action to make the full action "gg") * * If no action could ever match, returns `KeypressState.NoPossibleMatch`. */ export function getRelevantAction( keysPressed: string[], vimState: VimState, ): BaseAction | KeypressState { const possibleActionsForMode = actionMap.get(vimState.currentMode) ?? []; let hasPotentialMatch = false; for (const actionType of possibleActionsForMode) { // TODO: Constructing up to several hundred Actions every time we hit a key is moronic. // I think we can make `doesActionApply` and `couldActionApply` static... const action = new actionType(); if (action.doesActionApply(vimState, keysPressed)) { action.keysPressed = isLiteralMode(vimState.currentMode) ? [...vimState.recordedState.actionKeys] : unmapLiteral(action.keys, vimState.recordedState.actionKeys); return action; } hasPotentialMatch ||= action.couldActionApply(vimState, keysPressed); } return hasPotentialMatch ? KeypressState.WaitingOnKeys : KeypressState.NoPossibleMatch; } export function RegisterAction(action: new () => BaseAction): void { const actionInstance = new action(); for (const modeName of actionInstance.modes) { let actions = actionMap.get(modeName); if (!actions) { actions = []; actionMap.set(modeName, actions); } if (actionInstance.keys === undefined) { // action that can't be called directly continue; } actions.push(action); } } ================================================ FILE: src/actions/baseMotion.ts ================================================ import { Position } from 'vscode'; import { Mode } from '../mode/mode'; import { VimState } from '../state/vimState'; import { clamp } from '../util/util'; import { BaseAction } from './base'; export function isIMovement(o: IMovement | Position): o is IMovement { return (o as IMovement).start !== undefined && (o as IMovement).stop !== undefined; } export enum SelectionType { Concatenating, // Selections that concatenate repeated movements Expanding, // Selections that expand the start and end of the previous selection } /** * The result of a (more sophisticated) Movement. */ export interface IMovement { start: Position; stop: Position; /** * Whether this motion succeeded. Some commands, like fx when 'x' can't be found, * will not move the cursor. Furthermore, dfx won't delete *anything*, even though * deleting to the current character would generally delete 1 character. */ failed?: boolean; /** * Whether this motion resulted in the current multicursor index being removed. * This happens when multiple selections combine into one. */ removed?: boolean; } export function failedMovement(vimState: VimState): IMovement { return { start: vimState.cursorStartPosition, stop: vimState.cursorStopPosition, failed: true, }; } export abstract class BaseMovement extends BaseAction { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; override actionType = 'motion' as const; /** * If movement can be repeated with semicolon or comma this will be true when * running the repetition. */ isRepeat = false; protected selectionType = SelectionType.Concatenating; constructor(keysPressed?: string[], isRepeat?: boolean) { super(); if (keysPressed) { this.keysPressed = keysPressed; } if (isRepeat) { this.isRepeat = isRepeat; } } /** * Run the movement a single time. * * Generally returns a new Position. If necessary, it can return an IMovement instead. * Note: If returning an IMovement, make sure that repeated actions on a * visual selection work. For example, V}} */ public async execAction( position: Position, vimState: VimState, firstIteration: boolean, lastIteration: boolean, ): Promise { throw new Error('Not implemented!'); } /** * Run the movement in an operator context a single time. * * Some movements operate over different ranges when used for operators. */ protected async execActionForOperator( position: Position, vimState: VimState, firstIteration: boolean, lastIteration: boolean, ): Promise { return this.execAction(position, vimState, firstIteration, lastIteration); } /** * Run a movement count times. * * count: the number prefix the user entered, or 0 if they didn't enter one. */ public async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { let result!: Position | IMovement; let prevResult: Position | IMovement = failedMovement(vimState); let firstMovementStart = position; count = clamp(count, 1, 99999); for (let i = 0; i < count; i++) { const firstIteration = i === 0; const lastIteration = i === count - 1; result = vimState.recordedState.operator && lastIteration ? await this.execActionForOperator(position, vimState, firstIteration, lastIteration) : await this.execAction(position, vimState, firstIteration, lastIteration); if (result instanceof Position) { /** * This position will be passed to the `motion` on the next iteration, * it may cause some issues when count > 1. */ position = result; } else { if (result.failed) { return prevResult; } if (firstIteration) { firstMovementStart = result.start; } position = this.adjustPosition(position, result, lastIteration); } prevResult = result; } if (this.selectionType === SelectionType.Concatenating && isIMovement(result)) { result.start = firstMovementStart; } return result; } protected adjustPosition(position: Position, result: IMovement, lastIteration: boolean) { if (!lastIteration) { position = result.stop.getRightThroughLineBreaks(); } return position; } } ================================================ FILE: src/actions/commands/actions.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { Cursor } from '../../common/motion/cursor'; import { VimError } from '../../error'; import { globalState } from '../../state/globalState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { Clipboard } from '../../util/clipboard'; import { SpecialKeys } from '../../util/specialKeys'; import { reportSearch } from '../../util/statusBarTextUtils'; import { getCursorsAfterSync } from '../../util/util'; import { SearchDirection } from '../../vimscript/pattern'; import { shouldWrapKey } from '../wrapping'; import { ExCommandLine, SearchCommandLine } from './../../cmd_line/commandLine'; import { PositionDiff, earlierOf, laterOf, sorted } from './../../common/motion/position'; import { configuration } from './../../configuration/configuration'; import { Mode, visualBlockGetBottomRightPosition, visualBlockGetTopLeftPosition, } from './../../mode/mode'; import { Register, RegisterMode } from './../../register/register'; import { TextEditor } from './../../textEditor'; import { BaseCommand, RegisterAction } from './../base'; import * as operator from './../operator'; @RegisterAction class DisableExtension extends BaseCommand { modes = [ Mode.Normal, Mode.Insert, Mode.Visual, Mode.VisualBlock, Mode.VisualLine, Mode.SearchInProgressMode, Mode.CommandlineInProgress, Mode.Replace, Mode.EasyMotionMode, Mode.EasyMotionInputMode, Mode.SurroundInputMode, ]; keys = [SpecialKeys.ExtensionDisable]; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Disabled); } } @RegisterAction class EnableExtension extends BaseCommand { modes = [Mode.Disabled]; keys = [SpecialKeys.ExtensionEnable]; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction export class CommandNumber extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['']; override name = 'cmd_num'; override isCompleteAction = false; override actionType = 'number' as const; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { const num = parseInt(this.keysPressed[0], 10); const operatorCount = vimState.recordedState.operatorCount; if (operatorCount > 0) { const lastAction = vimState.recordedState.actionsRun[vimState.recordedState.actionsRun.length - 2]; if (!(lastAction instanceof CommandNumber)) { // We have set an operatorCount !== 0 after an operator, but now we got another count // number so we need to multiply them. vimState.recordedState.count = operatorCount * num; } else { // We are now getting another digit which means we need to multiply by 10 and add // the new digit multiplied by operatorCount. // // Example: user presses '2d31w': // - After '2' the number 2 is stored in 'count' // - After 'd' the count (2) is stored in 'operatorCount' // - After '3' the number 3 multiplied by 'operatorCount' (3 x 2 = 6) is stored in 'count' // - After '1' the count is multiplied by 10 and added by number 1 multiplied by 'operatorCount' // (6 * 10 + 1 * 2 = 62) // The final result will be the deletion of 62 words. vimState.recordedState.count = vimState.recordedState.count * 10 + num * operatorCount; } } else { vimState.recordedState.count = vimState.recordedState.count * 10 + num; } } public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { const isZero = keysPressed[0] === '0'; return ( super.doesActionApply(vimState, keysPressed) && ((isZero && vimState.recordedState.count > 0) || !isZero) ); } public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { const isZero = keysPressed[0] === '0'; return ( super.couldActionApply(vimState, keysPressed) && ((isZero && vimState.recordedState.count > 0) || !isZero) ); } } @RegisterAction export class CommandRegister extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['"', '']; override name = 'cmd_register'; override isCompleteAction = false; public override async exec(position: Position, vimState: VimState): Promise { const register = this.keysPressed[1]; if (Register.isValidRegister(register)) { vimState.recordedState.registerName = register; } else { // TODO: Changing isCompleteAction here is maybe a bit janky - should it be a function? this.isCompleteAction = true; } } } @RegisterAction class CommandEsc extends BaseCommand { modes = [ Mode.Visual, Mode.VisualLine, Mode.VisualBlock, Mode.Normal, Mode.SurroundInputMode, Mode.EasyMotionMode, Mode.EasyMotionInputMode, ]; keys = [[''], [''], ['']]; override runsOnceForEveryCursor() { return false; } override preservesDesiredColumn = true; public override async exec(position: Position, vimState: VimState): Promise { if (vimState.currentMode === Mode.Normal) { vimState.surround = undefined; if (vimState.isMultiCursor) { vimState.cursors = [vimState.cursor]; } else { // If there's nothing to do on the vim side, we might as well call some // of vscode's default "close notification" actions. I think we should // just add to this list as needed. await Promise.allSettled([ vscode.commands.executeCommand('closeReferenceSearchEditor'), vscode.commands.executeCommand('closeMarkersNavigation'), // TODO: closeDirtyDiff renamed to closeQuickDiff (see microsoft/vscode#235601) vscode.commands.executeCommand('closeDirtyDiff'), vscode.commands.executeCommand('closeQuickDiff'), vscode.commands.executeCommand('editor.action.inlineSuggest.hide'), ]); } } else { if (vimState.currentMode === Mode.EasyMotionMode) { vimState.easyMotion.clearDecorations(vimState.editor); } else if (vimState.currentMode === Mode.SurroundInputMode) { vimState.surround = undefined; } await vimState.setCurrentMode(Mode.Normal); } } } /** * Our Vim implementation selects up to but not including the final character * of a visual selection, instead opting to render a block cursor on the final * character. This works for everything except VSCode's native copy command, * which loses the final character because it's not selected. We override that * copy command here by default to include the final character. */ @RegisterAction class CommandOverrideCopy extends BaseCommand { modes = [Mode.Visual, Mode.VisualLine, Mode.VisualBlock, Mode.Insert, Mode.Normal]; keys = ['']; // A special key - see ModeHandler override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { let text = ''; if (vimState.currentMode === Mode.Visual) { text = vimState.cursors .map((range) => { const [start, stop] = sorted(range.start, range.stop); return vimState.document.getText(new vscode.Range(start, stop.getRight())); }) .join('\n'); } else if (vimState.currentMode === Mode.VisualLine) { text = vimState.cursors .map((range) => { return vimState.document.getText( new vscode.Range( earlierOf(range.start.getLineBegin(), range.stop.getLineBegin()), laterOf(range.start.getLineEnd(), range.stop.getLineEnd()), ), ); }) .join('\n'); } else if (vimState.currentMode === Mode.VisualBlock) { for (const { line } of TextEditor.iterateLinesInBlock(vimState)) { text += line + '\n'; } } else if (vimState.currentMode === Mode.Insert || vimState.currentMode === Mode.Normal) { text = vimState.editor.selections .map((selection) => { return vimState.document.getText(new vscode.Range(selection.start, selection.end)); }) .join('\n'); } const editorSelection = vimState.editor.selection; const hasSelectedText = !editorSelection.active.isEqual(editorSelection.anchor); if (hasSelectedText) { await Clipboard.Copy(text); } // all vim yank operations return to normal mode. await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class CommandCmdA extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { vimState.cursorStartPosition = new Position(0, vimState.desiredColumn); vimState.cursorStopPosition = new Position( vimState.document.lineCount - 1, vimState.desiredColumn, ); await vimState.setCurrentMode(Mode.VisualLine); } } @RegisterAction class MarkCommand extends BaseCommand { keys = ['m', '']; modes = [Mode.Normal]; public override async exec(position: Position, vimState: VimState): Promise { const markName = this.keysPressed[1]; vimState.historyTracker.addMark(vimState.document, position, markName); } } @RegisterAction class ShowCommandLine extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = [':']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { let commandLineText: string; if (vimState.currentMode === Mode.Normal) { if (vimState.recordedState.count) { commandLineText = `.,.+${vimState.recordedState.count - 1}`; } else { commandLineText = ''; } } else { commandLineText = "'<,'>"; } const previousMode = vimState.currentMode; await vimState.setCurrentMode(Mode.CommandlineInProgress); // TODO: Change or supplement `setCurrentMode` API so this isn't necessary if (vimState.modeData.mode === Mode.CommandlineInProgress) { vimState.modeData.commandLine = new ExCommandLine(commandLineText, previousMode); } } } @RegisterAction export class ShowCommandHistory extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['q', ':']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { const cmd = await vscode.window.showQuickPick(ExCommandLine.history.get().slice().reverse(), { placeHolder: 'Vim command history', ignoreFocusOut: false, }); if (cmd && cmd.length !== 0) { await new ExCommandLine(cmd, vimState.currentMode).run(vimState); } await vimState.setCurrentMode(Mode.Normal); } } ExCommandLine.onSearch = async (vimState: VimState) => { void new ShowCommandHistory().exec(vimState.cursorStopPosition, vimState); }; @RegisterAction export class ShowSearchHistory extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = [ ['q', '/'], ['q', '?'], ]; private direction = SearchDirection.Forward; override runsOnceForEveryCursor() { return false; } public constructor(direction = SearchDirection.Forward) { super(); this.direction = direction; } public override async exec(position: Position, vimState: VimState): Promise { if (this.keysPressed.includes('?')) { this.direction = SearchDirection.Backward; } const searchState = await SearchCommandLine.showSearchHistory(); if (searchState) { globalState.searchState = searchState; globalState.hl = true; const nextMatch = searchState.getNextSearchMatchPosition( vimState, vimState.cursorStartPosition, this.direction, ); if (!nextMatch) { throw this.direction === SearchDirection.Forward ? VimError.SearchHitBottom(searchState.searchString) : VimError.SearchHitTop(searchState.searchString); } vimState.cursorStopPosition = nextMatch.pos; reportSearch(nextMatch.index, searchState.getMatchRanges(vimState).length, vimState); } await vimState.setCurrentMode(Mode.Normal); } } // Register the command to execute on CtrlF. SearchCommandLine.onSearch = async (vimState: VimState, direction: SearchDirection) => { return new ShowSearchHistory(direction).exec(vimState.cursorStopPosition, vimState); }; @RegisterAction class DotRepeat extends BaseCommand { modes = [Mode.Normal]; keys = ['.']; override createsUndoPoint = true; public override async execCount(position: Position, vimState: VimState): Promise { if (globalState.previousFullAction) { const count = vimState.recordedState.count || 1; vimState.recordedState.transformer.addTransformation({ type: 'replayRecordedState', count, recordedState: globalState.previousFullAction, }); } else { // No previous action to repeat, so mark this as non-repeatable vimState.lastCommandDotRepeatable = false; } } } @RegisterAction class RepeatSubstitution extends BaseCommand { modes = [Mode.Normal]; keys = ['&']; override createsUndoPoint = true; public override async exec(position: Position, vimState: VimState): Promise { // Parsing the command from a string, while not ideal, is currently // necessary to make this work with and without neovim integration await ExCommandLine.parser.tryParse('s').command.execute(vimState); } } @RegisterAction class GoToOtherEndOfHighlightedText extends BaseCommand { modes = [Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['o']; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { [vimState.cursorStartPosition, vimState.cursorStopPosition] = [ vimState.cursorStopPosition, vimState.cursorStartPosition, ]; } } @RegisterAction class GoToOtherSideOfHighlightedText extends BaseCommand { modes = [Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['O']; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { if (vimState.currentMode === Mode.VisualBlock) { [vimState.cursorStartPosition, vimState.cursorStopPosition] = [ new Position(vimState.cursorStartPosition.line, vimState.cursorStopPosition.character), new Position(vimState.cursorStopPosition.line, vimState.cursorStartPosition.character), ]; } else { return new GoToOtherEndOfHighlightedText().exec(position, vimState); } } } @RegisterAction class DeleteToLineEnd extends BaseCommand { modes = [Mode.Normal]; keys = ['D']; override createsUndoPoint = true; public override async exec(position: Position, vimState: VimState): Promise { if (position.isLineEnd(vimState.document)) { return; } const linesDown = (vimState.recordedState.count || 1) - 1; const start = position; const end = position.getDown(linesDown).getLineEnd().getLeftThroughLineBreaks(); await new operator.DeleteOperator(this.multicursorIndex).run(vimState, start, end); } } @RegisterAction class YankLine extends BaseCommand { modes = [Mode.Normal]; keys = ['Y']; override name = 'yank_full_line'; public override async exec(position: Position, vimState: VimState): Promise { const linesDown = (vimState.recordedState.count || 1) - 1; const start = position.getLineBegin(); const end = position.getDown(linesDown).getLeft(); vimState.currentRegisterMode = RegisterMode.LineWise; await new operator.YankOperator(this.multicursorIndex).run(vimState, start, end); } } @RegisterAction class ChangeToLineEnd extends BaseCommand { modes = [Mode.Normal]; keys = ['C']; override runsOnceForEachCountPrefix = false; public override async exec(position: Position, vimState: VimState): Promise { const count = vimState.recordedState.count || 1; await new operator.ChangeOperator(this.multicursorIndex).run( vimState, position, position .getDown(Math.max(0, count - 1)) .getLineEnd() .getLeft(), ); } } @RegisterAction class ChangeLine extends BaseCommand { modes = [Mode.Normal]; keys = ['S']; override runsOnceForEachCountPrefix = false; public override async exec(position: Position, vimState: VimState): Promise { await new operator.ChangeOperator(this.multicursorIndex).runRepeat( vimState, position, vimState.recordedState.count || 1, ); } // Don't clash with sneak public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return super.doesActionApply(vimState, keysPressed) && !configuration.sneak; } public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { return super.couldActionApply(vimState, keysPressed) && !configuration.sneak; } } @RegisterAction class ActionDeleteChar extends BaseCommand { modes = [Mode.Normal]; keys = ['x']; override name = 'delete_char'; override createsUndoPoint = true; public override async exec(position: Position, vimState: VimState): Promise { // If line is empty, do nothing if (vimState.document.lineAt(position).text.length === 0) { return; } const timesToRepeat = vimState.recordedState.count || 1; await new operator.DeleteOperator(this.multicursorIndex).run( vimState, position, position.getRight(timesToRepeat - 1).getLeftIfEOL(), ); await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class ActionDeleteCharWithDeleteKey extends BaseCommand { modes = [Mode.Normal]; keys = ['']; override name = 'delete_char_with_del'; override runsOnceForEachCountPrefix = true; override createsUndoPoint = true; public override async execCount(position: Position, vimState: VimState): Promise { // If has a count in front of it, then deletes a character // off the count. Therefore, 100x, would apply 'x' 10 times. // http://vimdoc.sourceforge.net/htmldoc/change.html# if (vimState.recordedState.count !== 0) { vimState.recordedState.count = Math.floor(vimState.recordedState.count / 10); // Change actionsRunPressedKeys so that showCmd updates correctly vimState.recordedState.actionsRunPressedKeys = vimState.recordedState.count > 0 ? vimState.recordedState.count.toString().split('') : []; this.isCompleteAction = false; } else { await new ActionDeleteChar().execCount(position, vimState); } } } @RegisterAction class ActionDeleteLastChar extends BaseCommand { modes = [Mode.Normal]; keys = ['X']; override name = 'delete_last_char'; override createsUndoPoint = true; public override async exec(position: Position, vimState: VimState): Promise { if (position.character === 0) { return; } const timesToRepeat = vimState.recordedState.count || 1; await new operator.DeleteOperator(this.multicursorIndex).run( vimState, position.getLeft(timesToRepeat), position.getLeft(), ); } } @RegisterAction class VisualBlockDelete extends BaseCommand { modes = [Mode.VisualBlock]; keys = [['d'], ['x'], ['X']]; override createsUndoPoint = true; public override async exec(position: Position, vimState: VimState): Promise { const lines: string[] = []; for (const { line, start, end } of TextEditor.iterateLinesInBlock(vimState)) { lines.push(line); vimState.recordedState.transformer.addTransformation({ type: 'deleteRange', range: new vscode.Range(start, end), manuallySetCursorPositions: true, }); } const text = lines.length === 1 ? lines[0] : lines.join('\n'); vimState.currentRegisterMode = RegisterMode.BlockWise; Register.put(vimState, text, this.multicursorIndex, true); vimState.cursors = [ Cursor.atPosition( visualBlockGetTopLeftPosition(vimState.cursorStopPosition, vimState.cursorStartPosition), ), ]; await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class VisualBlockDeleteToLineEnd extends BaseCommand { modes = [Mode.VisualBlock]; keys = ['D']; override createsUndoPoint = true; public override async exec(position: Position, vimState: VimState): Promise { const lines: string[] = []; for (const { start } of TextEditor.iterateLinesInBlock(vimState)) { const range = new vscode.Range(start, start.getLineEnd()); lines.push(vimState.editor.document.getText(range)); vimState.recordedState.transformer.addTransformation({ type: 'deleteRange', range, manuallySetCursorPositions: true, }); } const topLeft = visualBlockGetTopLeftPosition( vimState.cursorStopPosition, vimState.cursorStartPosition, ); const text = lines.length === 1 ? lines[0] : lines.join('\n'); Register.put(vimState, text, this.multicursorIndex, true); vimState.cursors = [Cursor.atPosition(topLeft)]; await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class VisualBlockInsert extends BaseCommand { modes = [Mode.VisualBlock]; keys = ['I']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { const cursors: Cursor[] = []; for (const cursor of vimState.cursors) { for (const { line, start } of TextEditor.iterateLinesInBlock(vimState, cursor)) { if (line === '' && start.character !== 0) { continue; } cursors.push(Cursor.atPosition(start)); } } vimState.cursors = cursors; await vimState.setCurrentMode(Mode.Insert); vimState.isFakeMultiCursor = true; } } @RegisterAction class VisualBlockChange extends BaseCommand { modes = [Mode.VisualBlock]; keys = [['c'], ['s']]; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { const cursors: Cursor[] = []; const lines: string[] = []; for (const cursor of vimState.cursors) { const width = 1 + visualBlockGetBottomRightPosition(cursor.start, cursor.stop).character - visualBlockGetTopLeftPosition(cursor.start, cursor.stop).character; for (const { line, start, end } of TextEditor.iterateLinesInBlock(vimState, cursor)) { // TODO: is this behavior consistent with similar actions like VisualBlock `d`? lines.push(line.padEnd(width, ' ')); if (line) { vimState.recordedState.transformer.addTransformation({ type: 'deleteRange', range: new vscode.Range(start, end), manuallySetCursorPositions: true, }); cursors.push(Cursor.atPosition(start)); } } } vimState.cursors = cursors; const text = lines.length === 1 ? lines[0] : lines.join('\n'); Register.put(vimState, text, this.multicursorIndex, true); await vimState.setCurrentMode(Mode.Insert); vimState.isFakeMultiCursor = true; } } @RegisterAction class ActionChangeToEOLInVisualBlockMode extends BaseCommand { modes = [Mode.VisualBlock]; keys = ['C']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { const cursors: Cursor[] = []; for (const cursor of vimState.cursors) { for (const { start, end } of TextEditor.iterateLinesInBlock(vimState, cursor)) { vimState.recordedState.transformer.delete(new vscode.Range(start, start.getLineEnd())); cursors.push(Cursor.atPosition(end)); } } vimState.cursors = cursors; await vimState.setCurrentMode(Mode.Insert); vimState.isFakeMultiCursor = true; } } abstract class ActionGoToInsertVisualLineModeCommand extends BaseCommand { override runsOnceForEveryCursor() { return false; } abstract getCursorRangeForLine( line: vscode.TextLine, selectionStart: Position, selectionEnd: Position, ): Cursor; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Insert); vimState.isFakeMultiCursor = true; const resultingCursors: Cursor[] = []; const cursorsOnBlankLines: Cursor[] = []; for (const selection of vimState.editor.selections) { const { start, end } = selection; for (let i = start.line; i <= end.line; i++) { const line = vimState.document.lineAt(i); const cursorRange = this.getCursorRangeForLine(line, start, end); if (!line.isEmptyOrWhitespace) { resultingCursors.push(cursorRange); } else { cursorsOnBlankLines.push(cursorRange); } } } if (resultingCursors.length > 0) { vimState.cursors = resultingCursors; } else { vimState.cursors = cursorsOnBlankLines; } } } @RegisterAction class VisualLineInsert extends ActionGoToInsertVisualLineModeCommand { modes = [Mode.VisualLine]; keys = ['I']; getCursorRangeForLine(line: vscode.TextLine): Cursor { return Cursor.atPosition(new Position(line.lineNumber, line.firstNonWhitespaceCharacterIndex)); } } @RegisterAction class VisualLineAppend extends ActionGoToInsertVisualLineModeCommand { modes = [Mode.VisualLine]; keys = ['A']; getCursorRangeForLine(line: vscode.TextLine): Cursor { return Cursor.atPosition(new Position(line.lineNumber, line.range.end.character)); } } @RegisterAction class VisualInsert extends ActionGoToInsertVisualLineModeCommand { modes = [Mode.Visual]; keys = ['I']; getCursorRangeForLine( line: vscode.TextLine, selectionStart: Position, selectionEnd: Position, ): Cursor { return Cursor.atPosition( line.lineNumber === selectionStart.line ? selectionStart : new Position(line.lineNumber, line.firstNonWhitespaceCharacterIndex), ); } } @RegisterAction class VisualAppend extends ActionGoToInsertVisualLineModeCommand { modes = [Mode.Visual]; keys = ['A']; getCursorRangeForLine( line: vscode.TextLine, selectionStart: Position, selectionEnd: Position, ): Cursor { return Cursor.atPosition( line.lineNumber === selectionEnd.line ? selectionEnd : new Position(line.lineNumber, line.range.end.character), ); } } @RegisterAction class VisualBlockAppend extends BaseCommand { modes = [Mode.VisualBlock]; keys = ['A']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { const newCursors: Cursor[] = []; for (const cursor of vimState.cursors) { const [start, end] = sorted(cursor.start, cursor.stop); for (let lineNum = start.line; lineNum <= end.line; lineNum++) { const line = vimState.document.lineAt(lineNum); const insertionColumn = vimState.desiredColumn === Number.POSITIVE_INFINITY ? line.text.length : Math.max(cursor.start.character, cursor.stop.character) + 1; if (line.text.length < insertionColumn) { await TextEditor.insert( vimState.editor, ' '.repeat(insertionColumn - line.text.length), line.range.end, false, ); } newCursors.push(Cursor.atPosition(new Position(lineNum, insertionColumn))); } } vimState.cursors = newCursors; await vimState.setCurrentMode(Mode.Insert); vimState.isFakeMultiCursor = true; } } @RegisterAction class VisualLineDeleteChar extends BaseCommand { modes = [Mode.VisualLine]; keys = ['x']; override name = 'delete_char_visual_line_mode'; public override async exec(position: Position, vimState: VimState): Promise { const [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition); await new operator.DeleteOperator(this.multicursorIndex).run( vimState, start.getLineBegin(), end.getLineEnd(), ); } } @RegisterAction class VisualDeleteLine extends BaseCommand { modes = [Mode.Visual, Mode.VisualLine]; keys = ['X']; public override async exec(position: Position, vimState: VimState): Promise { const [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition); await new operator.DeleteOperator(this.multicursorIndex).run( vimState, start.getLineBegin(), end.getLineEnd(), ); } } @RegisterAction class VisualChangeLine extends BaseCommand { modes = [Mode.Visual, Mode.VisualLine]; keys = [['C'], ['R']]; public override async exec(position: Position, vimState: VimState): Promise { const [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition); await new operator.ChangeOperator(this.multicursorIndex).run( vimState, start.getLineBegin(), end.getLineEnd().getLeftIfEOL(), ); } } @RegisterAction class VisualChangeLine_2 extends BaseCommand { modes = [Mode.Visual, Mode.VisualLine]; keys = ['S']; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return !configuration.surround && super.doesActionApply(vimState, keysPressed); } public override async exec(position: Position, vimState: VimState): Promise { await new VisualChangeLine().exec(position, vimState); } } @RegisterAction class VisualBlockChangeLine extends BaseCommand { modes = [Mode.VisualBlock]; keys = [['R'], ['S']]; public override async exec(position: Position, vimState: VimState): Promise { return new VisualChangeLine().exec(position, vimState); } } @RegisterAction class ChangeChar extends BaseCommand { modes = [Mode.Normal]; keys = ['s']; public override async exec(position: Position, vimState: VimState): Promise { await new operator.ChangeOperator(this.multicursorIndex).run( vimState, position, position.getRight((vimState.recordedState.count || 1) - 1), ); } // Don't clash with surround or sneak modes! public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return ( super.doesActionApply(vimState, keysPressed) && !configuration.sneak && !vimState.recordedState.operator ); } public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { return ( super.couldActionApply(vimState, keysPressed) && !configuration.sneak && !vimState.recordedState.operator ); } } @RegisterAction class ToggleCaseAndMoveForward extends BaseCommand { modes = [Mode.Normal]; keys = ['~']; override createsUndoPoint = true; private toggleCase(text: string): string { let newText = ''; for (const char of text) { let toggled = char.toLocaleLowerCase(); if (toggled === char) { toggled = char.toLocaleUpperCase(); } newText += toggled; } return newText; } public override async exec(position: Position, vimState: VimState): Promise { const count = vimState.recordedState.count || 1; const range = new vscode.Range( position, shouldWrapKey(vimState.currentMode, '~') ? position.getOffsetThroughLineBreaks(count) : position.getRight(count), ); vimState.recordedState.transformer.replace( range, this.toggleCase(vimState.document.getText(range)), PositionDiff.exactPosition(range.end), ); } } @RegisterAction export class CommandUnicodeName extends BaseCommand { modes = [Mode.Normal]; keys = ['g', 'a']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { const char = vimState.document.getText(new vscode.Range(position, position.getRight())); const charCode = char.charCodeAt(0); // TODO: Handle charCode > 127 by also including StatusBar.setText( vimState, `<${char}> ${charCode}, Hex ${charCode.toString(16)}, Octal ${charCode.toString(8)}`, ); } } @RegisterAction class ShowHover extends BaseCommand { modes = [Mode.Normal]; keys = ['g', 'h']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await vscode.commands.executeCommand('editor.action.showHover'); } } /** * Multi-Cursor Command Overrides * * We currently have to override the VSCode key commands that get us into multi-cursor mode. * * Normally, we'd just listen for another cursor to be added in order to go into multi-cursor * mode rather than rewriting each keybinding one-by-one. We can't currently do that because * Visual Block Mode also creates additional cursors, but will get confused if you're in * multi-cursor mode. */ @RegisterAction export class ActionOverrideCmdD extends BaseCommand { modes = [Mode.Normal, Mode.Visual]; keys = [[''], ['g', 'b']]; override runsOnceForEveryCursor() { return false; } override runsOnceForEachCountPrefix = true; public override async exec(position: Position, vimState: VimState): Promise { await vscode.commands.executeCommand('editor.action.addSelectionToNextFindMatch'); vimState.cursors = getCursorsAfterSync(vimState.editor); // If this is the first cursor, select 1 character less // so that only the word is selected, no extra character vimState.cursors = vimState.cursors.map((x) => x.withNewStop(x.stop.getLeft())); await vimState.setCurrentMode(Mode.Visual); } } @RegisterAction class ActionOverrideCmdDInsert extends BaseCommand { modes = [Mode.Insert]; keys = ['']; override runsOnceForEveryCursor() { return false; } override runsOnceForEachCountPrefix = true; public override async exec(position: Position, vimState: VimState): Promise { // Since editor.action.addSelectionToNextFindMatch uses the selection to // determine where to add a word, we need to do a hack and manually set the // selections to the word boundaries before we make the api call. vimState.editor.selections = vimState.editor.selections.map((x, idx) => { const curPos = x.active; if (idx === 0) { return new vscode.Selection( curPos.prevWordStart(vimState.document), curPos.getLeft().nextWordEnd(vimState.document, { inclusive: true }).getRight(), ); } else { // Since we're adding the selections ourselves, we need to make sure // that our selection is actually over what our original word is const matchWordPos = vimState.editor.selections[0].active; const matchWordLength = matchWordPos.getLeft().nextWordEnd(vimState.document, { inclusive: true }).getRight() .character - matchWordPos.prevWordStart(vimState.document).character; const wordBegin = curPos.getLeft(matchWordLength); return new vscode.Selection(wordBegin, curPos); } }); vimState.recordedState.transformer.vscodeCommand('editor.action.addSelectionToNextFindMatch'); } } @RegisterAction class InsertCursorBelow extends BaseCommand { modes = [Mode.Normal, Mode.Visual]; keys = [ [''], // OSX [''], // Windows ]; override runsOnceForEveryCursor() { return false; } override runsOnceForEachCountPrefix = true; public override async exec(position: Position, vimState: VimState): Promise { vimState.recordedState.transformer.vscodeCommand('editor.action.insertCursorBelow'); } } @RegisterAction class InsertCursorAbove extends BaseCommand { modes = [Mode.Normal, Mode.Visual]; keys = [ [''], // OSX [''], // Windows ]; override runsOnceForEveryCursor() { return false; } override runsOnceForEachCountPrefix = true; public override async exec(position: Position, vimState: VimState): Promise { vimState.recordedState.transformer.vscodeCommand('editor.action.insertCursorAbove'); } } @RegisterAction class ShowFileOutline extends BaseCommand { modes = [Mode.Normal]; keys = ['g', 'O']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await vscode.commands.executeCommand('outline.focus'); } } ================================================ FILE: src/actions/commands/commandLine.ts ================================================ import * as vscode from 'vscode'; import { CommandLine, ExCommandLine, SearchCommandLine } from '../../cmd_line/commandLine'; import { ChangeCommand } from '../../cmd_line/commands/change'; import { VimError } from '../../error'; import { Mode } from '../../mode/mode'; import { Register, RegisterMode } from '../../register/register'; import { RecordedState } from '../../state/recordedState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { TextEditor } from '../../textEditor'; import { Clipboard } from '../../util/clipboard'; import { getPathDetails, readDirectory } from '../../util/path'; import { builtinExCommands } from '../../vimscript/exCommandParser'; import { SearchDirection } from '../../vimscript/pattern'; import { BaseCommand, RegisterAction } from '../base'; abstract class CommandLineAction extends BaseCommand { modes = [Mode.CommandlineInProgress, Mode.SearchInProgressMode]; override runsOnceForEveryCursor() { return false; } protected abstract run(vimState: VimState, commandLine: CommandLine): Promise; public override async exec(position: vscode.Position, vimState: VimState): Promise { if ( !( vimState.modeData.mode === Mode.CommandlineInProgress || vimState.modeData.mode === Mode.SearchInProgressMode ) ) { throw new Error(`Unexpected mode ${vimState.modeData.mode} in CommandLineAction`); } await this.run(vimState, vimState.modeData.commandLine); } } @RegisterAction class CommandLineTab extends CommandLineAction { override modes = [Mode.CommandlineInProgress]; keys = [[''], ['']]; private cycleCompletion(isTabForward: boolean, commandLine: ExCommandLine) { const autoCompleteItems = commandLine.autoCompleteItems; if (autoCompleteItems.length === 0) { return; } commandLine.autoCompleteIndex = isTabForward ? (commandLine.autoCompleteIndex + 1) % autoCompleteItems.length : (commandLine.autoCompleteIndex - 1 + autoCompleteItems.length) % autoCompleteItems.length; const lastPos = commandLine.preCompleteCharacterPos; const lastCmd = commandLine.preCompleteCommand; const evalCmd = lastCmd.slice(0, lastPos); const restCmd = lastCmd.slice(lastPos); commandLine.text = evalCmd + autoCompleteItems[commandLine.autoCompleteIndex] + restCmd; commandLine.cursorIndex = commandLine.text.length - restCmd.length; } protected async run(vimState: VimState, commandLine: CommandLine): Promise { if (!(commandLine instanceof ExCommandLine)) { throw new Error('Expected ExCommandLine in CommandLineTab::run()'); } const key = this.keysPressed[0]; const isTabForward = key === ''; // If we hit twice in a row, definitely cycle if ( commandLine.autoCompleteItems.length !== 0 && vimState.recordedState.actionsRun[vimState.recordedState.actionsRun.length - 2] instanceof CommandLineTab ) { this.cycleCompletion(isTabForward, commandLine); return; } let newCompletionItems: string[] = []; // Sub string since vim does completion before the cursor let evalCmd = commandLine.text.slice(0, commandLine.cursorIndex); const restCmd = commandLine.text.slice(commandLine.cursorIndex); // \s* is the match the extra space before any character like ': edit' const cmdRegex = /^\s*\w+$/; const fileRegex = /^\s*\w+\s+/g; if (cmdRegex.test(evalCmd)) { // Command completion newCompletionItems = builtinExCommands .map((pair) => pair[0][0] + pair[0][1]) .filter((cmd) => cmd.startsWith(evalCmd)) // Remove the already typed portion in the array .map((cmd) => cmd.slice(cmd.search(evalCmd) + evalCmd.length)) .sort(); } else if (fileRegex.exec(evalCmd)) { // File completion by searching if there is a space after the first word/command // ideally it should be a process of white-listing to selected commands like :e and :vsp const filePathInCmd = evalCmd.substring(fileRegex.lastIndex); const currentUri = vimState.document.uri; const isRemote = !!vscode.env.remoteName; const { fullDirPath, baseName, partialPath, path: p, } = getPathDetails(filePathInCmd, currentUri, isRemote); // Update the evalCmd in case of windows, where we change / to \ evalCmd = evalCmd.slice(0, fileRegex.lastIndex) + partialPath; // test if the baseName is . or .. const shouldAddDotItems = /^\.\.?$/g.test(baseName); const dirItems = await readDirectory( fullDirPath, p.sep, currentUri, isRemote, shouldAddDotItems, ); const startWithBaseNameRegex = new RegExp( `^${baseName}`, process.platform === 'win32' ? 'i' : '', ); newCompletionItems = dirItems .map((name): [RegExpExecArray | null, string] => [startWithBaseNameRegex.exec(name), name]) .filter(([isMatch]) => isMatch !== null) .map(([match, name]) => name.slice(match![0].length)) .sort(); } const newIndex = isTabForward ? 0 : newCompletionItems.length - 1; commandLine.autoCompleteIndex = newIndex; // If here only one items we fill cmd direct, so the next tab will not cycle the one item array commandLine.autoCompleteItems = newCompletionItems.length <= 1 ? [] : newCompletionItems; commandLine.preCompleteCharacterPos = commandLine.cursorIndex; commandLine.preCompleteCommand = evalCmd + restCmd; const completion = newCompletionItems.length === 0 ? '' : newCompletionItems[newIndex]; commandLine.text = evalCmd + completion + restCmd; commandLine.cursorIndex = commandLine.text.length - restCmd.length; } } @RegisterAction class ExCommandLineEnter extends CommandLineAction { override modes = [Mode.CommandlineInProgress]; keys = [['\n'], ['']]; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { await commandLine.run(vimState); if (commandLine instanceof ExCommandLine && commandLine.getCommand() instanceof ChangeCommand) { return; } await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class SearchCommandLineEnter extends CommandLineAction { override modes = [Mode.SearchInProgressMode]; keys = [['\n'], ['']]; override runsOnceForEveryCursor() { return true; } override isJump = true; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { await commandLine.run(vimState); if (this.multicursorIndex === vimState.cursors.length - 1) { // TODO: gah, this is stupid await vimState.setCurrentMode(commandLine.previousMode); } } } @RegisterAction class CommandLineEscape extends CommandLineAction { keys = [[''], [''], ['']]; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { await commandLine.escape(vimState); } } @RegisterAction class CommandLineCtrlF extends CommandLineAction { keys = ['']; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { await commandLine.ctrlF(vimState); } } @RegisterAction class CommandLineBackspace extends CommandLineAction { keys = [[''], [''], ['']]; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { await commandLine.backspace(vimState); } } @RegisterAction class CommandLineDelete extends CommandLineAction { keys = ['']; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { await commandLine.delete(vimState); } } @RegisterAction class CommandlineHome extends CommandLineAction { keys = [[''], ['']]; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { commandLine.home(); } } @RegisterAction class CommandLineEnd extends CommandLineAction { keys = [[''], ['']]; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { commandLine.end(); } } @RegisterAction class CommandLineDeleteWord extends CommandLineAction { keys = [[''], ['']]; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { commandLine.deleteWord(); } } @RegisterAction class CommandLineDeleteToBeginning extends CommandLineAction { keys = ['']; protected override async run(vimState: VimState, commandLine: CommandLine): Promise { commandLine.deleteToBeginning(); } } @RegisterAction class CommandLineWordLeft extends CommandLineAction { keys = ['']; protected async run(vimState: VimState, commandLine: CommandLine): Promise { commandLine.wordLeft(); } } @RegisterAction class CommandLineWordRight extends CommandLineAction { keys = ['']; protected async run(vimState: VimState, commandLine: CommandLine): Promise { commandLine.wordRight(); } } @RegisterAction class CommandLineHistoryBack extends CommandLineAction { keys = [[''], ['']]; protected async run(vimState: VimState, commandLine: CommandLine): Promise { commandLine.historyBack(); } } @RegisterAction class CommandLineHistoryForward extends CommandLineAction { keys = [[''], ['']]; protected async run(vimState: VimState, commandLine: CommandLine): Promise { commandLine.historyForward(); } } @RegisterAction class CommandInsertRegisterContentInCommandLine extends CommandLineAction { keys = ['', '']; override isCompleteAction = false; protected async run(vimState: VimState, commandLine: CommandLine): Promise { const registerKey = this.keysPressed[1]; if (!Register.isValidRegister(registerKey)) { return; } vimState.recordedState.registerName = registerKey; const register = await Register.get(vimState.recordedState.registerName, this.multicursorIndex); if (register === undefined) { StatusBar.displayError( vimState, VimError.NothingInRegister(vimState.recordedState.registerName), ); return; } let text: string; if (register.text instanceof Array) { text = register.text.join('\n'); } else if (register.text instanceof RecordedState) { let keyStrokes: string[] = []; for (const action of register.text.actionsRun) { keyStrokes = keyStrokes.concat(action.keysPressed); } text = keyStrokes.join('\n'); } else { text = register.text; } if (register.registerMode === RegisterMode.LineWise) { text += '\n'; } commandLine.text += text; commandLine.cursorIndex += text.length; } } @RegisterAction class CommandInsertWord extends CommandLineAction { keys = ['', '']; protected async run(vimState: VimState, commandLine: CommandLine): Promise { const word = TextEditor.getWord(vimState.document, vimState.cursorStopPosition.getLeftIfEOL()); if (word !== undefined) { commandLine.text += word; commandLine.cursorIndex += word.length; } } } @RegisterAction class CommandLineLeftRight extends CommandLineAction { keys = [[''], ['']]; private getTrimmedStatusBarText() { // first regex removes the : / and | from the string // second regex removes a single space from the end of the string const trimmedStatusBarText = StatusBar.getText() .replace(/^(?:\/|\:)(.*)(?:\|)(.*)/, '$1$2') .replace(/(.*) $/, '$1'); return trimmedStatusBarText; } protected async run(vimState: VimState, commandLine: CommandLine): Promise { const key = this.keysPressed[0]; const statusBarText = this.getTrimmedStatusBarText(); if (key === '') { commandLine.cursorIndex = Math.min(commandLine.cursorIndex + 1, statusBarText.length); } else if (key === '') { commandLine.cursorIndex = Math.max(commandLine.cursorIndex - 1, 0); } } } @RegisterAction class CommandLinePaste extends CommandLineAction { keys = [[''], ['']]; protected async run(vimState: VimState, commandLine: CommandLine): Promise { const textFromClipboard = await Clipboard.Paste(); commandLine.text = commandLine.text .substring(0, commandLine.cursorIndex) .concat(textFromClipboard) .concat(commandLine.text.slice(commandLine.cursorIndex)); commandLine.cursorIndex += textFromClipboard.length; } } @RegisterAction class CommandCtrlLInSearchMode extends CommandLineAction { override modes = [Mode.SearchInProgressMode]; keys = ['']; protected async run(vimState: VimState, commandLine: CommandLine): Promise { if (commandLine instanceof SearchCommandLine) { const currentMatch = commandLine.getCurrentMatchRange(vimState); if (currentMatch) { const line = vimState.document.lineAt(currentMatch.range.end).text; if (currentMatch.range.end.character < line.length) { commandLine.getSearchState().searchString += line[currentMatch.range.end.character]; commandLine.cursorIndex++; } } } } } @RegisterAction class CommandAdvanceCurrentMatch extends CommandLineAction { override modes = [Mode.SearchInProgressMode]; keys = [[''], ['']]; protected async run(vimState: VimState, commandLine: CommandLine): Promise { const key = this.keysPressed[0]; const direction = key === '' ? SearchDirection.Forward : key === '' ? SearchDirection.Backward : undefined; if (commandLine instanceof SearchCommandLine && direction !== undefined) { commandLine.advanceCurrentMatch(vimState, direction); } } } @RegisterAction class CommandLineType extends CommandLineAction { keys = [['']]; protected async run(vimState: VimState, commandLine: CommandLine): Promise { commandLine.typeCharacter(this.keysPressed[0]); } } ================================================ FILE: src/actions/commands/digraphs.ts ================================================ // prettier-ignore export const DefaultDigraphs = new Map([ ["NU", ["^@", 10]], ["SH", ["^A", 1]], ["SX", ["^B", 2]], ["EX", ["^C", 3]], ["ET", ["^D", 4]], ["EQ", ["^E", 5]], ["AK", ["^F", 6]], ["BL", ["^G", 7]], ["BS", ["^H", 8]], ["HT", ["^I", 9]], ["LF", ["^@", 10]], ["VT", ["^K", 11]], ["FF", ["^L", 12]], ["CR", ["^M", 13]], ["SO", ["^N", 14]], ["SI", ["^O", 15]], ["DL", ["^P", 16]], ["D1", ["^Q", 17]], ["D2", ["^R", 18]], ["D3", ["^S", 19]], ["D4", ["^T", 20]], ["NK", ["^U", 21]], ["SY", ["^V", 22]], ["EB", ["^W", 23]], ["CN", ["^X", 24]], ["EM", ["^Y", 25]], ["SB", ["^Z", 26]], ["EC", ["^[", 27]], ["FS", ["^\\", 28]], ["GS", ["^]", 29]], ["RS", ["^^", 30]], ["US", ["^_", 31]], ["SP", [" ", 32]], ["Nb", ["#", 35]], ["DO", ["$", 36]], ["At", ["@", 64]], ["<(", ["[", 91]], ["//", ["\\", 92]], [")>", ["]", 93]], ["'>", ["^", 94]], ["'!", ["`", 96]], ["(!", ["{", 123]], ["!!", ["|", 124]], ["!)", ["}", 125]], ["'?", ["~", 126]], ["DT", ["^?", 127]], ["PA", ["<80>", 128]], ["HO", ["<81>", 129]], ["BH", ["<82>", 130]], ["NH", ["<83>", 131]], ["IN", ["<84>", 132]], ["NL", ["<85>", 133]], ["SA", ["<86>", 134]], ["ES", ["<87>", 135]], ["HS", ["<88>", 136]], ["HJ", ["<89>", 137]], ["VS", ["<8a>", 138]], ["PD", ["<8b>", 139]], ["PU", ["<8c>", 140]], ["RI", ["<8d>", 141]], ["S2", ["<8e>", 142]], ["S3", ["<8f>", 143]], ["DC", ["<90>", 144]], ["P1", ["<91>", 145]], ["P2", ["<92>", 146]], ["TS", ["<93>", 147]], ["CC", ["<94>", 148]], ["MW", ["<95>", 149]], ["SG", ["<96>", 150]], ["EG", ["<97>", 151]], ["SS", ["<98>", 152]], ["GC", ["<99>", 153]], ["SC", ["<9a>", 154]], ["CI", ["<9b>", 155]], ["ST", ["<9c>", 156]], ["OC", ["<9d>", 157]], ["PM", ["<9e>", 158]], ["AC", ["<9f>", 159]], ["NS", [" ", 160]], ["!I", ["¡", 161]], ["~!", ["¡", 161]], ["Ct", ["¢", 162]], ["c|", ["¢", 162]], ["Pd", ["£", 163]], ["$$", ["£", 163]], ["Cu", ["¤", 164]], ["ox", ["¤", 164]], ["Ye", ["¥", 165]], ["Y-", ["¥", 165]], ["BB", ["¦", 166]], ["||", ["¦", 166]], ["SE", ["§", 167]], ["':", ["¨", 168]], ["Co", ["©", 169]], ["cO", ["©", 169]], ["-a", ["ª", 170]], ["<<", ["«", 171]], ["NO", ["¬", 172]], ["-,", ["¬", 172]], ["--", ["­", 173]], ["Rg", ["®", 174]], ["'m", ["¯", 175]], ["-=", ["¯", 175]], ["DG", ["°", 176]], ["~o", ["°", 176]], ["+-", ["±", 177]], ["2S", ["²", 178]], ["22", ["²", 178]], ["3S", ["³", 179]], ["33", ["³", 179]], ["''", ["´", 180]], ["My", ["µ", 181]], ["PI", ["¶", 182]], ["pp", ["¶", 182]], [".M", ["·", 183]], ["~.", ["·", 183]], ["',", ["¸", 184]], ["1S", ["¹", 185]], ["11", ["¹", 185]], ["-o", ["º", 186]], [">>", ["»", 187]], ["14", ["¼", 188]], ["12", ["½", 189]], ["34", ["¾", 190]], ["?I", ["¿", 191]], ["~?", ["¿", 191]], ["A!", ["À", 192]], ["A`", ["À", 192]], ["A'", ["Á", 193]], ["A>", ["Â", 194]], ["A^", ["Â", 194]], ["A?", ["Ã", 195]], ["A~", ["Ã", 195]], ["A:", ["Ä", 196]], ["A\"", ["Ä", 196]], ["AA", ["Å", 197]], ["A@", ["Å", 197]], ["AE", ["Æ", 198]], ["C,", ["Ç", 199]], ["E!", ["È", 200]], ["E`", ["È", 200]], ["E'", ["É", 201]], ["E>", ["Ê", 202]], ["E^", ["Ê", 202]], ["E:", ["Ë", 203]], ["E\"", ["Ë", 203]], ["I!", ["Ì", 204]], ["I`", ["Ì", 204]], ["I'", ["Í", 205]], ["I>", ["Î", 206]], ["I^", ["Î", 206]], ["I:", ["Ï", 207]], ["I\"", ["Ï", 207]], ["D-", ["Ð", 208]], ["N?", ["Ñ", 209]], ["N~", ["Ñ", 209]], ["O!", ["Ò", 210]], ["O`", ["Ò", 210]], ["O'", ["Ó", 211]], ["O>", ["Ô", 212]], ["O^", ["Ô", 212]], ["O?", ["Õ", 213]], ["O~", ["Õ", 213]], ["O:", ["Ö", 214]], ["*X", ["×", 215]], ["/\\", ["×", 215]], ["O/", ["Ø", 216]], ["U!", ["Ù", 217]], ["U`", ["Ù", 217]], ["U'", ["Ú", 218]], ["U>", ["Û", 219]], ["U^", ["Û", 219]], ["U:", ["Ü", 220]], ["Y'", ["Ý", 221]], ["TH", ["Þ", 222]], ["Ip", ["Þ", 222]], ["ss", ["ß", 223]], ["a!", ["à", 224]], ["a`", ["à", 224]], ["a'", ["á", 225]], ["a>", ["â", 226]], ["a^", ["â", 226]], ["a?", ["ã", 227]], ["a~", ["ã", 227]], ["a:", ["ä", 228]], ["a\"", ["ä", 228]], ["aa", ["å", 229]], ["a@", ["å", 229]], ["ae", ["æ", 230]], ["c,", ["ç", 231]], ["e!", ["è", 232]], ["e`", ["è", 232]], ["e'", ["é", 233]], ["e>", ["ê", 234]], ["e^", ["ê", 234]], ["e:", ["ë", 235]], ["e\"", ["ë", 235]], ["i!", ["ì", 236]], ["i`", ["ì", 236]], ["i'", ["í", 237]], ["i>", ["î", 238]], ["i^", ["î", 238]], ["i:", ["ï", 239]], ["d-", ["ð", 240]], ["n?", ["ñ", 241]], ["n~", ["ñ", 241]], ["o!", ["ò", 242]], ["o`", ["ò", 242]], ["o'", ["ó", 243]], ["o>", ["ô", 244]], ["o^", ["ô", 244]], ["o?", ["õ", 245]], ["o~", ["õ", 245]], ["o:", ["ö", 246]], ["-:", ["÷", 247]], ["o/", ["ø", 248]], ["u!", ["ù", 249]], ["u`", ["ù", 249]], ["u'", ["ú", 250]], ["u>", ["û", 251]], ["u^", ["û", 251]], ["u:", ["ü", 252]], ["y'", ["ý", 253]], ["th", ["þ", 254]], ["y:", ["ÿ", 255]], ["y\"", ["ÿ", 255]], ["A-", ["Ā", 256]], ["a-", ["ā", 257]], ["A(", ["Ă", 258]], ["a(", ["ă", 259]], ["A;", ["Ą", 260]], ["a;", ["ą", 261]], ["C'", ["Ć", 262]], ["c'", ["ć", 263]], ["C>", ["Ĉ", 264]], ["c>", ["ĉ", 265]], ["C.", ["Ċ", 266]], ["c.", ["ċ", 267]], ["C<", ["Č", 268]], ["c<", ["č", 269]], ["D<", ["Ď", 270]], ["d<", ["ď", 271]], ["D/", ["Đ", 272]], ["d/", ["đ", 273]], ["E-", ["Ē", 274]], ["e-", ["ē", 275]], ["E(", ["Ĕ", 276]], ["e(", ["ĕ", 277]], ["E.", ["Ė", 278]], ["e.", ["ė", 279]], ["E;", ["Ę", 280]], ["e;", ["ę", 281]], ["E<", ["Ě", 282]], ["e<", ["ě", 283]], ["G>", ["Ĝ", 284]], ["g>", ["ĝ", 285]], ["G(", ["Ğ", 286]], ["g(", ["ğ", 287]], ["G.", ["Ġ", 288]], ["g.", ["ġ", 289]], ["G,", ["Ģ", 290]], ["g,", ["ģ", 291]], ["H>", ["Ĥ", 292]], ["h>", ["ĥ", 293]], ["H/", ["Ħ", 294]], ["h/", ["ħ", 295]], ["I?", ["Ĩ", 296]], ["i?", ["ĩ", 297]], ["I-", ["Ī", 298]], ["i-", ["ī", 299]], ["I(", ["Ĭ", 300]], ["i(", ["ĭ", 301]], ["I;", ["Į", 302]], ["i;", ["į", 303]], ["I.", ["İ", 304]], ["i.", ["ı", 305]], ["IJ", ["IJ", 306]], ["ij", ["ij", 307]], ["J>", ["Ĵ", 308]], ["j>", ["ĵ", 309]], ["K,", ["Ķ", 310]], ["k,", ["ķ", 311]], ["kk", ["ĸ", 312]], ["L'", ["Ĺ", 313]], ["l'", ["ĺ", 314]], ["L,", ["Ļ", 315]], ["l,", ["ļ", 316]], ["L<", ["Ľ", 317]], ["l<", ["ľ", 318]], ["L.", ["Ŀ", 319]], ["l.", ["ŀ", 320]], ["L/", ["Ł", 321]], ["l/", ["ł", 322]], ["N'", ["Ń", 323]], ["n'", ["ń", 324]], ["N,", ["Ņ", 325]], ["n,", ["ņ", 326]], ["N<", ["Ň", 327]], ["n<", ["ň", 328]], ["'n", ["ʼn", 329]], ["NG", ["Ŋ", 330]], ["ng", ["ŋ", 331]], ["O-", ["Ō", 332]], ["o-", ["ō", 333]], ["O(", ["Ŏ", 334]], ["o(", ["ŏ", 335]], ["O\"", ["Ő", 336]], ["o\"", ["ő", 337]], ["OE", ["Œ", 338]], ["oe", ["œ", 339]], ["R'", ["Ŕ", 340]], ["r'", ["ŕ", 341]], ["R,", ["Ŗ", 342]], ["r,", ["ŗ", 343]], ["R<", ["Ř", 344]], ["r<", ["ř", 345]], ["S'", ["Ś", 346]], ["s'", ["ś", 347]], ["S>", ["Ŝ", 348]], ["s>", ["ŝ", 349]], ["S,", ["Ş", 350]], ["s,", ["ş", 351]], ["S<", ["Š", 352]], ["s<", ["š", 353]], ["T,", ["Ţ", 354]], ["t,", ["ţ", 355]], ["T<", ["Ť", 356]], ["t<", ["ť", 357]], ["T/", ["Ŧ", 358]], ["t/", ["ŧ", 359]], ["U?", ["Ũ", 360]], ["u?", ["ũ", 361]], ["U-", ["Ū", 362]], ["u-", ["ū", 363]], ["U(", ["Ŭ", 364]], ["u(", ["ŭ", 365]], ["U0", ["Ů", 366]], ["u0", ["ů", 367]], ["U\"", ["Ű", 368]], ["u\"", ["ű", 369]], ["U;", ["Ų", 370]], ["u;", ["ų", 371]], ["W>", ["Ŵ", 372]], ["w>", ["ŵ", 373]], ["Y>", ["Ŷ", 374]], ["y>", ["ŷ", 375]], ["Y:", ["Ÿ", 376]], ["Z'", ["Ź", 377]], ["z'", ["ź", 378]], ["Z.", ["Ż", 379]], ["z.", ["ż", 380]], ["Z<", ["Ž", 381]], ["z<", ["ž", 382]], ["O9", ["Ơ", 416]], ["o9", ["ơ", 417]], ["OI", ["Ƣ", 418]], ["oi", ["ƣ", 419]], ["yr", ["Ʀ", 422]], ["U9", ["Ư", 431]], ["u9", ["ư", 432]], ["Z/", ["Ƶ", 437]], ["z/", ["ƶ", 438]], ["ED", ["Ʒ", 439]], ["A<", ["Ǎ", 461]], ["a<", ["ǎ", 462]], ["I<", ["Ǐ", 463]], ["i<", ["ǐ", 464]], ["O<", ["Ǒ", 465]], ["o<", ["ǒ", 466]], ["U<", ["Ǔ", 467]], ["u<", ["ǔ", 468]], ["A1", ["Ǟ", 478]], ["a1", ["ǟ", 479]], ["A7", ["Ǡ", 480]], ["a7", ["ǡ", 481]], ["A3", ["Ǣ", 482]], ["a3", ["ǣ", 483]], ["G/", ["Ǥ", 484]], ["g/", ["ǥ", 485]], ["G<", ["Ǧ", 486]], ["g<", ["ǧ", 487]], ["K<", ["Ǩ", 488]], ["k<", ["ǩ", 489]], ["O;", ["Ǫ", 490]], ["o;", ["ǫ", 491]], ["O1", ["Ǭ", 492]], ["o1", ["ǭ", 493]], ["EZ", ["Ǯ", 494]], ["ez", ["ǯ", 495]], ["j<", ["ǰ", 496]], ["G'", ["Ǵ", 500]], ["g'", ["ǵ", 501]], [";S", ["ʿ", 703]], ["'<", ["ˇ", 711]], ["'(", ["˘", 728]], ["'.", ["˙", 729]], ["'0", ["˚", 730]], ["';", ["˛", 731]], ["'\"", ["˝", 733]], ["A%", ["Ά", 902]], ["E%", ["Έ", 904]], ["Y%", ["Ή", 905]], ["I%", ["Ί", 906]], ["O%", ["Ό", 908]], ["U%", ["Ύ", 910]], ["W%", ["Ώ", 911]], ["i3", ["ΐ", 912]], ["A*", ["Α", 913]], ["B*", ["Β", 914]], ["G*", ["Γ", 915]], ["D*", ["Δ", 916]], ["E*", ["Ε", 917]], ["Z*", ["Ζ", 918]], ["Y*", ["Η", 919]], ["H*", ["Θ", 920]], ["I*", ["Ι", 921]], ["K*", ["Κ", 922]], ["L*", ["Λ", 923]], ["M*", ["Μ", 924]], ["N*", ["Ν", 925]], ["C*", ["Ξ", 926]], ["O*", ["Ο", 927]], ["P*", ["Π", 928]], ["R*", ["Ρ", 929]], ["S*", ["Σ", 931]], ["T*", ["Τ", 932]], ["U*", ["Υ", 933]], ["F*", ["Φ", 934]], ["X*", ["Χ", 935]], ["Q*", ["Ψ", 936]], ["W*", ["Ω", 937]], ["J*", ["Ϊ", 938]], ["V*", ["Ϋ", 939]], ["a%", ["ά", 940]], ["e%", ["έ", 941]], ["y%", ["ή", 942]], ["i%", ["ί", 943]], ["u3", ["ΰ", 944]], ["a*", ["α", 945]], ["b*", ["β", 946]], ["g*", ["γ", 947]], ["d*", ["δ", 948]], ["e*", ["ε", 949]], ["z*", ["ζ", 950]], ["y*", ["η", 951]], ["h*", ["θ", 952]], ["i*", ["ι", 953]], ["k*", ["κ", 954]], ["l*", ["λ", 955]], ["m*", ["μ", 956]], ["n*", ["ν", 957]], ["c*", ["ξ", 958]], ["o*", ["ο", 959]], ["p*", ["π", 960]], ["r*", ["ρ", 961]], ["*s", ["ς", 962]], ["s*", ["σ", 963]], ["t*", ["τ", 964]], ["u*", ["υ", 965]], ["f*", ["φ", 966]], ["x*", ["χ", 967]], ["q*", ["ψ", 968]], ["w*", ["ω", 969]], ["j*", ["ϊ", 970]], ["v*", ["ϋ", 971]], ["o%", ["ό", 972]], ["u%", ["ύ", 973]], ["w%", ["ώ", 974]], ["'G", ["Ϙ", 984]], [",G", ["ϙ", 985]], ["T3", ["Ϛ", 986]], ["t3", ["ϛ", 987]], ["M3", ["Ϝ", 988]], ["m3", ["ϝ", 989]], ["K3", ["Ϟ", 990]], ["k3", ["ϟ", 991]], ["P3", ["Ϡ", 992]], ["p3", ["ϡ", 993]], ["'%", ["ϴ", 1012]], ["j3", ["ϵ", 1013]], ["IO", ["Ё", 1025]], ["D%", ["Ђ", 1026]], ["G%", ["Ѓ", 1027]], ["IE", ["Є", 1028]], ["DS", ["Ѕ", 1029]], ["II", ["І", 1030]], ["YI", ["Ї", 1031]], ["J%", ["Ј", 1032]], ["LJ", ["Љ", 1033]], ["NJ", ["Њ", 1034]], ["Ts", ["Ћ", 1035]], ["KJ", ["Ќ", 1036]], ["V%", ["Ў", 1038]], ["DZ", ["Џ", 1039]], ["A=", ["А", 1040]], ["B=", ["Б", 1041]], ["V=", ["В", 1042]], ["G=", ["Г", 1043]], ["D=", ["Д", 1044]], ["E=", ["Е", 1045]], ["Z%", ["Ж", 1046]], ["Z=", ["З", 1047]], ["I=", ["И", 1048]], ["J=", ["Й", 1049]], ["K=", ["К", 1050]], ["L=", ["Л", 1051]], ["M=", ["М", 1052]], ["N=", ["Н", 1053]], ["O=", ["О", 1054]], ["P=", ["П", 1055]], ["R=", ["Р", 1056]], ["S=", ["С", 1057]], ["T=", ["Т", 1058]], ["U=", ["У", 1059]], ["F=", ["Ф", 1060]], ["H=", ["Х", 1061]], ["C=", ["Ц", 1062]], ["C%", ["Ч", 1063]], ["S%", ["Ш", 1064]], ["Sc", ["Щ", 1065]], ["=\"", ["Ъ", 1066]], ["Y=", ["Ы", 1067]], ["%\"", ["Ь", 1068]], ["JE", ["Э", 1069]], ["JU", ["Ю", 1070]], ["JA", ["Я", 1071]], ["a=", ["а", 1072]], ["b=", ["б", 1073]], ["v=", ["в", 1074]], ["g=", ["г", 1075]], ["d=", ["д", 1076]], ["e=", ["е", 1077]], ["z%", ["ж", 1078]], ["z=", ["з", 1079]], ["i=", ["и", 1080]], ["j=", ["й", 1081]], ["k=", ["к", 1082]], ["l=", ["л", 1083]], ["m=", ["м", 1084]], ["n=", ["н", 1085]], ["o=", ["о", 1086]], ["p=", ["п", 1087]], ["r=", ["р", 1088]], ["s=", ["с", 1089]], ["t=", ["т", 1090]], ["u=", ["у", 1091]], ["f=", ["ф", 1092]], ["h=", ["х", 1093]], ["c=", ["ц", 1094]], ["c%", ["ч", 1095]], ["s%", ["ш", 1096]], ["sc", ["щ", 1097]], ["='", ["ъ", 1098]], ["y=", ["ы", 1099]], ["%'", ["ь", 1100]], ["je", ["э", 1101]], ["ju", ["ю", 1102]], ["ja", ["я", 1103]], ["io", ["ё", 1105]], ["d%", ["ђ", 1106]], ["g%", ["ѓ", 1107]], ["ie", ["є", 1108]], ["ds", ["ѕ", 1109]], ["ii", ["і", 1110]], ["yi", ["ї", 1111]], ["j%", ["ј", 1112]], ["lj", ["љ", 1113]], ["nj", ["њ", 1114]], ["ts", ["ћ", 1115]], ["kj", ["ќ", 1116]], ["v%", ["ў", 1118]], ["dz", ["џ", 1119]], ["Y3", ["Ѣ", 1122]], ["y3", ["ѣ", 1123]], ["O3", ["Ѫ", 1130]], ["o3", ["ѫ", 1131]], ["F3", ["Ѳ", 1138]], ["f3", ["ѳ", 1139]], ["V3", ["Ѵ", 1140]], ["v3", ["ѵ", 1141]], ["C3", ["Ҁ", 1152]], ["c3", ["ҁ", 1153]], ["G3", ["Ґ", 1168]], ["g3", ["ґ", 1169]], ["A+", ["א", 1488]], ["B+", ["ב", 1489]], ["G+", ["ג", 1490]], ["D+", ["ד", 1491]], ["H+", ["ה", 1492]], ["W+", ["ו", 1493]], ["Z+", ["ז", 1494]], ["X+", ["ח", 1495]], ["Tj", ["ט", 1496]], ["J+", ["י", 1497]], ["K%", ["ך", 1498]], ["K+", ["כ", 1499]], ["L+", ["ל", 1500]], ["M%", ["ם", 1501]], ["M+", ["מ", 1502]], ["N%", ["ן", 1503]], ["N+", ["נ", 1504]], ["S+", ["ס", 1505]], ["E+", ["ע", 1506]], ["P%", ["ף", 1507]], ["P+", ["פ", 1508]], ["Zj", ["ץ", 1509]], ["ZJ", ["צ", 1510]], ["Q+", ["ק", 1511]], ["R+", ["ר", 1512]], ["Sh", ["ש", 1513]], ["T+", ["ת", 1514]], [",+", ["،", 1548]], [";+", ["؛", 1563]], ["?+", ["؟", 1567]], ["H'", ["ء", 1569]], ["aM", ["آ", 1570]], ["aH", ["أ", 1571]], ["wH", ["ؤ", 1572]], ["ah", ["إ", 1573]], ["yH", ["ئ", 1574]], ["a+", ["ا", 1575]], ["b+", ["ب", 1576]], ["tm", ["ة", 1577]], ["t+", ["ت", 1578]], ["tk", ["ث", 1579]], ["g+", ["ج", 1580]], ["hk", ["ح", 1581]], ["x+", ["خ", 1582]], ["d+", ["د", 1583]], ["dk", ["ذ", 1584]], ["r+", ["ر", 1585]], ["z+", ["ز", 1586]], ["s+", ["س", 1587]], ["sn", ["ش", 1588]], ["c+", ["ص", 1589]], ["dd", ["ض", 1590]], ["tj", ["ط", 1591]], ["zH", ["ظ", 1592]], ["e+", ["ع", 1593]], ["i+", ["غ", 1594]], ["++", ["ـ", 1600]], ["f+", ["ف", 1601]], ["q+", ["ق", 1602]], ["k+", ["ك", 1603]], ["l+", ["ل", 1604]], ["m+", ["م", 1605]], ["n+", ["ن", 1606]], ["h+", ["ه", 1607]], ["w+", ["و", 1608]], ["j+", ["ى", 1609]], ["y+", ["ي", 1610]], [":+", ["ً", 1611]], ["\"+", ["ٌ", 1612]], ["=+", ["ٍ", 1613]], ["/+", ["َ", 1614]], ["'+", ["ُ", 1615]], ["1+", ["ِ", 1616]], ["3+", ["ّ", 1617]], ["0+", ["ْ", 1618]], ["aS", ["ٰ", 1648]], ["p+", ["پ", 1662]], ["v+", ["ڤ", 1700]], ["gf", ["گ", 1711]], ["0a", ["۰", 1776]], ["1a", ["۱", 1777]], ["2a", ["۲", 1778]], ["3a", ["۳", 1779]], ["4a", ["۴", 1780]], ["5a", ["۵", 1781]], ["6a", ["۶", 1782]], ["7a", ["۷", 1783]], ["8a", ["۸", 1784]], ["9a", ["۹", 1785]], ["B.", ["Ḃ", 7682]], ["b.", ["ḃ", 7683]], ["B_", ["Ḇ", 7686]], ["b_", ["ḇ", 7687]], ["D.", ["Ḋ", 7690]], ["d.", ["ḋ", 7691]], ["D_", ["Ḏ", 7694]], ["d_", ["ḏ", 7695]], ["D,", ["Ḑ", 7696]], ["d,", ["ḑ", 7697]], ["F.", ["Ḟ", 7710]], ["f.", ["ḟ", 7711]], ["G-", ["Ḡ", 7712]], ["g-", ["ḡ", 7713]], ["H.", ["Ḣ", 7714]], ["h.", ["ḣ", 7715]], ["H:", ["Ḧ", 7718]], ["h:", ["ḧ", 7719]], ["H,", ["Ḩ", 7720]], ["h,", ["ḩ", 7721]], ["K'", ["Ḱ", 7728]], ["k'", ["ḱ", 7729]], ["K_", ["Ḵ", 7732]], ["k_", ["ḵ", 7733]], ["L_", ["Ḻ", 7738]], ["l_", ["ḻ", 7739]], ["M'", ["Ḿ", 7742]], ["m'", ["ḿ", 7743]], ["M.", ["Ṁ", 7744]], ["m.", ["ṁ", 7745]], ["N.", ["Ṅ", 7748]], ["n.", ["ṅ", 7749]], ["N_", ["Ṉ", 7752]], ["n_", ["ṉ", 7753]], ["P'", ["Ṕ", 7764]], ["p'", ["ṕ", 7765]], ["P.", ["Ṗ", 7766]], ["p.", ["ṗ", 7767]], ["R.", ["Ṙ", 7768]], ["r.", ["ṙ", 7769]], ["R_", ["Ṟ", 7774]], ["r_", ["ṟ", 7775]], ["S.", ["Ṡ", 7776]], ["s.", ["ṡ", 7777]], ["T.", ["Ṫ", 7786]], ["t.", ["ṫ", 7787]], ["T_", ["Ṯ", 7790]], ["t_", ["ṯ", 7791]], ["V?", ["Ṽ", 7804]], ["v?", ["ṽ", 7805]], ["W!", ["Ẁ", 7808]], ["W`", ["Ẁ", 7808]], ["w!", ["ẁ", 7809]], ["w`", ["ẁ", 7809]], ["W'", ["Ẃ", 7810]], ["w'", ["ẃ", 7811]], ["W:", ["Ẅ", 7812]], ["w:", ["ẅ", 7813]], ["W.", ["Ẇ", 7814]], ["w.", ["ẇ", 7815]], ["X.", ["Ẋ", 7818]], ["x.", ["ẋ", 7819]], ["X:", ["Ẍ", 7820]], ["x:", ["ẍ", 7821]], ["Y.", ["Ẏ", 7822]], ["y.", ["ẏ", 7823]], ["Z>", ["Ẑ", 7824]], ["z>", ["ẑ", 7825]], ["Z_", ["Ẕ", 7828]], ["z_", ["ẕ", 7829]], ["h_", ["ẖ", 7830]], ["t:", ["ẗ", 7831]], ["w0", ["ẘ", 7832]], ["y0", ["ẙ", 7833]], ["A2", ["Ả", 7842]], ["a2", ["ả", 7843]], ["E2", ["Ẻ", 7866]], ["e2", ["ẻ", 7867]], ["E?", ["Ẽ", 7868]], ["e?", ["ẽ", 7869]], ["I2", ["Ỉ", 7880]], ["i2", ["ỉ", 7881]], ["O2", ["Ỏ", 7886]], ["o2", ["ỏ", 7887]], ["U2", ["Ủ", 7910]], ["u2", ["ủ", 7911]], ["Y!", ["Ỳ", 7922]], ["Y`", ["Ỳ", 7922]], ["y!", ["ỳ", 7923]], ["y`", ["ỳ", 7923]], ["Y2", ["Ỷ", 7926]], ["y2", ["ỷ", 7927]], ["Y?", ["Ỹ", 7928]], ["y?", ["ỹ", 7929]], [";'", ["ἀ", 7936]], [",'", ["ἁ", 7937]], [";!", ["ἂ", 7938]], [",!", ["ἃ", 7939]], ["?;", ["ἄ", 7940]], ["?,", ["ἅ", 7941]], ["!:", ["ἆ", 7942]], ["?:", ["ἇ", 7943]], ["1N", [" ", 8194]], ["1M", [" ", 8195]], ["3M", [" ", 8196]], ["4M", [" ", 8197]], ["6M", [" ", 8198]], ["1T", [" ", 8201]], ["1H", [" ", 8202]], ["-1", ["‐", 8208]], ["-N", ["–", 8211]], ["-M", ["—", 8212]], ["-3", ["―", 8213]], ["!2", ["‖", 8214]], ["=2", ["‗", 8215]], ["'6", ["‘", 8216]], ["'9", ["’", 8217]], [".9", ["‚", 8218]], ["9'", ["‛", 8219]], ["\"6", ["“", 8220]], ["\"9", ["”", 8221]], [":9", ["„", 8222]], ["9\"", ["‟", 8223]], ["/-", ["†", 8224]], ["/=", ["‡", 8225]], ["oo", ["•", 8226]], ["..", ["‥", 8229]], [",.", ["…", 8230]], ["%0", ["‰", 8240]], ["1'", ["′", 8242]], ["2'", ["″", 8243]], ["3'", ["‴", 8244]], ["4'", ["⁗", 8279]], ["1\"", ["‵", 8245]], ["2\"", ["‶", 8246]], ["3\"", ["‷", 8247]], ["Ca", ["‸", 8248]], ["<1", ["‹", 8249]], [">1", ["›", 8250]], [":X", ["※", 8251]], ["'-", ["‾", 8254]], ["/f", ["⁄", 8260]], ["0S", ["⁰", 8304]], ["4S", ["⁴", 8308]], ["5S", ["⁵", 8309]], ["6S", ["⁶", 8310]], ["7S", ["⁷", 8311]], ["8S", ["⁸", 8312]], ["9S", ["⁹", 8313]], ["+S", ["⁺", 8314]], ["-S", ["⁻", 8315]], ["=S", ["⁼", 8316]], ["(S", ["⁽", 8317]], [")S", ["⁾", 8318]], ["nS", ["ⁿ", 8319]], ["0s", ["₀", 8320]], ["1s", ["₁", 8321]], ["2s", ["₂", 8322]], ["3s", ["₃", 8323]], ["4s", ["₄", 8324]], ["5s", ["₅", 8325]], ["6s", ["₆", 8326]], ["7s", ["₇", 8327]], ["8s", ["₈", 8328]], ["9s", ["₉", 8329]], ["+s", ["₊", 8330]], ["-s", ["₋", 8331]], ["=s", ["₌", 8332]], ["(s", ["₍", 8333]], [")s", ["₎", 8334]], ["Li", ["₤", 8356]], ["Pt", ["₧", 8359]], ["W=", ["₩", 8361]], ["=e", ["€", 8364]], ["Eu", ["€", 8364]], ["=R", ["₽", 8381]], ["=P", ["₽", 8381]], ["oC", ["℃", 8451]], ["co", ["℅", 8453]], ["oF", ["℉", 8457]], ["N0", ["№", 8470]], ["PO", ["℗", 8471]], ["Rx", ["℞", 8478]], ["SM", ["℠", 8480]], ["TM", ["™", 8482]], ["Om", ["Ω", 8486]], ["AO", ["Å", 8491]], ["13", ["⅓", 8531]], ["23", ["⅔", 8532]], ["15", ["⅕", 8533]], ["25", ["⅖", 8534]], ["35", ["⅗", 8535]], ["45", ["⅘", 8536]], ["16", ["⅙", 8537]], ["56", ["⅚", 8538]], ["18", ["⅛", 8539]], ["38", ["⅜", 8540]], ["58", ["⅝", 8541]], ["78", ["⅞", 8542]], ["1R", ["Ⅰ", 8544]], ["2R", ["Ⅱ", 8545]], ["3R", ["Ⅲ", 8546]], ["4R", ["Ⅳ", 8547]], ["5R", ["Ⅴ", 8548]], ["6R", ["Ⅵ", 8549]], ["7R", ["Ⅶ", 8550]], ["8R", ["Ⅷ", 8551]], ["9R", ["Ⅸ", 8552]], ["aR", ["Ⅹ", 8553]], ["bR", ["Ⅺ", 8554]], ["cR", ["Ⅻ", 8555]], ["1r", ["ⅰ", 8560]], ["2r", ["ⅱ", 8561]], ["3r", ["ⅲ", 8562]], ["4r", ["ⅳ", 8563]], ["5r", ["ⅴ", 8564]], ["6r", ["ⅵ", 8565]], ["7r", ["ⅶ", 8566]], ["8r", ["ⅷ", 8567]], ["9r", ["ⅸ", 8568]], ["ar", ["ⅹ", 8569]], ["br", ["ⅺ", 8570]], ["cr", ["ⅻ", 8571]], ["<-", ["←", 8592]], ["-!", ["↑", 8593]], ["->", ["→", 8594]], ["-v", ["↓", 8595]], ["<>", ["↔", 8596]], ["UD", ["↕", 8597]], ["<=", ["⇐", 8656]], ["=>", ["⇒", 8658]], ["==", ["⇔", 8660]], ["FA", ["∀", 8704]], ["dP", ["∂", 8706]], ["TE", ["∃", 8707]], ["/0", ["∅", 8709]], ["DE", ["∆", 8710]], ["NB", ["∇", 8711]], ["(-", ["∈", 8712]], ["-)", ["∋", 8715]], ["*P", ["∏", 8719]], ["+Z", ["∑", 8721]], ["-2", ["−", 8722]], ["-+", ["∓", 8723]], ["*-", ["∗", 8727]], ["Ob", ["∘", 8728]], ["Sb", ["∙", 8729]], ["RT", ["√", 8730]], ["0(", ["∝", 8733]], ["00", ["∞", 8734]], ["-L", ["∟", 8735]], ["-V", ["∠", 8736]], ["PP", ["∥", 8741]], ["AN", ["∧", 8743]], ["OR", ["∨", 8744]], ["(U", ["∩", 8745]], [")U", ["∪", 8746]], ["In", ["∫", 8747]], ["DI", ["∬", 8748]], ["Io", ["∮", 8750]], [".:", ["∴", 8756]], [":.", ["∵", 8757]], [":R", ["∶", 8758]], ["::", ["∷", 8759]], ["?1", ["∼", 8764]], ["CG", ["∾", 8766]], ["?-", ["≃", 8771]], ["?=", ["≅", 8773]], ["?2", ["≈", 8776]], ["=?", ["≌", 8780]], ["HI", ["≓", 8787]], ["!=", ["≠", 8800]], ["=3", ["≡", 8801]], ["=<", ["≤", 8804]], [">=", ["≥", 8805]], ["<*", ["≪", 8810]], ["*>", ["≫", 8811]], ["!<", ["≮", 8814]], ["!>", ["≯", 8815]], ["(C", ["⊂", 8834]], [")C", ["⊃", 8835]], ["(_", ["⊆", 8838]], [")_", ["⊇", 8839]], ["0.", ["⊙", 8857]], ["02", ["⊚", 8858]], ["-T", ["⊥", 8869]], [".P", ["⋅", 8901]], [":3", ["⋮", 8942]], [".3", ["⋯", 8943]], ["Eh", ["⌂", 8962]], ["<7", ["⌈", 8968]], [">7", ["⌉", 8969]], ["7<", ["⌊", 8970]], ["7>", ["⌋", 8971]], ["NI", ["⌐", 8976]], ["(A", ["⌒", 8978]], ["TR", ["⌕", 8981]], ["Iu", ["⌠", 8992]], ["Il", ["⌡", 8993]], ["", ["〉", 9002]], ["Vs", ["␣", 9251]], ["1h", ["⑀", 9280]], ["3h", ["⑁", 9281]], ["2h", ["⑂", 9282]], ["4h", ["⑃", 9283]], ["1j", ["⑆", 9286]], ["2j", ["⑇", 9287]], ["3j", ["⑈", 9288]], ["4j", ["⑉", 9289]], ["1.", ["⒈", 9352]], ["2.", ["⒉", 9353]], ["3.", ["⒊", 9354]], ["4.", ["⒋", 9355]], ["5.", ["⒌", 9356]], ["6.", ["⒍", 9357]], ["7.", ["⒎", 9358]], ["8.", ["⒏", 9359]], ["9.", ["⒐", 9360]], ["hh", ["─", 9472]], ["HH", ["━", 9473]], ["vv", ["│", 9474]], ["VV", ["┃", 9475]], ["3-", ["┄", 9476]], ["3_", ["┅", 9477]], ["3!", ["┆", 9478]], ["3/", ["┇", 9479]], ["4-", ["┈", 9480]], ["4_", ["┉", 9481]], ["4!", ["┊", 9482]], ["4/", ["┋", 9483]], ["dr", ["┌", 9484]], ["dR", ["┍", 9485]], ["Dr", ["┎", 9486]], ["DR", ["┏", 9487]], ["dl", ["┐", 9488]], ["dL", ["┑", 9489]], ["Dl", ["┒", 9490]], ["LD", ["┓", 9491]], ["ur", ["└", 9492]], ["uR", ["┕", 9493]], ["Ur", ["┖", 9494]], ["UR", ["┗", 9495]], ["ul", ["┘", 9496]], ["uL", ["┙", 9497]], ["Ul", ["┚", 9498]], ["UL", ["┛", 9499]], ["vr", ["├", 9500]], ["vR", ["┝", 9501]], ["Vr", ["┠", 9504]], ["VR", ["┣", 9507]], ["vl", ["┤", 9508]], ["vL", ["┥", 9509]], ["Vl", ["┨", 9512]], ["VL", ["┫", 9515]], ["dh", ["┬", 9516]], ["dH", ["┯", 9519]], ["Dh", ["┰", 9520]], ["DH", ["┳", 9523]], ["uh", ["┴", 9524]], ["uH", ["┷", 9527]], ["Uh", ["┸", 9528]], ["UH", ["┻", 9531]], ["vh", ["┼", 9532]], ["vH", ["┿", 9535]], ["Vh", ["╂", 9538]], ["VH", ["╋", 9547]], ["FD", ["╱", 9585]], ["BD", ["╲", 9586]], ["TB", ["▀", 9600]], ["LB", ["▄", 9604]], ["FB", ["█", 9608]], ["lB", ["▌", 9612]], ["RB", ["▐", 9616]], [".S", ["░", 9617]], [":S", ["▒", 9618]], ["?S", ["▓", 9619]], ["fS", ["■", 9632]], ["OS", ["□", 9633]], ["RO", ["▢", 9634]], ["Rr", ["▣", 9635]], ["RF", ["▤", 9636]], ["RY", ["▥", 9637]], ["RH", ["▦", 9638]], ["RZ", ["▧", 9639]], ["RK", ["▨", 9640]], ["RX", ["▩", 9641]], ["sB", ["▪", 9642]], ["SR", ["▬", 9644]], ["Or", ["▭", 9645]], ["UT", ["▲", 9650]], ["uT", ["△", 9651]], ["PR", ["▶", 9654]], ["Tr", ["▷", 9655]], ["Dt", ["▼", 9660]], ["dT", ["▽", 9661]], ["PL", ["◀", 9664]], ["Tl", ["◁", 9665]], ["Db", ["◆", 9670]], ["Dw", ["◇", 9671]], ["LZ", ["◊", 9674]], ["0m", ["○", 9675]], ["0o", ["◎", 9678]], ["0M", ["●", 9679]], ["0L", ["◐", 9680]], ["0R", ["◑", 9681]], ["Sn", ["◘", 9688]], ["Ic", ["◙", 9689]], ["Fd", ["◢", 9698]], ["Bd", ["◣", 9699]], ["*2", ["★", 9733]], ["*1", ["☆", 9734]], ["H", ["☞", 9758]], ["0u", ["☺", 9786]], ["0U", ["☻", 9787]], ["SU", ["☼", 9788]], ["Fm", ["♀", 9792]], ["Ml", ["♂", 9794]], ["cS", ["♠", 9824]], ["cH", ["♡", 9825]], ["cD", ["♢", 9826]], ["cC", ["♣", 9827]], ["Md", ["♩", 9833]], ["M8", ["♪", 9834]], ["M2", ["♫", 9835]], ["Mb", ["♭", 9837]], ["Mx", ["♮", 9838]], ["MX", ["♯", 9839]], ["OK", ["✓", 10003]], ["XX", ["✗", 10007]], ["-X", ["✠", 10016]], ["IS", [" ", 12288]], [",_", ["、", 12289]], ["._", ["。", 12290]], ["+\"", ["〃", 12291]], ["+_", ["〄", 12292]], ["*_", ["々", 12293]], [";_", ["〆", 12294]], ["0_", ["〇", 12295]], ["<+", ["《", 12298]], [">+", ["》", 12299]], ["<'", ["「", 12300]], [">'", ["」", 12301]], ["<\"", ["『", 12302]], [">\"", ["』", 12303]], ["(\"", ["【", 12304]], [")\"", ["】", 12305]], ["=T", ["〒", 12306]], ["=_", ["〓", 12307]], ["('", ["〔", 12308]], [")'", ["〕", 12309]], ["(I", ["〖", 12310]], [")I", ["〗", 12311]], ["-?", ["〜", 12316]], ["A5", ["ぁ", 12353]], ["a5", ["あ", 12354]], ["I5", ["ぃ", 12355]], ["i5", ["い", 12356]], ["U5", ["ぅ", 12357]], ["u5", ["う", 12358]], ["E5", ["ぇ", 12359]], ["e5", ["え", 12360]], ["O5", ["ぉ", 12361]], ["o5", ["お", 12362]], ["ka", ["か", 12363]], ["ga", ["が", 12364]], ["ki", ["き", 12365]], ["gi", ["ぎ", 12366]], ["ku", ["く", 12367]], ["gu", ["ぐ", 12368]], ["ke", ["け", 12369]], ["ge", ["げ", 12370]], ["ko", ["こ", 12371]], ["go", ["ご", 12372]], ["sa", ["さ", 12373]], ["za", ["ざ", 12374]], ["si", ["し", 12375]], ["zi", ["じ", 12376]], ["su", ["す", 12377]], ["zu", ["ず", 12378]], ["se", ["せ", 12379]], ["ze", ["ぜ", 12380]], ["so", ["そ", 12381]], ["zo", ["ぞ", 12382]], ["ta", ["た", 12383]], ["da", ["だ", 12384]], ["ti", ["ち", 12385]], ["di", ["ぢ", 12386]], ["tU", ["っ", 12387]], ["tu", ["つ", 12388]], ["du", ["づ", 12389]], ["te", ["て", 12390]], ["de", ["で", 12391]], ["to", ["と", 12392]], ["do", ["ど", 12393]], ["na", ["な", 12394]], ["ni", ["に", 12395]], ["nu", ["ぬ", 12396]], ["ne", ["ね", 12397]], ["no", ["の", 12398]], ["ha", ["は", 12399]], ["ba", ["ば", 12400]], ["pa", ["ぱ", 12401]], ["hi", ["ひ", 12402]], ["bi", ["び", 12403]], ["pi", ["ぴ", 12404]], ["hu", ["ふ", 12405]], ["bu", ["ぶ", 12406]], ["pu", ["ぷ", 12407]], ["he", ["へ", 12408]], ["be", ["べ", 12409]], ["pe", ["ぺ", 12410]], ["ho", ["ほ", 12411]], ["bo", ["ぼ", 12412]], ["po", ["ぽ", 12413]], ["ma", ["ま", 12414]], ["mi", ["み", 12415]], ["mu", ["む", 12416]], ["me", ["め", 12417]], ["mo", ["も", 12418]], ["yA", ["ゃ", 12419]], ["ya", ["や", 12420]], ["yU", ["ゅ", 12421]], ["yu", ["ゆ", 12422]], ["yO", ["ょ", 12423]], ["yo", ["よ", 12424]], ["ra", ["ら", 12425]], ["ri", ["り", 12426]], ["ru", ["る", 12427]], ["re", ["れ", 12428]], ["ro", ["ろ", 12429]], ["wA", ["ゎ", 12430]], ["wa", ["わ", 12431]], ["wi", ["ゐ", 12432]], ["we", ["ゑ", 12433]], ["wo", ["を", 12434]], ["n5", ["ん", 12435]], ["vu", ["ゔ", 12436]], ["\"5", ["゛", 12443]], ["05", ["゜", 12444]], ["*5", ["ゝ", 12445]], ["+5", ["ゞ", 12446]], ["a6", ["ァ", 12449]], ["A6", ["ア", 12450]], ["i6", ["ィ", 12451]], ["I6", ["イ", 12452]], ["u6", ["ゥ", 12453]], ["U6", ["ウ", 12454]], ["e6", ["ェ", 12455]], ["E6", ["エ", 12456]], ["o6", ["ォ", 12457]], ["O6", ["オ", 12458]], ["Ka", ["カ", 12459]], ["Ga", ["ガ", 12460]], ["Ki", ["キ", 12461]], ["Gi", ["ギ", 12462]], ["Ku", ["ク", 12463]], ["Gu", ["グ", 12464]], ["Ke", ["ケ", 12465]], ["Ge", ["ゲ", 12466]], ["Ko", ["コ", 12467]], ["Go", ["ゴ", 12468]], ["Sa", ["サ", 12469]], ["Za", ["ザ", 12470]], ["Si", ["シ", 12471]], ["Zi", ["ジ", 12472]], ["Su", ["ス", 12473]], ["Zu", ["ズ", 12474]], ["Se", ["セ", 12475]], ["Ze", ["ゼ", 12476]], ["So", ["ソ", 12477]], ["Zo", ["ゾ", 12478]], ["Ta", ["タ", 12479]], ["Da", ["ダ", 12480]], ["Ti", ["チ", 12481]], ["Di", ["ヂ", 12482]], ["TU", ["ッ", 12483]], ["Tu", ["ツ", 12484]], ["Du", ["ヅ", 12485]], ["Te", ["テ", 12486]], ["De", ["デ", 12487]], ["To", ["ト", 12488]], ["Do", ["ド", 12489]], ["Na", ["ナ", 12490]], ["Ni", ["ニ", 12491]], ["Nu", ["ヌ", 12492]], ["Ne", ["ネ", 12493]], ["No", ["ノ", 12494]], ["Ha", ["ハ", 12495]], ["Ba", ["バ", 12496]], ["Pa", ["パ", 12497]], ["Hi", ["ヒ", 12498]], ["Bi", ["ビ", 12499]], ["Pi", ["ピ", 12500]], ["Hu", ["フ", 12501]], ["Bu", ["ブ", 12502]], ["Pu", ["プ", 12503]], ["He", ["ヘ", 12504]], ["Be", ["ベ", 12505]], ["Pe", ["ペ", 12506]], ["Ho", ["ホ", 12507]], ["Bo", ["ボ", 12508]], ["Po", ["ポ", 12509]], ["Ma", ["マ", 12510]], ["Mi", ["ミ", 12511]], ["Mu", ["ム", 12512]], ["Me", ["メ", 12513]], ["Mo", ["モ", 12514]], ["YA", ["ャ", 12515]], ["Ya", ["ヤ", 12516]], ["YU", ["ュ", 12517]], ["Yu", ["ユ", 12518]], ["YO", ["ョ", 12519]], ["Yo", ["ヨ", 12520]], ["Ra", ["ラ", 12521]], ["Ri", ["リ", 12522]], ["Ru", ["ル", 12523]], ["Re", ["レ", 12524]], ["Ro", ["ロ", 12525]], ["WA", ["ヮ", 12526]], ["Wa", ["ワ", 12527]], ["Wi", ["ヰ", 12528]], ["We", ["ヱ", 12529]], ["Wo", ["ヲ", 12530]], ["N6", ["ン", 12531]], ["Vu", ["ヴ", 12532]], ["KA", ["ヵ", 12533]], ["KE", ["ヶ", 12534]], ["Va", ["ヷ", 12535]], ["Vi", ["ヸ", 12536]], ["Ve", ["ヹ", 12537]], ["Vo", ["ヺ", 12538]], [".6", ["・", 12539]], ["-6", ["ー", 12540]], ["*6", ["ヽ", 12541]], ["+6", ["ヾ", 12542]], ["b4", ["ㄅ", 12549]], ["p4", ["ㄆ", 12550]], ["m4", ["ㄇ", 12551]], ["f4", ["ㄈ", 12552]], ["d4", ["ㄉ", 12553]], ["t4", ["ㄊ", 12554]], ["n4", ["ㄋ", 12555]], ["l4", ["ㄌ", 12556]], ["g4", ["ㄍ", 12557]], ["k4", ["ㄎ", 12558]], ["h4", ["ㄏ", 12559]], ["j4", ["ㄐ", 12560]], ["q4", ["ㄑ", 12561]], ["x4", ["ㄒ", 12562]], ["zh", ["ㄓ", 12563]], ["ch", ["ㄔ", 12564]], ["sh", ["ㄕ", 12565]], ["r4", ["ㄖ", 12566]], ["z4", ["ㄗ", 12567]], ["c4", ["ㄘ", 12568]], ["s4", ["ㄙ", 12569]], ["a4", ["ㄚ", 12570]], ["o4", ["ㄛ", 12571]], ["e4", ["ㄜ", 12572]], ["ai", ["ㄞ", 12574]], ["ei", ["ㄟ", 12575]], ["au", ["ㄠ", 12576]], ["ou", ["ㄡ", 12577]], ["an", ["ㄢ", 12578]], ["en", ["ㄣ", 12579]], ["aN", ["ㄤ", 12580]], ["eN", ["ㄥ", 12581]], ["er", ["ㄦ", 12582]], ["i4", ["ㄧ", 12583]], ["u4", ["ㄨ", 12584]], ["iu", ["ㄩ", 12585]], ["v4", ["ㄪ", 12586]], ["nG", ["ㄫ", 12587]], ["gn", ["ㄬ", 12588]], ["1c", ["㈠", 12832]], ["2c", ["㈡", 12833]], ["3c", ["㈢", 12834]], ["4c", ["㈣", 12835]], ["5c", ["㈤", 12836]], ["6c", ["㈥", 12837]], ["7c", ["㈦", 12838]], ["8c", ["㈧", 12839]], ["9c", ["㈨", 12840]], ["ff", ["ff", 64256]], ["fi", ["fi", 64257]], ["fl", ["fl", 64258]], ["ft", ["ſt", 64261]], ["st", ["st", 64262]], ]); ================================================ FILE: src/actions/commands/documentChange.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { PositionDiff, laterOf } from '../../common/motion/position'; import { VimState } from '../../state/vimState'; import { Transformation } from '../../transformations/transformations'; import { BaseCommand } from '../base'; type Change = vscode.TextDocumentContentChangeEvent; /** * A very special snowflake. * * Each keystroke when typing in Insert mode is its own Action, which means naively replaying a * realistic insertion (via `.` or a macro) does many small insertions, which is very slow. * So instead, we fold all those actions after the fact into a single DocumentContentChangeAction, * which compresses the changes, generally into a single document edit per cursor. */ export class DocumentContentChangeAction extends BaseCommand { modes = []; keys = []; private readonly cursorStart: Position; private cursorEnd: Position; constructor(cursorStart: Position) { super(); this.cursorStart = cursorStart; this.cursorEnd = cursorStart; } private contentChanges: Change[] = []; public addChanges(changes: Change[], cursorPosition: Position) { this.contentChanges = [...this.contentChanges, ...changes]; this.compressChanges(); this.cursorEnd = cursorPosition; } public getTransformation(positionDiff: PositionDiff): Transformation { return { type: 'contentChange', changes: this.contentChanges, diff: positionDiff, }; } public override async exec(position: Position, vimState: VimState): Promise { if (this.contentChanges.length === 0) { return; } let originalLeftBoundary = this.cursorStart; let rightBoundary: Position = position; for (const change of this.contentChanges) { if (change.range.start.line < originalLeftBoundary.line) { // This change should be ignored const linesAffected = change.range.end.line - change.range.start.line + 1; const resultLines = change.text.split('\n').length; originalLeftBoundary = originalLeftBoundary.with( Math.max(0, originalLeftBoundary.line + resultLines - linesAffected), ); continue; } // Translates diffPos from a position relative to originalLeftBoundary to one relative to position const translate = (diffPos: Position): Position => { const lineOffset = diffPos.line - originalLeftBoundary.line; const char = lineOffset === 0 ? position.character + diffPos.character - originalLeftBoundary.character : diffPos.character; // TODO: Should we document.validate() this position? return new Position(Math.max(position.line + lineOffset, 0), Math.max(char, 0)); }; const replaceRange = new vscode.Range( translate(change.range.start), translate(change.range.end), ); if (replaceRange.start.isAfter(rightBoundary)) { // This change should be ignored as it's out of boundary continue; } // Calculate new right boundary const textDiffLines = change.text.split('\n'); const numLinesAdded = textDiffLines.length - 1; const newRightBoundary = numLinesAdded === 0 ? new Position(replaceRange.start.line, replaceRange.start.character + change.text.length) : new Position(replaceRange.start.line + numLinesAdded, textDiffLines.pop()!.length); rightBoundary = laterOf(rightBoundary, newRightBoundary); if (replaceRange.start.isEqual(replaceRange.end)) { vimState.recordedState.transformer.insert( replaceRange.start, change.text, PositionDiff.exactPosition(translate(this.cursorEnd)), ); } else { vimState.recordedState.transformer.replace( replaceRange, change.text, PositionDiff.exactPosition(translate(this.cursorEnd)), ); } } } private compressChanges(): void { const merge = (first: Change, second: Change): Change | undefined => { if (first.rangeOffset + first.text.length === second.rangeOffset) { // Simple concatenation return { text: first.text + second.text, range: first.range, rangeOffset: first.rangeOffset, rangeLength: first.rangeLength, }; } else if ( first.rangeOffset <= second.rangeOffset && first.text.length >= second.rangeLength ) { const start = second.rangeOffset - first.rangeOffset; const end = start + second.rangeLength; const text = first.text.slice(0, start) + second.text + first.text.slice(end); // `second` replaces part of `first` // Most often, this is the result of confirming an auto-completion return { text, range: first.range, rangeOffset: first.rangeOffset, rangeLength: first.rangeLength, }; } else { // TODO: Do any of the cases falling into this `else` matter? // TODO: YES - make an insertion and then autocomplete to something totally different (replace subsumes insert) return undefined; } }; const compressed: Change[] = []; let prev: Change | undefined; for (const change of this.contentChanges) { if (prev === undefined) { prev = change; } else { const merged = merge(prev, change); if (merged) { prev = merged; } else { compressed.push(prev); prev = change; } } } if (prev !== undefined) { compressed.push(prev); } this.contentChanges = compressed; } } ================================================ FILE: src/actions/commands/file.ts ================================================ import path from 'path'; import { doesFileExist } from 'platform/fs'; import { Position, Range, Uri, window, workspace } from 'vscode'; import { FileCommand } from '../../cmd_line/commands/file'; import { VimError } from '../../error'; import { Mode } from '../../mode/mode'; import { Register } from '../../register/register'; import { RecordedState } from '../../state/recordedState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { WordType } from '../../textobject/word'; import { reportFileInfo } from '../../util/statusBarTextUtils'; import { BaseCommand, RegisterAction } from '../base'; @RegisterAction class ShowFileInfo extends BaseCommand { modes = [Mode.Normal]; keys = ['']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { reportFileInfo(position, vimState); } } @RegisterAction class GoToAlternateFile extends BaseCommand { modes = [Mode.Normal]; keys = [[''], ['']]; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { const altFile = await Register.get('#'); if (altFile?.text instanceof RecordedState) { throw new Error(`# register unexpectedly contained a RecordedState`); } else if (altFile === undefined || altFile.text === '') { StatusBar.displayError(vimState, VimError.NoAlternateFile()); } else { let files: Uri[]; if (await doesFileExist(Uri.file(altFile.text))) { files = [Uri.file(altFile.text)]; } else { files = await workspace.findFiles(altFile.text); } // TODO: if the path matches a file from multiple workspace roots, we may not choose the right one if (files.length > 0) { const document = await workspace.openTextDocument(files[0]); await window.showTextDocument(document); } } } } @RegisterAction class OpenFile extends BaseCommand { modes = [Mode.Normal, Mode.Visual]; keys = ['g', 'f']; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { let fullFilePath: string; if (vimState.currentMode === Mode.Visual) { fullFilePath = vimState.document.getText(vimState.editor.selection); } else { const range = new Range( position.prevWordStart(vimState.document, { wordType: WordType.FileName, inclusive: true }), position.nextWordStart(vimState.document, { wordType: WordType.FileName }), ); fullFilePath = vimState.document.getText(range).trim(); } const fileInfo = fullFilePath.match(/(.*?(?=:[0-9]+)|.*):?([0-9]*)$/); if (fileInfo) { const fileUri: Uri = await (async () => { const pathStr = fileInfo[1]; if (path.isAbsolute(pathStr)) { return Uri.file(pathStr); } else { let uri = Uri.file(path.resolve(path.dirname(vimState.document.uri.fsPath), pathStr)); if (!(await doesFileExist(uri))) { const workspaceRoot = workspace.getWorkspaceFolder(vimState.document.uri)?.uri; if (workspaceRoot) { uri = Uri.file(path.join(workspaceRoot.fsPath, pathStr)); if (!(await doesFileExist(uri))) { throw VimError.CantFindFileInPath(pathStr); } } } return uri; } })(); const line = parseInt(fileInfo[2], 10); const fileCommand = new FileCommand({ name: 'edit', bang: false, opt: [], file: fileUri.fsPath, cmd: isNaN(line) ? undefined : { type: 'line_number', line: line - 1 }, createFileIfNotExists: false, }); void fileCommand.execute(vimState); } } } ================================================ FILE: src/actions/commands/fold.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { Cursor } from '../../common/motion/cursor'; import { Mode } from '../../mode/mode'; import { VimState } from '../../state/vimState'; import { BaseCommand, RegisterAction } from '../base'; import { BaseOperator } from '../operator'; type FoldDirection = 'up' | 'down'; abstract class CommandFold extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; abstract commandName: string; direction: FoldDirection | undefined; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } public override async exec(position: Position, vimState: VimState): Promise { const timesToRepeat = vimState.recordedState.count || 1; const args = this.direction !== undefined ? { levels: timesToRepeat, direction: this.direction } : undefined; vimState.recordedState.transformer.vscodeCommand(this.commandName, args); await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class ToggleFold extends CommandFold { keys = ['z', 'a']; commandName = 'editor.toggleFold'; } @RegisterAction class CloseFold extends CommandFold { keys = ['z', 'c']; commandName = 'editor.fold'; override direction: FoldDirection = 'up'; } @RegisterAction class CloseAllFolds extends CommandFold { keys = ['z', 'M']; commandName = 'editor.foldAll'; } @RegisterAction class OpenFold extends CommandFold { keys = ['z', 'o']; commandName = 'editor.unfold'; override direction: FoldDirection = 'down'; } @RegisterAction class OpenAllFolds extends CommandFold { keys = ['z', 'R']; commandName = 'editor.unfoldAll'; } @RegisterAction class CloseAllFoldsRecursively extends CommandFold { override modes = [Mode.Normal]; keys = ['z', 'C']; commandName = 'editor.foldRecursively'; } @RegisterAction class OpenAllFoldsRecursively extends CommandFold { override modes = [Mode.Normal]; keys = ['z', 'O']; commandName = 'editor.unfoldRecursively'; } @RegisterAction class AddFold extends BaseOperator { override modes = [Mode.Normal, Mode.Visual]; keys = ['z', 'f']; readonly commandName = 'editor.createFoldingRangeFromSelection'; public async run(vimState: VimState, start: Position, end: Position): Promise { const previousSelections = vimState.lastVisualSelection; // keep in case of Normal mode vimState.editor.selection = new vscode.Selection(start, end); await vscode.commands.executeCommand(this.commandName); vimState.lastVisualSelection = previousSelections; vimState.cursors = [Cursor.atPosition(start)]; await vimState.setCurrentMode(Mode.Normal); // Vim behavior } } @RegisterAction class RemoveFold extends BaseCommand { override modes = [Mode.Normal, Mode.Visual]; keys = ['z', 'd']; readonly commandName = 'editor.removeManualFoldingRanges'; override async exec(position: Position, vimState: VimState): Promise { await vscode.commands.executeCommand(this.commandName); vimState.cursors = [ Cursor.atPosition( vimState.currentMode === Mode.Visual ? vimState.editor.selection.start : position, ), ]; await vimState.setCurrentMode(Mode.Normal); // Vim behavior } } ================================================ FILE: src/actions/commands/incrementDecrement.ts ================================================ import { Position, Range } from 'vscode'; import { Cursor } from '../../common/motion/cursor'; import { PositionDiff, sorted } from '../../common/motion/position'; import { NumericString } from '../../common/number/numericString'; import { Mode, isVisualMode, visualBlockGetBottomRightPosition, visualBlockGetTopLeftPosition, } from '../../mode/mode'; import { VimState } from '../../state/vimState'; import { TextEditor } from '../../textEditor'; import { BaseCommand, RegisterAction } from '../base'; abstract class IncrementDecrementNumberAction extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; override createsUndoPoint = true; abstract offset: number; abstract staircase: boolean; public override async exec(position: Position, vimState: VimState): Promise { const ranges = this.getSearchRanges(vimState); let stepNum = 1; for (const [idx, range] of ranges.entries()) { position = range.start; const text = vimState.document.lineAt(position).text; // Make sure position within the text is possible and return if not if (text.length <= position.character) { continue; } // Start looking to the right for the next word to increment, unless we're // already on a word to increment, in which case start at the beginning of // that word. const whereToStart = text[position.character].match(/\s/) ? position : position.prevWordStart(vimState.document, { inclusive: true }); wordLoop: for (let { start, end, word } of TextEditor.iterateWords( vimState.document, whereToStart, )) { if (start.isAfter(range.stop)) { break; } // '-' doesn't count as a word, but is important to include in parsing // the number, as long as it is not just part of the word (-foo2 for example) if (text[start.character - 1] === '-' && /\d/.test(text[start.character])) { start = start.getLeft(); word = text[start.character] + word; } // Strict number parsing so "1a" doesn't silently get converted to "1" do { const result = NumericString.parse(word); if (result === undefined) { break; } const { num, suffixOffset } = result; // Use suffix offset to check if current cursor is in or before detected number. if (position.character < start.character + suffixOffset) { const pos = await this.replaceNum( vimState, num, this.offset * stepNum * (vimState.recordedState.count || 1), start, end, ); if (this.staircase) { stepNum++; } if (vimState.currentMode === Mode.Normal) { vimState.recordedState.transformer.moveCursor( PositionDiff.exactPosition(pos.getLeft(num.suffix.length)), ); } break wordLoop; } else { // For situation like this: xyz1999em199[cursor]9m word = word.slice(suffixOffset); start = new Position(start.line, start.character + suffixOffset); } } while (true); } } if (isVisualMode(vimState.currentMode)) { vimState.recordedState.transformer.moveCursor(PositionDiff.exactPosition(ranges[0].start)); } await vimState.setCurrentMode(Mode.Normal); } private async replaceNum( vimState: VimState, start: NumericString, offset: number, startPos: Position, endPos: Position, ): Promise { const oldLength = endPos.character + 1 - startPos.character; start.value += offset; const newNum = start.toString(); const range = new Range(startPos, endPos.getRight()); vimState.recordedState.transformer.replace(range, newNum); if (oldLength !== newNum.length) { // Adjust end position according to difference in width of number-string endPos = new Position(endPos.line, startPos.character + newNum.length - 1); } return endPos; } /** * @returns a list of Ranges in which to search for numbers */ private getSearchRanges(vimState: VimState): Cursor[] { const ranges: Cursor[] = []; const [start, stop] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition); switch (vimState.currentMode) { case Mode.Normal: { ranges.push( new Cursor(vimState.cursorStopPosition, vimState.cursorStopPosition.getLineEnd()), ); break; } case Mode.Visual: { ranges.push(new Cursor(start, start.getLineEnd())); for (let line = start.line + 1; line < stop.line; line++) { const lineStart = new Position(line, 0); ranges.push(new Cursor(lineStart, lineStart.getLineEnd())); } ranges.push(new Cursor(stop.getLineBegin(), stop)); break; } case Mode.VisualLine: { for (let line = start.line; line <= stop.line; line++) { const lineStart = new Position(line, 0); ranges.push(new Cursor(lineStart, lineStart.getLineEnd())); } break; } case Mode.VisualBlock: { const topLeft = visualBlockGetTopLeftPosition(start, stop); const bottomRight = visualBlockGetBottomRightPosition(start, stop); for (let line = topLeft.line; line <= bottomRight.line; line++) { ranges.push( new Cursor( new Position(line, topLeft.character), new Position(line, bottomRight.character), ), ); } break; } default: throw new Error( `Unexpected mode ${vimState.currentMode} in IncrementDecrementNumberAction.getPositions()`, ); } return ranges; } } @RegisterAction class IncrementNumber extends IncrementDecrementNumberAction { keys = ['']; offset = +1; staircase = false; } @RegisterAction class DecrementNumber extends IncrementDecrementNumberAction { keys = ['']; offset = -1; staircase = false; } @RegisterAction class IncrementNumberStaircase extends IncrementDecrementNumberAction { keys = ['g', '']; offset = +1; staircase = true; } @RegisterAction class DecrementNumberStaircase extends IncrementDecrementNumberAction { keys = ['g', '']; offset = -1; staircase = true; } ================================================ FILE: src/actions/commands/insert.ts ================================================ import * as vscode from 'vscode'; import { Position, Range } from 'vscode'; import { Cursor } from '../../common/motion/cursor'; import { lineCompletionProvider } from '../../completion/lineCompletionProvider'; import { VimError } from '../../error'; import { RecordedState } from '../../state/recordedState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { getCursorsAfterSync, isHighSurrogate, isLowSurrogate } from '../../util/util'; import { BaseMovement } from '../baseMotion'; import { MoveDown, MoveLeft, MoveRight, MoveUp } from '../motion'; import { PositionDiff } from './../../common/motion/position'; import { configuration } from './../../configuration/configuration'; import { Mode } from './../../mode/mode'; import { Register, RegisterMode } from './../../register/register'; import { TextEditor } from './../../textEditor'; import { BaseCommand, RegisterAction } from './../base'; import { CommandNumber } from './actions'; import { DefaultDigraphs } from './digraphs'; import { DocumentContentChangeAction } from './documentChange'; import { EnterReplaceMode } from './replace'; @RegisterAction export class Insert extends BaseCommand { modes = [Mode.Normal]; keys = [['i'], ['']]; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Insert); } public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Only allow this command to be prefixed with a count or nothing, no other // actions or operators before let previousActionsNumbers = true; for (const prevAction of vimState.recordedState.actionsRun) { if (!(prevAction instanceof CommandNumber)) { previousActionsNumbers = false; break; } } if (vimState.recordedState.actionsRun.length === 0 || previousActionsNumbers) { return super.couldActionApply(vimState, keysPressed); } return false; } } @RegisterAction export class Append extends BaseCommand { modes = [Mode.Normal]; keys = ['a']; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Insert); vimState.cursorStopPosition = vimState.cursorStartPosition = position.getRight(); } public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Only allow this command to be prefixed with a count or nothing, no other actions or operators before if (!vimState.recordedState.actionsRun.every((action) => action instanceof CommandNumber)) { return false; } return super.couldActionApply(vimState, keysPressed); } } @RegisterAction class InsertAtLastChange extends BaseCommand { modes = [Mode.Normal]; keys = ['g', 'i']; public override async exec(position: Position, vimState: VimState): Promise { vimState.cursorStopPosition = vimState.cursorStartPosition = vimState.historyTracker.getLastChangeEndPosition() ?? new Position(0, 0); await vimState.setCurrentMode(Mode.Insert); } } @RegisterAction class InsertAfterFirstWhitespaceOnLine extends BaseCommand { modes = [Mode.Normal]; keys = ['I']; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Insert); vimState.cursorStopPosition = vimState.cursorStartPosition = TextEditor.getFirstNonWhitespaceCharOnLine(vimState.document, position.line); } } @RegisterAction class InsertAtLineBegin extends BaseCommand { modes = [Mode.Normal]; keys = ['g', 'I']; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Insert); vimState.cursorStopPosition = vimState.cursorStartPosition = position.getLineBegin(); } } @RegisterAction class InsertAtLineEnd extends BaseCommand { modes = [Mode.Normal]; keys = ['A']; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Insert); vimState.cursorStopPosition = vimState.cursorStartPosition = position.getLineEnd(); } } @RegisterAction class InsertAbove extends BaseCommand { modes = [Mode.Normal]; keys = ['O']; override runsOnceForEveryCursor() { return false; } public override async execCount(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Insert); const count = vimState.recordedState.count || 1; const charPos = position.getLineBeginRespectingIndent(vimState.document).character; for (let i = 0; i < count; i++) { await vscode.commands.executeCommand('editor.action.insertLineBefore'); } vimState.cursors = getCursorsAfterSync(vimState.editor); const endPos = vimState.cursor.start.character; const indentAmt = charPos - endPos; for (let i = 0; i < count; i++) { const newPos = new Position(vimState.cursor.start.line + i, charPos); if (i === 0) { vimState.cursor = Cursor.atPosition(newPos); } else { vimState.cursors.push(Cursor.atPosition(newPos)); } if (indentAmt >= 0) { vimState.recordedState.transformer.addTransformation({ type: 'insertText', // TODO: Use `editor.options.insertSpaces`, I think text: TextEditor.setIndentationLevel('', indentAmt, configuration.expandtab), position: newPos, cursorIndex: i, manuallySetCursorPositions: true, }); } else { vimState.recordedState.transformer.addTransformation({ type: 'deleteRange', cursorIndex: i, range: new Range(newPos, new Position(newPos.line, endPos)), manuallySetCursorPositions: true, }); } } vimState.cursors = vimState.cursors.reverse(); vimState.isFakeMultiCursor = true; } } @RegisterAction class InsertBelow extends BaseCommand { modes = [Mode.Normal]; keys = ['o']; override runsOnceForEveryCursor() { return false; } public override async execCount(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Insert); const count = vimState.recordedState.count || 1; for (let i = 0; i < count; i++) { await vscode.commands.executeCommand('editor.action.insertLineAfter'); } vimState.cursors = getCursorsAfterSync(vimState.editor); for (let i = 1; i < count; i++) { const newPos = new Position( vimState.cursorStartPosition.line - i, vimState.cursorStartPosition.character, ); vimState.cursors.push(Cursor.atPosition(newPos)); // Ahhhhhh. We have to manually set cursor position here as we need text // transformations AND to set multiple cursors. vimState.recordedState.transformer.addTransformation({ type: 'insertText', // TODO: Use `editor.options.insertSpaces`, I think text: TextEditor.setIndentationLevel('', newPos.character, configuration.expandtab), position: newPos, cursorIndex: i, manuallySetCursorPositions: true, }); } vimState.cursors = vimState.cursors.reverse(); vimState.isFakeMultiCursor = true; } } @RegisterAction export class ExitInsertMode extends BaseCommand { modes = [Mode.Insert]; keys = [[''], [''], ['']]; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { void vscode.commands.executeCommand('closeParameterHints'); void vscode.commands.executeCommand('editor.action.inlineSuggest.hide'); vimState.cursors = vimState.cursors.map((x) => x.withNewStop(x.stop.getLeft())); if (vimState.returnToInsertAfterCommand && position.character !== 0) { vimState.cursors = vimState.cursors.map((x) => x.withNewStop(x.stop.getRight())); } // only remove leading spaces inserted by vscode. // vscode only inserts them when user enter a new line, // ie, o/O in Normal mode or \n in Insert mode. const lastActionBeforeEsc = vimState.recordedState.actionsRun[vimState.recordedState.actionsRun.length - 2]; if ( vimState.document.languageId !== 'plaintext' && (lastActionBeforeEsc instanceof InsertBelow || lastActionBeforeEsc instanceof InsertAbove || (lastActionBeforeEsc instanceof DocumentContentChangeAction && lastActionBeforeEsc.keysPressed[lastActionBeforeEsc.keysPressed.length - 1] === '\n')) ) { for (const cursor of vimState.cursors) { const line = vimState.document.lineAt(cursor.stop); if (line.text.length > 0 && line.isEmptyOrWhitespace) { vimState.recordedState.transformer.delete(line.range); } } } await vimState.setCurrentMode(Mode.Normal); // If we wanted to repeat this insert (only for i and a), now is the time to do it. Insert // count amount of these strings before returning back to normal mode const shouldRepeatInsert = vimState.recordedState.count > 1 && vimState.recordedState.actionsRun.find( (a) => a instanceof Insert || a instanceof Append || a instanceof InsertAtLineBegin || a instanceof InsertAtLineEnd || a instanceof InsertAfterFirstWhitespaceOnLine || a instanceof InsertAtLastChange, ) !== undefined; // If this is the type to repeat insert, do this now if (shouldRepeatInsert) { const changeAction = vimState.recordedState.actionsRun .slice() .reverse() .find((a) => a instanceof DocumentContentChangeAction); if (changeAction instanceof DocumentContentChangeAction) { // Add count amount of inserts in the case of 4i= // TODO: A few actions such as should be repeated, but are not for (let i = 0; i < vimState.recordedState.count - 1; i++) { // If this is the last transform, move cursor back one character const positionDiff = i === vimState.recordedState.count - 2 ? PositionDiff.offset({ character: -1 }) : PositionDiff.identity(); // Add a transform containing the change vimState.recordedState.transformer.addTransformation( changeAction.getTransformation(positionDiff), ); } } } vimState.historyTracker.currentContentChanges = []; if (vimState.isFakeMultiCursor) { vimState.cursors = [vimState.cursor]; vimState.isFakeMultiCursor = false; } } } @RegisterAction export class InsertPreviousText extends BaseCommand { modes = [Mode.Insert]; keys = ['']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { const register = await Register.get('.'); if ( register === undefined || !(register.text instanceof RecordedState) || !register.text.actionsRun ) { throw VimError.NoInsertedTextYet(); } const recordedState = register.text.clone(); // The first action is entering Insert Mode, which is not necessary in this case recordedState.actionsRun.shift(); // The last action is leaving Insert Mode, which is not necessary in this case recordedState.actionsRun.pop(); if (recordedState.actionsRun?.[0] instanceof ArrowsInInsertMode) { // Note, arrow keys are the only Insert action command that can't be repeated here as far as @rebornix knows. recordedState.actionsRun.shift(); } vimState.recordedState.transformer.addTransformation({ type: 'replayRecordedState', count: 1, recordedState, }); } } @RegisterAction class InsertPreviousTextAndQuit extends BaseCommand { modes = [Mode.Insert]; keys = ['']; // public override async exec(position: Position, vimState: VimState): Promise { await new InsertPreviousText().exec(position, vimState); await vimState.setCurrentMode(Mode.Normal); } } abstract class IndentCommand extends BaseCommand { modes = [Mode.Insert]; abstract readonly delta: number; public override async exec(position: Position, vimState: VimState): Promise { const line = vimState.document.lineAt(position); const tabSize = Number(vimState.editor.options.tabSize); const indentationWidth = TextEditor.getIndentationLevel(line.text, tabSize); const newIndentationWidth = (Math.floor(indentationWidth / tabSize) + this.delta) * tabSize; vimState.recordedState.transformer.replace( new Range( position.getLineBegin(), position.with({ character: line.firstNonWhitespaceCharacterIndex }), ), TextEditor.setIndentationLevel( line.text, newIndentationWidth, vimState.editor.options.insertSpaces as boolean, ).match(/^(\s*)/)![1], ); } } @RegisterAction class IncreaseIndent extends IndentCommand { keys = ['']; override readonly delta = 1; } @RegisterAction class DecreaseIndent extends IndentCommand { keys = ['']; override readonly delta = -1; } @RegisterAction export class BackspaceInInsertMode extends BaseCommand { modes = [Mode.Insert]; keys = [[''], ['']]; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { vimState.recordedState.transformer.vscodeCommand('deleteLeft'); } } @RegisterAction class DeleteInInsertMode extends BaseCommand { modes = [Mode.Insert]; keys = ['']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { vimState.recordedState.transformer.vscodeCommand('deleteRight'); } } @RegisterAction export class TypeInInsertMode extends BaseCommand { modes = [Mode.Insert]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { const char = this.keysPressed.at(-1)!; let text = char; if (char.length === 1) { const prevHighSurrogate = vimState.modeData.mode === Mode.Insert ? vimState.modeData.highSurrogate : undefined; if (isHighSurrogate(char.charCodeAt(0))) { await vimState.setModeData({ mode: Mode.Insert, highSurrogate: char, }); if (prevHighSurrogate === undefined) return; text = prevHighSurrogate; } else { if (isLowSurrogate(char.charCodeAt(0)) && prevHighSurrogate !== undefined) { text = prevHighSurrogate + char; } await vimState.setModeData({ mode: Mode.Insert, highSurrogate: undefined, }); } } vimState.recordedState.transformer.addTransformation({ type: 'insertTextVSCode', text, isMultiCursor: vimState.isMultiCursor, }); } public override toString(): string { return this.keysPressed.at(-1)!; } } @RegisterAction class InsertDigraph extends BaseCommand { modes = [Mode.Insert]; keys = ['', '', '']; override isCompleteAction = false; public override async exec(position: Position, vimState: VimState): Promise { const digraph = this.keysPressed.slice(1, 3).join(''); const reverseDigraph = digraph.split('').reverse().join(''); let charCodes = (DefaultDigraphs.get(digraph) || DefaultDigraphs.get(reverseDigraph) || configuration.digraphs[digraph] || configuration.digraphs[reverseDigraph])[1]; if (!(charCodes instanceof Array)) { charCodes = [charCodes]; } const char = String.fromCharCode(...charCodes); vimState.recordedState.transformer.insert(position, char); } public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { if (!super.doesActionApply(vimState, keysPressed)) { return false; } const chars = keysPressed.slice(1, 3).join(''); const reverseChars = chars.split('').reverse().join(''); return ( chars in configuration.digraphs || reverseChars in configuration.digraphs || DefaultDigraphs.has(chars) || DefaultDigraphs.has(reverseChars) ); } public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { if (!super.couldActionApply(vimState, keysPressed)) { return false; } const chars = keysPressed.slice(1, keysPressed.length).join(''); const reverseChars = chars.split('').reverse().join(''); if (chars.length > 0) { const predicate = (digraph: string) => { const digraphChars = digraph.substring(0, chars.length); return chars === digraphChars || reverseChars === digraphChars; }; const match = Object.keys(configuration.digraphs).find(predicate) || [...DefaultDigraphs.keys()].find(predicate); return match !== undefined; } return true; } } @RegisterAction class InsertRegisterContent extends BaseCommand { modes = [Mode.Insert]; keys = ['', '']; override isCompleteAction = false; public override async exec(position: Position, vimState: VimState): Promise { const registerKey = this.keysPressed[1]; if (!Register.isValidRegister(registerKey)) { return; } const register = await Register.get(registerKey, this.multicursorIndex); if (register === undefined) { StatusBar.displayError(vimState, VimError.NothingInRegister(registerKey)); return; } if (register.text instanceof RecordedState) { vimState.recordedState.transformer.addTransformation({ type: 'macro', register: vimState.recordedState.registerName, replay: 'keystrokes', }); return; } let text = register.text; if (register.registerMode === RegisterMode.LineWise && !vimState.isMultiCursor) { text += '\n'; } vimState.recordedState.transformer.insert(position, text); } } @RegisterAction class ExecuteOneNormalCommandInInsertMode extends BaseCommand { modes = [Mode.Insert]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { vimState.returnToInsertAfterCommand = true; vimState.actionCount = 0; await new ExitInsertMode().exec(position, vimState); } } @RegisterAction export class InsertCharAbove extends BaseCommand { modes = [Mode.Insert]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { if (position.line === 0) { return; } const charPos = position.getUp(); if (charPos.isLineEnd(vimState.document)) { return; } const char = vimState.document.getText(new Range(charPos, charPos.getRight())); vimState.recordedState.transformer.insert(position, char); } } @RegisterAction export class InsertCharBelow extends BaseCommand { modes = [Mode.Insert]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { if (position.line >= vimState.document.lineCount - 1) { return; } const charPos = position.getDown(); if (charPos.isLineEnd(vimState.document)) { return; } const char = vimState.document.getText(new Range(charPos, charPos.getRight())); vimState.recordedState.transformer.insert(position, char); } } @RegisterAction class DeleteWord extends BaseCommand { modes = [Mode.Insert]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { if (position.isAtDocumentBegin()) { return; } let wordBegin: Position; if (position.isInLeadingWhitespace(vimState.document)) { wordBegin = position.getLineBegin(); } else if (position.isLineBeginning()) { wordBegin = position.getUp().getLineEnd(); } else { wordBegin = position.prevWordStart(vimState.document); } vimState.recordedState.transformer.delete(new Range(wordBegin, position)); vimState.cursorStopPosition = wordBegin; } } @RegisterAction class DeleteAllBeforeCursor extends BaseCommand { modes = [Mode.Insert]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { let start: Position; if (position.character === 0) { start = position.getLeftThroughLineBreaks(true); } else if (position.isInLeadingWhitespace(vimState.document)) { start = position.getLineBegin(); } else { start = position.getLineBeginRespectingIndent(vimState.document); } vimState.recordedState.transformer.delete(new Range(start, position)); vimState.cursorStopPosition = start; vimState.cursorStartPosition = start; } } @RegisterAction class SelectNextSuggestion extends BaseCommand { modes = [Mode.Insert]; keys = [[''], ['']]; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await vscode.commands.executeCommand('selectNextSuggestion'); } } @RegisterAction class SelectPrevSuggestion extends BaseCommand { modes = [Mode.Insert]; keys = ['']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await vscode.commands.executeCommand('selectPrevSuggestion'); } } @RegisterAction class CtrlVInInsertMode extends BaseCommand { modes = [Mode.Insert]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { const clipboard = await Register.get('*', this.multicursorIndex); const text = clipboard?.text instanceof RecordedState ? undefined : clipboard?.text; if (text) { vimState.recordedState.transformer.insert(vimState.cursorStopPosition, text); } } } @RegisterAction class ShowLineAutocomplete extends BaseCommand { modes = [Mode.Insert]; keys = ['', '']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await lineCompletionProvider.showLineCompletionsQuickPick(position, vimState); } } @RegisterAction class NewLineInsertMode extends BaseCommand { modes = [Mode.Insert]; keys = [[''], ['']]; public override async exec(position: Position, vimState: VimState): Promise { vimState.recordedState.transformer.insert( position, '\n', PositionDiff.offset({ character: -1 }), ); } } @RegisterAction class ReplaceAtCursorFromInsertMode extends BaseCommand { modes = [Mode.Insert]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { await new EnterReplaceMode().exec(position, vimState); } } @RegisterAction class CreateUndoPoint extends BaseCommand { modes = [Mode.Insert]; keys = ['', 'u']; public override async exec(position: Position, vimState: VimState): Promise { vimState.historyTracker.addChange(true); vimState.historyTracker.finishCurrentStep(); } } @RegisterAction export class ArrowsInInsertMode extends BaseMovement { override modes = [Mode.Insert]; keys = [[''], [''], [''], ['']]; public override async execAction(position: Position, vimState: VimState): Promise { // Moving with the arrow keys in Insert mode "resets" our insertion for the purpose of repeating with dot or ``. // No matter how we got into Insert mode, repeating will now be done as if we started with `i`. // Note that this does not affect macros, which re-construct a list of actions based on keypresses. // TODO: ACTUALLY, we should reset this only after something is typed (`Axyz.` does repeat the insertion) // TODO: This also should mark an "insertion end" for the purpose of `` (try `ixyz`) vimState.recordedState.actionsRun = [new Insert()]; // Force an undo point to be created vimState.historyTracker.addChange(true); vimState.historyTracker.finishCurrentStep(); let newPosition: Position; switch (this.keysPressed[0]) { case '': newPosition = await new MoveUp(this.keysPressed).execAction(position, vimState); break; case '': newPosition = await new MoveDown(this.keysPressed).execAction(position, vimState); break; case '': newPosition = await new MoveLeft(this.keysPressed).execAction(position, vimState); break; case '': newPosition = await new MoveRight(this.keysPressed).execAction(position, vimState); break; default: throw new Error(`Unexpected 'arrow' key: ${this.keys[0]}`); } return newPosition; } } ================================================ FILE: src/actions/commands/join.ts ================================================ import { Position, Range } from 'vscode'; import { Cursor } from '../../common/motion/cursor'; import { PositionDiff, sorted } from '../../common/motion/position'; import { configuration } from '../../configuration/configuration'; import { Mode } from '../../mode/mode'; import { RegisterMode } from '../../register/register'; import { VimState } from '../../state/vimState'; import { TextEditor } from '../../textEditor'; import { isTextTransformation } from '../../transformations/transformations'; import { BaseCommand, RegisterAction } from '../base'; @RegisterAction class ActionJoin extends BaseCommand { modes = [Mode.Normal]; keys = ['J']; override createsUndoPoint = true; override runsOnceForEachCountPrefix = false; public async execJoinLines( startPosition: Position, position: Position, vimState: VimState, count: number, ): Promise { count = count - 1 || 1; let startLineNumber: number; let endLineNumber: number; if (startPosition.isEqual(position) || startPosition.line === position.line) { if (position.line + 1 < vimState.document.lineCount) { startLineNumber = position.line; endLineNumber = position.getDown(count).line; } else { startLineNumber = position.line; endLineNumber = position.line; } } else { startLineNumber = startPosition.line; endLineNumber = position.line; } let trimmedLinesContent = vimState.document.lineAt(startPosition).text; let columnDeltaOffset: number = 0; for (let i = startLineNumber + 1; i <= endLineNumber; i++) { const line = vimState.document.lineAt(i); if (line.firstNonWhitespaceCharacterIndex < line.text.length) { // Compute number of spaces to separate the lines let insertSpace = ' '; if (trimmedLinesContent === '' || trimmedLinesContent.endsWith('\t')) { insertSpace = ''; } else if ( configuration.joinspaces && (trimmedLinesContent.endsWith('.') || trimmedLinesContent.endsWith('!') || trimmedLinesContent.endsWith('?')) ) { insertSpace = ' '; } else if ( configuration.joinspaces && (trimmedLinesContent.endsWith('. ') || trimmedLinesContent.endsWith('! ') || trimmedLinesContent.endsWith('? ')) ) { insertSpace = ' '; } else if (trimmedLinesContent.endsWith(' ')) { insertSpace = ''; } const lineTextWithoutIndent = line.text.substring(line.firstNonWhitespaceCharacterIndex); if (lineTextWithoutIndent.charAt(0) === ')') { insertSpace = ''; } trimmedLinesContent += insertSpace + lineTextWithoutIndent; columnDeltaOffset = lineTextWithoutIndent.length + insertSpace.length; } } const deleteRange = new Range( new Position(startLineNumber, 0), new Position(endLineNumber, TextEditor.getLineLength(endLineNumber)), ); if (!deleteRange.start.isEqual(deleteRange.end)) { if (startPosition.isEqual(position)) { vimState.recordedState.transformer.replace( new Range(deleteRange.start, deleteRange.end), trimmedLinesContent, PositionDiff.offset({ character: trimmedLinesContent.length - columnDeltaOffset - position.character, }), ); } else { vimState.recordedState.transformer.addTransformation({ type: 'replaceText', text: trimmedLinesContent, range: new Range(deleteRange.start, deleteRange.end), manuallySetCursorPositions: true, }); vimState.cursorStartPosition = vimState.cursorStopPosition = new Position( startPosition.line, trimmedLinesContent.length - columnDeltaOffset, ); await vimState.setCurrentMode(Mode.Normal); } } } public override async execCount(position: Position, vimState: VimState): Promise { const cursorsToIterateOver = [...vimState.cursors].sort((a, b) => a.start.line > b.start.line || (a.start.line === b.start.line && a.start.character > b.start.character) ? 1 : -1, ); const resultingCursors: Cursor[] = []; for (const [idx, { start, stop }] of cursorsToIterateOver.entries()) { this.multicursorIndex = idx; vimState.cursorStopPosition = stop; vimState.cursorStartPosition = start; await this.execJoinLines(start, stop, vimState, vimState.recordedState.count || 1); resultingCursors.push(new Cursor(vimState.cursorStartPosition, vimState.cursorStopPosition)); for (const transformation of vimState.recordedState.transformer.transformations) { if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) { transformation.cursorIndex = this.multicursorIndex; } } } vimState.cursors = resultingCursors; } } @RegisterAction class ActionJoinVisualMode extends BaseCommand { modes = [Mode.Visual, Mode.VisualLine]; keys = ['J']; public override async exec(position: Position, vimState: VimState): Promise { const [start, end] = sorted(vimState.editor.selection.start, vimState.editor.selection.end); /** * For joining lines, Visual Line behaves the same as Visual so we align the register mode here. */ vimState.currentRegisterMode = RegisterMode.CharacterWise; await new ActionJoin().execJoinLines(start, end, vimState, 1); } } @RegisterAction class ActionJoinVisualBlockMode extends BaseCommand { modes = [Mode.VisualBlock]; keys = ['J']; public override async exec(position: Position, vimState: VimState): Promise { const [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition); vimState.currentRegisterMode = RegisterMode.CharacterWise; await new ActionJoin().execJoinLines(start, end, vimState, 1); } } @RegisterAction class ActionJoinNoWhitespace extends BaseCommand { modes = [Mode.Normal]; keys = ['g', 'J']; override createsUndoPoint = true; // gJ is essentially J without the edge cases. ;-) public override async exec(position: Position, vimState: VimState): Promise { if (position.line === vimState.document.lineCount - 1) { return; // TODO: bell } const count = vimState.recordedState.count > 2 ? vimState.recordedState.count - 1 : 1; await this.execJoin(count, position, vimState); } public async execJoin(count: number, position: Position, vimState: VimState): Promise { const replaceRange = new Range( new Position(position.line, 0), new Position( Math.min(position.line + count, vimState.document.lineCount - 1), 0, ).getLineEnd(), ); const joinedText = vimState.document.getText(replaceRange).replace(/\r?\n/g, ''); // Put the cursor at the start of the last joined line's text const newCursorColumn = joinedText.length - vimState.document.lineAt(replaceRange.end).text.length; vimState.recordedState.transformer.replace( replaceRange, joinedText, PositionDiff.exactCharacter({ character: newCursorColumn, }), ); } } @RegisterAction class ActionJoinNoWhitespaceVisualMode extends BaseCommand { modes = [Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['g', 'J']; public override async exec(position: Position, vimState: VimState): Promise { const [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition); const count = start.line === end.line ? 1 : end.line - start.line; await new ActionJoinNoWhitespace().execJoin(count, start, vimState); await vimState.setCurrentMode(Mode.Normal); } } ================================================ FILE: src/actions/commands/macro.ts ================================================ import { Position } from 'vscode'; import { VimError } from '../../error'; import { Mode } from '../../mode/mode'; import { Register } from '../../register/register'; import { globalState } from '../../state/globalState'; import { RecordedState } from '../../state/recordedState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { BaseCommand, RegisterAction } from '../base'; @RegisterAction class RecordMacro extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [ ['q', ''], ['q', '"'], ]; public override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { const registerKey = this.keysPressed[1]; const register = registerKey.toLocaleLowerCase(); vimState.macro = new RecordedState(); vimState.macro.registerKey = registerKey; vimState.macro.registerName = register; if (!Register.isValidUppercaseRegister(registerKey) || !Register.has(register)) { // TODO: this seems suspect - why are we not putting `vimState.macro` in the register? Why are we setting `registerName`? const newRegister = new RecordedState(); newRegister.registerName = register; vimState.recordedState.registerName = register; Register.put(vimState, newRegister); } } } @RegisterAction export class QuitRecordMacro extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['q']; public override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { const macro = vimState.macro; if (macro === undefined) { return; } const existingMacro = (await Register.get(macro.registerName))?.text; if (existingMacro instanceof RecordedState) { if (Register.isValidUppercaseRegister(macro.registerKey)) { existingMacro.actionsRun = existingMacro.actionsRun.concat(macro.actionsRun); } else { existingMacro.actionsRun = macro.actionsRun; } } vimState.macro = undefined; } public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return super.doesActionApply(vimState, keysPressed) && vimState.macro !== undefined; } public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { return super.couldActionApply(vimState, keysPressed) && vimState.macro !== undefined; } } @RegisterAction class ExecuteLastMacro extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['@', '@']; override runsOnceForEachCountPrefix = true; override createsUndoPoint = true; override isJump = true; public override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { const { lastInvokedMacro } = globalState; if (lastInvokedMacro) { vimState.recordedState.transformer.addTransformation({ type: 'macro', register: lastInvokedMacro.registerName, replay: 'contentChange', }); } else { StatusBar.displayError(vimState, VimError.NoPreviouslyUsedRegister()); } } } @RegisterAction class ExecuteMacro extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['@', '']; override runsOnceForEachCountPrefix = true; override createsUndoPoint = true; public override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { const register = this.keysPressed[1].toLocaleLowerCase(); const isFilenameRegister = register === '%' || register === '#'; if (!Register.isValidRegister(register) || isFilenameRegister) { StatusBar.displayError(vimState, VimError.InvalidRegisterName(register)); } if (Register.has(register)) { vimState.recordedState.transformer.addTransformation({ type: 'macro', register, replay: 'contentChange', }); } } } ================================================ FILE: src/actions/commands/navigate.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { VimError } from '../../error'; import { Mode } from '../../mode/mode'; import { globalState } from '../../state/globalState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { BaseCommand, RegisterAction } from '../base'; @RegisterAction class GoToDeclaration extends BaseCommand { modes = [Mode.Normal]; keys = [ ['g', 'd'], ['g', 'D'], ]; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { await vscode.commands.executeCommand('editor.action.goToDeclaration'); if (vimState.editor === vscode.window.activeTextEditor) { // We didn't switch to a different editor vimState.cursorStartPosition = vimState.editor.selection.start; vimState.cursorStopPosition = vimState.editor.selection.end; } } } @RegisterAction class GoToDefinition extends BaseCommand { modes = [Mode.Normal]; keys = ['']; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { await vscode.commands.executeCommand('editor.action.revealDefinition'); if (vimState.editor === vscode.window.activeTextEditor) { // We didn't switch to a different editor vimState.cursorStopPosition = vimState.editor.selection.start; } } } @RegisterAction class OpenLink extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['g', 'x']; public override async exec(position: Position, vimState: VimState): Promise { void vscode.commands.executeCommand('editor.action.openLink'); } } @RegisterAction class GoBackInChangelist extends BaseCommand { modes = [Mode.Normal]; keys = ['g', ';']; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { const prevPos = vimState.historyTracker.prevChangeInChangeList(); if (prevPos instanceof VimError) { StatusBar.displayError(vimState, prevPos); } else { vimState.cursorStopPosition = prevPos; } } } @RegisterAction class GoForwardInChangelist extends BaseCommand { modes = [Mode.Normal]; keys = ['g', ',']; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { const nextPos = vimState.historyTracker.nextChangeInChangeList(); if (nextPos instanceof VimError) { StatusBar.displayError(vimState, nextPos); } else { vimState.cursorStopPosition = nextPos; } } } @RegisterAction class NavigateBack extends BaseCommand { modes = [Mode.Normal]; keys = [[''], ['']]; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await globalState.jumpTracker.jumpBack(position, vimState); } } @RegisterAction class NavigateForward extends BaseCommand { modes = [Mode.Normal]; keys = ['']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await globalState.jumpTracker.jumpForward(position, vimState); } } ================================================ FILE: src/actions/commands/put.ts ================================================ import * as vscode from 'vscode'; import { Position, TextDocument } from 'vscode'; import { Cursor } from '../../common/motion/cursor'; import { laterOf, PositionDiff, sorted } from '../../common/motion/position'; import { configuration } from '../../configuration/configuration'; import { VimError } from '../../error'; import { isVisualMode, Mode } from '../../mode/mode'; import { IRegisterContent, Register, RegisterMode } from '../../register/register'; import { RecordedState } from '../../state/recordedState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { TextEditor } from '../../textEditor'; import { Transformation } from '../../transformations/transformations'; import { reportLinesChanged } from '../../util/statusBarTextUtils'; import { BaseCommand, RegisterAction } from '../base'; function firstNonBlankChar(text: string): number { return text.match(/\S/)?.index ?? 0; } type GetCursorPositionParams = { document: TextDocument; mode: Mode; replaceRange: vscode.Range; registerMode: RegisterMode; count: number; text: string; returnToInsertAfterCommand: boolean; }; abstract class BasePutCommand extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; override createsUndoPoint = true; protected overwritesRegisterWithSelection = true; public override async exec(position: Position, vimState: VimState): Promise { const register = await Register.get(vimState.recordedState.registerName, this.multicursorIndex); if (register === undefined) { StatusBar.displayError( vimState, VimError.NothingInRegister(vimState.recordedState.registerName), ); return; } const count = vimState.recordedState.count || 1; const mode = vimState.currentMode === Mode.CommandlineInProgress ? Mode.Normal : vimState.currentMode; const registerMode = this.getRegisterMode(register); const replaceRange = this.getReplaceRange(mode, vimState.cursor, registerMode); let text = this.getRegisterText(mode, register, count); if (this.shouldAdjustIndent(mode, registerMode)) { let lineToMatch: number | undefined; if (mode === Mode.VisualLine) { const [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition); if (end.line < vimState.document.lineCount - 1) { lineToMatch = end.line + 1; } else if (start.line > 0) { lineToMatch = start.line - 1; } } else { lineToMatch = position.line; } text = this.adjustIndent( lineToMatch !== undefined ? vimState.document.lineAt(lineToMatch).text : '', text, ); } const newCursorPosition = this.getCursorPosition({ document: vimState.document, returnToInsertAfterCommand: vimState.returnToInsertAfterCommand, mode, replaceRange, registerMode, count, text, }); vimState.recordedState.transformer.moveCursor( PositionDiff.exactPosition(newCursorPosition), this.multicursorIndex ?? 0, ); if (registerMode === RegisterMode.LineWise) { text = this.adjustLinewiseRegisterText(mode, text); } for (const transformation of this.getTransformations( vimState.document, mode, replaceRange, registerMode, text, )) { vimState.recordedState.transformer.addTransformation(transformation); } // We do not run this in multi-cursor mode as it will overwrite the register for upcoming put iterations if (isVisualMode(mode) && !vimState.isMultiCursor) { // After using "p" or "P" in Visual mode the text that was put will be selected (from Vim's ":help gv"). vimState.lastVisualSelection = { mode, start: replaceRange.start, end: replaceRange.start.advancePositionByText(text), }; if (this.overwritesRegisterWithSelection) { vimState.recordedState.registerName = configuration.useSystemClipboard ? '*' : '"'; Register.put( vimState, vimState.document.getText(replaceRange), this.multicursorIndex, true, ); } } // Report lines changed let numNewlinesAfterPut = text.split('\n').length; if (registerMode === RegisterMode.LineWise) { numNewlinesAfterPut--; } reportLinesChanged(numNewlinesAfterPut, vimState); const isLastCursor = !vimState.isMultiCursor || vimState.cursors.length - 1 === this.multicursorIndex; // Place the cursor back into normal mode after all puts are completed if (isLastCursor) { await vimState.setCurrentMode(Mode.Normal); } } private getRegisterText(mode: Mode, register: IRegisterContent, count: number): string { if (register.text instanceof RecordedState) { return register.text.actionsRun .map((action) => action.keysPressed.join('')) .join('') .repeat(count); } if (register.registerMode === RegisterMode.CharacterWise) { return mode === Mode.VisualLine ? Array(count).fill(register.text).join('\n') : register.text.repeat(count); } else if (register.registerMode === RegisterMode.LineWise || mode === Mode.VisualLine) { return Array(count).fill(register.text).join('\n'); } else if (register.registerMode === RegisterMode.BlockWise) { const lines = register.text.split('\n'); const longestLength = Math.max(...lines.map((line) => line.length)); return lines .map((line) => { const space = longestLength - line.length; const lineWithSpace = line + ' '.repeat(space); return lineWithSpace.repeat(count - 1) + line; }) .join('\n'); } else { throw new Error(`Unexpected RegisterMode ${register.registerMode}`); } } private adjustIndent(lineToMatch: string, text: string): string { const lines = text.split('\n'); // Adjust indent to current line const tabSize = configuration.tabstop; // TODO: Use `editor.options.tabSize`, I think const indentationWidth = TextEditor.getIndentationLevel(lineToMatch, tabSize); const firstLineIdentationWidth = TextEditor.getIndentationLevel(lines[0], tabSize); return lines .map((line) => { const currentIdentationWidth = TextEditor.getIndentationLevel(line, tabSize); const newIndentationWidth = currentIdentationWidth - firstLineIdentationWidth + indentationWidth; // TODO: Use `editor.options.insertSpaces`, I think return TextEditor.setIndentationLevel(line, newIndentationWidth, configuration.expandtab); }) .join('\n'); } private getTransformations( document: TextDocument, mode: Mode, replaceRange: vscode.Range, registerMode: RegisterMode, text: string, ): Transformation[] { // Pasting block-wise content is very different, except in VisualLine mode, where it works exactly like line-wise if (registerMode === RegisterMode.BlockWise && mode !== Mode.VisualLine) { const transformations: Transformation[] = []; const lines = text.split('\n'); const lineCount = Math.max(lines.length, replaceRange.end.line - replaceRange.start.line + 1); const longestLength = Math.max(...lines.map((line) => line.length)); // Only relevant for Visual mode // If we replace 2 newlines, subsequent transformations need to take that into account (otherwise we get overlaps) let deletedNewlines = 0; for (let idx = 0; idx < lineCount; idx++) { const lineText = lines[idx] ?? ''; let range: vscode.Range; if (mode === Mode.VisualBlock) { if (replaceRange.start.line + idx > replaceRange.end.line) { const pos = replaceRange.start.with({ line: replaceRange.start.line + idx }); range = new vscode.Range(pos, pos); } else { range = new vscode.Range( replaceRange.start.with({ line: replaceRange.start.line + idx }), replaceRange.end.with({ line: replaceRange.start.line + idx }), ); } } else { if (idx > 0) { const pos = replaceRange.start.with({ line: replaceRange.start.line + idx + deletedNewlines, }); range = new vscode.Range(pos, pos); } else { range = new vscode.Range(replaceRange.start, replaceRange.end); deletedNewlines = document.getText(range).split('\n').length - 1; } } const lineNumber = replaceRange.start.line + idx; if (lineNumber > document.lineCount - 1) { transformations.push({ type: 'replaceText', range, text: '\n' + ' '.repeat(replaceRange.start.character) + lineText, }); } else { const lineLength = document.lineAt(lineNumber).text.length; const leftPadding = Math.max(replaceRange.start.character - lineLength, 0); let rightPadding = 0; if ( mode !== Mode.VisualBlock && ((lineNumber <= replaceRange.end.line && replaceRange.end.character < lineLength) || (lineNumber > replaceRange.end.line && replaceRange.start.character < lineLength)) ) { rightPadding = longestLength - lineText.length; } transformations.push({ type: 'replaceText', range, text: ' '.repeat(leftPadding) + lineText + ' '.repeat(rightPadding), }); } } return transformations; } if (mode === Mode.Normal || mode === Mode.Visual || mode === Mode.VisualLine) { return [ { type: 'replaceText', range: replaceRange, text, }, ]; } else if (mode === Mode.VisualBlock) { const transformations: Transformation[] = []; if (registerMode === RegisterMode.CharacterWise) { for (let line = replaceRange.start.line; line <= replaceRange.end.line; line++) { const range = new vscode.Range( new Position(line, replaceRange.start.character), new Position(line, replaceRange.end.character), ); const lineText = !text.includes('\n') || line === replaceRange.start.line ? text : ''; transformations.push({ type: 'replaceText', range, text: lineText, }); } } else if (registerMode === RegisterMode.LineWise) { // Weird case: first delete the block... for (let line = replaceRange.start.line; line <= replaceRange.end.line; line++) { const range = new vscode.Range( new Position(line, replaceRange.start.character), new Position(line, replaceRange.end.character), ); transformations.push({ type: 'replaceText', range, text: '', }); } // ...then paste the lines before/after the block const insertPos = this.putBefore() ? new Position(replaceRange.start.line, 0) : new Position(replaceRange.end.line, 0).getLineEnd(); transformations.push({ type: 'replaceText', range: new vscode.Range(insertPos, insertPos), text, }); } else { throw new Error(`Unexpected RegisterMode ${registerMode}`); } return transformations; } else { throw new Error(`Unexpected Mode ${mode}`); } } protected abstract putBefore(): boolean; protected abstract getRegisterMode(register: IRegisterContent): RegisterMode; protected abstract getReplaceRange( mode: Mode, cursor: Cursor, registerMode: RegisterMode, ): vscode.Range; protected abstract adjustLinewiseRegisterText(mode: Mode, text: string): string; protected abstract shouldAdjustIndent(mode: Mode, registerMode: RegisterMode): boolean; protected abstract getCursorPosition(params: GetCursorPositionParams): Position; } @RegisterAction class PutCommand extends BasePutCommand { keys: string[] | string[][] = ['p']; protected putBefore(): boolean { return false; } protected getRegisterMode(register: IRegisterContent): RegisterMode { return register.registerMode; } protected getReplaceRange(mode: Mode, cursor: Cursor, registerMode: RegisterMode): vscode.Range { if (mode === Mode.Normal) { let pos: Position; if (registerMode === RegisterMode.CharacterWise || registerMode === RegisterMode.BlockWise) { pos = cursor.stop.getRight(); } else if (registerMode === RegisterMode.LineWise) { pos = cursor.stop.getLineEnd(); } else { throw new Error(`Unexpected RegisterMode ${registerMode}`); } return new vscode.Range(pos, pos); } else if (mode === Mode.Visual) { const [start, end] = sorted(cursor.start, cursor.stop); return new vscode.Range(start, end.getRight()); } else if (mode === Mode.VisualLine) { const [start, end] = sorted(cursor.start, cursor.stop); return new vscode.Range(start.getLineBegin(), end.getLineEnd()); } else { const [start, end] = sorted(cursor.start, cursor.stop); return new vscode.Range(start, end.getRight()); } } protected adjustLinewiseRegisterText(mode: Mode, text: string): string { if (mode === Mode.Normal || mode === Mode.VisualBlock) { return '\n' + text; } else if (mode === Mode.Visual) { return '\n' + text + '\n'; } else { return text; } } protected shouldAdjustIndent(mode: Mode, registerMode: RegisterMode): boolean { return false; } protected getCursorPosition({ mode, replaceRange, registerMode, text, returnToInsertAfterCommand, }: GetCursorPositionParams): Position { const rangeStart = replaceRange.start; if (mode === Mode.Normal || mode === Mode.Visual) { if (registerMode === RegisterMode.CharacterWise) { if (text.includes('\n')) { return rangeStart; } else if (returnToInsertAfterCommand) { return rangeStart.advancePositionByText(text); } else { return rangeStart.advancePositionByText(text).getLeft(); } } else if (registerMode === RegisterMode.LineWise) { return new Position(rangeStart.line + 1, firstNonBlankChar(text)); } else if (registerMode === RegisterMode.BlockWise) { return rangeStart; } else { throw new Error(`Unexpected RegisterMode ${registerMode}`); } } else if (mode === Mode.VisualLine) { return rangeStart.with({ character: firstNonBlankChar(text) }); } else if (mode === Mode.VisualBlock) { if (registerMode === RegisterMode.LineWise) { return new Position(replaceRange.end.line + 1, firstNonBlankChar(text)); } else if (registerMode === RegisterMode.BlockWise) { return rangeStart; } else { return rangeStart.with({ character: rangeStart.character + text.length - 1 }); } } else { throw new Error(`Unexpected Mode ${mode}`); } } } @RegisterAction class PutBeforeCommand extends PutCommand { override keys: string[] | string[][] = ['P']; // Since Vim 9.0, Visual `P` does not overwrite the unnamed register with selection's contents override overwritesRegisterWithSelection = false; protected override putBefore(): boolean { return true; } protected override adjustLinewiseRegisterText(mode: Mode, text: string): string { if (mode === Mode.Normal || mode === Mode.VisualBlock) { return text + '\n'; } return super.adjustLinewiseRegisterText(mode, text); } protected override getReplaceRange( mode: Mode, cursor: Cursor, registerMode: RegisterMode, ): vscode.Range { if (mode === Mode.Normal) { if (registerMode === RegisterMode.CharacterWise || registerMode === RegisterMode.BlockWise) { const pos = cursor.stop; return new vscode.Range(pos, pos); } else if (registerMode === RegisterMode.LineWise) { const pos = cursor.stop.getLineBegin(); return new vscode.Range(pos, pos); } } return super.getReplaceRange(mode, cursor, registerMode); } protected override getCursorPosition({ mode, replaceRange, text, registerMode, ...params }: GetCursorPositionParams): Position { const rangeStart = replaceRange.start; if (mode === Mode.Normal || mode === Mode.VisualBlock) { if (registerMode === RegisterMode.LineWise) { return rangeStart.with({ character: firstNonBlankChar(text) }); } } return super.getCursorPosition({ mode, replaceRange, text, registerMode, ...params }); } } function PlaceCursorAfterText PutCommand>(Base: TBase) { return class CursorAfterText extends Base { protected override getCursorPosition({ document, mode, replaceRange, registerMode, count, text, ...params }: GetCursorPositionParams): Position { const rangeStart = replaceRange.start; if (mode === Mode.Normal || mode === Mode.Visual) { if (registerMode === RegisterMode.CharacterWise) { if (text.includes('\n')) { // Weird case: if there's a newline, the cursor goes to the same place, regardless of [count] // HACK: We're undoing the repeat() here - definitely a bit janky text = text.slice(0, text.length / count); } return rangeStart.advancePositionByText(text); } else if (registerMode === RegisterMode.LineWise) { let line = rangeStart.line + text.split('\n').length; if ( mode === Mode.Visual || (!this.putBefore() && rangeStart.line < document.lineCount - 1) ) { line++; } return new Position(line, 0); } else if (registerMode === RegisterMode.BlockWise) { const lines = text.split('\n'); const lastLine = rangeStart.line + lines.length - 1; const longestLineLength = Math.max(...lines.map((line) => line.length)); return new Position(lastLine, rangeStart.character + longestLineLength); } } else if (mode === Mode.VisualLine) { return new Position(rangeStart.line + text.split('\n').length, 0); } else if (mode === Mode.VisualBlock) { const lines = text.split('\n'); if (registerMode === RegisterMode.LineWise) { if (this.putBefore()) { return new Position(rangeStart.line + lines.length, 0); } else { return new Position(replaceRange.end.line + lines.length + 1, 0); } } else if (registerMode === RegisterMode.BlockWise) { return new Position( replaceRange.start.line + lines.length - 1, replaceRange.start.character + lines[lines.length - 1].length, ); } else { return rangeStart.with({ character: rangeStart.character + text.length }); } } return super.getCursorPosition({ document, mode, replaceRange, registerMode, count, text, ...params, }); } }; } @RegisterAction @PlaceCursorAfterText class GPutCommand extends PutCommand { override keys = ['g', 'p']; } @RegisterAction @PlaceCursorAfterText class GPutBeforeCommand extends PutBeforeCommand { override keys = ['g', 'P']; override overwritesRegisterWithSelection = true; } function AdjustIndent PutCommand>(Base: TBase) { return class AdjustedIndent extends Base { protected override shouldAdjustIndent(mode: Mode, registerMode: RegisterMode): boolean { return ( (mode === Mode.Normal || mode === Mode.VisualLine) && registerMode === RegisterMode.LineWise ); } }; } @RegisterAction @AdjustIndent class PutWithIndentCommand extends PutCommand { override keys = [']', 'p']; } @RegisterAction @AdjustIndent class PutBeforeWithIndentCommand extends PutBeforeCommand { override keys = [ ['[', 'P'], [']', 'P'], ['[', 'p'], ]; } function ExCommand PutCommand>(Base: TBase) { return class Ex extends Base { private insertLine?: number; public setInsertionLine(insertLine: number) { this.insertLine = insertLine; } protected override getRegisterMode(register: IRegisterContent): RegisterMode { return RegisterMode.LineWise; } protected override getReplaceRange( mode: Mode, cursor: Cursor, registerMode: RegisterMode, ): vscode.Range { const line = this.insertLine ?? laterOf(cursor.start, cursor.stop).line; const pos = this.putBefore() ? new Position(line, 0) : new Position(line, 0).getLineEnd(); return new vscode.Range(pos, pos); } protected override getCursorPosition({ replaceRange, text, }: GetCursorPositionParams): Position { const lines = text.split('\n'); return new Position( replaceRange.start.line + lines.length - (this.putBefore() ? 1 : 0), firstNonBlankChar(lines[lines.length - 1]), ); } }; } export const PutFromCmdLine = ExCommand(PutCommand); export const PutBeforeFromCmdLine = ExCommand(PutBeforeCommand); ================================================ FILE: src/actions/commands/replace.ts ================================================ import { Position, Range } from 'vscode'; import { Cursor } from '../../common/motion/cursor'; import { PositionDiff, sorted } from '../../common/motion/position'; import { DotCommandStatus, Mode, visualBlockGetTopLeftPosition } from '../../mode/mode'; import { ModeDataFor } from '../../mode/modeData'; import { VimState } from '../../state/vimState'; import { TextEditor } from '../../textEditor'; import { BaseCommand, RegisterAction } from '../base'; import { BaseMovement } from '../baseMotion'; import { MoveDown, MoveLeft, MoveRight, MoveUp } from '../motion'; @RegisterAction export class ReplaceCharacter extends BaseCommand { modes = [Mode.Normal]; keys = ['r', '']; override createsUndoPoint = true; override runsOnceForEachCountPrefix = false; public override async exec(position: Position, vimState: VimState): Promise { const timesToRepeat = vimState.recordedState.count || 1; const toReplace = this.keysPressed[1]; /** * includes , and but not any control keys, * so we ignore the former two keys and have a special handle for . */ if (['', ''].includes(toReplace.toUpperCase())) { return; } if (position.character + timesToRepeat > position.getLineEnd().character) { return; } let endPos = new Position(position.line, position.character + timesToRepeat); // Return if tried to repeat longer than linelength if (endPos.character > vimState.document.lineAt(endPos).text.length) { return; } // If last char (not EOL char), add 1 so that replace selection is complete if (endPos.character > vimState.document.lineAt(endPos).text.length) { endPos = new Position(endPos.line, endPos.character + 1); } if (toReplace === '') { vimState.recordedState.transformer.delete(new Range(position, endPos)); vimState.recordedState.transformer.vscodeCommand('tab'); vimState.recordedState.transformer.moveCursor( PositionDiff.offset({ character: -1 }), this.multicursorIndex, ); } else if (toReplace === '\n') { // A newline replacement always inserts exactly one newline (regardless // of count prefix) and puts the cursor on the next line. // We use `insertTextVSCode` so we get the right indentation vimState.recordedState.transformer.delete(new Range(position, endPos)); vimState.recordedState.transformer.addTransformation({ type: 'insertTextVSCode', text: '\n', }); } else { vimState.recordedState.transformer.addTransformation({ type: 'replaceText', text: toReplace.repeat(timesToRepeat), range: new Range(position, endPos), diff: PositionDiff.offset({ character: timesToRepeat - 1 }), manuallySetCursorPositions: vimState.dotCommandStatus === DotCommandStatus.Executing ? true : undefined, }); } } } @RegisterAction class ReplaceCharacterVisual extends BaseCommand { modes = [Mode.Visual, Mode.VisualLine]; keys = ['r', '']; override createsUndoPoint = true; public override async exec(position: Position, vimState: VimState): Promise { let toInsert = this.keysPressed[1]; if (toInsert === '') { toInsert = TextEditor.getTabCharacter(vimState.editor); } let visualSelectionOffset = 1; // If selection is reversed, reorganize it so that the text replace logic always works let [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition); if (vimState.currentMode === Mode.VisualLine) { [start, end] = [start.getLineBegin(), end.getLineEnd()]; } // Limit to not replace EOL const textLength = vimState.document.lineAt(end).text.length; if (textLength <= 0) { visualSelectionOffset = 0; } end = new Position(end.line, Math.min(end.character, textLength > 0 ? textLength - 1 : 0)); // Iterate over every line in the current selection for (let lineNum = start.line; lineNum <= end.line; lineNum++) { // Get line of text const lineText = vimState.document.lineAt(lineNum).text; if (start.line === end.line) { // This is a visual section all on one line, only replace the part within the selection vimState.recordedState.transformer.addTransformation({ type: 'replaceText', text: Array(end.character - start.character + 2).join(toInsert), range: new Range(start, new Position(end.line, end.character + 1)), manuallySetCursorPositions: true, }); } else if (lineNum === start.line) { // This is the first line of the selection so only replace after the cursor vimState.recordedState.transformer.addTransformation({ type: 'replaceText', text: Array(lineText.length - start.character + 1).join(toInsert), range: new Range(start, new Position(start.line, lineText.length)), manuallySetCursorPositions: true, }); } else if (lineNum === end.line) { // This is the last line of the selection so only replace before the cursor vimState.recordedState.transformer.addTransformation({ type: 'replaceText', text: Array(end.character + 1 + visualSelectionOffset).join(toInsert), range: new Range( new Position(end.line, 0), new Position(end.line, end.character + visualSelectionOffset), ), manuallySetCursorPositions: true, }); } else { // Replace the entire line length since it is in the middle of the selection vimState.recordedState.transformer.addTransformation({ type: 'replaceText', text: Array(lineText.length + 1).join(toInsert), range: new Range(new Position(lineNum, 0), new Position(lineNum, lineText.length)), manuallySetCursorPositions: true, }); } } vimState.cursorStopPosition = start; vimState.cursorStartPosition = start; await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class ReplaceCharacterVisualBlock extends BaseCommand { modes = [Mode.VisualBlock]; keys = ['r', '']; override createsUndoPoint = true; public override async exec(position: Position, vimState: VimState): Promise { let toInsert = this.keysPressed[1]; if (toInsert === '') { toInsert = TextEditor.getTabCharacter(vimState.editor); } for (const { start, end } of TextEditor.iterateLinesInBlock(vimState)) { if (end.isBeforeOrEqual(start)) { continue; } vimState.recordedState.transformer.addTransformation({ type: 'replaceText', text: Array(end.character - start.character + 1).join(toInsert), range: new Range(start, end), manuallySetCursorPositions: true, }); } vimState.cursors = [ Cursor.atPosition( visualBlockGetTopLeftPosition(vimState.cursorStopPosition, vimState.cursorStartPosition), ), ]; await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction export class EnterReplaceMode extends BaseCommand { modes = [Mode.Normal]; keys = ['R']; public override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Replace); } } @RegisterAction class ExitReplaceMode extends BaseCommand { modes = [Mode.Replace]; keys = [[''], [''], ['']]; public override async exec(position: Position, vimState: VimState): Promise { if (vimState.modeData.mode !== Mode.Replace) { throw new Error(`Unexpected mode ${vimState.modeData.mode} in ExitReplaceMode`); } const timesToRepeat = vimState.modeData.replaceState.timesToRepeat; const cursorIdx = this.multicursorIndex ?? 0; const changes = vimState.modeData.replaceState.getChanges(cursorIdx); // `3Rabc` results in 'abc' replacing the next characters 2 more times if (changes && timesToRepeat > 1) { const newText = changes .map((change) => change.after) .join('') .repeat(timesToRepeat - 1); vimState.recordedState.transformer.replace( new Range(position, position.getRight(newText.length)), newText, ); } else { vimState.cursorStopPosition = vimState.cursorStopPosition.getLeft(); } if (this.multicursorIndex === vimState.cursors.length - 1) { await vimState.setCurrentMode(Mode.Normal); } } } @RegisterAction class ReplaceModeToInsertMode extends BaseCommand { modes = [Mode.Replace]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Insert); } } @RegisterAction class BackspaceInReplaceMode extends BaseCommand { modes = [Mode.Replace]; keys = [[''], [''], [''], ['']]; public override async exec(position: Position, vimState: VimState): Promise { if (vimState.modeData.mode !== Mode.Replace) { throw new Error(`Unexpected mode ${vimState.modeData.mode} in BackspaceInReplaceMode`); } const cursorIdx = this.multicursorIndex ?? 0; const changes = vimState.modeData.replaceState.getChanges(cursorIdx); if (changes.length === 0) { // If you backspace before the beginning of where you started to replace, just move the cursor back. const newPosition = position.getLeftThroughLineBreaks(); vimState.modeData.replaceState.resetChanges(cursorIdx); vimState.cursorStopPosition = newPosition; vimState.cursorStartPosition = newPosition; } else { const { before } = changes.pop()!; if (before === '') { // We've gone beyond the originally existing text; just backspace. // TODO: should this use a 'deleteLeft' transformation? vimState.recordedState.transformer.delete( new Range(position.getLeftThroughLineBreaks(), position), ); } else { vimState.recordedState.transformer.replace( new Range(position.getLeft(), position), before, PositionDiff.offset({ character: -1 }), ); } } } } @RegisterAction class DeleteInReplaceMode extends BaseCommand { modes = [Mode.Replace]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { vimState.recordedState.transformer.vscodeCommand('deleteRight'); } } @RegisterAction class ReplaceInReplaceMode extends BaseCommand { modes = [Mode.Replace]; keys = ['']; override createsUndoPoint = true; public override async exec(position: Position, vimState: VimState): Promise { if (vimState.modeData.mode !== Mode.Replace) { throw new Error(`Unexpected mode ${vimState.modeData.mode} in ReplaceInReplaceMode`); } const char = this.keysPressed[0]; const isNewLineOrTab = char === '\n' || char === ''; const replaceRange = new Range(position, position.getRight()); let before = vimState.document.getText(replaceRange); if (!position.isLineEnd(vimState.document) && !isNewLineOrTab) { vimState.recordedState.transformer.replace( replaceRange, char, PositionDiff.offset({ character: 1 }), ); } else if (char === '') { vimState.recordedState.transformer.delete(replaceRange); vimState.recordedState.transformer.vscodeCommand('tab'); } else { vimState.recordedState.transformer.insert(position, char); before = ''; } vimState.modeData.replaceState.getChanges(this.multicursorIndex ?? 0).push({ before, after: char, }); } } @RegisterAction class CreateUndoPoint extends BaseCommand { modes = [Mode.Replace]; keys = ['', 'u']; public override async exec(position: Position, vimState: VimState): Promise { vimState.historyTracker.addChange(true); vimState.historyTracker.finishCurrentStep(); } } @RegisterAction class ArrowsInReplaceMode extends BaseMovement { override modes = [Mode.Replace]; keys = [[''], [''], [''], ['']]; public override async execAction(position: Position, vimState: VimState): Promise { // Force an undo point to be created vimState.historyTracker.addChange(true); vimState.historyTracker.finishCurrentStep(); let newPosition: Position = position; switch (this.keysPressed[0]) { case '': newPosition = await new MoveUp(this.keysPressed).execAction(position, vimState); break; case '': newPosition = await new MoveDown(this.keysPressed).execAction(position, vimState); break; case '': newPosition = await new MoveLeft(this.keysPressed).execAction(position, vimState); break; case '': newPosition = await new MoveRight(this.keysPressed).execAction(position, vimState); break; default: throw new Error(`Unexpected 'arrow' key: ${this.keys[0]}`); } (vimState.modeData as ModeDataFor).replaceState.resetChanges( this.multicursorIndex ?? 0, ); return newPosition; } } ================================================ FILE: src/actions/commands/scroll.ts ================================================ import { clamp } from 'lodash'; import * as vscode from 'vscode'; import { Position } from 'vscode'; import { configuration } from '../../configuration/configuration'; import { Mode, isVisualMode } from '../../mode/mode'; import { VimState } from '../../state/vimState'; import { EditorScrollByUnit, EditorScrollDirection, TextEditor } from '../../textEditor'; import { BaseCommand, RegisterAction } from '../base'; abstract class CommandEditorScroll extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; override runsOnceForEachCountPrefix = false; override runsOnceForEveryCursor(): boolean { return false; } abstract to: EditorScrollDirection; abstract by: EditorScrollByUnit; public override async exec(position: Position, vimState: VimState): Promise { const timesToRepeat = vimState.recordedState.count || 1; const scrolloff = configuration .getConfiguration('editor') .get('cursorSurroundingLines', 0); const visibleRange = vimState.editor.visibleRanges[0]; if (visibleRange === undefined) { return; } const linesAboveCursor = visibleRange.end.line - vimState.cursorStopPosition.line - timesToRepeat; const linesBelowCursor = vimState.cursorStopPosition.line - visibleRange.start.line - timesToRepeat; if (this.to === 'up' && scrolloff > linesAboveCursor) { vimState.cursorStopPosition = vimState.cursorStopPosition .getUp(scrolloff - linesAboveCursor) .withColumn(vimState.desiredColumn); } else if (this.to === 'down' && scrolloff > linesBelowCursor) { vimState.cursorStopPosition = vimState.cursorStopPosition .getDown(scrolloff - linesBelowCursor) .withColumn(vimState.desiredColumn); } vimState.postponedCodeViewChanges.push({ command: 'editorScroll', args: { to: this.to, by: this.by, value: timesToRepeat, select: isVisualMode(vimState.currentMode), }, }); } } @RegisterAction class CommandCtrlE extends CommandEditorScroll { keys = ['']; override preservesDesiredColumn = true; to: EditorScrollDirection = 'down'; by: EditorScrollByUnit = 'line'; } @RegisterAction class CommandCtrlY extends CommandEditorScroll { keys = ['']; override preservesDesiredColumn = true; to: EditorScrollDirection = 'up'; by: EditorScrollByUnit = 'line'; } /** * Commands like `` and `` act *sort* of like ``, but they move * your cursor down and put it on the first non-whitespace character of the line. */ abstract class CommandScrollAndMoveCursor extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; override runsOnceForEachCountPrefix = false; abstract to: EditorScrollDirection; /** if true, set scroll option instead of repeating command */ setScroll = false; /** * @returns the number of lines this command should move the cursor */ protected abstract getNumLines(visibleRanges: readonly vscode.Range[]): number; public override async exec(position: Position, vimState: VimState): Promise { const { visibleRanges } = vimState.editor; if (visibleRanges.length === 0) { return; } const smoothScrolling = configuration .getConfiguration('editor') .get('smoothScrolling', false); if (this.setScroll && vimState.recordedState.count) configuration.scroll = vimState.recordedState.count; const timesToRepeat = (!this.setScroll && vimState.recordedState.count) || 1; const moveLines = timesToRepeat * this.getNumLines(visibleRanges); let scrollLines = moveLines; if (this.to === 'down') { // This makes less wonky when `editor.scrollBeyondLastLine` is enabled scrollLines = Math.min( moveLines, vimState.document.lineCount - 1 - visibleRanges[visibleRanges.length - 1].end.line, ); } if (scrollLines > 0) { const args = { to: this.to, by: 'line', value: scrollLines, revealCursor: smoothScrolling, select: isVisualMode(vimState.currentMode), }; if (smoothScrolling) { await vscode.commands.executeCommand('editorScroll', args); } else { vimState.postponedCodeViewChanges.push({ command: 'editorScroll', args, }); } } const newPositionLine = clamp( position.line + (this.to === 'down' ? moveLines : -moveLines), 0, vimState.document.lineCount - 1, ); vimState.cursorStopPosition = new Position( newPositionLine, vimState.desiredColumn, ).obeyStartOfLine(vimState.document); } } @RegisterAction class CommandMoveFullPageUp extends CommandScrollAndMoveCursor { keys = ['']; to: EditorScrollDirection = 'up'; protected getNumLines(visibleRanges: vscode.Range[]) { return visibleRanges[0].end.line - visibleRanges[0].start.line; } } @RegisterAction class CommandMoveFullPageDown extends CommandScrollAndMoveCursor { keys = ['']; to: EditorScrollDirection = 'down'; protected getNumLines(visibleRanges: vscode.Range[]) { return visibleRanges[0].end.line - visibleRanges[0].start.line; } } @RegisterAction class CommandCtrlD extends CommandScrollAndMoveCursor { keys = ['']; to: EditorScrollDirection = 'down'; override setScroll = true; protected getNumLines(visibleRanges: vscode.Range[]) { return configuration.getScrollLines(visibleRanges); } } @RegisterAction class CommandCtrlU extends CommandScrollAndMoveCursor { keys = ['']; to: EditorScrollDirection = 'up'; override setScroll = true; protected getNumLines(visibleRanges: vscode.Range[]) { return configuration.getScrollLines(visibleRanges); } } @RegisterAction class CommandCenterScroll extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', 'z']; override preservesDesiredColumn = true; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } public override async exec(position: Position, vimState: VimState): Promise { // In these modes you want to center on the cursor position vimState.editor.revealRange( new vscode.Range(vimState.cursorStopPosition, vimState.cursorStopPosition), vscode.TextEditorRevealType.InCenter, ); } } @RegisterAction class CommandCenterScrollFirstChar extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', '.']; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } public override async exec(position: Position, vimState: VimState): Promise { // In these modes you want to center on the cursor position // This particular one moves cursor to first non blank char though vimState.editor.revealRange( new vscode.Range(vimState.cursorStopPosition, vimState.cursorStopPosition), vscode.TextEditorRevealType.InCenter, ); // Move cursor to first char of line vimState.cursorStopPosition = TextEditor.getFirstNonWhitespaceCharOnLine( vimState.document, vimState.cursorStopPosition.line, ); } } @RegisterAction class CommandTopScroll extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', 't']; override preservesDesiredColumn = true; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'revealLine', args: { lineNumber: position.line, at: 'top', }, }); } } @RegisterAction class CommandTopScrollFirstChar extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', '\n']; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } public override async exec(position: Position, vimState: VimState): Promise { // In these modes you want to center on the cursor position // This particular one moves cursor to first non blank char though vimState.postponedCodeViewChanges.push({ command: 'revealLine', args: { lineNumber: position.line, at: 'top', }, }); // Move cursor to first char of line vimState.cursorStopPosition = TextEditor.getFirstNonWhitespaceCharOnLine( vimState.document, vimState.cursorStopPosition.line, ); } } @RegisterAction class CommandBottomScroll extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', 'b']; override preservesDesiredColumn = true; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'revealLine', args: { lineNumber: position.line, at: 'bottom', }, }); } } @RegisterAction class CommandBottomScrollFirstChar extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', '-']; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } public override async exec(position: Position, vimState: VimState): Promise { // In these modes you want to center on the cursor position // This particular one moves cursor to first non blank char though vimState.postponedCodeViewChanges.push({ command: 'revealLine', args: { lineNumber: position.line, at: 'bottom', }, }); // Move cursor to first char of line vimState.cursorStopPosition = TextEditor.getFirstNonWhitespaceCharOnLine( vimState.document, vimState.cursorStopPosition.line, ); } } ================================================ FILE: src/actions/commands/search.ts ================================================ import * as _ from 'lodash'; import { escapeRegExp } from 'lodash'; import { Position, Selection } from 'vscode'; import { SearchCommandLine } from '../../cmd_line/commandLine'; import { sorted } from '../../common/motion/position'; import { configuration } from '../../configuration/configuration'; import { VimError } from '../../error'; import { Mode, isVisualMode } from '../../mode/mode'; import { Register } from '../../register/register'; import { globalState } from '../../state/globalState'; import { SearchState } from '../../state/searchState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { TextEditor } from '../../textEditor'; import { TextObject } from '../../textobject/textobject'; import { reportSearch } from '../../util/statusBarTextUtils'; import { SearchDirection } from '../../vimscript/pattern'; import { BaseCommand, RegisterAction } from '../base'; import { IMovement, failedMovement } from '../baseMotion'; /** * Search for the word under the cursor; used by [g]* and [g]# */ async function searchCurrentWord( position: Position, vimState: VimState, direction: SearchDirection, isExact: boolean, ): Promise { let currentWord = TextEditor.getWord(vimState.document, position); if (currentWord) { if (/\W/.test(currentWord[0]) || /\W/.test(currentWord[currentWord.length - 1])) { // TODO: this kind of sucks. JS regex does not consider the boundary between a special // character and whitespace to be a "word boundary", so we can't easily do an exact search. isExact = false; } if (isExact) { currentWord = _.escapeRegExp(currentWord); } // If the search is going left then use `getWordLeft()` on position to start // at the beginning of the word. This ensures that any matches happen // outside of the currently selected word. const searchStartCursorPosition = direction === SearchDirection.Backward ? vimState.cursorStopPosition.prevWordStart(vimState.document, { inclusive: true }) : vimState.cursorStopPosition; await createSearchStateAndMoveToMatch({ needle: currentWord, vimState, direction, isExact, searchStartCursorPosition, }); } else { StatusBar.displayError(vimState, VimError.NoStringUnderCursor()); } } /** * Search for the word under the cursor; used by [g]* and [g]# in visual mode when `visualstar` is enabled */ async function searchCurrentSelection(vimState: VimState, direction: SearchDirection) { const currentSelection = vimState.document.getText(vimState.editor.selection); // Go back to Normal mode, otherwise the selection grows to the next match. await vimState.setCurrentMode(Mode.Normal); const [start, end] = sorted(vimState.cursorStartPosition, vimState.cursorStopPosition); // Ensure that any matches happen outside of the currently selected word. const searchStartCursorPosition = direction === SearchDirection.Backward ? start.getLeft() : end.getRight(); await createSearchStateAndMoveToMatch({ needle: currentSelection, vimState, direction, isExact: false, searchStartCursorPosition, }); } /** * Used by [g]* and [g]# */ async function createSearchStateAndMoveToMatch(args: { needle: string; vimState: VimState; direction: SearchDirection; isExact: boolean; searchStartCursorPosition: Position; }): Promise { const { needle, vimState, isExact } = args; if (needle.length === 0) { return; } const escapedNeedle = escapeRegExp(needle).replaceAll('/', '\\/'); const searchString = isExact ? `\\<${escapedNeedle}\\>` : escapedNeedle; // Start a search for the given term. globalState.searchState = new SearchState( args.direction, vimState.cursorStopPosition, searchString, { ignoreSmartcase: true }, ); Register.setReadonlyRegister('/', globalState.searchState.searchString); void SearchCommandLine.addSearchStateToHistory(globalState.searchState); // Turn one of the highlighting flags back on (turned off with :nohl) globalState.hl = true; const nextMatch = globalState.searchState.getNextSearchMatchPosition( vimState, args.searchStartCursorPosition, ); if (nextMatch) { vimState.cursorStopPosition = nextMatch.pos; reportSearch( nextMatch.index, globalState.searchState.getMatchRanges(vimState).length, vimState, ); } else { StatusBar.displayError( vimState, args.direction === SearchDirection.Forward ? VimError.SearchHitBottom(globalState.searchState.searchString) : VimError.SearchHitTop(globalState.searchState.searchString), ); } } @RegisterAction class SearchCurrentWordExactForward extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['*']; override actionType = 'motion' as const; override runsOnceForEachCountPrefix = true; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { if (isVisualMode(vimState.currentMode) && configuration.visualstar) { await searchCurrentSelection(vimState, SearchDirection.Forward); } else { await searchCurrentWord(position, vimState, SearchDirection.Forward, true); } } } @RegisterAction class SearchCurrentWordForward extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['g', '*']; override actionType = 'motion' as const; override runsOnceForEachCountPrefix = true; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { await searchCurrentWord(position, vimState, SearchDirection.Forward, false); } } @RegisterAction class SearchCurrentWordExactBackward extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['#']; override actionType = 'motion' as const; override runsOnceForEachCountPrefix = true; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { if (isVisualMode(vimState.currentMode) && configuration.visualstar) { await searchCurrentSelection(vimState, SearchDirection.Backward); } else { await searchCurrentWord(position, vimState, SearchDirection.Backward, true); } } } @RegisterAction class SearchCurrentWordBackward extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['g', '#']; override actionType = 'motion' as const; override runsOnceForEachCountPrefix = true; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { await searchCurrentWord(position, vimState, SearchDirection.Backward, false); } } @RegisterAction class SearchForwards extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['/']; override actionType = 'motion' as const; override isJump = true; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.SearchInProgressMode); } } @RegisterAction class SearchBackwards extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['?']; override actionType = 'motion' as const; override isJump = true; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { // TODO: Better VimState API than this... await vimState.setModeData({ mode: Mode.SearchInProgressMode, commandLine: new SearchCommandLine(vimState, '', SearchDirection.Backward), firstVisibleLineBeforeSearch: vimState.editor.visibleRanges[0].start.line, }); } } abstract class SearchObject extends TextObject { override modes = [Mode.Normal, Mode.Visual, Mode.VisualBlock]; protected abstract readonly direction: SearchDirection; public async execAction(position: Position, vimState: VimState): Promise { const searchState = globalState.searchState; if (!searchState || searchState.searchString === '') { return failedMovement(vimState); } const newSearchState = new SearchState( this.direction, vimState.cursorStopPosition, searchState.searchString, {}, ); // At first, try to search for current word, and stop searching if matched. // Try to search for the next word if not matched or // if the cursor is at the end of a match string in visual-mode. let result = newSearchState.findContainingMatchRange(vimState, vimState.cursorStopPosition); if ( result && vimState.currentMode === Mode.Visual && vimState.cursorStopPosition.isEqual(result.range.end.getLeftThroughLineBreaks()) ) { result = undefined; } if (result === undefined) { // Try to search for the next word result = newSearchState.getNextSearchMatchRange(vimState, vimState.cursorStopPosition); if (result === undefined) { return failedMovement(vimState); } } reportSearch(result.index, searchState.getMatchRanges(vimState).length, vimState); const [start, stop] = [ vimState.currentMode === Mode.Normal ? result.range.start : vimState.cursorStopPosition, result.range.end.getLeftThroughLineBreaks(), ]; // Move the cursor, this is a bit hacky... vimState.cursorStartPosition = start; vimState.cursorStopPosition = stop; vimState.editor.selection = new Selection(start, stop); await vimState.setCurrentMode(Mode.Visual); return { start, stop, }; } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { return this.execAction(position, vimState); } } @RegisterAction class SearchObjectForward extends SearchObject { keys = ['g', 'n']; direction = SearchDirection.Forward; } @RegisterAction class SearchObjectBackward extends SearchObject { keys = ['g', 'N']; direction = SearchDirection.Backward; } ================================================ FILE: src/actions/commands/undo.ts ================================================ import { Position } from 'vscode'; import { Mode } from '../../mode/mode'; import { VimState } from '../../state/vimState'; import { BaseCommand, RegisterAction } from '../base'; @RegisterAction export class Undo extends BaseCommand { modes = [Mode.Normal]; keys = ['u']; // we support a count to undo by this setting override runsOnceForEachCountPrefix = true; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await vimState.historyTracker.goBackHistoryStep(); } } @RegisterAction class UndoOnLine extends BaseCommand { modes = [Mode.Normal]; keys = ['U']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await vimState.historyTracker.goBackHistoryStepsOnLine(); } } @RegisterAction export class Redo extends BaseCommand { modes = [Mode.Normal]; keys = ['']; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await vimState.historyTracker.goForwardHistoryStep(); } } ================================================ FILE: src/actions/commands/visual.ts ================================================ import { Position } from 'vscode'; import { Mode } from '../../mode/mode'; import { VimState } from '../../state/vimState'; import { BaseCommand, RegisterAction } from '../base'; @RegisterAction class EnterVisualMode extends BaseCommand { modes = [Mode.Normal, Mode.VisualLine, Mode.VisualBlock]; keys = ['v']; override isCompleteAction = false; public override async exec(position: Position, vimState: VimState): Promise { if (vimState.currentMode === Mode.Normal && vimState.recordedState.count > 1) { vimState.cursorStopPosition = position.getRight(vimState.recordedState.count - 1); } await vimState.setCurrentMode(Mode.Visual); } } @RegisterAction class ExitVisualMode extends BaseCommand { modes = [Mode.Visual]; keys = ['v']; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class EnterVisualLineMode extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualBlock]; keys = ['V']; public override async exec(position: Position, vimState: VimState): Promise { if (vimState.currentMode === Mode.Normal && vimState.recordedState.count > 1) { vimState.cursorStopPosition = position.getDown(vimState.recordedState.count - 1); } await vimState.setCurrentMode(Mode.VisualLine); } } @RegisterAction class ExitVisualLineMode extends BaseCommand { modes = [Mode.VisualLine]; keys = ['V']; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class EnterVisualBlockMode extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [[''], ['']]; public override async exec(position: Position, vimState: VimState): Promise { if (vimState.currentMode === Mode.Normal && vimState.recordedState.count > 1) { vimState.cursorStopPosition = position.getRight(vimState.recordedState.count - 1); } await vimState.setCurrentMode(Mode.VisualBlock); } } @RegisterAction class ExitVisualBlockMode extends BaseCommand { modes = [Mode.VisualBlock]; keys = [[''], ['']]; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class RestoreVisualSelection extends BaseCommand { modes = [Mode.Normal]; keys = ['g', 'v']; public override async exec(position: Position, vimState: VimState): Promise { if (vimState.lastVisualSelection === undefined) { return; } let { start, end, mode } = vimState.lastVisualSelection; if (mode !== Mode.Visual || !start.isEqual(end)) { if (end.line <= vimState.document.lineCount - 1) { if (mode === Mode.Visual && start.isBefore(end)) { end = end.getLeftThroughLineBreaks(true); } await vimState.setCurrentMode(mode); vimState.cursorStartPosition = start; vimState.cursorStopPosition = end; } } } } ================================================ FILE: src/actions/commands/window.ts ================================================ import { Position } from 'vscode'; import { OnlyCommand } from '../../cmd_line/commands/only'; import { QuitCommand } from '../../cmd_line/commands/quit'; import { TabCommand, TabCommandType } from '../../cmd_line/commands/tab'; import { WriteQuitCommand } from '../../cmd_line/commands/writequit'; import { Mode } from '../../mode/mode'; import { VimState } from '../../state/vimState'; import { BaseCommand, RegisterAction } from '../base'; @RegisterAction class Quit extends BaseCommand { modes = [Mode.Normal]; keys = [ ['', 'q'], ['', ''], ['', 'c'], ['', ''], ]; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { void new QuitCommand({}).execute(vimState); } } @RegisterAction class WriteQuit extends BaseCommand { modes = [Mode.Normal]; keys = [['Z', 'Z']]; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await new WriteQuitCommand({ bang: false, opt: [] }).execute(vimState); } } @RegisterAction class ForceQuit extends BaseCommand { modes = [Mode.Normal]; keys = [['Z', 'Q']]; override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { await new QuitCommand({ bang: true }).execute(vimState); } } @RegisterAction class Only extends BaseCommand { modes = [Mode.Normal]; keys = [ ['', 'o'], ['', ''], ]; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { void new OnlyCommand().execute(vimState); } } @RegisterAction class MoveToLeftPane extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [ ['', 'h'], ['', ''], ['', ''], ]; override runsOnceForEveryCursor(): boolean { return false; } override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.navigateLeft', args: {}, }); } } @RegisterAction class MoveToRightPane extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [ ['', 'l'], ['', ''], ['', ''], ]; override runsOnceForEveryCursor(): boolean { return false; } override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.navigateRight', args: {}, }); } } @RegisterAction class MoveToLowerPane extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [ ['', 'j'], ['', ''], ['', ''], ]; override runsOnceForEveryCursor(): boolean { return false; } override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.navigateDown', args: {}, }); } } @RegisterAction class MoveToUpperPane extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [ ['', 'k'], ['', ''], ['', ''], ]; override runsOnceForEveryCursor(): boolean { return false; } override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.navigateUp', args: {}, }); } } @RegisterAction class CycleThroughPanes extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [ ['', ''], ['', 'w'], ]; override runsOnceForEveryCursor(): boolean { return false; } override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.navigateEditorGroups', args: {}, }); } } @RegisterAction class VerticalSplit extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [ ['', 'v'], ['', ''], ]; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.splitEditor', args: undefined, }); } } @RegisterAction class OrthogonalSplit extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [ ['', 's'], ['', ''], ]; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.splitEditorOrthogonal', args: undefined, }); } } @RegisterAction class EvenPaneWidths extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['', '=']; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.evenEditorWidths', args: {}, }); } } @RegisterAction class IncreasePaneWidth extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['', '>']; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.increaseViewWidth', args: {}, }); } } @RegisterAction class DecreasePaneWidth extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['', '<']; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.decreaseViewWidth', args: {}, }); } } @RegisterAction class IncreasePaneHeight extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['', '+']; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.increaseViewHeight', args: {}, }); } } @RegisterAction class DecreasePaneHeight extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['', '-']; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { vimState.postponedCodeViewChanges.push({ command: 'workbench.action.decreaseViewHeight', args: {}, }); } } @RegisterAction class NextTab extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [['g', 't'], ['']]; override runsOnceForEachCountPrefix = false; override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { // gt behaves differently than gT and goes to an absolute index tab // (1-based), it does NOT iterate over next tabs if (vimState.recordedState.count > 0) { void new TabCommand({ type: TabCommandType.Edit, buf: vimState.recordedState.count, }).execute(vimState); } else { void new TabCommand({ type: TabCommandType.Next, bang: false, }).execute(vimState); } } } @RegisterAction class PreviousTab extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = [['g', 'T'], ['']]; override runsOnceForEachCountPrefix = true; // Yes, this is different from `{count}gt` override runsOnceForEveryCursor(): boolean { return false; } public override async exec(position: Position, vimState: VimState): Promise { void new TabCommand({ type: TabCommandType.Previous, bang: false, }).execute(vimState); } } ================================================ FILE: src/actions/include-main.ts ================================================ import '../textobject/textobject'; import './base'; import './motion'; import './operator'; // commands import './commands/actions'; import './commands/commandLine'; import './commands/digraphs'; import './commands/documentChange'; import './commands/file'; import './commands/fold'; import './commands/incrementDecrement'; import './commands/insert'; import './commands/join'; import './commands/macro'; import './commands/navigate'; import './commands/put'; import './commands/replace'; import './commands/scroll'; import './commands/search'; import './commands/undo'; import './commands/visual'; import './commands/window'; ================================================ FILE: src/actions/include-plugins.ts ================================================ // plugin import './plugins/camelCaseMotion'; import './plugins/easymotion/easymotion.cmd'; import './plugins/easymotion/registerMoveActions'; import './plugins/replaceWithRegister'; import './plugins/sneak'; import './plugins/surround'; import './plugins/targets/targets'; ================================================ FILE: src/actions/languages/python/motion.ts ================================================ import { Position, TextDocument } from 'vscode'; import { VimState } from '../../../state/vimState'; import { RegisterAction } from '../../base'; import { BaseMovement, failedMovement, IMovement } from '../../baseMotion'; type Type = 'function' | 'class'; type Edge = 'start' | 'end'; type Direction = 'next' | 'prev'; interface LineInfo { line: number; indentation: number; text: string; } interface StructureElement { type: Type; start: Position; end: Position; } // Older browsers don't support lookbehind - in this case, use an inferior regex rather than crashing let supportsLookbehind = true; try { new RegExp('(?<=x)'); } catch { supportsLookbehind = false; } /* * Utility class used to parse the lines in the document and * determine class and function boundaries * * The class keeps track of two positions: the ORIGINAL and the CURRENT * using their relative locations to make decisions. */ export class PythonDocument { _document: TextDocument; structure: StructureElement[]; static readonly reOnlyWhitespace = /\S/; static readonly reLastNonWhiteSpaceCharacter = supportsLookbehind ? new RegExp('(?<=\\S)\\s*$') : /(\S)\s*$/; static readonly reDefOrClass = /^\s*(?:async\s+)?(def|class) /; constructor(document: TextDocument) { this._document = document; const parsed = PythonDocument._parseLines(document); this.structure = PythonDocument._parseStructure(parsed); } /* * Generator of the lines of text in the document */ static *lines(document: TextDocument): Generator { for (let index = 0; index < document.lineCount; index++) { yield document.lineAt(index).text; } } /* * Calculate the indentation of a line of text. * Lines consisting entirely of whitespace of "starting" with a comment are defined * to have an indentation of "undefined". */ static _indentation(line: string): number | undefined { const index: number = line.search(PythonDocument.reOnlyWhitespace); // Return undefined if line is empty, just whitespace, or starts with a comment if (index === -1 || line[index] === '#') { return undefined; } return index; } /* * Parse a line of text to extract LineInfo * Return undefined if the line is empty or starts with a comment */ static _parseLine(index: number, text: string): LineInfo | undefined { const indentation = this._indentation(text); // Since indentation === 0 is a valid result we need to check for undefined explicitly return indentation !== undefined ? { line: index, indentation, text } : undefined; } static _parseLines(document: TextDocument): LineInfo[] { const lines = [...this.lines(document)]; // convert generator to Array const infos = lines.map((text, index) => this._parseLine(index, text)); return infos.filter((x) => x) as LineInfo[]; // filter out empty/comment lines (undefined info) } static _parseStructure(lines: LineInfo[]): StructureElement[] { const last = lines.length; const structure: StructureElement[] = []; for (let index = 0; index < last; index++) { const info = lines[index]; const text = info.text; const match = text.match(PythonDocument.reDefOrClass); if (match) { const type = match[1] === 'def' ? 'function' : 'class'; // Find the end of the current function/class let idx = index + 1; for (; idx < last; idx++) { if (lines[idx].indentation <= info.indentation) { break; } } // Since we stop when we find the first line with a less indentation // we pull back one line to get to the end of the function/class idx--; const endLine = lines[idx]; structure.push({ type, start: new Position(info.line, info.indentation), // Calculate position of last non-white character) end: new Position( endLine.line, endLine.text.search(PythonDocument.reLastNonWhiteSpaceCharacter) - 1, ), }); } } return structure; } /* * Find the position of the specified: * type: function or class * direction: next or prev * edge: start or end * * With this information one can determine all of the required motions */ find(type: Type, direction: Direction, edge: Edge, position: Position): Position | undefined { // Choose the ordering method name based on direction const isDirection = direction === 'next' ? 'isAfter' : 'isBefore'; // Filter function for all elements whose "edge" is in the correct "direction" // relative to the cursor's position, excluding the current function for prev direction const dir = (element: StructureElement) => { const pos = element[edge]; return direction === 'next' ? pos.isAfter(position) : pos.line < position.line; // For prev, we want strictly before }; // Filter out elements from structure based on type and direction const elements = this.structure.filter((elem) => elem.type === type).filter(dir); if (edge === 'end') { // When moving to an 'end' the elements should be started by the end position elements.sort((a, b) => a.end.line - b.end.line); } // Return the first match if any exist if (elements.length) { // If direction === 'next' return the first element // otherwise return the last element const index = direction === 'next' ? 0 : elements.length - 1; const element = elements[index]; const pos = element[edge]; // execAction MUST return a fully realized Position object created using new return pos; } return undefined; } // Use PythonDocument instance to move to specified class boundary static moveClassBoundary( document: TextDocument, position: Position, vimState: VimState, forward: boolean, start: boolean, ): Position | IMovement { const direction = forward ? 'next' : 'prev'; const edge = start ? 'start' : 'end'; return ( new PythonDocument(document).find('class', direction, edge, position) ?? failedMovement(vimState) ); } } // Uses the specified findFunction to execute the motion coupled to the shortcut (keys) abstract class BasePythonMovement extends BaseMovement { abstract type: Type; abstract direction: Direction; abstract edge: Edge; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return ( super.doesActionApply(vimState, keysPressed) && vimState.document.languageId === 'python' ); } public override async execAction( position: Position, vimState: VimState, ): Promise { const document = vimState.document; return ( new PythonDocument(document).find(this.type, this.direction, this.edge, position) ?? failedMovement(vimState) ); } } @RegisterAction class MovePythonNextFunctionStart extends BasePythonMovement { keys = [']', 'm']; type: Type = 'function'; direction: Direction = 'next'; edge: Edge = 'start'; } @RegisterAction class MovePythonPrevFunctionStart extends BasePythonMovement { keys = ['[', 'm']; type: Type = 'function'; direction: Direction = 'prev'; edge: Edge = 'start'; } @RegisterAction class MovePythonNextFunctionEnd extends BasePythonMovement { keys = [']', 'M']; type: Type = 'function'; direction: Direction = 'next'; edge: Edge = 'end'; } @RegisterAction class MovePythonPrevFunctionEnd extends BasePythonMovement { keys = ['[', 'M']; type: Type = 'function'; direction: Direction = 'prev'; edge: Edge = 'end'; } ================================================ FILE: src/actions/motion.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { sorted } from '../common/motion/position'; import { Notation } from '../configuration/notation'; import { VimError } from '../error'; import { globalState } from '../state/globalState'; import { StatusBar } from '../statusBar'; import { getCurrentParagraphBeginning, getCurrentParagraphEnd } from '../textobject/paragraph'; import { WordType } from '../textobject/word'; import { reportSearch } from '../util/statusBarTextUtils'; import { clamp, isHighSurrogate, isLowSurrogate } from '../util/util'; import { SearchDirection } from '../vimscript/pattern'; import { PairMatcher } from './../common/matching/matcher'; import { QuoteMatcher } from './../common/matching/quoteMatcher'; import { TagMatcher } from './../common/matching/tagMatcher'; import { configuration } from './../configuration/configuration'; import { isVisualMode, Mode } from './../mode/mode'; import { RegisterMode } from './../register/register'; import { VimState } from './../state/vimState'; import { CursorMoveByUnit, CursorMovePosition, TextEditor } from './../textEditor'; import { RegisterAction } from './base'; import { BaseMovement, failedMovement, IMovement, isIMovement, SelectionType } from './baseMotion'; import { PythonDocument } from './languages/python/motion'; import { ChangeOperator, DeleteOperator, YankOperator } from './operator'; import { SneakBackward, SneakForward } from './plugins/sneak'; import { SmartQuoteMatcher, WhichQuotes } from './plugins/targets/smartQuotesMatcher'; import { useSmartQuotes } from './plugins/targets/targetsConfig'; import { shouldWrapKey } from './wrapping'; function adjustForDesiredColumn(args: { position: Position; desiredColumn: number; multicursorIndex: number | undefined; }): Position { const { position, desiredColumn, multicursorIndex } = args; // HACK: until we put `desiredColumn` on `Cursor`, only the first cursor will respect it (except after `$`) if (multicursorIndex && multicursorIndex > 0 && desiredColumn !== Number.POSITIVE_INFINITY) { return position; } return position.with({ character: desiredColumn }); } /** * A movement is something like 'h', 'k', 'w', 'b', 'gg', etc. */ export abstract class ExpandingSelection extends BaseMovement { protected override selectionType = SelectionType.Expanding; protected override adjustPosition(position: Position, result: IMovement, lastIteration: boolean) { if (!lastIteration) { position = result.stop; } return position; } } abstract class MoveByScreenLine extends BaseMovement { override modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; abstract movementType: CursorMovePosition; by?: CursorMoveByUnit; value: number = 1; public override async execAction(position: Position, vimState: VimState) { return this.execActionWithCount(position, vimState, 1); } public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { const multicursorIndex = this.multicursorIndex ?? 0; if (multicursorIndex === 0) { if (vimState.currentMode === Mode.Visual) { // If we change the `vimState.editor.selections` directly with the forEach // for some reason vscode doesn't update them. But doing it this way does // update vscode's selections. vimState.editor.selections = vimState.editor.selections.map((s, i) => { if (s.active.isAfter(s.anchor)) { // The selection is on the right side of the cursor, while our representation // considers the cursor to be the left edge, so we need to move the selection // to the right place before executing the 'cursorMove' command. const active = s.active.getLeftThroughLineBreaks(); return new vscode.Selection(s.anchor, active); } else { return s; } }); } // When we have multicursors and run a 'cursorMove' command, vscode applies that command // to all cursors at the same time. So we should only run it once. await vscode.commands.executeCommand('cursorMove', { to: this.movementType, select: vimState.currentMode !== Mode.Normal, // select: ![Mode.Normal, Mode.Insert].includes(vimState.currentMode), by: this.by, value: this.value * count, }); } /** * HACK: * The `cursorMove` command is handling the selection for us. * So we are not following our design principal (do no real movement inside an action) here */ if (!vimState.editor.selections[multicursorIndex]) { // VS Code selections no longer have the same amount of cursors as we do. This means that // two or more selections combined into one. In this case we return these cursors as they // were with the removed flag so that they can be removed. // TODO: does this work in VisualBlock (where cursors are not 1 to 1 with selections)? return { start: vimState.cursorStartPosition, stop: vimState.cursorStopPosition, removed: true, }; } if (vimState.currentMode === Mode.Normal) { return vimState.editor.selections[multicursorIndex].active; } else { let start = vimState.editor.selections[multicursorIndex].anchor; const stop = vimState.editor.selections[multicursorIndex].active; // If we are moving up we need to keep getting the left of anchor/start because vscode is // to the right of the character in order to include it but our positions are always on the // left side of the character. // Also when we switch from being before anchor to being after anchor we need to move // the anchor/start to the left as well in order to include the character. if ( (start.isAfter(stop) && vimState.cursorStartPosition.isAfter(vimState.cursorStopPosition)) || (vimState.cursorStartPosition.isAfter(vimState.cursorStopPosition) && start.isBeforeOrEqual(stop)) ) { start = start.getLeft(); } return { start, stop }; } } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { const multicursorIndex = this.multicursorIndex ?? 0; if (multicursorIndex === 0) { // When we have multicursors and run a 'cursorMove' command, vscode applies that command // to all cursors at the same time. So we should only run it once. await vscode.commands.executeCommand('cursorMove', { to: this.movementType, select: true, by: this.by, value: this.value, }); } if (!vimState.editor.selections[multicursorIndex]) { // Vscode selections no longer have the same amount of cursors as we do. This means that // two or more selections combined into one. In this case we return these cursors as they // were with the removed flag so that they can be removed. return { start: vimState.cursorStartPosition, stop: vimState.cursorStopPosition, removed: true, }; } return { start: vimState.editor.selections[multicursorIndex].start, stop: vimState.editor.selections[multicursorIndex].end, }; } } class MoveUpByScreenLine extends MoveByScreenLine { keys = []; movementType: CursorMovePosition = 'up'; override by: CursorMoveByUnit = 'wrappedLine'; override value = 1; constructor(multicursorIndex: number) { super(); this.multicursorIndex = multicursorIndex; } } class MoveDownByScreenLine extends MoveByScreenLine { keys = []; movementType: CursorMovePosition = 'down'; override by: CursorMoveByUnit = 'wrappedLine'; override value = 1; constructor(multicursorIndex: number) { super(); this.multicursorIndex = multicursorIndex; } } abstract class MoveByScreenLineMaintainDesiredColumn extends MoveByScreenLine { override preservesDesiredColumn = true; public override async execAction( position: Position, vimState: VimState, ): Promise { const prevDesiredColumn = vimState.desiredColumn; const prevLine = vimState.editor.selection.active.line; if (vimState.currentMode !== Mode.Normal) { /** * As VIM and VSCode handle the end of selection index a little * differently we need to sometimes move the cursor at the end * of the selection back by a character. */ const start = vimState.editor.selection.start; if ( (this.movementType === 'down' && position.line > start.line) || (this.movementType === 'up' && position.line < prevLine) ) { await vscode.commands.executeCommand('cursorMove', { to: 'left', select: true, by: 'character', value: 1, }); } } await vscode.commands.executeCommand('cursorMove', { to: this.movementType, select: vimState.currentMode !== Mode.Normal, by: this.by, value: this.value, }); if (vimState.currentMode === Mode.Normal) { let returnedPos = vimState.editor.selection.active; if (prevLine !== returnedPos.line) { returnedPos = returnedPos.withColumn(prevDesiredColumn); } return returnedPos; } else { /** * cursorMove command is handling the selection for us. * So we are not following our design principal (do no real movement inside an action) here. */ let start = vimState.editor.selection.start; let stop = vimState.editor.selection.end; const curPos = vimState.editor.selection.active; // We want to swap the cursor start stop positions based on which direction we are moving, up or down if (start.isEqual(curPos) && !start.isEqual(stop)) { [start, stop] = [stop, start]; if (prevLine !== start.line) { start = start.getLeft(); } } if (position.line !== stop.line) { stop = stop.withColumn(prevDesiredColumn); } return { start, stop }; } } } class MoveDownFoldFix extends MoveByScreenLineMaintainDesiredColumn { keys = []; movementType: CursorMovePosition = 'down'; override by: CursorMoveByUnit = 'line'; override value = 1; public override async execAction(position: Position, vimState: VimState): Promise { if (position.line >= vimState.document.lineCount - 1) { return position; } let t: Position | IMovement = position; let prev: Position = position; const moveDownByScreenLine = new MoveDownByScreenLine(this.multicursorIndex ?? 0); while (true) { t = await moveDownByScreenLine.execAction(t, vimState); t = t instanceof Position ? t : t.stop; const lineChanged = prev.line !== t.line; // wrappedLine movement goes to eol character only when at the last line // thus a column change on wrappedLine movement represents a visual last line const colChanged = prev.character !== t.character; if (lineChanged || !colChanged) { break; } prev = t; } return adjustForDesiredColumn({ position: t, desiredColumn: vimState.desiredColumn, multicursorIndex: this.multicursorIndex, }); } } @RegisterAction export class MoveDown extends BaseMovement { keys = [['j'], [''], [''], ['']]; override preservesDesiredColumn = true; public override async execAction(position: Position, vimState: VimState): Promise { if ( vimState.currentMode === Mode.Insert && this.keysPressed[0] === '' && vimState.editor.document.uri.scheme === 'vscode-interactive-input' && position.line === vimState.document.lineCount - 1 && vimState.editor.selection.isEmpty ) { // navigate history in interactive window await vscode.commands.executeCommand('interactive.history.next'); return vimState.editor.selection.active; } if (configuration.foldfix && vimState.currentMode !== Mode.VisualBlock) { const moveDownFoldFix = new MoveDownFoldFix(); moveDownFoldFix.multicursorIndex = this.multicursorIndex; return moveDownFoldFix.execAction(position, vimState); } if (position.line < vimState.document.lineCount - 1) { return adjustForDesiredColumn({ position, desiredColumn: vimState.desiredColumn, multicursorIndex: this.multicursorIndex, }).getDown(); } return position; } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; return position.getDown(); } } @RegisterAction export class MoveUp extends BaseMovement { keys = [['k'], [''], ['']]; override preservesDesiredColumn = true; public override async execAction(position: Position, vimState: VimState): Promise { if ( vimState.currentMode === Mode.Insert && this.keysPressed[0] === '' && vimState.editor.document.uri.scheme === 'vscode-interactive-input' && position.line === 0 && vimState.editor.selection.isEmpty ) { // navigate history in interactive window await vscode.commands.executeCommand('interactive.history.previous'); return vimState.editor.selection.active; } if (configuration.foldfix && vimState.currentMode !== Mode.VisualBlock) { const moveUpFoldFix = new MoveUpFoldFix(); moveUpFoldFix.multicursorIndex = this.multicursorIndex; return moveUpFoldFix.execAction(position, vimState); } if (position.line > 0) { return adjustForDesiredColumn({ position, desiredColumn: vimState.desiredColumn, multicursorIndex: this.multicursorIndex, }).getUp(); } return position; } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; return position.getUp(); } } @RegisterAction class MoveUpFoldFix extends MoveByScreenLineMaintainDesiredColumn { keys = []; movementType: CursorMovePosition = 'up'; override by: CursorMoveByUnit = 'line'; override value = 1; public override async execAction(position: Position, vimState: VimState): Promise { if (position.line === 0) { return position; } let t: Position | IMovement; let prev: Position = position; const moveUpByScreenLine = new MoveUpByScreenLine(this.multicursorIndex ?? 0); while (true) { t = await moveUpByScreenLine.execAction(position, vimState); t = t instanceof Position ? t : t.stop; const lineChanged = prev.line !== t.line; const colChanged = prev.character !== t.character; if (lineChanged || !colChanged) { break; } prev = t; } return adjustForDesiredColumn({ position: t, desiredColumn: vimState.desiredColumn, multicursorIndex: this.multicursorIndex, }); } } @RegisterAction class CommandNextSearchMatch extends BaseMovement { keys = ['n']; override isJump = true; public override async execAction( position: Position, vimState: VimState, ): Promise { const searchState = globalState.searchState; if (!searchState || searchState.searchString === '') { return position; } // Turn one of the highlighting flags back on (turned off with :nohl) globalState.hl = true; if (searchState.getMatchRanges(vimState).length === 0) { StatusBar.displayError(vimState, VimError.PatternNotFound(searchState.searchString)); return failedMovement(vimState); } // we have to handle a special case here: searching for $ or \n, // which we approximate by positionIsEOL. In that case (but only when searching forward) // we need to "offset" by getRight for searching the next match, otherwise we get stuck. const searchForward = searchState.direction === SearchDirection.Forward; const positionIsEOL = position.getRight().isEqual(position.getLineEnd()); const nextMatch = positionIsEOL && searchForward ? searchState.getNextSearchMatchPosition(vimState, position.getRight()) : searchState.getNextSearchMatchPosition(vimState, position); if (!nextMatch) { StatusBar.displayError( vimState, searchState.direction === SearchDirection.Forward ? VimError.SearchHitBottom(searchState.searchString) : VimError.SearchHitTop(searchState.searchString), ); return failedMovement(vimState); } reportSearch(nextMatch.index, searchState.getMatchRanges(vimState).length, vimState); return nextMatch.pos; } } @RegisterAction class CommandPreviousSearchMatch extends BaseMovement { keys = ['N']; override isJump = true; public override async execAction( position: Position, vimState: VimState, ): Promise { const searchState = globalState.searchState; if (!searchState || searchState.searchString === '') { return position; } // Turn one of the highlighting flags back on (turned off with :nohl) globalState.hl = true; if (searchState.getMatchRanges(vimState).length === 0) { StatusBar.displayError(vimState, VimError.PatternNotFound(searchState.searchString)); return failedMovement(vimState); } const searchForward = searchState.direction === SearchDirection.Forward; const positionIsEOL = position.getRight().isEqual(position.getLineEnd()); // see implementation of n, above. const prevMatch = positionIsEOL && !searchForward ? searchState.getNextSearchMatchPosition( vimState, position.getRight(), SearchDirection.Backward, ) : searchState.getNextSearchMatchPosition(vimState, position, SearchDirection.Backward); if (!prevMatch) { StatusBar.displayError( vimState, searchState.direction === SearchDirection.Forward ? VimError.SearchHitTop(searchState.searchString) : VimError.SearchHitBottom(searchState.searchString), ); return failedMovement(vimState); } reportSearch(prevMatch.index, searchState.getMatchRanges(vimState).length, vimState); return prevMatch.pos; } } export abstract class BaseMarkMovement extends BaseMovement { override isJump = true; protected registerMode?: RegisterMode; protected abstract getNewPosition(document: vscode.TextDocument, position: Position): Position; public override async execAction( position: Position, vimState: VimState, ): Promise { const markName = this.keysPressed[1]; const mark = vimState.historyTracker.getMark(markName); if (mark === undefined) { throw VimError.MarkNotSet(); } if ( mark.isUppercaseMark && vimState.recordedState.operator && mark.document !== vimState.document ) { throw VimError.MarkNotSet(); } if (this.registerMode) { vimState.currentRegisterMode = this.registerMode; } const document = mark.isUppercaseMark ? mark.document : vimState.document; const newPosition = this.getNewPosition(document, mark.position); // Navigate to mark in another document if (mark.isUppercaseMark && mark.document !== vimState.document) { const options: vscode.TextDocumentShowOptions = { selection: new vscode.Range(newPosition, newPosition), }; await vscode.window.showTextDocument(mark.document, options); return failedMovement(vimState); // Don't move cursor in current document } // Navigate to mark in the current document return newPosition; } } @RegisterAction export class MarkMovementBOL extends BaseMarkMovement { keys = ["'", '']; protected override registerMode = RegisterMode.LineWise; protected override getNewPosition(document: vscode.TextDocument, position: Position): Position { return TextEditor.getFirstNonWhitespaceCharOnLine(document, position.line); } } @RegisterAction export class MarkMovement extends BaseMarkMovement { keys = ['`', '']; /** * If the exact position exists, returns that position. * If the character position is beyond the end of line, returns the end of line. * Otherwise returns the position at the end of the document. */ protected override getNewPosition(document: vscode.TextDocument, position: Position): Position { const lastLine = document.lineCount - 1; if (position.line > lastLine) { const lastLineLength = document.lineAt(lastLine).text.length; return new Position(lastLine, lastLineLength); } const { text } = document.lineAt(position.line); const character = Math.min(position.character, text.length); return new Position(position.line, character); } } @RegisterAction class NextMark extends BaseMovement { keys = [']', '`']; override isJump = true; public override async execAction(position: Position, vimState: VimState): Promise { const positions = vimState.historyTracker .getLocalMarks() .filter((mark) => mark.position.isAfter(position)) .map((mark) => mark.position) .sort((x, y) => x.compareTo(y)); return positions.length === 0 ? position : positions[0]; } } @RegisterAction class PrevMark extends BaseMovement { keys = ['[', '`']; override isJump = true; public override async execAction(position: Position, vimState: VimState): Promise { const positions = vimState.historyTracker .getLocalMarks() .filter((mark) => mark.position.isBefore(position)) .map((mark) => mark.position) .sort((x, y) => y.compareTo(x)); return positions.length === 0 ? position : positions[0]; } } @RegisterAction class NextMarkLinewise extends BaseMovement { keys = [']', "'"]; override isJump = true; public override async execAction(position: Position, vimState: VimState): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; const lines = vimState.historyTracker .getLocalMarks() .filter((mark) => mark.position.line > position.line) .map((mark) => mark.position.line); const line = lines.length === 0 ? position.line : Math.min(...lines); return new Position(line, 0).getLineBeginRespectingIndent(vimState.document); } } @RegisterAction class PrevMarkLinewise extends BaseMovement { keys = ['[', "'"]; override isJump = true; public override async execAction(position: Position, vimState: VimState): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; const lines = vimState.historyTracker .getLocalMarks() .filter((mark) => mark.position.line < position.line) .map((mark) => mark.position.line); const line = lines.length === 0 ? position.line : Math.max(...lines); return new Position(line, 0).getLineBeginRespectingIndent(vimState.document); } } @RegisterAction export class MoveLeft extends BaseMovement { keys = [['h'], [''], [''], [''], ['']]; public override async execAction(position: Position, vimState: VimState): Promise { const getLeftWhile = (p: Position): Position => { const line = vimState.document.lineAt(p.line).text; if (p.character === 0) { return p; } if ( isLowSurrogate(line.charCodeAt(p.character)) && isHighSurrogate(line.charCodeAt(p.character - 1)) ) { p = p.getLeft(); } const newPosition = p.getLeft(); if ( newPosition.character > 0 && isLowSurrogate(line.charCodeAt(newPosition.character)) && isHighSurrogate(line.charCodeAt(newPosition.character - 1)) ) { return newPosition.getLeft(); } else { return newPosition; } }; return shouldWrapKey(vimState.currentMode, this.keysPressed[0]) ? position.getLeftThroughLineBreaks( [Mode.Insert, Mode.Replace].includes(vimState.currentMode), ) : getLeftWhile(position); } } @RegisterAction export class MoveRight extends BaseMovement { keys = [['l'], [''], [' ']]; public override async execAction(position: Position, vimState: VimState): Promise { const getRightWhile = (p: Position): Position => { const line = vimState.document.lineAt(p.line).text; const newPosition = p.getRight(); if (newPosition.character >= vimState.document.lineAt(newPosition.line).text.length) { return newPosition; } if ( isLowSurrogate(line.charCodeAt(newPosition.character)) && isHighSurrogate(line.charCodeAt(p.character)) ) { return newPosition.getRight(); } else { return newPosition; } }; return shouldWrapKey(vimState.currentMode, this.keysPressed[0]) ? position.getRightThroughLineBreaks( [Mode.Insert, Mode.Replace].includes(vimState.currentMode), ) : getRightWhile(position); } } @RegisterAction class MoveDownNonBlank extends BaseMovement { keys = [['+'], ['\n'], ['']]; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; return TextEditor.getFirstNonWhitespaceCharOnLine( vimState.document, position.getDown(Math.max(count, 1)).line, ); } } @RegisterAction class MoveUpNonBlank extends BaseMovement { keys = ['-']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; return TextEditor.getFirstNonWhitespaceCharOnLine( vimState.document, position.getUp(Math.max(count, 1)).line, ); } } @RegisterAction class MoveDownUnderscore extends BaseMovement { keys = ['_']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; const pos = position.getDown(Math.max(count - 1, 0)); return vimState.recordedState.operator ? pos : TextEditor.getFirstNonWhitespaceCharOnLine(vimState.document, pos.line); } } @RegisterAction class MoveToColumn extends BaseMovement { keys = ['|']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { return new Position(position.line, Math.max(0, count - 1)); } } /** * Returns the Postion of the next instance of `char` on the line * @param char character to look for * @param count number of times to look * @param direction direction to look in */ function findHelper( vimState: VimState, start: Position, char: string, count: number, direction: 'forward' | 'backward', ): Position | undefined { const line = vimState.document.lineAt(start); let index = start.character; while (count > 0 && index >= 0) { if (direction === 'forward') { index = line.text.indexOf(char, index + 1); } else { index = line.text.lastIndexOf(char, index - 1); } count--; } if (index >= 0) { return new Position(start.line, index); } return undefined; } @RegisterAction class MoveFindForward extends BaseMovement { keys = ['f', '']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { if (configuration.sneakReplacesF) { const pos = await new SneakForward( this.keysPressed.concat('\n'), this.isRepeat, ).execActionWithCount(position, vimState, count); if (vimState.recordedState.operator && !isIMovement(pos)) { return pos.getRight(); } return pos; } count ||= 1; const toFind = Notation.ToControlCharacter(this.keysPressed[1]); let result = findHelper(vimState, position, toFind, count, 'forward'); vimState.lastSemicolonRepeatableMovement = new MoveFindForward(this.keysPressed, true); vimState.lastCommaRepeatableMovement = new MoveFindBackward(this.keysPressed, true); if (!result) { return failedMovement(vimState); } if (vimState.recordedState.operator) { result = result.getRight(); } return result; } } @RegisterAction class MoveFindBackward extends BaseMovement { keys = ['F', '']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { if (configuration.sneakReplacesF) { return new SneakBackward(this.keysPressed.concat('\n'), this.isRepeat).execActionWithCount( position, vimState, count, ); } count ||= 1; const toFind = Notation.ToControlCharacter(this.keysPressed[1]); const result = findHelper(vimState, position, toFind, count, 'backward'); vimState.lastSemicolonRepeatableMovement = new MoveFindBackward(this.keysPressed, true); vimState.lastCommaRepeatableMovement = new MoveFindForward(this.keysPressed, true); if (!result) { return failedMovement(vimState); } return result; } } function tilHelper( vimState: VimState, start: Position, char: string, count: number, direction: 'forward' | 'backward', ) { const result = findHelper(vimState, start, char, count, direction); return direction === 'forward' ? result?.getLeft() : result?.getRight(); } @RegisterAction class MoveTilForward extends BaseMovement { keys = ['t', '']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { count ||= 1; const toFind = Notation.ToControlCharacter(this.keysPressed[1]); let result = tilHelper(vimState, position, toFind, count, 'forward'); // For t vim executes ; as 2; and , as 2, if (result && this.isRepeat && position.isEqual(result) && count === 1) { result = tilHelper(vimState, position, toFind, 2, 'forward'); } vimState.lastSemicolonRepeatableMovement = new MoveTilForward(this.keysPressed, true); vimState.lastCommaRepeatableMovement = new MoveTilBackward(this.keysPressed, true); if (!result) { return failedMovement(vimState); } if (vimState.recordedState.operator) { result = result.getRight(); } return result; } } @RegisterAction class MoveTilBackward extends BaseMovement { keys = ['T', '']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { count ||= 1; const toFind = Notation.ToControlCharacter(this.keysPressed[1]); let result = tilHelper(vimState, position, toFind, count, 'backward'); // For T vim executes ; as 2; and , as 2, if (result && this.isRepeat && position.isEqual(result) && count === 1) { result = tilHelper(vimState, position, toFind, 2, 'backward'); } vimState.lastSemicolonRepeatableMovement = new MoveTilBackward(this.keysPressed, true); vimState.lastCommaRepeatableMovement = new MoveTilForward(this.keysPressed, true); if (!result) { return failedMovement(vimState); } return result; } } @RegisterAction class MoveRepeat extends BaseMovement { keys = [';']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { const movement = vimState.lastSemicolonRepeatableMovement; if (movement) { return movement.execActionWithCount(position, vimState, count); } return position; } } @RegisterAction class MoveRepeatReversed extends BaseMovement { keys = [',']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { const semiColonMovement = vimState.lastSemicolonRepeatableMovement; const commaMovement = vimState.lastCommaRepeatableMovement; if (commaMovement) { const result = commaMovement.execActionWithCount(position, vimState, count); // Make sure these don't change. Otherwise, comma's direction flips back // and forth when done repeatedly. This is a bit hacky, so feel free to refactor. vimState.lastSemicolonRepeatableMovement = semiColonMovement; vimState.lastCommaRepeatableMovement = commaMovement; return result; } return position; } } @RegisterAction export class MoveLineEnd extends BaseMovement { keys = [['$'], [''], ['']]; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { return position.getDown(Math.max(count - 1, 0)).getLineEnd(); } } @RegisterAction class MoveLineBegin extends BaseMovement { keys = [['0'], [''], ['']]; public override async execAction(position: Position, vimState: VimState): Promise { return position.getLineBegin(); } public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return super.doesActionApply(vimState, keysPressed) && vimState.recordedState.count === 0; } public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { return super.couldActionApply(vimState, keysPressed) && vimState.recordedState.count === 0; } } @RegisterAction class MoveScreenLineBegin extends MoveByScreenLine { keys = ['g', '0']; movementType: CursorMovePosition = 'wrappedLineStart'; } @RegisterAction class MoveScreenNonBlank extends MoveByScreenLine { keys = ['g', '^']; movementType: CursorMovePosition = 'wrappedLineFirstNonWhitespaceCharacter'; } @RegisterAction class MoveScreenLineEnd extends MoveByScreenLine { keys = ['g', '$']; movementType: CursorMovePosition = 'wrappedLineEnd'; } @RegisterAction class MoveScreenLineEndNonBlank extends MoveByScreenLine { keys = ['g', '_']; movementType: CursorMovePosition = 'wrappedLineLastNonWhitespaceCharacter'; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { count ||= 1; const pos = await super.execActionWithCount(position, vimState, count); // If in visual, return a selection if (pos instanceof Position) { return pos.getDown(count - 1); } else { return { start: pos.start, stop: pos.stop.getDown(count - 1).getLeftThroughLineBreaks() }; } } } @RegisterAction class MoveScreenLineCenter extends MoveByScreenLine { keys = ['g', 'm']; movementType: CursorMovePosition = 'wrappedLineColumnCenter'; } @RegisterAction class MoveUpByDisplayLine extends MoveByScreenLine { override modes = [Mode.Normal, Mode.Visual]; keys = [ ['g', 'k'], ['g', ''], ]; movementType: CursorMovePosition = 'up'; override by: CursorMoveByUnit = 'wrappedLine'; override value = 1; } @RegisterAction class MoveDownByDisplayLine extends MoveByScreenLine { override modes = [Mode.Normal, Mode.Visual]; keys = [ ['g', 'j'], ['g', ''], ]; movementType: CursorMovePosition = 'down'; override by: CursorMoveByUnit = 'wrappedLine'; override value = 1; } // Because we can't support moving by screen line when in visualLine mode, // we change to moving by regular line in visualLine mode. We can't move by // screen line is that our ranges only support a start and stop attribute, // and moving by screen line just snaps us back to the original position. // Check PR #1600 for discussion. @RegisterAction class MoveUpByScreenLineVisualLine extends MoveByScreenLine { override modes = [Mode.VisualLine]; keys = [ ['g', 'k'], ['g', ''], ]; movementType: CursorMovePosition = 'up'; override by: CursorMoveByUnit = 'line'; override value = 1; } @RegisterAction class MoveDownByScreenLineVisualLine extends MoveByScreenLine { override modes = [Mode.VisualLine]; keys = [ ['g', 'j'], ['g', ''], ]; movementType: CursorMovePosition = 'down'; override by: CursorMoveByUnit = 'line'; override value = 1; } @RegisterAction class MoveUpByScreenLineVisualBlock extends BaseMovement { override modes = [Mode.VisualBlock]; keys = [ ['g', 'k'], ['g', ''], ]; override preservesDesiredColumn = true; public override async execAction( position: Position, vimState: VimState, ): Promise { if (position.line > 0) { return adjustForDesiredColumn({ position, desiredColumn: vimState.desiredColumn, multicursorIndex: this.multicursorIndex, }).getUp(); } return position; } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; return position.getUp(); } } @RegisterAction class MoveDownByScreenLineVisualBlock extends BaseMovement { override modes = [Mode.VisualBlock]; keys = [ ['g', 'j'], ['g', ''], ]; override preservesDesiredColumn = true; public override async execAction( position: Position, vimState: VimState, ): Promise { if (position.line < vimState.document.lineCount - 1) { return adjustForDesiredColumn({ position, desiredColumn: vimState.desiredColumn, multicursorIndex: this.multicursorIndex, }).getDown(); } return position; } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; return position.getDown(); } } @RegisterAction class MoveScreenToRight extends MoveByScreenLine { override modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', 'h']; movementType: CursorMovePosition = 'right'; override by: CursorMoveByUnit = 'character'; override value = 1; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } } @RegisterAction class MoveScreenToLeft extends MoveByScreenLine { override modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', 'l']; movementType: CursorMovePosition = 'left'; override by: CursorMoveByUnit = 'character'; override value = 1; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } } @RegisterAction class MoveScreenToRightHalf extends MoveByScreenLine { override modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', 'H']; movementType: CursorMovePosition = 'right'; override by: CursorMoveByUnit = 'halfLine'; override value = 1; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } } @RegisterAction class MoveScreenToLeftHalf extends MoveByScreenLine { override modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; keys = ['z', 'L']; movementType: CursorMovePosition = 'left'; override by: CursorMoveByUnit = 'halfLine'; override value = 1; override isJump = true; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { // Don't run if there's an operator because the Sneak plugin uses z return ( super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined ); } } @RegisterAction class MoveToLineFromViewPortTop extends BaseMovement { keys = ['H']; override isJump = true; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; const topLine = vimState.editor.visibleRanges[0].start.line ?? 0; if (topLine === 0) { return { start: vimState.cursorStartPosition, stop: position.with({ line: topLine }).obeyStartOfLine(vimState.document), }; } const scrolloff = configuration .getConfiguration('editor') .get('cursorSurroundingLines', 0); const line = topLine + scrolloff; return { start: vimState.cursorStartPosition, stop: position.with({ line }).obeyStartOfLine(vimState.document), }; } } @RegisterAction class MoveToLineFromViewPortBottom extends BaseMovement { keys = ['L']; override isJump = true; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; const bottomLine = vimState.editor.visibleRanges[0].end.line ?? 0; const numLines = vimState.editor.document.lineCount; if (bottomLine === numLines - 1) { // NOTE: editor will scroll to accommodate editor.cursorSurroundingLines in this scenario return { start: vimState.cursorStartPosition, stop: position.with({ line: bottomLine }).obeyStartOfLine(vimState.document), }; } const scrolloff = configuration .getConfiguration('editor') .get('cursorSurroundingLines', 0); const line = bottomLine - scrolloff; return { start: vimState.cursorStartPosition, stop: position.with({ line }).obeyStartOfLine(vimState.document), }; } } @RegisterAction class MoveToMiddleLineInViewPort extends MoveByScreenLine { keys = ['M']; movementType: CursorMovePosition = 'viewPortCenter'; override by: CursorMoveByUnit = 'line'; override isJump = true; } @RegisterAction class MoveNonBlank extends BaseMovement { keys = ['^']; public override async execAction(position: Position, vimState: VimState): Promise { return TextEditor.getFirstNonWhitespaceCharOnLine(vimState.document, position.line); } } @RegisterAction class MoveNonBlankFirst extends BaseMovement { keys = [['g', 'g'], ['']]; override isJump = true; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; const line = clamp(count, 1, vimState.document.lineCount) - 1; return { start: vimState.cursorStartPosition, stop: position.with({ line }).obeyStartOfLine(vimState.document), }; } } @RegisterAction class MoveNonBlankLast extends BaseMovement { keys = ['G']; override isJump = true; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; let stop: Position; if (count === 0) { stop = new Position(vimState.document.lineCount - 1, position.character).obeyStartOfLine( vimState.document, ); } else { stop = new Position( Math.min(count, vimState.document.lineCount) - 1, position.character, ).obeyStartOfLine(vimState.document); } return { start: vimState.cursorStartPosition, stop, }; } } @RegisterAction class EndOfSpecificLine extends BaseMovement { keys = ['']; public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { const line = count ? clamp(count - 1, 0, vimState.document.lineCount - 1) : vimState.document.lineCount - 1; return new Position(line, 0).getLineEnd(); } } @RegisterAction export class MoveWordBegin extends BaseMovement { keys = ['w']; public override async execAction( position: Position, vimState: VimState, firstIteration: boolean, lastIteration: boolean, ): Promise { if ( lastIteration && !configuration.changeWordIncludesWhitespace && vimState.recordedState.operator instanceof ChangeOperator ) { const line = vimState.document.lineAt(position); if (line.text.length === 0) { return position; } const char = line.text[position.character]; /* From the Vim manual: Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is on a non-blank. This is because "cw" is interpreted as change-word, and a word does not include the following white space. */ if (' \t'.includes(char)) { return position.nextWordStart(vimState.document); } else { return position.nextWordEnd(vimState.document, { inclusive: true }).getRight(); } } else { return position.nextWordStart(vimState.document); } } public override async execActionForOperator( position: Position, vimState: VimState, firstIteration: boolean, lastIteration: boolean, ): Promise { const result = await this.execAction(position, vimState, firstIteration, lastIteration); /* From the Vim documentation: Another special case: When using the "w" motion in combination with an operator and the last word moved over is at the end of a line, the end of that word becomes the end of the operated text, not the first word in the next line. */ if ( result.line > position.line + 1 || (result.line === position.line + 1 && result.isFirstWordOfLine(vimState.document)) ) { return position.getLineEnd(); } if (result.isLineEnd(vimState.document)) { return new Position(result.line, result.character + 1); } return result; } } @RegisterAction export class MoveFullWordBegin extends BaseMovement { keys = [['W'], ['']]; public override async execAction(position: Position, vimState: VimState): Promise { if ( !configuration.changeWordIncludesWhitespace && vimState.recordedState.operator instanceof ChangeOperator ) { // TODO use execForOperator? Or maybe dont? // See note for w return position .nextWordEnd(vimState.document, { wordType: WordType.Big, inclusive: true }) .getRight(); } else { return position.nextWordStart(vimState.document, { wordType: WordType.Big }); } } } @RegisterAction class MoveWordEnd extends BaseMovement { keys = ['e']; public override async execAction(position: Position, vimState: VimState): Promise { return position.nextWordEnd(vimState.document); } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { const end = position.nextWordEnd(vimState.document); return new Position(end.line, end.character + 1); } } @RegisterAction class MoveFullWordEnd extends BaseMovement { keys = ['E']; public override async execAction(position: Position, vimState: VimState): Promise { return position.nextWordEnd(vimState.document, { wordType: WordType.Big }); } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { return position.nextWordEnd(vimState.document, { wordType: WordType.Big }).getRight(); } } @RegisterAction class MoveLastWordEnd extends BaseMovement { keys = ['g', 'e']; public override async execAction(position: Position, vimState: VimState): Promise { return position.prevWordEnd(vimState.document); } } @RegisterAction class MoveLastFullWordEnd extends BaseMovement { keys = ['g', 'E']; public override async execAction(position: Position, vimState: VimState): Promise { return position.prevWordEnd(vimState.document, { wordType: WordType.Big }); } } @RegisterAction class MoveBeginningWord extends BaseMovement { keys = [['b'], ['']]; public override async execAction(position: Position, vimState: VimState): Promise { return position.prevWordStart(vimState.document); } } @RegisterAction class MoveBeginningFullWord extends BaseMovement { keys = ['B']; public override async execAction(position: Position, vimState: VimState): Promise { return position.prevWordStart(vimState.document, { wordType: WordType.Big }); } } @RegisterAction class MovePreviousSentenceBegin extends BaseMovement { keys = ['(']; override isJump = true; public override async execAction(position: Position, vimState: VimState): Promise { return position.getSentenceBegin({ forward: false }); } } @RegisterAction class GoToOffset extends BaseMovement { keys = ['g', 'o']; override isJump = true; public override async execActionWithCount(position: Position, vimState: VimState, count: number) { vimState.currentRegisterMode = RegisterMode.LineWise; return vimState.document.positionAt((count || 1) - 1); } } @RegisterAction class MoveNextSentenceBegin extends BaseMovement { keys = [')']; override isJump = true; public override async execAction(position: Position, vimState: VimState): Promise { return position.getSentenceBegin({ forward: true }); } } @RegisterAction class MoveParagraphEnd extends BaseMovement { keys = ['}']; override isJump = true; iteration = 0; isFirstLineWise = false; public override async execAction(position: Position, vimState: VimState): Promise { const hasOperator = vimState.recordedState.operator; const paragraphEnd = getCurrentParagraphEnd(position); if (hasOperator) { /** * When paired with an `operator` and a `count` this move will be executed * multiple times which could cause issues like https://github.com/VSCodeVim/Vim/issues/4488 * because subsequent runs will receive back whatever position we return * (See comment in `BaseMotion.execActionWithCount()`). * * We keep track of the iteration we are in, this way we can * return the correct position when on the last iteration, and we don't * accidentally set the `registerMode` incorrectly. */ this.iteration++; const isLineWise = position.isLineBeginning() && vimState.currentMode === Mode.Normal; // TODO: `execAction` receives `firstIteration` and `lastIteration` - don't reinvent the wheel const isLastIteration = vimState.recordedState.count ? vimState.recordedState.count === this.iteration : true; /** * `position` may not represent the position of the cursor from which the command was initiated. * In the case that we will be repeating this move more than once * we want to respect whether the starting position was at the beginning of line or not. */ this.isFirstLineWise = this.iteration === 1 ? isLineWise : this.isFirstLineWise; vimState.currentRegisterMode = this.isFirstLineWise ? RegisterMode.LineWise : undefined; /** * `paragraphEnd` is the first blank line after the last word in the * current paragraph, we want the position just before that one to * accurately emulate Vim's behaviour, unless we are at EOF. */ return isLastIteration && !paragraphEnd.isAtDocumentEnd(vimState.document) ? paragraphEnd.getLeftThroughLineBreaks(true) : paragraphEnd; } return paragraphEnd; } } @RegisterAction class MoveParagraphBegin extends BaseMovement { keys = ['{']; override isJump = true; public override async execAction(position: Position, vimState: VimState): Promise { return getCurrentParagraphBeginning(position); } } abstract class MoveSectionBoundary extends BaseMovement { abstract begin: boolean; abstract forward: boolean; override isJump = true; public override async execAction( position: Position, vimState: VimState, ): Promise { const document = vimState.document; switch (document.languageId) { case 'python': return PythonDocument.moveClassBoundary( document, position, vimState, this.forward, this.begin, ); } const boundary = this.begin ? '{' : '}'; let line = position.line; if ( (this.forward && line === vimState.document.lineCount - 1) || (!this.forward && line === 0) ) { return TextEditor.getFirstNonWhitespaceCharOnLine(vimState.document, line); } line = this.forward ? line + 1 : line - 1; while (!vimState.document.lineAt(line).text.startsWith(boundary)) { if (this.forward) { if (line === vimState.document.lineCount - 1) { break; } line++; } else { if (line === 0) { break; } line--; } } return TextEditor.getFirstNonWhitespaceCharOnLine(vimState.document, line); } } @RegisterAction class MoveNextSectionBegin extends MoveSectionBoundary { keys = [']', ']']; begin = true; forward = true; } @RegisterAction class MoveNextSectionEnd extends MoveSectionBoundary { keys = [']', '[']; begin = false; forward = true; } @RegisterAction class MovePreviousSectionBegin extends MoveSectionBoundary { keys = ['[', '[']; begin = true; forward = false; } @RegisterAction class MovePreviousSectionEnd extends MoveSectionBoundary { keys = ['[', ']']; begin = false; forward = false; } @RegisterAction class MoveToMatchingBracket extends BaseMovement { keys = ['%']; override isJump = true; public override async execAction( position: Position, vimState: VimState, ): Promise { position = position.getLeftIfEOL(); const lineText = vimState.document.lineAt(position).text; const failure = failedMovement(vimState); for (let col = position.character; col < lineText.length; col++) { const currentChar = lineText[col]; const pairing = PairMatcher.getPercentPairing(currentChar); // we need to check pairing, because with text: bla |bla < blub > blub // this for loop will walk over bla and check for a pairing till it finds < if (pairing) { // We found an opening char, now move to the matching closing char return ( PairMatcher.nextPairedChar( new Position(position.line, col), lineText[col], vimState, false, ) || failure ); } } // No matchable character on the line; admit defeat return failure; } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { const result = await this.execAction(position, vimState); if (isIMovement(result)) { if (result.failed) { return result; } else { throw new Error('Did not ever handle this case!'); } } if (position.isAfter(result)) { return { start: result, stop: position.getRight(), }; } else { return result.getRight(); } } public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { // % has a special mode that lets you use it to jump to a percentage of the file // However, some other bracket motions inherit from this so only do this behavior for % explicitly if (Object.getPrototypeOf(this) === MoveToMatchingBracket.prototype) { if (count === 0) { if (vimState.recordedState.operator) { return this.execActionForOperator(position, vimState); } else { return this.execAction(position, vimState); } } // Check to make sure this is a valid percentage if (count < 0 || count > 100) { return failedMovement(vimState); } // See `:help N%` const targetLine = Math.trunc((count * vimState.document.lineCount + 99) / 100) - 1; return position.with({ line: targetLine }).obeyStartOfLine(vimState.document); } else { return super.execActionWithCount(position, vimState, count); } } } export abstract class MoveInsideCharacter extends ExpandingSelection { override modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; protected abstract charToMatch: string; /** True for "around" actions, such as `a(`, and false for "inside" actions, such as `i(` */ protected includeSurrounding = false; override isJump = true; public override async execAction( position: Position, vimState: VimState, firstIteration: boolean, lastIteration: boolean, ): Promise { const closingChar = PairMatcher.pairings[this.charToMatch].match; const [selStart, selEnd] = sorted(vimState.cursorStartPosition, position); // First, search backwards for the opening character of the sequence let openPos = PairMatcher.nextPairedChar(selStart, closingChar, vimState, true); if (openPos === undefined) { // If opening character not found, search forwards let lineNum = selStart.line; while (true) { if (lineNum >= vimState.document.lineCount) { break; } const lineText = vimState.document.lineAt(lineNum).text; const matchIndex = lineText.indexOf(this.charToMatch, selStart.character); if (matchIndex !== -1) { openPos = new Position(lineNum, matchIndex); break; } ++lineNum; } if (openPos === undefined) return failedMovement(vimState); } // Next, search forwards for the closing character which matches let closePos = PairMatcher.nextPairedChar(openPos, this.charToMatch, vimState, true); if (closePos === undefined) { return failedMovement(vimState); } if ( !this.includeSurrounding && (isVisualMode(vimState.currentMode) || !firstIteration) && selStart.getLeftThroughLineBreaks(false).isBeforeOrEqual(openPos) && selEnd.getRightThroughLineBreaks(false).isAfterOrEqual(closePos) ) { // Special case: inner, with all inner content already selected const outerOpenPos = PairMatcher.nextPairedChar(openPos, closingChar, vimState, false); const outerClosePos = outerOpenPos ? PairMatcher.nextPairedChar(outerOpenPos, this.charToMatch, vimState, false) : undefined; if (outerOpenPos && outerClosePos) { openPos = outerOpenPos; closePos = outerClosePos; } } if (this.includeSurrounding) { if (vimState.currentMode !== Mode.Visual) { closePos = new Position(closePos.line, closePos.character + 1); } } else { openPos = openPos.getRightThroughLineBreaks(); // If the closing character is the first on the line, don't swallow it. if (closePos.isInLeadingWhitespace(vimState.document)) { closePos = closePos.getLineBegin(); } if (vimState.currentMode === Mode.Visual) { closePos = closePos.getLeftThroughLineBreaks(); } } if (lastIteration && !isVisualMode(vimState.currentMode) && selStart.isBefore(openPos)) { vimState.recordedState.operatorPositionDiff = openPos.subtract(selStart); } // Adjust for VisualLine mode: exclude the line containing the closing brace // moves the cursor back to just within the brackets, accurately mirroring what // Vim does for Vi{ Vi( Vi[ etc. if ( !this.includeSurrounding && vimState.currentMode === Mode.VisualLine && closePos.line > openPos.line ) { const adjustedLine = closePos.line - 1; if (adjustedLine >= 0) { const lineText = vimState.document.lineAt(adjustedLine).text; closePos = new Position(adjustedLine, lineText.length); } } // TODO: setting the cursor manually like this shouldn't be necessary (probably a Cursor, not Position, should be passed to `exec`) vimState.cursorStartPosition = openPos; return { start: openPos, stop: closePos, }; } } @RegisterAction export class MoveInsideParentheses extends MoveInsideCharacter { keys = [ ['i', '('], ['i', ')'], ['i', 'b'], ]; charToMatch = '('; } @RegisterAction export class MoveAroundParentheses extends MoveInsideCharacter { keys = [ ['a', '('], ['a', ')'], ['a', 'b'], ]; charToMatch = '('; override includeSurrounding = true; } // special treatment for curly braces export abstract class MoveCurlyBrace extends MoveInsideCharacter { override modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; protected charToMatch: string = '{'; public override async execAction( position: Position, vimState: VimState, firstIteration: boolean, lastIteration: boolean, ): Promise { // curly braces has a special treatment. In case the cursor is before an opening curly brace, // and there are no characters before the opening curly brace in the same line, it should jump // to the next opening curly brace, even if it already inside a pair of curly braces. const text = vimState.document.lineAt(position).text; const openCurlyBraceIndexFromCursor = text.substring(position.character).indexOf('{'); const startSameAsEnd = vimState.cursorStartPosition.isEqual(position); if ( openCurlyBraceIndexFromCursor !== -1 && text.substring(0, position.character + openCurlyBraceIndexFromCursor).trim().length === 0 && startSameAsEnd ) { const curlyPos = position.with( position.line, position.character + openCurlyBraceIndexFromCursor, ); vimState.cursorStartPosition = vimState.cursorStopPosition = curlyPos; const movement = await super.execAction(curlyPos, vimState, firstIteration, lastIteration); if (movement.failed) { return movement; } const { start, stop } = movement; if (!isVisualMode(vimState.currentMode) && position.isBefore(start)) { vimState.recordedState.operatorPositionDiff = start.subtract(position); } else if (!isVisualMode(vimState.currentMode) && position.isAfter(stop)) { if (position.line === stop.line) { vimState.recordedState.operatorPositionDiff = stop.subtract(position); } else { vimState.recordedState.operatorPositionDiff = start.subtract(position); } } vimState.cursorStartPosition = start; vimState.cursorStopPosition = stop; return movement; } else { return super.execAction(position, vimState, firstIteration, lastIteration); } } } @RegisterAction export class MoveInsideCurlyBrace extends MoveCurlyBrace { keys = [ ['i', '{'], ['i', '}'], ['i', 'B'], ]; } @RegisterAction export class MoveAroundCurlyBrace extends MoveCurlyBrace { keys = [ ['a', '{'], ['a', '}'], ['a', 'B'], ]; override includeSurrounding = true; } @RegisterAction export class MoveInsideCaret extends MoveInsideCharacter { keys = [ ['i', '<'], ['i', '>'], ]; charToMatch = '<'; } @RegisterAction export class MoveAroundCaret extends MoveInsideCharacter { keys = [ ['a', '<'], ['a', '>'], ]; charToMatch = '<'; override includeSurrounding = true; } @RegisterAction export class MoveInsideSquareBracket extends MoveInsideCharacter { keys = [ ['i', '['], ['i', ']'], ]; charToMatch = '['; } @RegisterAction export class MoveAroundSquareBracket extends MoveInsideCharacter { keys = [ ['a', '['], ['a', ']'], ]; charToMatch = '['; override includeSurrounding = true; } // TODO: Shouldn't this be a TextObject? A clearer delineation between motions and objects should be made. export abstract class MoveQuoteMatch extends BaseMovement { override modes = [Mode.Normal, Mode.Visual, Mode.VisualBlock]; protected readonly anyQuote: boolean = false; protected abstract readonly charToMatch: '"' | "'" | '`'; protected includeQuotes = false; override isJump = true; readonly which: WhichQuotes = 'current'; // HACK: surround uses these classes, but does not want trailing whitespace to be included private adjustForTrailingWhitespace: boolean = true; constructor(adjustForTrailingWhitespace: boolean = true) { super(); this.adjustForTrailingWhitespace = adjustForTrailingWhitespace; } public override async execActionWithCount( position: Position, vimState: VimState, count: number, ): Promise { // TODO: this is super janky return (await super.execActionWithCount(position, vimState, 1)) as IMovement; } public override async execAction(position: Position, vimState: VimState): Promise { if ( !this.includeQuotes && (vimState.recordedState.count > 1 || vimState.recordedState.operatorCount > 1) ) { // i" special case: With a count of 2 the quotes are included, but no extra white space as with a"/a'/a`. // (a" does not make use of count) this.includeQuotes = true; this.adjustForTrailingWhitespace = false; } if (useSmartQuotes()) { const quoteMatcher = new SmartQuoteMatcher( this.anyQuote ? 'any' : this.charToMatch, vimState.document, ); const res = quoteMatcher.smartSurroundingQuotes(position, this.which); if (res === undefined) { return failedMovement(vimState); } let { start, stop, lineText } = res; if (!this.includeQuotes) { // Don't include the quotes start = start.translate({ characterDelta: 1 }); stop = stop.translate({ characterDelta: -1 }); } else if ( this.adjustForTrailingWhitespace && configuration.targets.smartQuotes.aIncludesSurroundingSpaces ) { // Include trailing whitespace if there is any... const trailingWhitespace = lineText.substring(stop.character + 1).search(/\S|$/); if (trailingWhitespace > 0) { stop = stop.translate({ characterDelta: trailingWhitespace }); } else { // ...otherwise include leading whitespace start = start.with({ character: lineText.substring(0, start.character).search(/\s*$/) }); } } if (!isVisualMode(vimState.currentMode) && position.isBefore(start)) { vimState.recordedState.operatorPositionDiff = start.subtract(position); } else if (!isVisualMode(vimState.currentMode) && position.isAfter(stop)) { if (position.line === stop.line) { vimState.recordedState.operatorPositionDiff = stop.getRight().subtract(position); } else { vimState.recordedState.operatorPositionDiff = start.subtract(position); } } vimState.cursorStartPosition = start; return { start, stop, }; } else { const text = vimState.document.lineAt(position).text; const quoteMatcher = new QuoteMatcher(this.charToMatch, text); const quoteIndices = quoteMatcher.surroundingQuotes(position.character); if (quoteIndices === undefined) { return failedMovement(vimState); } let [start, end] = quoteIndices; if (!this.includeQuotes) { // Don't include the quotes start++; end--; } else if (this.adjustForTrailingWhitespace) { // Include trailing whitespace if there is any... const trailingWhitespace = text.substring(end + 1).search(/\S|$/); if (trailingWhitespace > 0) { end += trailingWhitespace; } else { // ...otherwise include leading whitespace start = text.substring(0, start).search(/\s*$/); } } const startPos = new Position(position.line, start); const endPos = new Position(position.line, end); if (!isVisualMode(vimState.currentMode) && position.isBefore(startPos)) { vimState.recordedState.operatorPositionDiff = startPos.subtract(position); } return { start: startPos, stop: endPos, }; } } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { const result = await this.execAction(position, vimState); if (isIMovement(result)) { if (result.failed) { vimState.recordedState.hasRunOperator = false; vimState.recordedState.actionsRun = []; } else { result.stop = result.stop.getRight(); } } return result; } } @RegisterAction class MoveInsideSingleQuotes extends MoveQuoteMatch { keys = ['i', "'"]; readonly charToMatch = "'"; override includeQuotes = false; } @RegisterAction export class MoveAroundSingleQuotes extends MoveQuoteMatch { keys = ['a', "'"]; readonly charToMatch = "'"; override includeQuotes = true; } @RegisterAction class MoveInsideDoubleQuotes extends MoveQuoteMatch { keys = ['i', '"']; readonly charToMatch = '"'; override includeQuotes = false; } @RegisterAction export class MoveAroundDoubleQuotes extends MoveQuoteMatch { keys = ['a', '"']; readonly charToMatch = '"'; override includeQuotes = true; } @RegisterAction class MoveInsideBacktick extends MoveQuoteMatch { keys = ['i', '`']; readonly charToMatch = '`'; override includeQuotes = false; } @RegisterAction export class MoveAroundBacktick extends MoveQuoteMatch { keys = ['a', '`']; readonly charToMatch = '`'; override includeQuotes = true; } @RegisterAction class MoveToUnclosedRoundBracketBackward extends MoveToMatchingBracket { override keys = ['[', '(']; public override async execAction( position: Position, vimState: VimState, ): Promise { const charToMatch = ')'; const result = PairMatcher.nextPairedChar(position, charToMatch, vimState, false); if (!result) { return failedMovement(vimState); } return result; } } @RegisterAction class MoveToUnclosedRoundBracketForward extends MoveToMatchingBracket { override keys = [']', ')']; public override async execAction( position: Position, vimState: VimState, ): Promise { const charToMatch = '('; const result = PairMatcher.nextPairedChar(position, charToMatch, vimState, false); if (!result) { return failedMovement(vimState); } if ( vimState.recordedState.operator instanceof ChangeOperator || vimState.recordedState.operator instanceof DeleteOperator || vimState.recordedState.operator instanceof YankOperator ) { return result.getLeftThroughLineBreaks(); } return result; } } @RegisterAction class MoveToUnclosedCurlyBracketBackward extends MoveToMatchingBracket { override keys = ['[', '{']; public override async execAction( position: Position, vimState: VimState, ): Promise { const charToMatch = '}'; const result = PairMatcher.nextPairedChar(position, charToMatch, vimState, false); if (!result) { return failedMovement(vimState); } return result; } } @RegisterAction class MoveToUnclosedCurlyBracketForward extends MoveToMatchingBracket { override keys = [']', '}']; public override async execAction( position: Position, vimState: VimState, ): Promise { const charToMatch = '{'; const result = PairMatcher.nextPairedChar(position, charToMatch, vimState, false); if (!result) { return failedMovement(vimState); } if ( vimState.recordedState.operator instanceof ChangeOperator || vimState.recordedState.operator instanceof DeleteOperator || vimState.recordedState.operator instanceof YankOperator ) { return result.getLeftThroughLineBreaks(); } return result; } } abstract class MoveTagMatch extends ExpandingSelection { override modes = [Mode.Normal, Mode.Visual, Mode.VisualBlock]; protected includeTag = false; override isJump = true; public override async execAction(position: Position, vimState: VimState): Promise { const editorText = vimState.document.getText(); const offset = vimState.document.offsetAt(position); const tagMatcher = new TagMatcher(editorText, offset, vimState); const start = tagMatcher.findOpening(this.includeTag); const end = tagMatcher.findClosing(this.includeTag); if (start === undefined || end === undefined) { return failedMovement(vimState); } const startPosition = start >= 0 ? vimState.document.positionAt(start) : vimState.cursorStartPosition; let endPosition = end >= 0 ? vimState.document.positionAt(end) : position; if (vimState.currentMode === Mode.Visual || vimState.currentMode === Mode.SurroundInputMode) { endPosition = endPosition.getLeftThroughLineBreaks(); } if (position.isAfter(endPosition)) { vimState.recordedState.transformer.moveCursor(endPosition.subtract(position)); } else if (position.isBefore(startPosition)) { vimState.recordedState.transformer.moveCursor(startPosition.subtract(position)); } // if (start === end) { // if (vimState.recordedState.operator instanceof ChangeOperator) { // await vimState.setCurrentMode(ModeName.Insert); // } // return failedMovement(vimState); // } vimState.cursorStartPosition = startPosition; return { start: startPosition, stop: endPosition, }; } } @RegisterAction export class MoveInsideTag extends MoveTagMatch { keys = ['i', 't']; override includeTag = false; } @RegisterAction export class MoveAroundTag extends MoveTagMatch { keys = ['a', 't']; override includeTag = true; } ================================================ FILE: src/actions/operator.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { reportLinesChanged, reportLinesYanked } from '../util/statusBarTextUtils'; import { isHighSurrogate, isLowSurrogate } from '../util/util'; import { ExCommandLine } from './../cmd_line/commandLine'; import { Cursor } from './../common/motion/cursor'; import { PositionDiff, earlierOf, sorted } from './../common/motion/position'; import { configuration } from './../configuration/configuration'; import { DotCommandStatus, Mode, isVisualMode } from './../mode/mode'; import { Register, RegisterMode } from './../register/register'; import { VimState } from './../state/vimState'; import { TextEditor } from './../textEditor'; import { BaseAction, RegisterAction } from './base'; export abstract class BaseOperator extends BaseAction { override actionType = 'operator' as const; constructor(multicursorIndex?: number) { super(); this.multicursorIndex = multicursorIndex; } override createsUndoPoint = true; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { if (this.doesRepeatedOperatorApply(vimState, keysPressed)) { return true; } if (!this.modes.includes(vimState.currentMode)) { return false; } if (!BaseAction.CompareKeypressSequence(this.keys, keysPressed)) { return false; } if (this instanceof BaseOperator && vimState.recordedState.operator) { return false; } return true; } public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { if (!this.modes.includes(vimState.currentMode)) { return false; } if (!BaseAction.CompareKeypressSequence(this.keys.slice(0, keysPressed.length), keysPressed)) { return false; } if (this instanceof BaseOperator && vimState.recordedState.operator) { return false; } return true; } public doesRepeatedOperatorApply(vimState: VimState, keysPressed: string[]) { const nonCountActions = vimState.recordedState.actionsRun.filter((x) => x.name !== 'cmd_num'); const prevAction = nonCountActions.at(-1); return ( keysPressed.length === 1 && prevAction && this.modes.includes(vimState.currentMode) && // The previous action is the same as the one we're testing prevAction.constructor === this.constructor && // The key pressed is the same as the previous action's last key. BaseAction.CompareKeypressSequence(prevAction.keysPressed.slice(-1), keysPressed) ); } /** * Run this operator on a range, returning the new location of the cursor. */ public abstract run(vimState: VimState, start: Position, stop: Position): Promise; public async runRepeat(vimState: VimState, position: Position, count: number): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; await this.run( vimState, position.getLineBegin(), position.getDown(Math.max(0, count - 1)).getLineEnd(), ); } public highlightYankedRanges(vimState: VimState, ranges: vscode.Range[]) { if (!configuration.highlightedyank.enable) { return; } const yankDecoration = vscode.window.createTextEditorDecorationType({ backgroundColor: configuration.highlightedyank.color, color: configuration.highlightedyank.textColor, }); vimState.editor.setDecorations(yankDecoration, ranges); setTimeout(() => yankDecoration.dispose(), configuration.highlightedyank.duration); } } @RegisterAction export class DeleteOperator extends BaseOperator { public override name = 'delete_op'; public keys = ['d']; public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; public async run(vimState: VimState, start: Position, end: Position): Promise { // TODO: this is off by one when character-wise and not including last EOL const numLinesDeleted = Math.abs(start.line - end.line) + 1; if (vimState.currentRegisterMode === RegisterMode.LineWise) { start = start.getLineBegin(); end = end.getLineEnd(); } end = new Position(end.line, end.character + 1); const isOnLastLine = end.line === vimState.document.lineCount - 1; // Vim does this weird thing where it allows you to select and delete // the newline character, which it places 1 past the last character // in the line. Here we interpret a character position 1 past the end // as selecting the newline character. Don't allow this in visual block mode if ( vimState.currentMode !== Mode.VisualBlock && !isOnLastLine && end.character === vimState.document.lineAt(end).text.length + 1 ) { end = new Position(end.line + 1, 0); } const sLine = vimState.document.lineAt(start.line).text; const eLine = vimState.document.lineAt(end.line).text; if ( start.character !== 0 && isLowSurrogate(sLine.charCodeAt(start.character)) && isHighSurrogate(sLine.charCodeAt(start.character - 1)) ) { start = start.getLeft(); } if ( end.character !== 0 && isLowSurrogate(eLine.charCodeAt(end.character)) && isHighSurrogate(eLine.charCodeAt(end.character - 1)) ) { end = end.getRight(); } // Yank the text let text = vimState.document.getText(new vscode.Range(start, end)); if (vimState.currentRegisterMode === RegisterMode.LineWise) { // When deleting linewise, exclude final newline text = text.endsWith('\r\n') ? text.slice(0, -2) : text.endsWith('\n') ? text.slice(0, -1) : text; } Register.put(vimState, text, this.multicursorIndex, true); // When deleting the last line linewise, we need to delete the newline // character BEFORE the range because there isn't one after the range. if ( isOnLastLine && start.line !== 0 && vimState.currentRegisterMode === RegisterMode.LineWise ) { start = start.getUp().getLineEnd(); } let diff: PositionDiff | undefined; if (vimState.currentRegisterMode === RegisterMode.LineWise) { diff = PositionDiff.startOfLine(); } else if (start.character > vimState.document.lineAt(start).text.length) { diff = PositionDiff.offset({ character: -1 }); } vimState.recordedState.transformer.addTransformation({ type: 'deleteRange', range: new vscode.Range(start, end), diff, }); await vimState.setCurrentMode(Mode.Normal); reportLinesChanged(-numLinesDeleted, vimState); } } @RegisterAction class DeleteOperatorVisual extends BaseOperator { public keys = ['D']; public modes = [Mode.Visual, Mode.VisualLine]; public async run(vimState: VimState, start: Position, end: Position): Promise { // ensures linewise deletion when in visual mode // see special case in DeleteOperator.delete() vimState.currentRegisterMode = RegisterMode.LineWise; await new DeleteOperator(this.multicursorIndex).run(vimState, start, end); } } @RegisterAction export class YankOperator extends BaseOperator { public keys = ['y']; public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; override name = 'yank_op'; override createsUndoPoint = false; public async run(vimState: VimState, start: Position, end: Position): Promise { [start, end] = sorted(start, end); let extendedEnd = new Position(end.line, end.character + 1); if (vimState.currentRegisterMode === RegisterMode.LineWise) { start = start.getLineBegin(); extendedEnd = extendedEnd.getLineEnd(); } const sLine = vimState.document.lineAt(start.line).text; const eLine = vimState.document.lineAt(extendedEnd.line).text; if ( start.character !== 0 && isLowSurrogate(sLine.charCodeAt(start.character)) && isHighSurrogate(sLine.charCodeAt(start.character - 1)) ) { start = start.getLeft(); } if ( extendedEnd.character !== 0 && isLowSurrogate(eLine.charCodeAt(extendedEnd.character)) && isHighSurrogate(eLine.charCodeAt(extendedEnd.character - 1)) ) { extendedEnd = extendedEnd.getRight(); } const range = new vscode.Range(start, extendedEnd); let text = vimState.document.getText(range); // If we selected the newline character, add it as well. if ( vimState.currentMode === Mode.Visual && extendedEnd.character === vimState.document.lineAt(extendedEnd).text.length + 1 ) { text = text + '\n'; } this.highlightYankedRanges(vimState, [range]); Register.put(vimState, text, this.multicursorIndex, true); vimState.cursorStopPosition = vimState.currentMode === Mode.Normal && vimState.currentRegisterMode === RegisterMode.LineWise ? start.with({ character: vimState.cursorStopPosition.character }) : start; await vimState.setCurrentMode(Mode.Normal); const numLinesYanked = text.split('\n').length; reportLinesYanked(numLinesYanked, vimState); } } @RegisterAction class FilterOperator extends BaseOperator { public keys = ['!']; public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; public async run(vimState: VimState, start: Position, end: Position): Promise { [start, end] = sorted(start, end); let commandLineText: string; if (vimState.currentMode === Mode.Normal && start.line === end.line) { commandLineText = '.!'; } else if (vimState.currentMode === Mode.Normal && start.line !== end.line) { commandLineText = `.,.+${end.line - start.line}!`; } else { commandLineText = "'<,'>!"; } vimState.cursorStartPosition = start; if (vimState.currentMode === Mode.Normal) { vimState.cursorStopPosition = start; } else { vimState.cursors = [...vimState.cursorsInitialState]; } const previousMode = vimState.currentMode; await vimState.setCurrentMode(Mode.CommandlineInProgress); // TODO: Change or supplement `setCurrentMode` API so this isn't necessary if (vimState.modeData.mode === Mode.CommandlineInProgress) { vimState.modeData.commandLine = new ExCommandLine(commandLineText, previousMode); } } } @RegisterAction class ShiftYankOperatorVisual extends BaseOperator { public keys = ['Y']; public modes = [Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; public async run(vimState: VimState, start: Position, end: Position): Promise { vimState.currentRegisterMode = RegisterMode.LineWise; await new YankOperator(this.multicursorIndex).run(vimState, start, end); } } @RegisterAction class DeleteOperatorXVisual extends BaseOperator { public keys = [['x'], ['']]; public modes = [Mode.Visual, Mode.VisualLine]; public async run(vimState: VimState, start: Position, end: Position): Promise { await new DeleteOperator(this.multicursorIndex).run(vimState, start, end); } } @RegisterAction class ChangeOperatorSVisual extends BaseOperator { public keys = ['s']; public modes = [Mode.Visual, Mode.VisualLine]; // Don't clash with Sneak plugin public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return super.doesActionApply(vimState, keysPressed) && !configuration.sneak; } public async run(vimState: VimState, start: Position, end: Position): Promise { await new ChangeOperator(this.multicursorIndex).run(vimState, start, end); } } @RegisterAction class FormatOperator extends BaseOperator { public keys = ['=']; public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; public async run(vimState: VimState, start: Position, end: Position): Promise { // = operates on complete lines vimState.editor.selection = new vscode.Selection(start.getLineBegin(), end.getLineEnd()); await vscode.commands.executeCommand('editor.action.formatSelection'); let line = vimState.cursorStartPosition.line; if (vimState.cursorStartPosition.isAfter(vimState.cursorStopPosition)) { line = vimState.cursorStopPosition.line; } const newCursorPosition = TextEditor.getFirstNonWhitespaceCharOnLine(vimState.document, line); vimState.cursorStopPosition = newCursorPosition; vimState.cursorStartPosition = newCursorPosition; await vimState.setCurrentMode(Mode.Normal); } } abstract class ChangeCaseOperator extends BaseOperator { public modes = [Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; abstract transformText(text: string): string; public async run(vimState: VimState, startPos: Position, endPos: Position): Promise { if (vimState.currentMode === Mode.VisualBlock) { for (const { start, end } of TextEditor.iterateLinesInBlock(vimState)) { const range = new vscode.Range(start, end); vimState.recordedState.transformer.replace( range, this.transformText(vimState.document.getText(range)), ); } // HACK: currently must do this nonsense to collapse all cursors into one for (let i = 0; i < vimState.editor.selections.length; i++) { vimState.recordedState.transformer.moveCursor( PositionDiff.exactPosition(earlierOf(startPos, endPos)), i, ); } } else { if (vimState.currentRegisterMode === RegisterMode.LineWise) { startPos = startPos.getLineBegin(); endPos = endPos.getLineEnd(); } const range = new vscode.Range(startPos, new Position(endPos.line, endPos.character + 1)); vimState.recordedState.transformer.replace( range, this.transformText(vimState.document.getText(range)), PositionDiff.exactPosition(startPos), ); } await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class UpperCaseOperator extends ChangeCaseOperator { public keys = [['g', 'U'], ['U']]; public transformText(text: string): string { return text.toUpperCase(); } } @RegisterAction class UpperCaseWithMotion extends UpperCaseOperator { public override keys = [['g', 'U']]; public override modes = [Mode.Normal]; } @RegisterAction class LowerCaseOperator extends ChangeCaseOperator { public keys = [['g', 'u'], ['u']]; public transformText(text: string): string { return text.toLowerCase(); } } @RegisterAction class LowerCaseWithMotion extends LowerCaseOperator { public override keys = [['g', 'u']]; public override modes = [Mode.Normal]; } @RegisterAction class ToggleCaseOperator extends ChangeCaseOperator { public keys = [['g', '~'], ['~']]; public transformText(text: string): string { let newText = ''; for (const char of text) { let toggled = char.toLocaleLowerCase(); if (toggled === char) { toggled = char.toLocaleUpperCase(); } newText += toggled; } return newText; } } @RegisterAction class ToggleCaseWithMotion extends ToggleCaseOperator { public override keys = [['g', '~']]; public override modes = [Mode.Normal]; } @RegisterAction class IndentOperator extends BaseOperator { modes = [Mode.Normal]; keys = ['>']; public async run(vimState: VimState, start: Position, end: Position): Promise { vimState.editor.selection = new vscode.Selection(start.getLineBegin(), end.getLineEnd()); await vscode.commands.executeCommand('editor.action.indentLines'); await vimState.setCurrentMode(Mode.Normal); vimState.cursorStopPosition = start.obeyStartOfLine(vimState.document); } } /** * `3>` to indent a line 3 times in visual mode is actually a bit of a special case. * * > is an operator, and generally speaking, you don't run operators multiple times, you run motions multiple times. * e.g. `d3w` runs `w` 3 times, then runs d once. * * Same with literally every other operator motion combination... until `3>`in visual mode * walked into my life. */ @RegisterAction class IndentOperatorVisualAndVisualLine extends BaseOperator { modes = [Mode.Visual, Mode.VisualLine]; keys = ['>']; public async run(vimState: VimState, start: Position, end: Position): Promise { // Repeating this command with dot should apply the indent to the previous selection if ( vimState.dotCommandStatus === DotCommandStatus.Executing && vimState.dotCommandPreviousVisualSelection ) { if (vimState.cursorStartPosition.isAfter(vimState.cursorStopPosition)) { const shiftSelectionByNum = vimState.dotCommandPreviousVisualSelection.end.line - vimState.dotCommandPreviousVisualSelection.start.line; start = vimState.cursorStartPosition; const newEnd = vimState.cursorStartPosition.getDown(shiftSelectionByNum); vimState.editor.selection = new vscode.Selection(start, newEnd); } } for (let i = 0; i < (vimState.recordedState.count || 1); i++) { await vscode.commands.executeCommand('editor.action.indentLines'); } await vimState.setCurrentMode(Mode.Normal); vimState.cursorStopPosition = start.obeyStartOfLine(vimState.document); } } @RegisterAction class IndentOperatorVisualBlock extends BaseOperator { modes = [Mode.VisualBlock]; keys = ['>']; public async run(vimState: VimState, start: Position, end: Position): Promise { /** * Repeating this command with dot should apply the indent to the left edge of the * block formed by extending the cursor start position downward by the number of lines * in the previous visual block selection. */ if ( vimState.dotCommandStatus === DotCommandStatus.Executing && vimState.dotCommandPreviousVisualSelection ) { const shiftSelectionByNum = Math.abs( vimState.dotCommandPreviousVisualSelection.end.line - vimState.dotCommandPreviousVisualSelection.start.line, ); start = vimState.cursorStartPosition; end = vimState.cursorStartPosition.getDown(shiftSelectionByNum); vimState.editor.selection = new vscode.Selection(start, end); } for (let lineIdx = 0; lineIdx < end.line - start.line + 1; lineIdx++) { const tabSize = Number(vimState.editor.options.tabSize); const currentLineEnd = vimState.document.lineAt(start.line + lineIdx).range.end.character; if (currentLineEnd > start.character) { vimState.recordedState.transformer.addTransformation({ type: 'insertText', text: ' '.repeat(tabSize).repeat(vimState.recordedState.count || 1), position: start.getDown(lineIdx), manuallySetCursorPositions: true, }); } } await vimState.setCurrentMode(Mode.Normal); vimState.cursors = [Cursor.atPosition(start)]; } } @RegisterAction class OutdentOperator extends BaseOperator { modes = [Mode.Normal]; keys = ['<']; public async run(vimState: VimState, start: Position, end: Position): Promise { vimState.editor.selection = new vscode.Selection(start, end.getLineEnd()); await vscode.commands.executeCommand('editor.action.outdentLines'); await vimState.setCurrentMode(Mode.Normal); vimState.cursorStopPosition = TextEditor.getFirstNonWhitespaceCharOnLine( vimState.document, start.line, ); } } /** * See comment for IndentOperatorVisualAndVisualLine */ @RegisterAction class OutdentOperatorVisualAndVisualLine extends BaseOperator { modes = [Mode.Visual, Mode.VisualLine]; keys = ['<']; public async run(vimState: VimState, start: Position, end: Position): Promise { // Repeating this command with dot should apply the indent to the previous selection if ( vimState.dotCommandStatus === DotCommandStatus.Executing && vimState.dotCommandPreviousVisualSelection ) { if (vimState.cursorStartPosition.isAfter(vimState.cursorStopPosition)) { const shiftSelectionByNum = vimState.dotCommandPreviousVisualSelection.end.line - vimState.dotCommandPreviousVisualSelection.start.line; start = vimState.cursorStartPosition; const newEnd = vimState.cursorStartPosition.getDown(shiftSelectionByNum); vimState.editor.selection = new vscode.Selection(start, newEnd); } } for (let i = 0; i < (vimState.recordedState.count || 1); i++) { await vscode.commands.executeCommand('editor.action.outdentLines'); } await vimState.setCurrentMode(Mode.Normal); vimState.cursorStopPosition = TextEditor.getFirstNonWhitespaceCharOnLine( vimState.document, start.line, ); } } @RegisterAction class OutdentOperatorVisualBlock extends BaseOperator { modes = [Mode.VisualBlock]; keys = ['<']; public async run(vimState: VimState, start: Position, end: Position): Promise { /** * Repeating this command with dot should apply the outdent to the left edge of the * block formed by extending the cursor start position downward by the number of lines * in the previous visual block selection. */ if ( vimState.dotCommandStatus === DotCommandStatus.Executing && vimState.dotCommandPreviousVisualSelection ) { const shiftSelectionByNum = Math.abs( vimState.dotCommandPreviousVisualSelection.end.line - vimState.dotCommandPreviousVisualSelection.start.line, ); start = vimState.cursorStartPosition; end = vimState.cursorStartPosition.getDown(shiftSelectionByNum); vimState.editor.selection = new vscode.Selection(start, end); } for (let lineIdx = 0; lineIdx < end.line - start.line + 1; lineIdx++) { const tabSize = Number(vimState.editor.options.tabSize); const currentLine = vimState.document.lineAt(start.line + lineIdx); const currentLineEnd = currentLine.range.end.character; if (currentLineEnd > start.character) { const currentLineFromStart = currentLine.text.slice(start.character); const isFirstCharBlank = /\s/.test(currentLineFromStart.charAt(0)); if (isFirstCharBlank) { const currentLinePosition = start.getDown(lineIdx); const distToNonBlankChar = currentLineFromStart.match(/\S/)?.index ?? 0; const outdentDist = Math.min( distToNonBlankChar, tabSize * (vimState.recordedState.count || 1), ); vimState.recordedState.transformer.addTransformation({ type: 'deleteRange', range: new vscode.Range(currentLinePosition, currentLinePosition.getRight(outdentDist)), manuallySetCursorPositions: true, }); } } } await vimState.setCurrentMode(Mode.Normal); vimState.cursors = [Cursor.atPosition(start)]; } } @RegisterAction export class ChangeOperator extends BaseOperator { public keys = ['c']; public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; public async run(vimState: VimState, start: Position, end: Position): Promise { if (vimState.currentRegisterMode === RegisterMode.LineWise) { start = start.getLineBegin(); end = end.getLineEnd(); } else if (vimState.currentMode === Mode.Visual && end.isLineEnd(vimState.document)) { end = end.getRightThroughLineBreaks(); } else { end = end.getRight(); } const deleteRange = new vscode.Range(start, end); Register.put(vimState, vimState.document.getText(deleteRange), this.multicursorIndex, true); if (vimState.currentRegisterMode === RegisterMode.LineWise && configuration.autoindent) { // Linewise is a bit of a special case - we want to preserve the first line's indentation, // then let the language server adjust that indentation if it can. const firstLineIndent = vimState.document.getText( new vscode.Range( deleteRange.start.getLineBegin(), deleteRange.start.getLineBeginRespectingIndent(vimState.document), ), ); vimState.recordedState.transformer.replace( deleteRange, firstLineIndent, PositionDiff.exactPosition(new Position(deleteRange.start.line, firstLineIndent.length)), ); if (vimState.document.languageId !== 'plaintext') { vimState.recordedState.transformer.vscodeCommand('editor.action.reindentselectedlines'); vimState.recordedState.transformer.moveCursor( PositionDiff.endOfLine(), this.multicursorIndex, ); } } else { vimState.recordedState.transformer.delete(deleteRange); } await vimState.setCurrentMode(Mode.Insert); } } @RegisterAction class YankVisualBlockMode extends BaseOperator { public keys = ['y']; public modes = [Mode.VisualBlock]; override createsUndoPoint = false; runsOnceForEveryCursor() { return false; } public async run(vimState: VimState, startPos: Position, endPos: Position): Promise { const ranges: vscode.Range[] = []; const lines: string[] = []; for (const { line, start, end } of TextEditor.iterateLinesInBlock(vimState)) { lines.push(line); ranges.push(new vscode.Range(start, end)); } vimState.currentRegisterMode = RegisterMode.BlockWise; this.highlightYankedRanges(vimState, ranges); Register.put(vimState, lines.join('\n'), this.multicursorIndex, true); vimState.historyTracker.addMark(vimState.document, startPos, '<'); vimState.historyTracker.addMark(vimState.document, endPos, '>'); const numLinesYanked = lines.length; reportLinesYanked(numLinesYanked, vimState); await vimState.setCurrentMode(Mode.Normal); vimState.cursorStopPosition = startPos; } } @RegisterAction class CommentOperator extends BaseOperator { public keys = ['g', 'c']; public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; public async run(vimState: VimState, start: Position, end: Position): Promise { vimState.editor.selection = new vscode.Selection(start.getLineBegin(), end.getLineEnd()); await vscode.commands.executeCommand('editor.action.commentLine'); vimState.cursorStopPosition = new Position(start.line, 0); await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction export class ROT13Operator extends BaseOperator { public keys = ['g', '?']; public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; public async run(vimState: VimState, start: Position, end: Position): Promise { let selections: readonly vscode.Selection[]; if (isVisualMode(vimState.currentMode)) { selections = vimState.editor.selections; } else if (vimState.currentRegisterMode === RegisterMode.LineWise) { selections = [new vscode.Selection(start.getLineBegin(), end.getLineEnd())]; } else { selections = [new vscode.Selection(start, end.getRight())]; } for (const range of selections) { const original = vimState.document.getText(range); vimState.recordedState.transformer.replace(range, ROT13Operator.rot13(original)); } } /** * https://en.wikipedia.org/wiki/ROT13 */ public static rot13(str: string) { return str .split('') .map((char: string) => { let charCode = char.charCodeAt(0); if (char >= 'a' && char <= 'z') { const a = 'a'.charCodeAt(0); charCode = ((charCode - a + 13) % 26) + a; } if (char >= 'A' && char <= 'Z') { const A = 'A'.charCodeAt(0); charCode = ((charCode - A + 13) % 26) + A; } return String.fromCharCode(charCode); }) .join(''); } } @RegisterAction class CommentBlockOperator extends BaseOperator { public keys = ['g', 'C']; public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; public async run(vimState: VimState, start: Position, end: Position): Promise { if (vimState.currentMode === Mode.Normal) { // If we're in normal mode, we need to construct a selection for the // command to operate on. If we're not, we've already got it. const endPosition = end.getRight(); vimState.editor.selection = new vscode.Selection(start, endPosition); } await vscode.commands.executeCommand('editor.action.blockComment'); vimState.cursorStopPosition = start; await vimState.setCurrentMode(Mode.Normal); } } interface CommentTypeSingle { singleLine: true; start: string; } interface CommentTypeMultiLine { singleLine: false; start: string; inner: string; final: string; } type CommentType = CommentTypeSingle | CommentTypeMultiLine; @RegisterAction class ActionVisualReflowParagraph extends BaseOperator { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; keys = ['g', 'q']; public static CommentTypes: CommentType[] = [ { singleLine: false, start: '/**', inner: '*', final: '*/' }, { singleLine: false, start: '/*', inner: '*', final: '*/' }, { singleLine: false, start: '{-', inner: '-', final: '-}' }, { singleLine: true, start: '///' }, { singleLine: true, start: '//!' }, { singleLine: true, start: '//' }, { singleLine: true, start: '--' }, { singleLine: true, start: '#' }, { singleLine: true, start: ';' }, { singleLine: true, start: '*' }, { singleLine: true, start: '%' }, // Needs to come last, since everything starts with the empty string! { singleLine: true, start: '' }, ]; public getIndentation(s: string): string { // Use the indentation of the first non-whitespace line, if any such line is // selected. for (const line of s.split('\n')) { const result = line.match(/^\s+/g); const indent = result ? result[0] : ''; if (indent !== line) { return indent; } } return ''; } public reflowParagraph(s: string): string { const indent = this.getIndentation(s); let indentLevel = 0; for (const char of indent) { indentLevel += char === '\t' ? configuration.tabstop : 1; } const maximumLineLength = configuration.textwidth - indentLevel; // Chunk the lines by commenting style. interface Chunk { commentType: CommentType; content: string; indentLevelAfterComment: number; final: boolean; } const chunksToReflow: Chunk[] = []; for (const line of s.split('\n')) { let lastChunk: Chunk | undefined = chunksToReflow.at(-1); const trimmedLine = line.trimStart(); // See what comment type they are using. let commentType: CommentType | undefined; for (const type of ActionVisualReflowParagraph.CommentTypes) { if (trimmedLine.startsWith(type.start)) { commentType = type; break; } // If they're currently in a multiline comment, see if they continued it. if ( lastChunk && !lastChunk.final && type.start === lastChunk.commentType.start && !type.singleLine ) { if (trimmedLine.startsWith(type.inner)) { commentType = type; break; } if (trimmedLine.endsWith(type.final)) { commentType = type; break; } } } if (!commentType) { break; } // will never happen, just to satisfy typechecker. // Did they start a new comment type? if (!lastChunk || lastChunk.final || commentType.start !== lastChunk.commentType.start) { const chunk = { commentType, content: `${trimmedLine.substr(commentType.start.length).trimStart()}`, indentLevelAfterComment: 0, final: false, }; if (commentType.singleLine) { chunk.indentLevelAfterComment = trimmedLine.substr(commentType.start.length).length - chunk.content.length; } else if (chunk.content.endsWith(commentType.final)) { // Multiline comment started and ended on one line chunk.content = chunk.content .substr(0, chunk.content.length - commentType.final.length) .trim(); chunk.final = true; } chunksToReflow.push(chunk); continue; } // Parse out commenting style, gather words. lastChunk = chunksToReflow.at(-1)!; if (lastChunk.commentType.singleLine) { // is it a continuation of a comment like "//" lastChunk.content += `\n${trimmedLine .substr(lastChunk.commentType.start.length) .trimStart()}`; } else if (!lastChunk.final) { // are we in the middle of a multiline comment like "/*" if (trimmedLine.endsWith(lastChunk.commentType.final)) { lastChunk.final = true; const prefix = trimmedLine.startsWith(lastChunk.commentType.inner) ? lastChunk.commentType.inner.length : 0; lastChunk.content += `\n${trimmedLine .substr(prefix, trimmedLine.length - lastChunk.commentType.final.length - prefix) .trim()}`; } else if (trimmedLine.startsWith(lastChunk.commentType.inner)) { lastChunk.content += `\n${trimmedLine .substr(lastChunk.commentType.inner.length) .trimStart()}`; } else if (trimmedLine.startsWith(lastChunk.commentType.start)) { lastChunk.content += `\n${trimmedLine .substr(lastChunk.commentType.start.length) .trimStart()}`; } } } // Reflow each chunk. const result: string[] = []; for (const { commentType, content, indentLevelAfterComment } of chunksToReflow) { const indentAfterComment = Array(indentLevelAfterComment + 1).join(' '); const commentLength = commentType.start.length + indentAfterComment.length; // Start with a single empty content line. const lines: string[] = [``]; for (let line of content.split('\n')) { // Preserve blank lines in output. if (line.trim() === '') { // Replace empty content line with blank line. if (lines.at(-1) === '') { lines.pop(); } lines.push(line); // Add new empty content line for remaining content. lines.push(``); continue; } // Repeatedly partition line into pieces that fit in maximumLineLength while (line) { const lastLine = lines.at(-1)!; // Determine the separator that we'd need to add to the last line // in order to join onto this line. let separator; if (!lastLine) { separator = ''; } else if ( configuration.joinspaces && (lastLine.endsWith('.') || lastLine.endsWith('?') || lastLine.endsWith('!')) ) { separator = ' '; } else if (lastLine.endsWith(' ')) { if ( configuration.joinspaces && (lastLine.endsWith('. ') || lastLine.endsWith('? ') || lastLine.endsWith('! ')) ) { separator = ' '; } else { separator = ''; } } else { separator = ' '; } // Consider appending separator and part of line to last line const remaining = maximumLineLength - separator.length - lastLine.length - commentLength; const trimmedLine = line.trimStart(); if (trimmedLine.length <= remaining) { // Entire line fits on last line lines[lines.length - 1] += `${separator}${trimmedLine}`; break; } else { // Find largest portion of line that fits on last line, // by searching backward for a whitespace character (space or tab). let breakpoint = Math.max( trimmedLine.lastIndexOf(' ', remaining), trimmedLine.lastIndexOf('\t', remaining), ); if (breakpoint < 0) { // Next word is too long to fit on the current line. if (lastLine) { // Start a new line and try again next round. lines.push(''); continue; } else { // Next word is too long to fit on a line by itself. // Break it at the next word boundary, if there is one. breakpoint = trimmedLine.search(/[ \t]/); if (breakpoint < 0) breakpoint = line.length; } } // Split the line into the part that fits on the last line // and the remainder. Start a new line for the remainder. lines[lines.length - 1] += `${separator}${trimmedLine.slice(0, breakpoint).trimEnd()}`; line = line.slice(breakpoint + 1); lines.push(''); } } } // Drop final empty content line. if (lines.at(-1) === '') { lines.pop(); } for (let i = 0; i < lines.length; i++) { if (commentType.singleLine) { lines[i] = `${indent}${commentType.start}${indentAfterComment}${lines[i]}`; } else { if (i === 0) { if (lines[i] === '') { lines[i] = `${indent}${commentType.start}`; } else { lines[i] = `${indent}${commentType.start} ${lines[i]}`; } if (i === lines.length - 1) { lines[i] += ` ${commentType.final}`; } } else if (i === lines.length - 1) { if (lines[i] === '') { lines[i] = `${indent} ${commentType.final}`; } else { lines[i] = `${indent} ${commentType.inner} ${lines[i]} ${commentType.final}`; } } else { if (lines[i] === '') { lines[i] = `${indent} ${commentType.inner}`; } else { lines[i] = `${indent} ${commentType.inner} ${lines[i]}`; } } } } result.push(...lines); } return result.join('\n'); } public async run(vimState: VimState, start: Position, end: Position): Promise { [start, end] = sorted(start, end); start = start.getLineBegin(); end = end.getLineEnd(); let textToReflow = vimState.document.getText(new vscode.Range(start, end)); textToReflow = this.reflowParagraph(textToReflow); vimState.recordedState.transformer.replace( new vscode.Range(start, end), textToReflow, // Move cursor to front of line to realign the view PositionDiff.exactCharacter({ character: 0 }), ); await vimState.setCurrentMode(Mode.Normal); } } ================================================ FILE: src/actions/plugins/camelCaseMotion.ts ================================================ import { Position } from 'vscode'; import { configuration } from '../../configuration/configuration'; import { Mode } from '../../mode/mode'; import { VimState } from '../../state/vimState'; import { TextObject } from '../../textobject/textobject'; import { WordType } from '../../textobject/word'; import { RegisterAction } from '../base'; import { BaseMovement, IMovement } from '../baseMotion'; import { ChangeOperator } from '../operator'; abstract class CamelCaseBaseMovement extends BaseMovement { public override doesActionApply(vimState: VimState, keysPressed: string[]) { return configuration.camelCaseMotion.enable && super.doesActionApply(vimState, keysPressed); } public override couldActionApply(vimState: VimState, keysPressed: string[]) { return configuration.camelCaseMotion.enable && super.couldActionApply(vimState, keysPressed); } } abstract class CamelCaseTextObjectMovement extends TextObject { public override doesActionApply(vimState: VimState, keysPressed: string[]) { return configuration.camelCaseMotion.enable && super.doesActionApply(vimState, keysPressed); } public override couldActionApply(vimState: VimState, keysPressed: string[]) { return configuration.camelCaseMotion.enable && super.couldActionApply(vimState, keysPressed); } } // based off of `MoveWordBegin` @RegisterAction class MoveCamelCaseWordBegin extends CamelCaseBaseMovement { keys = ['', 'w']; public override async execAction(position: Position, vimState: VimState): Promise { if ( !configuration.changeWordIncludesWhitespace && vimState.recordedState.operator instanceof ChangeOperator ) { // TODO use execForOperator? Or maybe dont? // See note for w return position.nextWordEnd(vimState.document, { wordType: WordType.CamelCase }).getRight(); } else { return position.nextWordStart(vimState.document, { wordType: WordType.CamelCase }); } } } // based off of `MoveWordEnd` @RegisterAction class MoveCamelCaseWordEnd extends CamelCaseBaseMovement { keys = ['', 'e']; public override async execAction(position: Position, vimState: VimState): Promise { return position.nextWordEnd(vimState.document, { wordType: WordType.CamelCase }); } public override async execActionForOperator( position: Position, vimState: VimState, ): Promise { const end = position.nextWordEnd(vimState.document, { wordType: WordType.CamelCase }); return new Position(end.line, end.character + 1); } } // based off of `MoveBeginningWord` @RegisterAction class MoveBeginningCamelCaseWord extends CamelCaseBaseMovement { keys = ['', 'b']; public override async execAction(position: Position, vimState: VimState): Promise { return position.prevWordStart(vimState.document, { wordType: WordType.CamelCase }); } } // based off of `SelectInnerWord` @RegisterAction class SelectInnerCamelCaseWord extends CamelCaseTextObjectMovement { override modes = [Mode.Normal, Mode.Visual]; keys = ['i', '', 'w']; public async execAction(position: Position, vimState: VimState): Promise { let start: Position; let stop: Position; const currentChar = vimState.document.lineAt(position).text[position.character]; if (/\s/.test(currentChar)) { start = position.prevWordEnd(vimState.document, { wordType: WordType.CamelCase }).getRight(); stop = position .nextWordStart(vimState.document, { wordType: WordType.CamelCase }) .getLeftThroughLineBreaks(); } else { start = position.prevWordStart(vimState.document, { wordType: WordType.CamelCase, inclusive: true, }); stop = position.nextWordEnd(vimState.document, { wordType: WordType.CamelCase, inclusive: true, }); } if ( vimState.currentMode === Mode.Visual && !vimState.cursorStopPosition.isEqual(vimState.cursorStartPosition) ) { start = vimState.cursorStartPosition; if (vimState.cursorStopPosition.isBefore(vimState.cursorStartPosition)) { // If current cursor postion is before cursor start position, we are selecting words in reverser order. if (/\s/.test(currentChar)) { stop = position .prevWordEnd(vimState.document, { wordType: WordType.CamelCase }) .getRight(); } else { stop = position.prevWordStart(vimState.document, { wordType: WordType.CamelCase, inclusive: true, }); } } } return { start, stop, }; } } ================================================ FILE: src/actions/plugins/easymotion/easymotion.cmd.ts ================================================ import { Position } from 'vscode'; import { globalState } from '../../../state/globalState'; import { VimState } from '../../../state/vimState'; import { TextEditor } from '../../../textEditor'; import { configuration } from './../../../configuration/configuration'; import { Mode } from './../../../mode/mode'; import { BaseCommand, RegisterAction } from './../../base'; import { EasyMotion } from './easymotion'; import { MarkerGenerator } from './markerGenerator'; import { EasyMotionCharMoveOpions, EasyMotionMoveOptionsBase, EasyMotionSearchAction, EasyMotionWordMoveOpions, Match, SearchOptions, } from './types'; export interface EasymotionTrigger { key: string; leaderCount?: number; } export function buildTriggerKeys(trigger: EasymotionTrigger) { return [ ...Array.from({ length: trigger.leaderCount || 2 }, () => ''), ...trigger.key.split(''), ]; } abstract class BaseEasyMotionCommand extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; private _baseOptions: EasyMotionMoveOptionsBase; public abstract getMatches(position: Position, vimState: VimState): Match[]; constructor(baseOptions: EasyMotionMoveOptionsBase) { super(); this._baseOptions = baseOptions; } public abstract resolveMatchPosition(match: Match): Position; public processMarkers(matches: Match[], cursorPosition: Position, vimState: VimState) { // Clear existing markers, just in case vimState.easyMotion.clearMarkers(); let index = 0; const markerGenerator = new MarkerGenerator(matches.length); for (const match of matches) { const matchPosition = this.resolveMatchPosition(match); // Skip if the match position equals to cursor position if (!matchPosition.isEqual(cursorPosition)) { const marker = markerGenerator.generateMarker(index++, matchPosition); if (marker) { vimState.easyMotion.addMarker(marker); } } } } protected searchOptions(position: Position): SearchOptions { switch (this._baseOptions.searchOptions) { case 'min': return { min: position }; case 'max': return { max: position }; default: return {}; } } public override async exec(position: Position, vimState: VimState): Promise { // Only execute the action if the configuration is set if (configuration.easymotion) { // Search all occurences of the character pressed const matches = this.getMatches(position, vimState); // Stop if there are no matches if (matches.length > 0) { vimState.easyMotion = new EasyMotion(); this.processMarkers(matches, position, vimState); if (matches.length === 1) { // Only one found, navigate to it const marker = vimState.easyMotion.markers[0]; // Set cursor position based on marker entered vimState.cursorStopPosition = marker.position; vimState.easyMotion.clearDecorations(vimState.editor); } else { // Store mode to return to after performing easy motion vimState.easyMotion.previousMode = vimState.currentMode; // Enter the EasyMotion mode and await further keys await vimState.setCurrentMode(Mode.EasyMotionMode); } } } } } function getMatchesForString( position: Position, vimState: VimState, searchString: string, options?: SearchOptions, ): Match[] { switch (searchString) { case '': return []; case ' ': // Searching for space should only find the first space return vimState.easyMotion.sortedSearch( vimState.document, position, new RegExp(' {1,}', 'g'), options, ); default: // Search all occurences of the character pressed // If the input is not a letter, treating it as regex can cause issues if (!/[a-zA-Z]/.test(searchString)) { return vimState.easyMotion.sortedSearch(vimState.document, position, searchString, options); } const ignorecase = configuration.ignorecase && !(configuration.smartcase && /[A-Z]/.test(searchString)); const regexFlags = ignorecase ? 'gi' : 'g'; return vimState.easyMotion.sortedSearch( vimState.document, position, new RegExp(searchString, regexFlags), options, ); } } export class SearchByCharCommand extends BaseEasyMotionCommand implements EasyMotionSearchAction { keys = []; public searchString: string = ''; private _options: EasyMotionCharMoveOpions; get searchCharCount() { return this._options.charCount; } constructor(options: EasyMotionCharMoveOpions) { super(options); this._options = options; } public getMatches(position: Position, vimState: VimState): Match[] { return getMatchesForString(position, vimState, this.searchString, this.searchOptions(position)); } public shouldFire() { const charCount = this._options.charCount; return charCount ? this.searchString.length >= charCount : true; } public async fire(position: Position, vimState: VimState): Promise { await this.exec(position, vimState); } public resolveMatchPosition(match: Match): Position { const { line, character } = match.position; switch (this._options.labelPosition) { case 'after': return new Position(line, character + this._options.charCount); case 'before': return new Position(line, Math.max(0, character - 1)); default: return match.position; } } } export class SearchByNCharCommand extends BaseEasyMotionCommand implements EasyMotionSearchAction { keys = []; public searchString: string = ''; get searchCharCount() { return -1; } constructor() { super({}); } public resolveMatchPosition(match: Match): Position { return match.position; } public getMatches(position: Position, vimState: VimState): Match[] { return getMatchesForString( position, vimState, this.removeTrailingLineBreak(this.searchString), {}, ); } private removeTrailingLineBreak(s: string) { return s.replace(new RegExp('\n+$', 'g'), ''); } public shouldFire() { // Fire when typed return this.searchString.endsWith('\n'); } public async fire(position: Position, vimState: VimState): Promise { if (this.removeTrailingLineBreak(this.searchString) !== '') { await this.exec(position, vimState); } } } export abstract class EasyMotionCharMoveCommandBase extends BaseCommand { modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; private _action: EasyMotionSearchAction; constructor(action: EasyMotionSearchAction) { super(); this._action = action; } public override async exec(position: Position, vimState: VimState): Promise { // Only execute the action if easymotion is enabled if (configuration.easymotion) { vimState.easyMotion = new EasyMotion(); vimState.easyMotion.previousMode = vimState.currentMode; vimState.easyMotion.searchAction = this._action; globalState.hl = true; await vimState.setCurrentMode(Mode.EasyMotionInputMode); } } } export abstract class EasyMotionWordMoveCommandBase extends BaseEasyMotionCommand { private _options: EasyMotionWordMoveOpions; constructor(options: EasyMotionWordMoveOpions = {}) { super(options); this._options = options; } public getMatches(position: Position, vimState: VimState): Match[] { return this.getMatchesForWord(position, vimState, this.searchOptions(position)); } public resolveMatchPosition(match: Match): Position { const { line, character } = match.position; switch (this._options.labelPosition) { case 'after': return new Position(line, character + match.text.length - 1); default: return match.position; } } private getMatchesForWord( position: Position, vimState: VimState, options?: SearchOptions, ): Match[] { const regex = this._options.jumpToAnywhere ? new RegExp(configuration.easymotionJumpToAnywhereRegex, 'g') : new RegExp('\\w{1,}', 'g'); return vimState.easyMotion.sortedSearch(vimState.document, position, regex, options); } } export abstract class EasyMotionLineMoveCommandBase extends BaseEasyMotionCommand { private _options: EasyMotionMoveOptionsBase; constructor(options: EasyMotionMoveOptionsBase = {}) { super(options); this._options = options; } public resolveMatchPosition(match: Match): Position { return match.position; } public getMatches(position: Position, vimState: VimState): Match[] { return this.getMatchesForLineStart(position, vimState, this.searchOptions(position)); } private getMatchesForLineStart( position: Position, vimState: VimState, options?: SearchOptions, ): Match[] { // Search for the beginning of all non whitespace chars on each line before the cursor const matches = vimState.easyMotion.sortedSearch( vimState.document, position, new RegExp('^.', 'gm'), options, ); for (const match of matches) { match.position = TextEditor.getFirstNonWhitespaceCharOnLine( vimState.document, match.position.line, ); } return matches; } } @RegisterAction class EasyMotionCharInputMode extends BaseCommand { modes = [Mode.EasyMotionInputMode]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { const key = this.keysPressed[0]; const action = vimState.easyMotion.searchAction; action.searchString = key === '' || key === '' ? action.searchString.slice(0, -1) : action.searchString + key; if (action.shouldFire()) { // Skip Easymotion input mode to make sure not to back to it await vimState.setCurrentMode(vimState.easyMotion.previousMode); await action.fire(vimState.cursorStopPosition, vimState); } } } @RegisterAction class CommandEscEasyMotionCharInputMode extends BaseCommand { modes = [Mode.EasyMotionInputMode]; keys = ['']; public override async exec(position: Position, vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Normal); } } @RegisterAction class MoveEasyMotion extends BaseCommand { modes = [Mode.EasyMotionMode]; keys = ['']; override isJump = true; public override async exec(position: Position, vimState: VimState): Promise { const key = this.keysPressed[0]; if (key) { // "nail" refers to the accumulated depth keys const nail = vimState.easyMotion.accumulation + key; vimState.easyMotion.accumulation = nail; // Find markers starting with "nail" const markers = vimState.easyMotion.findMarkers(nail, true); if (markers.length === 1) { // Only one found, navigate to it const marker = markers[0]; vimState.easyMotion.clearDecorations(vimState.editor); // Restore the mode from before easy motion await vimState.setCurrentMode(vimState.easyMotion.previousMode); // Set cursor position based on marker entered vimState.cursorStopPosition = marker.position; } else if (markers.length === 0) { // None found, exit mode vimState.easyMotion.clearDecorations(vimState.editor); await vimState.setCurrentMode(vimState.easyMotion.previousMode); } } } } ================================================ FILE: src/actions/plugins/easymotion/easymotion.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { Mode } from '../../../mode/mode'; import { configuration } from './../../../configuration/configuration'; import { EasyMotionSearchAction, IEasyMotion, Marker, Match, SearchOptions } from './types'; export class EasyMotion implements IEasyMotion { /** * Refers to the accumulated keys for depth navigation */ public accumulation = ''; // TODO: is this actually always set? public searchAction!: EasyMotionSearchAction; /** * Array of all markers and decorations */ public readonly markers: Marker[]; private visibleMarkers: Marker[]; // Array of currently showing markers private decorations: vscode.DecorationOptions[][]; private static fade: vscode.TextEditorDecorationType | null = null; private static getFadeDecorationType(): vscode.TextEditorDecorationType { if (this.fade === null) { this.fade = vscode.window.createTextEditorDecorationType({ color: configuration.easymotionDimColor, }); } return this.fade; } private static readonly hide = vscode.window.createTextEditorDecorationType({ color: 'transparent', }); /** * TODO: For future motions */ private static specialCharactersRegex: RegExp = /[\-\[\]{}()*+?.,\\\^$|#\s]/g; /** * Caches for decorations */ private static decorationTypeCache: vscode.TextEditorDecorationType[] = []; /** * Mode to return to after attempting easymotion */ // TODO: make this optional (in some circumstances it isn't actually set) public previousMode!: Mode; constructor() { this.markers = []; this.visibleMarkers = []; this.decorations = []; } /** * Create and cache decoration types for different marker lengths */ public static getDecorationType( length: number, decorations?: vscode.DecorationRenderOptions, ): vscode.TextEditorDecorationType { const cache = this.decorationTypeCache[length]; if (cache) { return cache; } else { const type = vscode.window.createTextEditorDecorationType(decorations || {}); this.decorationTypeCache[length] = type; return type; } } /** * Clear all decorations */ public clearDecorations(editor: vscode.TextEditor) { for (let i = 1; i <= this.decorations.length; i++) { editor.setDecorations(EasyMotion.getDecorationType(i), []); } editor.setDecorations(EasyMotion.getFadeDecorationType(), []); editor.setDecorations(EasyMotion.hide, []); } /** * Clear all markers */ public clearMarkers() { while (this.markers.length) { this.markers.pop(); } this.visibleMarkers = []; } public addMarker(marker: Marker) { this.markers.push(marker); } /** * Find markers beginning with a string */ public findMarkers(nail: string, onlyVisible: boolean): Marker[] { const markers = onlyVisible ? this.visibleMarkers : this.markers; return markers.filter((marker) => marker.name.startsWith(nail)); } /** * Search and sort using the index of a match compared to the index of position (usually the cursor) */ public sortedSearch( document: vscode.TextDocument, position: Position, search: string | RegExp = '', options: SearchOptions = {}, ): Match[] { const regex = typeof search === 'string' ? new RegExp(search.replace(EasyMotion.specialCharactersRegex, '\\$&'), 'g') : search; const matches: Match[] = []; // Cursor index refers to the index of the marker that is on or to the right of the cursor let cursorIndex = position.character; let prevMatch: Match | undefined; // Calculate the min/max bounds for the search const lineCount = document.lineCount; const lineMin = options.min ? Math.max(options.min.line, 0) : 0; const lineMax = options.max ? Math.min(options.max.line + 1, lineCount) : lineCount; outer: for (let lineIdx = lineMin; lineIdx < lineMax; lineIdx++) { const line = document.lineAt(lineIdx).text; let result = regex.exec(line); while (result) { if (matches.length >= 1000) { break outer; } else { const pos = new Position(lineIdx, result.index); // Check if match is within bounds if ( (options.min && pos.isBefore(options.min)) || (options.max && pos.isAfter(options.max)) || Math.abs(pos.line - position.line) > 100 ) { // Stop searching after 100 lines in both directions result = regex.exec(line); } else { // Update cursor index to the marker on the right side of the cursor if (!prevMatch || prevMatch.position.isBefore(position)) { cursorIndex = matches.length; } // Matches on the cursor position should be ignored if (pos.isEqual(position)) { result = regex.exec(line); } else { prevMatch = new Match(pos, result[0], matches.length); matches.push(prevMatch); result = regex.exec(line); } } } } } // Sort by the index distance from the cursor index matches.sort((a: Match, b: Match): number => { const computeAboluteDiff = (matchIndex: number) => { const absDiff = Math.abs(cursorIndex - matchIndex); // Prioritize the matches on the right side of the cursor index return matchIndex < cursorIndex ? absDiff - 0.5 : absDiff; }; const absDiffA = computeAboluteDiff(a.index); const absDiffB = computeAboluteDiff(b.index); return absDiffA - absDiffB; }); return matches; } private getMarkerColor( customizedValue: string, themeColorId: string, ): string | vscode.ThemeColor { if (customizedValue) { return customizedValue; } else if (!themeColorId.startsWith('#')) { return new vscode.ThemeColor(themeColorId); } else { return themeColorId; } } private getEasymotionMarkerBackgroundColor() { return this.getMarkerColor(configuration.easymotionMarkerBackgroundColor, '#0000'); } private getEasymotionMarkerForegroundColorOneChar() { return this.getMarkerColor(configuration.easymotionMarkerForegroundColorOneChar, '#ff0000'); } private getEasymotionMarkerForegroundColorTwoCharFirst() { return this.getMarkerColor( configuration.easymotionMarkerForegroundColorTwoCharFirst, '#ffb400', ); } private getEasymotionMarkerForegroundColorTwoCharSecond() { return this.getMarkerColor( configuration.easymotionMarkerForegroundColorTwoCharSecond, '#b98300', ); } private getEasymotionDimColor() { return this.getMarkerColor(configuration.easymotionDimColor, '#777777'); } public updateDecorations(editor: vscode.TextEditor) { this.clearDecorations(editor); this.visibleMarkers = []; this.decorations = []; // Set the decorations for all the different marker lengths const dimmingZones: vscode.DecorationOptions[] = []; const dimmingRenderOptions: vscode.ThemableDecorationRenderOptions = { // we update the color here again in case the configuration has changed color: this.getEasymotionDimColor(), }; // Why this instead of `background-color` on the marker? // The easy fix would've been to let the user set the marker background to the same // color as the editor so it would hide the character behind, However this would require // the user to do more work, with this solution we temporarily hide the marked character // so no user specific setting is needed const hiddenChars: vscode.Range[] = []; const markers = this.markers .filter((m) => m.name.startsWith(this.accumulation)) .sort((a, b) => (a.position.isBefore(b.position) ? -1 : 1)); // Ignore markers that do not start with the accumulated depth level for (const marker of markers) { const pos = marker.position; // Get keys after the depth we're at const keystroke = marker.name.substr(this.accumulation.length); if (!this.decorations[keystroke.length]) { this.decorations[keystroke.length] = []; } // #region Hack (remove once backend handles this) /* This hack is here because the backend for easy motion reports two adjacent 2 char markers resulting in a 4 char wide markers, this isn't what happens in original easymotion for instance: for doom - original reports d[m][m2]m where [m] is a marker and [m2] is secondary - here it reports d[m][m][m][m]m The reason this won't work with current impl is that it overflows resulting in one extra hidden character, hence the check below (until backend truely mimics original) if two consecutive 2 char markers, we only use the first char from the current marker and reduce the char substitution by 1. Once backend properly reports adjacent markers all instances of `trim` can be removed */ let trim = 0; const next = markers[markers.indexOf(marker) + 1]; if ( next && next.position.character - pos.character === 1 && next.position.line === pos.line ) { const nextKeystroke = next.name.substr(this.accumulation.length); if (keystroke.length > 1 && nextKeystroke.length > 1) { trim = -1; } } // #endregion // First Char/One Char decoration const firstCharFontColor = keystroke.length > 1 ? this.getEasymotionMarkerForegroundColorTwoCharFirst() : this.getEasymotionMarkerForegroundColorOneChar(); const backgroundColor = this.getEasymotionMarkerBackgroundColor(); const firstCharRange = new vscode.Range(pos.line, pos.character, pos.line, pos.character); const firstCharRenderOptions: vscode.ThemableDecorationInstanceRenderOptions = { before: { contentText: keystroke.substring(0, 1), backgroundColor, color: firstCharFontColor, margin: `0 -1ch 0 0; position: absolute; font-weight: ${configuration.easymotionMarkerFontWeight};`, height: '100%', }, }; this.decorations[keystroke.length].push({ range: firstCharRange, renderOptions: { dark: firstCharRenderOptions, light: firstCharRenderOptions, }, }); // Second Char decoration if (keystroke.length + trim > 1) { const secondCharFontColor = this.getEasymotionMarkerForegroundColorTwoCharSecond(); const secondCharRange = new vscode.Range( pos.line, pos.character + 1, pos.line, pos.character + 1, ); const secondCharRenderOptions: vscode.ThemableDecorationInstanceRenderOptions = { before: { contentText: keystroke.slice(1), backgroundColor, color: secondCharFontColor, margin: `0 -1ch 0 0; position: absolute; font-weight: ${configuration.easymotionMarkerFontWeight};`, height: '100%', }, }; this.decorations[keystroke.length].push({ range: secondCharRange, renderOptions: { dark: secondCharRenderOptions, light: secondCharRenderOptions, }, }); } hiddenChars.push( new vscode.Range( pos.line, pos.character, pos.line, pos.character + keystroke.length + trim, ), ); if (configuration.easymotionDimBackground) { // This excludes markers from the dimming ranges by using them as anchors // each marker adds the range between it and previous marker to the dimming zone // except last marker after which the rest of document is dimmed // // example [m1] text that has multiple [m2] marks // |<------ |<---------------------- ---->| if (dimmingZones.length === 0) { dimmingZones.push({ range: new vscode.Range(0, 0, pos.line, pos.character), renderOptions: dimmingRenderOptions, }); } else { const prevMarker = markers[markers.indexOf(marker) - 1]; const prevKeystroke = prevMarker.name.substring(this.accumulation.length); const prevDimPos = prevMarker.position; const offsetPrevDimPos = prevDimPos.withColumn( prevDimPos.character + prevKeystroke.length, ); // Don't create dimming ranges in between consecutive markers (the 'after' is in the cases // where you have 2 char consecutive markers where the first one only shows the first char. // since we don't take that into account when creating 'offsetPrevDimPos' it will be after // the current marker position which means we are in the middle of two consecutive markers. // See the hack region above.) if (!offsetPrevDimPos.isAfterOrEqual(pos)) { dimmingZones.push({ range: new vscode.Range( offsetPrevDimPos.line, offsetPrevDimPos.character, pos.line, pos.character, ), renderOptions: dimmingRenderOptions, }); } } } this.visibleMarkers.push(marker); } // for the last marker dim till document end if (configuration.easymotionDimBackground && markers.length > 0) { const prevMarker = markers[markers.length - 1]; const prevKeystroke = prevMarker.name.substring(this.accumulation.length); const prevDimPos = dimmingZones[dimmingZones.length - 1].range.end; const offsetPrevDimPos = prevDimPos.withColumn(prevDimPos.character + prevKeystroke.length); // Don't create any more dimming ranges when the last marker is at document end if (!offsetPrevDimPos.isAtDocumentEnd(editor.document)) { dimmingZones.push({ range: new vscode.Range( offsetPrevDimPos, new Position(editor.document.lineCount, Number.MAX_VALUE), ), renderOptions: dimmingRenderOptions, }); } } for (let j = 1; j < this.decorations.length; j++) { if (this.decorations[j]) { editor.setDecorations(EasyMotion.getDecorationType(j), this.decorations[j]); } } editor.setDecorations(EasyMotion.hide, hiddenChars); if (configuration.easymotionDimBackground) { editor.setDecorations(EasyMotion.getFadeDecorationType(), dimmingZones); } } } ================================================ FILE: src/actions/plugins/easymotion/markerGenerator.ts ================================================ import { Position } from 'vscode'; import { configuration } from './../../../configuration/configuration'; import { Marker } from './types'; export class MarkerGenerator { private matchesCount: number; private keyTable: string[]; private prefixKeyTable: string[]; constructor(matchesCount: number) { this.matchesCount = matchesCount; this.keyTable = this.getKeyTable(); this.prefixKeyTable = this.createPrefixKeyTable(); } public generateMarker(index: number, markerPosition: Position): Marker | null { const { keyTable, prefixKeyTable } = this; if (index >= keyTable.length - prefixKeyTable.length) { const remainder = index - (keyTable.length - prefixKeyTable.length); const currentStep = Math.floor(remainder / keyTable.length) + 1; if (currentStep > prefixKeyTable.length) { return null; } else { const prefix = prefixKeyTable[currentStep - 1]; const label = keyTable[remainder % keyTable.length]; return { name: prefix + label, position: markerPosition, }; } } else { return { name: keyTable[index], position: markerPosition, }; } } private createPrefixKeyTable(): string[] { const totalRemainder = Math.max(this.matchesCount - this.keyTable.length, 0); const totalSteps = Math.ceil(totalRemainder / this.keyTable.length); const reversed = this.keyTable.slice().reverse(); const count = Math.min(totalSteps, reversed.length); return reversed.slice(0, count); } /** * The key sequence for marker name generation */ private getKeyTable(): string[] { if (configuration.easymotionKeys) { return configuration.easymotionKeys.split(''); } else { return 'hklyuiopnm,qwertzxcvbasdgjf;'.split(''); } } } ================================================ FILE: src/actions/plugins/easymotion/registerMoveActions.ts ================================================ import { RegisterAction } from './../../base'; import { buildTriggerKeys, EasyMotionCharMoveCommandBase, EasyMotionLineMoveCommandBase, EasyMotionWordMoveCommandBase, SearchByCharCommand, SearchByNCharCommand, } from './easymotion.cmd'; // EasyMotion n-char-move command @RegisterAction class EasyMotionNCharSearchCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: '/' }); constructor() { super(new SearchByNCharCommand()); } } // EasyMotion char-move commands @RegisterAction class EasyMotionTwoCharSearchCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: '2s' }); constructor() { super(new SearchByCharCommand({ charCount: 2 })); } } @RegisterAction class EasyMotionTwoCharFindForwardCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: '2f' }); constructor() { super(new SearchByCharCommand({ charCount: 2, searchOptions: 'min' })); } } @RegisterAction class EasyMotionTwoCharFindBackwardCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: '2F' }); constructor() { super(new SearchByCharCommand({ charCount: 2, searchOptions: 'max' })); } } @RegisterAction class EasyMotionTwoCharTilCharacterForwardCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: '2t' }); constructor() { super(new SearchByCharCommand({ charCount: 2, searchOptions: 'min', labelPosition: 'before' })); } } // easymotion-bd-t2 @RegisterAction class EasyMotionTwoCharTilCharacterBidirectionalCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: 'bd2t', leaderCount: 3 }); constructor() { super(new SearchByCharCommand({ charCount: 2, labelPosition: 'before' })); } } @RegisterAction class EasyMotionTwoCharTilBackwardCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: '2T' }); constructor() { super(new SearchByCharCommand({ charCount: 2, searchOptions: 'max', labelPosition: 'after' })); } } @RegisterAction class EasyMotionSearchCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: 's' }); constructor() { super(new SearchByCharCommand({ charCount: 1 })); } } @RegisterAction class EasyMotionFindForwardCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: 'f' }); constructor() { super(new SearchByCharCommand({ charCount: 1, searchOptions: 'min' })); } } @RegisterAction class EasyMotionFindBackwardCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: 'F' }); constructor() { super(new SearchByCharCommand({ charCount: 1, searchOptions: 'max' })); } } @RegisterAction class EasyMotionTilCharacterForwardCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: 't' }); constructor() { super(new SearchByCharCommand({ charCount: 1, searchOptions: 'min', labelPosition: 'before' })); } } // easymotion-bd-t @RegisterAction class EasyMotionTilCharacterBidirectionalCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: 'bdt', leaderCount: 3 }); constructor() { super(new SearchByCharCommand({ charCount: 1, labelPosition: 'before' })); } } @RegisterAction class EasyMotionTilBackwardCommand extends EasyMotionCharMoveCommandBase { keys = buildTriggerKeys({ key: 'T' }); constructor() { super(new SearchByCharCommand({ charCount: 1, searchOptions: 'max', labelPosition: 'after' })); } } // EasyMotion word-move commands @RegisterAction class EasyMotionStartOfWordForwardsCommand extends EasyMotionWordMoveCommandBase { keys = buildTriggerKeys({ key: 'w' }); constructor() { super({ searchOptions: 'min' }); } } // easymotion-bd-w @RegisterAction class EasyMotionStartOfWordBidirectionalCommand extends EasyMotionWordMoveCommandBase { keys = buildTriggerKeys({ key: 'bdw', leaderCount: 3 }); } @RegisterAction class EasyMotionLineForward extends EasyMotionWordMoveCommandBase { keys = buildTriggerKeys({ key: 'l' }); constructor() { super({ jumpToAnywhere: true, searchOptions: 'min', labelPosition: 'after' }); } } @RegisterAction class EasyMotionLineBackward extends EasyMotionWordMoveCommandBase { keys = buildTriggerKeys({ key: 'h' }); constructor() { super({ jumpToAnywhere: true, searchOptions: 'max', labelPosition: 'after' }); } } // easymotion "JumpToAnywhere" motion @RegisterAction class EasyMotionJumpToAnywhereCommand extends EasyMotionWordMoveCommandBase { keys = buildTriggerKeys({ key: 'j', leaderCount: 3 }); constructor() { super({ jumpToAnywhere: true, labelPosition: 'after' }); } } @RegisterAction class EasyMotionEndOfWordForwardsCommand extends EasyMotionWordMoveCommandBase { keys = buildTriggerKeys({ key: 'e' }); constructor() { super({ searchOptions: 'min', labelPosition: 'after' }); } } // easymotion-bd-e @RegisterAction class EasyMotionEndOfWordBidirectionalCommand extends EasyMotionWordMoveCommandBase { keys = buildTriggerKeys({ key: 'bde', leaderCount: 3 }); constructor() { super({ labelPosition: 'after' }); } } @RegisterAction class EasyMotionBeginningWordCommand extends EasyMotionWordMoveCommandBase { keys = buildTriggerKeys({ key: 'b' }); constructor() { super({ searchOptions: 'max' }); } } @RegisterAction class EasyMotionEndBackwardCommand extends EasyMotionWordMoveCommandBase { keys = buildTriggerKeys({ key: 'ge' }); constructor() { super({ searchOptions: 'max', labelPosition: 'after' }); } } // EasyMotion line-move commands @RegisterAction class EasyMotionStartOfLineForwardsCommand extends EasyMotionLineMoveCommandBase { keys = buildTriggerKeys({ key: 'j' }); constructor() { super({ searchOptions: 'min' }); } } @RegisterAction class EasyMotionStartOfLineBackwordsCommand extends EasyMotionLineMoveCommandBase { keys = buildTriggerKeys({ key: 'k' }); constructor() { super({ searchOptions: 'max' }); } } // easymotion-bd-jk @RegisterAction class EasyMotionStartOfLineBidirectionalCommand extends EasyMotionLineMoveCommandBase { keys = buildTriggerKeys({ key: 'bdjk', leaderCount: 3 }); } ================================================ FILE: src/actions/plugins/easymotion/types.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { Mode } from '../../../mode/mode'; import type { VimState } from '../../../state/vimState'; export type LabelPosition = 'after' | 'before'; export type JumpToAnywhere = true | false; export interface EasyMotionMoveOptionsBase { searchOptions?: 'min' | 'max'; } export interface EasyMotionCharMoveOpions extends EasyMotionMoveOptionsBase { charCount: number; labelPosition?: LabelPosition; } export interface EasyMotionWordMoveOpions extends EasyMotionMoveOptionsBase { labelPosition?: LabelPosition; jumpToAnywhere?: JumpToAnywhere; } export interface Marker { name: string; position: Position; } export class Match { public position: Position; public readonly text: string; public readonly index: number; constructor(position: Position, text: string, index: number) { this.position = position; this.text = text; this.index = index; } public toRange(): vscode.Range { return new vscode.Range(this.position, this.position.translate(0, this.text.length)); } } export interface SearchOptions { /** * The minimum bound of the search */ min?: Position; /** * The maximum bound of the search */ max?: Position; } export interface EasyMotionSearchAction { searchString: string; /** * True if it should go to Easymotion mode */ shouldFire(): boolean; /** * Command to execute when it should fire */ fire(position: Position, vimState: VimState): Promise; getMatches(position: Position, vimState: VimState): Match[]; readonly searchCharCount: number; } export interface IEasyMotion { accumulation: string; previousMode: Mode; markers: Marker[]; searchAction: EasyMotionSearchAction; addMarker(marker: Marker): void; findMarkers(nail: string, onlyVisible: boolean): Marker[]; sortedSearch( document: vscode.TextDocument, position: Position, search?: string | RegExp, options?: SearchOptions, ): Match[]; updateDecorations(editor: vscode.TextEditor): void; clearMarkers(): void; clearDecorations(editor: vscode.TextEditor): void; } ================================================ FILE: src/actions/plugins/imswitcher.ts ================================================ import { exec } from 'child_process'; import { configuration } from '../../configuration/configuration'; import { Mode } from '../../mode/mode'; import { Logger } from '../../util/logger'; /** * This function executes a shell command and returns the standard output as a string. */ function executeShell(cmd: string): Promise { return new Promise((resolve, reject) => { try { exec(cmd, (err, stdout, stderr) => { if (err) { reject(err); } else { resolve(stdout); } }); } catch (error) { reject(error as Error); } }); } /** * InputMethodSwitcher changes input method when mode changed */ export class InputMethodSwitcher { private execute: (cmd: string) => Promise; private savedIMKey = ''; constructor(execute: (cmd: string) => Promise = executeShell) { this.execute = execute; } public async switchInputMethod(prevMode: Mode, newMode: Mode) { if (configuration.autoSwitchInputMethod.enable !== true) { return; } // when you exit from insert-like mode, save origin input method and set it to default const isPrevModeInsertLike = this.isInsertLikeMode(prevMode); const isNewModeInsertLike = this.isInsertLikeMode(newMode); if (isPrevModeInsertLike !== isNewModeInsertLike) { if (isNewModeInsertLike) { await this.resumeIM(); } else { await this.switchToDefaultIM(); } } } // save origin input method and set input method to default private async switchToDefaultIM() { const obtainIMCmd = configuration.autoSwitchInputMethod.obtainIMCmd; try { const insertIMKey = await this.execute(obtainIMCmd); if (insertIMKey !== undefined) { this.savedIMKey = insertIMKey.trim(); } } catch (e) { Logger.error(`Error switching to default IM. err=${e}`); } const defaultIMKey = configuration.autoSwitchInputMethod.defaultIM; if (defaultIMKey !== this.savedIMKey) { await this.switchToIM(defaultIMKey); } } // resume origin inputmethod private async resumeIM() { if (this.savedIMKey !== configuration.autoSwitchInputMethod.defaultIM) { await this.switchToIM(this.savedIMKey); } } private async switchToIM(imKey: string) { let switchIMCmd = configuration.autoSwitchInputMethod.switchIMCmd; if (imKey !== '' && imKey !== undefined) { switchIMCmd = switchIMCmd.replace('{im}', imKey); try { await this.execute(switchIMCmd); } catch (e) { Logger.error(`Error switching to IM. err=${e}`); } } } private isInsertLikeMode(mode: Mode): boolean { return [Mode.Insert, Mode.Replace, Mode.SurroundInputMode].includes(mode); } } ================================================ FILE: src/actions/plugins/pluginDefaultMappings.ts ================================================ import { IConfiguration, IKeyRemapping } from '../../configuration/iconfiguration'; export class PluginDefaultMappings { // plugin authers may add entries here private static defaultMappings: Array<{ mode: string; configSwitch: string; mapping: IKeyRemapping; }> = [ // default maps for surround { mode: 'normalModeKeyBindingsNonRecursive', configSwitch: 'surround', mapping: { before: ['y', 's'], after: [''] }, }, { mode: 'normalModeKeyBindingsNonRecursive', configSwitch: 'surround', mapping: { before: ['y', 's', 's'], after: ['', ''] }, }, { mode: 'normalModeKeyBindingsNonRecursive', configSwitch: 'surround', mapping: { before: ['c', 's'], after: [''] }, }, { mode: 'normalModeKeyBindingsNonRecursive', configSwitch: 'surround', mapping: { before: ['d', 's'], after: [''] }, }, ]; public static getPluginDefaultMappings(mode: string, config: IConfiguration): IKeyRemapping[] { return this.defaultMappings .filter((m) => m.mode === mode && config[m.configSwitch]) .map((m) => m.mapping); } } ================================================ FILE: src/actions/plugins/replaceWithRegister.ts ================================================ import { Position, Range } from 'vscode'; import { PositionDiff } from '../../common/motion/position'; import { configuration } from '../../configuration/configuration'; import { VimError } from '../../error'; import { Mode } from '../../mode/mode'; import { Register, RegisterMode } from '../../register/register'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { BaseOperator } from '../operator'; import { RegisterAction } from './../base'; @RegisterAction class ReplaceOperator extends BaseOperator { public keys = ['g', 'r']; public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return configuration.replaceWithRegister && super.doesActionApply(vimState, keysPressed); } public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { return configuration.replaceWithRegister && super.doesActionApply(vimState, keysPressed); } public async run(vimState: VimState, start: Position, end: Position): Promise { const range = vimState.currentRegisterMode === RegisterMode.LineWise ? new Range(start.getLineBegin(), end.getLineEnd()) : new Range(start, end.getRight()); const register = await Register.get(vimState.recordedState.registerName, this.multicursorIndex); if (register === undefined) { StatusBar.displayError( vimState, VimError.NothingInRegister(vimState.recordedState.registerName), ); return; } const replaceWith = register.text as string; vimState.recordedState.transformer.replace( range, replaceWith, PositionDiff.exactPosition(getCursorPosition(vimState, range, replaceWith)), ); await vimState.setCurrentMode(Mode.Normal); } } const getCursorPosition = (vimState: VimState, range: Range, replaceWith: string): Position => { const { recordedState: { actionKeys }, } = vimState; const lines = replaceWith.split('\n'); const wasRunAsLineAction = actionKeys.indexOf('r') === 0 && actionKeys.length === 1; // ie. grr const registerAndRangeAreSingleLines = lines.length === 1 && range.isSingleLine; const singleLineAction = registerAndRangeAreSingleLines && !wasRunAsLineAction; return singleLineAction ? cursorAtEndOfReplacement(range, replaceWith) : cursorAtFirstNonBlankCharOfLine(range.start.line, lines[0]); }; const cursorAtEndOfReplacement = (range: Range, replacement: string) => new Position(range.start.line, Math.max(0, range.start.character + replacement.length - 1)); const cursorAtFirstNonBlankCharOfLine = (line: number, text: string) => new Position(line, text.match(/\S/)?.index ?? 0); ================================================ FILE: src/actions/plugins/sneak.ts ================================================ import { Position } from 'vscode'; import { VimState } from '../../state/vimState'; import { BaseMovement, IMovement } from '../baseMotion'; import { configuration } from './../../configuration/configuration'; import { RegisterAction } from './../base'; @RegisterAction export class SneakForward extends BaseMovement { keys = [ ['s', '', ''], ['z', '', ''], ]; override isJump = true; public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { const startingLetter = vimState.recordedState.operator === undefined ? 's' : 'z'; return ( configuration.sneak && super.couldActionApply(vimState, keysPressed) && keysPressed[0] === startingLetter ); } public override async execAction( position: Position, vimState: VimState, ): Promise { if (!this.isRepeat) { vimState.lastSemicolonRepeatableMovement = new SneakForward(this.keysPressed, true); vimState.lastCommaRepeatableMovement = new SneakBackward(this.keysPressed, true); } if (this.keysPressed[2] === '\n') { // Single key sneak this.keysPressed[2] = ''; } const searchString = this.keysPressed[1] + this.keysPressed[2]; const document = vimState.document; const lineCount = document.lineCount; for (let i = position.line; i < lineCount; ++i) { const lineText = document.lineAt(i).text; // Start searching after the current character so we don't find the same match twice const fromIndex = i === position.line ? position.character + 1 : 0; let matchIndex = -1; const ignorecase = configuration.sneakUseIgnorecaseAndSmartcase && configuration.ignorecase && !(configuration.smartcase && /[A-Z]/.test(searchString)); // Check for matches if (ignorecase) { matchIndex = lineText .toLocaleLowerCase() .indexOf(searchString.toLocaleLowerCase(), fromIndex); } else { matchIndex = lineText.indexOf(searchString, fromIndex); } if (matchIndex >= 0) { return new Position(i, matchIndex); } } return position; } } @RegisterAction export class SneakBackward extends BaseMovement { keys = [ ['S', '', ''], ['Z', '', ''], ]; override isJump = true; public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { const startingLetter = vimState.recordedState.operator === undefined ? 'S' : 'Z'; return ( configuration.sneak && super.couldActionApply(vimState, keysPressed) && keysPressed[0] === startingLetter ); } public override async execAction( position: Position, vimState: VimState, ): Promise { if (!this.isRepeat) { vimState.lastSemicolonRepeatableMovement = new SneakBackward(this.keysPressed, true); vimState.lastCommaRepeatableMovement = new SneakForward(this.keysPressed, true); } if (this.keysPressed[2] === '\n') { // Single key sneak this.keysPressed[2] = ''; } const searchString = this.keysPressed[1] + this.keysPressed[2]; const document = vimState.document; for (let i = position.line; i >= 0; --i) { const lineText = document.lineAt(i).text; // Start searching before the current character so we don't find the same match twice const fromIndex = i === position.line ? position.character - 1 : +Infinity; let matchIndex = -1; const ignorecase = configuration.sneakUseIgnorecaseAndSmartcase && configuration.ignorecase && !(configuration.smartcase && /[A-Z]/.test(searchString)); // Check for matches if (ignorecase) { matchIndex = lineText .toLocaleLowerCase() .lastIndexOf(searchString.toLocaleLowerCase(), fromIndex); } else { matchIndex = lineText.lastIndexOf(searchString, fromIndex); } if (matchIndex >= 0) { return new Position(i, matchIndex); } } return position; } } ================================================ FILE: src/actions/plugins/surround.ts ================================================ import { Position, Range, window } from 'vscode'; import { VimState } from '../../state/vimState'; import { SelectABigWord, SelectInnerWord, SelectWord, TextObject, } from '../../textobject/textobject'; import { WordType } from '../../textobject/word'; import { isIMovement } from '../baseMotion'; import { MoveAroundBacktick, MoveAroundCaret, MoveAroundCurlyBrace, MoveAroundDoubleQuotes, MoveAroundParentheses, MoveAroundSingleQuotes, MoveAroundSquareBracket, MoveAroundTag, MoveFullWordBegin, MoveInsideCharacter, MoveInsideTag, MoveQuoteMatch, MoveWordBegin, } from '../motion'; import { PositionDiff, sorted } from './../../common/motion/position'; import { configuration } from './../../configuration/configuration'; import { DotCommandStatus, Mode } from './../../mode/mode'; import { BaseCommand, RegisterAction } from './../base'; import { BaseOperator } from './../operator'; type SurroundEdge = { leftEdge: Range; rightEdge: Range; /** we need to pass this with transformations */ cursorIndex: number; /** to support changing a tag, cstt */ leftTagName?: Range; rightTagName?: Range; }; type TagReplacement = { tag: string; /** when changing tag to tag, do we keep attributes? default: yes */ keepAttributes: boolean; }; export interface SurroundState { /** The operator paired with the surround action. "yank" is really "add", but it uses 'y' */ operator: 'change' | 'delete' | 'yank'; /** target of surround op: X in csXy and dsX */ target: string | undefined; /** the added surrounding, like ",',(). t = tag */ replacement: string; /** name of tag */ tag?: TagReplacement; /** name of function */ function?: string; /** for visual line mode */ addNewline?: boolean; edges: SurroundEdge[]; /** The mode before surround was triggered */ previousMode: Mode; } abstract class SurroundOperator extends BaseOperator { public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return configuration.surround && super.doesActionApply(vimState, keysPressed); } } @RegisterAction class YankSurroundOperator extends SurroundOperator { // needs: nnoremap ys . we leave it to Remapper to figure out y vs ys. public keys = ['']; public modes = [Mode.Normal]; public async run(vimState: VimState, start: Position, end: Position): Promise { // reset surround state when run for first cursor if (!this.multicursorIndex) { vimState.surround = { operator: 'yank', target: undefined, replacement: '', edges: [], previousMode: vimState.currentMode, }; } const getYankRanges = (): SurroundEdge => { // for special handling for w motion. // with "|surroundme ZONK" it will jump to Z, but we just want surroundme const endPlus1 = new Range(end.getRight(), end.getRight()); const prevWordEnd = end.getRight().prevWordEnd(vimState.document); const endW = new Range(prevWordEnd.getRight(), prevWordEnd.getRight()); const lastMotion = vimState.recordedState.actionsRun[vimState.recordedState.actionsRun.length - 1]; const ranWwMotion = lastMotion instanceof MoveWordBegin || lastMotion instanceof MoveFullWordBegin || lastMotion instanceof SelectABigWord || lastMotion instanceof SelectWord; const rightEdge = ranWwMotion ? endW : endPlus1; return { leftEdge: new Range(start, start), rightEdge, cursorIndex: multicursorIndex, }; }; // then collect ranges for all cursors const multicursorIndex = this.multicursorIndex ?? 0; vimState.surround!.edges.push(getYankRanges()); vimState.cursorStartPosition = start; // when called from visual operator, use end for stop to keep visual selection vimState.cursorStopPosition = vimState.currentMode === Mode.Visual ? end : start; await vimState.setCurrentMode(Mode.SurroundInputMode); } public override async runRepeat( vimState: VimState, position: Position, count: number, ): Promise { // we want to act on range: first non whitespace to last non whitespace await this.run( vimState, position.getLineBeginRespectingIndent(vimState.document), position .getDown(Math.max(0, count - 1)) .getLineEnd() .prevWordEnd(vimState.document), ); } } @RegisterAction class CommandSurroundModeStartVisual extends SurroundOperator { modes = [Mode.Visual]; keys = ['S']; public async run(vimState: VimState, start: Position, end: Position): Promise { [start, end] = sorted(start, end); await new YankSurroundOperator(this.multicursorIndex).run(vimState, start, end); return; } } @RegisterAction class CommandSurroundModeStartVisualLine extends SurroundOperator { modes = [Mode.VisualLine]; keys = ['S']; public async run(vimState: VimState, start: Position, end: Position): Promise { [start, end] = sorted(start.getLineBegin(), end.getLineEnd()); // reset surround state when run for first cursor if (!this.multicursorIndex) { vimState.surround = { target: undefined, operator: 'yank', replacement: '', addNewline: true, edges: [], previousMode: vimState.currentMode, }; } // collect ranges for all cursors vimState.surround?.edges.push({ leftEdge: new Range(start, start), rightEdge: new Range(end, end), cursorIndex: this.multicursorIndex ?? 0, }); vimState.cursorStartPosition = start; vimState.cursorStopPosition = end; await vimState.setCurrentMode(Mode.SurroundInputMode); return; } } abstract class CommandSurround extends BaseCommand { modes = [Mode.Normal]; override createsUndoPoint = true; override runsOnceForEveryCursor() { return true; } public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { const target = keysPressed[keysPressed.length - 1]; return ( configuration.surround && super.doesActionApply(vimState, keysPressed) && SurroundHelper.edgePairings[target] !== undefined ); } } @RegisterAction class CommandSurroundDeleteSurround extends CommandSurround { keys = ['', '']; keysHasCnt = false; public override async exec(position: Position, vimState: VimState): Promise { const target = this.keysPressed[this.keysPressed.length - 1]; // for derived class, support ds2X if (this.keysHasCnt) { const cntKey = this.keysPressed[this.keysPressed.length - 2]; // eslint-disable-next-line radix vimState.recordedState.count = parseInt(cntKey, undefined); } // for this operator, we set surround state and execute for each cursor one at a time vimState.surround = { operator: 'delete', target, replacement: '', edges: [], previousMode: Mode.Normal, }; // we need surround state initiated for this call const replaceRanges = await SurroundHelper.getReplaceRanges( vimState, position, this.multicursorIndex ?? 0, ); if (replaceRanges) { vimState.surround.edges = [replaceRanges]; await SurroundHelper.ExecuteSurround(vimState); } } } @RegisterAction class CommandSurroundDeleteSurroundCnt extends CommandSurroundDeleteSurround { // supports cnt up to 9, should be enough override keys = ['', '', '']; override keysHasCnt = true; } @RegisterAction class CommandSurroundChangeSurround extends CommandSurround { keys = ['', '']; override isCompleteAction = false; keysHasCnt = false; public override async exec(position: Position, vimState: VimState): Promise { const target = this.keysPressed[this.keysPressed.length - 1]; // for derived class, support ds2X if (this.keysHasCnt) { const cntKey = this.keysPressed[this.keysPressed.length - 2]; // eslint-disable-next-line radix vimState.recordedState.count = parseInt(cntKey, undefined); } // reset surround state when run for first cursor if (!this.multicursorIndex) { vimState.surround = { operator: 'change', target, replacement: '', edges: [], previousMode: Mode.Normal, }; } // we need state surround initiated for this call const replaceRanges = await SurroundHelper.getReplaceRanges( vimState, position, this.multicursorIndex ?? 0, ); // collect ranges for all cursors if (replaceRanges) { vimState.surround!.edges.push(replaceRanges); } await vimState.setCurrentMode(Mode.SurroundInputMode); } } @RegisterAction class CommandSurroundChangeSurroundCnt extends CommandSurroundChangeSurround { // supports cnt up to 9, should be enough override keys = ['', '', '']; override keysHasCnt = true; } @RegisterAction class CommandSurroundAddSurrounding extends BaseCommand { modes = [Mode.SurroundInputMode]; // add surrounding / read X when: ys + motion + X. or csYX keys = ['']; override isCompleteAction = true; override runsOnceForEveryCursor() { return false; } public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { const replacement = keysPressed[keysPressed.length - 1]; return ( configuration.surround && super.doesActionApply(vimState, keysPressed) && replacement !== 't' && // do not run this for surrounding with a tag replacement !== '<' && replacement !== 'f' && // or for surrounding with a function replacement !== 'F' && replacement !== '' ); } public override async exec(position: Position, vimState: VimState): Promise { const replacement = this.keysPressed[this.keysPressed.length - 1]; if (!vimState.surround || !SurroundHelper.edgePairings[replacement]) { // cant surround, abort. // this typically handles, when last keypress was wrong and not a valid surrounding vimState.surround = undefined; await vimState.setCurrentMode(Mode.Normal); return; } vimState.surround.replacement = replacement; await SurroundHelper.ExecuteSurround(vimState); } } @RegisterAction export class CommandSurroundAddSurroundingTag extends BaseCommand { modes = [Mode.SurroundInputMode]; // add surrounding / read X when: ys + motion + X keys = [['<'], ['t']]; override isCompleteAction = true; recordedTag = ''; // to save for repeat override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { if (!vimState.surround) { return; } vimState.surround.replacement = 't'; const tagInput = vimState.dotCommandStatus === DotCommandStatus.Executing || vimState.isReplayingMacro ? this.recordedTag : await this.readTag(); if (!tagInput) { vimState.surround = undefined; await vimState.setCurrentMode(Mode.Normal); return; } // record tag for repeat. this works because recordedState will store the actual objects this.recordedTag = tagInput; // local helper const checkReplaceAttributes = (tag: string) => { return tag.substring(tag.length - 1) === '>' ? { tag: tag.substring(0, tag.length - 1), keepAttributes: false } : { tag, keepAttributes: true }; }; // check as special case (set by >) if we want to replace the attributes on tag or keep them (default) vimState.surround.tag = checkReplaceAttributes(tagInput); // finally, we can exec surround await SurroundHelper.ExecuteSurround(vimState); } private async readTag(): Promise { return window.showInputBox({ prompt: 'Enter tag', ignoreFocusOut: true, }); } } @RegisterAction export class CommandSurroundAddSurroundingFunction extends BaseCommand { modes = [Mode.SurroundInputMode]; // add surrounding / read X when: ys + motion + X keys = [['f'], ['F'], ['']]; override isCompleteAction = true; recordedFunction = ''; // to save for repeat override runsOnceForEveryCursor() { return false; } public override async exec(position: Position, vimState: VimState): Promise { if (!vimState.surround) { return; } // reuse the spacing logic from the parentheses // for the right side of the replacement vimState.surround.replacement = this.keysPressed[this.keysPressed.length - 1] === 'F' ? '(' : ')'; const functionInput = vimState.dotCommandStatus === DotCommandStatus.Executing || vimState.isReplayingMacro ? this.recordedFunction : await this.readFunction(); if (!functionInput) { vimState.surround = undefined; await vimState.setCurrentMode(Mode.Normal); return; } // record function for repeat. this.recordedFunction = functionInput; // format the left side of the replacement based on the key pressed vimState.surround.function = this.formatFunction(functionInput); await SurroundHelper.ExecuteSurround(vimState); } private async readFunction(): Promise { return window.showInputBox({ prompt: 'Enter function', ignoreFocusOut: true, }); } private formatFunction(fn: string): string { switch (this.keysPressed[this.keysPressed.length - 1]) { case 'f': return fn + '('; case 'F': return fn + '( '; case '': default: return '(' + fn + ' '; } } } // following are static internal helper functions // top level helper is ExecuteSurround, which is called from exec and does the actual text transformations class SurroundHelper { /** a map which holds for each target key: inserted text + implementation helper */ static edgePairings: { [key: string]: { left: string; right: string; /** do we consume space on the edges? "(" vs ")" */ removeSpace: boolean; movement: () => MoveInsideCharacter | MoveQuoteMatch | MoveAroundTag | TextObject; /** typically to extend an inner word. with *foo*, from "foo" to "*foo*" */ extraChars?: number; }; } = { // helpful linter is helpful :-D '(': { left: '( ', right: ' )', removeSpace: true, movement: () => new MoveAroundParentheses(), }, ')': { left: '(', right: ')', removeSpace: false, movement: () => new MoveAroundParentheses() }, '[': { left: '[ ', right: ' ]', removeSpace: true, movement: () => new MoveAroundSquareBracket(), }, ']': { left: '[', right: ']', removeSpace: false, movement: () => new MoveAroundSquareBracket(), }, '{': { left: '{ ', right: ' }', removeSpace: true, movement: () => new MoveAroundCurlyBrace() }, '}': { left: '{', right: '}', removeSpace: false, movement: () => new MoveAroundCurlyBrace() }, '>': { left: '<', right: '>', removeSpace: false, movement: () => new MoveAroundCaret() }, '"': { left: '"', right: '"', removeSpace: false, movement: () => new MoveAroundDoubleQuotes(false), }, "'": { left: "'", right: "'", removeSpace: false, movement: () => new MoveAroundSingleQuotes(false), }, '`': { left: '`', right: '`', removeSpace: false, movement: () => new MoveAroundBacktick(false), }, '<': { left: '', right: '', removeSpace: false, movement: () => new MoveAroundTag() }, '*': { left: '*', right: '*', removeSpace: false, movement: () => new SelectInnerWord(), extraChars: 1, }, // aliases b: { left: '(', right: ')', removeSpace: false, movement: () => new MoveAroundParentheses() }, r: { left: '[', right: ']', removeSpace: false, movement: () => new MoveAroundSquareBracket() }, B: { left: '{', right: '}', removeSpace: false, movement: () => new MoveAroundCurlyBrace() }, a: { left: '<', right: '>', removeSpace: false, movement: () => new MoveAroundCaret() }, t: { left: '', right: '', removeSpace: false, movement: () => new MoveAroundTag() }, _: { left: '_', right: '_', removeSpace: false, movement: () => new SelectInnerWord() }, }; /** returns two ranges (for left and right replacement) for our surround target (X in dsX, csXy) relative to position */ public static async getReplaceRanges( vimState: VimState, position: Position, multicursorIndex: number, ): Promise { /* so this method is a bit of a dumpster for edge cases and ugly details the main idea is this: 1. from position, we execute a textobject movement to get the total range of our surround target 2. from there, we derive two ranges (left and right), where to apply delete/change 3. that our result to return */ // input verification if (!vimState.surround || !vimState.surround.target) { return undefined; } const target = this.edgePairings[vimState.surround.target]; if (!target) { return undefined; } // we want start, end of executing movement for surround target count times from position const { removeSpace, movement } = target; vimState.cursorStartPosition = position; // some textobj (MoveInsideCharacter) expect this const count = vimState.recordedState.count || 1; const targetMovement = await movement().execActionWithCount(position, vimState, count); if (!isIMovement(targetMovement) || !!targetMovement.failed) { // we want as result an IMovement, that did not fail. return undefined; } let rangeStart = targetMovement.start; let rangeEnd = targetMovement.stop; // some local helpers const getAdjustedRanges = (): SurroundEdge => { if (movement() instanceof MoveInsideCharacter) { // for parens, brackets, curly ... we have to adjust the right range // there seems to be inconsistency between MoveInsideCharacter and MoveQuoteMatch rangeEnd = rangeEnd.getLeft(); } if (target.extraChars) { rangeStart = rangeStart.getLeft(target.extraChars); rangeEnd = rangeEnd.getRight(target.extraChars); } // now start and end are on () // next, check if there is space to remove (foo) vs ( bar ) const delSpace = checkRemoveSpace(); // 0 or 1 return { leftEdge: new Range(rangeStart, rangeStart.getRight(1 + delSpace)), rightEdge: new Range(rangeEnd.getLeft(delSpace), rangeEnd.getRight()), cursorIndex: multicursorIndex, }; }; const checkRemoveSpace = (): number => { // capiche? const leftSpace = vimState.editor.document.getText( new Range(rangeStart.getRight(), rangeStart.getRight(2)), ); const rightSpace = vimState.editor.document.getText(new Range(rangeEnd.getLeft(), rangeEnd)); return removeSpace && leftSpace === ' ' && rightSpace === ' ' ? 1 : 0; }; const getAdjustedRangesForTag = async (): Promise => { // we are on start of opening tag and end of closing tag // return ranges from there to the other side // start -> bar <-- stop const openTagNameStart = rangeStart.getRight(); const openTagNameEnd = openTagNameStart .nextWordEnd(vimState.document, { wordType: WordType.TagName, inclusive: true }) .getRight(); const closeTagNameStart = rangeEnd .getLeft(2) .prevWordStart(vimState.document, { wordType: WordType.TagName, inclusive: true }); const closeTagNameEnd = rangeEnd.getLeft(); vimState.cursorStartPosition = position; // some textobj (MoveInsideCharacter) expect this vimState.cursorStopPosition = position; const innerTag = count === 1 ? await new MoveInsideTag().execActionWithCount(position, vimState, 1) : await new MoveAroundTag().execActionWithCount(position, vimState, count - 1); if (!isIMovement(innerTag) || !!innerTag.failed) { return undefined; } else { return { leftEdge: new Range(rangeStart, innerTag.start), // maybe there is a small bug with cstt for multicursor, 2nd+ cursors rightEdge: new Range(innerTag.stop, rangeEnd), leftTagName: new Range(openTagNameStart, openTagNameEnd), rightTagName: new Range(closeTagNameStart, closeTagNameEnd), cursorIndex: multicursorIndex, }; } }; // good to go, now we can calculate our ranges based on rangeStart and rangeEnd return vimState.surround.target === 't' ? getAdjustedRangesForTag() : getAdjustedRanges(); } /** executes our prepared surround changes */ public static async ExecuteSurround(vimState: VimState): Promise { const surroundState = vimState.surround; if (!surroundState || !surroundState.edges) { return; } const replacement = this.edgePairings[surroundState.replacement]; // undefined allowed only for delete operator if (!replacement && surroundState.operator !== 'delete') { throw new Error('replacement missing in pairs'); } // handle special case: cstt, replace only tag name if (surroundState.target === 't' && surroundState.tag && surroundState.tag.keepAttributes) { for (const { leftTagName, rightTagName } of surroundState.edges) { if (!surroundState.tag || !leftTagName || !rightTagName) { // throw ? continue; } vimState.recordedState.transformer.replace(leftTagName, surroundState.tag.tag); vimState.recordedState.transformer.replace(rightTagName, surroundState.tag.tag); } } // all other cases: ys, ds, cs else { const optNewline = surroundState.addNewline ? '\n' : ''; const leftFixed = surroundState.operator === 'delete' ? '' : surroundState.tag ? '<' + surroundState.tag.tag + '>' + optNewline : surroundState.function ? surroundState.function + optNewline : replacement.left + optNewline; const rightFixed = surroundState.operator === 'delete' ? '' : surroundState.tag ? optNewline + '' : optNewline + replacement.right; for (const { leftEdge, rightEdge, cursorIndex } of surroundState.edges) { vimState.recordedState.transformer.addTransformation({ type: 'replaceText', text: leftFixed, range: leftEdge, cursorIndex, // keep cursor on left edge / start. todo: not completly correct vor visual S diff: surroundState.operator === 'yank' ? PositionDiff.offset({ character: -leftFixed.length }) : undefined, }); vimState.recordedState.transformer.replace(rightEdge, rightFixed); } } // finish / cleanup. sql-koala was here :D await vimState.setCurrentMode(Mode.Normal); } private static trimAttributes(wholeTag: string) { const endTagIndex = wholeTag.indexOf(' '); const isAnyAttributeAfterTag = endTagIndex !== -1; return isAnyAttributeAfterTag ? wholeTag.substring(0, endTagIndex) : wholeTag; } } ================================================ FILE: src/actions/plugins/targets/lastNextObjectHelper.ts ================================================ import { Position } from 'vscode'; import { isVisualMode } from '../../../mode/mode'; import { VimState } from '../../../state/vimState'; import { Logger } from '../../../util/logger'; import { BaseMovement, failedMovement, IMovement } from '../../baseMotion'; import { MoveInsideCharacter } from '../../motion'; import { searchPosition } from './searchUtils'; import { bracketObjectsEnabled } from './targetsConfig'; /* * This function creates a last/next movement based on an existing one. * It works by searching for a next/last character, and then applying the given action in its position. * For examples of how to use it, see src/actions/plugins/targets/lastNextObjects.ts. */ function LastNextObject(type: new () => T, which: 'l' | 'n') { abstract class NextHandlerClass extends BaseMovement { public override readonly keys: readonly string[] | readonly string[][]; override isJump = true; // actual action (e.g. `i(` ) private readonly actual: T; readonly secondKey: 'l' | 'n' = which; // character to search forward/backward for next/last (e.g. `(` for next parenthesis) abstract readonly charToFind: string; // this is just to make sure we won't register anything that we can't handle. see constructor. readonly valid: boolean; public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { return this.valid && bracketObjectsEnabled() && super.doesActionApply(vimState, keysPressed); } constructor() { super(); this.actual = new type(); const secondKey = this.secondKey; const withWhichKey = (keys: string[]): string[] | undefined => { if (keys.length === 2) { return [keys[0], secondKey, keys[1]]; } else { return undefined; } }; // we want fail without throwing an exception, but with log, to not break the Vim const errMsg = `failed to register ${which === 'l' ? 'last' : 'next'} for ${type.name}`; // failed, but it should never happen if (this.actual.keys.length < 1) { this.valid = false; this.keys = []; Logger.error(errMsg); return; } if (typeof this.actual.keys[0] === 'string') { const keys = withWhichKey(this.actual.keys as string[]); // failed if (keys === undefined) { this.valid = false; this.keys = []; Logger.error(errMsg); return; } else { this.keys = keys; } } else { const keys = this.actual.keys.map((k) => withWhichKey(k as string[])); // failed if (!keys.every((p) => p !== undefined)) { this.valid = false; this.keys = []; Logger.error(errMsg); return; } else { this.keys = keys; } } this.valid = true; } public override async execAction( position: Position, vimState: VimState, firstIteration: boolean, lastIteration: boolean, ): Promise { const maybePosition = searchPosition(this.charToFind, vimState.document, position, { direction: which === 'l' ? '<' : '>', includeCursor: false, throughLineBreaks: true, }); if (maybePosition === undefined) { return failedMovement(vimState); } vimState.cursorStartPosition = maybePosition; vimState.cursorStopPosition = maybePosition; const movement = await this.actual.execAction( maybePosition, vimState, firstIteration, lastIteration, ); if (movement.failed) { return movement; } const { start, stop } = movement; if (!isVisualMode(vimState.currentMode) && position.isBefore(start)) { vimState.recordedState.operatorPositionDiff = start.subtract(position); } else if (!isVisualMode(vimState.currentMode) && position.isAfter(stop)) { if (position.line === stop.line) { vimState.recordedState.operatorPositionDiff = stop.subtract(position); } else { vimState.recordedState.operatorPositionDiff = start.subtract(position); } } vimState.cursorStartPosition = start; vimState.cursorStopPosition = stop; return movement; } } return NextHandlerClass; } export function LastObject(type: new () => T) { return LastNextObject(type, 'l'); } export function NextObject(type: new () => T) { return LastNextObject(type, 'n'); } ================================================ FILE: src/actions/plugins/targets/lastNextObjects.ts ================================================ import { RegisterAction } from '../../base'; import { MoveAroundCaret, MoveAroundCurlyBrace, MoveAroundParentheses, MoveAroundSquareBracket, MoveInsideCaret, MoveInsideCurlyBrace, MoveInsideParentheses, MoveInsideSquareBracket, } from '../../motion'; import { LastObject, NextObject } from './lastNextObjectHelper'; @RegisterAction class MoveInsideNextParentheses extends NextObject(MoveInsideParentheses) { override readonly charToFind: string = '('; } @RegisterAction class MoveInsideLastParentheses extends LastObject(MoveInsideParentheses) { override readonly charToFind: string = ')'; } @RegisterAction class MoveAroundNextParentheses extends NextObject(MoveAroundParentheses) { override readonly charToFind: string = '('; } @RegisterAction class MoveAroundLastParentheses extends LastObject(MoveAroundParentheses) { override readonly charToFind: string = ')'; } @RegisterAction class MoveInsideNextCurlyBrace extends NextObject(MoveInsideCurlyBrace) { override readonly charToFind: string = '{'; } @RegisterAction class MoveInsideLastCurlyBrace extends LastObject(MoveInsideCurlyBrace) { override readonly charToFind: string = '}'; } @RegisterAction class MoveAroundNextCurlyBrace extends NextObject(MoveAroundCurlyBrace) { override readonly charToFind: string = '{'; } @RegisterAction class MoveAroundLastCurlyBrace extends LastObject(MoveAroundCurlyBrace) { override readonly charToFind: string = '}'; } @RegisterAction class MoveInsideNextSquareBracket extends NextObject(MoveInsideSquareBracket) { override readonly charToFind: string = '['; } @RegisterAction class MoveInsideLastSquareBracket extends LastObject(MoveInsideSquareBracket) { override readonly charToFind: string = ']'; } @RegisterAction class MoveAroundNextSquareBracket extends NextObject(MoveAroundSquareBracket) { override readonly charToFind: string = '['; } @RegisterAction class MoveAroundLastSquareBracket extends LastObject(MoveAroundSquareBracket) { override readonly charToFind: string = ']'; } @RegisterAction class MoveInsideNextCaret extends NextObject(MoveInsideCaret) { override readonly charToFind: string = '<'; } @RegisterAction class MoveInsideLastCaret extends LastObject(MoveInsideCaret) { override readonly charToFind: string = '>'; } @RegisterAction class MoveAroundNextCaret extends NextObject(MoveAroundCaret) { override readonly charToFind: string = '<'; } @RegisterAction class MoveAroundLastCaret extends LastObject(MoveAroundCaret) { override readonly charToFind: string = '>'; } ================================================ FILE: src/actions/plugins/targets/searchUtils.ts ================================================ import { Position, TextDocument } from 'vscode'; export interface SearchFlags { direction?: '<' | '>'; includeCursor?: boolean; throughLineBreaks?: boolean; } function searchForward( str: string, document: TextDocument, start: Position, flags: { throughLineBreaks?: boolean; } = { throughLineBreaks: false, }, ): Position | undefined { let position = start; for ( let line = position.line; line < document.lineCount && (flags.throughLineBreaks || line === start.line); line++ ) { position = document.validatePosition(position.with({ line })); const text = document.lineAt(position).text; const index = text.indexOf(str, position.character); if (index >= 0) { return position.with({ character: index }); } position = position.with({ character: 0 }); // set at line begin for next iteration } return undefined; } function searchBackward( str: string, document: TextDocument, start: Position, flags: { throughLineBreaks?: boolean; } = { throughLineBreaks: false, }, ): Position | undefined { let position = start; for ( let line = position.line; line >= 0 && (flags.throughLineBreaks || line === start.line); line-- ) { position = document.validatePosition(position.with({ line })); const text = document.lineAt(position).text; const index = text.lastIndexOf(str, position.character); if (index >= 0) { return position.with({ character: index }); } position = position.with({ character: +Infinity }); // set at line end for next iteration } return undefined; } export function maybeGetLeft( position: Position, { count = 1, throughLineBreaks, dontMove, }: { count?: number; throughLineBreaks?: boolean; dontMove?: boolean }, ) { return dontMove ? position : throughLineBreaks ? position.getOffsetThroughLineBreaks(-count) : position.getLeft(count); } export function maybeGetRight( position: Position, { count = 1, throughLineBreaks, dontMove, }: { count?: number; throughLineBreaks?: boolean; dontMove?: boolean }, ) { return dontMove ? position : throughLineBreaks ? position.getOffsetThroughLineBreaks(count) : position.getRight(count); } export function searchPosition( str: string, document: TextDocument, start: Position, flags: SearchFlags = { direction: '>', includeCursor: true, throughLineBreaks: false, }, ): Position | undefined { if (flags.direction === '<') { start = maybeGetLeft(start, { dontMove: flags.includeCursor, throughLineBreaks: flags.throughLineBreaks, }); return searchBackward(str, document, start, flags); } else { start = maybeGetRight(start, { dontMove: flags.includeCursor, throughLineBreaks: flags.throughLineBreaks, }); return searchForward(str, document, start, flags); } } ================================================ FILE: src/actions/plugins/targets/smartQuotes.ts ================================================ import { Mode } from '../../../mode/mode'; import { RegisterAction } from '../../base'; import { MoveQuoteMatch } from '../../motion'; abstract class SmartQuotes extends MoveQuoteMatch { override modes = [Mode.Normal, Mode.Visual, Mode.VisualBlock]; } @RegisterAction export class MoveAroundNextSingleQuotes extends SmartQuotes { keys = ['a', 'n', "'"]; readonly charToMatch = "'"; override readonly which = 'next'; override includeQuotes = true; } @RegisterAction export class MoveInsideNextSingleQuotes extends SmartQuotes { keys = ['i', 'n', "'"]; readonly charToMatch = "'"; override readonly which = 'next'; override includeQuotes = false; } @RegisterAction export class MoveAroundLastSingleQuotes extends SmartQuotes { keys = ['a', 'l', "'"]; readonly charToMatch = "'"; override readonly which = 'last'; override includeQuotes = true; } @RegisterAction export class MoveInsideLastSingleQuotes extends SmartQuotes { keys = ['i', 'l', "'"]; readonly charToMatch = "'"; override readonly which = 'last'; override includeQuotes = false; } @RegisterAction export class MoveAroundNextDoubleQuotes extends SmartQuotes { keys = ['a', 'n', '"']; readonly charToMatch = '"'; override readonly which = 'next'; override includeQuotes = true; } @RegisterAction export class MoveInsideNextDoubleQuotes extends SmartQuotes { keys = ['i', 'n', '"']; readonly charToMatch = '"'; override readonly which = 'next'; override includeQuotes = false; } @RegisterAction export class MoveAroundLastDoubleQuotes extends SmartQuotes { keys = ['a', 'l', '"']; readonly charToMatch = '"'; override readonly which = 'last'; override includeQuotes = true; } @RegisterAction export class MoveInsideLastDoubleQuotes extends SmartQuotes { keys = ['i', 'l', '"']; readonly charToMatch = '"'; override readonly which = 'last'; override includeQuotes = false; } @RegisterAction export class MoveAroundNextBacktick extends SmartQuotes { keys = ['a', 'n', '`']; readonly charToMatch = '`'; override readonly which = 'next'; override includeQuotes = true; } @RegisterAction export class MoveInsideNextBacktick extends SmartQuotes { keys = ['i', 'n', '`']; readonly charToMatch = '`'; override readonly which = 'next'; override includeQuotes = false; } @RegisterAction export class MoveAroundLastBacktick extends SmartQuotes { keys = ['a', 'l', '`']; readonly charToMatch = '`'; override readonly which = 'last'; override includeQuotes = true; } @RegisterAction export class MoveInsideLastBacktick extends SmartQuotes { keys = ['i', 'l', '`']; readonly charToMatch = '`'; override readonly which = 'last'; override includeQuotes = false; } @RegisterAction export class MoveAroundQuote extends SmartQuotes { keys = ['a', 'q']; override readonly anyQuote = true; readonly charToMatch = '"'; // it is not in use, because anyQuote is true. override includeQuotes = true; } @RegisterAction export class MoveInsideQuote extends SmartQuotes { keys = ['i', 'q']; override readonly anyQuote = true; readonly charToMatch = '"'; // it is not in use, because anyQuote is true. override includeQuotes = false; } @RegisterAction export class MoveAroundNextQuote extends SmartQuotes { keys = ['a', 'n', 'q']; override readonly which = 'next'; override readonly anyQuote = true; readonly charToMatch = '"'; // it is not in use, because anyQuote is true. override includeQuotes = true; } @RegisterAction export class MoveInsideNextQuote extends SmartQuotes { keys = ['i', 'n', 'q']; override readonly which = 'next'; override readonly anyQuote = true; readonly charToMatch = '"'; // it is not in use, because anyQuote is true. override includeQuotes = false; } @RegisterAction export class MoveAroundLastQuote extends SmartQuotes { keys = ['a', 'l', 'q']; override readonly which = 'last'; override readonly anyQuote = true; readonly charToMatch = '"'; // it is not in use, because anyQuote is true. override includeQuotes = true; } @RegisterAction export class MoveInsideLastQuote extends SmartQuotes { keys = ['i', 'l', 'q']; override readonly which = 'last'; override readonly anyQuote = true; readonly charToMatch = '"'; // it is not in use, because anyQuote is true. override includeQuotes = false; } ================================================ FILE: src/actions/plugins/targets/smartQuotesMatcher.ts ================================================ import { Position, TextDocument } from 'vscode'; import { configuration } from '../../../configuration/configuration'; type Quote = '"' | "'" | '`'; enum QuoteMatch { Opening, Closing, } export type WhichQuotes = 'current' | 'next' | 'last'; type Dir = '>' | '<'; type SearchAction = { first: Dir; second: Dir; includeCurrent: boolean; }; type QuotesAction = { search: SearchAction | undefined; skipToLeft: number; // for last quotes, how many quotes need to skip while searching skipToRight: number; // for next quotes, how many quotes need to skip while searching }; /** * This mapping is used to give a way to identify which action we need to take when operating on a line. * The keys here are, in some sense, the number of quotes in the line, in the format of `lcr`, where: * `l` means left of the cursor, `c` whether the cursor is on a quote, and `r` is right of the cursor. * * It is based on the ideas used in `targets.vim`. For each line & cursor position, we count the number of quotes * left (#L) and right (#R) of the cursor. Using those numbers and whether the cursor it on a quote, we know * what action to make. * * For each entry we have an example of a line & position. */ const quoteDirs: Record = { '002': { // | "a" "b" "c" search: { first: '>', second: '>', includeCurrent: false }, skipToLeft: 0, skipToRight: 1, }, '012': { // |"a" "b" "c" " search: { first: '>', second: '>', includeCurrent: true }, skipToLeft: 0, skipToRight: 2, }, '102': { // "a" "|b" "c" " search: { first: '<', second: '>', includeCurrent: false }, skipToLeft: 2, skipToRight: 2, }, '112': { // "a" "b|" "c" search: { first: '<', second: '<', includeCurrent: true }, skipToLeft: 2, skipToRight: 1, }, '202': { // "a"| "b" "c" search: { first: '>', second: '>', includeCurrent: false }, skipToLeft: 1, skipToRight: 1, }, '211': { // "a" |"b" "c" search: { first: '>', second: '>', includeCurrent: true }, skipToLeft: 1, skipToRight: 2, }, '101': { // "a" "|b" "c" search: { first: '<', second: '>', includeCurrent: false }, skipToLeft: 2, skipToRight: 2, }, '011': { // |"a" "b" "c" search: { first: '>', second: '>', includeCurrent: true }, skipToLeft: 0, skipToRight: 2, }, '110': { // "a" "b" "c|" search: { first: '<', second: '<', includeCurrent: true }, skipToLeft: 2, skipToRight: 0, }, '212': { // "a" |"b" "c" " search: { first: '>', second: '>', includeCurrent: true }, skipToLeft: 1, skipToRight: 2, }, '111': { // "a" "b|" "c" " search: { first: '<', second: '<', includeCurrent: true }, skipToLeft: 2, skipToRight: 1, }, '200': { // "a" "b" "c"| search: { first: '<', second: '<', includeCurrent: false }, skipToLeft: 1, skipToRight: 0, }, '201': { // "a" "b" "c"| " // "a"| "b" "c" " search: { first: '>', second: '>', includeCurrent: false }, skipToLeft: 1, skipToRight: 1, }, '210': { // "a" "b" "c" |" search: undefined, skipToLeft: 1, skipToRight: 0, }, '001': { // | "a" "b" "c" " search: undefined, skipToLeft: 0, skipToRight: 1, }, '010': { // a|"b search: undefined, skipToLeft: 0, skipToRight: 0, }, '100': { // "a" "b" "c" "| search: undefined, skipToLeft: 2, skipToRight: 0, }, '000': { // |ab search: undefined, skipToLeft: 0, skipToRight: 0, }, }; export class SmartQuoteMatcher { static readonly escapeChar = '\\'; private document: TextDocument; private quote: Quote | 'any'; constructor(quote: Quote | 'any', document: TextDocument) { this.quote = quote; this.document = document; } private buildQuoteMap(text: string) { const quoteMap: QuoteMatch[] = []; let openingQuote = true; // Loop over text, marking quotes and respecting escape characters. for (let i = 0; i < text.length; i++) { if (text[i] === SmartQuoteMatcher.escapeChar) { i += 1; continue; } if ( (this.quote === 'any' && (text[i] === '"' || text[i] === "'" || text[i] === '`')) || text[i] === this.quote ) { quoteMap[i] = openingQuote ? QuoteMatch.Opening : QuoteMatch.Closing; openingQuote = !openingQuote; } } return quoteMap; } private static lineSearchAction(cursorIndex: number, quoteMap: QuoteMatch[]) { // base on ideas from targets.vim // cut line in left of, on and right of cursor const left = Array.from(quoteMap.entries()).slice(undefined, cursorIndex); const cursor = quoteMap[cursorIndex]; const right = Array.from(quoteMap.entries()).slice(cursorIndex + 1, undefined); // how many delimiters left, on and right of cursor const lc = left.filter(([_, v]) => v !== undefined).length; const cc = cursor !== undefined ? 1 : 0; const rc = right.filter(([_, v]) => v !== undefined).length; // truncate counts const lct = lc === 0 ? 0 : lc % 2 === 0 ? 2 : 1; const rct = rc === 0 ? 0 : rc >= 2 ? 2 : 1; const key = `${lct}${cc}${rct}`; const act = quoteDirs[key]; return act; } public smartSurroundingQuotes( position: Position, which: WhichQuotes, ): { start: Position; stop: Position; lineText: string } | undefined { position = this.document.validatePosition(position); const cursorIndex = position.character; const lineText = this.document.lineAt(position).text; const quoteMap = this.buildQuoteMap(lineText); const act = SmartQuoteMatcher.lineSearchAction(cursorIndex, quoteMap); if (which === 'current') { if (act.search) { const searchRes = this.smartSearch(cursorIndex, act.search, quoteMap); return searchRes ? { start: position.with({ character: searchRes[0] }), stop: position.with({ character: searchRes[1] }), lineText, } : undefined; } else { return undefined; } } else if (which === 'next') { // search quote in current line const right = Array.from(quoteMap.entries()).slice(cursorIndex + 1, undefined); const [index, found] = right.filter(([i, v]) => v !== undefined)[act.skipToRight] ?? [ +Infinity, undefined, ]; // find next position for surrounding quotes, possibly breaking through lines let nextPos; position = position.with({ character: index }); if (found === undefined && configuration.targets.smartQuotes.breakThroughLines) { // nextPos = State.evalGenerator(this.getNextQuoteThroughLineBreaks(), position); nextPos = this.getNextQuoteThroughLineBreaks(position); } else { nextPos = found !== undefined ? position : undefined; } // find surrounding with new position if (nextPos) { return this.smartSurroundingQuotes(nextPos, 'current'); } else { return undefined; } } else if (which === 'last') { // search quote in current line const left = Array.from(quoteMap.entries()).slice(undefined, cursorIndex); const [index, found] = left.reverse().filter(([i, v]) => v !== undefined)[act.skipToLeft] ?? [ 0, undefined, ]; // find last position for surrounding quotes, possibly breaking through lines let lastPos; position = position.with({ character: index }); if (found === undefined && configuration.targets.smartQuotes.breakThroughLines) { position = position.getLeftThroughLineBreaks(); lastPos = this.getLastQuoteThroughLineBreaks(position); } else { lastPos = found !== undefined ? position : undefined; } // find surrounding with new position if (lastPos) { return this.smartSurroundingQuotes(lastPos, 'current'); } else { return undefined; } } else { return undefined; } } private smartSearch( start: number, action: SearchAction, quoteMap: QuoteMatch[], ): [number, number] | undefined { const offset = action.includeCurrent ? 1 : 0; let cursorPos: number | undefined = start; let fst: number | undefined; let snd: number | undefined; if (action.first === '>') { cursorPos = fst = this.getNextQuote(cursorPos - offset, quoteMap); } else { // dir === '<' cursorPos = fst = this.getPrevQuote(cursorPos + offset, quoteMap); } if (cursorPos === undefined) return undefined; if (action.second === '>') { snd = this.getNextQuote(cursorPos, quoteMap); } else { // dir === '<' snd = this.getPrevQuote(cursorPos, quoteMap); } if (fst === undefined || snd === undefined) return undefined; if (fst < snd) return [fst, snd]; else return [snd, fst]; } private getNextQuoteThroughLineBreaks(position: Position): Position | undefined { for (let line = position.line; line < this.document.lineCount; line++) { position = this.document.validatePosition(position.with({ line })); const text = this.document.lineAt(position).text; if (this.quote === 'any') { for (let i = position.character; i < text.length; i++) { if (text[i] === '"' || text[i] === "'" || text[i] === '`') { return position.with({ character: i }); } } } else { const index = text.indexOf(this.quote, position.character); if (index >= 0) { return position.with({ character: index }); } } position = position.with({ character: 0 }); // set at line begin for next iteration } return undefined; } private getLastQuoteThroughLineBreaks(position: Position): Position | undefined { for (let line = position.line; line >= 0; line--) { position = this.document.validatePosition(position.with({ line })); const text = this.document.lineAt(position).text; if (this.quote === 'any') { for (let i = position.character; i >= 0; i--) { if (text[i] === '"' || text[i] === "'" || text[i] === '`') { return position.with({ character: i }); } } } else { const index = text.lastIndexOf(this.quote, position.character); if (index >= 0) { return position.with({ character: index }); } } position = position.with({ character: +Infinity }); // set at line end for next iteration } return undefined; } private getNextQuote(start: number, quoteMap: QuoteMatch[]): number | undefined { for (let i = start + 1; i < quoteMap.length; i++) { if (quoteMap[i] !== undefined) { return i; } } return undefined; } private getPrevQuote(start: number, quoteMap: QuoteMatch[]): number | undefined { for (let i = start - 1; i >= 0; i--) { if (quoteMap[i] !== undefined) { return i; } } return undefined; } } ================================================ FILE: src/actions/plugins/targets/targets.ts ================================================ // targets sub-plugins import './lastNextObjects'; import './smartQuotes'; ================================================ FILE: src/actions/plugins/targets/targetsConfig.ts ================================================ import { configuration } from '../../../configuration/configuration'; export function useSmartQuotes(): boolean { return ( (configuration.targets.enable === true && configuration.targets.smartQuotes.enable !== false) || (configuration.targets.enable === undefined && configuration.targets.smartQuotes.enable === true) ); } export function bracketObjectsEnabled(): boolean { return ( (configuration.targets.enable === true && configuration.targets.bracketObjects.enable !== false) || (configuration.targets.enable === undefined && configuration.targets.bracketObjects.enable === true) ); } ================================================ FILE: src/actions/types.d.ts ================================================ import type { Position } from 'vscode'; import type { VimState } from '../state/vimState'; export type ActionType = 'command' | 'motion' | 'operator' | 'number'; export interface IBaseAction { readonly name: string | undefined; readonly actionType: ActionType; readonly isJump: boolean; readonly createsUndoPoint: boolean; keysPressed: string[]; multicursorIndex: number | undefined; readonly preservesDesiredColumn: boolean; } export interface IBaseCommand extends IBaseAction { exec(position: Position, vimState: VimState): Promise; } export interface IBaseOperator extends IBaseAction { run(vimState: VimState, start: Position, stop: Position): Promise; runRepeat(vimState: VimState, position: Position, count: number): Promise; } ================================================ FILE: src/actions/wrapping.ts ================================================ import { configuration } from './../configuration/configuration'; import { Mode } from './../mode/mode'; /** * See https://vimhelp.org/options.txt.html#%27whichwrap%27 * * @returns true if the given key should cause the cursor to wrap around line boundary */ export const shouldWrapKey = (mode: Mode, key: string): boolean => { let k: string; if (key === '') { k = [Mode.Insert, Mode.Replace].includes(mode) ? '[' : '<'; } else if (key === '') { k = [Mode.Insert, Mode.Replace].includes(mode) ? ']' : '>'; } else if (['', '', ''].includes(key)) { k = 'b'; } else if (key === ' ') { k = 's'; } else if (['h', 'l', '~'].includes(key)) { k = key; } else { throw new Error(`shouldWrapKey called with unexpected key='${key}'`); } return configuration.whichwrap.split(',').includes(k); }; ================================================ FILE: src/cmd_line/commandLine.ts ================================================ import { Parser } from 'parsimmon'; import { ExtensionContext, Position, window } from 'vscode'; import { configuration } from '../configuration/configuration'; import { ErrorCode, VimError } from '../error'; import { CommandLineHistory, HistoryFile, SearchHistory } from '../history/historyFile'; import { Register } from '../register/register'; import { globalState } from '../state/globalState'; import { RecordedState } from '../state/recordedState'; import { IndexedPosition, IndexedRange, SearchState } from '../state/searchState'; import { VimState } from '../state/vimState'; import { StatusBar } from '../statusBar'; import { WordType, getWordLeftInText, getWordRightInText } from '../textobject/word'; import { SearchDecorations, getDecorationsForSearchMatchRanges } from '../util/decorationUtils'; import { Logger } from '../util/logger'; import { escapeCSSIcons, reportSearch } from '../util/statusBarTextUtils'; import { scrollView } from '../util/util'; import { ExCommand } from '../vimscript/exCommand'; import { LineRange } from '../vimscript/lineRange'; import { SearchDirection } from '../vimscript/pattern'; import { Mode } from './../mode/mode'; import { RegisterCommand } from './commands/register'; import { SubstituteCommand } from './commands/substitute'; export abstract class CommandLine { public cursorIndex: number; public previousMode: Mode; protected historyIndex: number | undefined; private savedText: string; constructor(text: string, previousMode: Mode) { this.cursorIndex = text.length; this.historyIndex = this.getHistory().get().length; this.previousMode = previousMode; this.savedText = text; } /** * @returns the text to be displayed in the status bar */ public abstract display(cursorChar: string): string; /** * What the user has typed, minus any prefix, etc. */ public abstract get text(): string; public abstract set text(text: string); /** * @returns the SearchState associated with this CommandLine, if one exists * * This applies to `/`, `:s`, `:g`, `:v`, etc. */ public abstract getSearchState(): SearchState | undefined; public abstract getHistory(): HistoryFile; public abstract getDecorations(vimState: VimState): SearchDecorations | undefined; /** * Called when `` is pressed */ public abstract run(vimState: VimState): Promise; /** * Called when `` is pressed */ public abstract escape(vimState: VimState): Promise; /** * Called when `` is pressed */ public abstract ctrlF(vimState: VimState): Promise; public historyBack(): void { if (this.historyIndex === 0) { return; } const historyEntries = this.getHistory().get(); if (this.historyIndex === undefined) { this.historyIndex = historyEntries.length - 1; this.savedText = this.text; } else if (this.historyIndex > 0) { this.historyIndex--; } this.text = historyEntries[this.historyIndex]; this.cursorIndex = this.text.length; } public historyForward(): void { if (this.historyIndex === undefined) { return; } const historyEntries = this.getHistory().get(); if (this.historyIndex === historyEntries.length - 1) { this.historyIndex = undefined; this.text = this.savedText; } else if (this.historyIndex < historyEntries.length - 1) { this.historyIndex++; this.text = historyEntries[this.historyIndex]; } this.cursorIndex = this.text.length; } /** * Called when `` is pressed */ public async backspace(vimState: VimState): Promise { if (this.cursorIndex === 0) { if (this.text.length === 0) { await this.escape(vimState); } return; } this.text = this.text.slice(0, this.cursorIndex - 1) + this.text.slice(this.cursorIndex); this.cursorIndex = Math.max(this.cursorIndex - 1, 0); } /** * Called when `` is pressed */ public async delete(vimState: VimState): Promise { if (this.cursorIndex === this.text.length) { return this.backspace(vimState); } this.text = this.text.slice(0, this.cursorIndex) + this.text.slice(this.cursorIndex + 1); } /** * Called when `` is pressed */ public home(): void { this.cursorIndex = 0; } /** * Called when `` is pressed */ public end(): void { this.cursorIndex = this.text.length; } /** * Called when `` is pressed */ public wordLeft(): void { this.cursorIndex = getWordLeftInText(this.text, this.cursorIndex, WordType.Big) ?? 0; } /** * Called when `` is pressed */ public wordRight(): void { this.cursorIndex = getWordRightInText(this.text, this.cursorIndex, WordType.Big) ?? this.text.length; } /** * Called when `` is pressed */ public deleteWord(): void { const wordStart = getWordLeftInText(this.text, this.cursorIndex, WordType.Normal); if (wordStart !== undefined) { this.text = this.text.substring(0, wordStart).concat(this.text.slice(this.cursorIndex)); this.cursorIndex = this.cursorIndex - (this.cursorIndex - wordStart); } } /** * Called when `` is pressed */ public deleteToBeginning(): void { this.text = this.text.slice(this.cursorIndex); this.cursorIndex = 0; } public typeCharacter(char: string): void { const modifiedString = this.text.split(''); modifiedString.splice(this.cursorIndex, 0, char); this.text = modifiedString.join(''); this.cursorIndex += char.length; } } export class ExCommandLine extends CommandLine { static history: CommandLineHistory; static parser: Parser<{ lineRange: LineRange | undefined; command: ExCommand }>; static onSearch: (vimState: VimState) => Promise; public static async loadHistory(context: ExtensionContext): Promise { ExCommandLine.history = new CommandLineHistory(context); await ExCommandLine.history.load(); } // TODO: Make this stuff private? public autoCompleteIndex = 0; public autoCompleteItems: string[] = []; public preCompleteCharacterPos = 0; public preCompleteCommand = ''; private commandText: string; private lineRange: LineRange | undefined; private command: ExCommand | undefined; constructor(commandText: string, previousMode: Mode) { super(commandText, previousMode); this.commandText = commandText; this.text = commandText; this.previousMode = previousMode; } public display(cursorChar: string): string { return escapeCSSIcons( `:${this.text.substring(0, this.cursorIndex)}${cursorChar}${this.text.substring( this.cursorIndex, )}`, ); } public get text(): string { return this.commandText; } public set text(text: string) { this.commandText = text; try { // TODO: This eager parsing is costly, and if it's not `:s` or similar, don't need to parse the args at all const { lineRange, command } = ExCommandLine.parser.tryParse(this.commandText); this.lineRange = lineRange; this.command = command; } catch (err) { this.lineRange = undefined; this.command = undefined; } } public getSearchState(): SearchState | undefined { return undefined; } public getCommand(): ExCommand | undefined { return this.command; } public getDecorations(vimState: VimState): SearchDecorations | undefined { return this.command instanceof SubstituteCommand && vimState.currentMode === Mode.CommandlineInProgress ? this.command.getSubstitutionDecorations(vimState, this.lineRange) : undefined; } public getHistory(): HistoryFile { return ExCommandLine.history; } public async run(vimState: VimState): Promise { Logger.info(`Executing :${this.text}`); void ExCommandLine.history.add(this.text); this.historyIndex = ExCommandLine.history.get().length; if (!(this.command instanceof RegisterCommand)) { // TODO(jfields): Wait...why are we saving the `:` register as a RecordedState? const recState = new RecordedState(); recState.registerName = ':'; recState.commandList = this.text.split(''); Register.setReadonlyRegister(':', recState); } try { if (this.command === undefined) { // TODO: A bit gross: ExCommandLine.parser.tryParse(this.text); throw new Error(`Expected parsing ExCommand '${this.text}' to fail`); } const useNeovim = configuration.enableNeovim && this.command.neovimCapable(); if (useNeovim && vimState.nvim) { const { statusBarText, error } = await vimState.nvim.run(vimState, this.text); StatusBar.setText(vimState, statusBarText, error); } else { if (this.lineRange) { await this.command.executeWithRange(vimState, this.lineRange); } else { await this.command.execute(vimState); } } } catch (e) { if (e instanceof VimError) { if ( e.code === ErrorCode.NotAnEditorCommand && configuration.enableNeovim && vimState.nvim ) { const { statusBarText } = await vimState.nvim.run(vimState, this.text); StatusBar.setText(vimState, statusBarText, true); } else { StatusBar.setText(vimState, e.toString(), true); } } else { Logger.error(`Error executing cmd=${this.text}. err=${e}.`); } } // Update state if this command is repeatable via dot command. vimState.lastCommandDotRepeatable = this.command?.isRepeatableWithDot ?? false; } public async escape(vimState: VimState): Promise { await vimState.setCurrentMode(Mode.Normal); if (this.text.length > 0) { void ExCommandLine.history.add(this.text); } } public async ctrlF(vimState: VimState): Promise { void ExCommandLine.onSearch(vimState); } } export class SearchCommandLine extends CommandLine { public static history: SearchHistory; public static readonly previousSearchStates: SearchState[] = []; public static onSearch: (vimState: VimState, direction: SearchDirection) => Promise; /** * Shows the search history as a QuickPick (popup list) * * @returns The SearchState that was selected by the user, if there was one. */ public static async showSearchHistory(): Promise { const items = SearchCommandLine.previousSearchStates .slice() .reverse() .map((searchState) => { return { label: searchState.searchString, searchState, }; }); const item = await window.showQuickPick(items, { placeHolder: 'Vim search history', ignoreFocusOut: false, }); return item?.searchState; } public static async loadHistory(context: ExtensionContext): Promise { SearchCommandLine.history = new SearchHistory(context); SearchCommandLine.history .get() .forEach((val) => SearchCommandLine.previousSearchStates.push( new SearchState(SearchDirection.Forward, new Position(0, 0), val, undefined), ), ); } public static async addSearchStateToHistory(searchState: SearchState) { const prevSearchString = SearchCommandLine.previousSearchStates.at(-1)?.searchString; // Store this search if different than previous if (searchState.searchString !== prevSearchString) { SearchCommandLine.previousSearchStates.push(searchState); if (SearchCommandLine.history !== undefined) { await SearchCommandLine.history.add(searchState.searchString); } } // Make sure search history does not exceed configuration option if (SearchCommandLine.previousSearchStates.length > configuration.history) { SearchCommandLine.previousSearchStates.splice(0, 1); } } /** * Keeps the state of the current match, i.e. the match to which the cursor moves when the search is executed. * Incremented / decremented by \ or \ in SearchInProgress mode. * Resets to 0 if the search string becomes empty. * * @see {@link getCurrentMatchRelativeIndex} */ private currentMatchDisplacement: number = 0; private searchState: SearchState; constructor(vimState: VimState, searchString: string, direction: SearchDirection) { super(searchString, vimState.currentMode); this.searchState = new SearchState(direction, vimState.cursorStopPosition, searchString); } public display(cursorChar: string): string { return escapeCSSIcons( `${this.searchState.direction === SearchDirection.Forward ? '/' : '?'}${this.text.substring( 0, this.cursorIndex, )}${cursorChar}${this.text.substring(this.cursorIndex)}`, ); } public get text(): string { return this.searchState.searchString; } public set text(text: string) { this.searchState.searchString = text; if (text === '') { this.currentMatchDisplacement = 0; } } public getSearchState(): SearchState { return this.searchState; } public getHistory(): HistoryFile { return SearchCommandLine.history; } /** * @returns the index of the current match, relative to the next match. */ private getCurrentMatchRelativeIndex(vimState: VimState): number { const count = vimState.recordedState.count || 1; return count - 1 + this.currentMatchDisplacement * count; } /** * @returns The start of the current match range (after applying the search offset) and its rank in the document's matches */ public getCurrentMatchPosition(vimState: VimState): IndexedPosition | undefined { return this.searchState.getNextSearchMatchPosition( vimState, vimState.cursorStopPosition, SearchDirection.Forward, this.getCurrentMatchRelativeIndex(vimState), ); } /** * @returns The current match range and its rank in the document's matches * * NOTE: This method does not take the search offset into account */ public getCurrentMatchRange(vimState: VimState): IndexedRange | undefined { return this.searchState.getNextSearchMatchRange( vimState, vimState.cursorStopPosition, SearchDirection.Forward, this.getCurrentMatchRelativeIndex(vimState), ); } public getDecorations(vimState: VimState): SearchDecorations | undefined { return getDecorationsForSearchMatchRanges( this.searchState.getMatchRanges(vimState), vimState.document, configuration.incsearch && vimState.currentMode === Mode.SearchInProgressMode ? this.getCurrentMatchRange(vimState)?.index : undefined, ); } public async run(vimState: VimState): Promise { // Repeat the previous search if no new string is entered if (this.text === '') { const prevSearchString = SearchCommandLine.previousSearchStates.at(-1)?.searchString; if (prevSearchString !== undefined) { this.text = prevSearchString; } } Logger.info(`Searching for ${this.text}`); this.cursorIndex = 0; Register.setReadonlyRegister('/', this.text); void SearchCommandLine.addSearchStateToHistory(this.searchState); globalState.hl = true; if (this.searchState.getMatchRanges(vimState).length === 0) { StatusBar.displayError(vimState, VimError.PatternNotFound(this.text)); return; } const currentMatch = this.getCurrentMatchPosition(vimState); if (currentMatch === undefined) { StatusBar.displayError( vimState, this.searchState.direction === SearchDirection.Backward ? VimError.SearchHitTop(this.text) : VimError.SearchHitBottom(this.text), ); return; } vimState.cursorStopPosition = currentMatch.pos; reportSearch(currentMatch.index, this.searchState.getMatchRanges(vimState).length, vimState); } public async escape(vimState: VimState): Promise { vimState.cursorStopPosition = this.searchState.cursorStartPosition; globalState.searchState = SearchCommandLine.previousSearchStates.at(-1); if (vimState.modeData.mode === Mode.SearchInProgressMode) { const offset = vimState.editor.visibleRanges[0].start.line - vimState.modeData.firstVisibleLineBeforeSearch; scrollView(vimState, offset); } await vimState.setCurrentMode(this.previousMode); if (this.text.length > 0) { void SearchCommandLine.addSearchStateToHistory(this.searchState); } } public async ctrlF(vimState: VimState): Promise { await SearchCommandLine.onSearch(vimState, this.searchState.direction); } /** * Called when or is pressed during SearchInProgress mode */ public advanceCurrentMatch(vimState: VimState, direction: SearchDirection): void { // always moves forward in the document, and always moves back, regardless of search direction. // To compensate, multiply the desired direction by the searchState's direction, so that // effectiveDirection == direction * (searchState.direction)^2 == direction. this.currentMatchDisplacement += this.searchState.direction * direction; // With nowrapscan, / shouldn't do anything if it would mean advancing past the last reachable match in the buffer. // We account for this by checking whether getCurrentMatchRange returns undefined once this.currentMatchDisplacement is advanced. // If it does, we undo the change to this.currentMatchDisplacement before exiting, making this command a noop. if (!configuration.wrapscan && !this.getCurrentMatchRange(vimState)) { this.currentMatchDisplacement -= this.searchState.direction * direction; } } } ================================================ FILE: src/cmd_line/commands/ascii.ts ================================================ import { CommandUnicodeName } from '../../actions/commands/actions'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; export class AsciiCommand extends ExCommand { async execute(vimState: VimState): Promise { await new CommandUnicodeName().exec(vimState.cursorStopPosition, vimState); } } ================================================ FILE: src/cmd_line/commands/bang.ts ================================================ import { all, Parser } from 'parsimmon'; import { PositionDiff } from '../../common/motion/position'; import { VimState } from '../../state/vimState'; import { externalCommand } from '../../util/externalCommand'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; export interface IBangCommandArguments { command: string; } export class BangCommand extends ExCommand { public static readonly argParser: Parser = all.map( (command) => new BangCommand({ command, }), ); protected _arguments: IBangCommandArguments; constructor(args: IBangCommandArguments) { super(); this._arguments = args; } public override neovimCapable(): boolean { return true; } private getReplaceDiff(text: string): PositionDiff { const lines = text.split('\n'); const numNewlines = lines.length - 1; const check = lines[0].match(/^\s*/); const numWhitespace = check ? check[0].length : 0; return PositionDiff.exactCharacter({ lineOffset: -numNewlines, character: numWhitespace, }); } async execute(vimState: VimState): Promise { await externalCommand.run(this._arguments.command); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { const resolvedRange = range.resolveToRange(vimState); // pipe in stdin from lines in range const input = vimState.document.getText(resolvedRange); const output = await externalCommand.run(this._arguments.command, input); // place cursor at the start of the replaced text and first non-whitespace character const diff = this.getReplaceDiff(output); vimState.recordedState.transformer.replace(resolvedRange, output, diff); } } ================================================ FILE: src/cmd_line/commands/breakpoints.ts ================================================ import { all, alt, eof, optWhitespace, regexp, seqObj, // eslint-disable-next-line id-denylist string, succeed, whitespace, } from 'parsimmon'; import * as path from 'path'; import * as vscode from 'vscode'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { fileNameParser, numberParser } from '../../vimscript/parserUtils'; function isSourceBreakpoint(b: vscode.Breakpoint): b is vscode.SourceBreakpoint { return (b as vscode.SourceBreakpoint).location !== undefined; } function isFunctionBreakpoint(b: vscode.Breakpoint): b is vscode.FunctionBreakpoint { return (b as vscode.FunctionBreakpoint).functionName !== undefined; } /** * Add Breakpoint Command */ type AddBreakpointHere = { type: 'here' }; type AddBreakpointFile = { type: 'file'; line: number; file: string }; type AddBreakpointFunction = { type: 'func'; function: string }; type AddBreakpointExpr = { type: 'expr'; expr: string }; type AddBreakpoint = | AddBreakpointHere | AddBreakpointFile | AddBreakpointFunction | AddBreakpointExpr; class AddBreakpointCommand extends ExCommand { private readonly addBreakpoint: AddBreakpoint; constructor(addBreakpoint: AddBreakpoint) { super(); this.addBreakpoint = addBreakpoint; } async execute(vimState: VimState): Promise { if (this.addBreakpoint.type === 'here') { const location = new vscode.Location(vimState.document.uri, vimState.cursorStartPosition); return vscode.debug.addBreakpoints([new vscode.SourceBreakpoint(location)]); } else if (this.addBreakpoint.type === 'file') { let file: vscode.Uri; if (this.addBreakpoint.file === '') { file = vimState.document.uri; } else { const workspaceFolder = vscode.workspace.getWorkspaceFolder(vimState.document.uri)?.uri ?? vscode.Uri.file('./'); file = vscode.Uri.joinPath(workspaceFolder, this.addBreakpoint.file); } const location = new vscode.Location( file, new vscode.Position(this.addBreakpoint.line - 1, 0), ); return vscode.debug.addBreakpoints([new vscode.SourceBreakpoint(location)]); } else if (this.addBreakpoint.type === 'func') { return vscode.debug.addBreakpoints([ new vscode.FunctionBreakpoint(this.addBreakpoint.function), ]); } else if (this.addBreakpoint.type === 'expr') { const location = new vscode.Location(vimState.document.uri, vimState.cursorStartPosition); return vscode.debug.addBreakpoints([ new vscode.SourceBreakpoint(location, undefined, this.addBreakpoint.expr), ]); } } } /** * Delete Breakpoint Command */ type DelBreakpointById = { type: 'byId'; id: number }; type DelAllBreakpoints = { type: 'all' }; type DelBreakpointFunction = { type: 'func'; function: string }; type DelBreakpointFile = { type: 'file'; line: number; file: string }; type DelBreakpointHere = { type: 'here' }; type DelBreakpoint = | DelBreakpointById | DelAllBreakpoints | DelBreakpointFunction | DelBreakpointFile | DelBreakpointHere; class DeleteBreakpointCommand extends ExCommand { private readonly delBreakpoint: DelBreakpoint; constructor(delBreakpoint: DelBreakpoint) { super(); this.delBreakpoint = delBreakpoint; } async execute(vimState: VimState): Promise { if (this.delBreakpoint.type === 'byId') { return vscode.debug.removeBreakpoints( vscode.debug.breakpoints.slice(this.delBreakpoint.id - 1, 1), ); } else if (this.delBreakpoint.type === 'all') { return vscode.debug.removeBreakpoints(vscode.debug.breakpoints); } else if (this.delBreakpoint.type === 'file') { let reqUri: vscode.Uri; if (this.delBreakpoint.file === '') { reqUri = vimState.document.uri; } else { const workspaceFolder = vscode.workspace.getWorkspaceFolder(vimState.document.uri)?.uri ?? vscode.Uri.file('./'); reqUri = vscode.Uri.joinPath(workspaceFolder, this.delBreakpoint.file); } const reqLine = this.delBreakpoint.line - 1; const breakpoint = vscode.debug.breakpoints .filter(isSourceBreakpoint) .find( (b) => b.location.uri.toString() === reqUri.toString() && b.location.range.start.line === reqLine, ); if (breakpoint) return vscode.debug.removeBreakpoints([breakpoint]); } else if (this.delBreakpoint.type === 'func') { const functionName = this.delBreakpoint.function; const breakpoint = vscode.debug.breakpoints .filter(isFunctionBreakpoint) .filter((b) => b.functionName === functionName); if (breakpoint) return vscode.debug.removeBreakpoints(breakpoint); } else if (this.delBreakpoint.type === 'here') { const location = new vscode.Location(vimState.document.uri, vimState.cursorStartPosition); const distFromLocationCharacter = (b: vscode.SourceBreakpoint) => Math.abs(b.location.range.start.character - location.range.start.character); const breakpoint = vscode.debug.breakpoints .filter(isSourceBreakpoint) .filter( (b) => b.location.uri.toString() === location.uri.toString() && b.location.range.start.line === location.range.start.line, ) .sort((a, b) => distFromLocationCharacter(a) - distFromLocationCharacter(b))[0]; if (breakpoint) return vscode.debug.removeBreakpoints([breakpoint]); } } } /** * List Breakpoints Command */ class ListBreakpointsCommand extends ExCommand { async execute(vimState: VimState): Promise { const breakpoints = vscode.debug.breakpoints; type BreakpointQuickPick = { breakpointId: string } & vscode.QuickPickItem; const lines = breakpoints.map((b, i): BreakpointQuickPick => { const { id, enabled, condition } = b; let label = ''; label += `#${i + 1}\t`; label += enabled ? '$(circle-filled)\t' : '$(circle-outline)\t'; label += condition ? '$(debug-breakpoint-conditional)\t' : '\t'; if (isSourceBreakpoint(b)) label += `${path.basename(b.location.uri.fsPath)}:${b.location.range.start.line + 1}`; if (isFunctionBreakpoint(b)) label += `$(debug-breakpoint-function)${b.functionName}`; return { label, breakpointId: id, }; }); await vscode.window.showQuickPick(lines).then(async (selected) => { if (selected) { const id = selected.breakpointId; const breakpoint = breakpoints.find((b) => b.id === id); if (breakpoint && isSourceBreakpoint(breakpoint)) { const pos = breakpoint.location.range.start; await vscode.window.showTextDocument(breakpoint.location.uri, { selection: new vscode.Range(pos, pos), }); } } }); } } export class Breakpoints { public static readonly argParsers = { add: whitespace .then( alt( // here seqObj(['type', string('here')], optWhitespace), // file seqObj( ['type', string('file')], ['line', optWhitespace.then(numberParser).fallback(1)], ['file', optWhitespace.then(fileNameParser).fallback('')], ), // func seqObj( ['type', string('func')], optWhitespace.then(numberParser).fallback(1), // we don't support line numbers in function names, but Vim does, so we'll allow it. ['function', optWhitespace.then(regexp(/\S+/))], ), // expr seqObj(['type', string('expr')], ['expr', optWhitespace.then(all)]), ), ) .or( // without arg eof.result({ type: 'here' }), ) .map((a: AddBreakpoint) => new AddBreakpointCommand(a)), del: whitespace .then( alt( // here seqObj(['type', string('here')], optWhitespace), // file seqObj( ['type', string('file')], ['line', optWhitespace.then(numberParser).fallback(1)], ['file', optWhitespace.then(fileNameParser).fallback('')], ), // func seqObj( ['type', string('func')], optWhitespace.then(numberParser).fallback(1), // we don't support line numbers in function names, but Vim does, so we'll allow it. ['function', optWhitespace.then(regexp(/\S+/))], ), // all string('*').then(optWhitespace).result({ type: 'all' }), // by number numberParser.map((n) => ({ type: 'byId', id: n })), ), ) .or( // without arg eof.result({ type: 'here' }), ) .map((a: DelBreakpoint) => new DeleteBreakpointCommand(a)), list: succeed(new ListBreakpointsCommand()), }; } ================================================ FILE: src/cmd_line/commands/bufferDelete.ts ================================================ import { alt, optWhitespace, Parser, seq, whitespace } from 'parsimmon'; import * as vscode from 'vscode'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { findTabInActiveTabGroup } from '../../util/util'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser, fileNameParser, numberParser } from '../../vimscript/parserUtils'; interface IBufferDeleteCommandArguments { bang: boolean; buffers: Array; } // // Implements :bd // http://vimdoc.sourceforge.net/htmldoc/windows.html#buffers // export class BufferDeleteCommand extends ExCommand { public static readonly argParser: Parser = seq( bangParser.skip(optWhitespace), alt(numberParser, fileNameParser).sepBy(whitespace), ).map(([bang, buffers]) => new BufferDeleteCommand({ bang, buffers })); public readonly arguments: IBufferDeleteCommandArguments; constructor(args: IBufferDeleteCommandArguments) { super(); this.arguments = args; } async execute(vimState: VimState): Promise { if (vimState.document.isDirty && !this.arguments.bang) { throw VimError.NoWriteSinceLastChange(); } const activeBuffer = vscode.window.tabGroups.activeTabGroup.tabs.findIndex((t) => t.isActive) + 1; if (this.arguments.buffers.length === 0) { this.arguments.buffers = [activeBuffer]; } let deletedBuffers = 0; for (let buffer of this.arguments.buffers) { if (typeof buffer === 'string') { const [idx, tab] = findTabInActiveTabGroup(buffer); buffer = idx + 1; } if (buffer < 1) { throw VimError.PositiveCountRequired(); } if (buffer > vscode.window.tabGroups.activeTabGroup.tabs.length) { continue; } if (buffer !== activeBuffer) { try { await vscode.commands.executeCommand('workbench.action.openEditorAtIndex', buffer - 1); } catch (e) { continue; } } if (this.arguments.bang) { await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); } else { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); } ++deletedBuffers; } if (deletedBuffers === 0) { throw VimError.NoBuffersDeleted(); } } // TODO: executeWithRange } ================================================ FILE: src/cmd_line/commands/change.ts ================================================ import * as vscode from 'vscode'; // eslint-disable-next-line id-denylist import { Parser, alt, any, optWhitespace, seq, whitespace } from 'parsimmon'; import { Position } from 'vscode'; import { Mode } from '../../mode/mode'; import { Register, RegisterMode } from '../../register/register'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; import { numberParser } from '../../vimscript/parserUtils'; export interface IChangeCommandArguments { register?: string; count?: number; } export class ChangeCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace.then( alt( numberParser.map((count) => { return { register: undefined, count }; }), // eslint-disable-next-line id-denylist seq(any.fallback(undefined), whitespace.then(numberParser).fallback(undefined)).map( ([register, count]) => { return { register, count }; }, ), ).map( ({ register, count }) => new ChangeCommand({ register, count, }), ), ); private readonly arguments: IChangeCommandArguments; constructor(args: IChangeCommandArguments) { super(); this.arguments = args; } public override neovimCapable(): boolean { return true; } /** * Deletes text between `startLine` and `endLine`, inclusive. * Puts the cursor at the start of the line where the deleted range was * Then enters insert mode */ private changeRange(startLine: number, endLine: number, vimState: VimState): void { const start = new Position(startLine, 0); const end = new Position(endLine + 1, 0); const range = new vscode.Range(start, end); const text = vimState.document.getText(range); vimState.recordedState.transformer.addTransformation({ type: 'replaceText', text: '\n', range, manuallySetCursorPositions: true, }); vimState.cursorStopPosition = start; if (this.arguments.register) { vimState.recordedState.registerName = this.arguments.register; } vimState.currentRegisterMode = RegisterMode.LineWise; Register.put(vimState, text, 0, true); } async execute(vimState: VimState): Promise { const linesToRemove = this.arguments.count ?? 1; // :c[hange][cnt] changes [cnt] lines const startLine = vimState.cursorStartPosition.line; const endLine = startLine + (linesToRemove - 1); this.changeRange(startLine, endLine, vimState); // Enter insert mode await vimState.setCurrentMode(Mode.Insert); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { /** * If a [cnt] and [range] is specified (e.g. :.+2c3), :change * the end of the [range]. * Ex. if two lines are VisualLine hightlighted, :<,>c3 will :c3 * from the end of the selected lines */ const { start, end } = range.resolve(vimState); if (this.arguments.count) { vimState.cursorStartPosition = new Position(end, 0); await this.execute(vimState); return; } this.changeRange(start, end, vimState); // Enter insert mode await vimState.setCurrentMode(Mode.Insert); } } ================================================ FILE: src/cmd_line/commands/close.ts ================================================ import { Parser } from 'parsimmon'; import * as vscode from 'vscode'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser } from '../../vimscript/parserUtils'; // // Implements :close // http://vimdoc.sourceforge.net/htmldoc/windows.html#:close // export class CloseCommand extends ExCommand { public static readonly argParser: Parser = bangParser.map( (bang) => new CloseCommand(bang), ); public readonly bang: boolean; constructor(bang: boolean) { super(); this.bang = bang; } async execute(vimState: VimState): Promise { if (vimState.document.isDirty && !this.bang) { throw VimError.NoWriteSinceLastChange(); } if (vscode.window.visibleTextEditors.length === 1) { throw VimError.CannotCloseLastWindow(); } const oldViewColumn = vimState.editor.viewColumn; await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); if ( vscode.window.activeTextEditor !== undefined && vscode.window.activeTextEditor.viewColumn === oldViewColumn ) { await vscode.commands.executeCommand('workbench.action.previousEditor'); } } } ================================================ FILE: src/cmd_line/commands/copy.ts ================================================ import { optWhitespace, Parser } from 'parsimmon'; import { Position, Range } from 'vscode'; import { PositionDiff } from '../../common/motion/position'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; import { Address, LineRange } from '../../vimscript/lineRange'; export class CopyCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(Address.parser.fallback(undefined)) .map((address) => new CopyCommand(address)); private address?: Address; constructor(address?: Address) { super(); this.address = address; } public override neovimCapable(): boolean { return true; } private copyLines(vimState: VimState, sourceStart: number, sourceEnd: number) { const dest = this.address?.resolve(vimState, 'left', false); if (dest === undefined || dest < -1 || dest > vimState.document.lineCount) { StatusBar.displayError(vimState, VimError.InvalidAddress()); return; } if (sourceEnd < sourceStart) { [sourceStart, sourceEnd] = [sourceEnd, sourceStart]; } const copiedText = vimState.document.getText( new Range(new Position(sourceStart, 0), new Position(sourceEnd, 0).getLineEnd()), ); let text: string; let position: Position; if (dest === -1) { text = copiedText + '\n'; position = new Position(0, 0); } else { text = '\n' + copiedText; position = new Position(dest, 0).getLineEnd(); } const lines = copiedText.split('\n'); const cursorPosition = new Position( Math.max(dest + lines.length, 0), lines[lines.length - 1].match(/\S/)?.index ?? 0, ); vimState.recordedState.transformer.insert( position, text, PositionDiff.exactPosition(cursorPosition), ); } public async execute(vimState: VimState): Promise { const line = vimState.cursor.stop.line; this.copyLines(vimState, line, line); } public override async executeWithRange(vimState: VimState, range: LineRange): Promise { const { start, end } = range.resolve(vimState); this.copyLines(vimState, start, end); } } ================================================ FILE: src/cmd_line/commands/delete.ts ================================================ import * as vscode from 'vscode'; // eslint-disable-next-line id-denylist import { Parser, alt, any, optWhitespace, seq, whitespace } from 'parsimmon'; import { Position } from 'vscode'; import { Register, RegisterMode } from '../../register/register'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; import { numberParser } from '../../vimscript/parserUtils'; export interface IDeleteCommandArguments { register?: string; count?: number; } export class DeleteCommand extends ExCommand { // TODO: this is copy-pasted from `:y[ank]` public static readonly argParser: Parser = optWhitespace.then( alt( numberParser.map((count) => { return { register: undefined, count }; }), // eslint-disable-next-line id-denylist seq(any.fallback(undefined), whitespace.then(numberParser).fallback(undefined)).map( ([register, count]) => { return { register, count }; }, ), ).map( ({ register, count }) => new DeleteCommand({ register, count, }), ), ); private readonly arguments: IDeleteCommandArguments; constructor(args: IDeleteCommandArguments) { super(); this.arguments = args; } public override neovimCapable(): boolean { return true; } /** * Deletes text between `startLine` and `endLine`, inclusive. * Puts the cursor at the start of the line where the deleted range was. */ private deleteRange(startLine: number, endLine: number, vimState: VimState): void { let start = new Position(startLine, 0); let end = new Position(endLine, 0).getLineEnd(); if (endLine < vimState.document.lineCount - 1) { end = end.getRightThroughLineBreaks(); } else if (startLine > 0) { start = start.getLeftThroughLineBreaks(); } const range = new vscode.Range(start, end); const text = vimState.document .getText(range) // Remove leading or trailing newline .replace(/^\r?\n/, '') .replace(/\r?\n$/, ''); vimState.recordedState.transformer.addTransformation({ type: 'deleteRange', range, manuallySetCursorPositions: true, }); vimState.cursorStopPosition = start.getLineBegin(); if (this.arguments.register) { vimState.recordedState.registerName = this.arguments.register; } vimState.currentRegisterMode = RegisterMode.LineWise; Register.put(vimState, text, 0, true); } async execute(vimState: VimState): Promise { const linesToRemove = this.arguments.count ?? 1; // :d[elete][cnt] removes [cnt] lines const startLine = vimState.cursorStartPosition.line; const endLine = startLine + (linesToRemove - 1); this.deleteRange(startLine, endLine, vimState); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { /** * If a [cnt] and [range] is specified (e.g. :.+2d3), :delete [cnt] is called from * the end of the [range]. * Ex. if two lines are VisualLine highlighted, :<,>d3 will :d3 * from the end of the selected lines. */ const { start, end } = range.resolve(vimState); if (this.arguments.count) { vimState.cursorStartPosition = new Position(end, 0); await this.execute(vimState); return; } this.deleteRange(start, end, vimState); } } ================================================ FILE: src/cmd_line/commands/digraph.ts ================================================ import * as vscode from 'vscode'; // eslint-disable-next-line id-denylist import { any, Parser, seq, whitespace } from 'parsimmon'; import { DefaultDigraphs } from '../../actions/commands/digraphs'; import { Digraph } from '../../configuration/iconfiguration'; import { VimState } from '../../state/vimState'; import { TextEditor } from '../../textEditor'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser, numberParser } from '../../vimscript/parserUtils'; import { configuration } from './../../configuration/configuration'; export interface IDigraphsCommandArguments { bang: boolean; newDigraph: [string, string, number[]] | undefined; } interface DigraphQuickPickItem extends vscode.QuickPickItem { charCodes: number[]; } export class DigraphsCommand extends ExCommand { public static readonly argParser: Parser = seq( bangParser, whitespace.then(seq(any, any, whitespace.then(numberParser).atLeast(1))).fallback(undefined), ).map(([bang, newDigraph]) => new DigraphsCommand({ bang, newDigraph })); private readonly arguments: IDigraphsCommandArguments; constructor(args: IDigraphsCommandArguments) { super(); this.arguments = args; } private makeQuickPicks(digraphs: Array<[string, Digraph]>): DigraphQuickPickItem[] { return digraphs.map(([shortcut, [charDesc, charCodes]]) => { if (!Array.isArray(charCodes)) { charCodes = [charCodes]; } return { label: shortcut, description: `${charDesc} (user)`, charCodes, }; }); } async execute(vimState: VimState): Promise { if (this.arguments.newDigraph) { const digraph = this.arguments.newDigraph[0] + this.arguments.newDigraph[1]; const charCodes = this.arguments.newDigraph[2]; DefaultDigraphs.set(digraph, [String.fromCharCode(...charCodes), charCodes]); } else { const digraphKeyAndContent = this.makeQuickPicks( Object.entries(configuration.digraphs), ).concat(this.makeQuickPicks([...DefaultDigraphs.entries()])); void vscode.window.showQuickPick(digraphKeyAndContent).then(async (val) => { if (val) { const char = String.fromCharCode(...val.charCodes); await TextEditor.insert(vimState.editor, char); } }); } } } ================================================ FILE: src/cmd_line/commands/echo.ts ================================================ import { all, optWhitespace, Parser, seq } from 'parsimmon'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; import { displayValue } from '../../vimscript/expression/displayValue'; import { EvaluationContext } from '../../vimscript/expression/evaluate'; import { expressionParser } from '../../vimscript/expression/parser'; import { Expression } from '../../vimscript/expression/types'; export class EchoCommand extends ExCommand { public static argParser(echoArgs: { sep: string; error: boolean }): Parser { return optWhitespace .then(seq(expressionParser.sepBy(optWhitespace), all)) .map(([expressions, trailing]) => { trailing = trailing.trimStart(); if (trailing) { throw VimError.InvalidExpression(trailing); } return new EchoCommand(echoArgs, expressions); }); } private sep: string; private error: boolean; private expressions: Expression[]; constructor(args: { sep: string; error: boolean }, expressions: Expression[]) { super(); this.sep = args.sep; this.error = args.error; this.expressions = expressions; } public override neovimCapable(): boolean { return true; } public async execute(vimState: VimState): Promise { const ctx = new EvaluationContext(vimState); const values = this.expressions.map((x) => ctx.evaluate(x)); const message = values.map((v) => displayValue(v)).join(this.sep); StatusBar.setText(vimState, message, this.error); } } ================================================ FILE: src/cmd_line/commands/eval.ts ================================================ import { all, optWhitespace, Parser, seq } from 'parsimmon'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { EvaluationContext } from '../../vimscript/expression/evaluate'; import { expressionParser, functionCallParser } from '../../vimscript/expression/parser'; import { Expression } from '../../vimscript/expression/types'; export class EvalCommand extends ExCommand { public static argParser: Parser = optWhitespace .then(seq(expressionParser.fallback(undefined), all)) .map(([expression, trailing]) => { trailing = trailing.trim(); if (expression === undefined) { throw VimError.InvalidExpression(trailing); } if (trailing) { throw VimError.TrailingCharacters(trailing); } return new EvalCommand(expression); }); private expression: Expression; constructor(expression: Expression) { super(); this.expression = expression; } public async execute(vimState: VimState): Promise { const ctx = new EvaluationContext(vimState); ctx.evaluate(this.expression); } } export class CallCommand extends ExCommand { public static argParser: Parser = optWhitespace .then(functionCallParser) .map((call) => new CallCommand(call)); private expression: Expression; private constructor(funcCall: Expression) { super(); this.expression = funcCall; } public async execute(vimState: VimState): Promise { const ctx = new EvaluationContext(vimState); ctx.evaluate(this.expression); } } ================================================ FILE: src/cmd_line/commands/explore.ts ================================================ import { commands } from 'vscode'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; export class ExploreCommand extends ExCommand { async execute(vimState: VimState): Promise { await commands.executeCommand('workbench.view.explorer'); } } ================================================ FILE: src/cmd_line/commands/file.ts ================================================ import { optWhitespace, seq } from 'parsimmon'; import { doesFileExist } from 'platform/fs'; import * as vscode from 'vscode'; import { Position } from 'vscode'; import { VimState } from '../../state/vimState'; import { Logger } from '../../util/logger'; import { getPathDetails, resolveUri } from '../../util/path'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser, FileCmd, fileCmdParser, fileNameParser, FileOpt, fileOptParser, } from '../../vimscript/parserUtils'; // TODO: // eslint-disable-next-line @typescript-eslint/no-require-imports import untildify = require('untildify'); export enum FilePosition { NewWindowVerticalSplit, NewWindowHorizontalSplit, } export type IFileCommandArguments = | { name: 'edit'; bang: boolean; opt: FileOpt; cmd?: FileCmd; file?: string; createFileIfNotExists?: boolean; } | { name: 'enew'; bang: boolean; createFileIfNotExists?: boolean; } | { name: 'new' | 'vnew' | 'split' | 'vsplit'; opt: FileOpt; cmd?: FileCmd; file?: string; createFileIfNotExists?: boolean; }; // TODO: This is a hack to get this to work in the short term with new arg parsing logic. type LegacyArgs = { file?: string; bang?: boolean; position?: FilePosition; cmd?: FileCmd; createFileIfNotExists?: boolean; }; function getLegacyArgs(args: IFileCommandArguments): LegacyArgs { if (args.name === 'edit') { return { file: args.file, bang: args.bang, cmd: args.cmd, createFileIfNotExists: true }; } else if (args.name === 'enew') { return { bang: args.bang, createFileIfNotExists: true }; } else if (args.name === 'new') { return { file: args.file, position: FilePosition.NewWindowHorizontalSplit, createFileIfNotExists: true, }; } else if (args.name === 'vnew') { return { file: args.file, position: FilePosition.NewWindowVerticalSplit, createFileIfNotExists: true, }; } else if (args.name === 'split') { return { file: args.file, position: FilePosition.NewWindowHorizontalSplit, // only to create if file arg is specified createFileIfNotExists: args.file !== undefined, }; } else if (args.name === 'vsplit') { return { file: args.file, position: FilePosition.NewWindowVerticalSplit, // only to create if file arg is specified createFileIfNotExists: args.file !== undefined, }; } else { throw new Error(`Unexpected FileCommand.arguments.name: ${args.name}`); } } export class FileCommand extends ExCommand { // TODO: There's a lot of duplication here // TODO: These `optWhitespace` calls should be `whitespace` public static readonly argParsers = { edit: seq( bangParser, optWhitespace.then(fileOptParser).fallback([]), optWhitespace.then(fileCmdParser).fallback(undefined), optWhitespace.then(fileNameParser).fallback(undefined), ).map(([bang, opt, cmd, file]) => new FileCommand({ name: 'edit', bang, opt, cmd, file })), enew: bangParser.map((bang) => new FileCommand({ name: 'enew', bang })), new: seq( optWhitespace.then(fileOptParser).fallback([]), optWhitespace.then(fileCmdParser).fallback(undefined), optWhitespace.then(fileNameParser).fallback(undefined), ).map(([opt, cmd, file]) => new FileCommand({ name: 'new', opt, cmd, file })), split: seq( optWhitespace.then(fileOptParser).fallback([]), optWhitespace.then(fileCmdParser).fallback(undefined), optWhitespace.then(fileNameParser).fallback(undefined), ).map(([opt, cmd, file]) => new FileCommand({ name: 'split', opt, cmd, file })), vnew: seq( optWhitespace.then(fileOptParser).fallback([]), optWhitespace.then(fileCmdParser).fallback(undefined), optWhitespace.then(fileNameParser).fallback(undefined), ).map(([opt, cmd, file]) => new FileCommand({ name: 'vnew', opt, cmd, file })), vsplit: seq( optWhitespace.then(fileOptParser).fallback([]), optWhitespace.then(fileCmdParser).fallback(undefined), optWhitespace.then(fileNameParser).fallback(undefined), ).map(([opt, cmd, file]) => new FileCommand({ name: 'vsplit', opt, cmd, file })), }; private readonly arguments: IFileCommandArguments; constructor(args: IFileCommandArguments) { super(); this.arguments = args; } async execute(vimState: VimState): Promise { const args = getLegacyArgs(this.arguments); if (args.bang) { await vscode.commands.executeCommand('workbench.action.files.revert'); return; } // Need to do this before the split since it loses the activeTextEditor const editorFileUri = vscode.window.activeTextEditor!.document.uri; const editorFilePath = editorFileUri.fsPath; // Do the split if requested let split = false; if (args.position === FilePosition.NewWindowVerticalSplit) { await vscode.commands.executeCommand('workbench.action.splitEditorRight'); split = true; } if (args.position === FilePosition.NewWindowHorizontalSplit) { await vscode.commands.executeCommand('workbench.action.splitEditorDown'); split = true; } const hidePreviousEditor = async () => { if (split === true) { await vscode.commands.executeCommand('workbench.action.previousEditor'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); } }; // No file was specified if (args.file === undefined) { if (args.createFileIfNotExists === true) { await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); await hidePreviousEditor(); } return; } // Only untidify when the currently open page and file completion is local if (args.file && editorFileUri.scheme === 'file') { args.file = untildify(args.file); } let fileUri = editorFileUri; // Using the empty string will request to open a file if (args.file === '') { // No file on split is fine and just return if (split === true) { return; } const fileList = await vscode.window.showOpenDialog({}); if (fileList && fileList.length > 0) { fileUri = fileList[0]; } } else { // remove file:// args.file = args.file.replace(/^file:\/\//, ''); // Using a filename, open or create the file const isRemote = !!vscode.env.remoteName; const { fullPath, path: p } = getPathDetails(args.file, editorFileUri, isRemote); // Only if the expanded path of the full path is different than // the currently opened window path if (fullPath !== editorFilePath) { const uriPath = resolveUri(fullPath, p.sep, editorFileUri, isRemote); if (uriPath === null) { // return if the path is invalid return; } let fileExists = await doesFileExist(uriPath); if (fileExists) { // If the file without the added ext exists fileUri = uriPath; } else { // if file does not exist // try to find it with the same extension as the current file const pathWithExt = fullPath + p.extname(editorFilePath); const uriPathWithExt = resolveUri(pathWithExt, p.sep, editorFileUri, isRemote); if (uriPathWithExt !== null) { fileExists = await doesFileExist(uriPathWithExt); if (fileExists) { // if the file with the added ext exists fileUri = uriPathWithExt; } } } // If both with and without ext path do not exist if (!fileExists) { if (args.createFileIfNotExists) { // Change the scheme to untitled to open an // untitled tab fileUri = uriPath.with({ scheme: 'untitled' }); } else { Logger.error(`${args.file} does not exist.`); return; } } } } const doc = await vscode.workspace.openTextDocument(fileUri); const lineNumber = args.cmd?.type === 'line_number' ? args.cmd.line : args.cmd?.type === 'last_line' ? doc.lineCount - 1 : undefined; const options: vscode.TextDocumentShowOptions = {}; if (lineNumber !== undefined && lineNumber >= 0) { const pos = new Position(lineNumber, 0); options.selection = new vscode.Range(pos, pos); } await vscode.window.showTextDocument(doc, options); await hidePreviousEditor(); } } ================================================ FILE: src/cmd_line/commands/fileInfo.ts ================================================ import { all, optWhitespace, Parser, seq } from 'parsimmon'; import { VimState } from '../../state/vimState'; import { reportFileInfo } from '../../util/statusBarTextUtils'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser } from '../../vimscript/parserUtils'; export class FileInfoCommand extends ExCommand { public static readonly argParser: Parser = seq( bangParser, optWhitespace.then(all), ).map(([bang, fileName]) => new FileInfoCommand({ bang, fileName })); private args: { bang: boolean; fileName?: string; }; private constructor(args: { bang: boolean; fileName?: string }) { super(); this.args = args; } async execute(vimState: VimState): Promise { // TODO: Use `this.args` reportFileInfo(vimState.cursor.start, vimState); } } ================================================ FILE: src/cmd_line/commands/goto.ts ================================================ import { optWhitespace, Parser } from 'parsimmon'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; import { numberParser } from '../../vimscript/parserUtils'; export class GotoCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(numberParser.fallback(undefined)) .map((count) => new GotoCommand(count)); private offset?: number; constructor(offset?: number) { super(); this.offset = offset; } private gotoOffset(vimState: VimState, offset: number) { vimState.cursorStopPosition = vimState.document.positionAt(offset); } public async execute(vimState: VimState): Promise { this.gotoOffset(vimState, this.offset ?? 0); } public override async executeWithRange(vimState: VimState, range: LineRange): Promise { if (this.offset === undefined) { this.offset = range.resolve(vimState)?.end ?? 0; } this.gotoOffset(vimState, this.offset); } } ================================================ FILE: src/cmd_line/commands/gotoLine.ts ================================================ import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; export class GotoLineCommand extends ExCommand { public async execute(vimState: VimState): Promise { return; } public override async executeWithRange(vimState: VimState, range: LineRange): Promise { vimState.cursorStartPosition = vimState.cursorStopPosition = vimState.cursorStopPosition .with({ line: range.resolve(vimState).end }) .obeyStartOfLine(vimState.document); } } ================================================ FILE: src/cmd_line/commands/grep.ts ================================================ import * as vscode from 'vscode'; import { optWhitespace, Parser, seq, whitespace } from 'parsimmon'; import { VimError } from '../../error'; import { ExCommand } from '../../vimscript/exCommand'; import { fileNameParser } from '../../vimscript/parserUtils'; import { Pattern, SearchDirection } from '../../vimscript/pattern'; // Still missing: // When a number is put before the command this is used // as the maximum number of matches to find. Use // ":1vimgrep pattern file" to find only the first. // Useful if you only want to check if there is a match // and quit quickly when it's found. // Without the 'j' flag Vim jumps to the first match. // With 'j' only the quickfix list is updated. // With the [!] any changes in the current buffer are // abandoned. interface IGrepCommandArguments { pattern: Pattern; files: string[]; } // Implements :grep // https://vimdoc.sourceforge.net/htmldoc/quickfix.html#:vimgrep export class GrepCommand extends ExCommand { // TODO: parse the pattern for flags to notify the user that they are not supported yet public static readonly argParser: Parser = optWhitespace.then( seq( Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }), fileNameParser.sepBy(whitespace), ).map(([pattern, files]) => new GrepCommand({ pattern, files })), ); public readonly arguments: IGrepCommandArguments; constructor(args: IGrepCommandArguments) { super(); this.arguments = args; } async execute(): Promise { const { pattern, files } = this.arguments; if (files.length === 0) { throw VimError.NoFileName(); } // There are other arguments that can be passed, but probably need to dig into the VSCode source code, since they are not listed in the API reference // https://code.visualstudio.com/api/references/commands // This link on the other hand has the commands and I used this as a reference // https://stackoverflow.com/questions/62251045/search-find-in-files-keybinding-can-take-arguments-workbench-view-search-can await vscode.commands.executeCommand('workbench.action.findInFiles', { query: pattern.patternString, filesToInclude: files.join(','), triggerSearch: true, isRegex: true, }); await vscode.commands.executeCommand('search.action.focusSearchList'); // TODO: Only if there's no [j] flag await vscode.commands.executeCommand('search.action.focusNextSearchResult'); } } ================================================ FILE: src/cmd_line/commands/history.ts ================================================ // eslint-disable-next-line id-denylist import { Parser, alt, optWhitespace, string } from 'parsimmon'; import { ShowCommandHistory, ShowSearchHistory } from '../../actions/commands/actions'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { nameAbbrevParser } from '../../vimscript/parserUtils'; import { SearchDirection } from '../../vimscript/pattern'; export enum HistoryCommandType { Cmd, Search, Expr, Input, Debug, All, } const historyTypeParser: Parser = alt( alt(nameAbbrevParser('c', 'md'), string(':')).result(HistoryCommandType.Cmd), alt(nameAbbrevParser('s', 'earch'), string('/')).result(HistoryCommandType.Search), alt(nameAbbrevParser('e', 'xpr'), string('=')).result(HistoryCommandType.Expr), alt(nameAbbrevParser('i', 'nput'), string('@')).result(HistoryCommandType.Input), alt(nameAbbrevParser('d', 'ebug'), string('>')).result(HistoryCommandType.Debug), nameAbbrevParser('a', 'll').result(HistoryCommandType.All), ); export interface IHistoryCommandArguments { type: HistoryCommandType; // TODO: :history can also accept a range } // http://vimdoc.sourceforge.net/htmldoc/cmdline.html#:history export class HistoryCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(historyTypeParser.fallback(HistoryCommandType.Cmd)) .map((type) => new HistoryCommand({ type })); private readonly arguments: IHistoryCommandArguments; constructor(args: IHistoryCommandArguments) { super(); this.arguments = args; } async execute(vimState: VimState): Promise { switch (this.arguments.type) { case HistoryCommandType.Cmd: await new ShowCommandHistory().exec(vimState.cursorStopPosition, vimState); break; case HistoryCommandType.Search: await new ShowSearchHistory(SearchDirection.Forward).exec( vimState.cursorStopPosition, vimState, ); break; // TODO: Implement these case HistoryCommandType.Expr: throw new Error('Not implemented'); case HistoryCommandType.Input: throw new Error('Not implemented'); case HistoryCommandType.Debug: throw new Error('Not implemented'); case HistoryCommandType.All: throw new Error('Not implemented'); } } } ================================================ FILE: src/cmd_line/commands/jumps.ts ================================================ import { QuickPickItem, Range, window } from 'vscode'; import { Jump } from '../../jumps/jump'; import { globalState } from '../../state/globalState'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; class JumpPickItem implements QuickPickItem { jump: Jump; label: string; description?: string; detail?: string; picked?: boolean; alwaysShow?: boolean; constructor(jump: Jump, idx: number) { this.jump = jump; this.label = jump.fileName; this.detail = `jump ${idx} line ${jump.position.line + 1} col ${jump.position.character}`; try { this.description = jump.document.lineAt(jump.position).text; } catch (e) { this.description = undefined; } } } export class JumpsCommand extends ExCommand { async execute(vimState: VimState): Promise { const jumpTracker = globalState.jumpTracker; if (jumpTracker.hasJumps) { const quickPickItems = jumpTracker.jumps.map((jump, idx) => new JumpPickItem(jump, idx)); const item = await window.showQuickPick(quickPickItems, { canPickMany: false, }); if (item && item.jump.document !== undefined) { void window.showTextDocument(item.jump.document, { selection: new Range(item.jump.position, item.jump.position), }); } } else { void window.showInformationMessage('No jumps available'); } } } export class ClearJumpsCommand extends ExCommand { async execute(vimState: VimState): Promise { const jumpTracker = globalState.jumpTracker; jumpTracker.clearJumps(); } } ================================================ FILE: src/cmd_line/commands/leftRightCenter.ts ================================================ import { optWhitespace, Parser } from 'parsimmon'; import { Range, TextLine } from 'vscode'; import { configuration } from '../../configuration/configuration'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { Address, LineRange } from '../../vimscript/lineRange'; import { numberParser } from '../../vimscript/parserUtils'; type LeftArgs = { indent: number; }; export class LeftCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(numberParser.fallback(0)) .map((indent) => new LeftCommand({ indent })); private args: LeftArgs; constructor(args: LeftArgs) { super(); this.args = args; } async execute(vimState: VimState): Promise { await this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' }))); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { const { start, end } = range.resolve(vimState); const lines: TextLine[] = []; for (let line = start; line <= end; line++) { lines.push(vimState.document.lineAt(line)); } vimState.recordedState.transformer.replace( new Range(lines[0].range.start, lines[lines.length - 1].range.end), lines .map( (line) => ' '.repeat(this.args.indent) + line.text.slice(line.firstNonWhitespaceCharacterIndex), ) .join('\n'), ); } } type RightArgs = { width: number; }; export class RightCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(numberParser.fallback(undefined)) .map((width) => new RightCommand({ width: width ?? configuration.textwidth })); private args: RightArgs; constructor(args: RightArgs) { super(); this.args = args; } async execute(vimState: VimState): Promise { await this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' }))); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { const { start, end } = range.resolve(vimState); const lines: TextLine[] = []; for (let line = start; line <= end; line++) { lines.push(vimState.document.lineAt(line)); } vimState.recordedState.transformer.replace( new Range(lines[0].range.start, lines[lines.length - 1].range.end), lines .map((line) => { const indent = ' '.repeat( Math.max( 0, this.args.width - (line.text.length - line.firstNonWhitespaceCharacterIndex), ), ); return indent + line.text.slice(line.firstNonWhitespaceCharacterIndex); }) .join('\n'), ); } } type CenterArgs = { width: number; }; export class CenterCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(numberParser.fallback(undefined)) .map((width) => new CenterCommand({ width: width ?? configuration.textwidth })); private args: CenterArgs; constructor(args: CenterArgs) { super(); this.args = args; } async execute(vimState: VimState): Promise { await this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' }))); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { const { start, end } = range.resolve(vimState); const lines: TextLine[] = []; for (let line = start; line <= end; line++) { lines.push(vimState.document.lineAt(line)); } vimState.recordedState.transformer.replace( new Range(lines[0].range.start, lines[lines.length - 1].range.end), lines .map((line) => { const indent = ' '.repeat( Math.max( 0, this.args.width - (line.text.length - line.firstNonWhitespaceCharacterIndex), ) / 2, ); return indent + line.text.slice(line.firstNonWhitespaceCharacterIndex); }) .join('\n'), ); } } ================================================ FILE: src/cmd_line/commands/let.ts ================================================ // eslint-disable-next-line id-denylist import { all, alt, optWhitespace, Parser, sepBy, seq, seqMap, string, whitespace } from 'parsimmon'; import { VimError } from '../../error'; import { Register } from '../../register/register'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; import { add, concat, divide, int, modulo, multiply, subtract, toExpr, } from '../../vimscript/expression/build'; import { displayValue } from '../../vimscript/expression/displayValue'; import { EvaluationContext, toInt, toString } from '../../vimscript/expression/evaluate'; import { envVariableParser, expressionParser, optionParser, registerParser, variableParser, varNameParser, } from '../../vimscript/expression/parser'; import { EnvVariableExpression, Expression, OptionExpression, RegisterExpression, Value, VariableExpression, } from '../../vimscript/expression/types'; import { bangParser } from '../../vimscript/parserUtils'; type Unpack = { type: 'unpack'; names: string[]; }; type Index = { type: 'index'; variable: VariableExpression; index: Expression; }; type Slice = { type: 'slice'; variable: VariableExpression; start: Expression | undefined; end: Expression | undefined; }; export type LetCommandOperation = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '.=' | '..='; export type LetCommandVariable = | VariableExpression | OptionExpression | RegisterExpression | EnvVariableExpression; export type LetCommandArgs = | { operation: LetCommandOperation; variable: LetCommandVariable | Unpack | Index | Slice; expression: Expression; lock: boolean; } | { operation: 'print'; variables: LetCommandVariable[]; }; const operationParser: Parser = alt( string('='), string('+='), string('-='), string('*='), string('/='), string('%='), string('.='), string('..='), ); const letVarParser = alt( variableParser, optionParser, envVariableParser, registerParser, ); const unpackParser: Parser = sepBy(varNameParser, string(',').trim(optWhitespace)) .wrap(string('[').then(optWhitespace), optWhitespace.then(string(']'))) .map((names) => ({ type: 'unpack', names, })); const indexParser: Parser = seq( variableParser, expressionParser.wrap(string('[').then(optWhitespace), optWhitespace.then(string(']'))), ).map(([variable, index]) => ({ type: 'index', variable, index, })); const sliceParser: Parser = seq( variableParser, string('[').then(optWhitespace).then(expressionParser).fallback(undefined), string(':').trim(optWhitespace), expressionParser.fallback(undefined).skip(optWhitespace.then(string(']'))), ).map(([variable, start, _, end]) => ({ type: 'slice', variable, start, end, })); export class LetCommand extends ExCommand { public static readonly argParser = (lock: boolean) => alt( // `:let {var} = {expr}` // `:let {var} += {expr}` // `:let {var} -= {expr}` // `:let {var} .= {expr}` whitespace.then( seq( alt( unpackParser, sliceParser, indexParser, letVarParser, ), operationParser.trim(optWhitespace), expressionParser.fallback(undefined), all, ).map(([variable, operation, expression, trailing]) => { trailing = trailing.trim(); if (expression === undefined) { throw VimError.InvalidExpression(trailing); } if (trailing) { throw VimError.TrailingCharacters(trailing); } return new LetCommand({ operation, variable, expression, lock, }); }), ), // `:let` // `:let {var-name} ...` optWhitespace .then(letVarParser.sepBy(whitespace)) .map((variables) => new LetCommand({ operation: 'print', variables })), ); private args: LetCommandArgs; constructor(args: LetCommandArgs) { super(); this.args = args; } async execute(vimState: VimState): Promise { const context = new EvaluationContext(vimState); if (this.args.operation === 'print') { if (this.args.variables.length === 0) { // TODO } else { const variable = this.args.variables.at(-1)!; const value = context.evaluate(variable); const prefix = value.type === 'number' ? '#' : value.type === 'funcref' ? '*' : ''; StatusBar.setText(vimState, `${variable.name} ${prefix}${displayValue(value)}`); } } else { const variable = this.args.variable; if (this.args.lock) { if (this.args.operation !== '=') { throw VimError.CannotModifyExistingVariable(); } if (variable.type !== 'variable') { if (variable.type === 'register') { throw VimError.CannotLock('a register'); } if (variable.type === 'option') { throw VimError.CannotLock('an option'); } if (variable.type === 'env_variable') { throw VimError.CannotLock('an environment variable'); } if (variable.type === 'slice') { throw VimError.CannotLock('a range'); } throw VimError.CannotLock('a list or dict'); } } const value = context.evaluate(this.args.expression); const newValue = (_var: Expression, _value: Value) => { if (this.args.operation === '+=') { return context.evaluate(add(_var, toExpr(_value))); } else if (this.args.operation === '-=') { return context.evaluate(subtract(_var, toExpr(_value))); } else if (this.args.operation === '*=') { return context.evaluate(multiply(_var, toExpr(_value))); } else if (this.args.operation === '/=') { return context.evaluate(divide(_var, toExpr(_value))); } else if (this.args.operation === '%=') { return context.evaluate(modulo(_var, toExpr(_value))); } else if (this.args.operation === '.=') { return context.evaluate(concat(_var, toExpr(_value))); } else if (this.args.operation === '..=') { return context.evaluate(concat(_var, toExpr(_value))); } return _value; }; if (variable.type === 'variable') { if ( variable.namespace === 'v' && variable.name in [ 'count', 'false', 'key', 'null', 'operator', 'prevcount', 'progname', 'progpath', 'servername', 'shell_error', 'swapname', 't_bool', 't_dict', 't_float', 't_func', 't_list', 't_number', 't_string', 't_blob', 'true', 'val', 'version', 'vim_did_enter', ] ) { throw VimError.CannotChangeReadOnlyVariable(`v:${variable.name}`); } context.setVariable(variable, newValue(variable, value), this.args.lock); } else if (variable.type === 'register') { if (this.args.operation === '=') { vimState.recordedState.registerName = variable.name; Register.put(vimState, toString(value)); } else if (this.args.operation === '.=' || this.args.operation === '..=') { throw VimError.WrongVariableType(this.args.operation); // TODO } else { throw VimError.WrongVariableType(this.args.operation); } } else if (variable.type === 'option') { // TODO } else if (variable.type === 'env_variable') { if (this.args.operation === '=') { process.env[variable.name] = toString(value); } else if (this.args.operation === '.=' || this.args.operation === '..=') { process.env[variable.name] = (process.env[variable.name] ?? '') + toString(value); } else { throw VimError.WrongVariableType(this.args.operation); } } else if (variable.type === 'unpack') { // TODO: Support :let [a, b; rest] = ["aval", "bval", 3, 4] if (value.type !== 'list') { throw VimError.ListRequired(); } if (variable.names.length < value.items.length) { throw VimError.LessTargetsThanListItems(); } if (variable.names.length > value.items.length) { throw VimError.MoreTargetsThanListItems(); } for (const [i, name] of variable.names.entries()) { const item: VariableExpression = { type: 'variable', namespace: undefined, name }; context.setVariable(item, newValue(item, value.items[i]), this.args.lock); } } else if (variable.type === 'index') { const varValue = context.evaluate(variable.variable); if (varValue.type === 'list') { const idx = toInt(context.evaluate(variable.index)); const newItem = newValue( { type: 'index', expression: variable.variable, index: int(idx), }, value, ); varValue.items[idx] = newItem; context.setVariable(variable.variable, varValue, this.args.lock); } else if (varValue.type === 'dictionary') { const key = toString(context.evaluate(variable.index)); const newItem = newValue( { type: 'entry', expression: variable.variable, entryName: key, }, value, ); varValue.items.set(key, newItem); context.setVariable(variable.variable, varValue, this.args.lock); } else { // TODO: Support blobs throw VimError.CanOnlyIndexAListDictionaryOrBlob(); } } else if (variable.type === 'slice') { // TODO: Operations other than `=`? // TODO: Support blobs const varValue = context.evaluate(variable.variable); if (varValue.type !== 'list' || value.type !== 'list') { throw VimError.CanOnlyIndexAListDictionaryOrBlob(); } if (value.type !== 'list') { throw VimError.SliceRequiresAListOrBlobValue(); } const start = variable.start ? toInt(context.evaluate(variable.start)) : 0; if (start > varValue.items.length - 1) { throw VimError.ListIndexOutOfRange(start); } // NOTE: end is inclusive, unlike in JS const end = variable.end ? toInt(context.evaluate(variable.end)) : varValue.items.length - 1; const slots = end - start + 1; if (slots > value.items.length) { throw VimError.ListValueHasNotEnoughItems(); } else if (slots < value.items.length) { // TODO: Allow this when going past end of list and end === undefined throw VimError.ListValueHasMoreItemsThanTarget(); } let i = start; for (const item of value.items) { varValue.items[i] = item; ++i; } context.setVariable(variable.variable, varValue, this.args.lock); } } } } export class UnletCommand extends ExCommand { public static readonly argParser = seqMap( bangParser, whitespace.then(variableParser.sepBy(whitespace)), (bang, variables) => { if (variables.length === 0) { throw VimError.ArgumentRequired(); } return new UnletCommand(variables, bang); }, ); private variables: VariableExpression[]; private bang: boolean; public constructor(variables: VariableExpression[], bang: boolean) { super(); this.variables = variables; this.bang = bang; } async execute(vimState: VimState): Promise { const ctx = new EvaluationContext(vimState); for (const variable of this.variables) { const store = ctx.getVariableStore(variable.namespace); const existed = store?.delete(variable.name); if (!existed && !this.bang) { throw VimError.NoSuchVariable( variable.namespace ? `${variable.namespace}:${variable.name}` : variable.name, ); } } } } ================================================ FILE: src/cmd_line/commands/marks.ts ================================================ import { QuickPickItem, window } from 'vscode'; // eslint-disable-next-line id-denylist import { Parser, alt, noneOf, optWhitespace, regexp, seq, string, whitespace } from 'parsimmon'; import { Position } from 'vscode'; import { Cursor } from '../../common/motion/cursor'; import { VimError } from '../../error'; import { IMark } from '../../history/historyTracker'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; class MarkQuickPickItem implements QuickPickItem { mark: IMark; label: string; description: string; detail: string; picked = false; alwaysShow = false; constructor(vimState: VimState, mark: IMark) { this.mark = mark; this.label = mark.name; if (mark.isUppercaseMark && mark.document !== vimState.document) { this.description = mark.document.fileName; } else { this.description = vimState.document.lineAt(mark.position).text.trim(); } this.detail = `line ${mark.position.line} col ${mark.position.character}`; } } export class MarksCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(noneOf('|')) .many() .map((marks) => new MarksCommand(marks)); private marksFilter: string[]; constructor(marksFilter: string[]) { super(); this.marksFilter = marksFilter; } async execute(vimState: VimState): Promise { const quickPickItems: MarkQuickPickItem[] = vimState.historyTracker .getMarks() .filter((mark) => { return this.marksFilter.length === 0 || this.marksFilter.includes(mark.name); }) .map((mark) => new MarkQuickPickItem(vimState, mark)); if (quickPickItems.length > 0) { const item = await window.showQuickPick(quickPickItems, { canPickMany: false, }); if (item) { vimState.cursors = [Cursor.atPosition(item.mark.position)]; } } else { void window.showInformationMessage('No marks set'); } } } type DeleteMarksArgs = Array<{ start: string; end: string } | string> | '!'; export class DeleteMarksCommand extends ExCommand { public static readonly argParser: Parser = alt( string('!'), whitespace.then( optWhitespace .then( alt<{ start: string; end: string } | string>( seq(regexp(/[a-z]/).skip(string('-')), regexp(/[a-z]/)).map(([start, end]) => { return { start, end }; }), seq(regexp(/[A-Z]/).skip(string('-')), regexp(/[A-Z]/)).map(([start, end]) => { return { start, end }; }), seq(regexp(/[0-9]/).skip(string('-')), regexp(/[0-9]/)).map(([start, end]) => { return { start, end }; }), noneOf('-'), ), ) .many(), ), ).map((marks) => new DeleteMarksCommand(marks)); private args: DeleteMarksArgs; constructor(args: DeleteMarksArgs) { super(); this.args = args; } private static resolveMarkList(vimState: VimState, args: DeleteMarksArgs) { const asciiRange = (start: string, end: string) => { if (start > end) { throw VimError.InvalidArgument474(); } const [asciiStart, asciiEnd] = [start.charCodeAt(0), end.charCodeAt(0)]; const chars: string[] = []; for (let ascii = asciiStart; ascii <= asciiEnd; ascii++) { chars.push(String.fromCharCode(ascii)); } return chars; }; if (args === '!') { // TODO: clear change list return asciiRange('a', 'z'); } const marks: string[] = []; for (const x of args) { if (typeof x === 'string') { marks.push(x); } else { const range = asciiRange(x.start, x.end); if (range === undefined) { throw VimError.InvalidArgument474(); } marks.push(...range.concat()); } } return marks; } async execute(vimState: VimState): Promise { const marks = DeleteMarksCommand.resolveMarkList(vimState, this.args); vimState.historyTracker.removeMarks(marks); } } export class MarkCommand extends ExCommand { public static readonly argParser: Parser = seq( optWhitespace, regexp(/[a-zA-Z'`<>[\].]/).desc('mark name'), optWhitespace, ).map(([, markName]) => new MarkCommand(markName)); private markName: string; constructor(markName: string) { super(); this.markName = markName; } async execute(vimState: VimState): Promise { const position = vimState.cursorStopPosition; vimState.historyTracker.addMark(vimState.document, position, this.markName); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { /** * When a range is specified, the mark is set at the last line of the range. * For example, :1,5mark a will set mark 'a' at line 5. */ const { end } = range.resolve(vimState); const position = new Position(end, 0); vimState.historyTracker.addMark(vimState.document, position, this.markName); } } ================================================ FILE: src/cmd_line/commands/move.ts ================================================ import { optWhitespace, Parser } from 'parsimmon'; import { Position, Range } from 'vscode'; import { PositionDiff } from '../../common/motion/position'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; import { Address, LineRange } from '../../vimscript/lineRange'; export class MoveCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(Address.parser.fallback(undefined)) .map((address) => new MoveCommand(address)); private address?: Address; constructor(address?: Address) { super(); this.address = address; } public override neovimCapable(): boolean { return true; } private moveLines(vimState: VimState, sourceStart: number, sourceEnd: number) { const dest = this.address?.resolve(vimState, 'left', false); if (dest === undefined || dest < -1 || dest > vimState.document.lineCount) { StatusBar.displayError(vimState, VimError.InvalidAddress()); return; } if (sourceEnd < sourceStart) { [sourceStart, sourceEnd] = [sourceEnd, sourceStart]; } /* make sure 1. not move a range to the place inside itself. 2. not move a range to the place right below or above itself, which leads to no change. */ if (dest >= sourceStart && dest <= sourceEnd) { StatusBar.displayError(vimState, VimError.InvalidAddress()); return; } // copy const copiedText = vimState.document.getText( new Range(new Position(sourceStart, 0), new Position(sourceEnd, 0).getLineEnd()), ); let text: string; let position: Position; if (dest === -1) { text = copiedText + '\n'; position = new Position(0, 0); } else { text = '\n' + copiedText; position = new Position(dest, 0).getLineEnd(); } const lines = copiedText.split('\n'); let cursorPosition: Position; if (dest > sourceEnd) { // make the cursor position at the beginning of the endline. cursorPosition = new Position(Math.max(dest, 0), lines.at(-1)!.match(/\S/)?.index ?? 0); } else { cursorPosition = new Position( Math.max(dest + lines.length, 0), lines.at(-1)!.match(/\S/)?.index ?? 0, ); } // delete let start = new Position(sourceStart, 0); let end = new Position(sourceEnd, 0).getLineEnd(); if (sourceEnd < vimState.document.lineCount - 1) { end = end.getRightThroughLineBreaks(); } else if (sourceStart > 0) { start = start.getLeftThroughLineBreaks(); } vimState.recordedState.transformer.addTransformation({ type: 'deleteRange', range: new Range(start, end), manuallySetCursorPositions: true, }); // insert vimState.recordedState.transformer.insert( position, text, PositionDiff.exactPosition(cursorPosition), ); } public async execute(vimState: VimState): Promise { const line = vimState.cursor.stop.line; this.moveLines(vimState, line, line); } public override async executeWithRange(vimState: VimState, range: LineRange): Promise { const { start, end } = range.resolve(vimState); this.moveLines(vimState, start, end); } } ================================================ FILE: src/cmd_line/commands/nohl.ts ================================================ import { globalState } from '../../state/globalState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; export class NohlCommand extends ExCommand { async execute(vimState: VimState): Promise { globalState.hl = false; // Clear the `match x of y` message from status bar StatusBar.clear(vimState); } } ================================================ FILE: src/cmd_line/commands/normal.ts ================================================ import { Parser, all, whitespace } from 'parsimmon'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; export class NormalCommand extends ExCommand { // TODO: support to parse `:normal!` public static readonly argParser: Parser = whitespace .then(all) .map((keystrokes) => new NormalCommand(keystrokes)); private readonly keystrokes: string; constructor(argument: string) { super(); this.keystrokes = argument; } override async execute(vimState: VimState): Promise { vimState.recordedState.transformer.addTransformation({ type: 'executeNormal', keystrokes: this.keystrokes, }); } override async executeWithRange(vimState: VimState, lineRange: LineRange): Promise { vimState.recordedState.transformer.addTransformation({ type: 'executeNormal', keystrokes: this.keystrokes, range: lineRange, }); } } ================================================ FILE: src/cmd_line/commands/only.ts ================================================ import * as vscode from 'vscode'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; export class OnlyCommand extends ExCommand { async execute(vimState: VimState): Promise { await Promise.allSettled([ vscode.commands.executeCommand('workbench.action.joinAllGroups'), vscode.commands.executeCommand('workbench.action.maximizeEditor'), vscode.commands.executeCommand('workbench.action.closePanel'), ]); } } ================================================ FILE: src/cmd_line/commands/print.ts ================================================ import { Parser, succeed } from 'parsimmon'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; import { Address, LineRange } from '../../vimscript/lineRange'; type PrintArgs = { printNumbers: boolean; printText: boolean; }; // TODO: `:l[ist]` is more than an alias // TODO: `:z` export class PrintCommand extends ExCommand { // TODO: Print {count} and [flags] public static readonly argParser = (args: { printNumbers: boolean; printText: boolean; }): Parser => succeed(new PrintCommand(args)); private args: PrintArgs; constructor(args: PrintArgs) { super(); this.args = args; } async execute(vimState: VimState): Promise { // TODO: Wrong default for `:=` void this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' }))); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { const { end } = range.resolve(vimState); // For now, we just print the last line. // TODO: Create a dynamic document if there's more than one line? const line = vimState.document.lineAt(end); let output: string; if (this.args.printNumbers) { if (this.args.printText) { output = `${line.lineNumber + 1} ${line.text}`; } else { output = `${line.lineNumber + 1}`; } } else { if (this.args.printText) { output = `${line.text}`; } else { output = ''; } } StatusBar.setText(vimState, output); } } ================================================ FILE: src/cmd_line/commands/put.ts ================================================ import { configuration } from '../../configuration/configuration'; import { VimState } from '../../state/vimState'; // eslint-disable-next-line id-denylist import { Parser, all, alt, any, optWhitespace, seq, string } from 'parsimmon'; import { Position } from 'vscode'; import { PutBeforeFromCmdLine, PutFromCmdLine } from '../../actions/commands/put'; import { VimError } from '../../error'; import { Register } from '../../register/register'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; import { EvaluationContext, toString } from '../../vimscript/expression/evaluate'; import { expressionParser } from '../../vimscript/expression/parser'; import { Expression } from '../../vimscript/expression/types'; import { LineRange } from '../../vimscript/lineRange'; import { bangParser } from '../../vimscript/parserUtils'; export interface IPutCommandArguments { bang: boolean; register?: string; fromExpression?: Expression; } // // Implements :put // http://vimdoc.sourceforge.net/htmldoc/change.html#:put // export class PutExCommand extends ExCommand { public static readonly argParser: Parser = seq( bangParser, optWhitespace.then( alt>( string('=') .then(optWhitespace) .then(seq(expressionParser.fallback(undefined), all)) .map(([expression, trailing]) => { trailing = trailing.trim(); if (expression === undefined) { if (trailing) { throw VimError.InvalidExpression(trailing); } return { register: '=' }; } if (trailing) { throw VimError.TrailingCharacters(trailing); } return { fromExpression: expression }; }), // eslint-disable-next-line id-denylist any.map((register) => ({ register })).fallback({ register: undefined }), ), ), ).map(([bang, register]) => new PutExCommand({ bang, ...register })); private static lastExpression: Expression | undefined; public readonly arguments: IPutCommandArguments; constructor(args: IPutCommandArguments) { super(); this.arguments = args; } public override neovimCapable(): boolean { return true; } async doPut(vimState: VimState, position: Position): Promise { if (this.arguments.register === '=' && this.arguments.fromExpression === undefined) { if (PutExCommand.lastExpression === undefined) { return; } this.arguments.fromExpression = PutExCommand.lastExpression; } if (this.arguments.fromExpression) { PutExCommand.lastExpression = this.arguments.fromExpression; this.arguments.register = '='; const value = new EvaluationContext(vimState).evaluate(this.arguments.fromExpression); const stringified = value.type === 'list' ? value.items.map(toString).join('\n') : toString(value); Register.overwriteRegister(vimState, this.arguments.register, stringified, 0); } const registerName = this.arguments.register || (configuration.useSystemClipboard ? '*' : '"'); if (!Register.isValidRegister(registerName)) { StatusBar.displayError(vimState, VimError.TrailingCharacters()); return; } vimState.recordedState.registerName = registerName; const putCmd = this.arguments.bang ? new PutBeforeFromCmdLine() : new PutFromCmdLine(); putCmd.setInsertionLine(position.line); await putCmd.exec(position, vimState); } async execute(vimState: VimState): Promise { await this.doPut(vimState, vimState.cursorStopPosition); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { const { end } = range.resolve(vimState); await this.doPut(vimState, new Position(end, 0).getLineEnd()); } } ================================================ FILE: src/cmd_line/commands/pwd.ts ================================================ import * as vscode from 'vscode'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; /** * Implements the :pwd command, which prints the current working directory. */ export class PwdCommand extends ExCommand { async execute(vimState: VimState): Promise { const workspaceFolders = vscode.workspace.workspaceFolders; if (workspaceFolders && workspaceFolders.length > 0) { const currentDir = workspaceFolders[0].uri.fsPath; StatusBar.setText(vimState, `Current Directory: ${currentDir}`); } else { StatusBar.setText(vimState, 'No workspace folder is open.'); } } } ================================================ FILE: src/cmd_line/commands/quit.ts ================================================ import { Parser } from 'parsimmon'; import * as vscode from 'vscode'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser } from '../../vimscript/parserUtils'; export interface IQuitCommandArguments { bang?: boolean; quitAll?: boolean; } // // Implements :quit // http://vimdoc.sourceforge.net/htmldoc/editing.html#:quit // export class QuitCommand extends ExCommand { public static readonly argParser: (quitAll: boolean) => Parser = ( quitAll: boolean, ) => bangParser.map( (bang) => new QuitCommand({ bang, quitAll, }), ); public arguments: IQuitCommandArguments; constructor(args: IQuitCommandArguments) { super(); this.arguments = args; } async execute(vimState: VimState): Promise { // NOTE: We can't currently get all open text editors, so this isn't perfect. See #3809 const duplicatedInSplit = vscode.window.visibleTextEditors.filter((editor) => editor.document === vimState.document) .length > 1; if ( vimState.document.isDirty && !this.arguments.bang && (!duplicatedInSplit || this.arguments.quitAll) ) { throw VimError.NoWriteSinceLastChange(); } if (this.arguments.quitAll) { if (!this.arguments.bang) { await vscode.commands.executeCommand('workbench.action.closeAllEditors'); } else { const totalTabCount = vscode.window.tabGroups.all.flatMap((group) => group.tabs).length; for (let i = 0; i < totalTabCount; i++) { await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); } } } else { if (!this.arguments.bang) { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); } else { await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); } } } } ================================================ FILE: src/cmd_line/commands/read.ts ================================================ // eslint-disable-next-line id-denylist import { all, alt, optWhitespace, Parser, seq, string, whitespace } from 'parsimmon'; import { SUPPORT_READ_COMMAND } from 'platform/constants'; import { readFileAsync } from 'platform/fs'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { fileNameParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils'; export type IReadCommandArguments = | { opt: FileOpt; cmd: string } | { opt: FileOpt; file: string } | { opt: FileOpt }; // // Implements :read and :read! // http://vimdoc.sourceforge.net/htmldoc/insert.html#:read // http://vimdoc.sourceforge.net/htmldoc/insert.html#:read! // export class ReadCommand extends ExCommand { public static readonly argParser: Parser = seq( whitespace.then(fileOptParser).fallback([]), optWhitespace .then( alt<{ cmd: string } | { file: string }>( string('!') .then(all) .map((cmd) => ({ cmd })), fileNameParser.map((file) => ({ file })), ), ) .fallback({}), ).map(([opt, other]) => new ReadCommand({ opt, ...other })); private readonly arguments: IReadCommandArguments; constructor(args: IReadCommandArguments) { super(); this.arguments = args; } public override neovimCapable(): boolean { return true; } async execute(vimState: VimState): Promise { const textToInsert = await this.getTextToInsert(vimState); if (textToInsert) { vimState.recordedState.transformer.insert( vimState.cursorStopPosition.getLineEnd(), '\n' + textToInsert, ); } } // TODO: executeWithRange() async getTextToInsert(vimState: VimState): Promise { if ('file' in this.arguments) { return readFileAsync(this.arguments.file, 'utf8'); } else if ('cmd' in this.arguments) { if (this.arguments.cmd.length > 0) { if (SUPPORT_READ_COMMAND) { const cmd = this.arguments.cmd; return new Promise(async (resolve, reject) => { const { exec } = await import('child_process'); exec(cmd, (err, stdout, stderr) => { if (err) { reject(err); } else { resolve(stdout); } }); }); } else { return ''; } } else { // TODO: error message? return ''; } } else { return vimState.document.getText(); } } } ================================================ FILE: src/cmd_line/commands/redo.ts ================================================ import { optWhitespace, Parser } from 'parsimmon'; import { Position } from 'vscode'; import { Redo } from '../../actions/commands/undo'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { numberParser } from '../../vimscript/parserUtils'; // // Implements :red[o] // http://vimdoc.sourceforge.net/htmldoc/undo.html#redo // export class RedoCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(numberParser) .fallback(undefined) .map((count) => new RedoCommand(count)); private count?: number; private constructor(count?: number) { super(); this.count = count; } async execute(vimState: VimState): Promise { // TODO: Use `this.count` await new Redo().exec(new Position(0, 0), vimState); } } ================================================ FILE: src/cmd_line/commands/register.ts ================================================ import * as vscode from 'vscode'; // eslint-disable-next-line id-denylist import { Parser, any, optWhitespace } from 'parsimmon'; import { VimError } from '../../error'; import { Register } from '../../register/register'; import { RecordedState } from '../../state/recordedState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; export class RegisterCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace.then( // eslint-disable-next-line id-denylist any.sepBy(optWhitespace).map((registers) => new RegisterCommand(registers)), ); private readonly registers: string[]; constructor(registers: string[]) { super(); this.registers = registers; } private async getRegisterDisplayValue(register: string): Promise { let result = (await Register.get(register))?.text; if (result instanceof Array) { result = result.join('\n').substr(0, 100); } else if (result instanceof RecordedState) { result = result.actionsRun.map((x) => x.keysPressed.join('')).join(''); } return result; } async displayRegisterValue(vimState: VimState, register: string): Promise { let result = await this.getRegisterDisplayValue(register); if (result === undefined) { StatusBar.displayError(vimState, VimError.NothingInRegister(register)); } else { result = result.replace(/\n/g, '\\n'); void vscode.window.showInformationMessage(`${register} ${result}`); } } private regSortOrder(register: string): number { const specials = ['-', '*', '+', '.', ':', '%', '#', '/', '=']; if (register === '"') { return 0; } else if (register >= '0' && register <= '9') { return 10 + parseInt(register, 10); } else if (register >= 'a' && register <= 'z') { return 100 + (register.charCodeAt(0) - 'a'.charCodeAt(0)); } else if (specials.includes(register)) { return 1000 + specials.indexOf(register); } else { throw new Error(`Unexpected register ${register}`); } } async execute(vimState: VimState): Promise { if (this.registers.length === 1) { await this.displayRegisterValue(vimState, this.registers[0]); } else { const currentRegisterKeys = Register.getKeys() .filter( (reg) => reg !== '_' && (this.registers.length === 0 || this.registers.includes(reg)), ) .sort((reg1: string, reg2: string) => this.regSortOrder(reg1) - this.regSortOrder(reg2)); const registerKeyAndContent = new Array(); for (const registerKey of currentRegisterKeys) { const displayValue = await this.getRegisterDisplayValue(registerKey); if (typeof displayValue === 'string') { registerKeyAndContent.push({ label: registerKey, description: displayValue, }); } } void vscode.window.showQuickPick(registerKeyAndContent).then(async (val) => { if (val) { const result = val.description; void vscode.window.showInformationMessage(`${val.label} ${result}`); } }); } } } ================================================ FILE: src/cmd_line/commands/retab.ts ================================================ import { optWhitespace, Parser, seq } from 'parsimmon'; import { Range } from 'vscode'; import { configuration } from '../../configuration/configuration'; import { isVisualMode } from '../../mode/mode'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; import { bangParser, numberParser } from '../../vimscript/parserUtils'; import { SetCommand } from './set'; export interface IRetabCommandArguments { replaceSpaces: boolean; newTabstop?: number; } interface UpdatedLineSegment { value: string; length: number; } // :[range]ret[ab][!] [new_tabstop] export class RetabCommand extends ExCommand { public static readonly argParser: Parser = seq( bangParser, optWhitespace.then(numberParser).fallback(undefined), ).map( ([replaceSpaces, newTabstop]) => new RetabCommand({ replaceSpaces, newTabstop, }), ); private readonly arguments: IRetabCommandArguments; constructor(args: IRetabCommandArguments) { super(); this.arguments = args; } async execute(vimState: VimState): Promise { if (isVisualMode(vimState.currentMode)) { const { start, end } = vimState.editor.selection; this.retab(vimState, start.line, end.line); } else { this.retab(vimState, 0, vimState.document.lineCount - 1); } } override async executeWithRange(vimState: VimState, range: LineRange): Promise { const { start, end } = range.resolve(vimState); this.retab(vimState, start, end); } private concat(count: number, char: string): string { let result = ''; for (let i = 0; i < count; i++) { result += char; } return result; } private hasTabs(str: string): boolean { return str.indexOf('\t') >= 0; } expandtab(str: string, start = 0, tabstop = configuration.tabstop): string { let expanded = ''; let i = start; for (const char of str) { if (char === '\t') { const spaces = tabstop - (i % tabstop) || tabstop; expanded += this.concat(spaces, ' '); i += spaces; } else { expanded += char; i++; } } return expanded; } retabLineSegment( segment: string, start: number, tabstop = configuration.tabstop, ): UpdatedLineSegment { const retab = this.arguments.replaceSpaces || this.hasTabs(segment); if (!retab) { return { value: segment, length: segment.length, }; } const retabTabstop = this.arguments.newTabstop || tabstop; const detabbed = this.expandtab(segment, start, tabstop); const spaces = Math.min((start + detabbed.length) % retabTabstop, detabbed.length); const tabs = Math.ceil((detabbed.length - spaces) / retabTabstop); let result = ''; result += this.concat(tabs, '\t'); result += this.concat(spaces, ' '); return { value: result, length: detabbed.length, }; } retabLine(line: string, tabstop = configuration.tabstop): string { const segments = line.split(/(\s+)/); let i = 0; let retabbed = ''; for (const str of segments) { if (!str) { continue; } if (![' ', '\t'].includes(str[0])) { retabbed += str; i += str.length; } else { const result = this.retabLineSegment(str, i, tabstop); retabbed += result.value; i += result.length; } } return retabbed; } public retab(vimState: VimState, startLine: number, endLine: number) { const originalLines: string[] = []; const lastLine = Math.min(endLine, vimState.document.lineCount - 1); for (let i = startLine; i <= lastLine; i++) { originalLines.push(vimState.document.lineAt(i).text); } const replacedLines = originalLines.map((line: string) => { return configuration.expandtab ? this.expandtab(line) : this.retabLine(line); }); const replacedContent = replacedLines.join('\n'); const lastLineLength = originalLines[originalLines.length - 1].length; vimState.recordedState.transformer.replace( new Range(startLine, 0, endLine, lastLineLength), replacedContent, ); if (this.arguments.newTabstop) { const setTabstop = new SetCommand({ type: 'equal', option: 'tabstop', value: this.arguments.newTabstop.toString(), }); void setTabstop.execute(vimState); } } } ================================================ FILE: src/cmd_line/commands/set.ts ================================================ // eslint-disable-next-line id-denylist import { alt, oneOf, Parser, regexp, seq, string, whitespace } from 'parsimmon'; import { configuration, optionAliases } from '../../configuration/configuration'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; type SetOperation = | { // :se[t] // :se[t] {option} type: 'show_or_set'; option: string | undefined; } | { // :se[t] {option}? type: 'show'; option: string; } | { // :se[t] no{option} type: 'unset'; option: string; } | { // :se[t] {option}! // :se[t] inv{option} type: 'invert'; option: string; } | { // :se[t] {option}& // :se[t] {option}&vi // :se[t] {option}&vim type: 'default'; option: string; source: 'vi' | 'vim' | ''; } | { // :se[t] {option}={value} // :se[t] {option}:{value} type: 'equal'; option: string; value: string; } | { // :se[t] {option}+={value} type: 'add'; option: string; value: string; } | { // :se[t] {option}^={value} type: 'multiply'; option: string; value: string; } | { // :se[t] {option}-={value} type: 'subtract'; option: string; value: string; }; const optionParser = regexp(/[a-z]+/); const valueParser = regexp(/\S*/); const setOperationParser: Parser = whitespace .then( alt( string('no') .then(optionParser) .map((option) => { return { type: 'unset', option, }; }), string('inv') .then(optionParser) .map((option) => { return { type: 'invert', option, }; }), optionParser.skip(string('!')).map((option) => { return { type: 'invert', option, }; }), optionParser.skip(string('?')).map((option) => { return { type: 'show', option, }; }), seq(optionParser.skip(string('&')), alt(string('vim'), string('vi'), string(''))).map( ([option, source]) => { return { type: 'default', option, source, }; }, ), seq(optionParser.skip(oneOf('=:')), valueParser).map(([option, value]) => { return { type: 'equal', option, value, }; }), seq(optionParser.skip(string('+=')), valueParser).map(([option, value]) => { return { type: 'add', option, value, }; }), seq(optionParser.skip(string('^=')), valueParser).map(([option, value]) => { return { type: 'multiply', option, value, }; }), seq(optionParser.skip(string('-=')), valueParser).map(([option, value]) => { return { type: 'subtract', option, value, }; }), optionParser.map((option) => { return { type: 'show_or_set', option, }; }), ), ) .fallback({ type: 'show_or_set', option: undefined }); export class SetCommand extends ExCommand { public static readonly argParser: Parser = setOperationParser.map( (operation) => new SetCommand(operation), ); private readonly operation: SetOperation; constructor(operation: SetOperation) { super(); this.operation = operation; } // Listeners for options that need to be updated when they change private static listeners: { [key: string]: Array<() => void> } = {}; static addListener(option: string, listener: () => void) { if (!(option in SetCommand.listeners)) SetCommand.listeners[option] = []; SetCommand.listeners[option].push(listener); } async execute(vimState: VimState): Promise { if (this.operation.option === undefined) { // TODO: Show all options that differ from their default value return; } const option = optionAliases.get(this.operation.option) ?? this.operation.option; const currentValue = configuration[option] as string | number | boolean | undefined; if (currentValue === undefined) { throw VimError.UnknownOption(option); } const type = typeof currentValue === 'boolean' ? 'boolean' : typeof currentValue === 'string' ? 'string' : 'number'; switch (this.operation.type) { case 'show_or_set': { if (this.operation.option === 'all') { // TODO: Show all options } else { if (type === 'boolean') { configuration[option] = true; } else { this.showOption(vimState, option, currentValue); } } break; } case 'show': { this.showOption(vimState, option, currentValue); break; } case 'unset': { if (type === 'boolean') { configuration[option] = false; } else { throw VimError.InvalidArgument474(`no${option}`); } break; } case 'invert': { if (type === 'boolean') { configuration[option] = !currentValue; } else { // TODO: Could also be {option}! throw VimError.InvalidArgument474(`inv${option}`); } break; } case 'default': { if (this.operation.option === 'all') { // TODO: Set all options to default } else { // TODO: Set the option to default } break; } case 'equal': { if (type === 'boolean') { // TODO: Could also be {option}:{value} throw VimError.InvalidArgument474(`${option}=${this.operation.value}`); } else if (type === 'string') { configuration[option] = this.operation.value; } else { const value = Number.parseInt(this.operation.value, 10); if (isNaN(value)) { // TODO: Could also be {option}:{value} throw VimError.NumberRequiredAfterEqual(`${option}=${this.operation.value}`); } configuration[option] = value; } break; } case 'add': { if (type === 'boolean') { throw VimError.InvalidArgument474(`${option}+=${this.operation.value}`); } else if (type === 'string') { configuration[option] = currentValue + this.operation.value; } else { const value = Number.parseInt(this.operation.value, 10); if (isNaN(value)) { throw VimError.NumberRequiredAfterEqual(`${option}+=${this.operation.value}`); } configuration[option] = (currentValue as number) + value; } break; } case 'multiply': { if (type === 'boolean') { throw VimError.InvalidArgument474(`${option}^=${this.operation.value}`); } else if (type === 'string') { configuration[option] = this.operation.value + currentValue; } else { const value = Number.parseInt(this.operation.value, 10); if (isNaN(value)) { throw VimError.NumberRequiredAfterEqual(`${option}^=${this.operation.value}`); } configuration[option] = (currentValue as number) * value; } break; } case 'subtract': { if (type === 'boolean') { throw VimError.InvalidArgument474(`${option}-=${this.operation.value}`); } else if (type === 'string') { configuration[option] = (currentValue as string).split(this.operation.value).join(''); } else { const value = Number.parseInt(this.operation.value, 10); if (isNaN(value)) { throw VimError.NumberRequiredAfterEqual(`${option}-=${this.operation.value}`); } configuration[option] = (currentValue as number) - value; } break; } default: const guard: never = this.operation; throw new Error('Got unexpected SetOperation.type'); } if (option in SetCommand.listeners) { for (const listener of SetCommand.listeners[option]) { listener(); } } } private showOption(vimState: VimState, option: string, value: boolean | string | number) { if (typeof value === 'boolean') { StatusBar.setText(vimState, value ? option : `no${option}`); } else { StatusBar.setText(vimState, `${option}=${value}`); } } } ================================================ FILE: src/cmd_line/commands/sh.ts ================================================ import { window } from 'vscode'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; export class ShCommand extends ExCommand { async execute(vimState: VimState): Promise { window.createTerminal().show(); } } ================================================ FILE: src/cmd_line/commands/shift.ts ================================================ import { Position, Selection } from 'vscode'; // eslint-disable-next-line id-denylist import { optWhitespace, Parser, seq, string } from 'parsimmon'; import { PositionDiff } from '../../common/motion/position'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { Address, LineRange } from '../../vimscript/lineRange'; import { numberParser } from '../../vimscript/parserUtils'; export type ShiftDirection = '>' | '<'; export type ShiftArgs = { dir: '>' | '<'; depth: number; numLines: number | undefined; }; export class ShiftCommand extends ExCommand { public static readonly argParser = (dir: '>' | '<'): Parser => optWhitespace .then( seq( // `:>>>` indents 3 times `shiftwidth` string(dir) .many() .map((shifts) => shifts.length + 1) .skip(optWhitespace), // `:> 2` indents 2 lines numberParser.fallback(undefined), ), ) .map(([depth, numLines]) => new ShiftCommand({ dir, depth, numLines })); private args: ShiftArgs; constructor(args: ShiftArgs) { super(); this.args = args; } public async execute(vimState: VimState): Promise { await this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' }))); } public override async executeWithRange(vimState: VimState, range: LineRange): Promise { let { start, end } = range.resolve(vimState); if (this.args.numLines !== undefined) { start = end; end = start + this.args.numLines; } vimState.editor.selection = new Selection(new Position(start, 0), new Position(end, 0)); for (let i = 0; i < this.args.depth; i++) { if (this.args.dir === '>') { vimState.recordedState.transformer.vscodeCommand('editor.action.indentLines'); } else if (this.args.dir === '<') { vimState.recordedState.transformer.vscodeCommand('editor.action.outdentLines'); } } vimState.recordedState.transformer.moveCursor(PositionDiff.startOfLine()); } } ================================================ FILE: src/cmd_line/commands/smile.ts ================================================ import * as vscode from 'vscode'; import { VimState } from '../../state/vimState'; import { TextEditor } from '../../textEditor'; import { ExCommand } from '../../vimscript/exCommand'; export class SmileCommand extends ExCommand { static readonly smileText: string = ` oooo$$$$$$$$$$$$oooo oo$$$$$$$$$$$$$$$$$$$$$$$$o oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o o$ $$ o$ o $ oo o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o $$ $$ $$o$ oo $ $ "$ o$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$o $$$o$$o$ "$$$$$$o$ o$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$o $$$$$$$$ $$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$$$$$$ """$$$ "$$$""""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$ $$$ o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$o o$$" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$o $$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o o$$$oooo$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$$$$$$$$$$$$$$ $$$$$$$$"$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$"""""""" """" $$$$ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" o$$$ "$$$o """$$$$$$$$$$$$$$$$$$"$$" $$$ $$$o "$$""$$$$$$"""" o$$$ $$$$o o$$$" "$$$$o o$$$$$$o"$$$$o o$$$$ "$$$$$oo ""$$$$o$$$$$o o$$$$"" ""$$$$$oooo "$$$o$$$$$$$$$""" ""$$$$$$$oo $$$$$$$$$$ """"$$$$$$$$$$$ $$$$$$$$$$$$ $$$$$$$$$$" "$$$""""`; constructor() { super(); } async execute(vimState: VimState): Promise { await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); await TextEditor.insert(vscode.window.activeTextEditor!, SmileCommand.smileText); } } ================================================ FILE: src/cmd_line/commands/sort.ts ================================================ import { oneOf, optWhitespace, Parser, seq } from 'parsimmon'; import * as vscode from 'vscode'; import { PositionDiff } from '../../common/motion/position'; import { NumericString, NumericStringRadix } from '../../common/number/numericString'; import { isVisualMode } from '../../mode/mode'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; import { bangParser } from '../../vimscript/parserUtils'; export interface ISortCommandArguments { reverse: boolean; ignoreCase: boolean; unique: boolean; numeric: boolean; // TODO: support other flags // TODO(#6676): support pattern } export class SortCommand extends ExCommand { public static readonly argParser: Parser = seq( bangParser, optWhitespace.then(oneOf('bfilnorux').many()), ).map( ([bang, flags]) => new SortCommand({ reverse: bang, ignoreCase: flags.includes('i'), unique: flags.includes('u'), numeric: flags.includes('n'), }), ); private readonly arguments: ISortCommandArguments; constructor(args: ISortCommandArguments) { super(); this.arguments = args; } public override neovimCapable(): boolean { return true; } async execute(vimState: VimState): Promise { if (isVisualMode(vimState.currentMode)) { const { start, end } = vimState.editor.selection; await this.sortLines(vimState, start.line, end.line); } else { await this.sortLines(vimState, 0, vimState.document.lineCount - 1); } } async sortLines(vimState: VimState, startLine: number, endLine: number) { let originalLines: string[] = []; for ( let currentLine = startLine; currentLine <= endLine && currentLine < vimState.document.lineCount; currentLine++ ) { originalLines.push(vimState.document.lineAt(currentLine).text); } const lastLineLength = originalLines[originalLines.length - 1].length; if (this.arguments.unique) { const seen = new Set(); const uniqueLines: string[] = []; for (const line of originalLines) { const adjustedLine = this.arguments.ignoreCase ? line.toLowerCase() : line; if (!seen.has(adjustedLine)) { seen.add(adjustedLine); uniqueLines.push(line); } } originalLines = uniqueLines; } let sortedLines; if (this.arguments.numeric) { sortedLines = originalLines.sort( (a: string, b: string) => (NumericString.parse(a, NumericStringRadix.Dec)?.num.value ?? Number.MAX_VALUE) - (NumericString.parse(b, NumericStringRadix.Dec)?.num.value ?? Number.MAX_VALUE), ); } else if (this.arguments.ignoreCase) { sortedLines = originalLines.sort((a: string, b: string) => a.localeCompare(b)); } else { sortedLines = originalLines.sort(); } if (this.arguments.reverse) { sortedLines.reverse(); } const sortedContent = sortedLines.join('\n'); vimState.recordedState.transformer.replace( new vscode.Range(startLine, 0, endLine, lastLineLength), sortedContent, PositionDiff.exactPosition( new vscode.Position(startLine, sortedLines[0].match(/\S/)?.index ?? 0), ), ); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { const { start, end } = range.resolve(vimState); await this.sortLines(vimState, start, end); } } ================================================ FILE: src/cmd_line/commands/substitute.ts ================================================ import { Parser, alt, // eslint-disable-next-line id-denylist any, noneOf, oneOf, optWhitespace, regexp, seq, // eslint-disable-next-line id-denylist string, } from 'parsimmon'; import { CancellationTokenSource, DecorationOptions, Position, Range, window } from 'vscode'; import { PositionDiff } from '../../common/motion/position'; import { configuration } from '../../configuration/configuration'; import { decoration } from '../../configuration/decoration'; import { VimError } from '../../error'; import { Jump } from '../../jumps/jump'; import { globalState } from '../../state/globalState'; import { SearchState } from '../../state/searchState'; import { SubstituteState } from '../../state/substituteState'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { SearchDecorations, ensureVisible, formatDecorationText } from '../../util/decorationUtils'; import { escapeCSSIcons } from '../../util/statusBarTextUtils'; import { ExCommand } from '../../vimscript/exCommand'; import { str } from '../../vimscript/expression/build'; import { displayValue } from '../../vimscript/expression/displayValue'; import { EvaluationContext } from '../../vimscript/expression/evaluate'; import { expressionParser } from '../../vimscript/expression/parser'; import { Expression } from '../../vimscript/expression/types'; import { Address, LineRange } from '../../vimscript/lineRange'; import { numberParser } from '../../vimscript/parserUtils'; import { Pattern, PatternMatch, SearchDirection } from '../../vimscript/pattern'; type ReplaceStringComponent = | { type: 'string'; value: string } | { type: 'capture_group'; group: number | '&' } | { type: 'prev_replace_string' } | { type: 'expression'; expression: Expression } | { type: 'change_case'; case: 'upper' | 'lower'; duration: 'char' | 'until_end' } | { type: 'change_case_end' }; export class ReplaceString { private components: ReplaceStringComponent[]; constructor(components: ReplaceStringComponent[]) { this.components = components; } public toString(): string { return this.components .map((component) => { if (component.type === 'string') { return component.value; } else if (component.type === 'capture_group') { return component.group === '&' ? '&' : `\\${component.group}`; } else if (component.type === 'prev_replace_string') { return '~'; } else if (component.type === 'expression') { return '\\='; // TODO } else if (component.type === 'change_case') { if (component.case === 'upper') { return component.duration === 'char' ? '\\u' : '\\U'; } else { return component.duration === 'char' ? '\\l' : '\\L'; } } else if (component.type === 'change_case_end') { return '\\E'; } else { const guard: never = component; return ''; } }) .join(''); } public resolve(vimState: VimState, matches: string[]): string { let result = ''; let changeCase: 'upper' | 'lower' | undefined; let changeCaseChar: 'upper' | 'lower' | undefined; for (const component of this.components) { let newChangeCaseChar: 'upper' | 'lower' | undefined; let _result: string = ''; if (component.type === 'string') { _result = component.value; } else if (component.type === 'capture_group') { const group: number = component.group === '&' ? 0 : component.group; _result = matches[group] ?? ''; } else if (component.type === 'prev_replace_string') { _result = globalState.substituteState?.replaceString.toString() ?? ''; } else if (component.type === 'expression') { const ctx = new EvaluationContext(vimState); const value = (() => { try { return ctx.evaluate(component.expression); } catch (e) { return str(''); // TODO: Is this right? } })(); if (value.type === 'list') { for (const item of value.items) { _result += displayValue(item) + '\n'; } } else { _result = displayValue(value); } } else if (component.type === 'change_case') { if (component.duration === 'until_end') { changeCase = component.case; } else { newChangeCaseChar = component.case; } } else if (component.type === 'change_case_end') { changeCase = undefined; } else { const guard: never = component; } if (_result) { if (changeCase) { _result = changeCase === 'upper' ? _result.toLocaleUpperCase() : _result.toLocaleLowerCase(); } if (changeCaseChar) { _result = (changeCaseChar === 'upper' ? _result[0].toLocaleUpperCase() : _result[0].toLocaleLowerCase()) + _result.slice(1); } result += _result; } changeCaseChar = newChangeCaseChar; } return result; } } /** * NOTE: for "pattern", undefined is different from an empty string. * when it's undefined, it means to repeat the previous REPLACEMENT and ignore "replace". * when it's an empty string, it means to use the previous SEARCH (not replacement) state, * and replace with whatever's set by "replace" (even an empty string). */ export interface ISubstituteCommandArguments { pattern: Pattern | undefined; replace: ReplaceString; flags: SubstituteFlags; count?: number; } /** * The flags that you can use for the substitute commands: * [&] Must be the first one: Keep the flags from the previous substitute command. * [c] Confirm each substitution. * [e] When the search pattern fails, do not issue an error message and, in * particular, continue in maps as if no error occurred. * [g] Replace all occurrences in the line. Without this argument, replacement * occurs only for the first occurrence in each line. * [i] Ignore case for the pattern. * [I] Don't ignore case for the pattern. * [n] Report the number of matches, do not actually substitute. * [p] Print the line containing the last substitute. * [#] Like [p] and prepend the line number. * [l] Like [p] but print the text like |:list|. * [r] When the search pattern is empty, use the previously used search pattern * instead of the search pattern from the last substitute or ":global". */ export interface SubstituteFlags { keepPreviousFlags?: true; // TODO: use this flag confirmEach?: true; suppressError?: true; // TODO: use this flag replaceAll?: true; ignoreCase?: true; // TODO: use this flag noIgnoreCase?: true; // TODO: use this flag printCount?: true; // TODO: use the following flags: printLastMatchedLine?: true; printLastMatchedLineWithNumber?: true; printLastMatchedLineWithList?: true; usePreviousPattern?: true; } // TODO: `:help sub-replace-special` const replaceStringParser = (delimiter: string): Parser => alt( string('\\=') .then(expressionParser) .map((expr) => [{ type: 'expression', expression: expr }]), alt( string('\\').then( // eslint-disable-next-line id-denylist any.fallback(undefined).map((escaped) => { if (escaped === undefined || escaped === '\\') { return { type: 'string', value: '\\' }; } else if (escaped === '/') { return { type: 'string', value: '/' }; } else if (escaped === 'b') { return { type: 'string', value: '\b' }; } else if (escaped === 'r') { return { type: 'string', value: '\r' }; } else if (escaped === 'n') { return { type: 'string', value: '\n' }; } else if (escaped === 't') { return { type: 'string', value: '\t' }; } else if (escaped === '&') { return { type: 'string', value: '&' }; } else if (escaped === '~') { return { type: 'string', value: '~' }; } else if (/[0-9]/.test(escaped)) { return { type: 'capture_group', group: Number.parseInt(escaped, 10) }; } else if (escaped === 'u') { return { type: 'change_case', case: 'upper', duration: 'char' }; } else if (escaped === 'l') { return { type: 'change_case', case: 'lower', duration: 'char' }; } else if (escaped === 'U') { return { type: 'change_case', case: 'upper', duration: 'until_end' }; } else if (escaped === 'L') { return { type: 'change_case', case: 'lower', duration: 'until_end' }; } else if (escaped === 'e' || escaped === 'E') { return { type: 'change_case_end' }; } else { return { type: 'string', value: `\\${escaped}` }; } }), ), string('&').result({ type: 'capture_group', group: '&' }), string('~').result({ type: 'prev_replace_string' }), noneOf(delimiter).map((value) => ({ type: 'string', value })), ).many(), ).map((components) => new ReplaceString(components)); const substituteFlagsParser: Parser = seq( string('&').fallback(undefined), oneOf('cegiInp#lr').many(), ).map(([amp, flagChars]) => { const flags: SubstituteFlags = {}; if (amp === '&') { flags.keepPreviousFlags = true; } for (const flag of flagChars) { switch (flag) { case 'c': flags.confirmEach = true; break; case 'e': flags.suppressError = true; break; case 'g': flags.replaceAll = true; break; case 'i': flags.ignoreCase = true; break; case 'I': flags.noIgnoreCase = true; break; case 'n': flags.printCount = true; break; case 'p': flags.printLastMatchedLine = true; break; case '#': flags.printLastMatchedLineWithNumber = true; break; case 'l': flags.printLastMatchedLineWithList = true; break; case 'r': flags.usePreviousPattern = true; break; } } return flags; }); const countParser: Parser = optWhitespace .then(numberParser) .fallback(undefined); /** * vim has a distinctly different state for previous search and for previous substitute. However, in SOME * cases a substitution will also update the search state along with the substitute state. * * Also, the substitute command itself will sometimes use the search state, and other times it will use the * substitute state. * * These are the following cases and how vim handles them: * 1. :s/this/that * - standard search/replace * - update substitution state * - update search state too! * 2. :s or :s [flags] * - use previous SUBSTITUTION state, and repeat previous substitution pattern and replace. * - do not touch search state! * - changing substitution state is dont-care cause we're repeating it ;) * 3. :s/ or :s// or :s/// * - use previous SEARCH state (not substitution), and DELETE the string matching the pattern (replace with nothing) * - update substitution state * - updating search state is dont-care cause we're repeating it ;) * 4. :s/this or :s/this/ or :s/this// * - input is pattern - replacement is empty (delete) * - update replacement state * - update search state too! */ export class SubstituteCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace.then( alt( // :s[ubstitute]/{pattern}/{string}/[flags] [count] regexp(/[^\w\s\\|"]{1}/).chain((delimiter) => seq( Pattern.parser({ direction: SearchDirection.Forward, delimiter }), replaceStringParser(delimiter), string(delimiter).then(substituteFlagsParser).fallback({}), countParser, ).map( ([pattern, replace, flags, count]) => new SubstituteCommand({ pattern, replace, flags, count }), ), ), // :s[ubstitute] [flags] [count] seq(substituteFlagsParser, countParser).map( ([flags, count]) => new SubstituteCommand({ pattern: undefined, replace: new ReplaceString([]), flags, count, }), ), ), ); public readonly arguments: ISubstituteCommandArguments; protected abort: boolean; private cSearchHighlights?: DecorationOptions[]; private confirmedSubstitutions?: DecorationOptions[]; constructor(args: ISubstituteCommandArguments) { super(); this.arguments = args; this.abort = false; } public override neovimCapable(): boolean { // We need to use VSCode's quickpick capabilities to do confirmation return !this.arguments.flags.confirmEach; } public getSubstitutionDecorations( vimState: VimState, lineRange = new LineRange(new Address({ type: 'current_line' })), ): SearchDecorations { const substitutionAppend: DecorationOptions[] = []; const substitutionReplace: DecorationOptions[] = []; const searchHighlight: DecorationOptions[] = []; const subsArr: DecorationOptions[] = configuration.inccommand === 'replace' ? substitutionReplace : substitutionAppend; const { pattern, replace } = this.resolvePatterns(false); const showReplacements = this.arguments.pattern?.closed && configuration.inccommand && !this.arguments.flags.printCount; let matches: PatternMatch[] = []; if (pattern?.patternString) { matches = pattern.allMatches(vimState, { lineRange }); } const global = (configuration.gdefault || configuration.substituteGlobalFlag) !== (this.arguments.flags.replaceAll ?? false); const lines = new Set(); for (const match of matches) { if (!global && lines.has(match.range.start.line)) { // If not global, only replace one match per line continue; } lines.add(match.range.start.line); if (showReplacements) { const contentText = formatDecorationText( replace.resolve(vimState, match.groups), vimState.editor.options.tabSize as number, ); subsArr.push({ range: match.range, renderOptions: { [configuration.inccommand === 'append' ? 'after' : 'before']: { contentText }, }, }); } else { searchHighlight.push(ensureVisible(match.range, vimState.document)); } } return { substitutionAppend, substitutionReplace, searchHighlight }; } /** * @returns If match, (# newlines added) - (# newlines removed) * Else, undefined */ private async replaceMatchRange( vimState: VimState, match: PatternMatch, ): Promise { if (this.arguments.flags.printCount) { return 0; } const replaceText = this.arguments.replace.resolve(vimState, match.groups); if (this.arguments.flags.confirmEach) { if (await this.confirmReplacement(vimState, match, replaceText)) { vimState.recordedState.transformer.replace(match.range, replaceText); } else { return undefined; } } else { vimState.recordedState.transformer.replace(match.range, replaceText); } const addedNewlines = replaceText.split('\n').length - 1; const removedNewlines = match.groups[0].split('\n').length - 1; return addedNewlines - removedNewlines; } private async confirmReplacement( vimState: VimState, match: PatternMatch, replaceText: string, ): Promise { const cancellationToken = new CancellationTokenSource(); const validSelections: readonly string[] = ['y', 'n', 'a', 'q', 'l']; let selection: string = ''; const prompt = escapeCSSIcons( `Replace with ${formatDecorationText( replaceText, vimState.editor.options.tabSize as number, '\\n', )} (${validSelections.join('/')})?`, ); const newConfirmationSearchHighlights = this.cSearchHighlights?.filter((d) => !d.range.isEqual(match.range)) ?? []; vimState.editor.revealRange(new Range(match.range.start.line, 0, match.range.start.line, 0)); vimState.editor.setDecorations(decoration.searchHighlight, newConfirmationSearchHighlights); vimState.editor.setDecorations(decoration.searchMatch, [ ensureVisible(match.range, vimState.document), ]); vimState.editor.setDecorations( decoration.confirmedSubstitution, this.confirmedSubstitutions ?? [], ); await window.showInputBox( { ignoreFocusOut: true, prompt, placeHolder: validSelections.join('/'), validateInput: (input: string): string => { if (validSelections.includes(input)) { selection = input; cancellationToken.cancel(); } return prompt; }, }, cancellationToken.token, ); if (selection === 'q' || selection === 'l' || !selection) { this.abort = true; } else if (selection === 'a') { this.arguments.flags.confirmEach = undefined; } if (selection === 'y' || selection === 'a' || selection === 'l') { if (this.cSearchHighlights) { this.cSearchHighlights = newConfirmationSearchHighlights; } this.confirmedSubstitutions?.push({ range: match.range, renderOptions: { before: { contentText: formatDecorationText( replaceText, vimState.editor.options.tabSize as number, ), }, }, }); return true; } return false; } /** * @returns the concrete Pattern and ReplaceString to be used for this substitution. * If throwErrors is true, errors will be thrown :) */ private resolvePatterns(throwErrors: boolean): { pattern: Pattern | undefined; replace: ReplaceString; } { let { pattern, replace } = this.arguments; if (pattern === undefined) { // If no pattern is entered, use previous SUBSTITUTION state and don't update search state // i.e. :s const prevSubstituteState = globalState.substituteState; if ( prevSubstituteState?.searchPattern === undefined || prevSubstituteState.searchPattern.patternString === '' ) { if (throwErrors) { throw VimError.NoPreviousSubstituteRegularExpression(); } } else { pattern = prevSubstituteState.searchPattern; replace = prevSubstituteState.replaceString; } } else { if (pattern.patternString === '') { // If an explicitly empty pattern is entered, use previous search state (including search with * and #) and update both states // e.g :s/ or :s/// const prevSearchState = globalState.searchState; if (prevSearchState === undefined || prevSearchState.searchString === '') { if (throwErrors) { throw VimError.NoPreviousRegularExpression(); } } else { pattern = prevSearchState.pattern; } } } return { pattern, replace }; } async execute(vimState: VimState): Promise { await this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' }))); } override async executeWithRange(vimState: VimState, lineRange: LineRange): Promise { let { start, end } = lineRange.resolve(vimState); if (this.arguments.count && this.arguments.count >= 0) { start = end; end = end + this.arguments.count - 1; } // TODO: this is all a bit gross const { pattern, replace } = this.resolvePatterns(true); this.arguments.replace = replace; // `/g` flag inverts the default behavior (from `gdefault`) const global = (configuration.gdefault || configuration.substituteGlobalFlag) !== (this.arguments.flags.replaceAll ?? false); // TODO: `allMatches` lies for patterns with empty branches, which makes this wrong (not that anyone cares) const allMatches = pattern?.allMatches(vimState, { // TODO: This method should probably take start/end lines as numbers lineRange: new LineRange( new Address({ type: 'number', num: start + 1 }), ',', new Address({ type: 'number', num: end + 1 }), ), }) ?? []; let replaceableMatches; if (global) { // every match is replaceable replaceableMatches = allMatches; } else { // only the first match on a line is replaceable const replaceableLines = new Set(); replaceableMatches = allMatches.filter((match) => { if (replaceableLines.has(match.range.start.line)) { return false; } replaceableLines.add(match.range.start.line); return true; }); } if (this.arguments.flags.confirmEach) { vimState.editor.setDecorations(decoration.substitutionAppend, []); vimState.editor.setDecorations(decoration.substitutionReplace, []); if (configuration.inccommand) { this.confirmedSubstitutions = []; } if (configuration.incsearch) { this.cSearchHighlights = replaceableMatches.map((match) => ensureVisible(match.range, vimState.document), ); } } const substitutionLines = new Set(); let substitutions = 0; let netNewLines = 0; for (const match of replaceableMatches) { if (this.abort) { break; } const newLines = await this.replaceMatchRange(vimState, match); if (newLines !== undefined) { substitutions++; substitutionLines.add(match.range.start.line); netNewLines += newLines; } } if (substitutions > 0 && !this.arguments.flags.printCount) { // if any substitutions were made, jump to latest one const lastLine = Math.max(...substitutionLines.values()) + netNewLines; const cursor = new Position(Math.max(0, lastLine), 0); globalState.jumpTracker.recordJump( new Jump({ document: vimState.document, position: cursor, }), Jump.fromStateNow(vimState), ); vimState.recordedState.transformer.moveCursor(PositionDiff.exactPosition(cursor), 0); } this.confirmedSubstitutions = undefined; this.cSearchHighlights = undefined; vimState.editor.setDecorations(decoration.confirmedSubstitution, []); this.setStatusBarText(vimState, substitutions, substitutionLines.size); if (this.arguments.pattern !== undefined) { globalState.substituteState = new SubstituteState(pattern, replace); globalState.searchState = new SearchState( SearchDirection.Forward, vimState.cursorStopPosition, pattern?.patternString, {}, ); } } private setStatusBarText(vimState: VimState, substitutions: number, lines: number) { if (substitutions === 0) { StatusBar.displayError( vimState, VimError.PatternNotFound(this.arguments.pattern?.patternString), ); } else if (this.arguments.flags.printCount) { StatusBar.setText( vimState, `${substitutions} match${substitutions > 1 ? 'es' : ''} on ${lines} line${ lines > 1 ? 's' : '' }`, ); } else if (substitutions > configuration.report) { StatusBar.setText( vimState, `${substitutions} substitution${substitutions > 1 ? 's' : ''} on ${lines} line${ lines > 1 ? 's' : '' }`, ); } } } ================================================ FILE: src/cmd_line/commands/tab.ts ================================================ // eslint-disable-next-line id-denylist import { all, alt, optWhitespace, seq, string, whitespace } from 'parsimmon'; import * as path from 'path'; import * as vscode from 'vscode'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { findTabInActiveTabGroup } from '../../util/util'; import { ExCommand } from '../../vimscript/exCommand'; import { FileCmd, FileOpt, bangParser, fileCmdParser, fileNameParser, fileOptParser, numberParser, } from '../../vimscript/parserUtils'; export enum TabCommandType { Next, Previous, First, Last, Edit, New, Close, Only, Move, } // TODO: many of these arguments aren't used export type ITabCommandArguments = | { type: TabCommandType.Edit; cmd?: FileCmd; buf: string | number; } | { type: TabCommandType.First | TabCommandType.Last; cmd?: FileCmd; } | { type: TabCommandType.Next | TabCommandType.Previous; bang: boolean; cmd?: FileCmd; count?: number; } | { type: TabCommandType.Close | TabCommandType.Only; bang: boolean; count?: number; } | { type: TabCommandType.New; opt: FileOpt; cmd?: FileCmd; file?: string; } | { type: TabCommandType.Move; direction?: 'left' | 'right'; count?: number; }; // // Implements most buffer and tab ex commands // http://vimdoc.sourceforge.net/htmldoc/tabpage.html // export class TabCommand extends ExCommand { // TODO: `count` is parsed as a number, which is incomplete public static readonly argParsers = { bfirst: whitespace .then(fileCmdParser) .fallback(undefined) .map((cmd) => { return new TabCommand({ type: TabCommandType.First, cmd }); }), blast: whitespace .then(fileCmdParser) .fallback(undefined) .map((cmd) => { return new TabCommand({ type: TabCommandType.Last, cmd }); }), bnext: seq( bangParser, optWhitespace.then(fileCmdParser).fallback(undefined), optWhitespace.then(numberParser).fallback(undefined), ).map(([bang, cmd, count]) => { return new TabCommand({ type: TabCommandType.Next, bang, cmd, count }); }), bprev: seq( bangParser, optWhitespace.then(fileCmdParser).fallback(undefined), optWhitespace.then(numberParser).fallback(undefined), ).map(([bang, cmd, count]) => { return new TabCommand({ type: TabCommandType.Previous, bang, cmd, count }); }), buffer: seq( optWhitespace.then(fileCmdParser).fallback(undefined), alt( optWhitespace.then(numberParser), optWhitespace.then(fileNameParser), ).fallback(undefined), ).map(([cmd, arg]) => { return new TabCommand({ type: TabCommandType.Edit, cmd, buf: arg ?? 0 }); }), tabclose: seq(bangParser, optWhitespace.then(numberParser).fallback(undefined)).map( ([bang, count]) => { return new TabCommand({ type: TabCommandType.Close, bang, count }); }, ), tabonly: seq(bangParser, optWhitespace.then(numberParser).fallback(undefined)).map( ([bang, count]) => { return new TabCommand({ type: TabCommandType.Only, bang, count }); }, ), tabnew: seq( optWhitespace.then(fileOptParser).fallback([]), optWhitespace.then(fileCmdParser).fallback(undefined), optWhitespace.then(fileNameParser).fallback(undefined), ).map(([opt, cmd, file]) => { return new TabCommand({ type: TabCommandType.New, opt, cmd, file, }); }), tabmove: optWhitespace .then( seq( alt(string('+'), string('-')).fallback(undefined), numberParser.fallback(undefined), all, ), ) .map(([plusminus, count, trailing]) => { if (trailing || (plusminus && count === 0)) { throw VimError.InvalidArgument475((plusminus ?? '') + (count ?? '') + trailing); } const direction = plusminus === '+' ? 'right' : plusminus === '-' ? 'left' : undefined; return new TabCommand({ type: TabCommandType.Move, direction, count, }); }), }; public readonly arguments: ITabCommandArguments; constructor(args: ITabCommandArguments) { super(); this.arguments = args; } private async executeCommandWithCount(count: number, command: string): Promise { for (let i = 0; i < count; i++) { await vscode.commands.executeCommand(command); } } async execute(vimState: VimState): Promise { switch (this.arguments.type) { case TabCommandType.Edit: if ( this.arguments.buf !== undefined && typeof this.arguments.buf === 'number' && this.arguments.buf >= 0 ) { await vscode.commands.executeCommand( 'workbench.action.openEditorAtIndex', this.arguments.buf - 1, ); } else if (this.arguments.buf !== undefined && typeof this.arguments.buf === 'string') { const [idx, tab] = findTabInActiveTabGroup(this.arguments.buf); if ((tab.input as vscode.TextDocument).uri !== undefined) { const uri = (tab.input as vscode.TextDocument).uri; await vscode.commands.executeCommand('vscode.open', uri); } } break; case TabCommandType.Next: if (this.arguments.count !== undefined && this.arguments.count <= 0) { break; } if (this.arguments.count) { const tabGroup = vscode.window.tabGroups.activeTabGroup; if (0 < this.arguments.count && this.arguments.count <= tabGroup.tabs.length) { const tab = tabGroup.tabs[this.arguments.count - 1]; if ((tab.input as vscode.TextDocument).uri !== undefined) { const uri = (tab.input as vscode.TextDocument).uri; await vscode.commands.executeCommand('vscode.open', uri); } } } else { await vscode.commands.executeCommand('workbench.action.nextEditorInGroup'); } break; case TabCommandType.Previous: if (this.arguments.count !== undefined && this.arguments.count <= 0) { break; } await this.executeCommandWithCount( this.arguments.count || 1, 'workbench.action.previousEditorInGroup', ); break; case TabCommandType.First: await vscode.commands.executeCommand('workbench.action.openEditorAtIndex1'); break; case TabCommandType.Last: await vscode.commands.executeCommand('workbench.action.lastEditorInGroup'); break; case TabCommandType.New: { const hasFile = !(this.arguments.file === undefined || this.arguments.file === ''); if (hasFile) { const isAbsolute = path.isAbsolute(this.arguments.file!); const currentFilePath = vscode.window.activeTextEditor!.document.uri.fsPath; const isInWorkspace = vscode.workspace.workspaceFolders !== undefined && vscode.workspace.workspaceFolders.length > 0; let toOpenPath: string; if (isAbsolute) { toOpenPath = this.arguments.file!; } else if (isInWorkspace) { const workspacePath = vscode.workspace.workspaceFolders![0].uri.path; if (currentFilePath.startsWith(workspacePath)) { toOpenPath = path.join(path.dirname(currentFilePath), this.arguments.file!); } else { toOpenPath = path.join(workspacePath, this.arguments.file!); } } else { toOpenPath = path.join(path.dirname(currentFilePath), this.arguments.file!); } if (toOpenPath !== currentFilePath) { await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(toOpenPath)); } } else { await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); } break; } case TabCommandType.Close: // Navigate the correct position if (this.arguments.count === undefined) { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); break; } if (this.arguments.count === 0) { // Wrong paramter break; } // TODO: Close Page {count}. Page count is one-based. break; case TabCommandType.Only: await vscode.commands.executeCommand('workbench.action.closeOtherEditors'); break; case TabCommandType.Move: { const { count, direction } = this.arguments; let args; if (direction !== undefined) { args = { to: direction, by: 'tab', value: count ?? 1 }; } else if (count === 0) { args = { to: 'first' }; } else if (count === undefined) { args = { to: 'last' }; } else { args = { to: 'position', by: 'tab', value: count + 1 }; } await vscode.commands.executeCommand('moveActiveEditor', args); break; } default: break; } } } ================================================ FILE: src/cmd_line/commands/terminal.ts ================================================ import { Parser, succeed } from 'parsimmon'; import * as vscode from 'vscode'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; export class TerminalCommand extends ExCommand { public static readonly argParser: Parser = succeed(new TerminalCommand()); async execute(vimState: VimState): Promise { await vscode.commands.executeCommand('workbench.action.createTerminalEditor'); } } ================================================ FILE: src/cmd_line/commands/undo.ts ================================================ import { optWhitespace, Parser } from 'parsimmon'; import { Position } from 'vscode'; import { Undo } from '../../actions/commands/undo'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { numberParser } from '../../vimscript/parserUtils'; // // Implements :u[ndo] // http://vimdoc.sourceforge.net/htmldoc/undo.html // export class UndoCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace .then(numberParser) .fallback(undefined) .map((count) => new UndoCommand(count)); private count?: number; private constructor(count?: number) { super(); this.count = count; } async execute(vimState: VimState): Promise { // TODO: Use `this.count` await new Undo().exec(new Position(0, 0), vimState); } } ================================================ FILE: src/cmd_line/commands/vscode.ts ================================================ import { all, Parser, whitespace } from 'parsimmon'; import * as vscode from 'vscode'; import { VimError } from '../../error'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; export class VsCodeCommand extends ExCommand { public static readonly argParser: Parser = whitespace .then(all) .map((command) => new VsCodeCommand(command)); private command: string; public constructor(command: string) { super(); this.command = command; if (!this.command) { throw VimError.ArgumentRequired(); } } async execute(vimState: VimState): Promise { await vscode.commands.executeCommand(this.command); } } ================================================ FILE: src/cmd_line/commands/wall.ts ================================================ import { Parser } from 'parsimmon'; import * as vscode from 'vscode'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser } from '../../vimscript/parserUtils'; // // Implements :wall (write all) // http://vimdoc.sourceforge.net/htmldoc/editing.html#:wall // export class WallCommand extends ExCommand { public static readonly argParser: Parser = bangParser.map( (bang) => new WallCommand(bang), ); private readonly bang: boolean; constructor(bang?: boolean) { super(); this.bang = bang ?? false; } async execute(vimState: VimState): Promise { // TODO : overwrite readonly files when bang? == true await vscode.workspace.saveAll(false); } } ================================================ FILE: src/cmd_line/commands/write.ts ================================================ // eslint-disable-next-line id-denylist import { all, alt, optWhitespace, Parser, seq, string } from 'parsimmon'; import * as path from 'path'; import * as fs from 'platform/fs'; import * as vscode from 'vscode'; import { VimState } from '../../state/vimState'; import { StatusBar } from '../../statusBar'; import { Logger } from '../../util/logger'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser, fileNameParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils'; export type IWriteCommandArguments = { bang: boolean; opt: FileOpt; bgWrite: boolean; file?: string; cmd?: string; }; // // Implements :write // http://vimdoc.sourceforge.net/htmldoc/editing.html#:write // export class WriteCommand extends ExCommand { public static readonly argParser: Parser = seq( bangParser.skip(optWhitespace), fileOptParser.skip(optWhitespace), alt<{ cmd: string } | { file: string }>( string('!') .then(all) .map((cmd) => { return { cmd }; }), fileNameParser.map((file) => { return { file }; }), // TODO: Support `:help :w_a` ('>>') ).fallback({}), ).map(([bang, opt, other]) => new WriteCommand({ bang, opt, bgWrite: true, ...other })); public readonly arguments: IWriteCommandArguments; constructor(args: IWriteCommandArguments) { super(); this.arguments = args; } async execute(vimState: VimState): Promise { // TODO: Use arguments: opt, file, cmd // If the file isn't on disk because it's brand new or on a remote file system, let VS Code handle it if (vimState.document.isUntitled || vimState.document.uri.scheme !== 'file') { await this.background(vscode.commands.executeCommand('workbench.action.files.save')); return; } try { if (this.arguments.file) { await this.saveAs(vimState, this.arguments.file); } else { await fs.accessAsync(vimState.document.fileName, fs.constants.W_OK); await this.save(vimState); } } catch (accessErr) { if (this.arguments.bang) { try { const mode = await fs.getMode(vimState.document.fileName); await fs.chmodAsync(vimState.document.fileName, 0o666); // We must do a foreground write so we can await the save // and chmod the file back to its original state this.arguments.bgWrite = false; await this.save(vimState); await fs.chmodAsync(vimState.document.fileName, mode); } catch (e) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access StatusBar.setText(vimState, e.message); } } else { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access StatusBar.setText(vimState, accessErr.message); } } } // TODO: Aparentemente foi tudo, claro que falta alguns ERRORS e bla bla bla tipo o do :w 8/ E212 mas fds o E357 sobrepoe // TODO: fazer PR (#1876) private async saveAs(vimState: VimState, fileName: string): Promise { try { const filePath = path.resolve(path.dirname(vimState.document.fileName), fileName); const fileExists = await fs.existsAsync(filePath); const uri = vscode.Uri.file(path.resolve(path.dirname(vimState.document.fileName), filePath)); // An extension to the file must be specified. if (path.extname(filePath) === '') { StatusBar.setText(vimState, `E357: The file extension must be specified`, true); return; } // Checks if the file exists. if (fileExists) { const stats = await vscode.workspace.fs.stat(uri); const isDirectory = stats.type === vscode.FileType.Directory; // If it's a directory, throw an error. if (isDirectory) { StatusBar.setText(vimState, `E17: "${filePath}" is a directory`, true); return; } // Create a pop-up asking if user wants to overwrite the file. const confirmOverwrite = await vscode.window.showWarningMessage( `File "${fileName}" already exists. Do you want to overwrite it?`, { modal: true }, 'Yes', 'No', ); if (confirmOverwrite === 'No') { return; } } // Create a new file in 'filePath', appending the current's file content to it. await vscode.window.showTextDocument(vimState.document, { preview: false }); await vscode.commands.executeCommand('workbench.action.files.save', uri); await vscode.workspace.fs.copy(vimState.document.uri, uri, { overwrite: true }); StatusBar.setText( vimState, `"${fileName}" ${fileExists ? '' : '[New]'} ${vimState.document.lineCount}L ${ vimState.document.getText().length }C written`, ); } catch (e) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access StatusBar.setText(vimState, e.message); } } private async save(vimState: VimState): Promise { if (this.shouldShowDocument(vimState.document.uri)) { await vscode.window.showTextDocument(vimState.document, { preview: false }); } await this.background( vimState.document.save().then((success) => { if (success) { StatusBar.setText( vimState, `"${path.basename(vimState.document.fileName)}" ${vimState.document.lineCount}L ${ vimState.document.getText().length }C written`, ); } else { Logger.warn(':w failed'); // TODO: What's the right thing to do here? } }), ); } /** * Determines whether to call showTextDocument when saving. * Avoids disrupting diff views and handles preview tab pinning. */ private shouldShowDocument(documentUri: vscode.Uri): boolean { const uriString = documentUri.toString(); const matchingTab = vscode.window.tabGroups.activeTabGroup.tabs.find((tab: vscode.Tab) => this.tabContainsDocument(tab, uriString), ); if (matchingTab) { return matchingTab.isPreview; } // No matching tab found, show it return true; } /** * Check if a tab contains the specified document URI. * Handles regular tabs and diff tabs. */ private tabContainsDocument(tab: vscode.Tab, uriString: string): boolean { const input = tab.input as | { uri?: vscode.Uri; original?: vscode.Uri; modified?: vscode.Uri } | undefined; return ( input?.uri?.toString() === uriString || input?.original?.toString() === uriString || input?.modified?.toString() === uriString ); } private async background(fn: Thenable): Promise { if (!this.arguments.bgWrite) { await fn; } } } ================================================ FILE: src/cmd_line/commands/writequit.ts ================================================ import { optWhitespace, Parser, seq } from 'parsimmon'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser, fileNameParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils'; import { QuitCommand } from './quit'; import { WriteCommand } from './write'; // // Implements :writequit // http://vimdoc.sourceforge.net/htmldoc/editing.html#write-quit // export interface IWriteQuitCommandArguments { bang: boolean; opt: FileOpt; file?: string; } export class WriteQuitCommand extends ExCommand { public static readonly argParser: Parser = seq( bangParser.skip(optWhitespace), fileOptParser.skip(optWhitespace), fileNameParser.fallback(undefined), ).map(([bang, opt, file]) => new WriteQuitCommand(file ? { bang, opt, file } : { bang, opt })); private readonly args: IWriteQuitCommandArguments; constructor(args: IWriteQuitCommandArguments) { super(); this.args = args; } // Writing command. Taken as a basis from the "write.ts" file. async execute(vimState: VimState): Promise { await new WriteCommand({ bgWrite: false, ...this.args }).execute(vimState); await new QuitCommand({ // wq! fails when no file name is provided bang: false, }).execute(vimState); } } ================================================ FILE: src/cmd_line/commands/writequitall.ts ================================================ import { Parser, seq, whitespace } from 'parsimmon'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { bangParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils'; import * as wall from '../commands/wall'; import * as quit from './quit'; // // Implements :writequitall // http://vimdoc.sourceforge.net/htmldoc/editing.html#:wqall // export interface IWriteQuitAllCommandArguments { bang: boolean; fileOpt: FileOpt; } export class WriteQuitAllCommand extends ExCommand { public static readonly argParser: Parser = seq( bangParser, whitespace.then(fileOptParser).fallback([]), ).map(([bang, fileOpt]) => new WriteQuitAllCommand({ bang, fileOpt })); private readonly arguments: IWriteQuitAllCommandArguments; constructor(args: IWriteQuitAllCommandArguments) { super(); this.arguments = args; } // Writing command. Taken as a basis from the "write.ts" file. async execute(vimState: VimState): Promise { const quitArgs: quit.IQuitCommandArguments = { // wq! fails when no file name is provided bang: false, }; const wallCmd = new wall.WallCommand(this.arguments.bang); await wallCmd.execute(vimState); // TODO: fileOpt is not used quitArgs.quitAll = true; const quitCmd = new quit.QuitCommand(quitArgs); await quitCmd.execute(vimState); } } ================================================ FILE: src/cmd_line/commands/yank.ts ================================================ // eslint-disable-next-line id-denylist import { alt, any, optWhitespace, Parser, seq, whitespace } from 'parsimmon'; import { Position } from 'vscode'; import { YankOperator } from '../../actions/operator'; import { RegisterMode } from '../../register/register'; import { VimState } from '../../state/vimState'; import { ExCommand } from '../../vimscript/exCommand'; import { LineRange } from '../../vimscript/lineRange'; import { numberParser } from '../../vimscript/parserUtils'; export interface YankCommandArguments { register?: string; count?: number; } export class YankCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace.then( alt( numberParser.map((count) => { return { register: undefined, count }; }), // eslint-disable-next-line id-denylist seq(any.fallback(undefined), whitespace.then(numberParser).fallback(undefined)).map( ([register, count]) => { return { register, count }; }, ), ).map( ({ register, count }) => new YankCommand({ register, count, }), ), ); private readonly arguments: YankCommandArguments; constructor(args: YankCommandArguments) { super(); this.arguments = args; } private async yank(vimState: VimState, start: Position, end: Position) { vimState.currentRegisterMode = RegisterMode.LineWise; if (this.arguments.register) { vimState.recordedState.registerName = this.arguments.register; } const cursorPosition = vimState.cursorStopPosition; await new YankOperator().run(vimState, start.getLineBegin(), end.getLineEnd()); // YankOperator moves the cursor - undo that vimState.cursorStopPosition = cursorPosition; } async execute(vimState: VimState): Promise { const linesToYank = this.arguments.count ?? 1; const startPosition = vimState.cursorStartPosition; const endPosition = linesToYank ? startPosition.getDown(linesToYank - 1).getLineEnd() : vimState.cursorStopPosition; await this.yank(vimState, startPosition, endPosition); } override async executeWithRange(vimState: VimState, range: LineRange): Promise { /** * If a [cnt] and [range] is specified (e.g. :.+2y3), :yank [cnt] is called from * the end of the [range]. * Ex. if two lines are VisualLine highlighted, :<,>y3 will :y3 * from the end of the selected lines. */ const { start, end } = range.resolve(vimState); if (this.arguments.count) { vimState.cursorStartPosition = new Position(end, 0); await this.execute(vimState); return; } await this.yank(vimState, new Position(start, 0), new Position(end, 0)); } } ================================================ FILE: src/common/matching/matcher.ts ================================================ import { Position } from 'vscode'; import { configuration } from '../../configuration/configuration'; import { VimState } from '../../state/vimState'; export type Pairing = { match: string; isNextMatchForward: boolean; directionless?: boolean; }; /** * PairMatcher finds the position matching the given character, respecting nested * instances of the pair. */ export class PairMatcher { static pairings: { [key: string]: Pairing; } = { '(': { match: ')', isNextMatchForward: true }, '{': { match: '}', isNextMatchForward: true }, '[': { match: ']', isNextMatchForward: true }, ')': { match: '(', isNextMatchForward: false }, '}': { match: '{', isNextMatchForward: false }, ']': { match: '[', isNextMatchForward: false }, // These characters can't be used for "%"-based matching, but are still // useful for text objects. // matchesWithPercentageMotion can be overwritten with configuration.matchpairs '<': { match: '>', isNextMatchForward: true }, '>': { match: '<', isNextMatchForward: false }, // These are useful for deleting closing and opening quotes, but don't seem to negatively // affect how text objects such as `ci"` work, which was my worry. '"': { match: '"', isNextMatchForward: false, directionless: true }, "'": { match: "'", isNextMatchForward: false, directionless: true }, '`': { match: '`', isNextMatchForward: false, directionless: true }, }; private static findPairedChar( position: Position, charToFind: string, charToStack: string, stackHeight: number, isNextMatchForward: boolean, vimState: VimState, allowCurrentPosition: boolean, ): Position | undefined { let lineNumber = position.line; const linePosition = position.character; const lineCount = vimState.document.lineCount; const cursorChar = vimState.document.lineAt(position).text[position.character]; if ( allowCurrentPosition && vimState.cursorStartPosition.isEqual(vimState.cursorStopPosition) && cursorChar === charToFind ) { return position; } while (PairMatcher.keepSearching(lineNumber, lineCount, isNextMatchForward)) { let lineText = vimState.document.lineAt(lineNumber).text.split(''); const originalLineLength = lineText.length; if (lineNumber === position.line) { if (isNextMatchForward) { lineText = lineText.slice(linePosition + 1, originalLineLength); } else { lineText = lineText.slice(0, linePosition); } } while (true) { if (lineText.length <= 0 || stackHeight <= -1) { break; } let nextChar: string | undefined; if (isNextMatchForward) { nextChar = lineText.shift(); } else { nextChar = lineText.pop(); } if (nextChar === charToStack) { stackHeight++; } else if (nextChar === charToFind) { stackHeight--; } else { continue; } } if (stackHeight <= -1) { let pairMemberChar: number; if (isNextMatchForward) { pairMemberChar = Math.max(0, originalLineLength - lineText.length - 1); } else { pairMemberChar = lineText.length; } return new Position(lineNumber, pairMemberChar); } if (isNextMatchForward) { lineNumber++; } else { lineNumber--; } } return undefined; } private static keepSearching(lineNumber: number, lineCount: number, isNextMatchForward: boolean) { if (isNextMatchForward) { return lineNumber <= lineCount - 1; } else { return lineNumber >= 0; } } static getPercentPairing(char: string): Pairing | undefined { for (const pairing of configuration.matchpairs.split(',')) { const components = pairing.split(':'); if (components.length === 2) { if (components[0] === char) { return { match: components[1], isNextMatchForward: true, }; } else if (components[1] === char) { return { match: components[0], isNextMatchForward: false, }; } } } return undefined; } static nextPairedChar( position: Position, charToMatch: string, vimState: VimState, allowCurrentPosition: boolean, ): Position | undefined { /** * We do a fairly basic implementation that only tracks the state of the type of * character you're over and its pair (e.g. "[" and "]"). This is similar to * what Vim does. * * It can't handle strings very well - something like "|( ')' )" where | is the * cursor will cause it to go to the ) in the quotes, even though it should skip over it. * * PRs welcomed! (TODO) * Though ideally VSC implements https://github.com/Microsoft/vscode/issues/7177 */ const pairing = this.pairings[charToMatch]; if (pairing === undefined || pairing.directionless) { return undefined; } const stackHeight = 0; const charToFind = pairing.match; const charToStack = charToMatch; return PairMatcher.findPairedChar( position, charToFind, charToStack, stackHeight, pairing.isNextMatchForward, vimState, allowCurrentPosition, ); } } ================================================ FILE: src/common/matching/quoteMatcher.ts ================================================ enum QuoteMatch { Opening, Closing, } /** * QuoteMatcher matches quoted strings, respecting escaped quotes (\") and friends */ export class QuoteMatcher { static readonly escapeChar = '\\'; private readonly quoteMap: QuoteMatch[] = []; constructor(quote: '"' | "'" | '`', corpus: string) { let openingQuote = true; // Loop over corpus, marking quotes and respecting escape characters. for (let i = 0; i < corpus.length; i++) { if (corpus[i] === QuoteMatcher.escapeChar) { i += 1; continue; } if (corpus[i] === quote) { this.quoteMap[i] = openingQuote ? QuoteMatch.Opening : QuoteMatch.Closing; openingQuote = !openingQuote; } } } public surroundingQuotes(cursorIndex: number): [number, number] | undefined { const cursorQuoteType = this.quoteMap[cursorIndex]; if (cursorQuoteType === QuoteMatch.Opening) { const closing = this.getNextQuote(cursorIndex); return closing !== undefined ? [cursorIndex, closing] : undefined; } else if (cursorQuoteType === QuoteMatch.Closing) { return [this.getPrevQuote(cursorIndex)!, cursorIndex]; } else { const opening = this.getPrevQuote(cursorIndex) ?? this.getNextQuote(cursorIndex); if (opening !== undefined) { const closing = this.getNextQuote(opening); if (closing !== undefined) { return [opening, closing]; } } } return undefined; } private getNextQuote(start: number): number | undefined { for (let i = start + 1; i < this.quoteMap.length; i++) { if (this.quoteMap[i] !== undefined) { return i; } } return undefined; } private getPrevQuote(start: number): number | undefined { for (let i = start - 1; i >= 0; i--) { if (this.quoteMap[i] !== undefined) { return i; } } return undefined; } } ================================================ FILE: src/common/matching/tagMatcher.ts ================================================ import { VimState } from '../../state/vimState'; import { TextEditor } from '../../textEditor'; type Tag = { name: string; type: 'close' | 'open'; startPos: number; endPos: number }; type MatchedTag = { tag: string; openingTagStart: number; openingTagEnd: number; closingTagStart: number; closingTagEnd: number; }; export class TagMatcher { // see regexr.com/3t585 static TAG_REGEX = /\<(\/)?([^\>\<\s\/]+)(?:[^\>\<]*?)(\/)?\>/g; static OPEN_FORWARD_SLASH = 1; static TAG_NAME = 2; static CLOSE_FORWARD_SLASH = 3; openStart: number | undefined; openEnd: number | undefined; closeStart: number | undefined; closeEnd: number | undefined; constructor(corpus: string, position: number, vimState: VimState) { let match = TagMatcher.TAG_REGEX.exec(corpus); const tags: Tag[] = []; // Gather all the existing tags. while (match) { // Node is a self closing tag, skip. if (match[TagMatcher.CLOSE_FORWARD_SLASH]) { match = TagMatcher.TAG_REGEX.exec(corpus); continue; } tags.push({ name: match[TagMatcher.TAG_NAME], type: match[TagMatcher.OPEN_FORWARD_SLASH] ? 'close' : 'open', startPos: match.index, endPos: TagMatcher.TAG_REGEX.lastIndex, }); match = TagMatcher.TAG_REGEX.exec(corpus); } const stack: Tag[] = []; const matchedTags: MatchedTag[] = []; for (const tag of tags) { // We have to push on the stack // if it is an open tag. if (tag.type === 'open') { stack.push(tag); } else { // We have an unmatched closing tag, // so try and match it with any existing tag. for (let i = stack.length - 1; i >= 0; i--) { const openNode = stack[i]; if (openNode.type === 'open' && openNode.name === tag.name) { // A matching tag was found, ignore // any tags that were in between. matchedTags.push({ tag: openNode.name, openingTagStart: openNode.startPos, openingTagEnd: openNode.endPos, closingTagStart: tag.startPos, closingTagEnd: tag.endPos, }); stack.splice(i); break; } } } } const firstNonWhitespacePositionOnLine = TextEditor.getFirstNonWhitespaceCharOnLine( vimState.document, vimState.cursorStartPosition.line, ); /** * This adjustment fixes the following situation: * * | * test * * * Now in tag matching situations, the tag opening on the cursor line is considered as well * (if there is only whitespace before the tag and the cursor is standing on these whitespaces) */ const startPos = vimState.cursorStartPosition.character < firstNonWhitespacePositionOnLine.character ? firstNonWhitespacePositionOnLine : vimState.cursorStartPosition; const startPosOffset = vimState.document.offsetAt(startPos); const endPosOffset = position; const tagsSurrounding = matchedTags.filter((n) => { return startPosOffset >= n.openingTagStart && endPosOffset < n.closingTagEnd; }); if (!tagsSurrounding.length) { return; } const nodeSurrounding = this.determineRelevantTag( tagsSurrounding, startPosOffset, vimState.cursorStartPosition.compareTo(vimState.cursorStopPosition) !== 0, ); if (!nodeSurrounding) { return; } this.openStart = nodeSurrounding.openingTagStart; this.closeEnd = nodeSurrounding.closingTagEnd; // if the inner tag content is already selected, expand to enclose tags with 'it' as in vim if ( startPosOffset === nodeSurrounding.openingTagEnd && endPosOffset + 1 === nodeSurrounding.closingTagStart ) { this.openEnd = this.openStart; this.closeStart = this.closeEnd; } else { this.openEnd = nodeSurrounding.openingTagEnd; this.closeStart = nodeSurrounding.closingTagStart; } } /** * Most of the time the relevant tag is the innermost tag, but when Visual mode is active, * the rules are different. * When the cursorStart is standing on the < character of the inner tag, with "at" we must * jump to the outer tag. */ determineRelevantTag( tagsSurrounding: MatchedTag[], adjustedStartPosOffset: number, selectionActive: boolean, ): MatchedTag | undefined { const relevantTag = tagsSurrounding[0]; if (selectionActive && adjustedStartPosOffset === relevantTag.openingTagStart) { // we adjusted so we have to return the outer tag return tagsSurrounding[1]; } else { return relevantTag; } } findOpening(inclusive: boolean): number | undefined { if (inclusive) { return this.openStart; } return this.openEnd; } findClosing(inclusive: boolean): number | undefined { if (inclusive) { return this.closeEnd; } return this.closeStart; } } ================================================ FILE: src/common/motion/cursor.ts ================================================ import { Position, Selection, TextDocument } from 'vscode'; export class Cursor { public readonly start: Position; public readonly stop: Position; constructor(start: Position, stop: Position) { this.start = start; this.stop = stop; } public static atPosition(position: Position): Cursor { return new Cursor(position, position); } /** * Create a Cursor from a VSCode selection. */ public static fromSelection(sel: Selection): Cursor { return new Cursor(sel.anchor, sel.active); } public isValid(document: TextDocument) { return this.start.isValid(document) && this.stop.isValid(document); } public equals(other: Cursor): boolean { return this.start.isEqual(other.start) && this.stop.isEqual(other.stop); } /** * Returns a new Cursor which is the same as this Cursor, but with the provided stop value. */ public withNewStop(stop: Position): Cursor { return new Cursor(this.start, stop); } /** * Returns a new Cursor which is the same as this Cursor, but with the provided start value. */ public withNewStart(start: Position): Cursor { return new Cursor(start, this.stop); } public toString(): string { return `[${this.start.toString()} | ${this.stop.toString()}]`; } public validate(document: TextDocument): Cursor { return new Cursor(document.validatePosition(this.start), document.validatePosition(this.stop)); } } ================================================ FILE: src/common/motion/position.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { getSentenceBegin, getSentenceEnd } from '../../textobject/sentence'; import { WordType, nextWordEnd, nextWordStart, prevWordEnd, prevWordStart, } from '../../textobject/word'; import { clamp } from '../../util/util'; import { configuration } from './../../configuration/configuration'; import { TextEditor } from './../../textEditor'; /** * Controls how a PositionDiff affects the Position it's applied to. */ enum PositionDiffType { /** Sets both the line and character exactly */ ExactPosition, /** Offsets both the line and character */ Offset, /** Offsets the line and sets the column exactly */ ExactCharacter, /** Brings the Position to the beginning of the line if `vim.startofline` is true */ ObeyStartOfLine, /** Brings the Position to the end of the line */ EndOfLine, } /** * Represents a difference between two Positions. * Add it to a Position to get another Position. */ export class PositionDiff { public readonly line: number; public readonly character: number; public readonly type: PositionDiffType; private constructor(type: PositionDiffType, line: number, character: number) { this.type = type; this.line = line; this.character = character; } /** Has no effect */ public static identity(): PositionDiff { return PositionDiff.offset({ line: 0, character: 0 }); } /** Offsets both the Position's line and character */ public static offset({ line = 0, character = 0 }): PositionDiff { return new PositionDiff(PositionDiffType.Offset, line, character); } /** Sets the Position's line and character exactly */ public static exactPosition(position: Position): PositionDiff { return new PositionDiff(PositionDiffType.ExactPosition, position.line, position.character); } /** Brings the Position to the beginning of the line if `vim.startofline` is true */ public static startOfLine(): PositionDiff { return new PositionDiff(PositionDiffType.ObeyStartOfLine, 0, 0); } /** Brings the Position to the end of the line */ public static endOfLine(): PositionDiff { return new PositionDiff(PositionDiffType.EndOfLine, 0, 0); } /** Offsets the Position's line and sets its character exactly */ public static exactCharacter({ lineOffset, character, }: { lineOffset?: number; character: number; }): PositionDiff { return new PositionDiff(PositionDiffType.ExactCharacter, lineOffset ?? 0, character); } public toString(): string { switch (this.type) { case PositionDiffType.Offset: return `[ Diff: Offset ${this.line} ${this.character} ]`; case PositionDiffType.ExactCharacter: return `[ Diff: ExactCharacter ${this.line} ${this.character} ]`; case PositionDiffType.ExactPosition: return `[ Diff: ExactPosition ${this.line} ${this.character} ]`; case PositionDiffType.ObeyStartOfLine: return `[ Diff: ObeyStartOfLine ${this.line} ]`; case PositionDiffType.EndOfLine: return `[ Diff: EndOfLine ${this.line} ]`; default: const guard: never = this.type; throw new Error(`Unknown PositionDiffType: ${this.type}`); } } } /** * @returns the Position of the 2 provided which comes earlier in the document. */ export function earlierOf(p1: Position, p2: Position): Position { return p1.isBefore(p2) ? p1 : p2; } /** * @returns the Position of the 2 provided which comes later in the document. */ export function laterOf(p1: Position, p2: Position): Position { return p1.isBefore(p2) ? p2 : p1; } /** * @returns the given Positions in the order they appear in the document. */ export function sorted(p1: Position, p2: Position): [Position, Position] { return p1.isBefore(p2) ? [p1, p2] : [p2, p1]; } declare module 'vscode' { interface Position { toString(): string; add(document: vscode.TextDocument, diff: PositionDiff, boundsCheck?: boolean): Position; subtract(other: Position): PositionDiff; /** * @returns a new Position with the same line and the given character. * Does bounds-checking to make sure the result is valid. * @deprecated use `Position.with` instead */ withColumn(column: number): Position; /** * @returns the Position `count` characters to the left of this Position. Does not go over line breaks. */ getLeft(count?: number): Position; /** * @returns the Position `count` characters to the right of this Position. Does not go over line breaks. */ getRight(count?: number): Position; /** * @returns the Position `count` lines down from this Position */ getDown(count?: number): Position; /** * @returns the Position `count` lines up from this Position */ getUp(count?: number): Position; getLeftThroughLineBreaks(includeEol?: boolean): Position; getRightThroughLineBreaks(includeEol?: boolean): Position; getOffsetThroughLineBreaks(offset: number): Position; /** * @returns the start of the first word to the left of the current position, like `b` * * @param wordType how word boundaries are determined * @param inclusive if true, returns the current position if it's at the start of a word */ prevWordStart( document: vscode.TextDocument, args?: { wordType?: WordType; inclusive?: boolean }, ): Position; /** * @returns the start of the first word to the right of the current position, like `w` * * @param wordType how word boundaries are determined * @param inclusive if true, returns the current position if it's at the start of a word */ nextWordStart( document: vscode.TextDocument, args?: { wordType?: WordType; inclusive?: boolean }, ): Position; /** * @returns the end of the first word to the left of the current position, like `ge` * * @param wordType how word boundaries are determined */ prevWordEnd(document: vscode.TextDocument, args?: { wordType?: WordType }): Position; /** * @returns the end of the first word to the right of the current position, like `e` * * @param wordType how word boundaries are determined * @param inclusive if true, returns the current position if it's at the end of a word */ nextWordEnd( document: vscode.TextDocument, args?: { wordType?: WordType; inclusive?: boolean }, ): Position; getSentenceBegin(args: { forward: boolean }): Position; getSentenceEnd(): Position; getLineBegin(): Position; /** * @returns the beginning of the line, excluding preceeding whitespace. * This respects the `autoindent` setting, and returns `getLineBegin()` if auto-indent is disabled. */ getLineBeginRespectingIndent(document: vscode.TextDocument): Position; /** * @returns a new Position at the end of this position's line. */ getLineEnd(): Position; /** * @returns a new Position one to the left if this Position is on the EOL. Otherwise, returns this position. */ getLeftIfEOL(): Position; /** * @returns the position that the cursor would be at if you pasted *text* at the current position. */ advancePositionByText(text: string): Position; /** * Is this position at the beginning of the line? */ isLineBeginning(): boolean; /** * Is this position at the end of the line? */ isLineEnd(document: vscode.TextDocument): boolean; isFirstWordOfLine(document: vscode.TextDocument): boolean; isAtDocumentBegin(): boolean; isAtDocumentEnd(document: vscode.TextDocument): boolean; /** * Returns whether the current position is in the leading whitespace of a line */ isInLeadingWhitespace(document: vscode.TextDocument): boolean; /** * If `vim.startofline` is set, get first non-blank character's position. */ obeyStartOfLine(document: vscode.TextDocument): Position; isValid(document: vscode.TextDocument): boolean; } } Position.prototype.toString = function (this: Position) { return `[${this.line}, ${this.character}]`; }; Position.prototype.add = function ( this: Position, document: vscode.TextDocument, diff: PositionDiff, boundsCheck = true, ): Position { if (diff.type === PositionDiffType.ExactPosition) { return new Position(diff.line, diff.character); } const resultLine = clamp(this.line + diff.line, 0, document.lineCount - 1); let resultChar: number; if (diff.type === PositionDiffType.Offset) { resultChar = this.character + diff.character; } else if (diff.type === PositionDiffType.ExactCharacter) { resultChar = diff.character; } else if (diff.type === PositionDiffType.ObeyStartOfLine) { resultChar = this.obeyStartOfLine(document).character; } else if (diff.type === PositionDiffType.EndOfLine) { resultChar = this.getLineEnd().character; } else { throw new Error(`Unknown PositionDiffType: ${diff.type}`); } const pos = new Position(resultLine, Math.max(resultChar, 0)); return boundsCheck ? document.validatePosition(pos) : pos; }; Position.prototype.subtract = function (this: Position, other: Position): PositionDiff { return PositionDiff.offset({ line: this.line - other.line, character: this.character - other.character, }); }; /** * @returns a new Position with the same line and the given character. * Does bounds-checking to make sure the result is valid. */ Position.prototype.withColumn = function (this: Position, column: number): Position { column = clamp(column, 0, TextEditor.getLineLength(this.line)); return new Position(this.line, column); }; /** * @returns the Position `count` characters to the left of this Position. Does not go over line breaks. */ Position.prototype.getLeft = function (this: Position, count = 1): Position { return new Position(this.line, Math.max(this.character - count, 0)); }; /** * @returns the Position `count` characters to the right of this Position. Does not go over line breaks. */ Position.prototype.getRight = function (this: Position, count = 1): Position { return new Position( this.line, Math.min(this.character + count, TextEditor.getLineLength(this.line)), ); }; /** * @returns the Position `count` lines down from this Position */ Position.prototype.getDown = function (this: Position, count = 1): Position { if (vscode.window.activeTextEditor) { const line = Math.min(this.line + count, TextEditor.getLineCount() - 1); return new Position(line, Math.min(this.character, TextEditor.getLineLength(line))); } else { return this.translate({ lineDelta: count }); } }; /** * @returns the Position `count` lines up from this Position */ Position.prototype.getUp = function (this: Position, count = 1): Position { const line = Math.max(this.line - count, 0); return new Position(line, Math.min(this.character, TextEditor.getLineLength(line))); }; /** * Same as getLeft, but goes up to the previous line on line breaks. * Equivalent to left arrow (in a non-vim editor!) */ Position.prototype.getLeftThroughLineBreaks = function ( this: Position, includeEol = true, ): Position { if (!this.isLineBeginning()) { return this.getLeft(); } // First char on first line, can not go left any more if (this.line === 0) { return this; } if (includeEol) { return this.getUp().getLineEnd(); } else { return this.getUp().getLineEnd().getLeft(); } }; Position.prototype.getRightThroughLineBreaks = function ( this: Position, includeEol = false, ): Position { const document = vscode.window.activeTextEditor?.document; if (document === undefined) { return this; } if (this.isAtDocumentEnd(document)) { return this; } const lineLength = document.lineAt(this.line).text.length; if (this.line < document.lineCount - 1) { const pos = includeEol ? this : this.getRight(); if (pos.character === lineLength) { return this.with({ character: 0 }).getDown(); } } else if (!includeEol && this.character === lineLength - 1) { // Last character of document, don't go on to non-existent EOL return this; } return this.getRight(); }; Position.prototype.getOffsetThroughLineBreaks = function ( this: Position, offset: number, ): Position { let pos = new Position(this.line, this.character); if (offset < 0) { for (let i = 0; i < -offset; i++) { pos = pos.getLeftThroughLineBreaks(); } } else { for (let i = 0; i < offset; i++) { pos = pos.getRightThroughLineBreaks(); } } return pos; }; Position.prototype.prevWordStart = function ( this: Position, document: vscode.TextDocument, args?: { wordType?: WordType; inclusive?: boolean }, ): Position { return prevWordStart(document, this, args?.wordType ?? WordType.Normal, args?.inclusive ?? false); }; Position.prototype.nextWordStart = function ( this: Position, document: vscode.TextDocument, args?: { wordType?: WordType; inclusive?: boolean }, ): Position { return nextWordStart(document, this, args?.wordType ?? WordType.Normal, args?.inclusive ?? false); }; Position.prototype.prevWordEnd = function ( this: Position, document: vscode.TextDocument, args?: { wordType?: WordType }, ): Position { return prevWordEnd(document, this, args?.wordType ?? WordType.Normal); }; Position.prototype.nextWordEnd = function ( this: Position, document: vscode.TextDocument, args?: { wordType?: WordType; inclusive?: boolean }, ): Position { return nextWordEnd(document, this, args?.wordType ?? WordType.Normal, args?.inclusive ?? false); }; Position.prototype.getSentenceBegin = function ( this: Position, args: { forward: boolean }, ): Position { return getSentenceBegin(this, args); }; Position.prototype.getSentenceEnd = function (this: Position): Position { return getSentenceEnd(this); }; /** * @returns a new Position at the beginning of the current line. */ Position.prototype.getLineBegin = function (this: Position): Position { return new Position(this.line, 0); }; /** * @returns the beginning of the line, excluding preceeding whitespace. * This respects the `autoindent` setting, and returns `getLineBegin()` if auto-indent is disabled. */ Position.prototype.getLineBeginRespectingIndent = function ( this: Position, document: vscode.TextDocument, ): Position { if (!configuration.autoindent) { return this.getLineBegin(); } return TextEditor.getFirstNonWhitespaceCharOnLine(document, this.line); }; /** * @returns a new Position at the end of this position's line. */ Position.prototype.getLineEnd = function (this: Position): Position { return new Position(this.line, TextEditor.getLineLength(this.line)); }; /** * @returns a new Position one to the left if this Position is on the EOL. Otherwise, returns this position. */ Position.prototype.getLeftIfEOL = function (this: Position): Position { return this.character === TextEditor.getLineLength(this.line) ? this.getLeft() : this; }; /** * @returns the position that the cursor would be at if you pasted *text* at the current position. */ Position.prototype.advancePositionByText = function (this: Position, text: string): Position { const newlines: number[] = []; let idx = text.indexOf('\n', 0); while (idx >= 0) { newlines.push(idx); idx = text.indexOf('\n', idx + 1); } if (newlines.length === 0) { return new Position(this.line, this.character + text.length); } else { return new Position(this.line + newlines.length, text.length - (newlines.at(-1)! + 1)); } }; /** * Is this position at the beginning of the line? */ Position.prototype.isLineBeginning = function (this: Position): boolean { return this.character === 0; }; /** * Is this position at the end of the line? */ Position.prototype.isLineEnd = function (this: Position, document: vscode.TextDocument): boolean { return this.character >= document.lineAt(this.line).range.end.character; }; Position.prototype.isFirstWordOfLine = function ( this: Position, document: vscode.TextDocument, ): boolean { return ( TextEditor.getFirstNonWhitespaceCharOnLine(document, this.line).character === this.character ); }; Position.prototype.isAtDocumentBegin = function (this: Position): boolean { return this.line === 0 && this.isLineBeginning(); }; Position.prototype.isAtDocumentEnd = function ( this: Position, document: vscode.TextDocument, ): boolean { return this.isEqual(TextEditor.getDocumentEnd(document)); }; /** * Returns whether the current position is in the leading whitespace of a line * @param allowEmpty : Use true if "" is valid */ Position.prototype.isInLeadingWhitespace = function ( this: Position, document: vscode.TextDocument, ): boolean { return /^\s+$/.test(document.getText(new vscode.Range(this.getLineBegin(), this))); }; /** * If `vim.startofline` is set, get first non-blank character's position. */ Position.prototype.obeyStartOfLine = function ( this: Position, document: vscode.TextDocument, ): Position { return configuration.startofline ? TextEditor.getFirstNonWhitespaceCharOnLine(document, this.line) : this; }; Position.prototype.isValid = function (this: Position, document: vscode.TextDocument): boolean { try { if (this.line >= document.lineCount) { return false; } const charCount = document.lineAt(this.line).range.end.character; if (this.character > charCount + 1) { return false; } } catch (e) { return false; } return true; }; ================================================ FILE: src/common/number/numericString.ts ================================================ /** * aaaa0x111bbbbbb * |-------------| => NumericString * |--| => prefix * |---| => core * |----| => suffix * || => numPrefix * |-| => num * * Greedy matching, leftmost match wins. * If multiple matches begin at the same position, the match with the biggest * span wins. * If multiple matches have the same begin position and span (This usually * happens on octal and decimal), following priority sequence is used: * (decimal => octal => hexadecimal) * * Example: * | core | What we got | Rather than | * ------------------|--------|----------------------|---------------------| * Leftmost rule: | 010xff | (010)xff [octal] | 01(0xff) [hex] | * Biggest span rule:| 0xff | (0xff) [hex] | (0)xff [decimal] | * Priority rule: | 00007 | (00007) [octal] | (00007) [decimal] | * * Side Effect: * -0xf Will be parsed as (-0)xf rather than -(0xf), current workaround is * capturing '-' in hexadecimal regex but not consider '-' as a part * of the number. This is achieved by using `negative` boolean value * in `NumericString`. */ export enum NumericStringRadix { Oct = 8, Dec = 10, Hex = 16, } export class NumericString { radix: NumericStringRadix; value: number; numLength: number; prefix: string; suffix: string; // If a negative sign should be manually added when converting to string. negative: boolean; isCapital: boolean; // Map radix to number prefix private static numPrefix: { [key: number]: string } = { [NumericStringRadix.Oct]: '0', [NumericStringRadix.Dec]: '', [NumericStringRadix.Hex]: '0x', }; // Keep octal at the top of decimal to avoid regarding 0000007 as decimal. // '000009' matches decimal. // '000007' matches octal. // '-0xf' matches hex rather than decimal '-0' private static matchings: Array<{ regex: RegExp; radix: NumericStringRadix }> = [ { regex: /(-)?0[0-7]+/, radix: NumericStringRadix.Oct }, { regex: /(-)?\d+/, radix: NumericStringRadix.Dec }, { regex: /(-)?0x[\da-fA-F]+/, radix: NumericStringRadix.Hex }, ]; // Return parse result and offset of suffix public static parse( input: string, targetRadix?: NumericStringRadix, ): { num: NumericString; suffixOffset: number } | undefined { const filteredMatchings = targetRadix !== undefined ? NumericString.matchings.filter((matching) => matching.radix === targetRadix) : NumericString.matchings; // Find core numeric part of input let coreBegin = -1; let coreLength = -1; let coreRadix = -1; let coreSign = false; for (const { regex, radix } of filteredMatchings) { const match = regex.exec(input); if (match != null) { // Get the leftmost and largest match if ( coreRadix < 0 || match.index < coreBegin || (match.index === coreBegin && match[0].length > coreLength) ) { coreBegin = match.index; coreLength = match[0].length; coreRadix = radix; coreSign = match[1] === '-'; } } } if (coreRadix < 0) { return undefined; } const coreEnd = coreBegin + coreLength; const prefix = input.slice(0, coreBegin); const core = input.slice(coreBegin, coreEnd); const suffix = input.slice(coreEnd, input.length); let value = parseInt(core, coreRadix); // 0x00ff: numLength = 4 // 077: numLength = 2 // -0999: numLength = 3 // The numLength is only useful for parsing non-decimal. Decimal with // leading zero will be trimmed in `toString()`. If value is negative, // remove the width of negative sign. const numLength = coreLength - NumericString.numPrefix[coreRadix].length - (coreSign ? 1 : 0); // According to original vim's behavior, for hex and octal, the leading // '-' *should* be captured and preserved but *should not* be regarded as // part of number, which means with , `-0xf` turns into `-0x10`. So // for hex and octal, we make the value absolute and set the negative // sign flag. let negative = false; if (coreRadix !== 10 && coreSign) { value = -value; negative = true; } let isCapital = false; if (coreRadix === 16) { for (const c of Array.from(input).reverse()) { if ('A' <= c && c <= 'F') { isCapital = true; break; } else if ('a' <= c && c <= 'f') { isCapital = false; break; } } } return { num: new NumericString(value, coreRadix, numLength, prefix, suffix, negative, isCapital), suffixOffset: coreEnd, }; } private constructor( value: number, radix: NumericStringRadix, numLength: number, prefix: string, suffix: string, negative: boolean, isCapital: boolean, ) { this.value = value; this.radix = radix; this.numLength = numLength; this.prefix = prefix; this.suffix = suffix; this.negative = negative; this.isCapital = isCapital; } public toString(): string { // For decreased octal and hexadecimal if (this.radix !== NumericStringRadix.Dec) { const max = 0xffffffff; while (this.value < 0) { this.value = max + this.value + 1; } } // Gen num part const absValue = Math.abs(this.value); let num = absValue.toString(this.radix); if (this.isCapital) { num = num.toUpperCase(); } // numLength of decimal *should not* be preserved. if (this.radix !== NumericStringRadix.Dec) { const diff = this.numLength - num.length; if (diff > 0) { // Preserve num length if it's narrower. num = '0'.repeat(diff) + num; } } const sign = this.negative || this.value < 0 ? '-' : ''; const core = sign + NumericString.numPrefix[this.radix] + num; return this.prefix + core + this.suffix; } } ================================================ FILE: src/completion/lineCompletionProvider.ts ================================================ import * as vscode from 'vscode'; import { Position } from 'vscode'; import { VimState } from '../state/vimState'; import { TextEditor } from './../textEditor'; /** * Return open text documents, with a given file at the top of the list. * @param startingFileName File that will be first in the array, typically current file */ const documentsStartingWith = (startingFileName: string) => { return [...vscode.workspace.textDocuments].sort((a, b) => { if (a.fileName === startingFileName) { return -1; } else if (b.fileName === startingFileName) { return 1; } return 0; }); }; /** * Get lines, with leading tabs or whitespace stripped. * @param document Document to get lines from. * @param lineToStartScanFrom Where to start looking for matches first. Closest matches are sorted first. * @param scanAboveFirst Whether to start scan above or below cursor. Other direction is scanned last. * @returns */ const linesWithoutIndentation = ( document: vscode.TextDocument, lineToStartScanFrom: number, scanAboveFirst: boolean, ): Array<{ sortPriority: number; text: string }> => { const distanceFromStartLine = (line: number) => { let sortPriority = scanAboveFirst ? lineToStartScanFrom - line : line - lineToStartScanFrom; if (sortPriority < 0) { // We prioritized any items in the main direction searched, // but now find closest items in opposite direction. sortPriority = lineToStartScanFrom + Math.abs(sortPriority); } return sortPriority; }; return document .getText() .split('\n') .map((text, line) => ({ sortPriority: distanceFromStartLine(line), text: text.replace(/^[ \t]*/, ''), })) .sort((a, b) => (a.sortPriority > b.sortPriority ? 1 : -1)); }; /** * Get all completions that match given text within open documents. * @example * a1 * a2 * a| // <--- Perform line completion here * a3 * a4 * // Returns: ['a2', 'a1', 'a3', 'a4'] * @param text Text to partially match. Indentation is stripped. * @param currentFileName Current file, which is prioritized in sorting. * @param currentPosition Current position, which is prioritized when sorting for current file. */ const getCompletionsForText = ( text: string, currentFileName: string, currentPosition: Position, ): string[] | null => { const matchedLines: string[] = []; for (const document of documentsStartingWith(currentFileName)) { let lineToStartScanFrom = 0; let scanAboveFirst = false; if (document.fileName === currentFileName) { lineToStartScanFrom = currentPosition.line; scanAboveFirst = true; } for (const line of linesWithoutIndentation(document, lineToStartScanFrom, scanAboveFirst)) { if ( !matchedLines.includes(line.text) && line.text && line.text.startsWith(text) && line.text !== text ) { matchedLines.push(line.text); } } } return matchedLines; }; /** * Get all completions that match given text within open documents. * Results are sorted in a few ways: * 1) The current document is prioritized over other open documents. * 2) For the current document, lines above the current cursor are always prioritized over lines below it. * 3) For the current document, lines are also prioritized based on distance from cursor. * 4) For other documents, lines are prioritized based on distance from the top. * @example * a1 * a2 * a| // <--- Perform line completion here * a3 * a4 * // Returns: ['a2', 'a1', 'a3', 'a4'] * @param position Position to start scan from * @param document Document to start scanning from, starting at the position (other open documents are scanned from top) */ export const getCompletionsForCurrentLine = ( position: Position, document: vscode.TextDocument, ): string[] | null => { const currentLineText = document.getText( new vscode.Range(TextEditor.getFirstNonWhitespaceCharOnLine(document, position.line), position), ); return getCompletionsForText(currentLineText, document.fileName, position); }; export const lineCompletionProvider = { /** * Get all completions that match given text within open documents. * Results are sorted by priority. * @see getCompletionsForCurrentLine * * Any trailing characters are stripped. Trailing characters are often * from auto-close, such as when importing in JavaScript ES6 and typing a * curly brace. So the trailing characters are removed on purpose. * * Modifies vimState, adding transformations that replaces the * current line's text with the chosen completion, with proper indentation. * * Here we use Quick Pick, instead of registerCompletionItemProvider * Originally I looked at using a standard completion dropdown using that method, * but it doesn't allow you to limit completions, and it became overwhelming * when e.g. trying to do a line completion when the cursor is positioned after * a space character (such that it shows almost any symbol in the list). * Quick Pick also allows for searching, which is a nice bonus. */ showLineCompletionsQuickPick: async (position: Position, vimState: VimState): Promise => { const completions = getCompletionsForCurrentLine(position, vimState.document); if (!completions) { return; } const selectedCompletion = await vscode.window.showQuickPick(completions); if (!selectedCompletion) { return; } vimState.recordedState.transformer.delete( new vscode.Range( TextEditor.getFirstNonWhitespaceCharOnLine(vimState.document, position.line), position.getLineEnd(), ), ); vimState.recordedState.transformer.addTransformation({ type: 'insertTextVSCode', text: selectedCompletion, }); }, }; ================================================ FILE: src/configuration/configuration.ts ================================================ import * as process from 'process'; import * as vscode from 'vscode'; import { Globals } from '../globals'; import { VSCodeContext } from '../util/vscodeContext'; import { configurationValidator } from './configurationValidator'; import { decoration } from './decoration'; import { ValidatorResults } from './iconfigurationValidator'; import { Notation } from './notation'; import { Digraph, IAutoSwitchInputMethod, ICamelCaseMotionConfiguration, IConfiguration, IHighlightedYankConfiguration, IKeyRemapping, IModeSpecificStrings, ITargetsConfiguration, } from './iconfiguration'; import { SUPPORT_VIMRC } from 'platform/constants'; import * as packagejson from '../../package.json'; import { Mode } from '../mode/mode'; // https://stackoverflow.com/questions/51465182/how-to-remove-index-signature-using-mapped-types/51956054#51956054 type RemoveIndex = { [P in keyof T as string extends P ? never : number extends P ? never : P]: T[P]; }; export const extensionVersion = packagejson.version; /** * Most options supported by Vim have a short alias. They are provided here. * Please keep this list up to date and sorted alphabetically. */ export const optionAliases: ReadonlyMap = new Map([ ['ai', 'autoindent'], ['et', 'expandtab'], ['gd', 'gdefault'], ['hi', 'history'], ['hls', 'hlsearch'], ['ic', 'ignorecase'], ['icm', 'inccommand'], ['is', 'incsearch'], ['isk', 'iskeyword'], ['js', 'joinspaces'], ['mmd', 'maxmapdepth'], ['mps', 'matchpairs'], ['nu', 'number'], ['rnu', 'relativenumber'], ['sc', 'showcmd'], ['scr', 'scroll'], ['so', 'scrolloff'], ['scs', 'smartcase'], ['smd', 'showmode'], ['sol', 'startofline'], ['to', 'timeout'], ['ts', 'tabstop'], ['tw', 'textwidth'], ['ws', 'wrapscan'], ['ww', 'whichwrap'], ]); type OptionValue = number | string | boolean; interface VSCodeKeybinding { key: string; mac?: string; linux?: string; command: string; when: string; } interface IHandleKeys { [key: string]: boolean; } interface IKeyBinding { key: string; command: string; } /** * Every Vim option we support should * 1. Be added to contribution section of `package.json`. * 2. Named as `vim.{optionName}`, `optionName` is the name we use in Vim. * 3. Define a public property in `Configuration` with the same name and a default value. * Or define a private property and define customized Getter/Setter accessors for it. * Always remember to decorate Getter accessor as @enumerable() * 4. If user doesn't set the option explicitly * a. we don't have a similar setting in Code, initialize the option as default value. * b. we have a similar setting in Code, use Code's setting. * * Vim option override sequence. * 1. `:set {option}` on the fly * 2. `vim.{option}` * 3. VS Code configuration * 4. VSCodeVim configuration default values * */ class Configuration implements IConfiguration { [key: string]: any; private readonly leaderDefault = '\\'; private readonly cursorTypeMap: { [key: string]: vscode.TextEditorCursorStyle } = { line: vscode.TextEditorCursorStyle.Line, block: vscode.TextEditorCursorStyle.Block, underline: vscode.TextEditorCursorStyle.Underline, 'line-thin': vscode.TextEditorCursorStyle.LineThin, 'block-outline': vscode.TextEditorCursorStyle.BlockOutline, 'underline-thin': vscode.TextEditorCursorStyle.UnderlineThin, }; private loadListeners: Array<() => void> = []; public addLoadListener(listener: () => void): void { this.loadListeners.push(listener); } public async load(): Promise { const vimConfigs: { [key: string]: any } = Globals.isTesting ? Globals.mockConfiguration : this.getConfiguration('vim'); // eslint-disable-next-line guard-for-in for (const option in this) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment let val = vimConfigs[option]; if (val !== null && val !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (val.constructor.name === Object.name) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument val = Configuration.unproxify(val); } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this[option] = val; } } if (SUPPORT_VIMRC && this.vimrc.enable) { await import('./vimrc').then((vimrcModel) => { return vimrcModel.vimrc.load(this); }); } this.leader = Notation.NormalizeKey(this.leader, this.leaderDefault); this.clearKeyBindingsMaps(); const validatorResults = await configurationValidator.validate(configuration); // read package.json for bound keys // enable/disable certain key combinations this.boundKeyCombinations = []; for (const keybinding of packagejson.contributes.keybindings) { if (keybinding.when.includes('listFocus')) { continue; } if (keybinding.command.startsWith('notebook')) { continue; } let key = keybinding.key; if (process.platform === 'darwin') { key = keybinding.mac || key; } else if (process.platform === 'linux') { key = keybinding.linux || key; } this.boundKeyCombinations.push({ key: Notation.NormalizeKey(key, this.leader), command: keybinding.command, }); } // decorations decoration.load(this); for (const boundKey of this.boundKeyCombinations) { // By default, all key combinations are used let useKey = true; const handleKey = this.handleKeys[boundKey.key]; if (handleKey !== undefined) { // enabled/disabled through `vim.handleKeys` useKey = handleKey; } else if (!this.useCtrlKeys && boundKey.key.slice(1, 3) === 'C-') { // user has disabled CtrlKeys and the current key is a CtrlKey // , still needs to be captured to overrideCopy if (boundKey.key === '' && this.overrideCopy) { useKey = true; } else { useKey = false; } } void VSCodeContext.set(`vim.use${boundKey.key}`, useKey); } void VSCodeContext.set('vim.overrideCopy', this.overrideCopy); void VSCodeContext.set('vim.overrideCtrlC', this.overrideCopy || this.useCtrlKeys); // workaround for circular dependency that would // prevent packaging if we simply called `updateLangmap(configuration.langmap);` this.loadListeners.forEach((listener) => listener()); return validatorResults; } getConfiguration(section: string = ''): RemoveIndex { const document = vscode.window.activeTextEditor?.document; const resource = document ? { uri: document.uri, languageId: document.languageId } : undefined; return vscode.workspace.getConfiguration(section, resource); } cursorStyleFromString(cursorStyle: string): vscode.TextEditorCursorStyle | undefined { return this.cursorTypeMap[cursorStyle]; } clearKeyBindingsMaps() { // Clear the KeyBindingsMaps so that the previous configuration maps don't leak to this one this.normalModeKeyBindingsMap = new Map(); this.insertModeKeyBindingsMap = new Map(); this.visualModeKeyBindingsMap = new Map(); this.commandLineModeKeyBindingsMap = new Map(); this.operatorPendingModeKeyBindingsMap = new Map(); } handleKeys: IHandleKeys = {}; useSystemClipboard = false; shell = ''; useCtrlKeys = false; overrideCopy = true; hlsearch = false; ignorecase = true; smartcase = true; autoindent = true; matchpairs = '(:),{:},[:]'; joinspaces = true; camelCaseMotion: ICamelCaseMotionConfiguration = { enable: true, }; replaceWithRegister = false; smartRelativeLine = false; sneak = false; sneakUseIgnorecaseAndSmartcase = false; sneakReplacesF = false; surround = true; argumentObjectSeparators = [',']; argumentObjectOpeningDelimiters = ['(', '[']; argumentObjectClosingDelimiters = [')', ']']; easymotion = false; easymotionMarkerBackgroundColor = '#0000'; easymotionMarkerForegroundColorOneChar = '#ff0000'; easymotionMarkerForegroundColorTwoCharFirst = '#ffb400'; easymotionMarkerForegroundColorTwoCharSecond = '#b98300'; easymotionIncSearchForegroundColor = '#7fbf00'; easymotionDimColor = '#777777'; easymotionDimBackground = true; easymotionMarkerFontWeight = 'bold'; easymotionKeys = 'hklyuiopnm,qwertzxcvbasdgjf;'; easymotionJumpToAnywhereRegex = '\\b[A-Za-z0-9]|[A-Za-z0-9]\\b|_.|#.|[a-z][A-Z]'; targets: ITargetsConfiguration = { enable: false, bracketObjects: { enable: true, }, smartQuotes: { enable: false, breakThroughLines: false, aIncludesSurroundingSpaces: true, }, }; autoSwitchInputMethod: IAutoSwitchInputMethod = { enable: false, defaultIM: '', obtainIMCmd: '', switchIMCmd: '', }; timeout = 1000; maxmapdepth = 1000; showcmd = true; showmodename = true; leader = this.leaderDefault; history = 50; inccommand: '' | 'append' | 'replace' = ''; incsearch = true; startInInsertMode = false; startInInsertModeSchemes: string[] = ['comment']; statusBarColorControl = false; statusBarColors: IModeSpecificStrings = { normal: ['#005f5f', '#ffffff'], insert: ['#5f0000', '#ffffff'], visual: ['#5f00af', '#ffffff'], visualline: ['#005f87', '#ffffff'], visualblock: ['#86592d', '#ffffff'], replace: ['#000000', '#ffffff'], }; searchHighlightColor = ''; searchHighlightTextColor = ''; searchMatchColor = ''; searchMatchTextColor = ''; substitutionColor = '#50f01080'; substitutionTextColor = ''; highlightedyank: IHighlightedYankConfiguration = { enable: false, color: 'rgba(250, 240, 170, 0.5)', textColor: '', duration: 200, }; @overlapSetting({ settingName: 'tabSize', defaultValue: 8 }) tabstop!: number; @overlapSetting({ settingName: 'cursorStyle', defaultValue: 'line' }) private editorCursorStyleRaw!: string; get editorCursorStyle(): vscode.TextEditorCursorStyle | undefined { return this.cursorStyleFromString(this.editorCursorStyleRaw); } set editorCursorStyle(val: vscode.TextEditorCursorStyle | undefined) { // nop } @overlapSetting({ settingName: 'insertSpaces', defaultValue: false }) expandtab!: boolean; @overlapSetting({ settingName: 'lineNumbers', defaultValue: true, map: new Map([ ['on', true], ['off', false], ['relative', false], ['interval', false], ]), }) // eslint-disable-next-line id-denylist number!: boolean; @overlapSetting({ settingName: 'lineNumbers', defaultValue: false, map: new Map([ ['on', false], ['off', false], ['relative', true], ['interval', false], ]), }) relativenumber!: boolean; @overlapSetting({ settingName: 'wordSeparators', defaultValue: '/\\()"\':,.;<>~!@#$%^&*|+=[]{}`?-', }) iskeyword!: string; @overlapSetting({ settingName: 'wordWrap', defaultValue: false, map: new Map([ ['on', true], ['off', false], ['wordWrapColumn', true], ['bounded', true], ]), }) wrap!: boolean; @overlapSetting({ settingName: 'cursorSurroundingLines', defaultValue: 0, }) scrolloff!: number; boundKeyCombinations: IKeyBinding[] = []; visualstar = false; mouseSelectionGoesIntoVisualMode = true; changeWordIncludesWhitespace = false; foldfix = false; disableExtension: boolean = false; enableNeovim = false; neovimPath = ''; neovimUseConfigFile = false; neovimConfigPath = ''; vimrc = { enable: false, path: '', }; digraphs: { [shortcut: string]: Digraph } = {}; gdefault = false; substituteGlobalFlag = false; // Deprecated in favor of gdefault whichwrap = 'b,s'; startofline = true; showMarksInGutter = false; report = 2; wrapscan = true; scroll = 0; getScrollLines(visibleRanges: vscode.Range[]): number { return this.scroll === 0 ? Math.ceil((visibleRanges[0].end.line - visibleRanges[0].start.line) / 2) : this.scroll; } cursorStylePerMode: IModeSpecificStrings = { normal: undefined, insert: undefined, visual: undefined, visualline: undefined, visualblock: undefined, replace: undefined, }; getCursorStyleForMode(mode: Mode): vscode.TextEditorCursorStyle | undefined { const cursorStyle = (this.cursorStylePerMode as unknown as Record)[ Mode[mode].toLowerCase() ]; return cursorStyle ? this.cursorStyleFromString(cursorStyle) : undefined; } // remappings insertModeKeyBindings: IKeyRemapping[] = []; insertModeKeyBindingsNonRecursive: IKeyRemapping[] = []; normalModeKeyBindings: IKeyRemapping[] = []; normalModeKeyBindingsNonRecursive: IKeyRemapping[] = []; operatorPendingModeKeyBindings: IKeyRemapping[] = []; operatorPendingModeKeyBindingsNonRecursive: IKeyRemapping[] = []; visualModeKeyBindings: IKeyRemapping[] = []; visualModeKeyBindingsNonRecursive: IKeyRemapping[] = []; commandLineModeKeyBindings: IKeyRemapping[] = []; commandLineModeKeyBindingsNonRecursive: IKeyRemapping[] = []; insertModeKeyBindingsMap: Map = new Map(); normalModeKeyBindingsMap: Map = new Map(); operatorPendingModeKeyBindingsMap: Map = new Map(); visualModeKeyBindingsMap: Map = new Map(); commandLineModeKeyBindingsMap: Map = new Map(); // langmap langmapBindingsMap: Map = new Map(); langmapReverseBindingsMap: Map = new Map(); langmap = ''; get textwidth(): number { const textwidth = this.getConfiguration('vim').get('textwidth', 80); if (typeof textwidth !== 'number') { return 80; } return textwidth; } private static unproxify(obj: { [key: string]: any }): object { const result: { [key: string]: any } = {}; // eslint-disable-next-line guard-for-in for (const key in obj) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const val = obj[key]; if (val !== null && val !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment result[key] = val; } } return result; } } // handle mapped settings between vscode to vim function overlapSetting(args: { settingName: string; defaultValue: OptionValue; map?: Map; }) { return (target: any, propertyKey: string) => { Object.defineProperty(target, propertyKey, { get() { // retrieve value from vim configuration // if the value is not defined or empty // look at the equivalent `editor` setting // if that is not defined then defer to the default value // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access let val = this['_' + propertyKey]; if (val !== undefined && val !== '') { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return val; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access val = this.getConfiguration('editor').get(args.settingName, args.defaultValue); if (args.map && val !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument val = args.map.get(val); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return val; }, set(value) { // synchronize the vim setting with the `editor` equivalent // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access this['_' + propertyKey] = value; if (value === undefined || value === '' || Globals.isTesting) { return; } if (args.map) { for (const [vscodeSetting, vimSetting] of args.map.entries()) { if (value === vimSetting) { value = vscodeSetting; break; } } } // update configuration asynchronously // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access this.getConfiguration('editor').update( args.settingName, value, vscode.ConfigurationTarget.Global, ); }, enumerable: true, configurable: true, }); }; } export const configuration = new Configuration(); ================================================ FILE: src/configuration/configurationValidator.ts ================================================ import { IConfiguration } from './iconfiguration'; import { IConfigurationValidator, ValidatorResults } from './iconfigurationValidator'; class ConfigurationValidator { private readonly validators: IConfigurationValidator[]; constructor() { this.validators = []; } public registerValidator(validator: IConfigurationValidator) { this.validators.push(validator); } public async validate(config: IConfiguration): Promise { const results = new ValidatorResults(); for (const validator of this.validators) { const validatorResults = await validator.validate(config); if (validatorResults.hasError) { // errors found in configuration, disable feature validator.disable(config); } results.concat(validatorResults); } return results; } } export const configurationValidator = new ConfigurationValidator(); ================================================ FILE: src/configuration/decoration.ts ================================================ import * as vscode from 'vscode'; import { IConfiguration } from './iconfiguration'; class DecorationImpl { private _default!: vscode.TextEditorDecorationType; private _searchHighlight!: vscode.TextEditorDecorationType; private _searchMatch!: vscode.TextEditorDecorationType; private _substitutionAppend!: vscode.TextEditorDecorationType; private _substitutionReplace!: vscode.TextEditorDecorationType; private _easyMotionIncSearch!: vscode.TextEditorDecorationType; private _easyMotionDimIncSearch!: vscode.TextEditorDecorationType; private _insertModeVirtualCharacter!: vscode.TextEditorDecorationType; private _operatorPendingModeCursor!: vscode.TextEditorDecorationType; private _operatorPendingModeCursorChar!: vscode.TextEditorDecorationType; private _markDecorationCache = new Map(); private _createMarkDecoration(name: string): vscode.TextEditorDecorationType { const escape: Record = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', }; if (name in escape) { name = escape[name]; } const svg = [ '', '', '', `${name}`, '', ].join(''); const uri = vscode.Uri.parse(`data:image/svg+xml;utf8,${encodeURI(svg)}`, true); return vscode.window.createTextEditorDecorationType({ isWholeLine: false, gutterIconPath: uri, gutterIconSize: 'cover', }); } public readonly confirmedSubstitution = vscode.window.createTextEditorDecorationType({ letterSpacing: '-9999999px', opacity: '0', }); public set default(value: vscode.TextEditorDecorationType) { if (this._default) { this._default.dispose(); } this._default = value; } public get default() { return this._default; } public set searchHighlight(value: vscode.TextEditorDecorationType) { if (this._searchHighlight) { this._searchHighlight.dispose(); } this._searchHighlight = value; } public get searchHighlight() { return this._searchHighlight; } public set searchMatch(value: vscode.TextEditorDecorationType) { if (this._searchMatch) { this._searchMatch.dispose(); } this._searchMatch = value; } public get searchMatch() { return this._searchMatch; } public set substitutionAppend(value: vscode.TextEditorDecorationType) { if (this._substitutionAppend) { this._substitutionAppend.dispose(); } this._substitutionAppend = value; } public get substitutionAppend() { return this._substitutionAppend; } public set substitutionReplace(value: vscode.TextEditorDecorationType) { if (this._substitutionReplace) { this._substitutionReplace.dispose(); } this._substitutionReplace = value; } public get substitutionReplace() { return this._substitutionReplace; } public get easyMotionIncSearch() { return this._easyMotionIncSearch; } public set easyMotionIncSearch(value: vscode.TextEditorDecorationType) { if (this._easyMotionIncSearch) { this._easyMotionIncSearch.dispose(); } this._easyMotionIncSearch = value; } public get easyMotionDimIncSearch() { return this._easyMotionDimIncSearch; } public set easyMotionDimIncSearch(value: vscode.TextEditorDecorationType) { if (this._easyMotionDimIncSearch) { this._easyMotionDimIncSearch.dispose(); } this._easyMotionDimIncSearch = value; } public getOrCreateMarkDecoration(name: string): vscode.TextEditorDecorationType { const decorationType = this.getMarkDecoration(name); if (decorationType) { return decorationType; } else { const type = this._createMarkDecoration(name); this._markDecorationCache.set(name, type); return type; } } public getMarkDecoration(name: string): vscode.TextEditorDecorationType | undefined { return this._markDecorationCache.get(name); } public allMarkDecorations(): IterableIterator { return this._markDecorationCache.values(); } public set insertModeVirtualCharacter(value: vscode.TextEditorDecorationType) { if (this._insertModeVirtualCharacter) { this._insertModeVirtualCharacter.dispose(); } this._insertModeVirtualCharacter = value; } public get insertModeVirtualCharacter() { return this._insertModeVirtualCharacter; } public set operatorPendingModeCursor(value: vscode.TextEditorDecorationType) { if (this._operatorPendingModeCursor) { this._operatorPendingModeCursor.dispose(); } this._operatorPendingModeCursor = value; } public get operatorPendingModeCursor() { return this._operatorPendingModeCursor; } public set operatorPendingModeCursorChar(value: vscode.TextEditorDecorationType) { if (this._operatorPendingModeCursorChar) { this._operatorPendingModeCursorChar.dispose(); } this._operatorPendingModeCursorChar = value; } public get operatorPendingModeCursorChar() { return this._operatorPendingModeCursorChar; } public load(configuration: IConfiguration) { this.default = vscode.window.createTextEditorDecorationType({ backgroundColor: new vscode.ThemeColor('editorCursor.foreground'), borderColor: new vscode.ThemeColor('editorCursor.foreground'), dark: { color: 'rgb(81,80,82)', }, light: { // used for light colored themes color: 'rgb(255, 255, 255)', }, borderStyle: 'solid', borderWidth: '1px', }); const searchHighlightBackgroundColor = configuration.searchHighlightColor ? configuration.searchHighlightColor : new vscode.ThemeColor('editor.findMatchHighlightBackground'); this.searchHighlight = vscode.window.createTextEditorDecorationType({ backgroundColor: searchHighlightBackgroundColor, color: configuration.searchHighlightTextColor, overviewRulerColor: new vscode.ThemeColor('editorOverviewRuler.findMatchForeground'), after: { color: 'transparent', backgroundColor: searchHighlightBackgroundColor, }, border: '1px solid', borderColor: new vscode.ThemeColor('editor.findMatchHighlightBorder'), }); const searchMatchBackgroundColor = configuration.searchMatchColor ? configuration.searchMatchColor : new vscode.ThemeColor('editor.findMatchBackground'); this.searchMatch = vscode.window.createTextEditorDecorationType({ backgroundColor: searchMatchBackgroundColor, color: configuration.searchMatchTextColor, overviewRulerColor: new vscode.ThemeColor('editorOverviewRuler.findMatchForeground'), after: { color: 'transparent', backgroundColor: searchMatchBackgroundColor, }, border: '2px solid', borderColor: new vscode.ThemeColor('editor.findMatchBorder'), }); const substitutionBackgroundColor = configuration.substitutionColor ? configuration.substitutionColor : new vscode.ThemeColor('editor.findMatchBackground'); this.substitutionAppend = vscode.window.createTextEditorDecorationType({ backgroundColor: searchHighlightBackgroundColor, color: configuration.searchHighlightTextColor, overviewRulerColor: new vscode.ThemeColor('editorOverviewRuler.findMatchForeground'), after: { color: configuration.substitutionTextColor, backgroundColor: substitutionBackgroundColor, border: '1px solid', borderColor: new vscode.ThemeColor('editor.findMatchBorder'), }, border: '1px dashed', borderColor: new vscode.ThemeColor('editor.findMatchBorder'), }); // Use letterSpacing and opacity to hide the decorated range, so that before text gets rendered over it this.substitutionReplace = vscode.window.createTextEditorDecorationType({ letterSpacing: '-9999999px', opacity: '0', overviewRulerColor: new vscode.ThemeColor('editorOverviewRuler.findMatchForeground'), before: { color: configuration.substitutionTextColor, backgroundColor: substitutionBackgroundColor, border: '1px solid', borderColor: new vscode.ThemeColor('editor.findMatchBorder'), }, }); this.easyMotionIncSearch = vscode.window.createTextEditorDecorationType({ color: configuration.easymotionIncSearchForegroundColor, fontWeight: configuration.easymotionMarkerFontWeight, }); this.easyMotionDimIncSearch = vscode.window.createTextEditorDecorationType({ color: configuration.easymotionDimColor, }); this.insertModeVirtualCharacter = vscode.window.createTextEditorDecorationType({ color: 'transparent', // no color to hide the existing character before: { color: 'currentColor', backgroundColor: new vscode.ThemeColor('editor.background'), borderColor: new vscode.ThemeColor('editor.background'), margin: '0 -1ch 0 0', height: '100%', }, }); // This creates the half block cursor when on operator pending mode this.operatorPendingModeCursor = vscode.window.createTextEditorDecorationType({ before: { // no color to hide the existing character. We only need the character here to make // the width be the same as the existing character. color: 'transparent', // The '-1ch' right margin is so that it displays on top of the existing character. The amount // here doesn't really matter, it could be '-1px' it just needs to be negative so that the left // of this 'before' element coincides with the left of the existing character. margin: `0 -1ch 0 0; position: absolute; bottom: 0; line-height: 0;`, height: '50%', backgroundColor: new vscode.ThemeColor('editorCursor.foreground'), }, }); // This puts a character on top of the half block cursor and on top of the existing character // to create the mix-blend 'magic' this.operatorPendingModeCursorChar = vscode.window.createTextEditorDecorationType({ // We make the existing character 'black' -> rgb(0,0,0), because when using the mix-blend-mode // with 'exclusion' it subtracts the darker color from the lightest color which means we will // subtract zero from our 'currentcolor' leaving us with 'currentcolor' on the part above the // background of the half cursor. color: 'black', before: { color: 'currentcolor', // The '-1ch' right margin is so that it displays on top of the existing character. The amount // here doesn't really matter, it could be '-1px' it just needs to be negative so that the left // of this 'before' element coincides with the left of the existing character. margin: `0 -1ch 0 0; position: absolute; mix-blend-mode: exclusion;`, height: '100%', }, }); } } export const decoration = new DecorationImpl(); ================================================ FILE: src/configuration/iconfiguration.ts ================================================ import * as vscode from 'vscode'; export type Digraph = [string, number | number[]]; export interface IModeSpecificStrings { normal: T | undefined; insert: T | undefined; visual: T | undefined; visualline: T | undefined; visualblock: T | undefined; replace: T | undefined; } export interface IKeyRemapping { before: string[]; after?: string[]; silent?: boolean; // 'recursive' is calculated when validating, according to the config that stored the remapping recursive?: boolean; commands?: Array<{ command: string; args: any[] } | string>; source?: 'vscode' | 'vimrc'; } export interface IVimrcKeyRemapping { keyRemapping: IKeyRemapping; keyRemappingType: string; } export interface IAutoSwitchInputMethod { enable: boolean; defaultIM: string; switchIMCmd: string; obtainIMCmd: string; } export interface IHighlightedYankConfiguration { /** * Boolean indicating whether yank highlighting should be enabled. */ enable: boolean; /** * Color of the yank highlight. */ color: string; /** * Color of the text being highlighted. */ textColor: string | undefined; /** * Duration in milliseconds of the yank highlight. */ duration: number; } export interface ICamelCaseMotionConfiguration { /** * Enable CamelCaseMotion plugin or not */ enable: boolean; } export interface ISmartQuotesConfiguration { /** * Enable SmartQuotes plugin or not */ enable: boolean; /** * Whether to break through lines when using [n]ext/[l]ast motion */ breakThroughLines: boolean; /** * Whether to use default vim behaviour when using `a` (e.g. da') which include surrounding spaces, or not, as for other text objects. */ aIncludesSurroundingSpaces: boolean; } export interface ITargetsConfiguration { /** * Enable Targets plugin or not */ enable: boolean; bracketObjects: { enable: boolean }; smartQuotes: ISmartQuotesConfiguration; } export interface IConfiguration { [key: string]: any; /** * Use the system's clipboard when copying. */ useSystemClipboard: boolean; /** * Enable ctrl- actions that would override existing VSCode actions. */ useCtrlKeys: boolean; /** * Override default VSCode copy behavior. */ overrideCopy: boolean; /** * Width in characters to word-wrap to. */ textwidth: number; /** * Should we highlight incremental search matches? */ hlsearch: boolean; /** * Ignore case when searching with / or ?. */ ignorecase: boolean; /** * In / or ?, default to ignorecase=true unless the user types a capital * letter. */ smartcase: boolean; /** * Indent automatically? */ autoindent: boolean; /** * Add two spaces after '.', '?', and '!' when joining or formatting? */ joinspaces: boolean; /** * CamelCaseMotion plugin options */ camelCaseMotion: ICamelCaseMotionConfiguration; /** * Use EasyMotion plugin? */ easymotion: boolean; /** * Use ReplaceWithRegister plugin? */ replaceWithRegister: boolean; /** * Use SmartRelativeLine plugin? */ smartRelativeLine: boolean; /** * Use sneak plugin? */ sneak: boolean; /** * Case sensitivity is determined by 'ignorecase' and 'smartcase' */ sneakUseIgnorecaseAndSmartcase: boolean; /** * Use single-character `sneak` instead of Vim's native `f`" */ sneakReplacesF: boolean; /** * Use surround plugin? */ surround: boolean; /** * Customize argument textobject delimiter and separator characters */ argumentObjectSeparators: string[]; argumentObjectOpeningDelimiters: string[]; argumentObjectClosingDelimiters: string[]; /** * Easymotion marker appearance settings */ easymotionMarkerBackgroundColor: string; easymotionMarkerForegroundColorOneChar: string; easymotionMarkerForegroundColorTwoCharFirst: string; easymotionMarkerForegroundColorTwoCharSecond: string; easymotionIncSearchForegroundColor: string; easymotionDimColor: string; easymotionDimBackground: boolean; easymotionMarkerFontWeight: string; easymotionKeys: string; /** * Timeout in milliseconds for remapped commands. */ timeout: number; /** * Maximum number of times a mapping is done without resulting in a * character to be used. This normally catches endless mappings, like * ":map x y" with ":map y x". It still does not catch ":map g wg", * because the 'w' is used before the next mapping is done. */ maxmapdepth: number; /** * Display partial commands on status bar? */ showcmd: boolean; /** * Display mode name text on status bar? */ showmodename: boolean; /** * What key should map to in key remappings? */ leader: string; /** * How much search or command history should be remembered */ history: number; /** * Show substitutions while user is typing? */ inccommand: '' | 'append' | 'replace'; /** * Show results of / or ? search as user is typing? */ incsearch: boolean; /** * Start in insert mode? */ startInInsertMode: boolean; /** * List of document URI schemes that should automatically start in Insert mode. * For example, ['comment'] for GitHub PR comment editors. */ startInInsertModeSchemes: string[]; /** * Enable changing of the status bar color based on mode */ statusBarColorControl: boolean; /** * Status bar colors to change to based on mode */ statusBarColors: IModeSpecificStrings; /** * Color of search highlights. */ searchHighlightColor: string; searchHighlightTextColor: string; /** * Color of current match */ searchMatchColor: string; searchMatchTextColor: string; /** * Color of substituted text */ substitutionColor: string; substitutionTextColor: string; /** * Yank highlight settings. */ highlightedyank: IHighlightedYankConfiguration; /** * Size of a tab character. */ tabstop: number; /** * Type of cursor user is using native to vscode */ editorCursorStyle: vscode.TextEditorCursorStyle | undefined; /** * Use spaces when the user presses tab? */ expandtab: boolean; /** * Show line numbers */ // eslint-disable-next-line id-denylist number: boolean; /** * Show relative line numbers? */ relativenumber: boolean; /** * keywords contain alphanumeric characters and '_'. * If not configured `editor.wordSeparators` is used */ iskeyword: string; /** * Characters that form pairs. The % command jumps from one to the other. * Only character pairs are allowed that are different, thus you cannot jump between two double quotes. * The characters must be separated by a colon. * The pairs must be separated by a comma. */ matchpairs: string; /** * In visual mode, start a search with * or # using the current selection */ visualstar: boolean; /** * Does dragging with the mouse put you into visual mode */ mouseSelectionGoesIntoVisualMode: boolean; /** * Includes trailing whitespace when changing word. */ changeWordIncludesWhitespace: boolean; /** * Uses a hack to fix moving around folds. */ foldfix: boolean; /** * "Soft"-disabling of extension. * Differs from VS Code's disablng of the extension as the extension * will still be loaded and activated, but all functionality will be disabled. */ disableExtension: boolean; /** * Neovim */ enableNeovim: boolean; neovimPath: string; neovimUseConfigFile: boolean; neovimConfigPath: string; /** * .vimrc */ vimrc: { enable: boolean; /** * Do not use this directly - VimrcImpl.path resolves this to a path that's guaranteed to exist. */ path: string; }; /** * Automatically apply the `/g` flag to substitute commands. */ gdefault: boolean; substituteGlobalFlag: boolean; // Deprecated in favor of gdefault /** * InputMethodSwicher */ autoSwitchInputMethod: IAutoSwitchInputMethod; /** * Keybindings */ insertModeKeyBindings: IKeyRemapping[]; insertModeKeyBindingsNonRecursive: IKeyRemapping[]; normalModeKeyBindings: IKeyRemapping[]; normalModeKeyBindingsNonRecursive: IKeyRemapping[]; operatorPendingModeKeyBindings: IKeyRemapping[]; operatorPendingModeKeyBindingsNonRecursive: IKeyRemapping[]; visualModeKeyBindings: IKeyRemapping[]; visualModeKeyBindingsNonRecursive: IKeyRemapping[]; commandLineModeKeyBindings: IKeyRemapping[]; commandLineModeKeyBindingsNonRecursive: IKeyRemapping[]; /** * These are constructed by the RemappingValidator */ insertModeKeyBindingsMap: Map; normalModeKeyBindingsMap: Map; operatorPendingModeKeyBindingsMap: Map; visualModeKeyBindingsMap: Map; commandLineModeKeyBindingsMap: Map; /** * Comma-separated list of motion keys that should wrap to next/previous line. */ whichwrap: string; cursorStylePerMode: IModeSpecificStrings; /** * Threshold to report changed lines to status bar */ report: number; /** * User-defined digraphs */ digraphs: { [shortcut: string]: Digraph }; /** * Searches wrap around the end of the file. */ wrapscan: boolean; /** * Number of lines to scroll with CTRL-U and CTRL-D commands. Set to 0 to use a half page scroll. */ scroll: number; /** * Number of line offset above or below cursor when moving. */ scrolloff: number; /** * When `true` the commands listed below move the cursor to the first non-blank of the line. When * `false` the cursor is kept in the same column (if possible). This applies to the commands: * ``, ``, ``, ``, `G`, `H`, `M`, `L`, `gg`, and to the commands `d`, `<<` * and `>>` with a linewise operator. */ startofline: boolean; /** * Show the currently set mark(s) in the gutter. */ showMarksInGutter: boolean; /** * Path to the shell to use for `!` and `:!` commands. */ shell: string; langmap: string; } ================================================ FILE: src/configuration/iconfigurationValidator.ts ================================================ import { IConfiguration } from './iconfiguration'; interface IValidatorResult { level: 'error' | 'warning'; message: string; } export class ValidatorResults { errors = new Array(); public append(validationResult: IValidatorResult) { this.errors.push(validationResult); } public concat(validationResults: ValidatorResults) { this.errors = this.errors.concat(validationResults.get()); } public get(): readonly IValidatorResult[] { return this.errors; } public get numErrors(): number { return this.errors.filter((e) => e.level === 'error').length; } public get hasError(): boolean { return this.numErrors > 0; } public get numWarnings(): number { return this.errors.filter((e) => e.level === 'warning').length; } public get hasWarning(): boolean { return this.numWarnings > 0; } } export interface IConfigurationValidator { validate(config: IConfiguration): Promise; disable(config: IConfiguration): void; } ================================================ FILE: src/configuration/langmap.ts ================================================ import { SetCommand } from '../cmd_line/commands/set'; import { Mode } from '../mode/mode'; import { configuration } from './configuration'; const nonMatchable = /<(any|leader|number|alpha|character|register|macro)>/; const literalKeys = /<(any|number|alpha|character)>/; // do not treat or as literal! const literalModes = [ Mode.Insert, Mode.Replace, Mode.CommandlineInProgress, Mode.SearchInProgressMode, ]; let lastLangmapString = ''; SetCommand.addListener('langmap', () => { updateLangmap(configuration.langmap); }); configuration.addLoadListener(() => { updateLangmap(configuration.langmap); }); updateLangmap(configuration.langmap); export function updateLangmap(langmapString: string) { if (lastLangmapString === langmapString) return; const { bindings, reverseBindings } = parseLangmap(langmapString); lastLangmapString = langmapString; configuration.langmap = langmapString; configuration.langmapBindingsMap = bindings; configuration.langmapReverseBindingsMap = reverseBindings; } /** * From :help langmap * The 'langmap' option is a list of parts, separated with commas. Each * part can be in one of two forms: * 1. A list of pairs. Each pair is a "from" character immediately * followed by the "to" character. Examples: "aA", "aAbBcC". * 2. A list of "from" characters, a semi-colon and a list of "to" * characters. Example: "abc;ABC" */ function parseLangmap(langmapString: string): { bindings: Map; reverseBindings: Map; } { if (!langmapString) return { bindings: new Map(), reverseBindings: new Map() }; const bindings: Map = new Map(); const reverseBindings: Map = new Map(); const getEscaped = (list: string) => { return list.split(/\\?(.)/).filter(Boolean); }; langmapString.split(/((?:[^\\,]|\\.)+),/).map((part) => { if (!part) return; const semicolon = part.split(/((?:[^\\;]|\\.)+);/); if (semicolon.length === 3) { const from = getEscaped(semicolon[1]); const to = getEscaped(semicolon[2]); if (from.length !== to.length) return; // skip over malformed part for (let i = 0; i < from.length; ++i) { bindings.set(from[i], to[i]); reverseBindings.set(to[i], from[i]); } } else if (semicolon.length === 1) { const pairs = getEscaped(part); if (pairs.length % 2 !== 0) return; // skip over malformed part for (let i = 0; i < pairs.length; i += 2) { bindings.set(pairs[i], pairs[i + 1]); reverseBindings.set(pairs[i + 1], pairs[i]); } } }); return { bindings, reverseBindings }; } export function isLiteralMode(mode: Mode): boolean { return literalModes.includes(mode); } function map(langmap: Map, key: string): string { // Notice that we're not currently remapping combinations. // From my experience, Vim doesn't handle ctrl remapping either. // It's possible that it's caused by my exact keyboard setup. // We might need to revisit this in the future, in case some user needs it. if (key.length !== 1) return key; return langmap.get(key) || key; } export function remapKey(key: string): string { return map(configuration.langmapBindingsMap, key); } function unmapKey(key: string): string { return map(configuration.langmapReverseBindingsMap, key); } // This is needed for bindings like "fa". // We expect this to jump to the next occurence of "a". // Thus, we need to revert "a" to its unmapped state. export function unmapLiteral( reference: readonly string[] | readonly string[][], keys: readonly string[], ): string[] { if (reference.length === 0 || keys.length === 0) return []; // find best matching if there are multiple if (Array.isArray(reference[0])) { for (const possibility of reference as string[][]) { if (possibility.length !== keys.length) continue; let allMatch = true; for (let i = 0; i < possibility.length; ++i) { if (nonMatchable.test(possibility[i])) continue; if (possibility[i] !== keys[i]) { allMatch = false; break; } } if (allMatch) return unmapLiteral(possibility, keys); } } const unmapped = [...keys]; for (let i = 0; i < keys.length; ++i) { if (literalKeys.test((reference as string[])[i])) { unmapped[i] = unmapKey(keys[i]); } } return unmapped; } ================================================ FILE: src/configuration/notation.ts ================================================ export class Notation { // Mapping from a regex to the normalized string that it should be converted to. private static readonly notationMap: ReadonlyArray<[RegExp, string]> = [ [/ctrl\+|c\-/gi, 'C-'], [/cmd\+|d\-/gi, 'D-'], [/shift\+|s\-/gi, 'S-'], [/escape|esc/gi, 'Esc'], [/backspace|bs/gi, 'BS'], [/delete|del/gi, 'Del'], [/home/gi, 'Home'], [/end/gi, 'End'], [/insert/gi, 'Insert'], [//gi, ' '], [/||/gi, '\n'], ]; private static shiftedLetterRegex = //; /** * Converts keystroke like to a single control character like \t */ public static ToControlCharacter(key: string) { if (key === '') { return '\t'; } return key; } public static IsControlKey(key: string): boolean { key = key.toLocaleUpperCase(); return ( this.isSurroundedByAngleBrackets(key) && key !== '' && key !== '' && key !== '' ); } /** * Normalizes key to AngleBracketNotation * (e.g. , Ctrl+x, normalized to ) * and converts the characters to their literals * (e.g. , , ) */ public static NormalizeKey(key: string, leaderKey: string): string { if (typeof key !== 'string') { return key; } if (key.length === 1) { return key; } key = key.toLocaleLowerCase(); if (!this.isSurroundedByAngleBrackets(key)) { key = `<${key}>`; } if (key === '') { return leaderKey; } if (['', '', '', ''].includes(key)) { return key; } for (const [regex, standardNotation] of this.notationMap) { key = key.replace(regex, standardNotation); } if (this.shiftedLetterRegex.test(key)) { key = key[3].toUpperCase(); } return key; } /** * Converts a key to a form which will look nice when logged, etc. */ public static printableKey(key: string, leaderKey: string) { const normalized = this.NormalizeKey(key, leaderKey); return normalized === ' ' ? '' : normalized === '\n' ? '' : normalized; } private static isSurroundedByAngleBrackets(key: string): boolean { return key.startsWith('<') && key.endsWith('>'); } } ================================================ FILE: src/configuration/remapper.ts ================================================ import * as vscode from 'vscode'; import { configuration } from '../configuration/configuration'; import { ForceStopRemappingError, VimError } from '../error'; import { Mode } from '../mode/mode'; import { ModeHandler } from '../mode/modeHandler'; import { StatusBar } from '../statusBar'; import { Logger } from '../util/logger'; import { SpecialKeys } from '../util/specialKeys'; import { exCommandParser } from '../vimscript/exCommandParser'; import { IKeyRemapping } from './iconfiguration'; interface IRemapper { /** * Send keys to remapper */ sendKey(keys: string[], modeHandler: ModeHandler): Promise; /** * Given keys pressed thus far, denotes if it is a potential remap */ readonly isPotentialRemap: boolean; } export class Remappers implements IRemapper { private readonly remappers = [ new InsertModeRemapper(), new NormalModeRemapper(), new VisualModeRemapper(), new CommandLineModeRemapper(), new OperatorPendingModeRemapper(), ]; get isPotentialRemap(): boolean { return this.remappers.some((r) => r.isPotentialRemap); } public async sendKey(keys: string[], modeHandler: ModeHandler): Promise { for (const remapper of this.remappers) { if (await remapper.sendKey(keys, modeHandler)) { return true; } } return false; } } export class Remapper implements IRemapper { private readonly configKey: string; private readonly remappedModes: Mode[]; /** * Checks if the current commandList is a potential remap. */ private _isPotentialRemap = false; /** * If the commandList has a remap but there is still another potential remap we * call it an Ambiguous Remap and we store it here. If later we need to handle it * we don't need to go looking for it. */ private hasAmbiguousRemap: IKeyRemapping | undefined; /** * If the commandList is a potential remap but has no ambiguous remap * yet, we say that it has a Potential Remap. * * This is to distinguish the commands with ambiguous remaps and the * ones without. * * Example 1: if 'aaaa' is mapped and so is 'aa', when the user has pressed * 'aaa' we say it has an Ambiguous Remap which is 'aa', because if the * user presses other key than 'a' next or waits for the timeout to finish * we need to now that there was a remap to run so we first run the 'aa' * remap and then handle the remaining keys. * * Example 2: if only 'aaaa' is mapped, when the user has pressed 'aaa' * we say it has a Potential Remap, because if the user presses other key * than 'a' next or waits for the timeout to finish we need to now that * there was a potential remap that never came or was broken, so we can * resend the keys again without allowing for a potential remap on the first * key, which means we won't get to the same state because the first key * will be handled as an action (in this case an 'Insert') */ private hasPotentialRemap = false; get isPotentialRemap(): boolean { return this._isPotentialRemap; } constructor(configKey: string, remappedModes: Mode[]) { this.configKey = configKey; this.remappedModes = remappedModes; } public async sendKey(keys: string[], modeHandler: ModeHandler): Promise { const { vimState, remapState } = modeHandler; this._isPotentialRemap = false; const allowPotentialRemapOnFirstKey = vimState.recordedState.allowPotentialRemapOnFirstKey; let remainingKeys: string[] = []; /** * Means that the timeout finished so we now can't allow the keys to be buffered again * because the user already waited for timeout. */ let allowBufferingKeys = true; if (!this.remappedModes.includes(vimState.currentModeIncludingPseudoModes)) { return false; } const userDefinedRemappings = configuration[this.configKey] as Map; if (keys.at(-1) === SpecialKeys.TimeoutFinished) { // Timeout finished. Don't let an ambiguous or potential remap start another timeout again keys = keys.slice(0, keys.length - 1); allowBufferingKeys = false; } if (keys.length === 0) { return true; } Logger.trace( `trying to find matching remap. keys=${keys}. mode=${ Mode[vimState.currentMode] }. keybindings=${this.configKey}.`, ); let remapping: IKeyRemapping | undefined = this.findMatchingRemap(userDefinedRemappings, keys); // Check to see if a remapping could potentially be applied when more keys are received let isPotentialRemap = Remapper.hasPotentialRemap(keys, userDefinedRemappings); this._isPotentialRemap = isPotentialRemap && allowBufferingKeys && allowPotentialRemapOnFirstKey; /** * Handle a broken potential or ambiguous remap * 1. If this Remapper doesn't have a remapping AND * 2. (It previously had an AmbiguousRemap OR a PotentialRemap) AND * 3. (It doesn't have a potential remap anymore OR timeout finished) AND * 4. keys length is more than 1 * * Points 1-3: If we no longer have a remapping but previously had one or a potential one * and there is no longer potential remappings because of another pressed key or because the * timeout has passed we need to handle those situations by resending the keys or handling the * ambiguous remap and resending any remaining keys. * Point 4: if there is only one key there is no point in resending it without allowing remaps * on first key, we can let the remapper go to the end because since either there was no potential * remap anymore or the timeout finished so this means that the next two checks (the 'Buffer keys * and create timeout' and 'Handle remapping and remaining keys') will never be hit, so it reaches * the end without doing anything which means that this key will be handled as an action as intended. */ if ( !remapping && (this.hasAmbiguousRemap || this.hasPotentialRemap) && (!isPotentialRemap || !allowBufferingKeys) && keys.length > 1 ) { if (this.hasAmbiguousRemap) { remapping = this.hasAmbiguousRemap; isPotentialRemap = false; this._isPotentialRemap = false; // Use the commandList to get the remaining keys so that it includes any existing // '' key remainingKeys = vimState.recordedState.commandList.slice(remapping.before.length); this.hasAmbiguousRemap = undefined; } if (!remapping) { // if there is still no remapping, handle all the keys without allowing // a potential remap on the first key so that we don't repeat everything // again, but still allow for other ambiguous remaps after the first key. // // Example: if 'iiii' is mapped in normal and 'ii' is mapped in insert mode, // and the user presses 'iiia' in normal mode or presses 'iii' and waits // for the timeout to finish, we want the first 'i' to be handled without // allowing potential remaps, which means it will go into insert mode, // but then the next 'ii' should be remapped in insert mode and after the // remap the 'a' should be handled. if (!allowBufferingKeys) { // Timeout finished and there is no remapping, so handle the buffered // keys but resend the '' key as well so we don't wait // for the timeout again but can still handle potential remaps. // // Example 1: if 'ccc' is mapped in normal mode and user presses 'cc' and // waits for the timeout to finish, this will resend the 'cc' // keys without allowing a potential remap on first key, which makes the // first 'c' be handled as a 'ChangeOperator' and the second 'c' which has // potential remaps (the 'ccc' remap) is buffered and the timeout started // but then the '' key comes straight away that clears the // timeout without waiting again, and makes the second 'c' be handled normally // as another 'ChangeOperator'. // // Example 2: if 'iiii' is mapped in normal and 'ii' is mapped in insert // mode, and the user presses 'iii' in normal mode and waits for the timeout // to finish, this will resend the 'iii' keys without allowing // a potential remap on first key, which makes the first 'i' be handled as // an 'CommandInsertAtCursor' and goes to insert mode, next the second 'i' // is buffered, then the third 'i' finds the insert mode remapping of 'ii' // and handles that remap, after the remapping being handled the '' // key comes that clears the timeout and since the commandList will be empty // we return true as we finished handling this sequence of keys. keys.push(SpecialKeys.TimeoutFinished); // include the '' key Logger.trace( `${this.configKey}. timeout finished, handling timed out buffer keys without allowing a new timeout.`, ); } Logger.trace( `${this.configKey}. potential remap broken. resending keys without allowing a potential remap on first key. keys=${keys}`, ); this.hasPotentialRemap = false; vimState.recordedState.allowPotentialRemapOnFirstKey = false; vimState.recordedState.resetCommandList(); if (remapState.wasPerformingRemapThatFinishedWaitingForTimeout) { // Some keys that broke the possible remap were typed by the user so handle them seperatly const lastRemapLength = remapState.wasPerformingRemapThatFinishedWaitingForTimeout.after!.length; const keysPressedByUser = keys.slice(lastRemapLength); keys = keys.slice(0, lastRemapLength); try { remapState.isCurrentlyPerformingRecursiveRemapping = true; await modeHandler.handleMultipleKeyEvents(keys); } catch (e) { if (e instanceof ForceStopRemappingError) { Logger.trace( `${this.configKey}. Stopped the remapping in the middle, ignoring the rest. Reason: ${e.message}`, ); } } finally { remapState.isCurrentlyPerformingRecursiveRemapping = false; remapState.wasPerformingRemapThatFinishedWaitingForTimeout = false; await modeHandler.handleMultipleKeyEvents(keysPressedByUser); } } else { Logger.debug(`Remapping to ${keys}`); await modeHandler.handleMultipleKeyEvents(keys); } return true; } } /** * Buffer keys and create timeout * 1. If the current keys have a potential remap AND * 2. The timeout hasn't finished yet so we allow buffering keys AND * 3. We allow potential remap on first key (check the note on RecordedState. TLDR: this will only * be false for one key, the first one, when we resend keys that had a potential remap but no longer * have it or the timeout finished) * * Points 1-3: If the current keys still have a potential remap and the timeout hasn't finished yet * and we are not preventing a potential remap on the first key then we need to buffer this keys * and wait for another key or the timeout to finish. */ if (isPotentialRemap && allowBufferingKeys && allowPotentialRemapOnFirstKey) { if (remapping) { // There are other potential remaps (ambiguous remaps), wait for other key or for the timeout // to finish. Also store this current ambiguous remap on '_hasAmbiguousRemap' so that if later // this ambiguous remap is broken or the user waits for timeout we don't need to go looking for // it again. this.hasAmbiguousRemap = remapping; Logger.trace( `${this.configKey}. ambiguous match found. before=${remapping.before}. after=${remapping.after}. command=${remapping.commands}. waiting for other key or timeout to finish.`, ); } else { this.hasPotentialRemap = true; Logger.trace( `${this.configKey}. potential remap found. waiting for other key or timeout to finish.`, ); } // Store BufferedKeys vimState.recordedState.bufferedKeys = [...keys]; // Create Timeout vimState.recordedState.bufferedKeysTimeoutObj = setTimeout(() => { void modeHandler.handleKeyEvent(SpecialKeys.TimeoutFinished); }, configuration.timeout); return true; } /** * Handle Remapping and any remaining keys * If we get here with a remapping that means we need to handle it. */ if (remapping) { if (!allowBufferingKeys) { // If the user already waited for the timeout to finish, prevent the // remapping from waiting for the timeout again by making a clone of // remapping and change 'after' to send the '' key at // the end. const newRemapping = { ...remapping }; newRemapping.after = remapping.after?.slice(0); newRemapping.after?.push(SpecialKeys.TimeoutFinished); remapping = newRemapping; } this.hasAmbiguousRemap = undefined; this.hasPotentialRemap = false; let skipFirstCharacter = false; // If we were performing a remapping already, it means this remapping has a parent remapping const hasParentRemapping = remapState.isCurrentlyPerformingRemapping; if (!hasParentRemapping) { remapState.mapDepth = 0; } if (!remapping.recursive) { remapState.isCurrentlyPerformingNonRecursiveRemapping = true; } else { remapState.isCurrentlyPerformingRecursiveRemapping = true; // As per the Vim documentation: (:help recursive) // If the {rhs} starts with {lhs}, the first character is not mapped // again (this is Vi compatible). // For example: // map ab abcd // will execute the "a" command and insert "bcd" in the text. The "ab" // in the {rhs} will not be mapped again. if (remapping.after?.join('').startsWith(remapping.before.join(''))) { skipFirstCharacter = true; } } // Increase mapDepth remapState.mapDepth++; Logger.trace( `${this.configKey}. match found. before=${remapping.before}. after=${remapping.after}. command=${remapping.commands}. remainingKeys=${remainingKeys}. mapDepth=${remapState.mapDepth}.`, ); let remapFailed = false; try { // Check maxMapDepth if (remapState.mapDepth >= configuration.maxmapdepth) { const vimError = VimError.RecursiveMapping(); StatusBar.displayError(vimState, vimError); throw ForceStopRemappingError.fromVimError(vimError); } // Hacky code incoming!!! If someone has a better way to do this please change it if (remapState.mapDepth % 10 === 0) { // Allow the user to press or key when inside an infinite looping remap. // When inside an infinite looping recursive mapping it would block the editor until it reached // the maxmapdepth. This 0ms wait allows the extension to handle any key typed by the user which // means it allows the user to press or to force stop the looping remap. // This shouldn't impact the normal use case because we're only running this every 10 nested // remaps. Also when the logs are set to Error only, a looping recursive remap takes around 1.5s // to reach 1000 mapDepth and give back control to the user, but when logs are set to debug it // can take as long as 7 seconds. const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)); await wait(0); } remapState.remapUsedACharacter = false; await this.handleRemapping(remapping, modeHandler, skipFirstCharacter); } catch (e) { if (e instanceof ForceStopRemappingError) { // If a motion fails or a VimError happens during any kind of remapping or if the user presses the // force stop remapping key ( or ) during a recursive remapping it should stop handling // the remap and all its parent remaps if we are on a chain of recursive remaps. // (Vim documentation :help map-error) remapFailed = true; // keep throwing until we reach the first parent if (hasParentRemapping) { throw e; } Logger.trace( `${this.configKey}. Stopped the remapping in the middle, ignoring the rest. Reason: ${e.message}`, ); } else { // If some other error happens during the remapping handling it should stop the remap and rethrow Logger.trace( `${this.configKey}. error found in the middle of remapping, ignoring the rest of the remap. error: ${e}`, ); throw e; } } finally { // Check if we are still inside a recursive remap if (!hasParentRemapping && remapState.isCurrentlyPerformingRecursiveRemapping) { // no more recursive remappings being handled if (vimState.recordedState.bufferedKeysTimeoutObj !== undefined) { // In order to be able to receive other keys and at the same time wait for timeout, we need // to create a timeout and return from the remapper so that modeHandler can be free to receive // more keys. This means that if we are inside a recursive remapping, when we return on the // last key of that remapping it will think that it is finished and set the currently // performing recursive remapping flag to false, which would result in the current bufferedKeys // not knowing they had a parent remapping. So we store that remapping here. remapState.wasPerformingRemapThatFinishedWaitingForTimeout = { ...remapping }; } remapState.isCurrentlyPerformingRecursiveRemapping = false; remapState.forceStopRecursiveRemapping = false; } if (!hasParentRemapping) { // Last remapping finished handling. Set undo step. vimState.historyTracker.finishCurrentStep(); } // NonRecursive remappings can't have nested remaps so after a finished remap we always set this to // false, because either we were performing a non recursive remap and now we finish or we weren't // performing a non recursive remapping and this was false anyway. remapState.isCurrentlyPerformingNonRecursiveRemapping = false; // if there were other remaining keys on the buffered keys that weren't part of the remapping // handle them now, except if the remap failed and the remaining keys weren't typed by the user. // (we know that if this remapping has a parent remapping then the remaining keys weren't typed // by the user, but instead were sent by the parent remapping handler) if (remainingKeys.length > 0 && !(remapFailed && hasParentRemapping)) { if (remapState.wasPerformingRemapThatFinishedWaitingForTimeout) { // If there was a performing remap that finished waiting for timeout then only the remaining keys // that are not part of that remap were typed by the user. let specialKey: string | undefined = ''; if (remainingKeys.at(-1) === SpecialKeys.TimeoutFinished) { specialKey = remainingKeys.pop(); } const lastRemap = remapState.wasPerformingRemapThatFinishedWaitingForTimeout.after!; const lastRemapWithoutAmbiguousRemap = lastRemap.slice(remapping.before.length); const keysPressedByUser = remainingKeys.slice(lastRemapWithoutAmbiguousRemap.length); remainingKeys = remainingKeys.slice(0, remainingKeys.length - keysPressedByUser.length); if (specialKey) { remainingKeys.push(specialKey); if (keysPressedByUser.length !== 0) { keysPressedByUser.push(specialKey); } } try { remapState.isCurrentlyPerformingRecursiveRemapping = true; await modeHandler.handleMultipleKeyEvents(remainingKeys); } catch (e) { Logger.trace( // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access `${this.configKey}. Stopped the remapping in the middle, ignoring the rest. Reason: ${e.message}`, ); } finally { remapState.isCurrentlyPerformingRecursiveRemapping = false; remapState.wasPerformingRemapThatFinishedWaitingForTimeout = false; if (keysPressedByUser.length > 0) { await modeHandler.handleMultipleKeyEvents(keysPressedByUser); } } } else { await modeHandler.handleMultipleKeyEvents(remainingKeys); } } } return true; } this.hasPotentialRemap = false; this.hasAmbiguousRemap = undefined; return false; } private async handleRemapping( remapping: IKeyRemapping, modeHandler: ModeHandler, skipFirstCharacter: boolean, ) { const { vimState, remapState } = modeHandler; vimState.recordedState.resetCommandList(); if (remapping.after) { Logger.debug(`Remapping ${remapping.before} to ${remapping.after}`); if (skipFirstCharacter) { remapState.isCurrentlyPerformingNonRecursiveRemapping = true; await modeHandler.handleKeyEvent(remapping.after[0]); remapState.isCurrentlyPerformingNonRecursiveRemapping = false; await modeHandler.handleMultipleKeyEvents(remapping.after.slice(1)); } else { await modeHandler.handleMultipleKeyEvents(remapping.after); } } if (remapping.commands) { const count = vimState.recordedState.count || 1; vimState.recordedState.count = 0; for (let i = 0; i < count; i++) { for (const command of remapping.commands) { let commandString: string; let commandArgs: string[]; if (typeof command === 'string') { commandString = command; commandArgs = []; } else { commandString = command.command; commandArgs = Array.isArray(command.args) ? (command.args as string[]) : command.args ? [command.args] : []; } if (commandString.slice(0, 1) === ':') { // Check if this is a vim command by looking for : // TODO: Parse once & cache? const result = exCommandParser.parse(commandString); if (result.status) { if (result.value.lineRange) { await result.value.command.executeWithRange(vimState, result.value.lineRange); } else { await result.value.command.execute(vimState); } } else { throw VimError.NotAnEditorCommand(commandString); } modeHandler.updateView(); } else { await vscode.commands.executeCommand(commandString, ...commandArgs); } // TODO add test cases (silent defined in IKeyRemapping) if (!remapping.silent) { StatusBar.setText(vimState, `${commandString} ${commandArgs.join(' ')}`); } } } } } protected findMatchingRemap( userDefinedRemappings: Map, inputtedKeys: string[], ): IKeyRemapping | undefined { if (userDefinedRemappings.size === 0) { return undefined; } const range = Remapper.getRemappedKeysLengthRange(userDefinedRemappings); const startingSliceLength = inputtedKeys.length; const inputtedString = inputtedKeys.join(''); for (let sliceLength = startingSliceLength; sliceLength >= range[0]; sliceLength--) { const keySlice = inputtedKeys.slice(-sliceLength).join(''); Logger.trace(`key=${inputtedKeys}. keySlice=${keySlice}.`); if (userDefinedRemappings.has(keySlice)) { const precedingKeys = inputtedString.slice(0, inputtedString.length - keySlice.length); if (precedingKeys.length > 0 && !/^[0-9]+$/.test(precedingKeys)) { Logger.trace(`key sequences need to match precisely. precedingKeys=${precedingKeys}.`); break; } return userDefinedRemappings.get(keySlice); } } return undefined; } /** * Given list of remappings, returns the length of the shortest and longest remapped keys * @param remappings */ protected static getRemappedKeysLengthRange( remappings: ReadonlyMap, ): [number, number] { if (remappings.size === 0) { return [0, 0]; } const keyLengths = Array.from(remappings.values()).map((remap) => remap.before.length); return [Math.min(...keyLengths), Math.max(...keyLengths)]; } /** * Given list of keys and list of remappings, returns true if the keys are a potential remap * @param keys the list of keys to be checked for potential remaps * @param remappings The remappings Map * @param countRemapAsPotential If the current keys are themselves a remap should they be considered a potential remap as well? */ protected static hasPotentialRemap( keys: string[], remappings: ReadonlyMap, countRemapAsPotential: boolean = false, ): boolean { const keysAsString = keys.join(''); const re = /^<([^>]+)>/; if (keysAsString !== '') { for (const remap of remappings.keys()) { if (remap.startsWith(keysAsString) && (remap !== keysAsString || countRemapAsPotential)) { // Don't confuse a key combination starting with '<' that is not a special key like '' // with a remap that starts with a special key. if (keysAsString.startsWith('<') && !re.test(keysAsString) && re.test(remap)) { continue; } return true; } } } return false; } } function keyBindingsConfigKey(mode: string): string { return `${mode}ModeKeyBindingsMap`; } class InsertModeRemapper extends Remapper { constructor() { super(keyBindingsConfigKey('insert'), [Mode.Insert, Mode.Replace]); } } class NormalModeRemapper extends Remapper { constructor() { super(keyBindingsConfigKey('normal'), [Mode.Normal]); } } class OperatorPendingModeRemapper extends Remapper { constructor() { super(keyBindingsConfigKey('operatorPending'), [Mode.OperatorPendingMode]); } } class VisualModeRemapper extends Remapper { constructor() { super(keyBindingsConfigKey('visual'), [Mode.Visual, Mode.VisualLine, Mode.VisualBlock]); } } class CommandLineModeRemapper extends Remapper { constructor() { super(keyBindingsConfigKey('commandLine'), [ Mode.CommandlineInProgress, Mode.SearchInProgressMode, ]); } } ================================================ FILE: src/configuration/validators/inputMethodSwitcherValidator.ts ================================================ import { existsAsync } from 'platform/fs'; import { Globals } from '../../globals'; import { configurationValidator } from '../configurationValidator'; import { IConfiguration } from '../iconfiguration'; import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; export class InputMethodSwitcherConfigurationValidator implements IConfigurationValidator { async validate(config: IConfiguration): Promise { const result = new ValidatorResults(); const inputMethodConfig = config.autoSwitchInputMethod; if (!inputMethodConfig.enable || Globals.isTesting) { return Promise.resolve(result); } if (!inputMethodConfig.switchIMCmd.includes('{im}')) { result.append({ level: 'error', message: 'vim.autoSwitchInputMethod.switchIMCmd is incorrect, it should contain the placeholder {im}.', }); } if (inputMethodConfig.obtainIMCmd === undefined || inputMethodConfig.obtainIMCmd === '') { result.append({ level: 'error', message: 'vim.autoSwitchInputMethod.obtainIMCmd is empty.', }); } else if (!(await existsAsync(this.getRawCmd(inputMethodConfig.obtainIMCmd)))) { result.append({ level: 'error', message: `Unable to find ${inputMethodConfig.obtainIMCmd}. Check your 'vim.autoSwitchInputMethod.obtainIMCmd' in VSCode setting.`, }); } if (inputMethodConfig.defaultIM === undefined || inputMethodConfig.defaultIM === '') { result.append({ level: 'error', message: 'vim.autoSwitchInputMethod.defaultIM is empty.', }); } else if (!(await existsAsync(this.getRawCmd(inputMethodConfig.switchIMCmd)))) { result.append({ level: 'error', message: `Unable to find ${inputMethodConfig.switchIMCmd}. Check your 'vim.autoSwitchInputMethod.switchIMCmd' in VSCode setting.`, }); } return Promise.resolve(result); } disable(config: IConfiguration) { config.autoSwitchInputMethod.enable = false; } private getRawCmd(cmd: string): string { return cmd.split(' ')[0]; } } configurationValidator.registerValidator(new InputMethodSwitcherConfigurationValidator()); ================================================ FILE: src/configuration/validators/neovimValidator.ts ================================================ import { execFileSync } from 'child_process'; import { existsSync } from 'fs'; import * as path from 'path'; import * as process from 'process'; import { configurationValidator } from '../configurationValidator'; import { IConfiguration } from '../iconfiguration'; import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; export class NeovimValidator implements IConfigurationValidator { validate(config: IConfiguration): Promise { const result = new ValidatorResults(); if (config.enableNeovim) { let triedToParsePath = false; try { // Try to find nvim in path if it is not defined if (config.neovimPath === '') { const pathVar = process.env.PATH; if (pathVar) { pathVar.split(path.delimiter).forEach((element) => { let neovimExecutable = 'nvim'; if (process.platform === 'win32') { neovimExecutable += '.exe'; } const testPath = path.join(element, neovimExecutable); if (existsSync(testPath)) { config.neovimPath = testPath; triedToParsePath = true; return; } }); } } execFileSync(config.neovimPath, ['--version']); } catch (e) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access let errorMessage = `Invalid neovimPath. ${e.message}.`; if (triedToParsePath) { errorMessage += `Tried to parse PATH ${config.neovimPath}.`; } result.append({ level: 'error', message: errorMessage, }); } // If Neovim config path doesn't exist, default to empty config path. if (config.neovimUseConfigFile && config.neovimConfigPath !== '') { if (!existsSync(config.neovimConfigPath)) { const warningMessage = `No config file found in neovimConfigPath. Neovim will search its default config path.`; config.neovimConfigPath = ''; result.append({ level: 'warning', message: warningMessage, }); } } } return Promise.resolve(result); } disable(config: IConfiguration) { config.enableNeovim = false; } } configurationValidator.registerValidator(new NeovimValidator()); ================================================ FILE: src/configuration/validators/remappingValidator.ts ================================================ import * as vscode from 'vscode'; import { PluginDefaultMappings } from '../../actions/plugins/pluginDefaultMappings'; import { configurationValidator } from '../configurationValidator'; import { IConfiguration, IKeyRemapping } from '../iconfiguration'; import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; import { Notation } from '../notation'; export class RemappingValidator implements IConfigurationValidator { private commandMap!: Map; async validate(config: IConfiguration): Promise { const result = new ValidatorResults(); const modeKeyBindingsKeys = [ 'insertModeKeyBindings', 'insertModeKeyBindingsNonRecursive', 'normalModeKeyBindings', 'normalModeKeyBindingsNonRecursive', 'operatorPendingModeKeyBindings', 'operatorPendingModeKeyBindingsNonRecursive', 'visualModeKeyBindings', 'visualModeKeyBindingsNonRecursive', 'commandLineModeKeyBindings', 'commandLineModeKeyBindingsNonRecursive', ]; for (const modeKeyBindingsKey of modeKeyBindingsKeys) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const keybindings = config[modeKeyBindingsKey]; // add default mappings for activated plugins // because we process keybindings backwards in next loop, user mapping will override for (const pluginMapping of PluginDefaultMappings.getPluginDefaultMappings( modeKeyBindingsKey, config, )) { // note concat(all mappings) does not work somehow // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access keybindings.push(pluginMapping); } const isRecursive = modeKeyBindingsKey.indexOf('NonRecursive') === -1; const modeMapName = modeKeyBindingsKey.replace('NonRecursive', ''); let modeKeyBindingsMap = config[modeMapName + 'Map'] as Map; if (!modeKeyBindingsMap) { modeKeyBindingsMap = new Map(); } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access for (let i = keybindings.length - 1; i >= 0; i--) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const remapping = keybindings[i] as IKeyRemapping; // set 'recursive' of the remapping according to where it was stored remapping.recursive = isRecursive; // validate const remappingError = await this.isRemappingValid(remapping); result.concat(remappingError); if (remappingError.hasError) { // errors with remapping, skip // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access keybindings.splice(i, 1); continue; } // normalize if (remapping.before) { remapping.before.forEach( (key, idx) => (remapping.before[idx] = Notation.NormalizeKey(key, config.leader)), ); } if (remapping.after) { remapping.after.forEach( (key, idx) => (remapping.after![idx] = Notation.NormalizeKey(key, config.leader)), ); } // check for duplicates const beforeKeys = remapping.before.join(''); if (modeKeyBindingsMap.has(beforeKeys)) { result.append({ level: 'warning', message: `${remapping.before}. Duplicate remapped key for ${beforeKeys}.`, }); continue; } // add to map modeKeyBindingsMap.set(beforeKeys, remapping); } config[modeMapName + 'Map'] = modeKeyBindingsMap; } return result; } disable(config: IConfiguration) { // no-op } private async isRemappingValid(remapping: IKeyRemapping): Promise { const result = new ValidatorResults(); if (!remapping.after && !remapping.commands) { result.append({ level: 'error', message: `${remapping.before} missing 'after' key or 'commands'.`, }); } if (!(remapping.before instanceof Array)) { result.append({ level: 'error', message: `Remapping of '${remapping.before}' should be a string array.`, }); } if (remapping.recursive === undefined) { result.append({ level: 'error', message: `Remapping of '${remapping.before}' missing 'recursive' info.`, }); } if (remapping.after && !(remapping.after instanceof Array)) { result.append({ level: 'error', message: `Remapping of '${remapping.after}' should be a string array.`, }); } if (remapping.commands) { for (const command of remapping.commands) { let cmd: string; if (typeof command === 'string') { cmd = command; } else if (command.command) { cmd = command.command; if (!(await this.isCommandValid(cmd))) { result.append({ level: 'warning', message: `${cmd} does not exist.` }); } } else { result.append({ level: 'error', message: `Remapping of '${remapping.before}' has wrong "commands" structure. Should be 'string[] | { "command": string, "args": any[] }[]'.`, }); } } } return result; } private async isCommandValid(command: string): Promise { if (command.startsWith(':')) { return true; } return (await this.getCommandMap()).has(command); } private async getCommandMap(): Promise> { if (this.commandMap == null) { this.commandMap = new Map( (await vscode.commands.getCommands(true)).map((x) => [x, true] as [string, boolean]), ); } return this.commandMap; } } configurationValidator.registerValidator(new RemappingValidator()); ================================================ FILE: src/configuration/validators/vimrcValidator.ts ================================================ import { configurationValidator } from '../configurationValidator'; import { IConfiguration } from '../iconfiguration'; import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; export class VimrcValidator implements IConfigurationValidator { async validate(config: IConfiguration): Promise { const result = new ValidatorResults(); // if (config.vimrc.enable && !fs.existsSync(vimrc.vimrcPath)) { // result.append({ // level: 'error', // message: `.vimrc not found at ${config.vimrc.path}`, // }); // } return result; } disable(config: IConfiguration): void { // no-op } } configurationValidator.registerValidator(new VimrcValidator()); ================================================ FILE: src/configuration/vimrc.ts ================================================ import * as _ from 'lodash'; import * as os from 'os'; import * as path from 'path'; import * as fs from 'platform/fs'; import * as vscode from 'vscode'; import { window } from 'vscode'; import { Logger } from '../util/logger'; import { IConfiguration, IVimrcKeyRemapping } from './iconfiguration'; import { vimrcKeyRemappingBuilder } from './vimrcKeyRemappingBuilder'; export class VimrcImpl { private _vimrcPath?: string; /** * Fully resolved path to the user's .vimrc */ public get vimrcPath(): string | undefined { return this._vimrcPath; } private static readonly SOURCE_REG_REX = /^(source)\s+(.+)/i; private static buildSource(line: string) { const matches = VimrcImpl.SOURCE_REG_REX.exec(line); if (!matches || matches.length < 3) { return undefined; } const sourceKeyword = matches[1]; const filePath = matches[2]; return VimrcImpl.expandHome(filePath); } private static async loadConfig(config: IConfiguration, configPath: string) { try { const vscodeCommands = await vscode.commands.getCommands(); const lines = (await fs.readFileAsync(configPath, 'utf8')).split(/\r?\n/); for (const line of lines) { if (line.trimStart().startsWith('"')) { continue; } const source = this.buildSource(line); if (source) { if (!(await fs.existsAsync(source))) { Logger.warn(`Unable to find "${source}" file for configuration.`); continue; } Logger.debug(`Loading "${source}" file for configuration.`); await VimrcImpl.loadConfig(config, source); continue; } const remap = await vimrcKeyRemappingBuilder.build(line, vscodeCommands); if (remap) { VimrcImpl.addRemapToConfig(config, remap); continue; } const unremap = await vimrcKeyRemappingBuilder.buildUnmapping(line); if (unremap) { VimrcImpl.removeRemapFromConfig(config, unremap); continue; } const clearRemap = await vimrcKeyRemappingBuilder.buildClearMapping(line); if (clearRemap) { VimrcImpl.clearRemapsFromConfig(config, clearRemap); continue; } } } catch (err) { void window.showWarningMessage(`vimrc file "${configPath}" is broken, err=${err}`); } } public async load(config: IConfiguration) { const _path = config.vimrc.path ? VimrcImpl.expandHome(config.vimrc.path) : await VimrcImpl.findDefaultVimrc(); if (!_path) { await window.showWarningMessage('No .vimrc found. Please set `vim.vimrc.path`.'); return; } if (!(await fs.existsAsync(_path))) { void window .showWarningMessage(`No .vimrc found at ${_path}.`, 'Create it') .then(async (choice: string | undefined) => { if (choice === 'Create it') { const newVimrc = await vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file(_path), }); if (newVimrc) { await fs.writeFileAsync(newVimrc.fsPath, '', 'utf-8'); const document = vscode.window.activeTextEditor?.document; const resource = document ? { uri: document.uri, languageId: document.languageId } : undefined; void vscode.workspace .getConfiguration('vim', resource) .update('vimrc.path', newVimrc.fsPath, true); await vscode.workspace.openTextDocument(newVimrc); // TODO: add some sample remaps/settings in here? await vscode.window.showTextDocument(newVimrc); } } }); } else { this._vimrcPath = _path; // Remove all the old remappings from the .vimrc file VimrcImpl.removeAllRemapsFromConfig(config); // Add the new remappings await VimrcImpl.loadConfig(config, this._vimrcPath); } } /** * Adds a remapping from .vimrc to the given configuration */ public static addRemapToConfig(config: IConfiguration, remap: IVimrcKeyRemapping): void { const mappings = (() => { switch (remap.keyRemappingType) { case 'map': return [ config.normalModeKeyBindings, config.visualModeKeyBindings, config.operatorPendingModeKeyBindings, ]; case 'nmap': case 'nma': case 'nm': return [config.normalModeKeyBindings]; case 'vmap': case 'vma': case 'vm': case 'xmap': case 'xma': case 'xm': return [config.visualModeKeyBindings]; case 'imap': case 'ima': case 'im': return [config.insertModeKeyBindings]; case 'cmap': case 'cma': case 'cm': return [config.commandLineModeKeyBindings]; case 'omap': case 'oma': case 'om': return [config.operatorPendingModeKeyBindings]; case 'lmap': case 'lma': case 'lm': case 'map!': return [config.insertModeKeyBindings, config.commandLineModeKeyBindings]; case 'noremap': case 'norema': case 'norem': case 'nore': case 'nor': case 'no': return [ config.normalModeKeyBindingsNonRecursive, config.visualModeKeyBindingsNonRecursive, config.operatorPendingModeKeyBindingsNonRecursive, ]; case 'nnoremap': case 'nnorema': case 'nnorem': case 'nnore': case 'nnor': case 'nno': case 'nn': return [config.normalModeKeyBindingsNonRecursive]; case 'vnoremap': case 'vnorema': case 'vnorem': case 'vnore': case 'vnor': case 'vno': case 'vn': case 'xnoremap': case 'xnorema': case 'xnorem': case 'xnore': case 'xnor': case 'xno': case 'xn': return [config.visualModeKeyBindingsNonRecursive]; case 'inoremap': case 'inorema': case 'inorem': case 'inore': case 'inor': case 'ino': return [config.insertModeKeyBindingsNonRecursive]; case 'cnoremap': case 'cnorema': case 'cnorem': case 'cnore': case 'cnor': case 'cno': return [config.commandLineModeKeyBindingsNonRecursive]; case 'onoremap': case 'onorema': case 'onorem': case 'onore': case 'onor': case 'ono': return [config.operatorPendingModeKeyBindingsNonRecursive]; case 'lnoremap': case 'lnorema': case 'lnorem': case 'lnore': case 'lnor': case 'lno': case 'ln': case 'noremap!': case 'norema!': case 'norem!': case 'nore!': case 'nor!': case 'no!': return [ config.insertModeKeyBindingsNonRecursive, config.commandLineModeKeyBindingsNonRecursive, ]; default: Logger.warn(`Encountered an unrecognized mapping type: '${remap.keyRemappingType}'`); return undefined; } })(); mappings?.forEach((remaps) => { // Don't override a mapping present in settings.json; those are more specific to VSCodeVim. if (!remaps.some((r) => _.isEqual(r.before, remap.keyRemapping.before))) { remaps.push(remap.keyRemapping); } }); } /** * Removes a remapping from .vimrc from the given configuration */ public static removeRemapFromConfig(config: IConfiguration, remap: IVimrcKeyRemapping): boolean { const mappings = (() => { switch (remap.keyRemappingType) { case 'unmap': case 'unma': case 'unm': return [ config.normalModeKeyBindings, config.normalModeKeyBindingsNonRecursive, config.visualModeKeyBindings, config.visualModeKeyBindingsNonRecursive, config.operatorPendingModeKeyBindings, config.operatorPendingModeKeyBindingsNonRecursive, ]; case 'nunmap': case 'nunma': case 'nunm': case 'nun': return [config.normalModeKeyBindings, config.normalModeKeyBindingsNonRecursive]; case 'vunmap': case 'vunma': case 'vunm': case 'vun': case 'vu': case 'xunmap': case 'xunma': case 'xunm': case 'xun': case 'xu': return [config.visualModeKeyBindings, config.visualModeKeyBindingsNonRecursive]; case 'iunmap': case 'iunma': case 'iunm': case 'iun': case 'iu': return [config.insertModeKeyBindings, config.insertModeKeyBindingsNonRecursive]; case 'cunmap': case 'cunma': case 'cunm': case 'cun': case 'cu': return [config.commandLineModeKeyBindings, config.commandLineModeKeyBindingsNonRecursive]; case 'ounmap': case 'ounma': case 'ounm': case 'oun': case 'ou': return [ config.operatorPendingModeKeyBindings, config.operatorPendingModeKeyBindingsNonRecursive, ]; case 'lunmap': case 'lunma': case 'lunm': case 'lun': case 'lu': case 'unmap!': case 'unma!': case 'unm!': return [ config.insertModeKeyBindings, config.insertModeKeyBindingsNonRecursive, config.commandLineModeKeyBindings, config.commandLineModeKeyBindingsNonRecursive, ]; default: Logger.warn(`Encountered an unrecognized unmapping type: '${remap.keyRemappingType}'`); return undefined; } })(); if (mappings) { mappings.forEach((remaps) => { // Don't remove a mapping present in settings.json; those are more specific to VSCodeVim. _.remove( remaps, (r) => r.source === 'vimrc' && _.isEqual(r.before, remap.keyRemapping.before), ); }); return true; } return false; } /** * Clears all remappings from .vimrc from the given configuration for specific mode */ public static clearRemapsFromConfig(config: IConfiguration, remap: IVimrcKeyRemapping): boolean { const mappings = (() => { switch (remap.keyRemappingType) { case 'mapclear': case 'mapclea': case 'mapcle': case 'mapcl': case 'mapc': return [ config.normalModeKeyBindings, config.normalModeKeyBindingsNonRecursive, config.visualModeKeyBindings, config.visualModeKeyBindingsNonRecursive, config.operatorPendingModeKeyBindings, config.operatorPendingModeKeyBindingsNonRecursive, ]; case 'nmapclear': case 'nmapclea': case 'nmapcle': case 'nmapcl': case 'nmapc': return [config.normalModeKeyBindings, config.normalModeKeyBindingsNonRecursive]; case 'vmapclear': case 'vmapclea': case 'vmapcle': case 'vmapcl': case 'vmapc': case 'xmapclear': case 'xmapclea': case 'xmapcle': case 'xmapcl': case 'xmapc': return [config.visualModeKeyBindings, config.visualModeKeyBindingsNonRecursive]; case 'imapclear': case 'imapclea': case 'imapcle': case 'imapcl': case 'imapc': return [config.insertModeKeyBindings, config.insertModeKeyBindingsNonRecursive]; case 'cmapclear': case 'cmapclea': case 'cmapcle': case 'cmapcl': case 'cmapc': return [config.commandLineModeKeyBindings, config.commandLineModeKeyBindingsNonRecursive]; case 'omapclear': case 'omapclea': case 'omapcle': case 'omapcl': case 'omapc': return [ config.operatorPendingModeKeyBindings, config.operatorPendingModeKeyBindingsNonRecursive, ]; case 'lmapclear': case 'lmapclea': case 'lmapcle': case 'lmapcl': case 'lmapc': case 'mapclear!': case 'mapclea!': case 'mapcle!': case 'mapcl!': case 'mapc!': return [ config.insertModeKeyBindings, config.insertModeKeyBindingsNonRecursive, config.commandLineModeKeyBindings, config.commandLineModeKeyBindingsNonRecursive, ]; default: Logger.warn(`Encountered an unrecognized clearMapping type: '${remap.keyRemappingType}'`); return undefined; } })(); if (mappings) { mappings.forEach((remaps) => { // Don't remove a mapping present in settings.json; those are more specific to VSCodeVim. _.remove(remaps, (r) => r.source === 'vimrc'); }); return true; } return false; } public static removeAllRemapsFromConfig(config: IConfiguration): void { const remapCollections = [ config.normalModeKeyBindings, config.operatorPendingModeKeyBindings, config.visualModeKeyBindings, config.insertModeKeyBindings, config.commandLineModeKeyBindings, config.normalModeKeyBindingsNonRecursive, config.operatorPendingModeKeyBindingsNonRecursive, config.visualModeKeyBindingsNonRecursive, config.insertModeKeyBindingsNonRecursive, config.commandLineModeKeyBindingsNonRecursive, ]; for (const remaps of remapCollections) { _.remove(remaps, (remap) => remap.source === 'vimrc'); } } private static async findDefaultVimrc(): Promise { const vscodeVimrcPath = path.join(os.homedir(), '.vscodevimrc'); if (await fs.existsAsync(vscodeVimrcPath)) { return vscodeVimrcPath; } let vimrcPath = path.join(os.homedir(), '.vimrc'); if (await fs.existsAsync(vimrcPath)) { return vimrcPath; } vimrcPath = path.join(os.homedir(), '_vimrc'); if (await fs.existsAsync(vimrcPath)) { return vimrcPath; } vimrcPath = path.join(os.homedir(), '.config/', 'nvim/', 'init.vim'); if (await fs.existsAsync(vimrcPath)) { return vimrcPath; } return undefined; } private static expandHome(filePath: string): string { // regex = Anything preceded by beginning of line // and immediately followed by '~' or '$HOME' const regex = /(?<=^(?:~|\$HOME)).*/; // Matches /pathToVimrc in $HOME/pathToVimrc or ~/pathToVimrc const matches = filePath.match(regex); if (!matches || matches.length > 1) { return filePath; } return path.join(os.homedir(), matches[0]); } } export const vimrc = new VimrcImpl(); ================================================ FILE: src/configuration/vimrcKeyRemappingBuilder.ts ================================================ import { IVimrcKeyRemapping } from './iconfiguration'; class VimrcKeyRemappingBuilderImpl { /** * Regex for mapping lines * * * `^(` -> start of mapping type capture * * `map!?`\ * _matches:_ * * :map * * :map! * * * `|smap`\ * _matches:_ * * :smap * * * `|[nvxoilc]m(?:a(?:p)?)?`\ * _matches:_ * * :nm[ap] * * :vm[ap] * * :xm[ap] * * :om[ap] * * :im[ap] * * :lm[ap] * * :cm[ap] * * * `|(?:` * * `[nvxl]no?r?|`\ * _matches:_ * * :nn[or] * * :vn[or] * * :xn[or] * * :ln[or] * * * `[oic]nor?|`\ * _matches:_ * * :ono[r] * * :ino[r] * * :cno[r] * * * `snor`\ * _matches:_ * * :snor * * `)(?:e(?:m(?:a(?:p)?)?)?)?`\ * _matches the remaining optional [emap]_ * * * `|no(?:r(?:e(?:m(?:a(?:p)?)?)?)?)?!?`\ * _matches:_ * * :no[remap] * * :no[remap]! * * `)` -> end of mapping type capture * * * `(?!.*(?:|