From 5215e3b8da7ce5f1b3f74167f6da0473beb097e6 Mon Sep 17 00:00:00 2001 From: Jason Poon Date: Thu, 2 Nov 2017 14:04:42 -0700 Subject: [PATCH] fix(line-endings): change all files to lf (#2111) --- .github/CONTRIBUTING.md | 206 +- .github/ISSUE_TEMPLATE.md | 92 +- .github/PULL_REQUEST_TEMPLATE.md | 22 +- gulpfile.js | 178 +- src/actions/commands/actions.ts | 7066 ++++++++++++------------- src/actions/motion.ts | 3740 ++++++------- src/cmd_line/commands/setoptions.ts | 212 +- src/cmd_line/subparsers/setoptions.ts | 152 +- src/common/matching/matcher.ts | 416 +- src/mode/mode.ts | 96 +- src/mode/modeInsert.ts | 22 +- src/mode/modeNormal.ts | 32 +- src/mode/modeSearchInProgress.ts | 22 +- src/mode/modeVisualBlock.ts | 66 +- test/mode/modeReplace.test.ts | 196 +- tsconfig.json | 44 +- tslint.json | 174 +- typings.json | 22 +- 18 files changed, 6379 insertions(+), 6379 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a4bec193ee9..246e62770dd 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,104 +1,104 @@ -# Contribution Guide - -The following is a set of guidelines for contributing to Vim for VSCode. -These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. -If you need help with Vim for VSCode, drop by on [Slack](https://vscodevim-slackin.azurewebsites.net/). -Thanks for helping us make Vim for VSCode better! - -## 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 -* Include as many details as possible -- include screenshots/gifs and repro steps where applicable. - -## 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, ensure: - -1. All tests pass. -2. If you added a new feature, add tests to exercise the new code path. -3. If you fixed a bug, add tests to ensure the bug stays away. -4. Submit the PR. Pour yourself a glass of champagne and feel good about contributing to open source! - -## First Time Setup - -1. Install prerequisites: - * latest [Visual Studio Code](https://code.visualstudio.com/) - * [Node.js](https://nodejs.org/) v4.0.0 or higher -2. Fork and clone the repository -3. `cd Vim` -4. Install the dependencies: - - ```bash - $ npm install -g gulp-cli - $ npm install - ``` -5. Open the folder in VS Code - -## Developing - -1. Watch for changes and recompile Typescript files. Run this in the `Vim` directory: `gulp watch` -2. Open Visual Studio Code and add the `Vim` directory as a folder. -3. Click on the debugger. You have two options - Launch Extension (to play around with the extension) and Launch Tests (to run the tests). - -## Code Architecture - -The code is split into two parts - ModeHandler (which is essentially the Vim state machine), and Actions (which are things that 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, like `w`, `h`, `{`, etc. ONLY updates the cursor position. At worst, might return 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 `*`. - -At one point, I wanted to have actions.ts be completely pure (no side effects whatsoever), so commands would just return objects indicating what side effects on the editor they would have. This explains the giant switch in handleCommand in ModeHandler. I now believe this to be a dumb idea and someone should get rid of it. - -Probably me. :wink: - -### The Vim State Machine - -It's contained entirely within modeHandler.ts. It's actually pretty complicated, and I probably won't be able to articulate all of the edge cases it contains. - -It consists of two data structures: - -* VimState - this is the state of Vim. It's what actions update. -* RecordedState - this is temporary state that will reset at the end of a change. (RecordedState is a poor name for this; I've been going back and forth on different names). - -#### 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" VSCode 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 - -To push a release: - -1. Bump the version number and create a git tag: `gulp patch|minor|major` -2. Push the changes: `git push origin --tags` - -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 - -### Visual Studio Code Slowdown - -If your autocomplete, your fuzzy file search, or your _everything_ is suddenly running slower, try to recall if you ever ran `npm test` instead of just running tests through Visual Studio Code. This will add a massive folder called `.vscode-test/` to your project, which Visual Studio Code will happily consume all of your CPU cycles indexing. - -Long story short, you can speed up VS Code by: - -`$ rm -rf .vscode-test/` - -## Styleguide - +# Contribution Guide + +The following is a set of guidelines for contributing to Vim for VSCode. +These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. +If you need help with Vim for VSCode, drop by on [Slack](https://vscodevim-slackin.azurewebsites.net/). +Thanks for helping us make Vim for VSCode better! + +## 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 +* Include as many details as possible -- include screenshots/gifs and repro steps where applicable. + +## 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, ensure: + +1. All tests pass. +2. If you added a new feature, add tests to exercise the new code path. +3. If you fixed a bug, add tests to ensure the bug stays away. +4. Submit the PR. Pour yourself a glass of champagne and feel good about contributing to open source! + +## First Time Setup + +1. Install prerequisites: + * latest [Visual Studio Code](https://code.visualstudio.com/) + * [Node.js](https://nodejs.org/) v4.0.0 or higher +2. Fork and clone the repository +3. `cd Vim` +4. Install the dependencies: + + ```bash + $ npm install -g gulp-cli + $ npm install + ``` +5. Open the folder in VS Code + +## Developing + +1. Watch for changes and recompile Typescript files. Run this in the `Vim` directory: `gulp watch` +2. Open Visual Studio Code and add the `Vim` directory as a folder. +3. Click on the debugger. You have two options - Launch Extension (to play around with the extension) and Launch Tests (to run the tests). + +## Code Architecture + +The code is split into two parts - ModeHandler (which is essentially the Vim state machine), and Actions (which are things that 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, like `w`, `h`, `{`, etc. ONLY updates the cursor position. At worst, might return 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 `*`. + +At one point, I wanted to have actions.ts be completely pure (no side effects whatsoever), so commands would just return objects indicating what side effects on the editor they would have. This explains the giant switch in handleCommand in ModeHandler. I now believe this to be a dumb idea and someone should get rid of it. + +Probably me. :wink: + +### The Vim State Machine + +It's contained entirely within modeHandler.ts. It's actually pretty complicated, and I probably won't be able to articulate all of the edge cases it contains. + +It consists of two data structures: + +* VimState - this is the state of Vim. It's what actions update. +* RecordedState - this is temporary state that will reset at the end of a change. (RecordedState is a poor name for this; I've been going back and forth on different names). + +#### 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" VSCode 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 + +To push a release: + +1. Bump the version number and create a git tag: `gulp patch|minor|major` +2. Push the changes: `git push origin --tags` + +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 + +### Visual Studio Code Slowdown + +If your autocomplete, your fuzzy file search, or your _everything_ is suddenly running slower, try to recall if you ever ran `npm test` instead of just running tests through Visual Studio Code. This will add a massive folder called `.vscode-test/` to your project, which Visual Studio Code will happily consume all of your CPU cycles indexing. + +Long story short, you can speed up VS Code by: + +`$ rm -rf .vscode-test/` + +## Styleguide + Please try your best to adhere our [style guidelines](https://github.com/VSCodeVim/Vim/blob/master/STYLE.md). \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 1dce9d471c7..657817862b3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,46 +1,46 @@ - - -* Click *thumbs-up* 👍 on this issue if you want it! -* Click *confused* 😕 on this issue if not having it makes VSCodeVim unusable. - -The VSCodeVim team prioritizes issues based on reaction count. - --------- - -**Is this a BUG REPORT or FEATURE REQUEST?** (choose one): - - - -**Environment**: - - - -- **VSCode Version**: -- **VsCodeVim Version**: -- **OS**: - -**What happened**: - - - -**What did you expect to happen**: - -**How to reproduce it**: + + +* Click *thumbs-up* 👍 on this issue if you want it! +* Click *confused* 😕 on this issue if not having it makes VSCodeVim unusable. + +The VSCodeVim team prioritizes issues based on reaction count. + +-------- + +**Is this a BUG REPORT or FEATURE REQUEST?** (choose one): + + + +**Environment**: + + + +- **VSCode Version**: +- **VsCodeVim Version**: +- **OS**: + +**What happened**: + + + +**What did you expect to happen**: + +**How to reproduce it**: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6ea43264b6f..0f7820c0847 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,11 +1,11 @@ - + diff --git a/gulpfile.js b/gulpfile.js index 5891e96a1b3..e640629dbfb 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,89 +1,89 @@ -var gulp = require('gulp'), - bump = require('gulp-bump'), - git = require('gulp-git'), - inject = require('gulp-inject-string'), - merge = require('merge-stream'), - tag_version = require('gulp-tag-version'), - tslint = require('gulp-tslint'), - typings = require('gulp-typings'), - shell = require('gulp-shell'), - exec = require('child_process').exec; - -var paths = { - src_ts: 'src/**/*.ts', - tests_ts: 'test/**/*.ts', -}; - -function versionBump(semver) { - return gulp - .src(['./package.json']) - .pipe(bump({ type: semver })) - .pipe(gulp.dest('./')) - .pipe(git.commit('bump package version')) - .pipe(tag_version()); -} - -gulp.task('typings', function() { - return gulp.src('./typings.json').pipe(typings()); -}); - -gulp.task('typings-vscode-definitions', ['typings'], function() { - // add vscode definitions - return gulp.src('./typings/index.d.ts').pipe(gulp.dest('./typings')); -}); - -gulp.task('tslint', function() { - var tslintOptions = { - summarizeFailureOutput: true, - }; - - var srcs = gulp - .src(paths.src_ts) - .pipe(tslint({ formatter: 'verbose' })) - .pipe(tslint.report(tslintOptions)); - var tests = gulp - .src(paths.tests_ts) - .pipe(tslint({ formatter: 'verbose' })) - .pipe(tslint.report(tslintOptions)); - return merge(srcs, tests); -}); - -gulp.task('prettier', function() { - runPrettier('git diff --name-only HEAD'); -}); - -gulp.task('forceprettier', function() { - // Enumerate files managed by git - runPrettier('git ls-files'); - // Enumerate untracked files - runPrettier('git ls-files --others --exclude-standard'); -}); - -function runPrettier(command) { - exec(command, function(err, stdout, stderr) { - const files = stdout.split('\n'); - for (const file of files) { - if (file.endsWith('.ts') || file.endsWith('.js')) { - exec( - `node ./node_modules/prettier/bin/prettier.js --write --print-width 100 --single-quote --trailing-comma es5 ${file}` - ); - } - } - }); -} - -gulp.task('default', ['prettier', 'tslint', 'compile']); - -gulp.task('compile', shell.task(['npm run vscode:prepublish'])); -gulp.task('watch', shell.task(['npm run compile'])); -gulp.task('init', ['typings', 'typings-vscode-definitions']); - -gulp.task('patch', ['default'], function() { - return versionBump('patch'); -}); -gulp.task('minor', ['default'], function() { - return versionBump('minor'); -}); -gulp.task('major', ['default'], function() { - return versionBump('major'); -}); +var gulp = require('gulp'), + bump = require('gulp-bump'), + git = require('gulp-git'), + inject = require('gulp-inject-string'), + merge = require('merge-stream'), + tag_version = require('gulp-tag-version'), + tslint = require('gulp-tslint'), + typings = require('gulp-typings'), + shell = require('gulp-shell'), + exec = require('child_process').exec; + +var paths = { + src_ts: 'src/**/*.ts', + tests_ts: 'test/**/*.ts', +}; + +function versionBump(semver) { + return gulp + .src(['./package.json']) + .pipe(bump({ type: semver })) + .pipe(gulp.dest('./')) + .pipe(git.commit('bump package version')) + .pipe(tag_version()); +} + +gulp.task('typings', function() { + return gulp.src('./typings.json').pipe(typings()); +}); + +gulp.task('typings-vscode-definitions', ['typings'], function() { + // add vscode definitions + return gulp.src('./typings/index.d.ts').pipe(gulp.dest('./typings')); +}); + +gulp.task('tslint', function() { + var tslintOptions = { + summarizeFailureOutput: true, + }; + + var srcs = gulp + .src(paths.src_ts) + .pipe(tslint({ formatter: 'verbose' })) + .pipe(tslint.report(tslintOptions)); + var tests = gulp + .src(paths.tests_ts) + .pipe(tslint({ formatter: 'verbose' })) + .pipe(tslint.report(tslintOptions)); + return merge(srcs, tests); +}); + +gulp.task('prettier', function() { + runPrettier('git diff --name-only HEAD'); +}); + +gulp.task('forceprettier', function() { + // Enumerate files managed by git + runPrettier('git ls-files'); + // Enumerate untracked files + runPrettier('git ls-files --others --exclude-standard'); +}); + +function runPrettier(command) { + exec(command, function(err, stdout, stderr) { + const files = stdout.split('\n'); + for (const file of files) { + if (file.endsWith('.ts') || file.endsWith('.js')) { + exec( + `node ./node_modules/prettier/bin/prettier.js --write --print-width 100 --single-quote --trailing-comma es5 ${file}` + ); + } + } + }); +} + +gulp.task('default', ['prettier', 'tslint', 'compile']); + +gulp.task('compile', shell.task(['npm run vscode:prepublish'])); +gulp.task('watch', shell.task(['npm run compile'])); +gulp.task('init', ['typings', 'typings-vscode-definitions']); + +gulp.task('patch', ['default'], function() { + return versionBump('patch'); +}); +gulp.task('minor', ['default'], function() { + return versionBump('minor'); +}); +gulp.task('major', ['default'], function() { + return versionBump('major'); +}); diff --git a/src/actions/commands/actions.ts b/src/actions/commands/actions.ts index 6154e13e197..c694c44cd5d 100644 --- a/src/actions/commands/actions.ts +++ b/src/actions/commands/actions.ts @@ -1,3533 +1,3533 @@ -import { RecordedState, VimState } from './../../mode/modeHandler'; -import { SearchState, SearchDirection } from './../../state/searchState'; -import { ReplaceState } from './../../state/replaceState'; -import { VisualBlockMode } from './../../mode/modeVisualBlock'; -import { ModeName } from './../../mode/mode'; -import { Range } from './../../common/motion/range'; -import { TextEditor, EditorScrollByUnit, EditorScrollDirection } from './../../textEditor'; -import { Register, RegisterMode } from './../../register/register'; -import { NumericString } from './../../common/number/numericString'; -import { Position, PositionDiff } from './../../common/motion/position'; -import { Tab, TabCommand } from './../../cmd_line/commands/tab'; -import { Configuration } from './../../configuration/configuration'; -import { allowVSCodeToPropagateCursorUpdatesAndReturnThem } from '../../util'; -import { isTextTransformation } from './../../transformations/transformations'; -import { FileCommand } from './../../cmd_line/commands/file'; -import { QuitCommand } from './../../cmd_line/commands/quit'; -import { OnlyCommand } from './../../cmd_line/commands/only'; -import * as vscode from 'vscode'; -import * as util from './../../util'; -import { RegisterAction } from './../base'; -import * as operator from './../operator'; -import { BaseAction } from './../base'; - -export class DocumentContentChangeAction extends BaseAction { - contentChanges: { - positionDiff: PositionDiff; - textDiff: vscode.TextDocumentContentChangeEvent; - }[] = []; - - public async exec(position: Position, vimState: VimState): Promise { - if (this.contentChanges.length === 0) { - return vimState; - } - - let originalLeftBoundary: vscode.Position; - - if ( - this.contentChanges[0].textDiff.text === '' && - this.contentChanges[0].textDiff.rangeLength === 1 - ) { - originalLeftBoundary = this.contentChanges[0].textDiff.range.end; - } else { - originalLeftBoundary = this.contentChanges[0].textDiff.range.start; - } - - let rightBoundary: vscode.Position = position; - let newStart: vscode.Position | undefined; - let newEnd: vscode.Position | undefined; - - for (let i = 0; i < this.contentChanges.length; i++) { - let contentChange = this.contentChanges[i].textDiff; - - if (contentChange.range.start.line < originalLeftBoundary.line) { - // This change should be ignored - let linesEffected = contentChange.range.end.line - contentChange.range.start.line + 1; - let resultLines = contentChange.text.split('\n').length; - originalLeftBoundary = originalLeftBoundary.with( - originalLeftBoundary.line + resultLines - linesEffected - ); - continue; - } - - if (contentChange.range.start.line === originalLeftBoundary.line) { - newStart = position.with( - position.line, - position.character + contentChange.range.start.character - originalLeftBoundary.character - ); - - if (contentChange.range.end.line === originalLeftBoundary.line) { - newEnd = position.with( - position.line, - position.character + contentChange.range.end.character - originalLeftBoundary.character - ); - } else { - newEnd = position.with( - position.line + contentChange.range.end.line - originalLeftBoundary.line, - contentChange.range.end.character - ); - } - } else { - newStart = position.with( - position.line + contentChange.range.start.line - originalLeftBoundary.line, - contentChange.range.start.character - ); - newEnd = position.with( - position.line + contentChange.range.end.line - originalLeftBoundary.line, - contentChange.range.end.character - ); - } - - if (newStart.isAfter(rightBoundary)) { - // This change should be ignored as it's out of boundary - continue; - } - - // Calculate new right boundary - let newLineCount = contentChange.text.split('\n').length; - let newRightBoundary: vscode.Position; - - if (newLineCount === 1) { - newRightBoundary = newStart.with( - newStart.line, - newStart.character + contentChange.text.length - ); - } else { - newRightBoundary = new vscode.Position( - newStart.line + newLineCount - 1, - contentChange.text.split('\n').pop()!.length - ); - } - - if (newRightBoundary.isAfter(rightBoundary)) { - rightBoundary = newRightBoundary; - } - - vimState.editor.selection = new vscode.Selection(newStart, newEnd); - - if (newStart.isEqual(newEnd)) { - await TextEditor.insert(contentChange.text, Position.FromVSCodePosition(newStart)); - } else { - await TextEditor.replace(vimState.editor.selection, contentChange.text); - } - } - - /** - * We're making an assumption here that content changes are always in order, and I'm not sure - * we're guaranteed that, but it seems to work well enough in practice. - */ - if (newStart && newEnd) { - const last = this.contentChanges[this.contentChanges.length - 1]; - - vimState.cursorStartPosition = Position.FromVSCodePosition(newStart) - .advancePositionByText(last.textDiff.text) - .add(last.positionDiff); - vimState.cursorPosition = Position.FromVSCodePosition(newEnd) - .advancePositionByText(last.textDiff.text) - .add(last.positionDiff); - } - - vimState.currentMode = ModeName.Insert; - return vimState; - } -} - -/** - * A command is something like , :, v, i, etc. - */ -export abstract class BaseCommand extends BaseAction { - /** - * If isCompleteAction is true, then triggering this command is a complete action - - * that means that we'll go and try to run it. - */ - isCompleteAction = true; - - multicursorIndex: number | undefined = undefined; - - /** - * 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. - */ - runsOnceForEachCountPrefix = false; - - canBeRepeatedWithDot = 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 { - let timesToRepeat = this.runsOnceForEachCountPrefix ? vimState.recordedState.count || 1 : 1; - - if (!this.runsOnceForEveryCursor()) { - for (let i = 0; i < timesToRepeat; i++) { - vimState = await this.exec(position, vimState); - } - - for (const transformation of vimState.recordedState.transformations) { - if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) { - transformation.cursorIndex = 0; - } - } - - return vimState; - } - - let resultingCursors: Range[] = []; - - const cursorsToIterateOver = vimState.allCursors - .map(x => new Range(x.start, x.stop)) - .sort( - (a, b) => - a.start.line > b.start.line || - (a.start.line === b.start.line && a.start.character > b.start.character) - ? 1 - : -1 - ); - - let cursorIndex = 0; - for (const { start, stop } of cursorsToIterateOver) { - this.multicursorIndex = cursorIndex++; - - vimState.cursorPosition = stop; - vimState.cursorStartPosition = start; - - for (let j = 0; j < timesToRepeat; j++) { - vimState = await this.exec(stop, vimState); - } - - resultingCursors.push(new Range(vimState.cursorStartPosition, vimState.cursorPosition)); - - for (const transformation of vimState.recordedState.transformations) { - if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) { - transformation.cursorIndex = this.multicursorIndex; - } - } - } - - vimState.allCursors = resultingCursors; - - return vimState; - } -} - -// begin actions - -@RegisterAction -export class CommandNumber extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['']; - isCompleteAction = false; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - const number = parseInt(this.keysPressed[0], 10); - - vimState.recordedState.count = vimState.recordedState.count * 10 + number; - - return vimState; - } - - public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { - const isZero = keysPressed[0] === '0'; - - return ( - super.doesActionApply(vimState, keysPressed) && - ((isZero && vimState.recordedState.count > 0) || !isZero) - ); - } - - public 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 = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['"', '']; - isCompleteAction = false; - - public async exec(position: Position, vimState: VimState): Promise { - const register = this.keysPressed[1]; - vimState.recordedState.registerName = register; - return vimState; - } - - public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { - const register = keysPressed[1]; - - return super.doesActionApply(vimState, keysPressed) && Register.isValidRegister(register); - } - - public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { - const register = keysPressed[1]; - - return super.couldActionApply(vimState, keysPressed) && Register.isValidRegister(register); - } -} - -@RegisterAction -class CommandInsertRegisterContentInSearchMode extends BaseCommand { - modes = [ModeName.SearchInProgressMode]; - keys = ['', '']; - isCompleteAction = false; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.recordedState.registerName = this.keysPressed[1]; - const register = await Register.get(vimState); - let text: string; - - if (register.text instanceof Array) { - text = (register.text as string[]).join('\n'); - } else if (register.text instanceof RecordedState) { - let keyStrokes: string[] = []; - - for (let 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'; - } - - const searchState = vimState.globalState.searchState!; - searchState.searchString += text; - return vimState; - } -} - -@RegisterAction -class CommandRecordMacro extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['q', '']; - - public async exec(position: Position, vimState: VimState): Promise { - const register = this.keysPressed[1]; - vimState.recordedMacro = new RecordedState(); - vimState.recordedMacro.registerName = register.toLocaleLowerCase(); - - if (!/^[A-Z]+$/.test(register) || !Register.has(register)) { - // If register name is upper case, it means we are appending commands to existing register instead of overriding. - let newRegister = new RecordedState(); - newRegister.registerName = register; - Register.putByKey(newRegister, register); - } - - vimState.isRecordingMacro = true; - return vimState; - } - - public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { - const register = this.keysPressed[1]; - - return ( - super.doesActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register) - ); - } - - public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { - const register = this.keysPressed[1]; - - return ( - super.couldActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register) - ); - } -} - -@RegisterAction -export class CommandQuitRecordMacro extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['q']; - - public async exec(position: Position, vimState: VimState): Promise { - let existingMacro = (await Register.getByKey(vimState.recordedMacro.registerName)) - .text as RecordedState; - existingMacro.actionsRun = existingMacro.actionsRun.concat(vimState.recordedMacro.actionsRun); - vimState.isRecordingMacro = false; - return vimState; - } - - public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { - return super.doesActionApply(vimState, keysPressed) && vimState.isRecordingMacro; - } - - public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { - return super.couldActionApply(vimState, keysPressed) && vimState.isRecordingMacro; - } -} - -@RegisterAction -class CommandExecuteMacro extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['@', '']; - runsOnceForEachCountPrefix = true; - canBeRepeatedWithDot = true; - - public async exec(position: Position, vimState: VimState): Promise { - const register = this.keysPressed[1]; - vimState.recordedState.transformations.push({ - type: 'macro', - register: register, - replay: 'contentChange', - }); - - return vimState; - } - - public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { - const register = keysPressed[1]; - - return ( - super.doesActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register) - ); - } - - public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { - const register = keysPressed[1]; - - return ( - super.couldActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register) - ); - } -} - -@RegisterAction -class CommandExecuteLastMacro extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['@', '@']; - runsOnceForEachCountPrefix = true; - canBeRepeatedWithDot = true; - - public async exec(position: Position, vimState: VimState): Promise { - let lastInvokedMacro = vimState.historyTracker.lastInvokedMacro; - - if (lastInvokedMacro) { - vimState.recordedState.transformations.push({ - type: 'macro', - register: lastInvokedMacro.registerName, - replay: 'contentChange', - }); - } - - return vimState; - } -} - -@RegisterAction -class CommandEsc extends BaseCommand { - modes = [ - ModeName.Visual, - ModeName.VisualLine, - ModeName.VisualBlock, - ModeName.Normal, - ModeName.SearchInProgressMode, - ModeName.SurroundInputMode, - ModeName.EasyMotionMode, - ModeName.EasyMotionInputMode, - ]; - keys = [[''], [''], ['']]; - - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - if (vimState.currentMode === ModeName.Normal && !vimState.isMultiCursor) { - // 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 vscode.commands.executeCommand('closeReferenceSearchEditor'); - return vimState; - } - - if ( - vimState.currentMode !== ModeName.Visual && - vimState.currentMode !== ModeName.VisualLine && - vimState.currentMode !== ModeName.EasyMotionMode - ) { - // Normally, you don't have to iterate over all cursors, - // as that is handled for you by the state machine. ESC is - // a special case since runsOnceForEveryCursor is false. - - vimState.allCursors = vimState.allCursors.map(x => x.withNewStop(x.stop.getLeft())); - } - - if (vimState.currentMode === ModeName.SearchInProgressMode) { - if (vimState.globalState.searchState) { - vimState.cursorPosition = vimState.globalState.searchState.searchCursorStartPosition; - } - } - - if (vimState.currentMode === ModeName.Normal && vimState.isMultiCursor) { - vimState.isMultiCursor = false; - } - - if (vimState.currentMode === ModeName.EasyMotionMode) { - // Escape or other termination keys were pressed, exit mode - vimState.easyMotion.clearDecorations(); - vimState.currentMode = ModeName.Normal; - } - - // Abort surround operation - if (vimState.currentMode === ModeName.SurroundInputMode) { - vimState.surround = undefined; - } - - vimState.currentMode = ModeName.Normal; - - if (!vimState.isMultiCursor) { - vimState.allCursors = [vimState.allCursors[0]]; - } - - return vimState; - } -} - -@RegisterAction -class CommandEscReplaceMode extends BaseCommand { - modes = [ModeName.Replace]; - keys = [[''], ['']]; - - public async exec(position: Position, vimState: VimState): Promise { - const timesToRepeat = vimState.replaceState!.timesToRepeat; - let textToAdd = ''; - - for (let i = 1; i < timesToRepeat; i++) { - textToAdd += vimState.replaceState!.newChars.join(''); - } - - vimState.recordedState.transformations.push({ - type: 'insertText', - text: textToAdd, - position: position, - diff: new PositionDiff(0, -1), - }); - - vimState.currentMode = ModeName.Normal; - - return vimState; - } -} - -abstract class CommandEditorScroll extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - runsOnceForEachCountPrefix = false; - keys: string[]; - to: EditorScrollDirection; - by: EditorScrollByUnit; - - public async exec(position: Position, vimState: VimState): Promise { - let timesToRepeat = vimState.recordedState.count || 1; - - vimState.postponedCodeViewChanges.push({ - command: 'editorScroll', - args: { - to: this.to, - by: this.by, - value: timesToRepeat, - revealCursor: true, - select: - [ModeName.Visual, ModeName.VisualBlock, ModeName.VisualLine].indexOf( - vimState.currentMode - ) >= 0, - }, - }); - return vimState; - } -} - -@RegisterAction -class CommandCtrlE extends CommandEditorScroll { - keys = ['']; - to: EditorScrollDirection = 'down'; - by: EditorScrollByUnit = 'line'; -} - -@RegisterAction -class CommandCtrlY extends CommandEditorScroll { - keys = ['']; - to: EditorScrollDirection = 'up'; - by: EditorScrollByUnit = 'line'; -} - -@RegisterAction -class CommandMoveFullPageUp extends CommandEditorScroll { - keys = ['']; - to: EditorScrollDirection = 'up'; - by: EditorScrollByUnit = 'page'; -} - -@RegisterAction -class CommandMoveFullPageDown extends CommandEditorScroll { - keys = ['']; - to: EditorScrollDirection = 'down'; - by: EditorScrollByUnit = 'page'; -} - -@RegisterAction -class CommandMoveHalfPageDown extends CommandEditorScroll { - keys = ['']; - to: EditorScrollDirection = 'down'; - by: EditorScrollByUnit = 'halfPage'; -} - -@RegisterAction -class CommandMoveHalfPageUp extends CommandEditorScroll { - keys = ['']; - to: EditorScrollDirection = 'up'; - by: EditorScrollByUnit = 'halfPage'; -} - -@RegisterAction -export class CommandInsertAtCursor extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['i']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Insert; - return vimState; - } - - public 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 -class CommandReplaceAtCursor extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['R']; - runsOnceForEachCountPrefix = false; - - public async exec(position: Position, vimState: VimState): Promise { - let timesToRepeat = vimState.recordedState.count || 1; - - vimState.currentMode = ModeName.Replace; - vimState.replaceState = new ReplaceState(position, timesToRepeat); - - return vimState; - } -} - -@RegisterAction -class CommandReplaceInReplaceMode extends BaseCommand { - modes = [ModeName.Replace]; - keys = ['']; - canBeRepeatedWithDot = true; - - public async exec(position: Position, vimState: VimState): Promise { - const char = this.keysPressed[0]; - const replaceState = vimState.replaceState!; - - if (char === '') { - if (position.isBeforeOrEqual(replaceState.replaceCursorStartPosition)) { - // If you backspace before the beginning of where you started to replace, - // just move the cursor back. - - vimState.cursorPosition = position.getLeft(); - vimState.cursorStartPosition = position.getLeft(); - } else if ( - position.line > replaceState.replaceCursorStartPosition.line || - position.character > replaceState.originalChars.length - ) { - vimState.recordedState.transformations.push({ - type: 'deleteText', - position: position, - }); - } else { - vimState.recordedState.transformations.push({ - type: 'replaceText', - text: replaceState.originalChars[position.character - 1], - start: position.getLeft(), - end: position, - diff: new PositionDiff(0, -1), - }); - } - - replaceState.newChars.pop(); - } else { - if (!position.isLineEnd() && char !== '\n') { - vimState.recordedState.transformations.push({ - type: 'replaceText', - text: char, - start: position, - end: position.getRight(), - diff: new PositionDiff(0, 1), - }); - } else { - vimState.recordedState.transformations.push({ - type: 'insertText', - text: char, - position: position, - }); - } - - replaceState.newChars.push(char); - } - - vimState.currentMode = ModeName.Replace; - return vimState; - } -} - -@RegisterAction -class CommandInsertInSearchMode extends BaseCommand { - modes = [ModeName.SearchInProgressMode]; - keys = [[''], [''], ['']]; - runsOnceForEveryCursor() { - return this.keysPressed[0] === '\n'; - } - - public async exec(position: Position, vimState: VimState): Promise { - const key = this.keysPressed[0]; - const searchState = vimState.globalState.searchState!; - const prevSearchList = vimState.globalState.searchStatePrevious!; - - // handle special keys first - if (key === '' || key === '') { - searchState.searchString = searchState.searchString.slice(0, -1); - } else if (key === '\n') { - vimState.currentMode = vimState.globalState.searchState!.previousMode; - - // Repeat the previous search if no new string is entered - if (searchState.searchString === '') { - if (prevSearchList.length > 0) { - searchState.searchString = prevSearchList[prevSearchList.length - 1].searchString; - } - } - - // Store this search if different than previous - if (vimState.globalState.searchStatePrevious.length !== 0) { - let previousSearchState = vimState.globalState.searchStatePrevious; - if ( - searchState.searchString !== - previousSearchState[previousSearchState.length - 1]!.searchString - ) { - previousSearchState.push(searchState); - } - } else { - vimState.globalState.searchStatePrevious.push(searchState); - } - - // Make sure search history does not exceed configuration option - if (vimState.globalState.searchStatePrevious.length > Configuration.history) { - vimState.globalState.searchStatePrevious.splice(0, 1); - } - - // Update the index to the end of the search history - vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length - 1; - - // Move cursor to next match - vimState.cursorPosition = searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; - - return vimState; - } else if (key === '') { - vimState.globalState.searchStateIndex -= 1; - - // Clamp the history index to stay within bounds of search history length - vimState.globalState.searchStateIndex = Math.max(vimState.globalState.searchStateIndex, 0); - - if (prevSearchList[vimState.globalState.searchStateIndex] !== undefined) { - searchState.searchString = - prevSearchList[vimState.globalState.searchStateIndex].searchString; - } - } else if (key === '') { - vimState.globalState.searchStateIndex += 1; - - // If past the first history item, allow user to enter their own search string (not using history) - if ( - vimState.globalState.searchStateIndex > - vimState.globalState.searchStatePrevious.length - 1 - ) { - searchState.searchString = ''; - vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length; - return vimState; - } - - if (prevSearchList[vimState.globalState.searchStateIndex] !== undefined) { - searchState.searchString = - prevSearchList[vimState.globalState.searchStateIndex].searchString; - } - } else { - searchState.searchString += this.keysPressed[0]; - } - - return vimState; - } -} - -@RegisterAction -class CommandEscInSearchMode extends BaseCommand { - modes = [ModeName.SearchInProgressMode]; - keys = ['']; - runsOnceForEveryCursor() { - return this.keysPressed[0] === '\n'; - } - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Normal; - vimState.globalState.searchState = undefined; - - return vimState; - } -} - -@RegisterAction -class CommandCtrlVInSearchMode extends BaseCommand { - modes = [ModeName.SearchInProgressMode]; - keys = ['']; - runsOnceForEveryCursor() { - return this.keysPressed[0] === '\n'; - } - - public async exec(position: Position, vimState: VimState): Promise { - const searchState = vimState.globalState.searchState!; - const textFromClipboard = util.clipboardPaste(); - - searchState.searchString += textFromClipboard; - return vimState; - } -} - -@RegisterAction -class CommandCmdVInSearchMode extends BaseCommand { - modes = [ModeName.SearchInProgressMode]; - keys = ['']; - runsOnceForEveryCursor() { - return this.keysPressed[0] === '\n'; - } - - public async exec(position: Position, vimState: VimState): Promise { - const searchState = vimState.globalState.searchState!; - const textFromClipboard = util.clipboardPaste(); - - searchState.searchString += textFromClipboard; - return vimState; - } -} - -/** - * 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 = [ - ModeName.Visual, - ModeName.VisualLine, - ModeName.VisualBlock, - ModeName.Insert, - ModeName.Normal, - ]; - keys = ['']; // A special key - see ModeHandler - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - let text = ''; - - if (vimState.currentMode === ModeName.Visual || vimState.currentMode === ModeName.Normal) { - text = vimState.allCursors - .map(range => { - const start = Position.EarlierOf(range.start, range.stop); - const stop = Position.LaterOf(range.start, range.stop); - return vimState.editor.document.getText(new vscode.Range(start, stop.getRight())); - }) - .join('\n'); - } else if (vimState.currentMode === ModeName.VisualLine) { - text = vimState.allCursors - .map(range => { - return vimState.editor.document.getText( - new vscode.Range( - Position.EarlierOf(range.start.getLineBegin(), range.stop.getLineBegin()), - Position.LaterOf(range.start.getLineEnd(), range.stop.getLineEnd()) - ) - ); - }) - .join('\n'); - } else if (vimState.currentMode === ModeName.VisualBlock) { - for (const { line } of Position.IterateLine(vimState)) { - text += line + '\n'; - } - } else if (vimState.currentMode === ModeName.Insert) { - text = vimState.editor.selections - .map(selection => { - return vimState.editor.document.getText(new vscode.Range(selection.start, selection.end)); - }) - .join('\n'); - } - - util.clipboardCopy(text); - // all vim yank operations return to normal mode. - vimState.currentMode = ModeName.Normal; - - return vimState; - } -} - -@RegisterAction -class CommandCmdA extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.cursorStartPosition = new Position(0, vimState.desiredColumn); - vimState.cursorPosition = new Position(TextEditor.getLineCount() - 1, vimState.desiredColumn); - vimState.currentMode = ModeName.VisualLine; - - return vimState; - } -} - -function searchCurrentWord( - position: Position, - vimState: VimState, - direction: SearchDirection, - isExact: boolean -) { - const currentWord = TextEditor.getWord(position); - - // 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.cursorPosition.getWordLeft(true) - : vimState.cursorPosition; - - return createSearchStateAndMoveToMatch({ - needle: currentWord, - vimState, - direction, - isExact, - searchStartCursorPosition, - }); -} - -function searchCurrentSelection(vimState: VimState, direction: SearchDirection) { - const selection = TextEditor.getSelection(); - const end = new Position(selection.end.line, selection.end.character); - const currentSelection = TextEditor.getText(selection.with(selection.start, end)); - - // Go back to Normal mode, otherwise the selection grows to the next match. - vimState.currentMode = ModeName.Normal; - - // If the search is going left then use `getLeft()` on the selection start. - // If going right then use `getRight()` on the selection end. This ensures - // that any matches happen outside of the currently selected word. - const searchStartCursorPosition = - direction === SearchDirection.Backward - ? vimState.lastVisualSelectionStart.getLeft() - : vimState.lastVisualSelectionEnd.getRight(); - - return createSearchStateAndMoveToMatch({ - needle: currentSelection, - vimState, - direction, - isExact: false, - searchStartCursorPosition, - }); -} - -function createSearchStateAndMoveToMatch(args: { - needle?: string | undefined; - vimState: VimState; - direction: SearchDirection; - isExact: boolean; - searchStartCursorPosition: Position; -}) { - const { needle, vimState, isExact } = args; - - if (needle === undefined || needle.length === 0) { - return vimState; - } - - const searchString = isExact ? `\\b${needle}\\b` : needle; - - // Start a search for the given term. - vimState.globalState.searchState = new SearchState( - args.direction, - vimState.cursorPosition, - searchString, - { isRegex: isExact }, - vimState.currentMode - ); - - vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition( - args.searchStartCursorPosition - ).pos; - - // Turn one of the highlighting flags back on (turned off with :nohl) - vimState.globalState.hl = true; - - return vimState; -} - -@RegisterAction -class CommandSearchCurrentWordExactForward extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['*']; - isMotion = true; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - return searchCurrentWord(position, vimState, SearchDirection.Forward, true); - } -} - -@RegisterAction -class CommandSearchCurrentWordForward extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['g', '*']; - isMotion = true; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - return searchCurrentWord(position, vimState, SearchDirection.Forward, false); - } -} - -@RegisterAction -class CommandSearchVisualForward extends BaseCommand { - modes = [ModeName.Visual, ModeName.VisualLine]; - keys = ['*']; - isMotion = true; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - return searchCurrentSelection(vimState, SearchDirection.Forward); - } -} - -@RegisterAction -class CommandSearchCurrentWordExactBackward extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['#']; - isMotion = true; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - return searchCurrentWord(position, vimState, SearchDirection.Backward, true); - } -} - -@RegisterAction -class CommandSearchCurrentWordBackward extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['g', '#']; - isMotion = true; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - return searchCurrentWord(position, vimState, SearchDirection.Backward, false); - } -} - -@RegisterAction -class CommandSearchVisualBackward extends BaseCommand { - modes = [ModeName.Visual, ModeName.VisualLine]; - keys = ['#']; - isMotion = true; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - return searchCurrentSelection(vimState, SearchDirection.Backward); - } -} - -@RegisterAction -export class CommandSearchForwards extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['/']; - isMotion = true; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.globalState.searchState = new SearchState( - SearchDirection.Forward, - vimState.cursorPosition, - '', - { isRegex: true }, - vimState.currentMode - ); - vimState.currentMode = ModeName.SearchInProgressMode; - - // Reset search history index - vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length; - - vimState.globalState.hl = true; - - return vimState; - } -} - -@RegisterAction -export class CommandSearchBackwards extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['?']; - isMotion = true; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.globalState.searchState = new SearchState( - SearchDirection.Backward, - vimState.cursorPosition, - '', - { isRegex: true }, - vimState.currentMode - ); - vimState.currentMode = ModeName.SearchInProgressMode; - - // Reset search history index - vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length; - - vimState.globalState.hl = true; - - return vimState; - } -} - -@RegisterAction -export class MarkCommand extends BaseCommand { - keys = ['m', '']; - modes = [ModeName.Normal]; - - public async exec(position: Position, vimState: VimState): Promise { - const markName = this.keysPressed[1]; - - vimState.historyTracker.addMark(position, markName); - - return vimState; - } -} - -@RegisterAction -export class PutCommand extends BaseCommand { - keys = ['p']; - modes = [ModeName.Normal]; - runsOnceForEachCountPrefix = true; - canBeRepeatedWithDot = true; - - constructor(multicursorIndex?: number) { - super(); - this.multicursorIndex = multicursorIndex; - } - public static async GetText( - vimState: VimState, - multicursorIndex: number | undefined = undefined - ): Promise { - const register = await Register.get(vimState); - - if (vimState.isMultiCursor) { - if (multicursorIndex === undefined) { - console.log('ERROR: no multi cursor index when calling PutCommand#getText'); - - throw new Error('Bad!'); - } - - if (vimState.isMultiCursor && typeof register.text === 'object') { - return register.text[multicursorIndex]; - } - } - - return register.text as string; - } - - public async exec( - position: Position, - vimState: VimState, - after: boolean = false, - adjustIndent: boolean = false - ): Promise { - const register = await Register.get(vimState); - const dest = after ? position : position.getRight(); - - if (register.text instanceof RecordedState) { - /** - * Paste content from recordedState. This one is actually complex as - * Vim has internal key code for key strokes.For example, Backspace - * is stored as `<80>kb`. So if you replay a macro, which is stored - * in a register as `a1<80>kb2`, youshall just get `2` inserted as - * `a` represents entering Insert Mode, `<80>bk` represents - * Backspace. However here, we shall - * insert the plain text content of the register, which is `a1<80>kb2`. - */ - vimState.recordedState.transformations.push({ - type: 'macro', - register: vimState.recordedState.registerName, - replay: 'keystrokes', - }); - return vimState; - } else if (typeof register.text === 'object' && vimState.currentMode === ModeName.VisualBlock) { - return await this.execVisualBlockPaste(register.text, position, vimState, after); - } - - let text = await PutCommand.GetText(vimState, this.multicursorIndex); - - let textToAdd: string; - let whereToAddText: Position; - let diff = new PositionDiff(0, 0); - - if (register.registerMode === RegisterMode.CharacterWise) { - textToAdd = text; - whereToAddText = dest; - } else if ( - vimState.currentMode === ModeName.Visual && - register.registerMode === RegisterMode.LineWise - ) { - // in the specific case of linewise register data during visual mode, - // we need extra newline feeds - textToAdd = '\n' + text + '\n'; - whereToAddText = dest; - } else { - if (adjustIndent) { - // Adjust indent to current line - let indentationWidth = TextEditor.getIndentationLevel(TextEditor.getLineAt(position).text); - let firstLineIdentationWidth = TextEditor.getIndentationLevel(text.split('\n')[0]); - - text = text - .split('\n') - .map(line => { - let currentIdentationWidth = TextEditor.getIndentationLevel(line); - let newIndentationWidth = - currentIdentationWidth - firstLineIdentationWidth + indentationWidth; - - return TextEditor.setIndentationLevel(line, newIndentationWidth); - }) - .join('\n'); - } - - if (after) { - // P insert before current line - textToAdd = text + '\n'; - whereToAddText = dest.getLineBegin(); - } else { - // p paste after current line - textToAdd = '\n' + text; - whereToAddText = dest.getLineEnd(); - } - } - - // More vim weirdness: If the thing you're pasting has a newline, the cursor - // stays in the same place. Otherwise, it moves to the end of what you pasted. - - const numNewlines = text.split('\n').length - 1; - const currentLineLength = TextEditor.getLineAt(position).text.length; - - if (register.registerMode === RegisterMode.LineWise) { - const check = text.match(/^\s*/); - let numWhitespace = 0; - - if (check) { - numWhitespace = check[0].length; - } - - if (after) { - diff = PositionDiff.NewBOLDiff(-numNewlines - 1, numWhitespace); - } else { - diff = PositionDiff.NewBOLDiff(currentLineLength > 0 ? 1 : -numNewlines, numWhitespace); - } - } else { - if (text.indexOf('\n') === -1) { - if (!position.isLineEnd()) { - if (after) { - diff = new PositionDiff(0, -1); - } else { - diff = new PositionDiff(0, textToAdd.length); - } - } - } else { - if (position.isLineEnd()) { - diff = PositionDiff.NewBOLDiff(-numNewlines, position.character); - } else { - if (after) { - diff = PositionDiff.NewBOLDiff(-numNewlines, position.character); - } else { - diff = new PositionDiff(0, 1); - } - } - } - } - - vimState.recordedState.transformations.push({ - type: 'insertText', - text: textToAdd, - position: whereToAddText, - diff: diff, - }); - - vimState.currentRegisterMode = register.registerMode; - return vimState; - } - - private async execVisualBlockPaste( - block: string[], - position: Position, - vimState: VimState, - after: boolean - ): Promise { - if (after) { - position = position.getRight(); - } - - // Add empty lines at the end of the document, if necessary. - let linesToAdd = Math.max(0, block.length - (TextEditor.getLineCount() - position.line) + 1); - - if (linesToAdd > 0) { - await TextEditor.insertAt( - Array(linesToAdd).join('\n'), - new Position( - TextEditor.getLineCount() - 1, - TextEditor.getLineAt(new Position(TextEditor.getLineCount() - 1, 0)).text.length - ) - ); - } - - // paste the entire block. - for (let lineIndex = position.line; lineIndex < position.line + block.length; lineIndex++) { - const line = block[lineIndex - position.line]; - const insertPos = new Position( - lineIndex, - Math.min(position.character, TextEditor.getLineAt(new Position(lineIndex, 0)).text.length) - ); - - await TextEditor.insertAt(line, insertPos); - } - - vimState.currentRegisterMode = RegisterMode.FigureItOutFromCurrentMode; - return vimState; - } - - public async execCount(position: Position, vimState: VimState): Promise { - const result = await super.execCount(position, vimState); - - if ( - vimState.effectiveRegisterMode() === RegisterMode.LineWise && - vimState.recordedState.count > 0 - ) { - const numNewlines = - (await PutCommand.GetText(vimState, this.multicursorIndex)).split('\n').length * - vimState.recordedState.count; - - result.recordedState.transformations.push({ - type: 'moveCursor', - diff: new PositionDiff(-numNewlines + 1, 0), - cursorIndex: this.multicursorIndex, - }); - } - - return result; - } -} - -@RegisterAction -export class GPutCommand extends BaseCommand { - keys = ['g', 'p']; - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - runsOnceForEachCountPrefix = true; - canBeRepeatedWithDot = true; - - public async exec(position: Position, vimState: VimState): Promise { - const result = await new PutCommand().exec(position, vimState); - - return result; - } - - public async execCount(position: Position, vimState: VimState): Promise { - const register = await Register.get(vimState); - let addedLinesCount: number; - - if (register.text instanceof RecordedState) { - vimState.recordedState.transformations.push({ - type: 'macro', - register: vimState.recordedState.registerName, - replay: 'keystrokes', - }); - - return vimState; - } - if (typeof register.text === 'object') { - // visual block mode - addedLinesCount = register.text.length * vimState.recordedState.count; - } else { - addedLinesCount = register.text.split('\n').length; - } - - const result = await super.execCount(position, vimState); - - if (vimState.effectiveRegisterMode() === RegisterMode.LineWise) { - const line = TextEditor.getLineAt(position).text; - const addAnotherLine = line.length > 0 && addedLinesCount > 1; - - result.recordedState.transformations.push({ - type: 'moveCursor', - diff: PositionDiff.NewBOLDiff(1 + (addAnotherLine ? 1 : 0), 0), - cursorIndex: this.multicursorIndex, - }); - } - - return result; - } -} - -@RegisterAction -export class PutWithIndentCommand extends BaseCommand { - keys = [']', 'p']; - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - runsOnceForEachCountPrefix = true; - canBeRepeatedWithDot = true; - - public async exec(position: Position, vimState: VimState): Promise { - const result = await new PutCommand().exec(position, vimState, false, true); - return result; - } - - public async execCount(position: Position, vimState: VimState): Promise { - return await super.execCount(position, vimState); - } -} - -@RegisterAction -export class PutCommandVisual extends BaseCommand { - keys = [['p'], ['P']]; - modes = [ModeName.Visual, ModeName.VisualLine]; - runsOnceForEachCountPrefix = true; - - public async exec( - position: Position, - vimState: VimState, - after: boolean = false - ): Promise { - let start = vimState.cursorStartPosition; - let end = vimState.cursorPosition; - const isLineWise = vimState.currentMode === ModeName.VisualLine; - if (start.isAfter(end)) { - [start, end] = [end, start]; - } - - // If the to be inserted text is linewise we have a seperate logic delete the - // selection first than insert - let register = await Register.get(vimState); - if (register.registerMode === RegisterMode.LineWise) { - let deleteResult = await new operator.DeleteOperator(this.multicursorIndex).run( - vimState, - start, - end, - false - ); - // to ensure, that the put command nows this is - // an linewise register insertion in visual mode - let oldMode = deleteResult.currentMode; - deleteResult.currentMode = ModeName.Visual; - deleteResult = await new PutCommand().exec(start, deleteResult, true); - deleteResult.currentMode = oldMode; - return deleteResult; - } - - // The reason we need to handle Delete and Yank separately is because of - // linewise mode. If we're in visualLine mode, then we want to copy - // linewise but not necessarily delete linewise. - let putResult = await new PutCommand(this.multicursorIndex).exec(start, vimState, true); - putResult.currentRegisterMode = isLineWise ? RegisterMode.LineWise : RegisterMode.CharacterWise; - putResult.recordedState.registerName = Configuration.useSystemClipboard ? '*' : '"'; - putResult = await new operator.YankOperator(this.multicursorIndex).run(putResult, start, end); - putResult.currentRegisterMode = RegisterMode.CharacterWise; - putResult = await new operator.DeleteOperator(this.multicursorIndex).run( - putResult, - start, - end.getLeftIfEOL(), - false - ); - putResult.currentRegisterMode = RegisterMode.FigureItOutFromCurrentMode; - return putResult; - } - - // TODO - execWithCount -} - -@RegisterAction -export class PutBeforeCommand extends BaseCommand { - public keys = ['P']; - public modes = [ModeName.Normal]; - canBeRepeatedWithDot = true; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - const command = new PutCommand(); - command.multicursorIndex = this.multicursorIndex; - - const result = await command.exec(position, vimState, true); - - return result; - } -} - -@RegisterAction -export class GPutBeforeCommand extends BaseCommand { - keys = ['g', 'P']; - modes = [ModeName.Normal]; - - public async exec(position: Position, vimState: VimState): Promise { - const result = await new PutCommand().exec(position, vimState, true); - const register = await Register.get(vimState); - let addedLinesCount: number; - - if (register.text instanceof RecordedState) { - vimState.recordedState.transformations.push({ - type: 'macro', - register: vimState.recordedState.registerName, - replay: 'keystrokes', - }); - - return vimState; - } else if (typeof register.text === 'object') { - // visual block mode - addedLinesCount = register.text.length * vimState.recordedState.count; - } else { - addedLinesCount = register.text.split('\n').length; - } - - if (vimState.effectiveRegisterMode() === RegisterMode.LineWise) { - const line = TextEditor.getLineAt(position).text; - const addAnotherLine = line.length > 0 && addedLinesCount > 1; - - result.recordedState.transformations.push({ - type: 'moveCursor', - diff: PositionDiff.NewBOLDiff(1 + (addAnotherLine ? 1 : 0), 0), - cursorIndex: this.multicursorIndex, - }); - } - - return result; - } -} - -@RegisterAction -export class PutBeforeWithIndentCommand extends BaseCommand { - keys = ['[', 'p']; - modes = [ModeName.Normal]; - - public async exec(position: Position, vimState: VimState): Promise { - const result = await new PutCommand().exec(position, vimState, true, true); - - if (vimState.effectiveRegisterMode() === RegisterMode.LineWise) { - result.cursorPosition = result.cursorPosition - .getPreviousLineBegin() - .getFirstLineNonBlankChar(); - } - - return result; - } -} - -@RegisterAction -class CommandShowCommandLine extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = [':']; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - vimState.recordedState.transformations.push({ - type: 'showCommandLine', - }); - - if (vimState.currentMode === ModeName.Normal) { - vimState.commandInitialText = ''; - } else { - vimState.commandInitialText = "'<,'>"; - } - vimState.currentMode = ModeName.Normal; - - return vimState; - } -} - -@RegisterAction -class CommandDot extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['.']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.recordedState.transformations.push({ - type: 'dot', - }); - - return vimState; - } -} - -abstract class CommandFold extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - commandName: string; - - public async exec(position: Position, vimState: VimState): Promise { - await vscode.commands.executeCommand(this.commandName); - vimState.currentMode = ModeName.Normal; - return vimState; - } -} - -@RegisterAction -class CommandCloseFold extends CommandFold { - keys = ['z', 'c']; - commandName = 'editor.fold'; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - let timesToRepeat = vimState.recordedState.count || 1; - await vscode.commands.executeCommand('editor.fold', { levels: timesToRepeat, direction: 'up' }); - vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); - return vimState; - } -} - -@RegisterAction -class CommandCloseAllFolds extends CommandFold { - keys = ['z', 'M']; - commandName = 'editor.foldAll'; -} - -@RegisterAction -class CommandOpenFold extends CommandFold { - keys = ['z', 'o']; - commandName = 'editor.unfold'; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - let timesToRepeat = vimState.recordedState.count || 1; - await vscode.commands.executeCommand('editor.unfold', { - levels: timesToRepeat, - direction: 'down', - }); - - return vimState; - } -} - -@RegisterAction -class CommandOpenAllFolds extends CommandFold { - keys = ['z', 'R']; - commandName = 'editor.unfoldAll'; -} - -@RegisterAction -class CommandCloseAllFoldsRecursively extends CommandFold { - modes = [ModeName.Normal]; - keys = ['z', 'C']; - commandName = 'editor.foldRecursively'; -} - -@RegisterAction -class CommandOpenAllFoldsRecursively extends CommandFold { - modes = [ModeName.Normal]; - keys = ['z', 'O']; - commandName = 'editor.unfoldRecursively'; -} - -@RegisterAction -class CommandCenterScroll extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['z', 'z']; - - public 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.cursorPosition, vimState.cursorPosition), - vscode.TextEditorRevealType.InCenter - ); - - return vimState; - } -} - -@RegisterAction -class CommandCenterScrollFirstChar extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['z', '.']; - - public 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.cursorPosition, vimState.cursorPosition), - vscode.TextEditorRevealType.InCenter - ); - - // Move cursor to first char of line - vimState.cursorPosition = vimState.cursorPosition.getFirstLineNonBlankChar(); - - return vimState; - } -} - -@RegisterAction -class CommandTopScroll extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['z', 't']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.postponedCodeViewChanges.push({ - command: 'revealLine', - args: { - lineNumber: position.line, - at: 'top', - }, - }); - return vimState; - } -} - -@RegisterAction -class CommandTopScrollFirstChar extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['z', '\n']; - - public 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.cursorPosition = vimState.cursorPosition.getFirstLineNonBlankChar(); - - return vimState; - } -} - -@RegisterAction -class CommandBottomScroll extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['z', 'b']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.postponedCodeViewChanges.push({ - command: 'revealLine', - args: { - lineNumber: position.line, - at: 'bottom', - }, - }); - return vimState; - } -} - -@RegisterAction -class CommandBottomScrollFirstChar extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['z', '-']; - - public 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.cursorPosition = vimState.cursorPosition.getFirstLineNonBlankChar(); - - return vimState; - } -} - -@RegisterAction -class CommandGoToOtherEndOfHighlightedText extends BaseCommand { - modes = [ModeName.Visual, ModeName.VisualLine]; - keys = ['o']; - - public async exec(position: Position, vimState: VimState): Promise { - [vimState.cursorStartPosition, vimState.cursorPosition] = [ - vimState.cursorPosition, - vimState.cursorStartPosition, - ]; - - return vimState; - } -} - -@RegisterAction -class CommandUndo extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['u']; - runsOnceForEveryCursor() { - return false; - } - mustBeFirstKey = true; - - public async exec(position: Position, vimState: VimState): Promise { - const newPositions = await vimState.historyTracker.goBackHistoryStep(); - - if (newPositions !== undefined) { - vimState.allCursors = newPositions.map(x => new Range(x, x)); - } - - vimState.alteredHistory = true; - return vimState; - } -} - -@RegisterAction -class CommandRedo extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['']; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - const newPositions = await vimState.historyTracker.goForwardHistoryStep(); - - if (newPositions !== undefined) { - vimState.allCursors = newPositions.map(x => new Range(x, x)); - } - - vimState.alteredHistory = true; - return vimState; - } -} - -@RegisterAction -class CommandDeleteToLineEnd extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['D']; - canBeRepeatedWithDot = true; - runsOnceForEveryCursor() { - return true; - } - - public async exec(position: Position, vimState: VimState): Promise { - if (position.isLineEnd()) { - return vimState; - } - - return await new operator.DeleteOperator(this.multicursorIndex).run( - vimState, - position, - position.getLineEnd().getLeft() - ); - } -} - -@RegisterAction -export class CommandYankFullLine extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['Y']; - - public async exec(position: Position, vimState: VimState): Promise { - const linesDown = (vimState.recordedState.count || 1) - 1; - const start = position.getLineBegin(); - const end = new Position(position.line + linesDown, 0).getLineEnd().getLeft(); - - vimState.currentRegisterMode = RegisterMode.LineWise; - - return await new operator.YankOperator().run(vimState, start, end); - } -} - -@RegisterAction -class CommandChangeToLineEnd extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['C']; - runsOnceForEachCountPrefix = false; - mustBeFirstKey = true; - - public async exec(position: Position, vimState: VimState): Promise { - let count = vimState.recordedState.count || 1; - - return new operator.ChangeOperator().run( - vimState, - position, - position - .getDownByCount(Math.max(0, count - 1)) - .getLineEnd() - .getLeft() - ); - } -} - -@RegisterAction -class CommandClearLine extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['S']; - runsOnceForEachCountPrefix = false; - - public async exec(position: Position, vimState: VimState): Promise { - let count = vimState.recordedState.count || 1; - let end = position - .getDownByCount(Math.max(0, count - 1)) - .getLineEnd() - .getLeft(); - return new operator.ChangeOperator().run( - vimState, - position.getLineBeginRespectingIndent(), - end - ); - } -} - -@RegisterAction -class CommandExitVisualMode extends BaseCommand { - modes = [ModeName.Visual, ModeName.VisualLine]; - keys = ['v']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Normal; - - return vimState; - } -} - -@RegisterAction -class CommandVisualMode extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['v']; - isCompleteAction = false; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Visual; - - return vimState; - } -} - -@RegisterAction -class CommandReselectVisual extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['g', 'v']; - - public async exec(position: Position, vimState: VimState): Promise { - // Try to restore selection only if valid - if ( - vimState.lastVisualSelectionEnd !== undefined && - vimState.lastVisualSelectionStart !== undefined && - vimState.lastVisualMode !== undefined - ) { - if (vimState.lastVisualSelectionEnd.line <= TextEditor.getLineCount() - 1) { - vimState.currentMode = vimState.lastVisualMode; - vimState.cursorStartPosition = vimState.lastVisualSelectionStart; - vimState.cursorPosition = vimState.lastVisualSelectionEnd.getLeft(); - } - } - return vimState; - } -} - -@RegisterAction -class CommandVisualBlockMode extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualBlock]; - keys = ['']; - - public async exec(position: Position, vimState: VimState): Promise { - if (vimState.currentMode === ModeName.VisualBlock) { - vimState.currentMode = ModeName.Normal; - } else { - vimState.currentMode = ModeName.VisualBlock; - } - - return vimState; - } -} - -@RegisterAction -class CommandVisualLineMode extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual]; - keys = ['V']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.VisualLine; - - return vimState; - } -} - -@RegisterAction -class CommandExitVisualLineMode extends BaseCommand { - modes = [ModeName.VisualLine]; - keys = ['V']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Normal; - - return vimState; - } -} - -@RegisterAction -class CommandOpenFile extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual]; - keys = ['g', 'f']; - - public async exec(position: Position, vimState: VimState): Promise { - let fullFilePath: string = ''; - - if (vimState.currentMode === ModeName.Visual) { - const selection = TextEditor.getSelection(); - const end = new Position(selection.end.line, selection.end.character + 1); - fullFilePath = TextEditor.getText(selection.with(selection.start, end)); - } else { - const start = position.getFilePathLeft(true); - const end = position.getFilePathRight(); - const range = new vscode.Range(start, end); - - fullFilePath = TextEditor.getText(range).trim(); - } - const fileInfo = fullFilePath.match(/(.*?(?=:[0-9]+)|.*):?([0-9]*)$/); - if (fileInfo) { - const filePath = fileInfo[1]; - const lineNumber = parseInt(fileInfo[2], 10); - const fileCommand = new FileCommand({ name: filePath, lineNumber: lineNumber }); - fileCommand.execute(); - } - - return vimState; - } -} - -@RegisterAction -class CommandGoToDefinition extends BaseCommand { - modes = [ModeName.Normal]; - keys = [['g', 'd'], ['']]; - - public async exec(position: Position, vimState: VimState): Promise { - const oldActiveEditor = vimState.editor; - - await vscode.commands.executeCommand('editor.action.goToDeclaration'); - - if (oldActiveEditor === vimState.editor) { - vimState.cursorPosition = Position.FromVSCodePosition(vimState.editor.selection.start); - } - - return vimState; - } -} - -@RegisterAction -class CommandGoBackInChangelist extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['g', ';']; - - public async exec(position: Position, vimState: VimState): Promise { - const originalIndex = vimState.historyTracker.changelistIndex; - const prevPos = vimState.historyTracker.getChangePositionAtindex(originalIndex - 1); - const currPos = vimState.historyTracker.getChangePositionAtindex(originalIndex); - - if (prevPos !== undefined) { - vimState.cursorPosition = prevPos[0]; - vimState.historyTracker.changelistIndex = originalIndex - 1; - } else if (currPos !== undefined) { - vimState.cursorPosition = currPos[0]; - } - - return vimState; - } -} - -@RegisterAction -class CommandGoForwardInChangelist extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['g', ',']; - - public async exec(position: Position, vimState: VimState): Promise { - const originalIndex = vimState.historyTracker.changelistIndex; - const nextPos = vimState.historyTracker.getChangePositionAtindex(originalIndex + 1); - const currPos = vimState.historyTracker.getChangePositionAtindex(originalIndex); - - if (nextPos !== undefined) { - vimState.cursorPosition = nextPos[0]; - vimState.historyTracker.changelistIndex = originalIndex + 1; - } else if (currPos !== undefined) { - vimState.cursorPosition = currPos[0]; - } - - return vimState; - } -} - -@RegisterAction -class CommandGoLastChange extends BaseCommand { - modes = [ModeName.Normal]; - keys = [['`', '.'], ["'", '.']]; - - public async exec(position: Position, vimState: VimState): Promise { - const lastPos = vimState.historyTracker.getLastHistoryStartPosition(); - - if (lastPos !== undefined) { - vimState.cursorPosition = lastPos[0]; - } - - return vimState; - } -} - -@RegisterAction -class CommandInsertAtLastChange extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['g', 'i']; - - public async exec(position: Position, vimState: VimState): Promise { - const lastPos = vimState.historyTracker.getLastChangeEndPosition(); - - if (lastPos !== undefined) { - vimState.cursorPosition = lastPos; - vimState.currentMode = ModeName.Insert; - } - - return vimState; - } -} - -@RegisterAction -export class CommandInsertAtFirstCharacter extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual]; - keys = ['I']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Insert; - vimState.cursorPosition = position.getFirstLineNonBlankChar(); - - return vimState; - } -} - -@RegisterAction -class CommandInsertAtLineBegin extends BaseCommand { - modes = [ModeName.Normal]; - mustBeFirstKey = true; - keys = ['g', 'I']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Insert; - vimState.cursorPosition = position.getLineBegin(); - - return vimState; - } -} - -@RegisterAction -export class CommandInsertAfterCursor extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['a']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Insert; - vimState.cursorPosition = position.getRight(); - - return vimState; - } - - public 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 CommandInsertAtLineEnd extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual]; - keys = ['A']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Insert; - vimState.cursorPosition = position.getLineEnd(); - - return vimState; - } -} - -@RegisterAction -class CommandInsertNewLineAbove extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['O']; - runsOnceForEveryCursor() { - return false; - } - - public async execCount(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Insert; - let count = vimState.recordedState.count || 1; - // Why do we do this? Who fucking knows??? If the cursor is at the - // beginning of the line, then editor.action.insertLineBefore does some - // weird things with following paste command. Refer to - // https://github.com/VSCodeVim/Vim/pull/1663#issuecomment-300573129 for - // more details. - const tPos = Position.FromVSCodePosition( - vscode.window.activeTextEditor!.selection.active - ).getRight(); - vscode.window.activeTextEditor!.selection = new vscode.Selection(tPos, tPos); - for (let i = 0; i < count; i++) { - await vscode.commands.executeCommand('editor.action.insertLineBefore'); - } - - vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); - for (let i = 0; i < count; i++) { - const newPos = new Position( - vimState.allCursors[0].start.line + i, - vimState.allCursors[0].start.character - ); - vimState.allCursors.push(new Range(newPos, newPos)); - } - vimState.allCursors = vimState.allCursors.reverse(); - vimState.isFakeMultiCursor = true; - vimState.isMultiCursor = true; - return vimState; - } -} - -@RegisterAction -class CommandInsertNewLineBefore extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['o']; - runsOnceForEveryCursor() { - return false; - } - - public async execCount(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Insert; - let count = vimState.recordedState.count || 1; - - for (let i = 0; i < count; i++) { - await vscode.commands.executeCommand('editor.action.insertLineAfter'); - } - vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); - for (let i = 1; i < count; i++) { - const newPos = new Position( - vimState.allCursors[0].start.line - i, - vimState.allCursors[0].start.character - ); - vimState.allCursors.push(new Range(newPos, newPos)); - - // Ahhhhhh. We have to manually set cursor position here as we need text - // transformations AND to set multiple cursors. - vimState.recordedState.transformations.push({ - type: 'insertText', - text: TextEditor.setIndentationLevel('', newPos.character), - position: newPos, - cursorIndex: i, - manuallySetCursorPositions: true, - }); - } - vimState.allCursors = vimState.allCursors.reverse(); - vimState.isFakeMultiCursor = true; - vimState.isMultiCursor = true; - return vimState; - } -} - -@RegisterAction -class CommandNavigateBack extends BaseCommand { - modes = [ModeName.Normal]; - keys = [[''], ['']]; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - const oldActiveEditor = vimState.editor; - - await vscode.commands.executeCommand('workbench.action.navigateBack'); - - if (oldActiveEditor === vimState.editor) { - vimState.cursorPosition = Position.FromVSCodePosition(vimState.editor.selection.start); - } - - return vimState; - } -} - -@RegisterAction -class CommandNavigateForward extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['']; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - const oldActiveEditor = vimState.editor; - - await vscode.commands.executeCommand('workbench.action.navigateForward'); - - if (oldActiveEditor === vimState.editor) { - vimState.cursorPosition = Position.FromVSCodePosition(vimState.editor.selection.start); - } - - return vimState; - } -} - -@RegisterAction -class CommandNavigateLast extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['`', '`']; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - const oldActiveEditor = vimState.editor; - - await vscode.commands.executeCommand('workbench.action.navigateLast'); - - if (oldActiveEditor === vimState.editor) { - vimState.cursorPosition = Position.FromVSCodePosition(vimState.editor.selection.start); - } - - return vimState; - } -} - -@RegisterAction -class CommandNavigateLastBOL extends BaseCommand { - modes = [ModeName.Normal]; - keys = ["'", "'"]; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - const oldActiveEditor = vimState.editor; - - await vscode.commands.executeCommand('workbench.action.navigateLast'); - - if (oldActiveEditor === vimState.editor) { - const pos = Position.FromVSCodePosition(vimState.editor.selection.start); - vimState.cursorPosition = pos.getFirstLineNonBlankChar(); - } - - return vimState; - } -} - -@RegisterAction -class CommandQuit extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['', 'q']; - - public async exec(position: Position, vimState: VimState): Promise { - new QuitCommand({}).execute(); - - return vimState; - } -} - -@RegisterAction -class CommandOnly extends BaseCommand { - modes = [ModeName.Normal]; - keys = [['', 'o'], ['', 'C-o']]; - - public async exec(position: Position, vimState: VimState): Promise { - new OnlyCommand({}).execute(); - - return vimState; - } -} - -@RegisterAction -class MoveToRightPane extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = [['', 'l'], ['', ''], ['']]; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.postponedCodeViewChanges.push({ - command: 'workbench.action.navigateRight', - args: {}, - }); - - return vimState; - } -} - -@RegisterAction -class MoveToLowerPane extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = [['', 'j'], ['', ''], ['']]; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.postponedCodeViewChanges.push({ - command: 'workbench.action.navigateDown', - args: {}, - }); - - return vimState; - } -} - -@RegisterAction -class MoveToUpperPane extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = [['', 'k'], ['', ''], ['']]; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.postponedCodeViewChanges.push({ - command: 'workbench.action.navigateUp', - args: {}, - }); - - return vimState; - } -} - -@RegisterAction -class MoveToLeftPane extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = [['', 'h'], ['', ''], ['']]; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.postponedCodeViewChanges.push({ - command: 'workbench.action.navigateLeft', - args: {}, - }); - - return vimState; - } -} - -@RegisterAction -class CycleThroughPanes extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = [['', ''], ['', 'w']]; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.postponedCodeViewChanges.push({ - command: 'workbench.action.navigateEditorGroups', - args: {}, - }); - - return vimState; - } -} - -class BaseTabCommand extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - runsOnceForEachCountPrefix = true; -} - -@RegisterAction -class VerticalSplit extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['', 'v']; - - public async exec(position: Position, vimState: VimState): Promise { - vimState.postponedCodeViewChanges.push({ - command: 'workbench.action.splitEditor', - args: {}, - }); - - return vimState; - } -} - -@RegisterAction -class CommandTabNext extends BaseTabCommand { - keys = [['g', 't'], ['']]; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - new TabCommand({ - tab: Tab.Next, - count: vimState.recordedState.count, - }).execute(); - - return vimState; - } -} - -@RegisterAction -class CommandTabPrevious extends BaseTabCommand { - keys = [['g', 'T'], ['']]; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - new TabCommand({ - tab: Tab.Previous, - count: 1, - }).execute(); - - return vimState; - } -} - -@RegisterAction -class ActionDeleteChar extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['x']; - runsOnceForEachCountPrefix = true; - canBeRepeatedWithDot = true; - - public async exec(position: Position, vimState: VimState): Promise { - // If line is empty, do nothing - if (TextEditor.getLineAt(position).text.length < 1) { - return vimState; - } - - const state = await new operator.DeleteOperator(this.multicursorIndex).run( - vimState, - position, - position - ); - - state.currentMode = ModeName.Normal; - - return state; - } -} - -@RegisterAction -class ActionDeleteCharWithDeleteKey extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['']; - runsOnceForEachCountPrefix = true; - canBeRepeatedWithDot = true; - - public 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); - vimState.recordedState.actionKeys = vimState.recordedState.count.toString().split(''); - vimState.recordedState.commandList = vimState.recordedState.count.toString().split(''); - this.isCompleteAction = false; - return vimState; - } - return await new ActionDeleteChar().execCount(position, vimState); - } -} - -@RegisterAction -class ActionDeleteLastChar extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['X']; - canBeRepeatedWithDot = true; - - public async exec(position: Position, vimState: VimState): Promise { - if (position.character === 0) { - return vimState; - } - - return await new operator.DeleteOperator(this.multicursorIndex).run( - vimState, - position.getLeft(), - position.getLeft() - ); - } -} - -@RegisterAction -class ActionJoin extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['J']; - canBeRepeatedWithDot = true; - runsOnceForEachCountPrefix = false; - - private firstNonWhitespaceIndex(str: string): number { - for (let i = 0, len = str.length; i < len; i++) { - let chCode = str.charCodeAt(i); - if (chCode !== 32 /** space */ && chCode !== 9 /** tab */) { - return i; - } - } - return -1; - } - - public async execJoinLines( - startPosition: Position, - position: Position, - vimState: VimState, - count: number - ): Promise { - count = count - 1 || 1; - - let startLineNumber: number, - startColumn: number, - endLineNumber: number, - endColumn: number, - columnDeltaOffset: number = 0; - - if (startPosition.isEqual(position) || startPosition.line === position.line) { - if (position.line + 1 < TextEditor.getLineCount()) { - startLineNumber = position.line; - startColumn = 0; - endLineNumber = startLineNumber + count; - endColumn = TextEditor.getLineMaxColumn(endLineNumber); - } else { - startLineNumber = position.line; - startColumn = 0; - endLineNumber = position.line; - endColumn = TextEditor.getLineMaxColumn(endLineNumber); - } - } else { - startLineNumber = startPosition.line; - startColumn = 0; - endLineNumber = position.line; - endColumn = TextEditor.getLineMaxColumn(endLineNumber); - } - - let trimmedLinesContent = TextEditor.getLineAt(startPosition).text; - - for (let i = startLineNumber + 1; i <= endLineNumber; i++) { - let lineText = TextEditor.getLineAt(new Position(i, 0)).text; - - let firstNonWhitespaceIdx = this.firstNonWhitespaceIndex(lineText); - - if (firstNonWhitespaceIdx >= 0) { - let insertSpace = true; - - if ( - trimmedLinesContent === '' || - trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === ' ' || - trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === '\t' - ) { - insertSpace = false; - } - - let lineTextWithoutIndent = lineText.substr(firstNonWhitespaceIdx); - - if (lineTextWithoutIndent.charAt(0) === ')') { - insertSpace = false; - } - - trimmedLinesContent += (insertSpace ? ' ' : '') + lineTextWithoutIndent; - - if (insertSpace) { - columnDeltaOffset = lineTextWithoutIndent.length + 1; - } else { - columnDeltaOffset = lineTextWithoutIndent.length; - } - } else { - if ( - trimmedLinesContent === '' || - trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === ' ' || - trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === '\t' - ) { - columnDeltaOffset += 0; - } else { - trimmedLinesContent += ' '; - columnDeltaOffset += 1; - } - } - } - - let deleteStartPosition = new Position(startLineNumber, startColumn); - let deleteEndPosition = new Position(endLineNumber, endColumn); - - if (!deleteStartPosition.isEqual(deleteEndPosition)) { - if (startPosition.isEqual(position)) { - vimState.recordedState.transformations.push({ - type: 'replaceText', - text: trimmedLinesContent, - start: deleteStartPosition, - end: deleteEndPosition, - diff: new PositionDiff( - 0, - trimmedLinesContent.length - columnDeltaOffset - position.character - ), - }); - } else { - vimState.recordedState.transformations.push({ - type: 'replaceText', - text: trimmedLinesContent, - start: deleteStartPosition, - end: deleteEndPosition, - manuallySetCursorPositions: true, - }); - - vimState.cursorPosition = new Position( - startPosition.line, - trimmedLinesContent.length - columnDeltaOffset - ); - vimState.cursorStartPosition = vimState.cursorPosition; - vimState.currentMode = ModeName.Normal; - } - } - - return vimState; - } - - public async execCount(position: Position, vimState: VimState): Promise { - let timesToRepeat = vimState.recordedState.count || 1; - let resultingCursors: Range[] = []; - let i = 0; - - const cursorsToIterateOver = vimState.allCursors - .map(x => new Range(x.start, x.stop)) - .sort( - (a, b) => - a.start.line > b.start.line || - (a.start.line === b.start.line && a.start.character > b.start.character) - ? 1 - : -1 - ); - - for (const { start, stop } of cursorsToIterateOver) { - this.multicursorIndex = i++; - - vimState.cursorPosition = stop; - vimState.cursorStartPosition = start; - - vimState = await this.execJoinLines(start, stop, vimState, timesToRepeat); - - resultingCursors.push(new Range(vimState.cursorStartPosition, vimState.cursorPosition)); - - for (const transformation of vimState.recordedState.transformations) { - if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) { - transformation.cursorIndex = this.multicursorIndex; - } - } - } - - vimState.allCursors = resultingCursors; - - return vimState; - } -} - -@RegisterAction -class ActionJoinVisualMode extends BaseCommand { - modes = [ModeName.Visual, ModeName.VisualLine]; - keys = ['J']; - - public async exec(position: Position, vimState: VimState): Promise { - let actionJoin = new ActionJoin(); - let start = Position.FromVSCodePosition(vimState.editor.selection.start); - let end = Position.FromVSCodePosition(vimState.editor.selection.end); - - if (start.isAfter(end)) { - [start, end] = [end, start]; - } - - /** - * For joining lines, Visual Line behaves the same as Visual so we align the register mode here. - */ - vimState.currentRegisterMode = RegisterMode.CharacterWise; - vimState = await actionJoin.execJoinLines(start, end, vimState, 1); - - return vimState; - } -} - -@RegisterAction -class ActionJoinNoWhitespace extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['g', 'J']; - canBeRepeatedWithDot = true; - runsOnceForEachCountPrefix = true; - - // gJ is essentially J without the edge cases. ;-) - - public async exec(position: Position, vimState: VimState): Promise { - if (position.line === TextEditor.getLineCount() - 1) { - return vimState; // TODO: bell - } - - let lineOne = TextEditor.getLineAt(position).text; - let lineTwo = TextEditor.getLineAt(position.getNextLineBegin()).text; - - lineTwo = lineTwo.substring(position.getNextLineBegin().getFirstLineNonBlankChar().character); - - let resultLine = lineOne + lineTwo; - - let newState = await new operator.DeleteOperator(this.multicursorIndex).run( - vimState, - position.getLineBegin(), - lineTwo.length > 0 - ? position - .getNextLineBegin() - .getLineEnd() - .getLeft() - : position.getLineEnd() - ); - - vimState.recordedState.transformations.push({ - type: 'insertText', - text: resultLine, - position: position, - diff: new PositionDiff(0, -lineTwo.length), - }); - - newState.cursorPosition = new Position(position.line, lineOne.length); - - return newState; - } -} - -@RegisterAction -class ActionJoinNoWhitespaceVisualMode extends BaseCommand { - modes = [ModeName.Visual]; - keys = ['g', 'J']; - - public async exec(position: Position, vimState: VimState): Promise { - let actionJoin = new ActionJoinNoWhitespace(); - let start = Position.FromVSCodePosition(vimState.editor.selection.start); - let end = Position.FromVSCodePosition(vimState.editor.selection.end); - - if (start.line === end.line) { - return vimState; - } - - if (start.isAfter(end)) { - [start, end] = [end, start]; - } - - for (let i = start.line; i < end.line; i++) { - vimState = await actionJoin.exec(start, vimState); - } - - return vimState; - } -} - -@RegisterAction -class ActionReplaceCharacter extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['r', '']; - canBeRepeatedWithDot = true; - runsOnceForEachCountPrefix = false; - - public async exec(position: Position, vimState: VimState): Promise { - let 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 (['', ''].indexOf(toReplace.toUpperCase()) >= 0) { - return vimState; - } - - if (position.character + timesToRepeat > position.getLineEnd().character) { - return vimState; - } - - let endPos = new Position(position.line, position.character + timesToRepeat); - - // Return if tried to repeat longer than linelength - if (endPos.character > TextEditor.getLineAt(endPos).text.length) { - return vimState; - } - - // If last char (not EOL char), add 1 so that replace selection is complete - if (endPos.character > TextEditor.getLineAt(endPos).text.length) { - endPos = new Position(endPos.line, endPos.character + 1); - } - - if (toReplace === '') { - vimState.recordedState.transformations.push({ - type: 'deleteRange', - range: new Range(position, endPos), - }); - vimState.recordedState.transformations.push({ - type: 'tab', - cursorIndex: this.multicursorIndex, - diff: new PositionDiff(0, -1), - }); - } else { - vimState.recordedState.transformations.push({ - type: 'replaceText', - text: toReplace.repeat(timesToRepeat), - start: position, - end: endPos, - diff: new PositionDiff(0, timesToRepeat - 1), - }); - } - return vimState; - } - - public async execCount(position: Position, vimState: VimState): Promise { - return super.execCount(position, vimState); - } -} - -@RegisterAction -class ActionReplaceCharacterVisual extends BaseCommand { - modes = [ModeName.Visual, ModeName.VisualLine]; - keys = ['r', '']; - runsOnceForEveryCursor() { - return false; - } - canBeRepeatedWithDot = true; - - public async exec(position: Position, vimState: VimState): Promise { - const toInsert = this.keysPressed[1]; - - let visualSelectionOffset = 1; - let start = vimState.cursorStartPosition; - let end = vimState.cursorPosition; - - // If selection is reversed, reorganize it so that the text replace logic always works - if (end.isBeforeOrEqual(start)) { - [start, end] = [end, start]; - } - - // Limit to not replace EOL - const textLength = TextEditor.getLineAt(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 (var lineNum = start.line; lineNum <= end.line; lineNum++) { - // Get line of text - const lineText = TextEditor.getLineAt(new Position(lineNum, 0)).text; - - if (start.line === end.line) { - // This is a visual section all on one line, only replace the part within the selection - vimState.recordedState.transformations.push({ - type: 'replaceText', - text: Array(end.character - start.character + 2).join(toInsert), - start: start, - end: 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.transformations.push({ - type: 'replaceText', - text: Array(lineText.length - start.character + 1).join(toInsert), - start: start, - end: 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.transformations.push({ - type: 'replaceText', - text: Array(end.character + 1 + visualSelectionOffset).join(toInsert), - start: new Position(end.line, 0), - end: 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.transformations.push({ - type: 'replaceText', - text: Array(lineText.length + 1).join(toInsert), - start: new Position(lineNum, 0), - end: new Position(lineNum, lineText.length), - manuallySetCursorPositions: true, - }); - } - } - - vimState.cursorPosition = start; - vimState.cursorStartPosition = start; - vimState.currentMode = ModeName.Normal; - return vimState; - } -} - -@RegisterAction -class ActionReplaceCharacterVisualBlock extends BaseCommand { - modes = [ModeName.VisualBlock]; - keys = ['r', '']; - runsOnceForEveryCursor() { - return false; - } - canBeRepeatedWithDot = true; - - public async exec(position: Position, vimState: VimState): Promise { - const toInsert = this.keysPressed[1]; - for (const { start, end } of Position.IterateLine(vimState)) { - if (end.isBeforeOrEqual(start)) { - continue; - } - - vimState.recordedState.transformations.push({ - type: 'replaceText', - text: Array(end.character - start.character + 1).join(toInsert), - start: start, - end: end, - manuallySetCursorPositions: true, - }); - } - - const topLeft = VisualBlockMode.getTopLeftPosition( - vimState.cursorPosition, - vimState.cursorStartPosition - ); - vimState.allCursors = [new Range(topLeft, topLeft)]; - vimState.currentMode = ModeName.Normal; - - return vimState; - } -} - -@RegisterAction -class ActionXVisualBlock extends BaseCommand { - modes = [ModeName.VisualBlock]; - keys = ['x']; - canBeRepeatedWithDot = true; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - for (const { start, end } of Position.IterateLine(vimState)) { - vimState.recordedState.transformations.push({ - type: 'deleteRange', - range: new Range(start, end), - manuallySetCursorPositions: true, - }); - } - - const topLeft = VisualBlockMode.getTopLeftPosition( - vimState.cursorPosition, - vimState.cursorStartPosition - ); - - vimState.allCursors = [new Range(topLeft, topLeft)]; - vimState.currentMode = ModeName.Normal; - - return vimState; - } -} - -@RegisterAction -class ActionDVisualBlock extends ActionXVisualBlock { - modes = [ModeName.VisualBlock]; - keys = ['d']; - canBeRepeatedWithDot = true; - runsOnceForEveryCursor() { - return false; - } -} - -@RegisterAction -class ActionShiftDVisualBlock extends BaseCommand { - modes = [ModeName.VisualBlock]; - keys = ['D']; - canBeRepeatedWithDot = true; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - for (const { start } of Position.IterateLine(vimState)) { - vimState.recordedState.transformations.push({ - type: 'deleteRange', - range: new Range(start, start.getLineEnd()), - manuallySetCursorPositions: true, - }); - } - - const topLeft = VisualBlockMode.getTopLeftPosition( - vimState.cursorPosition, - vimState.cursorStartPosition - ); - - vimState.allCursors = [new Range(topLeft, topLeft)]; - vimState.currentMode = ModeName.Normal; - - return vimState; - } -} - -@RegisterAction -class ActionGoToInsertVisualBlockMode extends BaseCommand { - modes = [ModeName.VisualBlock]; - keys = ['I']; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Insert; - vimState.isMultiCursor = true; - vimState.isFakeMultiCursor = true; - - for (const { line, start } of Position.IterateLine(vimState)) { - if (line === '' && start.character !== 0) { - continue; - } - vimState.allCursors.push(new Range(start, start)); - } - vimState.allCursors = vimState.allCursors.slice(1); - return vimState; - } -} - -@RegisterAction -class ActionChangeInVisualBlockMode extends BaseCommand { - modes = [ModeName.VisualBlock]; - keys = [['c'], ['s']]; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - for (const { start, end } of Position.IterateLine(vimState)) { - vimState.recordedState.transformations.push({ - type: 'deleteRange', - range: new Range(start, end), - manuallySetCursorPositions: true, - }); - } - - vimState.currentMode = ModeName.Insert; - vimState.isMultiCursor = true; - vimState.isFakeMultiCursor = true; - - for (const { start } of Position.IterateLine(vimState)) { - vimState.allCursors.push(new Range(start, start)); - } - vimState.allCursors = vimState.allCursors.slice(1); - - return vimState; - } -} - -@RegisterAction -class ActionChangeToEOLInVisualBlockMode extends BaseCommand { - modes = [ModeName.VisualBlock]; - keys = [['C'], ['S']]; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - for (const { start } of Position.IterateLine(vimState)) { - vimState.recordedState.transformations.push({ - type: 'deleteRange', - range: new Range(start, start.getLineEnd()), - collapseRange: true, - }); - } - - vimState.currentMode = ModeName.Insert; - vimState.isMultiCursor = true; - vimState.isFakeMultiCursor = true; - - for (const { end } of Position.IterateLine(vimState)) { - vimState.allCursors.push(new Range(end, end)); - } - vimState.allCursors = vimState.allCursors.slice(1); - - return vimState; - } -} - -@RegisterAction -class ActionGoToInsertVisualBlockModeAppend extends BaseCommand { - modes = [ModeName.VisualBlock]; - keys = ['A']; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - vimState.currentMode = ModeName.Insert; - vimState.isMultiCursor = true; - vimState.isFakeMultiCursor = true; - - for (const { line, end } of Position.IterateLine(vimState)) { - if (line.trim() === '') { - vimState.recordedState.transformations.push({ - type: 'replaceText', - text: TextEditor.setIndentationLevel(line, end.character), - start: new Position(end.line, 0), - end: new Position(end.line, end.character), - }); - } - vimState.allCursors.push(new Range(end, end)); - } - vimState.allCursors = vimState.allCursors.slice(1); - return vimState; - } -} - -@RegisterAction -class ActionDeleteLineVisualMode extends BaseCommand { - modes = [ModeName.Visual, ModeName.VisualLine]; - keys = ['X']; - - public async exec(position: Position, vimState: VimState): Promise { - if (vimState.currentMode === ModeName.Visual) { - return await new operator.DeleteOperator(this.multicursorIndex).run( - vimState, - vimState.cursorStartPosition.getLineBegin(), - vimState.cursorPosition.getLineEnd() - ); - } else { - return await new operator.DeleteOperator(this.multicursorIndex).run( - vimState, - position.getLineBegin(), - position.getLineEnd() - ); - } - } -} - -@RegisterAction -class ActionChangeLineVisualMode extends BaseCommand { - modes = [ModeName.Visual]; - keys = ['C']; - - public async exec(position: Position, vimState: VimState): Promise { - return await new operator.DeleteOperator(this.multicursorIndex).run( - vimState, - vimState.cursorStartPosition.getLineBegin(), - vimState.cursorPosition.getLineEndIncludingEOL() - ); - } -} - -@RegisterAction -class ActionRemoveLineVisualMode extends BaseCommand { - modes = [ModeName.Visual]; - keys = ['R']; - - public async exec(position: Position, vimState: VimState): Promise { - return await new operator.DeleteOperator(this.multicursorIndex).run( - vimState, - vimState.cursorStartPosition.getLineBegin(), - vimState.cursorPosition.getLineEndIncludingEOL() - ); - } -} - -@RegisterAction -class ActionChangeChar extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['s']; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - const state = await new operator.ChangeOperator().run(vimState, position, position); - - state.currentMode = ModeName.Insert; - - return state; - } - - // Don't clash with surround mode! - public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { - return super.doesActionApply(vimState, keysPressed) && !vimState.recordedState.operator; - } - - public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { - return super.doesActionApply(vimState, keysPressed) && !vimState.recordedState.operator; - } -} - -@RegisterAction -class ToggleCaseAndMoveForward extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['~']; - canBeRepeatedWithDot = true; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - await new operator.ToggleCaseOperator().run( - vimState, - vimState.cursorPosition, - vimState.cursorPosition - ); - - vimState.cursorPosition = vimState.cursorPosition.getRight(); - return vimState; - } -} - -abstract class IncrementDecrementNumberAction extends BaseCommand { - modes = [ModeName.Normal]; - canBeRepeatedWithDot = true; - offset: number; - - public async exec(position: Position, vimState: VimState): Promise { - const text = TextEditor.getLineAt(position).text; - - // 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.getWordLeft(true); - - for (let { start, end, word } of Position.IterateWords(whereToStart)) { - // '-' doesn't count as a word, but is important to include in parsing - // the number - if (text[start.character - 1] === '-') { - start = start.getLeft(); - word = text[start.character] + word; - } - // Strict number parsing so "1a" doesn't silently get converted to "1" - do { - const num = NumericString.parse(word); - if ( - num !== null && - position.character < start.character + num.prefix.length + num.value.toString().length - ) { - vimState.cursorPosition = await this.replaceNum( - num, - this.offset * (vimState.recordedState.count || 1), - start, - end - ); - vimState.cursorPosition = vimState.cursorPosition.getLeftByCount(num.suffix.length); - return vimState; - } else if (num !== null) { - word = word.slice(num.prefix.length + num.value.toString().length); - start = new Position( - start.line, - start.character + num.prefix.length + num.value.toString().length - ); - } else { - break; - } - } while (true); - } - // No usable numbers, return the original position - return vimState; - } - - public async replaceNum( - start: NumericString, - offset: number, - startPos: Position, - endPos: Position - ): Promise { - const oldWidth = start.toString().length; - start.value += offset; - const newNum = start.toString(); - - const range = new vscode.Range(startPos, endPos.getRight()); - - if (oldWidth === newNum.length) { - await TextEditor.replace(range, newNum); - } else { - // Can't use replace, since new number is a different width than old - await TextEditor.delete(range); - await TextEditor.insertAt(newNum, startPos); - // Adjust end position according to difference in width of number-string - endPos = new Position(endPos.line, endPos.character + (newNum.length - oldWidth)); - } - - return endPos; - } -} - -@RegisterAction -class IncrementNumberAction extends IncrementDecrementNumberAction { - keys = ['']; - offset = +1; -} - -@RegisterAction -class DecrementNumberAction extends IncrementDecrementNumberAction { - keys = ['']; - offset = -1; -} - -@RegisterAction -class ActionTriggerHover extends BaseCommand { - modes = [ModeName.Normal]; - keys = ['g', 'h']; - runsOnceForEveryCursor() { - return false; - } - - public async exec(position: Position, vimState: VimState): Promise { - await vscode.commands.executeCommand('editor.action.showHover'); - - return vimState; - } -} - -/** - * 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 -class ActionOverrideCmdD extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual]; - keys = [[''], ['g', 'b']]; - runsOnceForEveryCursor() { - return false; - } - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - await vscode.commands.executeCommand('editor.action.addSelectionToNextFindMatch'); - vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); - - // If this is the first cursor, select 1 character less - // so that only the word is selected, no extra character - vimState.allCursors = vimState.allCursors.map(x => x.withNewStop(x.stop.getLeft())); - - vimState.currentMode = ModeName.Visual; - - return vimState; - } -} - -@RegisterAction -class ActionOverrideCmdDInsert extends BaseCommand { - modes = [ModeName.Insert]; - keys = ['']; - runsOnceForEveryCursor() { - return false; - } - runsOnceForEachCountPrefix = true; - - public 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. - vscode.window.activeTextEditor!.selections = vscode.window - .activeTextEditor!.selections.map((x, idx) => { - const curPos = Position.FromVSCodePosition(x.active); - if (idx === 0) { - return new vscode.Selection( - curPos.getWordLeft(false), - curPos - .getLeft() - .getCurrentWordEnd(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 = Position.FromVSCodePosition( - vscode.window.activeTextEditor!.selections[0].active - ); - const matchWordLength = - matchWordPos - .getLeft() - .getCurrentWordEnd(true) - .getRight().character - matchWordPos.getWordLeft(false).character; - const wordBegin = curPos.getLeftByCount(matchWordLength); - return new vscode.Selection(wordBegin, curPos); - } - }); - await vscode.commands.executeCommand('editor.action.addSelectionToNextFindMatch'); - vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); - return vimState; - } -} - -@RegisterAction -class ActionOverrideCmdAltDown extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual]; - keys = [ - [''], // OSX - [''], // Windows - ]; - runsOnceForEveryCursor() { - return false; - } - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - await vscode.commands.executeCommand('editor.action.insertCursorBelow'); - vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); - - return vimState; - } -} - -@RegisterAction -class ActionOverrideCmdAltUp extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual]; - keys = [ - [''], // OSX - [''], // Windows - ]; - runsOnceForEveryCursor() { - return false; - } - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { - await vscode.commands.executeCommand('editor.action.insertCursorAbove'); - vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); - - return vimState; - } -} +import { RecordedState, VimState } from './../../mode/modeHandler'; +import { SearchState, SearchDirection } from './../../state/searchState'; +import { ReplaceState } from './../../state/replaceState'; +import { VisualBlockMode } from './../../mode/modeVisualBlock'; +import { ModeName } from './../../mode/mode'; +import { Range } from './../../common/motion/range'; +import { TextEditor, EditorScrollByUnit, EditorScrollDirection } from './../../textEditor'; +import { Register, RegisterMode } from './../../register/register'; +import { NumericString } from './../../common/number/numericString'; +import { Position, PositionDiff } from './../../common/motion/position'; +import { Tab, TabCommand } from './../../cmd_line/commands/tab'; +import { Configuration } from './../../configuration/configuration'; +import { allowVSCodeToPropagateCursorUpdatesAndReturnThem } from '../../util'; +import { isTextTransformation } from './../../transformations/transformations'; +import { FileCommand } from './../../cmd_line/commands/file'; +import { QuitCommand } from './../../cmd_line/commands/quit'; +import { OnlyCommand } from './../../cmd_line/commands/only'; +import * as vscode from 'vscode'; +import * as util from './../../util'; +import { RegisterAction } from './../base'; +import * as operator from './../operator'; +import { BaseAction } from './../base'; + +export class DocumentContentChangeAction extends BaseAction { + contentChanges: { + positionDiff: PositionDiff; + textDiff: vscode.TextDocumentContentChangeEvent; + }[] = []; + + public async exec(position: Position, vimState: VimState): Promise { + if (this.contentChanges.length === 0) { + return vimState; + } + + let originalLeftBoundary: vscode.Position; + + if ( + this.contentChanges[0].textDiff.text === '' && + this.contentChanges[0].textDiff.rangeLength === 1 + ) { + originalLeftBoundary = this.contentChanges[0].textDiff.range.end; + } else { + originalLeftBoundary = this.contentChanges[0].textDiff.range.start; + } + + let rightBoundary: vscode.Position = position; + let newStart: vscode.Position | undefined; + let newEnd: vscode.Position | undefined; + + for (let i = 0; i < this.contentChanges.length; i++) { + let contentChange = this.contentChanges[i].textDiff; + + if (contentChange.range.start.line < originalLeftBoundary.line) { + // This change should be ignored + let linesEffected = contentChange.range.end.line - contentChange.range.start.line + 1; + let resultLines = contentChange.text.split('\n').length; + originalLeftBoundary = originalLeftBoundary.with( + originalLeftBoundary.line + resultLines - linesEffected + ); + continue; + } + + if (contentChange.range.start.line === originalLeftBoundary.line) { + newStart = position.with( + position.line, + position.character + contentChange.range.start.character - originalLeftBoundary.character + ); + + if (contentChange.range.end.line === originalLeftBoundary.line) { + newEnd = position.with( + position.line, + position.character + contentChange.range.end.character - originalLeftBoundary.character + ); + } else { + newEnd = position.with( + position.line + contentChange.range.end.line - originalLeftBoundary.line, + contentChange.range.end.character + ); + } + } else { + newStart = position.with( + position.line + contentChange.range.start.line - originalLeftBoundary.line, + contentChange.range.start.character + ); + newEnd = position.with( + position.line + contentChange.range.end.line - originalLeftBoundary.line, + contentChange.range.end.character + ); + } + + if (newStart.isAfter(rightBoundary)) { + // This change should be ignored as it's out of boundary + continue; + } + + // Calculate new right boundary + let newLineCount = contentChange.text.split('\n').length; + let newRightBoundary: vscode.Position; + + if (newLineCount === 1) { + newRightBoundary = newStart.with( + newStart.line, + newStart.character + contentChange.text.length + ); + } else { + newRightBoundary = new vscode.Position( + newStart.line + newLineCount - 1, + contentChange.text.split('\n').pop()!.length + ); + } + + if (newRightBoundary.isAfter(rightBoundary)) { + rightBoundary = newRightBoundary; + } + + vimState.editor.selection = new vscode.Selection(newStart, newEnd); + + if (newStart.isEqual(newEnd)) { + await TextEditor.insert(contentChange.text, Position.FromVSCodePosition(newStart)); + } else { + await TextEditor.replace(vimState.editor.selection, contentChange.text); + } + } + + /** + * We're making an assumption here that content changes are always in order, and I'm not sure + * we're guaranteed that, but it seems to work well enough in practice. + */ + if (newStart && newEnd) { + const last = this.contentChanges[this.contentChanges.length - 1]; + + vimState.cursorStartPosition = Position.FromVSCodePosition(newStart) + .advancePositionByText(last.textDiff.text) + .add(last.positionDiff); + vimState.cursorPosition = Position.FromVSCodePosition(newEnd) + .advancePositionByText(last.textDiff.text) + .add(last.positionDiff); + } + + vimState.currentMode = ModeName.Insert; + return vimState; + } +} + +/** + * A command is something like , :, v, i, etc. + */ +export abstract class BaseCommand extends BaseAction { + /** + * If isCompleteAction is true, then triggering this command is a complete action - + * that means that we'll go and try to run it. + */ + isCompleteAction = true; + + multicursorIndex: number | undefined = undefined; + + /** + * 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. + */ + runsOnceForEachCountPrefix = false; + + canBeRepeatedWithDot = 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 { + let timesToRepeat = this.runsOnceForEachCountPrefix ? vimState.recordedState.count || 1 : 1; + + if (!this.runsOnceForEveryCursor()) { + for (let i = 0; i < timesToRepeat; i++) { + vimState = await this.exec(position, vimState); + } + + for (const transformation of vimState.recordedState.transformations) { + if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) { + transformation.cursorIndex = 0; + } + } + + return vimState; + } + + let resultingCursors: Range[] = []; + + const cursorsToIterateOver = vimState.allCursors + .map(x => new Range(x.start, x.stop)) + .sort( + (a, b) => + a.start.line > b.start.line || + (a.start.line === b.start.line && a.start.character > b.start.character) + ? 1 + : -1 + ); + + let cursorIndex = 0; + for (const { start, stop } of cursorsToIterateOver) { + this.multicursorIndex = cursorIndex++; + + vimState.cursorPosition = stop; + vimState.cursorStartPosition = start; + + for (let j = 0; j < timesToRepeat; j++) { + vimState = await this.exec(stop, vimState); + } + + resultingCursors.push(new Range(vimState.cursorStartPosition, vimState.cursorPosition)); + + for (const transformation of vimState.recordedState.transformations) { + if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) { + transformation.cursorIndex = this.multicursorIndex; + } + } + } + + vimState.allCursors = resultingCursors; + + return vimState; + } +} + +// begin actions + +@RegisterAction +export class CommandNumber extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['']; + isCompleteAction = false; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + const number = parseInt(this.keysPressed[0], 10); + + vimState.recordedState.count = vimState.recordedState.count * 10 + number; + + return vimState; + } + + public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { + const isZero = keysPressed[0] === '0'; + + return ( + super.doesActionApply(vimState, keysPressed) && + ((isZero && vimState.recordedState.count > 0) || !isZero) + ); + } + + public 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 = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['"', '']; + isCompleteAction = false; + + public async exec(position: Position, vimState: VimState): Promise { + const register = this.keysPressed[1]; + vimState.recordedState.registerName = register; + return vimState; + } + + public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { + const register = keysPressed[1]; + + return super.doesActionApply(vimState, keysPressed) && Register.isValidRegister(register); + } + + public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { + const register = keysPressed[1]; + + return super.couldActionApply(vimState, keysPressed) && Register.isValidRegister(register); + } +} + +@RegisterAction +class CommandInsertRegisterContentInSearchMode extends BaseCommand { + modes = [ModeName.SearchInProgressMode]; + keys = ['', '']; + isCompleteAction = false; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.recordedState.registerName = this.keysPressed[1]; + const register = await Register.get(vimState); + let text: string; + + if (register.text instanceof Array) { + text = (register.text as string[]).join('\n'); + } else if (register.text instanceof RecordedState) { + let keyStrokes: string[] = []; + + for (let 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'; + } + + const searchState = vimState.globalState.searchState!; + searchState.searchString += text; + return vimState; + } +} + +@RegisterAction +class CommandRecordMacro extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['q', '']; + + public async exec(position: Position, vimState: VimState): Promise { + const register = this.keysPressed[1]; + vimState.recordedMacro = new RecordedState(); + vimState.recordedMacro.registerName = register.toLocaleLowerCase(); + + if (!/^[A-Z]+$/.test(register) || !Register.has(register)) { + // If register name is upper case, it means we are appending commands to existing register instead of overriding. + let newRegister = new RecordedState(); + newRegister.registerName = register; + Register.putByKey(newRegister, register); + } + + vimState.isRecordingMacro = true; + return vimState; + } + + public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { + const register = this.keysPressed[1]; + + return ( + super.doesActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register) + ); + } + + public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { + const register = this.keysPressed[1]; + + return ( + super.couldActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register) + ); + } +} + +@RegisterAction +export class CommandQuitRecordMacro extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['q']; + + public async exec(position: Position, vimState: VimState): Promise { + let existingMacro = (await Register.getByKey(vimState.recordedMacro.registerName)) + .text as RecordedState; + existingMacro.actionsRun = existingMacro.actionsRun.concat(vimState.recordedMacro.actionsRun); + vimState.isRecordingMacro = false; + return vimState; + } + + public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { + return super.doesActionApply(vimState, keysPressed) && vimState.isRecordingMacro; + } + + public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { + return super.couldActionApply(vimState, keysPressed) && vimState.isRecordingMacro; + } +} + +@RegisterAction +class CommandExecuteMacro extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['@', '']; + runsOnceForEachCountPrefix = true; + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + const register = this.keysPressed[1]; + vimState.recordedState.transformations.push({ + type: 'macro', + register: register, + replay: 'contentChange', + }); + + return vimState; + } + + public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { + const register = keysPressed[1]; + + return ( + super.doesActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register) + ); + } + + public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { + const register = keysPressed[1]; + + return ( + super.couldActionApply(vimState, keysPressed) && Register.isValidRegisterForMacro(register) + ); + } +} + +@RegisterAction +class CommandExecuteLastMacro extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['@', '@']; + runsOnceForEachCountPrefix = true; + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + let lastInvokedMacro = vimState.historyTracker.lastInvokedMacro; + + if (lastInvokedMacro) { + vimState.recordedState.transformations.push({ + type: 'macro', + register: lastInvokedMacro.registerName, + replay: 'contentChange', + }); + } + + return vimState; + } +} + +@RegisterAction +class CommandEsc extends BaseCommand { + modes = [ + ModeName.Visual, + ModeName.VisualLine, + ModeName.VisualBlock, + ModeName.Normal, + ModeName.SearchInProgressMode, + ModeName.SurroundInputMode, + ModeName.EasyMotionMode, + ModeName.EasyMotionInputMode, + ]; + keys = [[''], [''], ['']]; + + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + if (vimState.currentMode === ModeName.Normal && !vimState.isMultiCursor) { + // 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 vscode.commands.executeCommand('closeReferenceSearchEditor'); + return vimState; + } + + if ( + vimState.currentMode !== ModeName.Visual && + vimState.currentMode !== ModeName.VisualLine && + vimState.currentMode !== ModeName.EasyMotionMode + ) { + // Normally, you don't have to iterate over all cursors, + // as that is handled for you by the state machine. ESC is + // a special case since runsOnceForEveryCursor is false. + + vimState.allCursors = vimState.allCursors.map(x => x.withNewStop(x.stop.getLeft())); + } + + if (vimState.currentMode === ModeName.SearchInProgressMode) { + if (vimState.globalState.searchState) { + vimState.cursorPosition = vimState.globalState.searchState.searchCursorStartPosition; + } + } + + if (vimState.currentMode === ModeName.Normal && vimState.isMultiCursor) { + vimState.isMultiCursor = false; + } + + if (vimState.currentMode === ModeName.EasyMotionMode) { + // Escape or other termination keys were pressed, exit mode + vimState.easyMotion.clearDecorations(); + vimState.currentMode = ModeName.Normal; + } + + // Abort surround operation + if (vimState.currentMode === ModeName.SurroundInputMode) { + vimState.surround = undefined; + } + + vimState.currentMode = ModeName.Normal; + + if (!vimState.isMultiCursor) { + vimState.allCursors = [vimState.allCursors[0]]; + } + + return vimState; + } +} + +@RegisterAction +class CommandEscReplaceMode extends BaseCommand { + modes = [ModeName.Replace]; + keys = [[''], ['']]; + + public async exec(position: Position, vimState: VimState): Promise { + const timesToRepeat = vimState.replaceState!.timesToRepeat; + let textToAdd = ''; + + for (let i = 1; i < timesToRepeat; i++) { + textToAdd += vimState.replaceState!.newChars.join(''); + } + + vimState.recordedState.transformations.push({ + type: 'insertText', + text: textToAdd, + position: position, + diff: new PositionDiff(0, -1), + }); + + vimState.currentMode = ModeName.Normal; + + return vimState; + } +} + +abstract class CommandEditorScroll extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + runsOnceForEachCountPrefix = false; + keys: string[]; + to: EditorScrollDirection; + by: EditorScrollByUnit; + + public async exec(position: Position, vimState: VimState): Promise { + let timesToRepeat = vimState.recordedState.count || 1; + + vimState.postponedCodeViewChanges.push({ + command: 'editorScroll', + args: { + to: this.to, + by: this.by, + value: timesToRepeat, + revealCursor: true, + select: + [ModeName.Visual, ModeName.VisualBlock, ModeName.VisualLine].indexOf( + vimState.currentMode + ) >= 0, + }, + }); + return vimState; + } +} + +@RegisterAction +class CommandCtrlE extends CommandEditorScroll { + keys = ['']; + to: EditorScrollDirection = 'down'; + by: EditorScrollByUnit = 'line'; +} + +@RegisterAction +class CommandCtrlY extends CommandEditorScroll { + keys = ['']; + to: EditorScrollDirection = 'up'; + by: EditorScrollByUnit = 'line'; +} + +@RegisterAction +class CommandMoveFullPageUp extends CommandEditorScroll { + keys = ['']; + to: EditorScrollDirection = 'up'; + by: EditorScrollByUnit = 'page'; +} + +@RegisterAction +class CommandMoveFullPageDown extends CommandEditorScroll { + keys = ['']; + to: EditorScrollDirection = 'down'; + by: EditorScrollByUnit = 'page'; +} + +@RegisterAction +class CommandMoveHalfPageDown extends CommandEditorScroll { + keys = ['']; + to: EditorScrollDirection = 'down'; + by: EditorScrollByUnit = 'halfPage'; +} + +@RegisterAction +class CommandMoveHalfPageUp extends CommandEditorScroll { + keys = ['']; + to: EditorScrollDirection = 'up'; + by: EditorScrollByUnit = 'halfPage'; +} + +@RegisterAction +export class CommandInsertAtCursor extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['i']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Insert; + return vimState; + } + + public 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 +class CommandReplaceAtCursor extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['R']; + runsOnceForEachCountPrefix = false; + + public async exec(position: Position, vimState: VimState): Promise { + let timesToRepeat = vimState.recordedState.count || 1; + + vimState.currentMode = ModeName.Replace; + vimState.replaceState = new ReplaceState(position, timesToRepeat); + + return vimState; + } +} + +@RegisterAction +class CommandReplaceInReplaceMode extends BaseCommand { + modes = [ModeName.Replace]; + keys = ['']; + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + const char = this.keysPressed[0]; + const replaceState = vimState.replaceState!; + + if (char === '') { + if (position.isBeforeOrEqual(replaceState.replaceCursorStartPosition)) { + // If you backspace before the beginning of where you started to replace, + // just move the cursor back. + + vimState.cursorPosition = position.getLeft(); + vimState.cursorStartPosition = position.getLeft(); + } else if ( + position.line > replaceState.replaceCursorStartPosition.line || + position.character > replaceState.originalChars.length + ) { + vimState.recordedState.transformations.push({ + type: 'deleteText', + position: position, + }); + } else { + vimState.recordedState.transformations.push({ + type: 'replaceText', + text: replaceState.originalChars[position.character - 1], + start: position.getLeft(), + end: position, + diff: new PositionDiff(0, -1), + }); + } + + replaceState.newChars.pop(); + } else { + if (!position.isLineEnd() && char !== '\n') { + vimState.recordedState.transformations.push({ + type: 'replaceText', + text: char, + start: position, + end: position.getRight(), + diff: new PositionDiff(0, 1), + }); + } else { + vimState.recordedState.transformations.push({ + type: 'insertText', + text: char, + position: position, + }); + } + + replaceState.newChars.push(char); + } + + vimState.currentMode = ModeName.Replace; + return vimState; + } +} + +@RegisterAction +class CommandInsertInSearchMode extends BaseCommand { + modes = [ModeName.SearchInProgressMode]; + keys = [[''], [''], ['']]; + runsOnceForEveryCursor() { + return this.keysPressed[0] === '\n'; + } + + public async exec(position: Position, vimState: VimState): Promise { + const key = this.keysPressed[0]; + const searchState = vimState.globalState.searchState!; + const prevSearchList = vimState.globalState.searchStatePrevious!; + + // handle special keys first + if (key === '' || key === '') { + searchState.searchString = searchState.searchString.slice(0, -1); + } else if (key === '\n') { + vimState.currentMode = vimState.globalState.searchState!.previousMode; + + // Repeat the previous search if no new string is entered + if (searchState.searchString === '') { + if (prevSearchList.length > 0) { + searchState.searchString = prevSearchList[prevSearchList.length - 1].searchString; + } + } + + // Store this search if different than previous + if (vimState.globalState.searchStatePrevious.length !== 0) { + let previousSearchState = vimState.globalState.searchStatePrevious; + if ( + searchState.searchString !== + previousSearchState[previousSearchState.length - 1]!.searchString + ) { + previousSearchState.push(searchState); + } + } else { + vimState.globalState.searchStatePrevious.push(searchState); + } + + // Make sure search history does not exceed configuration option + if (vimState.globalState.searchStatePrevious.length > Configuration.history) { + vimState.globalState.searchStatePrevious.splice(0, 1); + } + + // Update the index to the end of the search history + vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length - 1; + + // Move cursor to next match + vimState.cursorPosition = searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; + + return vimState; + } else if (key === '') { + vimState.globalState.searchStateIndex -= 1; + + // Clamp the history index to stay within bounds of search history length + vimState.globalState.searchStateIndex = Math.max(vimState.globalState.searchStateIndex, 0); + + if (prevSearchList[vimState.globalState.searchStateIndex] !== undefined) { + searchState.searchString = + prevSearchList[vimState.globalState.searchStateIndex].searchString; + } + } else if (key === '') { + vimState.globalState.searchStateIndex += 1; + + // If past the first history item, allow user to enter their own search string (not using history) + if ( + vimState.globalState.searchStateIndex > + vimState.globalState.searchStatePrevious.length - 1 + ) { + searchState.searchString = ''; + vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length; + return vimState; + } + + if (prevSearchList[vimState.globalState.searchStateIndex] !== undefined) { + searchState.searchString = + prevSearchList[vimState.globalState.searchStateIndex].searchString; + } + } else { + searchState.searchString += this.keysPressed[0]; + } + + return vimState; + } +} + +@RegisterAction +class CommandEscInSearchMode extends BaseCommand { + modes = [ModeName.SearchInProgressMode]; + keys = ['']; + runsOnceForEveryCursor() { + return this.keysPressed[0] === '\n'; + } + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Normal; + vimState.globalState.searchState = undefined; + + return vimState; + } +} + +@RegisterAction +class CommandCtrlVInSearchMode extends BaseCommand { + modes = [ModeName.SearchInProgressMode]; + keys = ['']; + runsOnceForEveryCursor() { + return this.keysPressed[0] === '\n'; + } + + public async exec(position: Position, vimState: VimState): Promise { + const searchState = vimState.globalState.searchState!; + const textFromClipboard = util.clipboardPaste(); + + searchState.searchString += textFromClipboard; + return vimState; + } +} + +@RegisterAction +class CommandCmdVInSearchMode extends BaseCommand { + modes = [ModeName.SearchInProgressMode]; + keys = ['']; + runsOnceForEveryCursor() { + return this.keysPressed[0] === '\n'; + } + + public async exec(position: Position, vimState: VimState): Promise { + const searchState = vimState.globalState.searchState!; + const textFromClipboard = util.clipboardPaste(); + + searchState.searchString += textFromClipboard; + return vimState; + } +} + +/** + * 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 = [ + ModeName.Visual, + ModeName.VisualLine, + ModeName.VisualBlock, + ModeName.Insert, + ModeName.Normal, + ]; + keys = ['']; // A special key - see ModeHandler + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + let text = ''; + + if (vimState.currentMode === ModeName.Visual || vimState.currentMode === ModeName.Normal) { + text = vimState.allCursors + .map(range => { + const start = Position.EarlierOf(range.start, range.stop); + const stop = Position.LaterOf(range.start, range.stop); + return vimState.editor.document.getText(new vscode.Range(start, stop.getRight())); + }) + .join('\n'); + } else if (vimState.currentMode === ModeName.VisualLine) { + text = vimState.allCursors + .map(range => { + return vimState.editor.document.getText( + new vscode.Range( + Position.EarlierOf(range.start.getLineBegin(), range.stop.getLineBegin()), + Position.LaterOf(range.start.getLineEnd(), range.stop.getLineEnd()) + ) + ); + }) + .join('\n'); + } else if (vimState.currentMode === ModeName.VisualBlock) { + for (const { line } of Position.IterateLine(vimState)) { + text += line + '\n'; + } + } else if (vimState.currentMode === ModeName.Insert) { + text = vimState.editor.selections + .map(selection => { + return vimState.editor.document.getText(new vscode.Range(selection.start, selection.end)); + }) + .join('\n'); + } + + util.clipboardCopy(text); + // all vim yank operations return to normal mode. + vimState.currentMode = ModeName.Normal; + + return vimState; + } +} + +@RegisterAction +class CommandCmdA extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.cursorStartPosition = new Position(0, vimState.desiredColumn); + vimState.cursorPosition = new Position(TextEditor.getLineCount() - 1, vimState.desiredColumn); + vimState.currentMode = ModeName.VisualLine; + + return vimState; + } +} + +function searchCurrentWord( + position: Position, + vimState: VimState, + direction: SearchDirection, + isExact: boolean +) { + const currentWord = TextEditor.getWord(position); + + // 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.cursorPosition.getWordLeft(true) + : vimState.cursorPosition; + + return createSearchStateAndMoveToMatch({ + needle: currentWord, + vimState, + direction, + isExact, + searchStartCursorPosition, + }); +} + +function searchCurrentSelection(vimState: VimState, direction: SearchDirection) { + const selection = TextEditor.getSelection(); + const end = new Position(selection.end.line, selection.end.character); + const currentSelection = TextEditor.getText(selection.with(selection.start, end)); + + // Go back to Normal mode, otherwise the selection grows to the next match. + vimState.currentMode = ModeName.Normal; + + // If the search is going left then use `getLeft()` on the selection start. + // If going right then use `getRight()` on the selection end. This ensures + // that any matches happen outside of the currently selected word. + const searchStartCursorPosition = + direction === SearchDirection.Backward + ? vimState.lastVisualSelectionStart.getLeft() + : vimState.lastVisualSelectionEnd.getRight(); + + return createSearchStateAndMoveToMatch({ + needle: currentSelection, + vimState, + direction, + isExact: false, + searchStartCursorPosition, + }); +} + +function createSearchStateAndMoveToMatch(args: { + needle?: string | undefined; + vimState: VimState; + direction: SearchDirection; + isExact: boolean; + searchStartCursorPosition: Position; +}) { + const { needle, vimState, isExact } = args; + + if (needle === undefined || needle.length === 0) { + return vimState; + } + + const searchString = isExact ? `\\b${needle}\\b` : needle; + + // Start a search for the given term. + vimState.globalState.searchState = new SearchState( + args.direction, + vimState.cursorPosition, + searchString, + { isRegex: isExact }, + vimState.currentMode + ); + + vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition( + args.searchStartCursorPosition + ).pos; + + // Turn one of the highlighting flags back on (turned off with :nohl) + vimState.globalState.hl = true; + + return vimState; +} + +@RegisterAction +class CommandSearchCurrentWordExactForward extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['*']; + isMotion = true; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + return searchCurrentWord(position, vimState, SearchDirection.Forward, true); + } +} + +@RegisterAction +class CommandSearchCurrentWordForward extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['g', '*']; + isMotion = true; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + return searchCurrentWord(position, vimState, SearchDirection.Forward, false); + } +} + +@RegisterAction +class CommandSearchVisualForward extends BaseCommand { + modes = [ModeName.Visual, ModeName.VisualLine]; + keys = ['*']; + isMotion = true; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + return searchCurrentSelection(vimState, SearchDirection.Forward); + } +} + +@RegisterAction +class CommandSearchCurrentWordExactBackward extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['#']; + isMotion = true; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + return searchCurrentWord(position, vimState, SearchDirection.Backward, true); + } +} + +@RegisterAction +class CommandSearchCurrentWordBackward extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['g', '#']; + isMotion = true; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + return searchCurrentWord(position, vimState, SearchDirection.Backward, false); + } +} + +@RegisterAction +class CommandSearchVisualBackward extends BaseCommand { + modes = [ModeName.Visual, ModeName.VisualLine]; + keys = ['#']; + isMotion = true; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + return searchCurrentSelection(vimState, SearchDirection.Backward); + } +} + +@RegisterAction +export class CommandSearchForwards extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['/']; + isMotion = true; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.globalState.searchState = new SearchState( + SearchDirection.Forward, + vimState.cursorPosition, + '', + { isRegex: true }, + vimState.currentMode + ); + vimState.currentMode = ModeName.SearchInProgressMode; + + // Reset search history index + vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length; + + vimState.globalState.hl = true; + + return vimState; + } +} + +@RegisterAction +export class CommandSearchBackwards extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['?']; + isMotion = true; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.globalState.searchState = new SearchState( + SearchDirection.Backward, + vimState.cursorPosition, + '', + { isRegex: true }, + vimState.currentMode + ); + vimState.currentMode = ModeName.SearchInProgressMode; + + // Reset search history index + vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length; + + vimState.globalState.hl = true; + + return vimState; + } +} + +@RegisterAction +export class MarkCommand extends BaseCommand { + keys = ['m', '']; + modes = [ModeName.Normal]; + + public async exec(position: Position, vimState: VimState): Promise { + const markName = this.keysPressed[1]; + + vimState.historyTracker.addMark(position, markName); + + return vimState; + } +} + +@RegisterAction +export class PutCommand extends BaseCommand { + keys = ['p']; + modes = [ModeName.Normal]; + runsOnceForEachCountPrefix = true; + canBeRepeatedWithDot = true; + + constructor(multicursorIndex?: number) { + super(); + this.multicursorIndex = multicursorIndex; + } + public static async GetText( + vimState: VimState, + multicursorIndex: number | undefined = undefined + ): Promise { + const register = await Register.get(vimState); + + if (vimState.isMultiCursor) { + if (multicursorIndex === undefined) { + console.log('ERROR: no multi cursor index when calling PutCommand#getText'); + + throw new Error('Bad!'); + } + + if (vimState.isMultiCursor && typeof register.text === 'object') { + return register.text[multicursorIndex]; + } + } + + return register.text as string; + } + + public async exec( + position: Position, + vimState: VimState, + after: boolean = false, + adjustIndent: boolean = false + ): Promise { + const register = await Register.get(vimState); + const dest = after ? position : position.getRight(); + + if (register.text instanceof RecordedState) { + /** + * Paste content from recordedState. This one is actually complex as + * Vim has internal key code for key strokes.For example, Backspace + * is stored as `<80>kb`. So if you replay a macro, which is stored + * in a register as `a1<80>kb2`, youshall just get `2` inserted as + * `a` represents entering Insert Mode, `<80>bk` represents + * Backspace. However here, we shall + * insert the plain text content of the register, which is `a1<80>kb2`. + */ + vimState.recordedState.transformations.push({ + type: 'macro', + register: vimState.recordedState.registerName, + replay: 'keystrokes', + }); + return vimState; + } else if (typeof register.text === 'object' && vimState.currentMode === ModeName.VisualBlock) { + return await this.execVisualBlockPaste(register.text, position, vimState, after); + } + + let text = await PutCommand.GetText(vimState, this.multicursorIndex); + + let textToAdd: string; + let whereToAddText: Position; + let diff = new PositionDiff(0, 0); + + if (register.registerMode === RegisterMode.CharacterWise) { + textToAdd = text; + whereToAddText = dest; + } else if ( + vimState.currentMode === ModeName.Visual && + register.registerMode === RegisterMode.LineWise + ) { + // in the specific case of linewise register data during visual mode, + // we need extra newline feeds + textToAdd = '\n' + text + '\n'; + whereToAddText = dest; + } else { + if (adjustIndent) { + // Adjust indent to current line + let indentationWidth = TextEditor.getIndentationLevel(TextEditor.getLineAt(position).text); + let firstLineIdentationWidth = TextEditor.getIndentationLevel(text.split('\n')[0]); + + text = text + .split('\n') + .map(line => { + let currentIdentationWidth = TextEditor.getIndentationLevel(line); + let newIndentationWidth = + currentIdentationWidth - firstLineIdentationWidth + indentationWidth; + + return TextEditor.setIndentationLevel(line, newIndentationWidth); + }) + .join('\n'); + } + + if (after) { + // P insert before current line + textToAdd = text + '\n'; + whereToAddText = dest.getLineBegin(); + } else { + // p paste after current line + textToAdd = '\n' + text; + whereToAddText = dest.getLineEnd(); + } + } + + // More vim weirdness: If the thing you're pasting has a newline, the cursor + // stays in the same place. Otherwise, it moves to the end of what you pasted. + + const numNewlines = text.split('\n').length - 1; + const currentLineLength = TextEditor.getLineAt(position).text.length; + + if (register.registerMode === RegisterMode.LineWise) { + const check = text.match(/^\s*/); + let numWhitespace = 0; + + if (check) { + numWhitespace = check[0].length; + } + + if (after) { + diff = PositionDiff.NewBOLDiff(-numNewlines - 1, numWhitespace); + } else { + diff = PositionDiff.NewBOLDiff(currentLineLength > 0 ? 1 : -numNewlines, numWhitespace); + } + } else { + if (text.indexOf('\n') === -1) { + if (!position.isLineEnd()) { + if (after) { + diff = new PositionDiff(0, -1); + } else { + diff = new PositionDiff(0, textToAdd.length); + } + } + } else { + if (position.isLineEnd()) { + diff = PositionDiff.NewBOLDiff(-numNewlines, position.character); + } else { + if (after) { + diff = PositionDiff.NewBOLDiff(-numNewlines, position.character); + } else { + diff = new PositionDiff(0, 1); + } + } + } + } + + vimState.recordedState.transformations.push({ + type: 'insertText', + text: textToAdd, + position: whereToAddText, + diff: diff, + }); + + vimState.currentRegisterMode = register.registerMode; + return vimState; + } + + private async execVisualBlockPaste( + block: string[], + position: Position, + vimState: VimState, + after: boolean + ): Promise { + if (after) { + position = position.getRight(); + } + + // Add empty lines at the end of the document, if necessary. + let linesToAdd = Math.max(0, block.length - (TextEditor.getLineCount() - position.line) + 1); + + if (linesToAdd > 0) { + await TextEditor.insertAt( + Array(linesToAdd).join('\n'), + new Position( + TextEditor.getLineCount() - 1, + TextEditor.getLineAt(new Position(TextEditor.getLineCount() - 1, 0)).text.length + ) + ); + } + + // paste the entire block. + for (let lineIndex = position.line; lineIndex < position.line + block.length; lineIndex++) { + const line = block[lineIndex - position.line]; + const insertPos = new Position( + lineIndex, + Math.min(position.character, TextEditor.getLineAt(new Position(lineIndex, 0)).text.length) + ); + + await TextEditor.insertAt(line, insertPos); + } + + vimState.currentRegisterMode = RegisterMode.FigureItOutFromCurrentMode; + return vimState; + } + + public async execCount(position: Position, vimState: VimState): Promise { + const result = await super.execCount(position, vimState); + + if ( + vimState.effectiveRegisterMode() === RegisterMode.LineWise && + vimState.recordedState.count > 0 + ) { + const numNewlines = + (await PutCommand.GetText(vimState, this.multicursorIndex)).split('\n').length * + vimState.recordedState.count; + + result.recordedState.transformations.push({ + type: 'moveCursor', + diff: new PositionDiff(-numNewlines + 1, 0), + cursorIndex: this.multicursorIndex, + }); + } + + return result; + } +} + +@RegisterAction +export class GPutCommand extends BaseCommand { + keys = ['g', 'p']; + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + runsOnceForEachCountPrefix = true; + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + const result = await new PutCommand().exec(position, vimState); + + return result; + } + + public async execCount(position: Position, vimState: VimState): Promise { + const register = await Register.get(vimState); + let addedLinesCount: number; + + if (register.text instanceof RecordedState) { + vimState.recordedState.transformations.push({ + type: 'macro', + register: vimState.recordedState.registerName, + replay: 'keystrokes', + }); + + return vimState; + } + if (typeof register.text === 'object') { + // visual block mode + addedLinesCount = register.text.length * vimState.recordedState.count; + } else { + addedLinesCount = register.text.split('\n').length; + } + + const result = await super.execCount(position, vimState); + + if (vimState.effectiveRegisterMode() === RegisterMode.LineWise) { + const line = TextEditor.getLineAt(position).text; + const addAnotherLine = line.length > 0 && addedLinesCount > 1; + + result.recordedState.transformations.push({ + type: 'moveCursor', + diff: PositionDiff.NewBOLDiff(1 + (addAnotherLine ? 1 : 0), 0), + cursorIndex: this.multicursorIndex, + }); + } + + return result; + } +} + +@RegisterAction +export class PutWithIndentCommand extends BaseCommand { + keys = [']', 'p']; + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + runsOnceForEachCountPrefix = true; + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + const result = await new PutCommand().exec(position, vimState, false, true); + return result; + } + + public async execCount(position: Position, vimState: VimState): Promise { + return await super.execCount(position, vimState); + } +} + +@RegisterAction +export class PutCommandVisual extends BaseCommand { + keys = [['p'], ['P']]; + modes = [ModeName.Visual, ModeName.VisualLine]; + runsOnceForEachCountPrefix = true; + + public async exec( + position: Position, + vimState: VimState, + after: boolean = false + ): Promise { + let start = vimState.cursorStartPosition; + let end = vimState.cursorPosition; + const isLineWise = vimState.currentMode === ModeName.VisualLine; + if (start.isAfter(end)) { + [start, end] = [end, start]; + } + + // If the to be inserted text is linewise we have a seperate logic delete the + // selection first than insert + let register = await Register.get(vimState); + if (register.registerMode === RegisterMode.LineWise) { + let deleteResult = await new operator.DeleteOperator(this.multicursorIndex).run( + vimState, + start, + end, + false + ); + // to ensure, that the put command nows this is + // an linewise register insertion in visual mode + let oldMode = deleteResult.currentMode; + deleteResult.currentMode = ModeName.Visual; + deleteResult = await new PutCommand().exec(start, deleteResult, true); + deleteResult.currentMode = oldMode; + return deleteResult; + } + + // The reason we need to handle Delete and Yank separately is because of + // linewise mode. If we're in visualLine mode, then we want to copy + // linewise but not necessarily delete linewise. + let putResult = await new PutCommand(this.multicursorIndex).exec(start, vimState, true); + putResult.currentRegisterMode = isLineWise ? RegisterMode.LineWise : RegisterMode.CharacterWise; + putResult.recordedState.registerName = Configuration.useSystemClipboard ? '*' : '"'; + putResult = await new operator.YankOperator(this.multicursorIndex).run(putResult, start, end); + putResult.currentRegisterMode = RegisterMode.CharacterWise; + putResult = await new operator.DeleteOperator(this.multicursorIndex).run( + putResult, + start, + end.getLeftIfEOL(), + false + ); + putResult.currentRegisterMode = RegisterMode.FigureItOutFromCurrentMode; + return putResult; + } + + // TODO - execWithCount +} + +@RegisterAction +export class PutBeforeCommand extends BaseCommand { + public keys = ['P']; + public modes = [ModeName.Normal]; + canBeRepeatedWithDot = true; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + const command = new PutCommand(); + command.multicursorIndex = this.multicursorIndex; + + const result = await command.exec(position, vimState, true); + + return result; + } +} + +@RegisterAction +export class GPutBeforeCommand extends BaseCommand { + keys = ['g', 'P']; + modes = [ModeName.Normal]; + + public async exec(position: Position, vimState: VimState): Promise { + const result = await new PutCommand().exec(position, vimState, true); + const register = await Register.get(vimState); + let addedLinesCount: number; + + if (register.text instanceof RecordedState) { + vimState.recordedState.transformations.push({ + type: 'macro', + register: vimState.recordedState.registerName, + replay: 'keystrokes', + }); + + return vimState; + } else if (typeof register.text === 'object') { + // visual block mode + addedLinesCount = register.text.length * vimState.recordedState.count; + } else { + addedLinesCount = register.text.split('\n').length; + } + + if (vimState.effectiveRegisterMode() === RegisterMode.LineWise) { + const line = TextEditor.getLineAt(position).text; + const addAnotherLine = line.length > 0 && addedLinesCount > 1; + + result.recordedState.transformations.push({ + type: 'moveCursor', + diff: PositionDiff.NewBOLDiff(1 + (addAnotherLine ? 1 : 0), 0), + cursorIndex: this.multicursorIndex, + }); + } + + return result; + } +} + +@RegisterAction +export class PutBeforeWithIndentCommand extends BaseCommand { + keys = ['[', 'p']; + modes = [ModeName.Normal]; + + public async exec(position: Position, vimState: VimState): Promise { + const result = await new PutCommand().exec(position, vimState, true, true); + + if (vimState.effectiveRegisterMode() === RegisterMode.LineWise) { + result.cursorPosition = result.cursorPosition + .getPreviousLineBegin() + .getFirstLineNonBlankChar(); + } + + return result; + } +} + +@RegisterAction +class CommandShowCommandLine extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = [':']; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + vimState.recordedState.transformations.push({ + type: 'showCommandLine', + }); + + if (vimState.currentMode === ModeName.Normal) { + vimState.commandInitialText = ''; + } else { + vimState.commandInitialText = "'<,'>"; + } + vimState.currentMode = ModeName.Normal; + + return vimState; + } +} + +@RegisterAction +class CommandDot extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['.']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.recordedState.transformations.push({ + type: 'dot', + }); + + return vimState; + } +} + +abstract class CommandFold extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + commandName: string; + + public async exec(position: Position, vimState: VimState): Promise { + await vscode.commands.executeCommand(this.commandName); + vimState.currentMode = ModeName.Normal; + return vimState; + } +} + +@RegisterAction +class CommandCloseFold extends CommandFold { + keys = ['z', 'c']; + commandName = 'editor.fold'; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + let timesToRepeat = vimState.recordedState.count || 1; + await vscode.commands.executeCommand('editor.fold', { levels: timesToRepeat, direction: 'up' }); + vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); + return vimState; + } +} + +@RegisterAction +class CommandCloseAllFolds extends CommandFold { + keys = ['z', 'M']; + commandName = 'editor.foldAll'; +} + +@RegisterAction +class CommandOpenFold extends CommandFold { + keys = ['z', 'o']; + commandName = 'editor.unfold'; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + let timesToRepeat = vimState.recordedState.count || 1; + await vscode.commands.executeCommand('editor.unfold', { + levels: timesToRepeat, + direction: 'down', + }); + + return vimState; + } +} + +@RegisterAction +class CommandOpenAllFolds extends CommandFold { + keys = ['z', 'R']; + commandName = 'editor.unfoldAll'; +} + +@RegisterAction +class CommandCloseAllFoldsRecursively extends CommandFold { + modes = [ModeName.Normal]; + keys = ['z', 'C']; + commandName = 'editor.foldRecursively'; +} + +@RegisterAction +class CommandOpenAllFoldsRecursively extends CommandFold { + modes = [ModeName.Normal]; + keys = ['z', 'O']; + commandName = 'editor.unfoldRecursively'; +} + +@RegisterAction +class CommandCenterScroll extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['z', 'z']; + + public 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.cursorPosition, vimState.cursorPosition), + vscode.TextEditorRevealType.InCenter + ); + + return vimState; + } +} + +@RegisterAction +class CommandCenterScrollFirstChar extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['z', '.']; + + public 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.cursorPosition, vimState.cursorPosition), + vscode.TextEditorRevealType.InCenter + ); + + // Move cursor to first char of line + vimState.cursorPosition = vimState.cursorPosition.getFirstLineNonBlankChar(); + + return vimState; + } +} + +@RegisterAction +class CommandTopScroll extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['z', 't']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.postponedCodeViewChanges.push({ + command: 'revealLine', + args: { + lineNumber: position.line, + at: 'top', + }, + }); + return vimState; + } +} + +@RegisterAction +class CommandTopScrollFirstChar extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['z', '\n']; + + public 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.cursorPosition = vimState.cursorPosition.getFirstLineNonBlankChar(); + + return vimState; + } +} + +@RegisterAction +class CommandBottomScroll extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['z', 'b']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.postponedCodeViewChanges.push({ + command: 'revealLine', + args: { + lineNumber: position.line, + at: 'bottom', + }, + }); + return vimState; + } +} + +@RegisterAction +class CommandBottomScrollFirstChar extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['z', '-']; + + public 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.cursorPosition = vimState.cursorPosition.getFirstLineNonBlankChar(); + + return vimState; + } +} + +@RegisterAction +class CommandGoToOtherEndOfHighlightedText extends BaseCommand { + modes = [ModeName.Visual, ModeName.VisualLine]; + keys = ['o']; + + public async exec(position: Position, vimState: VimState): Promise { + [vimState.cursorStartPosition, vimState.cursorPosition] = [ + vimState.cursorPosition, + vimState.cursorStartPosition, + ]; + + return vimState; + } +} + +@RegisterAction +class CommandUndo extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['u']; + runsOnceForEveryCursor() { + return false; + } + mustBeFirstKey = true; + + public async exec(position: Position, vimState: VimState): Promise { + const newPositions = await vimState.historyTracker.goBackHistoryStep(); + + if (newPositions !== undefined) { + vimState.allCursors = newPositions.map(x => new Range(x, x)); + } + + vimState.alteredHistory = true; + return vimState; + } +} + +@RegisterAction +class CommandRedo extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['']; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + const newPositions = await vimState.historyTracker.goForwardHistoryStep(); + + if (newPositions !== undefined) { + vimState.allCursors = newPositions.map(x => new Range(x, x)); + } + + vimState.alteredHistory = true; + return vimState; + } +} + +@RegisterAction +class CommandDeleteToLineEnd extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['D']; + canBeRepeatedWithDot = true; + runsOnceForEveryCursor() { + return true; + } + + public async exec(position: Position, vimState: VimState): Promise { + if (position.isLineEnd()) { + return vimState; + } + + return await new operator.DeleteOperator(this.multicursorIndex).run( + vimState, + position, + position.getLineEnd().getLeft() + ); + } +} + +@RegisterAction +export class CommandYankFullLine extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['Y']; + + public async exec(position: Position, vimState: VimState): Promise { + const linesDown = (vimState.recordedState.count || 1) - 1; + const start = position.getLineBegin(); + const end = new Position(position.line + linesDown, 0).getLineEnd().getLeft(); + + vimState.currentRegisterMode = RegisterMode.LineWise; + + return await new operator.YankOperator().run(vimState, start, end); + } +} + +@RegisterAction +class CommandChangeToLineEnd extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['C']; + runsOnceForEachCountPrefix = false; + mustBeFirstKey = true; + + public async exec(position: Position, vimState: VimState): Promise { + let count = vimState.recordedState.count || 1; + + return new operator.ChangeOperator().run( + vimState, + position, + position + .getDownByCount(Math.max(0, count - 1)) + .getLineEnd() + .getLeft() + ); + } +} + +@RegisterAction +class CommandClearLine extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['S']; + runsOnceForEachCountPrefix = false; + + public async exec(position: Position, vimState: VimState): Promise { + let count = vimState.recordedState.count || 1; + let end = position + .getDownByCount(Math.max(0, count - 1)) + .getLineEnd() + .getLeft(); + return new operator.ChangeOperator().run( + vimState, + position.getLineBeginRespectingIndent(), + end + ); + } +} + +@RegisterAction +class CommandExitVisualMode extends BaseCommand { + modes = [ModeName.Visual, ModeName.VisualLine]; + keys = ['v']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Normal; + + return vimState; + } +} + +@RegisterAction +class CommandVisualMode extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['v']; + isCompleteAction = false; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Visual; + + return vimState; + } +} + +@RegisterAction +class CommandReselectVisual extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['g', 'v']; + + public async exec(position: Position, vimState: VimState): Promise { + // Try to restore selection only if valid + if ( + vimState.lastVisualSelectionEnd !== undefined && + vimState.lastVisualSelectionStart !== undefined && + vimState.lastVisualMode !== undefined + ) { + if (vimState.lastVisualSelectionEnd.line <= TextEditor.getLineCount() - 1) { + vimState.currentMode = vimState.lastVisualMode; + vimState.cursorStartPosition = vimState.lastVisualSelectionStart; + vimState.cursorPosition = vimState.lastVisualSelectionEnd.getLeft(); + } + } + return vimState; + } +} + +@RegisterAction +class CommandVisualBlockMode extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualBlock]; + keys = ['']; + + public async exec(position: Position, vimState: VimState): Promise { + if (vimState.currentMode === ModeName.VisualBlock) { + vimState.currentMode = ModeName.Normal; + } else { + vimState.currentMode = ModeName.VisualBlock; + } + + return vimState; + } +} + +@RegisterAction +class CommandVisualLineMode extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual]; + keys = ['V']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.VisualLine; + + return vimState; + } +} + +@RegisterAction +class CommandExitVisualLineMode extends BaseCommand { + modes = [ModeName.VisualLine]; + keys = ['V']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Normal; + + return vimState; + } +} + +@RegisterAction +class CommandOpenFile extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual]; + keys = ['g', 'f']; + + public async exec(position: Position, vimState: VimState): Promise { + let fullFilePath: string = ''; + + if (vimState.currentMode === ModeName.Visual) { + const selection = TextEditor.getSelection(); + const end = new Position(selection.end.line, selection.end.character + 1); + fullFilePath = TextEditor.getText(selection.with(selection.start, end)); + } else { + const start = position.getFilePathLeft(true); + const end = position.getFilePathRight(); + const range = new vscode.Range(start, end); + + fullFilePath = TextEditor.getText(range).trim(); + } + const fileInfo = fullFilePath.match(/(.*?(?=:[0-9]+)|.*):?([0-9]*)$/); + if (fileInfo) { + const filePath = fileInfo[1]; + const lineNumber = parseInt(fileInfo[2], 10); + const fileCommand = new FileCommand({ name: filePath, lineNumber: lineNumber }); + fileCommand.execute(); + } + + return vimState; + } +} + +@RegisterAction +class CommandGoToDefinition extends BaseCommand { + modes = [ModeName.Normal]; + keys = [['g', 'd'], ['']]; + + public async exec(position: Position, vimState: VimState): Promise { + const oldActiveEditor = vimState.editor; + + await vscode.commands.executeCommand('editor.action.goToDeclaration'); + + if (oldActiveEditor === vimState.editor) { + vimState.cursorPosition = Position.FromVSCodePosition(vimState.editor.selection.start); + } + + return vimState; + } +} + +@RegisterAction +class CommandGoBackInChangelist extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['g', ';']; + + public async exec(position: Position, vimState: VimState): Promise { + const originalIndex = vimState.historyTracker.changelistIndex; + const prevPos = vimState.historyTracker.getChangePositionAtindex(originalIndex - 1); + const currPos = vimState.historyTracker.getChangePositionAtindex(originalIndex); + + if (prevPos !== undefined) { + vimState.cursorPosition = prevPos[0]; + vimState.historyTracker.changelistIndex = originalIndex - 1; + } else if (currPos !== undefined) { + vimState.cursorPosition = currPos[0]; + } + + return vimState; + } +} + +@RegisterAction +class CommandGoForwardInChangelist extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['g', ',']; + + public async exec(position: Position, vimState: VimState): Promise { + const originalIndex = vimState.historyTracker.changelistIndex; + const nextPos = vimState.historyTracker.getChangePositionAtindex(originalIndex + 1); + const currPos = vimState.historyTracker.getChangePositionAtindex(originalIndex); + + if (nextPos !== undefined) { + vimState.cursorPosition = nextPos[0]; + vimState.historyTracker.changelistIndex = originalIndex + 1; + } else if (currPos !== undefined) { + vimState.cursorPosition = currPos[0]; + } + + return vimState; + } +} + +@RegisterAction +class CommandGoLastChange extends BaseCommand { + modes = [ModeName.Normal]; + keys = [['`', '.'], ["'", '.']]; + + public async exec(position: Position, vimState: VimState): Promise { + const lastPos = vimState.historyTracker.getLastHistoryStartPosition(); + + if (lastPos !== undefined) { + vimState.cursorPosition = lastPos[0]; + } + + return vimState; + } +} + +@RegisterAction +class CommandInsertAtLastChange extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['g', 'i']; + + public async exec(position: Position, vimState: VimState): Promise { + const lastPos = vimState.historyTracker.getLastChangeEndPosition(); + + if (lastPos !== undefined) { + vimState.cursorPosition = lastPos; + vimState.currentMode = ModeName.Insert; + } + + return vimState; + } +} + +@RegisterAction +export class CommandInsertAtFirstCharacter extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual]; + keys = ['I']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Insert; + vimState.cursorPosition = position.getFirstLineNonBlankChar(); + + return vimState; + } +} + +@RegisterAction +class CommandInsertAtLineBegin extends BaseCommand { + modes = [ModeName.Normal]; + mustBeFirstKey = true; + keys = ['g', 'I']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Insert; + vimState.cursorPosition = position.getLineBegin(); + + return vimState; + } +} + +@RegisterAction +export class CommandInsertAfterCursor extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['a']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Insert; + vimState.cursorPosition = position.getRight(); + + return vimState; + } + + public 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 CommandInsertAtLineEnd extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual]; + keys = ['A']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Insert; + vimState.cursorPosition = position.getLineEnd(); + + return vimState; + } +} + +@RegisterAction +class CommandInsertNewLineAbove extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['O']; + runsOnceForEveryCursor() { + return false; + } + + public async execCount(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Insert; + let count = vimState.recordedState.count || 1; + // Why do we do this? Who fucking knows??? If the cursor is at the + // beginning of the line, then editor.action.insertLineBefore does some + // weird things with following paste command. Refer to + // https://github.com/VSCodeVim/Vim/pull/1663#issuecomment-300573129 for + // more details. + const tPos = Position.FromVSCodePosition( + vscode.window.activeTextEditor!.selection.active + ).getRight(); + vscode.window.activeTextEditor!.selection = new vscode.Selection(tPos, tPos); + for (let i = 0; i < count; i++) { + await vscode.commands.executeCommand('editor.action.insertLineBefore'); + } + + vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); + for (let i = 0; i < count; i++) { + const newPos = new Position( + vimState.allCursors[0].start.line + i, + vimState.allCursors[0].start.character + ); + vimState.allCursors.push(new Range(newPos, newPos)); + } + vimState.allCursors = vimState.allCursors.reverse(); + vimState.isFakeMultiCursor = true; + vimState.isMultiCursor = true; + return vimState; + } +} + +@RegisterAction +class CommandInsertNewLineBefore extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['o']; + runsOnceForEveryCursor() { + return false; + } + + public async execCount(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Insert; + let count = vimState.recordedState.count || 1; + + for (let i = 0; i < count; i++) { + await vscode.commands.executeCommand('editor.action.insertLineAfter'); + } + vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); + for (let i = 1; i < count; i++) { + const newPos = new Position( + vimState.allCursors[0].start.line - i, + vimState.allCursors[0].start.character + ); + vimState.allCursors.push(new Range(newPos, newPos)); + + // Ahhhhhh. We have to manually set cursor position here as we need text + // transformations AND to set multiple cursors. + vimState.recordedState.transformations.push({ + type: 'insertText', + text: TextEditor.setIndentationLevel('', newPos.character), + position: newPos, + cursorIndex: i, + manuallySetCursorPositions: true, + }); + } + vimState.allCursors = vimState.allCursors.reverse(); + vimState.isFakeMultiCursor = true; + vimState.isMultiCursor = true; + return vimState; + } +} + +@RegisterAction +class CommandNavigateBack extends BaseCommand { + modes = [ModeName.Normal]; + keys = [[''], ['']]; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + const oldActiveEditor = vimState.editor; + + await vscode.commands.executeCommand('workbench.action.navigateBack'); + + if (oldActiveEditor === vimState.editor) { + vimState.cursorPosition = Position.FromVSCodePosition(vimState.editor.selection.start); + } + + return vimState; + } +} + +@RegisterAction +class CommandNavigateForward extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['']; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + const oldActiveEditor = vimState.editor; + + await vscode.commands.executeCommand('workbench.action.navigateForward'); + + if (oldActiveEditor === vimState.editor) { + vimState.cursorPosition = Position.FromVSCodePosition(vimState.editor.selection.start); + } + + return vimState; + } +} + +@RegisterAction +class CommandNavigateLast extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['`', '`']; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + const oldActiveEditor = vimState.editor; + + await vscode.commands.executeCommand('workbench.action.navigateLast'); + + if (oldActiveEditor === vimState.editor) { + vimState.cursorPosition = Position.FromVSCodePosition(vimState.editor.selection.start); + } + + return vimState; + } +} + +@RegisterAction +class CommandNavigateLastBOL extends BaseCommand { + modes = [ModeName.Normal]; + keys = ["'", "'"]; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + const oldActiveEditor = vimState.editor; + + await vscode.commands.executeCommand('workbench.action.navigateLast'); + + if (oldActiveEditor === vimState.editor) { + const pos = Position.FromVSCodePosition(vimState.editor.selection.start); + vimState.cursorPosition = pos.getFirstLineNonBlankChar(); + } + + return vimState; + } +} + +@RegisterAction +class CommandQuit extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['', 'q']; + + public async exec(position: Position, vimState: VimState): Promise { + new QuitCommand({}).execute(); + + return vimState; + } +} + +@RegisterAction +class CommandOnly extends BaseCommand { + modes = [ModeName.Normal]; + keys = [['', 'o'], ['', 'C-o']]; + + public async exec(position: Position, vimState: VimState): Promise { + new OnlyCommand({}).execute(); + + return vimState; + } +} + +@RegisterAction +class MoveToRightPane extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = [['', 'l'], ['', ''], ['']]; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.postponedCodeViewChanges.push({ + command: 'workbench.action.navigateRight', + args: {}, + }); + + return vimState; + } +} + +@RegisterAction +class MoveToLowerPane extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = [['', 'j'], ['', ''], ['']]; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.postponedCodeViewChanges.push({ + command: 'workbench.action.navigateDown', + args: {}, + }); + + return vimState; + } +} + +@RegisterAction +class MoveToUpperPane extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = [['', 'k'], ['', ''], ['']]; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.postponedCodeViewChanges.push({ + command: 'workbench.action.navigateUp', + args: {}, + }); + + return vimState; + } +} + +@RegisterAction +class MoveToLeftPane extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = [['', 'h'], ['', ''], ['']]; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.postponedCodeViewChanges.push({ + command: 'workbench.action.navigateLeft', + args: {}, + }); + + return vimState; + } +} + +@RegisterAction +class CycleThroughPanes extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = [['', ''], ['', 'w']]; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.postponedCodeViewChanges.push({ + command: 'workbench.action.navigateEditorGroups', + args: {}, + }); + + return vimState; + } +} + +class BaseTabCommand extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + runsOnceForEachCountPrefix = true; +} + +@RegisterAction +class VerticalSplit extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['', 'v']; + + public async exec(position: Position, vimState: VimState): Promise { + vimState.postponedCodeViewChanges.push({ + command: 'workbench.action.splitEditor', + args: {}, + }); + + return vimState; + } +} + +@RegisterAction +class CommandTabNext extends BaseTabCommand { + keys = [['g', 't'], ['']]; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + new TabCommand({ + tab: Tab.Next, + count: vimState.recordedState.count, + }).execute(); + + return vimState; + } +} + +@RegisterAction +class CommandTabPrevious extends BaseTabCommand { + keys = [['g', 'T'], ['']]; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + new TabCommand({ + tab: Tab.Previous, + count: 1, + }).execute(); + + return vimState; + } +} + +@RegisterAction +class ActionDeleteChar extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['x']; + runsOnceForEachCountPrefix = true; + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + // If line is empty, do nothing + if (TextEditor.getLineAt(position).text.length < 1) { + return vimState; + } + + const state = await new operator.DeleteOperator(this.multicursorIndex).run( + vimState, + position, + position + ); + + state.currentMode = ModeName.Normal; + + return state; + } +} + +@RegisterAction +class ActionDeleteCharWithDeleteKey extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['']; + runsOnceForEachCountPrefix = true; + canBeRepeatedWithDot = true; + + public 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); + vimState.recordedState.actionKeys = vimState.recordedState.count.toString().split(''); + vimState.recordedState.commandList = vimState.recordedState.count.toString().split(''); + this.isCompleteAction = false; + return vimState; + } + return await new ActionDeleteChar().execCount(position, vimState); + } +} + +@RegisterAction +class ActionDeleteLastChar extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['X']; + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + if (position.character === 0) { + return vimState; + } + + return await new operator.DeleteOperator(this.multicursorIndex).run( + vimState, + position.getLeft(), + position.getLeft() + ); + } +} + +@RegisterAction +class ActionJoin extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['J']; + canBeRepeatedWithDot = true; + runsOnceForEachCountPrefix = false; + + private firstNonWhitespaceIndex(str: string): number { + for (let i = 0, len = str.length; i < len; i++) { + let chCode = str.charCodeAt(i); + if (chCode !== 32 /** space */ && chCode !== 9 /** tab */) { + return i; + } + } + return -1; + } + + public async execJoinLines( + startPosition: Position, + position: Position, + vimState: VimState, + count: number + ): Promise { + count = count - 1 || 1; + + let startLineNumber: number, + startColumn: number, + endLineNumber: number, + endColumn: number, + columnDeltaOffset: number = 0; + + if (startPosition.isEqual(position) || startPosition.line === position.line) { + if (position.line + 1 < TextEditor.getLineCount()) { + startLineNumber = position.line; + startColumn = 0; + endLineNumber = startLineNumber + count; + endColumn = TextEditor.getLineMaxColumn(endLineNumber); + } else { + startLineNumber = position.line; + startColumn = 0; + endLineNumber = position.line; + endColumn = TextEditor.getLineMaxColumn(endLineNumber); + } + } else { + startLineNumber = startPosition.line; + startColumn = 0; + endLineNumber = position.line; + endColumn = TextEditor.getLineMaxColumn(endLineNumber); + } + + let trimmedLinesContent = TextEditor.getLineAt(startPosition).text; + + for (let i = startLineNumber + 1; i <= endLineNumber; i++) { + let lineText = TextEditor.getLineAt(new Position(i, 0)).text; + + let firstNonWhitespaceIdx = this.firstNonWhitespaceIndex(lineText); + + if (firstNonWhitespaceIdx >= 0) { + let insertSpace = true; + + if ( + trimmedLinesContent === '' || + trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === ' ' || + trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === '\t' + ) { + insertSpace = false; + } + + let lineTextWithoutIndent = lineText.substr(firstNonWhitespaceIdx); + + if (lineTextWithoutIndent.charAt(0) === ')') { + insertSpace = false; + } + + trimmedLinesContent += (insertSpace ? ' ' : '') + lineTextWithoutIndent; + + if (insertSpace) { + columnDeltaOffset = lineTextWithoutIndent.length + 1; + } else { + columnDeltaOffset = lineTextWithoutIndent.length; + } + } else { + if ( + trimmedLinesContent === '' || + trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === ' ' || + trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === '\t' + ) { + columnDeltaOffset += 0; + } else { + trimmedLinesContent += ' '; + columnDeltaOffset += 1; + } + } + } + + let deleteStartPosition = new Position(startLineNumber, startColumn); + let deleteEndPosition = new Position(endLineNumber, endColumn); + + if (!deleteStartPosition.isEqual(deleteEndPosition)) { + if (startPosition.isEqual(position)) { + vimState.recordedState.transformations.push({ + type: 'replaceText', + text: trimmedLinesContent, + start: deleteStartPosition, + end: deleteEndPosition, + diff: new PositionDiff( + 0, + trimmedLinesContent.length - columnDeltaOffset - position.character + ), + }); + } else { + vimState.recordedState.transformations.push({ + type: 'replaceText', + text: trimmedLinesContent, + start: deleteStartPosition, + end: deleteEndPosition, + manuallySetCursorPositions: true, + }); + + vimState.cursorPosition = new Position( + startPosition.line, + trimmedLinesContent.length - columnDeltaOffset + ); + vimState.cursorStartPosition = vimState.cursorPosition; + vimState.currentMode = ModeName.Normal; + } + } + + return vimState; + } + + public async execCount(position: Position, vimState: VimState): Promise { + let timesToRepeat = vimState.recordedState.count || 1; + let resultingCursors: Range[] = []; + let i = 0; + + const cursorsToIterateOver = vimState.allCursors + .map(x => new Range(x.start, x.stop)) + .sort( + (a, b) => + a.start.line > b.start.line || + (a.start.line === b.start.line && a.start.character > b.start.character) + ? 1 + : -1 + ); + + for (const { start, stop } of cursorsToIterateOver) { + this.multicursorIndex = i++; + + vimState.cursorPosition = stop; + vimState.cursorStartPosition = start; + + vimState = await this.execJoinLines(start, stop, vimState, timesToRepeat); + + resultingCursors.push(new Range(vimState.cursorStartPosition, vimState.cursorPosition)); + + for (const transformation of vimState.recordedState.transformations) { + if (isTextTransformation(transformation) && transformation.cursorIndex === undefined) { + transformation.cursorIndex = this.multicursorIndex; + } + } + } + + vimState.allCursors = resultingCursors; + + return vimState; + } +} + +@RegisterAction +class ActionJoinVisualMode extends BaseCommand { + modes = [ModeName.Visual, ModeName.VisualLine]; + keys = ['J']; + + public async exec(position: Position, vimState: VimState): Promise { + let actionJoin = new ActionJoin(); + let start = Position.FromVSCodePosition(vimState.editor.selection.start); + let end = Position.FromVSCodePosition(vimState.editor.selection.end); + + if (start.isAfter(end)) { + [start, end] = [end, start]; + } + + /** + * For joining lines, Visual Line behaves the same as Visual so we align the register mode here. + */ + vimState.currentRegisterMode = RegisterMode.CharacterWise; + vimState = await actionJoin.execJoinLines(start, end, vimState, 1); + + return vimState; + } +} + +@RegisterAction +class ActionJoinNoWhitespace extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['g', 'J']; + canBeRepeatedWithDot = true; + runsOnceForEachCountPrefix = true; + + // gJ is essentially J without the edge cases. ;-) + + public async exec(position: Position, vimState: VimState): Promise { + if (position.line === TextEditor.getLineCount() - 1) { + return vimState; // TODO: bell + } + + let lineOne = TextEditor.getLineAt(position).text; + let lineTwo = TextEditor.getLineAt(position.getNextLineBegin()).text; + + lineTwo = lineTwo.substring(position.getNextLineBegin().getFirstLineNonBlankChar().character); + + let resultLine = lineOne + lineTwo; + + let newState = await new operator.DeleteOperator(this.multicursorIndex).run( + vimState, + position.getLineBegin(), + lineTwo.length > 0 + ? position + .getNextLineBegin() + .getLineEnd() + .getLeft() + : position.getLineEnd() + ); + + vimState.recordedState.transformations.push({ + type: 'insertText', + text: resultLine, + position: position, + diff: new PositionDiff(0, -lineTwo.length), + }); + + newState.cursorPosition = new Position(position.line, lineOne.length); + + return newState; + } +} + +@RegisterAction +class ActionJoinNoWhitespaceVisualMode extends BaseCommand { + modes = [ModeName.Visual]; + keys = ['g', 'J']; + + public async exec(position: Position, vimState: VimState): Promise { + let actionJoin = new ActionJoinNoWhitespace(); + let start = Position.FromVSCodePosition(vimState.editor.selection.start); + let end = Position.FromVSCodePosition(vimState.editor.selection.end); + + if (start.line === end.line) { + return vimState; + } + + if (start.isAfter(end)) { + [start, end] = [end, start]; + } + + for (let i = start.line; i < end.line; i++) { + vimState = await actionJoin.exec(start, vimState); + } + + return vimState; + } +} + +@RegisterAction +class ActionReplaceCharacter extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['r', '']; + canBeRepeatedWithDot = true; + runsOnceForEachCountPrefix = false; + + public async exec(position: Position, vimState: VimState): Promise { + let 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 (['', ''].indexOf(toReplace.toUpperCase()) >= 0) { + return vimState; + } + + if (position.character + timesToRepeat > position.getLineEnd().character) { + return vimState; + } + + let endPos = new Position(position.line, position.character + timesToRepeat); + + // Return if tried to repeat longer than linelength + if (endPos.character > TextEditor.getLineAt(endPos).text.length) { + return vimState; + } + + // If last char (not EOL char), add 1 so that replace selection is complete + if (endPos.character > TextEditor.getLineAt(endPos).text.length) { + endPos = new Position(endPos.line, endPos.character + 1); + } + + if (toReplace === '') { + vimState.recordedState.transformations.push({ + type: 'deleteRange', + range: new Range(position, endPos), + }); + vimState.recordedState.transformations.push({ + type: 'tab', + cursorIndex: this.multicursorIndex, + diff: new PositionDiff(0, -1), + }); + } else { + vimState.recordedState.transformations.push({ + type: 'replaceText', + text: toReplace.repeat(timesToRepeat), + start: position, + end: endPos, + diff: new PositionDiff(0, timesToRepeat - 1), + }); + } + return vimState; + } + + public async execCount(position: Position, vimState: VimState): Promise { + return super.execCount(position, vimState); + } +} + +@RegisterAction +class ActionReplaceCharacterVisual extends BaseCommand { + modes = [ModeName.Visual, ModeName.VisualLine]; + keys = ['r', '']; + runsOnceForEveryCursor() { + return false; + } + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + const toInsert = this.keysPressed[1]; + + let visualSelectionOffset = 1; + let start = vimState.cursorStartPosition; + let end = vimState.cursorPosition; + + // If selection is reversed, reorganize it so that the text replace logic always works + if (end.isBeforeOrEqual(start)) { + [start, end] = [end, start]; + } + + // Limit to not replace EOL + const textLength = TextEditor.getLineAt(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 (var lineNum = start.line; lineNum <= end.line; lineNum++) { + // Get line of text + const lineText = TextEditor.getLineAt(new Position(lineNum, 0)).text; + + if (start.line === end.line) { + // This is a visual section all on one line, only replace the part within the selection + vimState.recordedState.transformations.push({ + type: 'replaceText', + text: Array(end.character - start.character + 2).join(toInsert), + start: start, + end: 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.transformations.push({ + type: 'replaceText', + text: Array(lineText.length - start.character + 1).join(toInsert), + start: start, + end: 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.transformations.push({ + type: 'replaceText', + text: Array(end.character + 1 + visualSelectionOffset).join(toInsert), + start: new Position(end.line, 0), + end: 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.transformations.push({ + type: 'replaceText', + text: Array(lineText.length + 1).join(toInsert), + start: new Position(lineNum, 0), + end: new Position(lineNum, lineText.length), + manuallySetCursorPositions: true, + }); + } + } + + vimState.cursorPosition = start; + vimState.cursorStartPosition = start; + vimState.currentMode = ModeName.Normal; + return vimState; + } +} + +@RegisterAction +class ActionReplaceCharacterVisualBlock extends BaseCommand { + modes = [ModeName.VisualBlock]; + keys = ['r', '']; + runsOnceForEveryCursor() { + return false; + } + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + const toInsert = this.keysPressed[1]; + for (const { start, end } of Position.IterateLine(vimState)) { + if (end.isBeforeOrEqual(start)) { + continue; + } + + vimState.recordedState.transformations.push({ + type: 'replaceText', + text: Array(end.character - start.character + 1).join(toInsert), + start: start, + end: end, + manuallySetCursorPositions: true, + }); + } + + const topLeft = VisualBlockMode.getTopLeftPosition( + vimState.cursorPosition, + vimState.cursorStartPosition + ); + vimState.allCursors = [new Range(topLeft, topLeft)]; + vimState.currentMode = ModeName.Normal; + + return vimState; + } +} + +@RegisterAction +class ActionXVisualBlock extends BaseCommand { + modes = [ModeName.VisualBlock]; + keys = ['x']; + canBeRepeatedWithDot = true; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + for (const { start, end } of Position.IterateLine(vimState)) { + vimState.recordedState.transformations.push({ + type: 'deleteRange', + range: new Range(start, end), + manuallySetCursorPositions: true, + }); + } + + const topLeft = VisualBlockMode.getTopLeftPosition( + vimState.cursorPosition, + vimState.cursorStartPosition + ); + + vimState.allCursors = [new Range(topLeft, topLeft)]; + vimState.currentMode = ModeName.Normal; + + return vimState; + } +} + +@RegisterAction +class ActionDVisualBlock extends ActionXVisualBlock { + modes = [ModeName.VisualBlock]; + keys = ['d']; + canBeRepeatedWithDot = true; + runsOnceForEveryCursor() { + return false; + } +} + +@RegisterAction +class ActionShiftDVisualBlock extends BaseCommand { + modes = [ModeName.VisualBlock]; + keys = ['D']; + canBeRepeatedWithDot = true; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + for (const { start } of Position.IterateLine(vimState)) { + vimState.recordedState.transformations.push({ + type: 'deleteRange', + range: new Range(start, start.getLineEnd()), + manuallySetCursorPositions: true, + }); + } + + const topLeft = VisualBlockMode.getTopLeftPosition( + vimState.cursorPosition, + vimState.cursorStartPosition + ); + + vimState.allCursors = [new Range(topLeft, topLeft)]; + vimState.currentMode = ModeName.Normal; + + return vimState; + } +} + +@RegisterAction +class ActionGoToInsertVisualBlockMode extends BaseCommand { + modes = [ModeName.VisualBlock]; + keys = ['I']; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Insert; + vimState.isMultiCursor = true; + vimState.isFakeMultiCursor = true; + + for (const { line, start } of Position.IterateLine(vimState)) { + if (line === '' && start.character !== 0) { + continue; + } + vimState.allCursors.push(new Range(start, start)); + } + vimState.allCursors = vimState.allCursors.slice(1); + return vimState; + } +} + +@RegisterAction +class ActionChangeInVisualBlockMode extends BaseCommand { + modes = [ModeName.VisualBlock]; + keys = [['c'], ['s']]; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + for (const { start, end } of Position.IterateLine(vimState)) { + vimState.recordedState.transformations.push({ + type: 'deleteRange', + range: new Range(start, end), + manuallySetCursorPositions: true, + }); + } + + vimState.currentMode = ModeName.Insert; + vimState.isMultiCursor = true; + vimState.isFakeMultiCursor = true; + + for (const { start } of Position.IterateLine(vimState)) { + vimState.allCursors.push(new Range(start, start)); + } + vimState.allCursors = vimState.allCursors.slice(1); + + return vimState; + } +} + +@RegisterAction +class ActionChangeToEOLInVisualBlockMode extends BaseCommand { + modes = [ModeName.VisualBlock]; + keys = [['C'], ['S']]; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + for (const { start } of Position.IterateLine(vimState)) { + vimState.recordedState.transformations.push({ + type: 'deleteRange', + range: new Range(start, start.getLineEnd()), + collapseRange: true, + }); + } + + vimState.currentMode = ModeName.Insert; + vimState.isMultiCursor = true; + vimState.isFakeMultiCursor = true; + + for (const { end } of Position.IterateLine(vimState)) { + vimState.allCursors.push(new Range(end, end)); + } + vimState.allCursors = vimState.allCursors.slice(1); + + return vimState; + } +} + +@RegisterAction +class ActionGoToInsertVisualBlockModeAppend extends BaseCommand { + modes = [ModeName.VisualBlock]; + keys = ['A']; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + vimState.currentMode = ModeName.Insert; + vimState.isMultiCursor = true; + vimState.isFakeMultiCursor = true; + + for (const { line, end } of Position.IterateLine(vimState)) { + if (line.trim() === '') { + vimState.recordedState.transformations.push({ + type: 'replaceText', + text: TextEditor.setIndentationLevel(line, end.character), + start: new Position(end.line, 0), + end: new Position(end.line, end.character), + }); + } + vimState.allCursors.push(new Range(end, end)); + } + vimState.allCursors = vimState.allCursors.slice(1); + return vimState; + } +} + +@RegisterAction +class ActionDeleteLineVisualMode extends BaseCommand { + modes = [ModeName.Visual, ModeName.VisualLine]; + keys = ['X']; + + public async exec(position: Position, vimState: VimState): Promise { + if (vimState.currentMode === ModeName.Visual) { + return await new operator.DeleteOperator(this.multicursorIndex).run( + vimState, + vimState.cursorStartPosition.getLineBegin(), + vimState.cursorPosition.getLineEnd() + ); + } else { + return await new operator.DeleteOperator(this.multicursorIndex).run( + vimState, + position.getLineBegin(), + position.getLineEnd() + ); + } + } +} + +@RegisterAction +class ActionChangeLineVisualMode extends BaseCommand { + modes = [ModeName.Visual]; + keys = ['C']; + + public async exec(position: Position, vimState: VimState): Promise { + return await new operator.DeleteOperator(this.multicursorIndex).run( + vimState, + vimState.cursorStartPosition.getLineBegin(), + vimState.cursorPosition.getLineEndIncludingEOL() + ); + } +} + +@RegisterAction +class ActionRemoveLineVisualMode extends BaseCommand { + modes = [ModeName.Visual]; + keys = ['R']; + + public async exec(position: Position, vimState: VimState): Promise { + return await new operator.DeleteOperator(this.multicursorIndex).run( + vimState, + vimState.cursorStartPosition.getLineBegin(), + vimState.cursorPosition.getLineEndIncludingEOL() + ); + } +} + +@RegisterAction +class ActionChangeChar extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['s']; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + const state = await new operator.ChangeOperator().run(vimState, position, position); + + state.currentMode = ModeName.Insert; + + return state; + } + + // Don't clash with surround mode! + public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { + return super.doesActionApply(vimState, keysPressed) && !vimState.recordedState.operator; + } + + public couldActionApply(vimState: VimState, keysPressed: string[]): boolean { + return super.doesActionApply(vimState, keysPressed) && !vimState.recordedState.operator; + } +} + +@RegisterAction +class ToggleCaseAndMoveForward extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['~']; + canBeRepeatedWithDot = true; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + await new operator.ToggleCaseOperator().run( + vimState, + vimState.cursorPosition, + vimState.cursorPosition + ); + + vimState.cursorPosition = vimState.cursorPosition.getRight(); + return vimState; + } +} + +abstract class IncrementDecrementNumberAction extends BaseCommand { + modes = [ModeName.Normal]; + canBeRepeatedWithDot = true; + offset: number; + + public async exec(position: Position, vimState: VimState): Promise { + const text = TextEditor.getLineAt(position).text; + + // 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.getWordLeft(true); + + for (let { start, end, word } of Position.IterateWords(whereToStart)) { + // '-' doesn't count as a word, but is important to include in parsing + // the number + if (text[start.character - 1] === '-') { + start = start.getLeft(); + word = text[start.character] + word; + } + // Strict number parsing so "1a" doesn't silently get converted to "1" + do { + const num = NumericString.parse(word); + if ( + num !== null && + position.character < start.character + num.prefix.length + num.value.toString().length + ) { + vimState.cursorPosition = await this.replaceNum( + num, + this.offset * (vimState.recordedState.count || 1), + start, + end + ); + vimState.cursorPosition = vimState.cursorPosition.getLeftByCount(num.suffix.length); + return vimState; + } else if (num !== null) { + word = word.slice(num.prefix.length + num.value.toString().length); + start = new Position( + start.line, + start.character + num.prefix.length + num.value.toString().length + ); + } else { + break; + } + } while (true); + } + // No usable numbers, return the original position + return vimState; + } + + public async replaceNum( + start: NumericString, + offset: number, + startPos: Position, + endPos: Position + ): Promise { + const oldWidth = start.toString().length; + start.value += offset; + const newNum = start.toString(); + + const range = new vscode.Range(startPos, endPos.getRight()); + + if (oldWidth === newNum.length) { + await TextEditor.replace(range, newNum); + } else { + // Can't use replace, since new number is a different width than old + await TextEditor.delete(range); + await TextEditor.insertAt(newNum, startPos); + // Adjust end position according to difference in width of number-string + endPos = new Position(endPos.line, endPos.character + (newNum.length - oldWidth)); + } + + return endPos; + } +} + +@RegisterAction +class IncrementNumberAction extends IncrementDecrementNumberAction { + keys = ['']; + offset = +1; +} + +@RegisterAction +class DecrementNumberAction extends IncrementDecrementNumberAction { + keys = ['']; + offset = -1; +} + +@RegisterAction +class ActionTriggerHover extends BaseCommand { + modes = [ModeName.Normal]; + keys = ['g', 'h']; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + await vscode.commands.executeCommand('editor.action.showHover'); + + return vimState; + } +} + +/** + * 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 +class ActionOverrideCmdD extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual]; + keys = [[''], ['g', 'b']]; + runsOnceForEveryCursor() { + return false; + } + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + await vscode.commands.executeCommand('editor.action.addSelectionToNextFindMatch'); + vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); + + // If this is the first cursor, select 1 character less + // so that only the word is selected, no extra character + vimState.allCursors = vimState.allCursors.map(x => x.withNewStop(x.stop.getLeft())); + + vimState.currentMode = ModeName.Visual; + + return vimState; + } +} + +@RegisterAction +class ActionOverrideCmdDInsert extends BaseCommand { + modes = [ModeName.Insert]; + keys = ['']; + runsOnceForEveryCursor() { + return false; + } + runsOnceForEachCountPrefix = true; + + public 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. + vscode.window.activeTextEditor!.selections = vscode.window + .activeTextEditor!.selections.map((x, idx) => { + const curPos = Position.FromVSCodePosition(x.active); + if (idx === 0) { + return new vscode.Selection( + curPos.getWordLeft(false), + curPos + .getLeft() + .getCurrentWordEnd(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 = Position.FromVSCodePosition( + vscode.window.activeTextEditor!.selections[0].active + ); + const matchWordLength = + matchWordPos + .getLeft() + .getCurrentWordEnd(true) + .getRight().character - matchWordPos.getWordLeft(false).character; + const wordBegin = curPos.getLeftByCount(matchWordLength); + return new vscode.Selection(wordBegin, curPos); + } + }); + await vscode.commands.executeCommand('editor.action.addSelectionToNextFindMatch'); + vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); + return vimState; + } +} + +@RegisterAction +class ActionOverrideCmdAltDown extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual]; + keys = [ + [''], // OSX + [''], // Windows + ]; + runsOnceForEveryCursor() { + return false; + } + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + await vscode.commands.executeCommand('editor.action.insertCursorBelow'); + vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); + + return vimState; + } +} + +@RegisterAction +class ActionOverrideCmdAltUp extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual]; + keys = [ + [''], // OSX + [''], // Windows + ]; + runsOnceForEveryCursor() { + return false; + } + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + await vscode.commands.executeCommand('editor.action.insertCursorAbove'); + vimState.allCursors = await allowVSCodeToPropagateCursorUpdatesAndReturnThem(); + + return vimState; + } +} diff --git a/src/actions/motion.ts b/src/actions/motion.ts index 726ba05f01c..d842625e224 100644 --- a/src/actions/motion.ts +++ b/src/actions/motion.ts @@ -1,1870 +1,1870 @@ -import * as vscode from 'vscode'; -import { ModeName } from './../mode/mode'; -import { Position, PositionDiff } from './../common/motion/position'; -import { Configuration } from './../configuration/configuration'; -import { TextEditor, CursorMovePosition, CursorMoveByUnit } from './../textEditor'; -import { VimState } from './../mode/modeHandler'; -import { RegisterMode } from './../register/register'; -import { PairMatcher } from './../common/matching/matcher'; -import { ReplaceState } from './../state/replaceState'; -import { QuoteMatcher } from './../common/matching/quoteMatcher'; -import { TagMatcher } from './../common/matching/tagMatcher'; -import { RegisterAction } from './base'; -import { ChangeOperator } from './operator'; -import { BaseAction } from './base'; - -export function isIMovement(o: IMovement | Position): o is IMovement { - return (o as IMovement).start !== undefined && (o as IMovement).stop !== undefined; -} - -/** - * 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; - - diff?: PositionDiff; - - // It /so/ annoys me that I have to put this here. - registerMode?: RegisterMode; -} - -/** - * A movement is something like 'h', 'k', 'w', 'b', 'gg', etc. - */ -export abstract class BaseMovement extends BaseAction { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - - isMotion = true; - - canBePrefixedWithCount = false; - - /** - * Whether we should change lastRepeatableMovement in VimState. - */ - public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { - return false; - } - - /** - * Whether we should change desiredColumn in VimState. - */ - public doesntChangeDesiredColumn = false; - - /** - * This is for commands like $ which force the desired column to be at - * the end of even the longest line. - */ - public setsDesiredColumnToEOL = false; - - /** - * 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): 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. - */ - public async execActionForOperator( - position: Position, - vimState: VimState - ): Promise { - return await this.execAction(position, vimState); - } - - /** - * 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 recordedState = vimState.recordedState; - let result: Position | IMovement = new Position(0, 0); // bogus init to satisfy typechecker - - if (count < 1) { - count = 1; - } else if (count > 99999) { - count = 99999; - } - - for (let i = 0; i < count; i++) { - const firstIteration = i === 0; - const lastIteration = i === count - 1; - const temporaryResult = - recordedState.operator && lastIteration - ? await this.execActionForOperator(position, vimState) - : await this.execAction(position, vimState); - - if (temporaryResult instanceof Position) { - result = temporaryResult; - position = temporaryResult; - } else if (isIMovement(temporaryResult)) { - if (result instanceof Position) { - result = { - start: new Position(0, 0), - stop: new Position(0, 0), - failed: false, - }; - } - - result.failed = result.failed || temporaryResult.failed; - - if (firstIteration) { - (result as IMovement).start = temporaryResult.start; - } - - if (lastIteration) { - (result as IMovement).stop = temporaryResult.stop; - } else { - position = temporaryResult.stop.getRightThroughLineBreaks(); - } - - result.registerMode = temporaryResult.registerMode; - } - } - - return result; - } -} - -abstract class MoveByScreenLine extends BaseMovement { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - movementType: CursorMovePosition; - by: CursorMoveByUnit; - value: number = 1; - - public async execAction(position: Position, vimState: VimState): Promise { - await vscode.commands.executeCommand('cursorMove', { - to: this.movementType, - select: vimState.currentMode !== ModeName.Normal, - by: this.by, - value: this.value, - }); - - if (vimState.currentMode === ModeName.Normal) { - return Position.FromVSCodePosition(vimState.editor.selection.active); - } 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 = Position.FromVSCodePosition(vimState.editor.selection.start); - let stop = Position.FromVSCodePosition(vimState.editor.selection.end); - let curPos = Position.FromVSCodePosition(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)) { - position = start; - [start, stop] = [stop, start]; - start = start.getLeft(); - } - - return { start, stop }; - } - } - - public async execActionForOperator(position: Position, vimState: VimState): Promise { - await vscode.commands.executeCommand('cursorMove', { - to: this.movementType, - select: true, - by: this.by, - value: this.value, - }); - - return { - start: Position.FromVSCodePosition(vimState.editor.selection.start), - stop: Position.FromVSCodePosition(vimState.editor.selection.end), - }; - } -} - -abstract class MoveByScreenLineMaintainDesiredColumn extends MoveByScreenLine { - doesntChangeDesiredColumn = true; - public async execAction(position: Position, vimState: VimState): Promise { - let prevDesiredColumn = vimState.desiredColumn; - let prevLine = vimState.editor.selection.active.line; - - await vscode.commands.executeCommand('cursorMove', { - to: this.movementType, - select: vimState.currentMode !== ModeName.Normal, - by: this.by, - value: this.value, - }); - - if (vimState.currentMode === ModeName.Normal) { - let returnedPos = Position.FromVSCodePosition(vimState.editor.selection.active); - if (prevLine !== returnedPos.line) { - returnedPos = returnedPos.setLocation(returnedPos.line, 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 = Position.FromVSCodePosition(vimState.editor.selection.start); - let stop = Position.FromVSCodePosition(vimState.editor.selection.end); - let curPos = Position.FromVSCodePosition(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)) { - position = start; - [start, stop] = [stop, start]; - start = start.getLeft(); - } - - return { start, stop }; - } - } -} - -class MoveDownByScreenLineMaintainDesiredColumn extends MoveByScreenLineMaintainDesiredColumn { - movementType: CursorMovePosition = 'down'; - by: CursorMoveByUnit = 'wrappedLine'; - value = 1; -} - -class MoveDownFoldFix extends MoveByScreenLineMaintainDesiredColumn { - movementType: CursorMovePosition = 'down'; - by: CursorMoveByUnit = 'line'; - value = 1; - - public async execAction(position: Position, vimState: VimState): Promise { - if (position.line === TextEditor.getLineCount() - 1) { - return position; - } - let t: Position; - let count = 0; - const prevDesiredColumn = vimState.desiredColumn; - do { - t = await new MoveDownByScreenLine().execAction(position, vimState); - count += 1; - } while (t.line === position.line); - if (t.line > position.line + 1) { - return t; - } - while (count > 0) { - t = await new MoveUpByScreenLine().execAction(position, vimState); - count--; - } - vimState.desiredColumn = prevDesiredColumn; - return await position.getDown(vimState.desiredColumn); - } -} - -@RegisterAction -class MoveDown extends BaseMovement { - keys = ['j']; - doesntChangeDesiredColumn = true; - - public async execAction(position: Position, vimState: VimState): Promise { - if (Configuration.foldfix && vimState.currentMode !== ModeName.VisualBlock) { - return new MoveDownFoldFix().execAction(position, vimState); - } - return position.getDown(vimState.desiredColumn); - } - - public async execActionForOperator(position: Position, vimState: VimState): Promise { - vimState.currentRegisterMode = RegisterMode.LineWise; - return position.getDown(position.getLineEnd().character); - } -} - -@RegisterAction -class MoveDownArrow extends MoveDown { - keys = ['']; -} - -class MoveUpByScreenLineMaintainDesiredColumn extends MoveByScreenLineMaintainDesiredColumn { - movementType: CursorMovePosition = 'up'; - by: CursorMoveByUnit = 'wrappedLine'; - value = 1; -} - -@RegisterAction -class MoveUp extends BaseMovement { - keys = ['k']; - doesntChangeDesiredColumn = true; - - public async execAction(position: Position, vimState: VimState): Promise { - if (Configuration.foldfix && vimState.currentMode !== ModeName.VisualBlock) { - return new MoveUpFoldFix().execAction(position, vimState); - } - return position.getUp(vimState.desiredColumn); - } - - public async execActionForOperator(position: Position, vimState: VimState): Promise { - vimState.currentRegisterMode = RegisterMode.LineWise; - return position.getUp(position.getLineEnd().character); - } -} - -@RegisterAction -class MoveUpFoldFix extends MoveByScreenLineMaintainDesiredColumn { - movementType: CursorMovePosition = 'up'; - by: CursorMoveByUnit = 'line'; - value = 1; - - public async execAction(position: Position, vimState: VimState): Promise { - if (position.line === 0) { - return position; - } - let t: Position; - const prevDesiredColumn = vimState.desiredColumn; - let count = 0; - - do { - t = await new MoveUpByScreenLineMaintainDesiredColumn().execAction( - position, - vimState - ); - count += 1; - } while (t.line === position.line); - vimState.desiredColumn = prevDesiredColumn; - if (t.line < position.line - 1) { - return t; - } - while (count > 0) { - t = await new MoveDownByScreenLine().execAction(position, vimState); - count--; - } - vimState.desiredColumn = prevDesiredColumn; - return await position.getUp(vimState.desiredColumn); - } -} - -@RegisterAction -class MoveUpArrow extends MoveUp { - keys = ['']; -} - -@RegisterAction -class ArrowsInReplaceMode extends BaseMovement { - modes = [ModeName.Replace]; - keys = [[''], [''], [''], ['']]; - - public async execAction(position: Position, vimState: VimState): Promise { - let newPosition: Position = position; - - switch (this.keysPressed[0]) { - case '': - newPosition = await new MoveUpArrow().execAction(position, vimState); - break; - case '': - newPosition = await new MoveDownArrow().execAction(position, vimState); - break; - case '': - newPosition = await new MoveLeftArrow().execAction(position, vimState); - break; - case '': - newPosition = await new MoveRightArrow().execAction(position, vimState); - break; - default: - break; - } - vimState.replaceState = new ReplaceState(newPosition); - return newPosition; - } -} - -@RegisterAction -class UpArrowInReplaceMode extends ArrowsInReplaceMode { - keys = [['']]; -} - -@RegisterAction -class DownArrowInReplaceMode extends ArrowsInReplaceMode { - keys = [['']]; -} - -@RegisterAction -class LeftArrowInReplaceMode extends ArrowsInReplaceMode { - keys = [['']]; -} - -@RegisterAction -class RightArrowInReplaceMode extends ArrowsInReplaceMode { - keys = [['']]; -} - -@RegisterAction -class CommandNextSearchMatch extends BaseMovement { - keys = ['n']; - - public async execAction(position: Position, vimState: VimState): Promise { - const searchState = vimState.globalState.searchState; - - if (!searchState || searchState.searchString === '') { - return position; - } - // Turn one of the highlighting flags back on (turned off with :nohl) - vimState.globalState.hl = true; - - if (vimState.cursorPosition.getRight().isEqual(vimState.cursorPosition.getLineEnd())) { - return searchState.getNextSearchMatchPosition(vimState.cursorPosition.getRight()).pos; - } - - // Turn one of the highlighting flags back on (turned off with :nohl) - - return searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; - } -} - -@RegisterAction -class CommandPreviousSearchMatch extends BaseMovement { - keys = ['N']; - - public async execAction(position: Position, vimState: VimState): Promise { - const searchState = vimState.globalState.searchState; - - if (!searchState || searchState.searchString === '') { - return position; - } - - // Turn one of the highlighting flags back on (turned off with :nohl) - vimState.globalState.hl = true; - - return searchState.getNextSearchMatchPosition(vimState.cursorPosition, -1).pos; - } -} - -@RegisterAction -export class MarkMovementBOL extends BaseMovement { - keys = ["'", '']; - - public async execAction(position: Position, vimState: VimState): Promise { - const markName = this.keysPressed[1]; - const mark = vimState.historyTracker.getMark(markName); - - vimState.currentRegisterMode = RegisterMode.LineWise; - - return mark.position.getFirstLineNonBlankChar(); - } -} - -@RegisterAction -export class MarkMovement extends BaseMovement { - keys = ['`', '']; - - public async execAction(position: Position, vimState: VimState): Promise { - const markName = this.keysPressed[1]; - const mark = vimState.historyTracker.getMark(markName); - - return mark.position; - } -} - -@RegisterAction -export class MoveLeft extends BaseMovement { - keys = ['h']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getLeft(); - } -} - -@RegisterAction -class MoveLeftArrow extends MoveLeft { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['']; -} - -@RegisterAction -class BackSpaceInNormalMode extends BaseMovement { - modes = [ModeName.Normal]; - keys = ['']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getLeftThroughLineBreaks(); - } -} - -@RegisterAction -class MoveRight extends BaseMovement { - keys = ['l']; - - public async execAction(position: Position, vimState: VimState): Promise { - return new Position(position.line, position.character + 1); - } -} - -@RegisterAction -class MoveRightArrow extends MoveRight { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - keys = ['']; -} - -@RegisterAction -class MoveRightWithSpace extends BaseMovement { - keys = [' ']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getRightThroughLineBreaks(); - } -} - -@RegisterAction -class MoveDownNonBlank extends BaseMovement { - keys = ['+']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - return position.getDownByCount(Math.max(count, 1)).getFirstLineNonBlankChar(); - } -} - -@RegisterAction -class MoveUpNonBlank extends BaseMovement { - keys = ['-']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - return position.getUpByCount(Math.max(count, 1)).getFirstLineNonBlankChar(); - } -} - -@RegisterAction -class MoveDownUnderscore extends BaseMovement { - keys = ['_']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - return position.getDownByCount(Math.max(count - 1, 0)).getFirstLineNonBlankChar(); - } -} - -@RegisterAction -class MoveToColumn extends BaseMovement { - keys = ['|']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - return new Position(position.line, Math.max(0, count - 1)); - } -} - -@RegisterAction -class MoveFindForward extends BaseMovement { - keys = ['f', '']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - count = count || 1; - const toFind = this.keysPressed[1]; - let result = position.findForwards(toFind, count); - - if (!result) { - return { start: position, stop: position, failed: true }; - } - - if (vimState.recordedState.operator) { - result = result.getRight(); - } - - return result; - } - - public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { - return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); - } -} - -@RegisterAction -class MoveFindBackward extends BaseMovement { - keys = ['F', '']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - count = count || 1; - const toFind = this.keysPressed[1]; - let result = position.findBackwards(toFind, count); - - if (!result) { - return { start: position, stop: position, failed: true }; - } - - return result; - } - - public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { - return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); - } -} - -@RegisterAction -class MoveTilForward extends BaseMovement { - keys = ['t', '']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - count = count || 1; - const toFind = this.keysPressed[1]; - let result = position.tilForwards(toFind, count); - - if (!result) { - return { start: position, stop: position, failed: true }; - } - - if (vimState.recordedState.operator) { - result = result.getRight(); - } - - return result; - } - - public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { - return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); - } -} - -@RegisterAction -class MoveTilBackward extends BaseMovement { - keys = ['T', '']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - count = count || 1; - const toFind = this.keysPressed[1]; - let result = position.tilBackwards(toFind, count); - - if (!result) { - return { start: position, stop: position, failed: true }; - } - - return result; - } - - public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { - return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); - } -} - -@RegisterAction -class MoveRepeat extends BaseMovement { - keys = [';']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - const movement = VimState.lastRepeatableMovement; - if (movement) { - const result = await movement.execActionWithCount(position, vimState, count); - /** - * For t and T commands vim executes ; as 2; - * This way the cursor will get to the next instance of - */ - if (result instanceof Position && position.isEqual(result) && count <= 1) { - return await movement.execActionWithCount(position, vimState, 2); - } - return result; - } - return position; - } -} - -@RegisterAction -class MoveRepeatReversed extends BaseMovement { - keys = [',']; - static reverseMotionMapping: Map BaseMovement> = new Map([ - [MoveFindForward, () => new MoveFindBackward()], - [MoveFindBackward, () => new MoveFindForward()], - [MoveTilForward, () => new MoveTilBackward()], - [MoveTilBackward, () => new MoveTilForward()], - ]); - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - const movement = VimState.lastRepeatableMovement; - if (movement) { - const reverse = MoveRepeatReversed.reverseMotionMapping.get(movement.constructor)!(); - reverse.keysPressed = [(reverse.keys as string[])[0], movement.keysPressed[1]]; - - let result = await reverse.execActionWithCount(position, vimState, count); - // For t and T commands vim executes ; as 2; - if (result instanceof Position && position.isEqual(result) && count <= 1) { - result = await reverse.execActionWithCount(position, vimState, 2); - } - return result; - } - return position; - } -} - -@RegisterAction -class MoveLineEnd extends BaseMovement { - keys = [['$'], [''], ['']]; - setsDesiredColumnToEOL = true; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - return position.getDownByCount(Math.max(count - 1, 0)).getLineEnd(); - } -} - -@RegisterAction -class MoveLineBegin extends BaseMovement { - keys = [['0'], [''], ['']]; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getLineBegin(); - } - - public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { - return super.doesActionApply(vimState, keysPressed) && vimState.recordedState.count === 0; - } - - public 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'; - canBePrefixedWithCount = true; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - count = count || 1; - const pos = await this.execAction(position, vimState); - const newPos: Position | IMovement = pos as Position; - - // If in visual, return a selection - if (pos instanceof Position) { - return pos.getDownByCount(count - 1); - } else if (isIMovement(pos)) { - return { start: pos.start, stop: pos.stop.getDownByCount(count - 1).getLeft() }; - } - - return newPos.getDownByCount(count - 1); - } -} - -@RegisterAction -class MoveScreenLineCenter extends MoveByScreenLine { - keys = ['g', 'm']; - movementType: CursorMovePosition = 'wrappedLineColumnCenter'; -} - -@RegisterAction -export class MoveUpByScreenLine extends MoveByScreenLine { - modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual]; - keys = [['g', 'k'], ['g', '']]; - movementType: CursorMovePosition = 'up'; - by: CursorMoveByUnit = 'wrappedLine'; - value = 1; -} - -@RegisterAction -class MoveDownByScreenLine extends MoveByScreenLine { - modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual]; - keys = [['g', 'j'], ['g', '']]; - movementType: CursorMovePosition = 'down'; - by: CursorMoveByUnit = 'wrappedLine'; - 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 { - modes = [ModeName.VisualLine]; - keys = [['g', 'k'], ['g', '']]; - movementType: CursorMovePosition = 'up'; - by: CursorMoveByUnit = 'line'; - value = 1; -} - -@RegisterAction -class MoveDownByScreenLineVisualLine extends MoveByScreenLine { - modes = [ModeName.VisualLine]; - keys = [['g', 'j'], ['g', '']]; - movementType: CursorMovePosition = 'down'; - by: CursorMoveByUnit = 'line'; - value = 1; -} - -@RegisterAction -class MoveUpByScreenLineVisualBlock extends BaseMovement { - modes = [ModeName.VisualBlock]; - keys = [['g', 'k'], ['g', '']]; - doesntChangeDesiredColumn = true; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getUp(vimState.desiredColumn); - } - - public async execActionForOperator(position: Position, vimState: VimState): Promise { - vimState.currentRegisterMode = RegisterMode.LineWise; - return position.getUp(position.getLineEnd().character); - } -} - -@RegisterAction -class MoveDownByScreenLineVisualBlock extends BaseMovement { - modes = [ModeName.VisualBlock]; - keys = [['g', 'j'], ['g', '']]; - doesntChangeDesiredColumn = true; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getDown(vimState.desiredColumn); - } - - public async execActionForOperator(position: Position, vimState: VimState): Promise { - vimState.currentRegisterMode = RegisterMode.LineWise; - return position.getDown(position.getLineEnd().character); - } -} - -@RegisterAction -class MoveScreenToRight extends MoveByScreenLine { - modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['z', 'h']; - movementType: CursorMovePosition = 'right'; - by: CursorMoveByUnit = 'character'; - value = 1; -} - -@RegisterAction -class MoveScreenToLeft extends MoveByScreenLine { - modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['z', 'l']; - movementType: CursorMovePosition = 'left'; - by: CursorMoveByUnit = 'character'; - value = 1; -} - -@RegisterAction -class MoveScreenToRightHalf extends MoveByScreenLine { - modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['z', 'H']; - movementType: CursorMovePosition = 'right'; - by: CursorMoveByUnit = 'halfLine'; - value = 1; -} - -@RegisterAction -class MoveScreenToLeftHalf extends MoveByScreenLine { - modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ['z', 'L']; - movementType: CursorMovePosition = 'left'; - by: CursorMoveByUnit = 'halfLine'; - value = 1; -} - -@RegisterAction -class MoveToLineFromViewPortTop extends MoveByScreenLine { - keys = ['H']; - movementType: CursorMovePosition = 'viewPortTop'; - by: CursorMoveByUnit = 'line'; - value = 1; - canBePrefixedWithCount = true; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - this.value = count < 1 ? 1 : count; - return await this.execAction(position, vimState); - } -} - -@RegisterAction -class MoveToLineFromViewPortBottom extends MoveByScreenLine { - keys = ['L']; - movementType: CursorMovePosition = 'viewPortBottom'; - by: CursorMoveByUnit = 'line'; - value = 1; - canBePrefixedWithCount = true; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - this.value = count < 1 ? 1 : count; - return await this.execAction(position, vimState); - } -} - -@RegisterAction -class MoveToMiddleLineInViewPort extends MoveByScreenLine { - keys = ['M']; - movementType: CursorMovePosition = 'viewPortCenter'; - by: CursorMoveByUnit = 'line'; -} - -@RegisterAction -class MoveNonBlank extends BaseMovement { - keys = ['^']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getFirstLineNonBlankChar(); - } -} - -@RegisterAction -class MoveNextLineNonBlank extends BaseMovement { - keys = ['\n']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - vimState.currentRegisterMode = RegisterMode.LineWise; - - // Count === 0 if just pressing enter in normal mode, need to still go down 1 line - if (count === 0) { - count++; - } - - return position.getDownByCount(count).getFirstLineNonBlankChar(); - } -} - -@RegisterAction -class MoveNonBlankFirst extends BaseMovement { - keys = ['g', 'g']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - if (count === 0) { - return position.getDocumentBegin().getFirstLineNonBlankChar(); - } - - return new Position(count - 1, 0).getFirstLineNonBlankChar(); - } -} - -@RegisterAction -class MoveNonBlankLast extends BaseMovement { - keys = ['G']; - - public async execActionWithCount( - position: Position, - vimState: VimState, - count: number - ): Promise { - let stop: Position; - - if (count === 0) { - stop = new Position(TextEditor.getLineCount() - 1, 0); - } else { - stop = new Position(Math.min(count, TextEditor.getLineCount()) - 1, 0); - } - - return { - start: vimState.cursorStartPosition, - stop: stop, - registerMode: RegisterMode.LineWise, - }; - } -} - -@RegisterAction -export class MoveWordBegin extends BaseMovement { - keys = ['w']; - - public async execAction( - position: Position, - vimState: VimState, - isLastIteration: boolean = false - ): Promise { - if (isLastIteration && vimState.recordedState.operator instanceof ChangeOperator) { - if (TextEditor.getLineAt(position).text.length < 1) { - return position; - } - - const line = TextEditor.getLineAt(position).text; - const char = line[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'.indexOf(char) >= 0) { - return position.getWordRight(); - } else { - return position.getCurrentWordEnd(true).getRight(); - } - } else { - return position.getWordRight(); - } - } - - public async execActionForOperator(position: Position, vimState: VimState): Promise { - const result = await this.execAction(position, vimState, true); - - /* - 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()) - ) { - return position.getLineEnd(); - } - - if (result.isLineEnd()) { - return new Position(result.line, result.character + 1); - } - - return result; - } -} - -@RegisterAction -class MoveFullWordBegin extends BaseMovement { - keys = ['W']; - - public async execAction(position: Position, vimState: VimState): Promise { - if (vimState.recordedState.operator instanceof ChangeOperator) { - // TODO use execForOperator? Or maybe dont? - - // See note for w - return position.getCurrentBigWordEnd().getRight(); - } else { - return position.getBigWordRight(); - } - } -} - -@RegisterAction -class MoveWordEnd extends BaseMovement { - keys = ['e']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getCurrentWordEnd(); - } - - public async execActionForOperator(position: Position, vimState: VimState): Promise { - let end = position.getCurrentWordEnd(); - - return new Position(end.line, end.character + 1); - } -} - -@RegisterAction -class MoveFullWordEnd extends BaseMovement { - keys = ['E']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getCurrentBigWordEnd(); - } - - public async execActionForOperator(position: Position, vimState: VimState): Promise { - return position.getCurrentBigWordEnd().getRight(); - } -} - -@RegisterAction -class MoveLastWordEnd extends BaseMovement { - keys = ['g', 'e']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getLastWordEnd(); - } -} - -@RegisterAction -class MoveLastFullWordEnd extends BaseMovement { - keys = ['g', 'E']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getLastBigWordEnd(); - } -} - -@RegisterAction -class MoveBeginningWord extends BaseMovement { - keys = ['b']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getWordLeft(); - } -} - -@RegisterAction -class MoveBeginningFullWord extends BaseMovement { - keys = ['B']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getBigWordLeft(); - } -} - -@RegisterAction -class MovePreviousSentenceBegin extends BaseMovement { - keys = ['(']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getSentenceBegin({ forward: false }); - } -} - -@RegisterAction -class MoveNextSentenceBegin extends BaseMovement { - keys = [')']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getSentenceBegin({ forward: true }); - } -} - -@RegisterAction -class MoveParagraphEnd extends BaseMovement { - keys = ['}']; - - public async execAction(position: Position, vimState: VimState): Promise { - const isLineWise = - position.isLineBeginning() && - vimState.currentMode === ModeName.Normal && - vimState.recordedState.operator; - let paragraphEnd = position.getCurrentParagraphEnd(); - vimState.currentRegisterMode = isLineWise - ? RegisterMode.LineWise - : RegisterMode.FigureItOutFromCurrentMode; - return isLineWise ? paragraphEnd.getLeftThroughLineBreaks(true) : paragraphEnd; - } -} - -@RegisterAction -class MoveParagraphBegin extends BaseMovement { - keys = ['{']; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getCurrentParagraphBeginning(); - } -} - -abstract class MoveSectionBoundary extends BaseMovement { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - boundary: string; - forward: boolean; - - public async execAction(position: Position, vimState: VimState): Promise { - return position.getSectionBoundary({ - forward: this.forward, - boundary: this.boundary, - }); - } -} - -@RegisterAction -class MoveNextSectionBegin extends MoveSectionBoundary { - keys = [']', ']']; - boundary = '{'; - forward = true; -} - -@RegisterAction -class MoveNextSectionEnd extends MoveSectionBoundary { - keys = [']', '[']; - boundary = '}'; - forward = true; -} - -@RegisterAction -class MovePreviousSectionBegin extends MoveSectionBoundary { - keys = ['[', '[']; - boundary = '{'; - forward = false; -} - -@RegisterAction -class MovePreviousSectionEnd extends MoveSectionBoundary { - keys = ['[', ']']; - boundary = '}'; - forward = false; -} - -@RegisterAction -class MoveToMatchingBracket extends BaseMovement { - keys = ['%']; - - public async execAction(position: Position, vimState: VimState): Promise { - position = position.getLeftIfEOL(); - - const text = TextEditor.getLineAt(position).text; - const charToMatch = text[position.character]; - const toFind = PairMatcher.pairings[charToMatch]; - const failure = { start: position, stop: position, failed: true }; - - if (!toFind || !toFind.matchesWithPercentageMotion) { - // If we're not on a match, go right until we find a - // pairable character or hit the end of line. - - for (let i = position.character; i < text.length; i++) { - if (PairMatcher.pairings[text[i]]) { - // We found an opening char, now move to the matching closing char - const openPosition = new Position(position.line, i); - return PairMatcher.nextPairedChar(openPosition, text[i], true) || failure; - } - } - - return failure; - } - - return PairMatcher.nextPairedChar(position, charToMatch, true) || failure; - } - - public 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.compareTo(result) > 0) { - return { - start: result, - stop: position.getRight(), - }; - } else { - return result.getRight(); - } - } - - public 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 { start: position, stop: position, failed: true }; - } - - const targetLine = Math.round(count * TextEditor.getLineCount() / 100); - return new Position(targetLine - 1, 0).getFirstLineNonBlankChar(); - } else { - return super.execActionWithCount(position, vimState, count); - } - } -} - -export abstract class MoveInsideCharacter extends BaseMovement { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; - protected charToMatch: string; - protected includeSurrounding = false; - - public async execAction(position: Position, vimState: VimState): Promise { - const failure = { start: position, stop: position, failed: true }; - const text = TextEditor.getLineAt(position).text; - const closingChar = PairMatcher.pairings[this.charToMatch].match; - const closedMatch = text[position.character] === closingChar; - - // First, search backwards for the opening character of the sequence - let startPos = PairMatcher.nextPairedChar(position, closingChar, closedMatch); - if (startPos === undefined) { - return failure; - } - - let startPlusOne: Position; - - if (startPos.isAfterOrEqual(startPos.getLineEnd().getLeft())) { - startPlusOne = new Position(startPos.line + 1, 0); - } else { - startPlusOne = new Position(startPos.line, startPos.character + 1); - } - - let endPos = PairMatcher.nextPairedChar(startPlusOne, this.charToMatch, false); - if (endPos === undefined) { - return failure; - } - - if (this.includeSurrounding) { - if (vimState.currentMode !== ModeName.Visual) { - endPos = new Position(endPos.line, endPos.character + 1); - } - } else { - startPos = startPlusOne; - if (vimState.currentMode === ModeName.Visual) { - endPos = endPos.getLeftThroughLineBreaks(); - } - } - - // If the closing character is the first on the line, don't swallow it. - if (!this.includeSurrounding) { - if (endPos.getLeft().isInLeadingWhitespace()) { - endPos = endPos.getLineBegin(); - if (vimState.currentMode === ModeName.Visual) { - endPos = endPos.getLeftThroughLineBreaks(); - } - } - } - - if (position.isBefore(startPos)) { - vimState.recordedState.operatorPositionDiff = startPos.subtract(position); - } - - return { - start: startPos, - stop: endPos, - diff: new PositionDiff(0, startPos === position ? 1 : 0), - }; - } - - public 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 = []; - } - } - return result; - } -} - -@RegisterAction -class MoveIParentheses extends MoveInsideCharacter { - keys = ['i', '(']; - charToMatch = '('; -} - -@RegisterAction -class MoveIClosingParentheses extends MoveInsideCharacter { - keys = ['i', ')']; - charToMatch = '('; -} - -@RegisterAction -class MoveIClosingParenthesesBlock extends MoveInsideCharacter { - keys = ['i', 'b']; - charToMatch = '('; -} - -@RegisterAction -export class MoveAParentheses extends MoveInsideCharacter { - keys = ['a', '(']; - charToMatch = '('; - includeSurrounding = true; -} - -@RegisterAction -class MoveAClosingParentheses extends MoveInsideCharacter { - keys = ['a', ')']; - charToMatch = '('; - includeSurrounding = true; -} - -@RegisterAction -class MoveAParenthesesBlock extends MoveInsideCharacter { - keys = ['a', 'b']; - charToMatch = '('; - includeSurrounding = true; -} - -@RegisterAction -class MoveICurlyBrace extends MoveInsideCharacter { - keys = ['i', '{']; - charToMatch = '{'; -} - -@RegisterAction -class MoveIClosingCurlyBrace extends MoveInsideCharacter { - keys = ['i', '}']; - charToMatch = '{'; -} - -@RegisterAction -class MoveIClosingCurlyBraceBlock extends MoveInsideCharacter { - keys = ['i', 'B']; - charToMatch = '{'; -} - -@RegisterAction -export class MoveACurlyBrace extends MoveInsideCharacter { - keys = ['a', '{']; - charToMatch = '{'; - includeSurrounding = true; -} - -@RegisterAction -export class MoveAClosingCurlyBrace extends MoveInsideCharacter { - keys = ['a', '}']; - charToMatch = '{'; - includeSurrounding = true; -} - -@RegisterAction -class MoveAClosingCurlyBraceBlock extends MoveInsideCharacter { - keys = ['a', 'B']; - charToMatch = '{'; - includeSurrounding = true; -} - -@RegisterAction -class MoveICaret extends MoveInsideCharacter { - keys = ['i', '<']; - charToMatch = '<'; -} - -@RegisterAction -class MoveIClosingCaret extends MoveInsideCharacter { - keys = ['i', '>']; - charToMatch = '<'; -} - -@RegisterAction -export class MoveACaret extends MoveInsideCharacter { - keys = ['a', '<']; - charToMatch = '<'; - includeSurrounding = true; -} - -@RegisterAction -class MoveAClosingCaret extends MoveInsideCharacter { - keys = ['a', '>']; - charToMatch = '<'; - includeSurrounding = true; -} - -@RegisterAction -class MoveISquareBracket extends MoveInsideCharacter { - keys = ['i', '[']; - charToMatch = '['; -} - -@RegisterAction -class MoveIClosingSquareBraket extends MoveInsideCharacter { - keys = ['i', ']']; - charToMatch = '['; -} - -@RegisterAction -export class MoveASquareBracket extends MoveInsideCharacter { - keys = ['a', '[']; - charToMatch = '['; - includeSurrounding = true; -} - -@RegisterAction -class MoveAClosingSquareBracket extends MoveInsideCharacter { - keys = ['a', ']']; - charToMatch = '['; - includeSurrounding = true; -} - -export abstract class MoveQuoteMatch extends BaseMovement { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualBlock]; - protected charToMatch: string; - protected includeSurrounding = false; - - public async execAction(position: Position, vimState: VimState): Promise { - const text = TextEditor.getLineAt(position).text; - const quoteMatcher = new QuoteMatcher(this.charToMatch, text); - const start = quoteMatcher.findOpening(position.character); - const end = quoteMatcher.findClosing(start + 1); - - if (start === -1 || end === -1 || end === start || end < position.character) { - return { - start: position, - stop: position, - failed: true, - }; - } - - let startPos = new Position(position.line, start); - let endPos = new Position(position.line, end); - - if (!this.includeSurrounding) { - startPos = startPos.getRight(); - endPos = endPos.getLeft(); - } - - if (position.isBefore(startPos)) { - vimState.recordedState.operatorPositionDiff = startPos.subtract(position); - } - - return { - start: startPos, - stop: endPos, - }; - } - - public 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', "'"]; - charToMatch = "'"; - includeSurrounding = false; -} - -@RegisterAction -export class MoveASingleQuotes extends MoveQuoteMatch { - keys = ['a', "'"]; - charToMatch = "'"; - includeSurrounding = true; -} - -@RegisterAction -class MoveInsideDoubleQuotes extends MoveQuoteMatch { - keys = ['i', '"']; - charToMatch = '"'; - includeSurrounding = false; -} - -@RegisterAction -export class MoveADoubleQuotes extends MoveQuoteMatch { - keys = ['a', '"']; - charToMatch = '"'; - includeSurrounding = true; -} - -@RegisterAction -class MoveInsideBacktick extends MoveQuoteMatch { - keys = ['i', '`']; - charToMatch = '`'; - includeSurrounding = false; -} - -@RegisterAction -export class MoveABacktick extends MoveQuoteMatch { - keys = ['a', '`']; - charToMatch = '`'; - includeSurrounding = true; -} - -@RegisterAction -class MoveToUnclosedRoundBracketBackward extends MoveToMatchingBracket { - keys = ['[', '(']; - - public async execAction(position: Position, vimState: VimState): Promise { - const failure = { start: position, stop: position, failed: true }; - const charToMatch = ')'; - const result = PairMatcher.nextPairedChar( - position.getLeftThroughLineBreaks(), - charToMatch, - false - ); - - if (!result) { - return failure; - } - return result; - } -} - -@RegisterAction -class MoveToUnclosedRoundBracketForward extends MoveToMatchingBracket { - keys = [']', ')']; - - public async execAction(position: Position, vimState: VimState): Promise { - const failure = { start: position, stop: position, failed: true }; - const charToMatch = '('; - const result = PairMatcher.nextPairedChar( - position.getRightThroughLineBreaks(), - charToMatch, - false - ); - - if (!result) { - return failure; - } - return result; - } -} - -@RegisterAction -class MoveToUnclosedCurlyBracketBackward extends MoveToMatchingBracket { - keys = ['[', '{']; - - public async execAction(position: Position, vimState: VimState): Promise { - const failure = { start: position, stop: position, failed: true }; - const charToMatch = '}'; - const result = PairMatcher.nextPairedChar( - position.getLeftThroughLineBreaks(), - charToMatch, - false - ); - - if (!result) { - return failure; - } - return result; - } -} - -@RegisterAction -class MoveToUnclosedCurlyBracketForward extends MoveToMatchingBracket { - keys = [']', '}']; - - public async execAction(position: Position, vimState: VimState): Promise { - const failure = { start: position, stop: position, failed: true }; - const charToMatch = '{'; - const result = PairMatcher.nextPairedChar( - position.getRightThroughLineBreaks(), - charToMatch, - false - ); - - if (!result) { - return failure; - } - return result; - } -} - -abstract class MoveTagMatch extends BaseMovement { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualBlock]; - protected includeTag = false; - - public async execAction(position: Position, vimState: VimState): Promise { - const editorText = TextEditor.getText(); - const offset = TextEditor.getOffsetAt(position); - const tagMatcher = new TagMatcher(editorText, offset); - const start = tagMatcher.findOpening(this.includeTag); - const end = tagMatcher.findClosing(this.includeTag); - - if (start === undefined || end === undefined) { - return { - start: position, - stop: position, - failed: true, - }; - } - - let startPosition = start ? TextEditor.getPositionAt(start) : position; - let endPosition = end ? TextEditor.getPositionAt(end) : position; - - if (position.isAfter(endPosition)) { - vimState.recordedState.transformations.push({ - type: 'moveCursor', - diff: endPosition.subtract(position), - }); - } else if (position.isBefore(startPosition)) { - vimState.recordedState.transformations.push({ - type: 'moveCursor', - diff: startPosition.subtract(position), - }); - } - if (start === end) { - if (vimState.recordedState.operator instanceof ChangeOperator) { - vimState.currentMode = ModeName.Insert; - } - return { - start: startPosition, - stop: startPosition, - failed: true, - }; - } - return { - start: startPosition, - stop: endPosition.getLeftThroughLineBreaks(true), - }; - } - - public 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 -export class MoveInsideTag extends MoveTagMatch { - keys = ['i', 't']; - includeTag = false; -} - -@RegisterAction -export class MoveAroundTag extends MoveTagMatch { - keys = ['a', 't']; - includeTag = true; -} - -export class ArrowsInInsertMode extends BaseMovement { - modes = [ModeName.Insert]; - keys: string[]; - canBePrefixedWithCount = true; - - public async execAction(position: Position, vimState: VimState): Promise { - // we are in Insert Mode and arrow keys will clear all other actions except the first action, which enters Insert Mode. - // Please note the arrow key movement can be repeated while using `.` but it can't be repeated when using `` in Insert Mode. - vimState.recordedState.actionsRun = [ - vimState.recordedState.actionsRun.shift()!, - vimState.recordedState.actionsRun.pop()!, - ]; - let newPosition: Position = position; - - switch (this.keys[0]) { - case '': - newPosition = await new MoveUpArrow().execAction(position, vimState); - break; - case '': - newPosition = await new MoveDownArrow().execAction(position, vimState); - break; - case '': - newPosition = await new MoveLeftArrow().execAction(position, vimState); - break; - case '': - newPosition = await new MoveRightArrow().execAction(position, vimState); - break; - default: - break; - } - vimState.replaceState = new ReplaceState(newPosition); - return newPosition; - } -} - -@RegisterAction -class UpArrowInInsertMode extends ArrowsInInsertMode { - keys = ['']; -} - -@RegisterAction -class DownArrowInInsertMode extends ArrowsInInsertMode { - keys = ['']; -} - -@RegisterAction -class LeftArrowInInsertMode extends ArrowsInInsertMode { - keys = ['']; -} - -@RegisterAction -class RightArrowInInsertMode extends ArrowsInInsertMode { - keys = ['']; -} +import * as vscode from 'vscode'; +import { ModeName } from './../mode/mode'; +import { Position, PositionDiff } from './../common/motion/position'; +import { Configuration } from './../configuration/configuration'; +import { TextEditor, CursorMovePosition, CursorMoveByUnit } from './../textEditor'; +import { VimState } from './../mode/modeHandler'; +import { RegisterMode } from './../register/register'; +import { PairMatcher } from './../common/matching/matcher'; +import { ReplaceState } from './../state/replaceState'; +import { QuoteMatcher } from './../common/matching/quoteMatcher'; +import { TagMatcher } from './../common/matching/tagMatcher'; +import { RegisterAction } from './base'; +import { ChangeOperator } from './operator'; +import { BaseAction } from './base'; + +export function isIMovement(o: IMovement | Position): o is IMovement { + return (o as IMovement).start !== undefined && (o as IMovement).stop !== undefined; +} + +/** + * 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; + + diff?: PositionDiff; + + // It /so/ annoys me that I have to put this here. + registerMode?: RegisterMode; +} + +/** + * A movement is something like 'h', 'k', 'w', 'b', 'gg', etc. + */ +export abstract class BaseMovement extends BaseAction { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + + isMotion = true; + + canBePrefixedWithCount = false; + + /** + * Whether we should change lastRepeatableMovement in VimState. + */ + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return false; + } + + /** + * Whether we should change desiredColumn in VimState. + */ + public doesntChangeDesiredColumn = false; + + /** + * This is for commands like $ which force the desired column to be at + * the end of even the longest line. + */ + public setsDesiredColumnToEOL = false; + + /** + * 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): 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. + */ + public async execActionForOperator( + position: Position, + vimState: VimState + ): Promise { + return await this.execAction(position, vimState); + } + + /** + * 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 recordedState = vimState.recordedState; + let result: Position | IMovement = new Position(0, 0); // bogus init to satisfy typechecker + + if (count < 1) { + count = 1; + } else if (count > 99999) { + count = 99999; + } + + for (let i = 0; i < count; i++) { + const firstIteration = i === 0; + const lastIteration = i === count - 1; + const temporaryResult = + recordedState.operator && lastIteration + ? await this.execActionForOperator(position, vimState) + : await this.execAction(position, vimState); + + if (temporaryResult instanceof Position) { + result = temporaryResult; + position = temporaryResult; + } else if (isIMovement(temporaryResult)) { + if (result instanceof Position) { + result = { + start: new Position(0, 0), + stop: new Position(0, 0), + failed: false, + }; + } + + result.failed = result.failed || temporaryResult.failed; + + if (firstIteration) { + (result as IMovement).start = temporaryResult.start; + } + + if (lastIteration) { + (result as IMovement).stop = temporaryResult.stop; + } else { + position = temporaryResult.stop.getRightThroughLineBreaks(); + } + + result.registerMode = temporaryResult.registerMode; + } + } + + return result; + } +} + +abstract class MoveByScreenLine extends BaseMovement { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + movementType: CursorMovePosition; + by: CursorMoveByUnit; + value: number = 1; + + public async execAction(position: Position, vimState: VimState): Promise { + await vscode.commands.executeCommand('cursorMove', { + to: this.movementType, + select: vimState.currentMode !== ModeName.Normal, + by: this.by, + value: this.value, + }); + + if (vimState.currentMode === ModeName.Normal) { + return Position.FromVSCodePosition(vimState.editor.selection.active); + } 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 = Position.FromVSCodePosition(vimState.editor.selection.start); + let stop = Position.FromVSCodePosition(vimState.editor.selection.end); + let curPos = Position.FromVSCodePosition(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)) { + position = start; + [start, stop] = [stop, start]; + start = start.getLeft(); + } + + return { start, stop }; + } + } + + public async execActionForOperator(position: Position, vimState: VimState): Promise { + await vscode.commands.executeCommand('cursorMove', { + to: this.movementType, + select: true, + by: this.by, + value: this.value, + }); + + return { + start: Position.FromVSCodePosition(vimState.editor.selection.start), + stop: Position.FromVSCodePosition(vimState.editor.selection.end), + }; + } +} + +abstract class MoveByScreenLineMaintainDesiredColumn extends MoveByScreenLine { + doesntChangeDesiredColumn = true; + public async execAction(position: Position, vimState: VimState): Promise { + let prevDesiredColumn = vimState.desiredColumn; + let prevLine = vimState.editor.selection.active.line; + + await vscode.commands.executeCommand('cursorMove', { + to: this.movementType, + select: vimState.currentMode !== ModeName.Normal, + by: this.by, + value: this.value, + }); + + if (vimState.currentMode === ModeName.Normal) { + let returnedPos = Position.FromVSCodePosition(vimState.editor.selection.active); + if (prevLine !== returnedPos.line) { + returnedPos = returnedPos.setLocation(returnedPos.line, 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 = Position.FromVSCodePosition(vimState.editor.selection.start); + let stop = Position.FromVSCodePosition(vimState.editor.selection.end); + let curPos = Position.FromVSCodePosition(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)) { + position = start; + [start, stop] = [stop, start]; + start = start.getLeft(); + } + + return { start, stop }; + } + } +} + +class MoveDownByScreenLineMaintainDesiredColumn extends MoveByScreenLineMaintainDesiredColumn { + movementType: CursorMovePosition = 'down'; + by: CursorMoveByUnit = 'wrappedLine'; + value = 1; +} + +class MoveDownFoldFix extends MoveByScreenLineMaintainDesiredColumn { + movementType: CursorMovePosition = 'down'; + by: CursorMoveByUnit = 'line'; + value = 1; + + public async execAction(position: Position, vimState: VimState): Promise { + if (position.line === TextEditor.getLineCount() - 1) { + return position; + } + let t: Position; + let count = 0; + const prevDesiredColumn = vimState.desiredColumn; + do { + t = await new MoveDownByScreenLine().execAction(position, vimState); + count += 1; + } while (t.line === position.line); + if (t.line > position.line + 1) { + return t; + } + while (count > 0) { + t = await new MoveUpByScreenLine().execAction(position, vimState); + count--; + } + vimState.desiredColumn = prevDesiredColumn; + return await position.getDown(vimState.desiredColumn); + } +} + +@RegisterAction +class MoveDown extends BaseMovement { + keys = ['j']; + doesntChangeDesiredColumn = true; + + public async execAction(position: Position, vimState: VimState): Promise { + if (Configuration.foldfix && vimState.currentMode !== ModeName.VisualBlock) { + return new MoveDownFoldFix().execAction(position, vimState); + } + return position.getDown(vimState.desiredColumn); + } + + public async execActionForOperator(position: Position, vimState: VimState): Promise { + vimState.currentRegisterMode = RegisterMode.LineWise; + return position.getDown(position.getLineEnd().character); + } +} + +@RegisterAction +class MoveDownArrow extends MoveDown { + keys = ['']; +} + +class MoveUpByScreenLineMaintainDesiredColumn extends MoveByScreenLineMaintainDesiredColumn { + movementType: CursorMovePosition = 'up'; + by: CursorMoveByUnit = 'wrappedLine'; + value = 1; +} + +@RegisterAction +class MoveUp extends BaseMovement { + keys = ['k']; + doesntChangeDesiredColumn = true; + + public async execAction(position: Position, vimState: VimState): Promise { + if (Configuration.foldfix && vimState.currentMode !== ModeName.VisualBlock) { + return new MoveUpFoldFix().execAction(position, vimState); + } + return position.getUp(vimState.desiredColumn); + } + + public async execActionForOperator(position: Position, vimState: VimState): Promise { + vimState.currentRegisterMode = RegisterMode.LineWise; + return position.getUp(position.getLineEnd().character); + } +} + +@RegisterAction +class MoveUpFoldFix extends MoveByScreenLineMaintainDesiredColumn { + movementType: CursorMovePosition = 'up'; + by: CursorMoveByUnit = 'line'; + value = 1; + + public async execAction(position: Position, vimState: VimState): Promise { + if (position.line === 0) { + return position; + } + let t: Position; + const prevDesiredColumn = vimState.desiredColumn; + let count = 0; + + do { + t = await new MoveUpByScreenLineMaintainDesiredColumn().execAction( + position, + vimState + ); + count += 1; + } while (t.line === position.line); + vimState.desiredColumn = prevDesiredColumn; + if (t.line < position.line - 1) { + return t; + } + while (count > 0) { + t = await new MoveDownByScreenLine().execAction(position, vimState); + count--; + } + vimState.desiredColumn = prevDesiredColumn; + return await position.getUp(vimState.desiredColumn); + } +} + +@RegisterAction +class MoveUpArrow extends MoveUp { + keys = ['']; +} + +@RegisterAction +class ArrowsInReplaceMode extends BaseMovement { + modes = [ModeName.Replace]; + keys = [[''], [''], [''], ['']]; + + public async execAction(position: Position, vimState: VimState): Promise { + let newPosition: Position = position; + + switch (this.keysPressed[0]) { + case '': + newPosition = await new MoveUpArrow().execAction(position, vimState); + break; + case '': + newPosition = await new MoveDownArrow().execAction(position, vimState); + break; + case '': + newPosition = await new MoveLeftArrow().execAction(position, vimState); + break; + case '': + newPosition = await new MoveRightArrow().execAction(position, vimState); + break; + default: + break; + } + vimState.replaceState = new ReplaceState(newPosition); + return newPosition; + } +} + +@RegisterAction +class UpArrowInReplaceMode extends ArrowsInReplaceMode { + keys = [['']]; +} + +@RegisterAction +class DownArrowInReplaceMode extends ArrowsInReplaceMode { + keys = [['']]; +} + +@RegisterAction +class LeftArrowInReplaceMode extends ArrowsInReplaceMode { + keys = [['']]; +} + +@RegisterAction +class RightArrowInReplaceMode extends ArrowsInReplaceMode { + keys = [['']]; +} + +@RegisterAction +class CommandNextSearchMatch extends BaseMovement { + keys = ['n']; + + public async execAction(position: Position, vimState: VimState): Promise { + const searchState = vimState.globalState.searchState; + + if (!searchState || searchState.searchString === '') { + return position; + } + // Turn one of the highlighting flags back on (turned off with :nohl) + vimState.globalState.hl = true; + + if (vimState.cursorPosition.getRight().isEqual(vimState.cursorPosition.getLineEnd())) { + return searchState.getNextSearchMatchPosition(vimState.cursorPosition.getRight()).pos; + } + + // Turn one of the highlighting flags back on (turned off with :nohl) + + return searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; + } +} + +@RegisterAction +class CommandPreviousSearchMatch extends BaseMovement { + keys = ['N']; + + public async execAction(position: Position, vimState: VimState): Promise { + const searchState = vimState.globalState.searchState; + + if (!searchState || searchState.searchString === '') { + return position; + } + + // Turn one of the highlighting flags back on (turned off with :nohl) + vimState.globalState.hl = true; + + return searchState.getNextSearchMatchPosition(vimState.cursorPosition, -1).pos; + } +} + +@RegisterAction +export class MarkMovementBOL extends BaseMovement { + keys = ["'", '']; + + public async execAction(position: Position, vimState: VimState): Promise { + const markName = this.keysPressed[1]; + const mark = vimState.historyTracker.getMark(markName); + + vimState.currentRegisterMode = RegisterMode.LineWise; + + return mark.position.getFirstLineNonBlankChar(); + } +} + +@RegisterAction +export class MarkMovement extends BaseMovement { + keys = ['`', '']; + + public async execAction(position: Position, vimState: VimState): Promise { + const markName = this.keysPressed[1]; + const mark = vimState.historyTracker.getMark(markName); + + return mark.position; + } +} + +@RegisterAction +export class MoveLeft extends BaseMovement { + keys = ['h']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getLeft(); + } +} + +@RegisterAction +class MoveLeftArrow extends MoveLeft { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['']; +} + +@RegisterAction +class BackSpaceInNormalMode extends BaseMovement { + modes = [ModeName.Normal]; + keys = ['']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getLeftThroughLineBreaks(); + } +} + +@RegisterAction +class MoveRight extends BaseMovement { + keys = ['l']; + + public async execAction(position: Position, vimState: VimState): Promise { + return new Position(position.line, position.character + 1); + } +} + +@RegisterAction +class MoveRightArrow extends MoveRight { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + keys = ['']; +} + +@RegisterAction +class MoveRightWithSpace extends BaseMovement { + keys = [' ']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getRightThroughLineBreaks(); + } +} + +@RegisterAction +class MoveDownNonBlank extends BaseMovement { + keys = ['+']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + return position.getDownByCount(Math.max(count, 1)).getFirstLineNonBlankChar(); + } +} + +@RegisterAction +class MoveUpNonBlank extends BaseMovement { + keys = ['-']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + return position.getUpByCount(Math.max(count, 1)).getFirstLineNonBlankChar(); + } +} + +@RegisterAction +class MoveDownUnderscore extends BaseMovement { + keys = ['_']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + return position.getDownByCount(Math.max(count - 1, 0)).getFirstLineNonBlankChar(); + } +} + +@RegisterAction +class MoveToColumn extends BaseMovement { + keys = ['|']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + return new Position(position.line, Math.max(0, count - 1)); + } +} + +@RegisterAction +class MoveFindForward extends BaseMovement { + keys = ['f', '']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + count = count || 1; + const toFind = this.keysPressed[1]; + let result = position.findForwards(toFind, count); + + if (!result) { + return { start: position, stop: position, failed: true }; + } + + if (vimState.recordedState.operator) { + result = result.getRight(); + } + + return result; + } + + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); + } +} + +@RegisterAction +class MoveFindBackward extends BaseMovement { + keys = ['F', '']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + count = count || 1; + const toFind = this.keysPressed[1]; + let result = position.findBackwards(toFind, count); + + if (!result) { + return { start: position, stop: position, failed: true }; + } + + return result; + } + + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); + } +} + +@RegisterAction +class MoveTilForward extends BaseMovement { + keys = ['t', '']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + count = count || 1; + const toFind = this.keysPressed[1]; + let result = position.tilForwards(toFind, count); + + if (!result) { + return { start: position, stop: position, failed: true }; + } + + if (vimState.recordedState.operator) { + result = result.getRight(); + } + + return result; + } + + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); + } +} + +@RegisterAction +class MoveTilBackward extends BaseMovement { + keys = ['T', '']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + count = count || 1; + const toFind = this.keysPressed[1]; + let result = position.tilBackwards(toFind, count); + + if (!result) { + return { start: position, stop: position, failed: true }; + } + + return result; + } + + public canBeRepeatedWithSemicolon(vimState: VimState, result: Position | IMovement) { + return !vimState.recordedState.operator || !(isIMovement(result) && result.failed); + } +} + +@RegisterAction +class MoveRepeat extends BaseMovement { + keys = [';']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + const movement = VimState.lastRepeatableMovement; + if (movement) { + const result = await movement.execActionWithCount(position, vimState, count); + /** + * For t and T commands vim executes ; as 2; + * This way the cursor will get to the next instance of + */ + if (result instanceof Position && position.isEqual(result) && count <= 1) { + return await movement.execActionWithCount(position, vimState, 2); + } + return result; + } + return position; + } +} + +@RegisterAction +class MoveRepeatReversed extends BaseMovement { + keys = [',']; + static reverseMotionMapping: Map BaseMovement> = new Map([ + [MoveFindForward, () => new MoveFindBackward()], + [MoveFindBackward, () => new MoveFindForward()], + [MoveTilForward, () => new MoveTilBackward()], + [MoveTilBackward, () => new MoveTilForward()], + ]); + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + const movement = VimState.lastRepeatableMovement; + if (movement) { + const reverse = MoveRepeatReversed.reverseMotionMapping.get(movement.constructor)!(); + reverse.keysPressed = [(reverse.keys as string[])[0], movement.keysPressed[1]]; + + let result = await reverse.execActionWithCount(position, vimState, count); + // For t and T commands vim executes ; as 2; + if (result instanceof Position && position.isEqual(result) && count <= 1) { + result = await reverse.execActionWithCount(position, vimState, 2); + } + return result; + } + return position; + } +} + +@RegisterAction +class MoveLineEnd extends BaseMovement { + keys = [['$'], [''], ['']]; + setsDesiredColumnToEOL = true; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + return position.getDownByCount(Math.max(count - 1, 0)).getLineEnd(); + } +} + +@RegisterAction +class MoveLineBegin extends BaseMovement { + keys = [['0'], [''], ['']]; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getLineBegin(); + } + + public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { + return super.doesActionApply(vimState, keysPressed) && vimState.recordedState.count === 0; + } + + public 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'; + canBePrefixedWithCount = true; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + count = count || 1; + const pos = await this.execAction(position, vimState); + const newPos: Position | IMovement = pos as Position; + + // If in visual, return a selection + if (pos instanceof Position) { + return pos.getDownByCount(count - 1); + } else if (isIMovement(pos)) { + return { start: pos.start, stop: pos.stop.getDownByCount(count - 1).getLeft() }; + } + + return newPos.getDownByCount(count - 1); + } +} + +@RegisterAction +class MoveScreenLineCenter extends MoveByScreenLine { + keys = ['g', 'm']; + movementType: CursorMovePosition = 'wrappedLineColumnCenter'; +} + +@RegisterAction +export class MoveUpByScreenLine extends MoveByScreenLine { + modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual]; + keys = [['g', 'k'], ['g', '']]; + movementType: CursorMovePosition = 'up'; + by: CursorMoveByUnit = 'wrappedLine'; + value = 1; +} + +@RegisterAction +class MoveDownByScreenLine extends MoveByScreenLine { + modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual]; + keys = [['g', 'j'], ['g', '']]; + movementType: CursorMovePosition = 'down'; + by: CursorMoveByUnit = 'wrappedLine'; + 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 { + modes = [ModeName.VisualLine]; + keys = [['g', 'k'], ['g', '']]; + movementType: CursorMovePosition = 'up'; + by: CursorMoveByUnit = 'line'; + value = 1; +} + +@RegisterAction +class MoveDownByScreenLineVisualLine extends MoveByScreenLine { + modes = [ModeName.VisualLine]; + keys = [['g', 'j'], ['g', '']]; + movementType: CursorMovePosition = 'down'; + by: CursorMoveByUnit = 'line'; + value = 1; +} + +@RegisterAction +class MoveUpByScreenLineVisualBlock extends BaseMovement { + modes = [ModeName.VisualBlock]; + keys = [['g', 'k'], ['g', '']]; + doesntChangeDesiredColumn = true; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getUp(vimState.desiredColumn); + } + + public async execActionForOperator(position: Position, vimState: VimState): Promise { + vimState.currentRegisterMode = RegisterMode.LineWise; + return position.getUp(position.getLineEnd().character); + } +} + +@RegisterAction +class MoveDownByScreenLineVisualBlock extends BaseMovement { + modes = [ModeName.VisualBlock]; + keys = [['g', 'j'], ['g', '']]; + doesntChangeDesiredColumn = true; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getDown(vimState.desiredColumn); + } + + public async execActionForOperator(position: Position, vimState: VimState): Promise { + vimState.currentRegisterMode = RegisterMode.LineWise; + return position.getDown(position.getLineEnd().character); + } +} + +@RegisterAction +class MoveScreenToRight extends MoveByScreenLine { + modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['z', 'h']; + movementType: CursorMovePosition = 'right'; + by: CursorMoveByUnit = 'character'; + value = 1; +} + +@RegisterAction +class MoveScreenToLeft extends MoveByScreenLine { + modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['z', 'l']; + movementType: CursorMovePosition = 'left'; + by: CursorMoveByUnit = 'character'; + value = 1; +} + +@RegisterAction +class MoveScreenToRightHalf extends MoveByScreenLine { + modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['z', 'H']; + movementType: CursorMovePosition = 'right'; + by: CursorMoveByUnit = 'halfLine'; + value = 1; +} + +@RegisterAction +class MoveScreenToLeftHalf extends MoveByScreenLine { + modes = [ModeName.Insert, ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ['z', 'L']; + movementType: CursorMovePosition = 'left'; + by: CursorMoveByUnit = 'halfLine'; + value = 1; +} + +@RegisterAction +class MoveToLineFromViewPortTop extends MoveByScreenLine { + keys = ['H']; + movementType: CursorMovePosition = 'viewPortTop'; + by: CursorMoveByUnit = 'line'; + value = 1; + canBePrefixedWithCount = true; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + this.value = count < 1 ? 1 : count; + return await this.execAction(position, vimState); + } +} + +@RegisterAction +class MoveToLineFromViewPortBottom extends MoveByScreenLine { + keys = ['L']; + movementType: CursorMovePosition = 'viewPortBottom'; + by: CursorMoveByUnit = 'line'; + value = 1; + canBePrefixedWithCount = true; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + this.value = count < 1 ? 1 : count; + return await this.execAction(position, vimState); + } +} + +@RegisterAction +class MoveToMiddleLineInViewPort extends MoveByScreenLine { + keys = ['M']; + movementType: CursorMovePosition = 'viewPortCenter'; + by: CursorMoveByUnit = 'line'; +} + +@RegisterAction +class MoveNonBlank extends BaseMovement { + keys = ['^']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getFirstLineNonBlankChar(); + } +} + +@RegisterAction +class MoveNextLineNonBlank extends BaseMovement { + keys = ['\n']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + vimState.currentRegisterMode = RegisterMode.LineWise; + + // Count === 0 if just pressing enter in normal mode, need to still go down 1 line + if (count === 0) { + count++; + } + + return position.getDownByCount(count).getFirstLineNonBlankChar(); + } +} + +@RegisterAction +class MoveNonBlankFirst extends BaseMovement { + keys = ['g', 'g']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + if (count === 0) { + return position.getDocumentBegin().getFirstLineNonBlankChar(); + } + + return new Position(count - 1, 0).getFirstLineNonBlankChar(); + } +} + +@RegisterAction +class MoveNonBlankLast extends BaseMovement { + keys = ['G']; + + public async execActionWithCount( + position: Position, + vimState: VimState, + count: number + ): Promise { + let stop: Position; + + if (count === 0) { + stop = new Position(TextEditor.getLineCount() - 1, 0); + } else { + stop = new Position(Math.min(count, TextEditor.getLineCount()) - 1, 0); + } + + return { + start: vimState.cursorStartPosition, + stop: stop, + registerMode: RegisterMode.LineWise, + }; + } +} + +@RegisterAction +export class MoveWordBegin extends BaseMovement { + keys = ['w']; + + public async execAction( + position: Position, + vimState: VimState, + isLastIteration: boolean = false + ): Promise { + if (isLastIteration && vimState.recordedState.operator instanceof ChangeOperator) { + if (TextEditor.getLineAt(position).text.length < 1) { + return position; + } + + const line = TextEditor.getLineAt(position).text; + const char = line[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'.indexOf(char) >= 0) { + return position.getWordRight(); + } else { + return position.getCurrentWordEnd(true).getRight(); + } + } else { + return position.getWordRight(); + } + } + + public async execActionForOperator(position: Position, vimState: VimState): Promise { + const result = await this.execAction(position, vimState, true); + + /* + 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()) + ) { + return position.getLineEnd(); + } + + if (result.isLineEnd()) { + return new Position(result.line, result.character + 1); + } + + return result; + } +} + +@RegisterAction +class MoveFullWordBegin extends BaseMovement { + keys = ['W']; + + public async execAction(position: Position, vimState: VimState): Promise { + if (vimState.recordedState.operator instanceof ChangeOperator) { + // TODO use execForOperator? Or maybe dont? + + // See note for w + return position.getCurrentBigWordEnd().getRight(); + } else { + return position.getBigWordRight(); + } + } +} + +@RegisterAction +class MoveWordEnd extends BaseMovement { + keys = ['e']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getCurrentWordEnd(); + } + + public async execActionForOperator(position: Position, vimState: VimState): Promise { + let end = position.getCurrentWordEnd(); + + return new Position(end.line, end.character + 1); + } +} + +@RegisterAction +class MoveFullWordEnd extends BaseMovement { + keys = ['E']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getCurrentBigWordEnd(); + } + + public async execActionForOperator(position: Position, vimState: VimState): Promise { + return position.getCurrentBigWordEnd().getRight(); + } +} + +@RegisterAction +class MoveLastWordEnd extends BaseMovement { + keys = ['g', 'e']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getLastWordEnd(); + } +} + +@RegisterAction +class MoveLastFullWordEnd extends BaseMovement { + keys = ['g', 'E']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getLastBigWordEnd(); + } +} + +@RegisterAction +class MoveBeginningWord extends BaseMovement { + keys = ['b']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getWordLeft(); + } +} + +@RegisterAction +class MoveBeginningFullWord extends BaseMovement { + keys = ['B']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getBigWordLeft(); + } +} + +@RegisterAction +class MovePreviousSentenceBegin extends BaseMovement { + keys = ['(']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getSentenceBegin({ forward: false }); + } +} + +@RegisterAction +class MoveNextSentenceBegin extends BaseMovement { + keys = [')']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getSentenceBegin({ forward: true }); + } +} + +@RegisterAction +class MoveParagraphEnd extends BaseMovement { + keys = ['}']; + + public async execAction(position: Position, vimState: VimState): Promise { + const isLineWise = + position.isLineBeginning() && + vimState.currentMode === ModeName.Normal && + vimState.recordedState.operator; + let paragraphEnd = position.getCurrentParagraphEnd(); + vimState.currentRegisterMode = isLineWise + ? RegisterMode.LineWise + : RegisterMode.FigureItOutFromCurrentMode; + return isLineWise ? paragraphEnd.getLeftThroughLineBreaks(true) : paragraphEnd; + } +} + +@RegisterAction +class MoveParagraphBegin extends BaseMovement { + keys = ['{']; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getCurrentParagraphBeginning(); + } +} + +abstract class MoveSectionBoundary extends BaseMovement { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + boundary: string; + forward: boolean; + + public async execAction(position: Position, vimState: VimState): Promise { + return position.getSectionBoundary({ + forward: this.forward, + boundary: this.boundary, + }); + } +} + +@RegisterAction +class MoveNextSectionBegin extends MoveSectionBoundary { + keys = [']', ']']; + boundary = '{'; + forward = true; +} + +@RegisterAction +class MoveNextSectionEnd extends MoveSectionBoundary { + keys = [']', '[']; + boundary = '}'; + forward = true; +} + +@RegisterAction +class MovePreviousSectionBegin extends MoveSectionBoundary { + keys = ['[', '[']; + boundary = '{'; + forward = false; +} + +@RegisterAction +class MovePreviousSectionEnd extends MoveSectionBoundary { + keys = ['[', ']']; + boundary = '}'; + forward = false; +} + +@RegisterAction +class MoveToMatchingBracket extends BaseMovement { + keys = ['%']; + + public async execAction(position: Position, vimState: VimState): Promise { + position = position.getLeftIfEOL(); + + const text = TextEditor.getLineAt(position).text; + const charToMatch = text[position.character]; + const toFind = PairMatcher.pairings[charToMatch]; + const failure = { start: position, stop: position, failed: true }; + + if (!toFind || !toFind.matchesWithPercentageMotion) { + // If we're not on a match, go right until we find a + // pairable character or hit the end of line. + + for (let i = position.character; i < text.length; i++) { + if (PairMatcher.pairings[text[i]]) { + // We found an opening char, now move to the matching closing char + const openPosition = new Position(position.line, i); + return PairMatcher.nextPairedChar(openPosition, text[i], true) || failure; + } + } + + return failure; + } + + return PairMatcher.nextPairedChar(position, charToMatch, true) || failure; + } + + public 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.compareTo(result) > 0) { + return { + start: result, + stop: position.getRight(), + }; + } else { + return result.getRight(); + } + } + + public 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 { start: position, stop: position, failed: true }; + } + + const targetLine = Math.round(count * TextEditor.getLineCount() / 100); + return new Position(targetLine - 1, 0).getFirstLineNonBlankChar(); + } else { + return super.execActionWithCount(position, vimState, count); + } + } +} + +export abstract class MoveInsideCharacter extends BaseMovement { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock]; + protected charToMatch: string; + protected includeSurrounding = false; + + public async execAction(position: Position, vimState: VimState): Promise { + const failure = { start: position, stop: position, failed: true }; + const text = TextEditor.getLineAt(position).text; + const closingChar = PairMatcher.pairings[this.charToMatch].match; + const closedMatch = text[position.character] === closingChar; + + // First, search backwards for the opening character of the sequence + let startPos = PairMatcher.nextPairedChar(position, closingChar, closedMatch); + if (startPos === undefined) { + return failure; + } + + let startPlusOne: Position; + + if (startPos.isAfterOrEqual(startPos.getLineEnd().getLeft())) { + startPlusOne = new Position(startPos.line + 1, 0); + } else { + startPlusOne = new Position(startPos.line, startPos.character + 1); + } + + let endPos = PairMatcher.nextPairedChar(startPlusOne, this.charToMatch, false); + if (endPos === undefined) { + return failure; + } + + if (this.includeSurrounding) { + if (vimState.currentMode !== ModeName.Visual) { + endPos = new Position(endPos.line, endPos.character + 1); + } + } else { + startPos = startPlusOne; + if (vimState.currentMode === ModeName.Visual) { + endPos = endPos.getLeftThroughLineBreaks(); + } + } + + // If the closing character is the first on the line, don't swallow it. + if (!this.includeSurrounding) { + if (endPos.getLeft().isInLeadingWhitespace()) { + endPos = endPos.getLineBegin(); + if (vimState.currentMode === ModeName.Visual) { + endPos = endPos.getLeftThroughLineBreaks(); + } + } + } + + if (position.isBefore(startPos)) { + vimState.recordedState.operatorPositionDiff = startPos.subtract(position); + } + + return { + start: startPos, + stop: endPos, + diff: new PositionDiff(0, startPos === position ? 1 : 0), + }; + } + + public 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 = []; + } + } + return result; + } +} + +@RegisterAction +class MoveIParentheses extends MoveInsideCharacter { + keys = ['i', '(']; + charToMatch = '('; +} + +@RegisterAction +class MoveIClosingParentheses extends MoveInsideCharacter { + keys = ['i', ')']; + charToMatch = '('; +} + +@RegisterAction +class MoveIClosingParenthesesBlock extends MoveInsideCharacter { + keys = ['i', 'b']; + charToMatch = '('; +} + +@RegisterAction +export class MoveAParentheses extends MoveInsideCharacter { + keys = ['a', '(']; + charToMatch = '('; + includeSurrounding = true; +} + +@RegisterAction +class MoveAClosingParentheses extends MoveInsideCharacter { + keys = ['a', ')']; + charToMatch = '('; + includeSurrounding = true; +} + +@RegisterAction +class MoveAParenthesesBlock extends MoveInsideCharacter { + keys = ['a', 'b']; + charToMatch = '('; + includeSurrounding = true; +} + +@RegisterAction +class MoveICurlyBrace extends MoveInsideCharacter { + keys = ['i', '{']; + charToMatch = '{'; +} + +@RegisterAction +class MoveIClosingCurlyBrace extends MoveInsideCharacter { + keys = ['i', '}']; + charToMatch = '{'; +} + +@RegisterAction +class MoveIClosingCurlyBraceBlock extends MoveInsideCharacter { + keys = ['i', 'B']; + charToMatch = '{'; +} + +@RegisterAction +export class MoveACurlyBrace extends MoveInsideCharacter { + keys = ['a', '{']; + charToMatch = '{'; + includeSurrounding = true; +} + +@RegisterAction +export class MoveAClosingCurlyBrace extends MoveInsideCharacter { + keys = ['a', '}']; + charToMatch = '{'; + includeSurrounding = true; +} + +@RegisterAction +class MoveAClosingCurlyBraceBlock extends MoveInsideCharacter { + keys = ['a', 'B']; + charToMatch = '{'; + includeSurrounding = true; +} + +@RegisterAction +class MoveICaret extends MoveInsideCharacter { + keys = ['i', '<']; + charToMatch = '<'; +} + +@RegisterAction +class MoveIClosingCaret extends MoveInsideCharacter { + keys = ['i', '>']; + charToMatch = '<'; +} + +@RegisterAction +export class MoveACaret extends MoveInsideCharacter { + keys = ['a', '<']; + charToMatch = '<'; + includeSurrounding = true; +} + +@RegisterAction +class MoveAClosingCaret extends MoveInsideCharacter { + keys = ['a', '>']; + charToMatch = '<'; + includeSurrounding = true; +} + +@RegisterAction +class MoveISquareBracket extends MoveInsideCharacter { + keys = ['i', '[']; + charToMatch = '['; +} + +@RegisterAction +class MoveIClosingSquareBraket extends MoveInsideCharacter { + keys = ['i', ']']; + charToMatch = '['; +} + +@RegisterAction +export class MoveASquareBracket extends MoveInsideCharacter { + keys = ['a', '[']; + charToMatch = '['; + includeSurrounding = true; +} + +@RegisterAction +class MoveAClosingSquareBracket extends MoveInsideCharacter { + keys = ['a', ']']; + charToMatch = '['; + includeSurrounding = true; +} + +export abstract class MoveQuoteMatch extends BaseMovement { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualBlock]; + protected charToMatch: string; + protected includeSurrounding = false; + + public async execAction(position: Position, vimState: VimState): Promise { + const text = TextEditor.getLineAt(position).text; + const quoteMatcher = new QuoteMatcher(this.charToMatch, text); + const start = quoteMatcher.findOpening(position.character); + const end = quoteMatcher.findClosing(start + 1); + + if (start === -1 || end === -1 || end === start || end < position.character) { + return { + start: position, + stop: position, + failed: true, + }; + } + + let startPos = new Position(position.line, start); + let endPos = new Position(position.line, end); + + if (!this.includeSurrounding) { + startPos = startPos.getRight(); + endPos = endPos.getLeft(); + } + + if (position.isBefore(startPos)) { + vimState.recordedState.operatorPositionDiff = startPos.subtract(position); + } + + return { + start: startPos, + stop: endPos, + }; + } + + public 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', "'"]; + charToMatch = "'"; + includeSurrounding = false; +} + +@RegisterAction +export class MoveASingleQuotes extends MoveQuoteMatch { + keys = ['a', "'"]; + charToMatch = "'"; + includeSurrounding = true; +} + +@RegisterAction +class MoveInsideDoubleQuotes extends MoveQuoteMatch { + keys = ['i', '"']; + charToMatch = '"'; + includeSurrounding = false; +} + +@RegisterAction +export class MoveADoubleQuotes extends MoveQuoteMatch { + keys = ['a', '"']; + charToMatch = '"'; + includeSurrounding = true; +} + +@RegisterAction +class MoveInsideBacktick extends MoveQuoteMatch { + keys = ['i', '`']; + charToMatch = '`'; + includeSurrounding = false; +} + +@RegisterAction +export class MoveABacktick extends MoveQuoteMatch { + keys = ['a', '`']; + charToMatch = '`'; + includeSurrounding = true; +} + +@RegisterAction +class MoveToUnclosedRoundBracketBackward extends MoveToMatchingBracket { + keys = ['[', '(']; + + public async execAction(position: Position, vimState: VimState): Promise { + const failure = { start: position, stop: position, failed: true }; + const charToMatch = ')'; + const result = PairMatcher.nextPairedChar( + position.getLeftThroughLineBreaks(), + charToMatch, + false + ); + + if (!result) { + return failure; + } + return result; + } +} + +@RegisterAction +class MoveToUnclosedRoundBracketForward extends MoveToMatchingBracket { + keys = [']', ')']; + + public async execAction(position: Position, vimState: VimState): Promise { + const failure = { start: position, stop: position, failed: true }; + const charToMatch = '('; + const result = PairMatcher.nextPairedChar( + position.getRightThroughLineBreaks(), + charToMatch, + false + ); + + if (!result) { + return failure; + } + return result; + } +} + +@RegisterAction +class MoveToUnclosedCurlyBracketBackward extends MoveToMatchingBracket { + keys = ['[', '{']; + + public async execAction(position: Position, vimState: VimState): Promise { + const failure = { start: position, stop: position, failed: true }; + const charToMatch = '}'; + const result = PairMatcher.nextPairedChar( + position.getLeftThroughLineBreaks(), + charToMatch, + false + ); + + if (!result) { + return failure; + } + return result; + } +} + +@RegisterAction +class MoveToUnclosedCurlyBracketForward extends MoveToMatchingBracket { + keys = [']', '}']; + + public async execAction(position: Position, vimState: VimState): Promise { + const failure = { start: position, stop: position, failed: true }; + const charToMatch = '{'; + const result = PairMatcher.nextPairedChar( + position.getRightThroughLineBreaks(), + charToMatch, + false + ); + + if (!result) { + return failure; + } + return result; + } +} + +abstract class MoveTagMatch extends BaseMovement { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualBlock]; + protected includeTag = false; + + public async execAction(position: Position, vimState: VimState): Promise { + const editorText = TextEditor.getText(); + const offset = TextEditor.getOffsetAt(position); + const tagMatcher = new TagMatcher(editorText, offset); + const start = tagMatcher.findOpening(this.includeTag); + const end = tagMatcher.findClosing(this.includeTag); + + if (start === undefined || end === undefined) { + return { + start: position, + stop: position, + failed: true, + }; + } + + let startPosition = start ? TextEditor.getPositionAt(start) : position; + let endPosition = end ? TextEditor.getPositionAt(end) : position; + + if (position.isAfter(endPosition)) { + vimState.recordedState.transformations.push({ + type: 'moveCursor', + diff: endPosition.subtract(position), + }); + } else if (position.isBefore(startPosition)) { + vimState.recordedState.transformations.push({ + type: 'moveCursor', + diff: startPosition.subtract(position), + }); + } + if (start === end) { + if (vimState.recordedState.operator instanceof ChangeOperator) { + vimState.currentMode = ModeName.Insert; + } + return { + start: startPosition, + stop: startPosition, + failed: true, + }; + } + return { + start: startPosition, + stop: endPosition.getLeftThroughLineBreaks(true), + }; + } + + public 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 +export class MoveInsideTag extends MoveTagMatch { + keys = ['i', 't']; + includeTag = false; +} + +@RegisterAction +export class MoveAroundTag extends MoveTagMatch { + keys = ['a', 't']; + includeTag = true; +} + +export class ArrowsInInsertMode extends BaseMovement { + modes = [ModeName.Insert]; + keys: string[]; + canBePrefixedWithCount = true; + + public async execAction(position: Position, vimState: VimState): Promise { + // we are in Insert Mode and arrow keys will clear all other actions except the first action, which enters Insert Mode. + // Please note the arrow key movement can be repeated while using `.` but it can't be repeated when using `` in Insert Mode. + vimState.recordedState.actionsRun = [ + vimState.recordedState.actionsRun.shift()!, + vimState.recordedState.actionsRun.pop()!, + ]; + let newPosition: Position = position; + + switch (this.keys[0]) { + case '': + newPosition = await new MoveUpArrow().execAction(position, vimState); + break; + case '': + newPosition = await new MoveDownArrow().execAction(position, vimState); + break; + case '': + newPosition = await new MoveLeftArrow().execAction(position, vimState); + break; + case '': + newPosition = await new MoveRightArrow().execAction(position, vimState); + break; + default: + break; + } + vimState.replaceState = new ReplaceState(newPosition); + return newPosition; + } +} + +@RegisterAction +class UpArrowInInsertMode extends ArrowsInInsertMode { + keys = ['']; +} + +@RegisterAction +class DownArrowInInsertMode extends ArrowsInInsertMode { + keys = ['']; +} + +@RegisterAction +class LeftArrowInInsertMode extends ArrowsInInsertMode { + keys = ['']; +} + +@RegisterAction +class RightArrowInInsertMode extends ArrowsInInsertMode { + keys = ['']; +} diff --git a/src/cmd_line/commands/setoptions.ts b/src/cmd_line/commands/setoptions.ts index 4e0ab8e5cc0..8e99c2696f8 100644 --- a/src/cmd_line/commands/setoptions.ts +++ b/src/cmd_line/commands/setoptions.ts @@ -1,106 +1,106 @@ -import * as node from '../node'; -import * as util from '../../util'; -import { Configuration } from '../../configuration/configuration'; - -export enum SetOptionOperator { - /* - * Set string or number option to {value}. - * White space between {option} and '=' is allowed and will be ignored. White space between '=' and {value} is not allowed. - */ - Equal, - /* - * Toggle option: set, switch it on. - * Number option: show value. - * String option: show value. - */ - Set, - /* - * Toggle option: Reset, switch it off. - */ - Reset, - /** - * Toggle option: Insert value. - */ - Invert, - /* - * Add the {value} to a number option, or append the {value} to a string option. - * When the option is a comma separated list, a comma is added, unless the value was empty. - */ - Append, - /* - * Subtract the {value} from a number option, or remove the {value} from a string option, if it is there. - */ - Subtract, - /** - * Multiply the {value} to a number option, or prepend the {value} to a string option. - */ - Multiply, - /** - * Show value of {option}. - */ - Info, -} - -export interface IOptionArgs extends node.ICommandArgs { - name?: string; - operator?: SetOptionOperator; - value?: string | number | boolean; -} - -export class SetOptionsCommand extends node.CommandBase { - protected _arguments: IOptionArgs; - - constructor(args: IOptionArgs) { - super(); - this._name = 'setoptions'; - this._arguments = args; - } - - get arguments(): IOptionArgs { - return this._arguments; - } - - async execute(): Promise { - if (!this._arguments.name) { - throw new Error('Unknown option'); - } - - switch (this._arguments.operator) { - case SetOptionOperator.Set: - Configuration[this._arguments.name] = true; - break; - case SetOptionOperator.Reset: - Configuration[this._arguments.name] = false; - break; - case SetOptionOperator.Equal: - Configuration[this._arguments.name] = this._arguments.value!; - break; - case SetOptionOperator.Invert: - Configuration[this._arguments.name] = !Configuration[this._arguments.name]; - break; - case SetOptionOperator.Append: - Configuration[this._arguments.name] += this._arguments.value!; - break; - case SetOptionOperator.Subtract: - if (typeof this._arguments.value! === 'number') { - Configuration[this._arguments.name] -= this._arguments.value! as number; - } else { - let initialValue = Configuration[this._arguments.name]; - Configuration[this._arguments.name] = initialValue - .split(this._arguments.value! as string) - .join(''); - } - break; - case SetOptionOperator.Info: - let value = Configuration[this._arguments.name]; - if (value === undefined) { - await util.showError(`E518 Unknown option: ${this._arguments.name}`); - } else { - await util.showInfo(`${this._arguments.name}=${value}`); - } - break; - default: - break; - } - } -} +import * as node from '../node'; +import * as util from '../../util'; +import { Configuration } from '../../configuration/configuration'; + +export enum SetOptionOperator { + /* + * Set string or number option to {value}. + * White space between {option} and '=' is allowed and will be ignored. White space between '=' and {value} is not allowed. + */ + Equal, + /* + * Toggle option: set, switch it on. + * Number option: show value. + * String option: show value. + */ + Set, + /* + * Toggle option: Reset, switch it off. + */ + Reset, + /** + * Toggle option: Insert value. + */ + Invert, + /* + * Add the {value} to a number option, or append the {value} to a string option. + * When the option is a comma separated list, a comma is added, unless the value was empty. + */ + Append, + /* + * Subtract the {value} from a number option, or remove the {value} from a string option, if it is there. + */ + Subtract, + /** + * Multiply the {value} to a number option, or prepend the {value} to a string option. + */ + Multiply, + /** + * Show value of {option}. + */ + Info, +} + +export interface IOptionArgs extends node.ICommandArgs { + name?: string; + operator?: SetOptionOperator; + value?: string | number | boolean; +} + +export class SetOptionsCommand extends node.CommandBase { + protected _arguments: IOptionArgs; + + constructor(args: IOptionArgs) { + super(); + this._name = 'setoptions'; + this._arguments = args; + } + + get arguments(): IOptionArgs { + return this._arguments; + } + + async execute(): Promise { + if (!this._arguments.name) { + throw new Error('Unknown option'); + } + + switch (this._arguments.operator) { + case SetOptionOperator.Set: + Configuration[this._arguments.name] = true; + break; + case SetOptionOperator.Reset: + Configuration[this._arguments.name] = false; + break; + case SetOptionOperator.Equal: + Configuration[this._arguments.name] = this._arguments.value!; + break; + case SetOptionOperator.Invert: + Configuration[this._arguments.name] = !Configuration[this._arguments.name]; + break; + case SetOptionOperator.Append: + Configuration[this._arguments.name] += this._arguments.value!; + break; + case SetOptionOperator.Subtract: + if (typeof this._arguments.value! === 'number') { + Configuration[this._arguments.name] -= this._arguments.value! as number; + } else { + let initialValue = Configuration[this._arguments.name]; + Configuration[this._arguments.name] = initialValue + .split(this._arguments.value! as string) + .join(''); + } + break; + case SetOptionOperator.Info: + let value = Configuration[this._arguments.name]; + if (value === undefined) { + await util.showError(`E518 Unknown option: ${this._arguments.name}`); + } else { + await util.showInfo(`${this._arguments.name}=${value}`); + } + break; + default: + break; + } + } +} diff --git a/src/cmd_line/subparsers/setoptions.ts b/src/cmd_line/subparsers/setoptions.ts index a3ba5139c36..69c0903e8c0 100644 --- a/src/cmd_line/subparsers/setoptions.ts +++ b/src/cmd_line/subparsers/setoptions.ts @@ -1,76 +1,76 @@ -import * as node from '../commands/setoptions'; -import { Scanner } from '../scanner'; - -export function parseOption(args: string): node.IOptionArgs { - let scanner = new Scanner(args); - scanner.skipWhiteSpace(); - - if (scanner.isAtEof) { - return {}; - } - - let optionName = scanner.nextWord('?!&=:^+-'.split('')); - - if (optionName.startsWith('no')) { - return { - name: optionName.substring(2, optionName.length), - operator: node.SetOptionOperator.Reset, - }; - } - - if (optionName.startsWith('inv')) { - return { - name: optionName.substring(3, optionName.length), - operator: node.SetOptionOperator.Invert, - }; - } - - scanner.skipWhiteSpace(); - - if (scanner.isAtEof) { - return { - name: optionName, - operator: node.SetOptionOperator.Set, - }; - } - - let operator = scanner.next(); - let optionArgs: node.IOptionArgs = { - name: optionName, - value: scanner.nextWord([]), - }; - - switch (operator) { - case '=': - case ':': - optionArgs.operator = node.SetOptionOperator.Equal; - break; - case '!': - optionArgs.operator = node.SetOptionOperator.Invert; - break; - case '^': - optionArgs.operator = node.SetOptionOperator.Multiply; - break; - case '+': - optionArgs.operator = node.SetOptionOperator.Append; - break; - case '-': - optionArgs.operator = node.SetOptionOperator.Subtract; - break; - case '?': - optionArgs.operator = node.SetOptionOperator.Info; - break; - case '&': - optionArgs.operator = node.SetOptionOperator.Reset; - break; - default: - throw new Error('Unknown option'); - } - - return optionArgs; -} - -export function parseOptionsCommandArgs(args: string): node.SetOptionsCommand { - let option = parseOption(args); - return new node.SetOptionsCommand(option); -} +import * as node from '../commands/setoptions'; +import { Scanner } from '../scanner'; + +export function parseOption(args: string): node.IOptionArgs { + let scanner = new Scanner(args); + scanner.skipWhiteSpace(); + + if (scanner.isAtEof) { + return {}; + } + + let optionName = scanner.nextWord('?!&=:^+-'.split('')); + + if (optionName.startsWith('no')) { + return { + name: optionName.substring(2, optionName.length), + operator: node.SetOptionOperator.Reset, + }; + } + + if (optionName.startsWith('inv')) { + return { + name: optionName.substring(3, optionName.length), + operator: node.SetOptionOperator.Invert, + }; + } + + scanner.skipWhiteSpace(); + + if (scanner.isAtEof) { + return { + name: optionName, + operator: node.SetOptionOperator.Set, + }; + } + + let operator = scanner.next(); + let optionArgs: node.IOptionArgs = { + name: optionName, + value: scanner.nextWord([]), + }; + + switch (operator) { + case '=': + case ':': + optionArgs.operator = node.SetOptionOperator.Equal; + break; + case '!': + optionArgs.operator = node.SetOptionOperator.Invert; + break; + case '^': + optionArgs.operator = node.SetOptionOperator.Multiply; + break; + case '+': + optionArgs.operator = node.SetOptionOperator.Append; + break; + case '-': + optionArgs.operator = node.SetOptionOperator.Subtract; + break; + case '?': + optionArgs.operator = node.SetOptionOperator.Info; + break; + case '&': + optionArgs.operator = node.SetOptionOperator.Reset; + break; + default: + throw new Error('Unknown option'); + } + + return optionArgs; +} + +export function parseOptionsCommandArgs(args: string): node.SetOptionsCommand { + let option = parseOption(args); + return new node.SetOptionsCommand(option); +} diff --git a/src/common/matching/matcher.ts b/src/common/matching/matcher.ts index e6a756d72dd..619dba5c70e 100644 --- a/src/common/matching/matcher.ts +++ b/src/common/matching/matcher.ts @@ -1,208 +1,208 @@ -import { Position, PositionDiff } from './../motion/position'; -import { TextEditor } from './../../textEditor'; -import * as vscode from 'vscode'; - -function escapeRegExpCharacters(value: string): string { - return value.replace(/[\-\\\{\}\*\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&'); -} - -let toReversedString = (function() { - function reverse(str: string): string { - let reversedStr = ''; - for (let i = str.length - 1; i >= 0; i--) { - reversedStr += str.charAt(i); - } - return reversedStr; - } - - let lastInput: string = ''; - let lastOutput: string = ''; - return function(str: string): string { - if (lastInput !== str) { - lastInput = str; - lastOutput = reverse(lastInput); - } - return lastOutput; - }; -})(); - -/** - * PairMatcher finds the position matching the given character, respecting nested - * instances of the pair. - */ -export class PairMatcher { - static pairings: { - [key: string]: { - match: string; - nextMatchIsForward: boolean; - directionLess?: boolean; - matchesWithPercentageMotion?: boolean; - }; - } = { - '(': { match: ')', nextMatchIsForward: true, matchesWithPercentageMotion: true }, - '{': { match: '}', nextMatchIsForward: true, matchesWithPercentageMotion: true }, - '[': { match: ']', nextMatchIsForward: true, matchesWithPercentageMotion: true }, - ')': { match: '(', nextMatchIsForward: false, matchesWithPercentageMotion: true }, - '}': { match: '{', nextMatchIsForward: false, matchesWithPercentageMotion: true }, - ']': { match: '[', nextMatchIsForward: false, matchesWithPercentageMotion: true }, - // These characters can't be used for "%"-based matching, but are still - // useful for text objects. - '<': { match: '>', nextMatchIsForward: true }, - '>': { match: '<', nextMatchIsForward: 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: '"', nextMatchIsForward: false, directionLess: true }, - "'": { match: "'", nextMatchIsForward: false, directionLess: true }, - '`': { match: '`', nextMatchIsForward: false, directionLess: true }, - }; - - static nextPairedChar( - position: Position, - charToMatch: string, - closed: boolean = true - ): 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 toFind = this.pairings[charToMatch]; - - if (toFind === undefined || toFind.directionLess) { - return undefined; - } - - let regex = new RegExp( - '(' + escapeRegExpCharacters(charToMatch) + '|' + escapeRegExpCharacters(toFind.match) + ')', - 'i' - ); - - let stackHeight = closed ? 0 : 1; - let matchedPosition: Position | undefined = undefined; - - // find matched bracket up - if (!toFind.nextMatchIsForward) { - for (let lineNumber = position.line; lineNumber >= 0; lineNumber--) { - let lineText = TextEditor.getLineAt(new Position(lineNumber, 0)).text; - let startOffset = - lineNumber === position.line ? lineText.length - position.character - 1 : 0; - - while (true) { - let queryText = toReversedString(lineText).substr(startOffset); - if (queryText === '') { - break; - } - - let m = queryText.match(regex); - - if (!m) { - break; - } - - let matchedChar = m[0]; - if (matchedChar === charToMatch) { - stackHeight++; - } - - if (matchedChar === toFind.match) { - stackHeight--; - } - - if (stackHeight === 0) { - matchedPosition = new Position( - lineNumber, - lineText.length - startOffset - m.index! - 1 - ); - return matchedPosition; - } - - startOffset = startOffset + m.index! + 1; - } - } - } else { - for ( - let lineNumber = position.line, lineCount = TextEditor.getLineCount(); - lineNumber < lineCount; - lineNumber++ - ) { - let lineText = TextEditor.getLineAt(new Position(lineNumber, 0)).text; - let startOffset = lineNumber === position.line ? position.character : 0; - - while (true) { - let queryText = lineText.substr(startOffset); - if (queryText === '') { - break; - } - - let m = queryText.match(regex); - - if (!m) { - break; - } - - let matchedChar = m[0]; - if (matchedChar === charToMatch) { - stackHeight++; - } - - if (matchedChar === toFind.match) { - stackHeight--; - } - - if (stackHeight === 0) { - matchedPosition = new Position(lineNumber, startOffset + m.index!); - return matchedPosition; - } - - startOffset = startOffset + m.index! + 1; - } - } - } - - if (matchedPosition) { - return matchedPosition; - } - - // TODO(bell) - return undefined; - } - - /** - * Given a current position, find an immediate following bracket and return the range. If - * no matching bracket is found immediately following the opening bracket, return undefined. - */ - static immediateMatchingBracket(currentPosition: Position): vscode.Range | undefined { - // Don't delete bracket unless autoClosingBrackets is set - if (!vscode.workspace.getConfiguration().get('editor.autoClosingBrackets')) { - return undefined; - } - - const deleteRange = new vscode.Range( - currentPosition, - currentPosition.getLeftThroughLineBreaks() - ); - const deleteText = vscode.window.activeTextEditor!.document.getText(deleteRange); - let matchRange: vscode.Range | undefined; - let isNextMatch = false; - - if ('{[("\'`'.indexOf(deleteText) > -1) { - const matchPosition = currentPosition.add(new PositionDiff(0, 1)); - matchRange = new vscode.Range(matchPosition, matchPosition.getLeftThroughLineBreaks()); - isNextMatch = - vscode.window.activeTextEditor!.document.getText(matchRange) === - PairMatcher.pairings[deleteText].match; - } - - if (isNextMatch && matchRange) { - return matchRange; - } - - return undefined; - } -} +import { Position, PositionDiff } from './../motion/position'; +import { TextEditor } from './../../textEditor'; +import * as vscode from 'vscode'; + +function escapeRegExpCharacters(value: string): string { + return value.replace(/[\-\\\{\}\*\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&'); +} + +let toReversedString = (function() { + function reverse(str: string): string { + let reversedStr = ''; + for (let i = str.length - 1; i >= 0; i--) { + reversedStr += str.charAt(i); + } + return reversedStr; + } + + let lastInput: string = ''; + let lastOutput: string = ''; + return function(str: string): string { + if (lastInput !== str) { + lastInput = str; + lastOutput = reverse(lastInput); + } + return lastOutput; + }; +})(); + +/** + * PairMatcher finds the position matching the given character, respecting nested + * instances of the pair. + */ +export class PairMatcher { + static pairings: { + [key: string]: { + match: string; + nextMatchIsForward: boolean; + directionLess?: boolean; + matchesWithPercentageMotion?: boolean; + }; + } = { + '(': { match: ')', nextMatchIsForward: true, matchesWithPercentageMotion: true }, + '{': { match: '}', nextMatchIsForward: true, matchesWithPercentageMotion: true }, + '[': { match: ']', nextMatchIsForward: true, matchesWithPercentageMotion: true }, + ')': { match: '(', nextMatchIsForward: false, matchesWithPercentageMotion: true }, + '}': { match: '{', nextMatchIsForward: false, matchesWithPercentageMotion: true }, + ']': { match: '[', nextMatchIsForward: false, matchesWithPercentageMotion: true }, + // These characters can't be used for "%"-based matching, but are still + // useful for text objects. + '<': { match: '>', nextMatchIsForward: true }, + '>': { match: '<', nextMatchIsForward: 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: '"', nextMatchIsForward: false, directionLess: true }, + "'": { match: "'", nextMatchIsForward: false, directionLess: true }, + '`': { match: '`', nextMatchIsForward: false, directionLess: true }, + }; + + static nextPairedChar( + position: Position, + charToMatch: string, + closed: boolean = true + ): 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 toFind = this.pairings[charToMatch]; + + if (toFind === undefined || toFind.directionLess) { + return undefined; + } + + let regex = new RegExp( + '(' + escapeRegExpCharacters(charToMatch) + '|' + escapeRegExpCharacters(toFind.match) + ')', + 'i' + ); + + let stackHeight = closed ? 0 : 1; + let matchedPosition: Position | undefined = undefined; + + // find matched bracket up + if (!toFind.nextMatchIsForward) { + for (let lineNumber = position.line; lineNumber >= 0; lineNumber--) { + let lineText = TextEditor.getLineAt(new Position(lineNumber, 0)).text; + let startOffset = + lineNumber === position.line ? lineText.length - position.character - 1 : 0; + + while (true) { + let queryText = toReversedString(lineText).substr(startOffset); + if (queryText === '') { + break; + } + + let m = queryText.match(regex); + + if (!m) { + break; + } + + let matchedChar = m[0]; + if (matchedChar === charToMatch) { + stackHeight++; + } + + if (matchedChar === toFind.match) { + stackHeight--; + } + + if (stackHeight === 0) { + matchedPosition = new Position( + lineNumber, + lineText.length - startOffset - m.index! - 1 + ); + return matchedPosition; + } + + startOffset = startOffset + m.index! + 1; + } + } + } else { + for ( + let lineNumber = position.line, lineCount = TextEditor.getLineCount(); + lineNumber < lineCount; + lineNumber++ + ) { + let lineText = TextEditor.getLineAt(new Position(lineNumber, 0)).text; + let startOffset = lineNumber === position.line ? position.character : 0; + + while (true) { + let queryText = lineText.substr(startOffset); + if (queryText === '') { + break; + } + + let m = queryText.match(regex); + + if (!m) { + break; + } + + let matchedChar = m[0]; + if (matchedChar === charToMatch) { + stackHeight++; + } + + if (matchedChar === toFind.match) { + stackHeight--; + } + + if (stackHeight === 0) { + matchedPosition = new Position(lineNumber, startOffset + m.index!); + return matchedPosition; + } + + startOffset = startOffset + m.index! + 1; + } + } + } + + if (matchedPosition) { + return matchedPosition; + } + + // TODO(bell) + return undefined; + } + + /** + * Given a current position, find an immediate following bracket and return the range. If + * no matching bracket is found immediately following the opening bracket, return undefined. + */ + static immediateMatchingBracket(currentPosition: Position): vscode.Range | undefined { + // Don't delete bracket unless autoClosingBrackets is set + if (!vscode.workspace.getConfiguration().get('editor.autoClosingBrackets')) { + return undefined; + } + + const deleteRange = new vscode.Range( + currentPosition, + currentPosition.getLeftThroughLineBreaks() + ); + const deleteText = vscode.window.activeTextEditor!.document.getText(deleteRange); + let matchRange: vscode.Range | undefined; + let isNextMatch = false; + + if ('{[("\'`'.indexOf(deleteText) > -1) { + const matchPosition = currentPosition.add(new PositionDiff(0, 1)); + matchRange = new vscode.Range(matchPosition, matchPosition.getLeftThroughLineBreaks()); + isNextMatch = + vscode.window.activeTextEditor!.document.getText(matchRange) === + PairMatcher.pairings[deleteText].match; + } + + if (isNextMatch && matchRange) { + return matchRange; + } + + return undefined; + } +} diff --git a/src/mode/mode.ts b/src/mode/mode.ts index 48c74b5a7bc..83826e6b475 100644 --- a/src/mode/mode.ts +++ b/src/mode/mode.ts @@ -1,48 +1,48 @@ -export enum ModeName { - Normal, - Insert, - Visual, - VisualBlock, - VisualLine, - SearchInProgressMode, - Replace, - EasyMotionMode, - EasyMotionInputMode, - SurroundInputMode, -} - -export enum VSCodeVimCursorType { - Block, - Line, - LineThin, - Underline, - TextDecoration, - Native, -} - -export abstract class Mode { - private _isActive: boolean; - private _name: ModeName; - - public text: string; - public cursorType: VSCodeVimCursorType; - - public isVisualMode = false; - - constructor(name: ModeName) { - this._name = name; - this._isActive = false; - } - - get name(): ModeName { - return this._name; - } - - get isActive(): boolean { - return this._isActive; - } - - set isActive(val: boolean) { - this._isActive = val; - } -} +export enum ModeName { + Normal, + Insert, + Visual, + VisualBlock, + VisualLine, + SearchInProgressMode, + Replace, + EasyMotionMode, + EasyMotionInputMode, + SurroundInputMode, +} + +export enum VSCodeVimCursorType { + Block, + Line, + LineThin, + Underline, + TextDecoration, + Native, +} + +export abstract class Mode { + private _isActive: boolean; + private _name: ModeName; + + public text: string; + public cursorType: VSCodeVimCursorType; + + public isVisualMode = false; + + constructor(name: ModeName) { + this._name = name; + this._isActive = false; + } + + get name(): ModeName { + return this._name; + } + + get isActive(): boolean { + return this._isActive; + } + + set isActive(val: boolean) { + this._isActive = val; + } +} diff --git a/src/mode/modeInsert.ts b/src/mode/modeInsert.ts index 1eac99c34bc..b791a98a975 100644 --- a/src/mode/modeInsert.ts +++ b/src/mode/modeInsert.ts @@ -1,11 +1,11 @@ -import { ModeName, Mode } from './mode'; -import { VSCodeVimCursorType } from './mode'; - -export class InsertMode extends Mode { - public text = 'Insert Mode'; - public cursorType = VSCodeVimCursorType.Native; - - constructor() { - super(ModeName.Insert); - } -} +import { ModeName, Mode } from './mode'; +import { VSCodeVimCursorType } from './mode'; + +export class InsertMode extends Mode { + public text = 'Insert Mode'; + public cursorType = VSCodeVimCursorType.Native; + + constructor() { + super(ModeName.Insert); + } +} diff --git a/src/mode/modeNormal.ts b/src/mode/modeNormal.ts index c2dcdd7a94e..7589b5beddb 100644 --- a/src/mode/modeNormal.ts +++ b/src/mode/modeNormal.ts @@ -1,16 +1,16 @@ -import { ModeName, Mode } from './mode'; -import { ModeHandler } from './modeHandler'; -import { VSCodeVimCursorType } from './mode'; - -export class NormalMode extends Mode { - private _modeHandler: ModeHandler; - - public text = 'Normal Mode'; - public cursorType = VSCodeVimCursorType.Block; - - constructor(modeHandler: ModeHandler) { - super(ModeName.Normal); - - this._modeHandler = modeHandler; - } -} +import { ModeName, Mode } from './mode'; +import { ModeHandler } from './modeHandler'; +import { VSCodeVimCursorType } from './mode'; + +export class NormalMode extends Mode { + private _modeHandler: ModeHandler; + + public text = 'Normal Mode'; + public cursorType = VSCodeVimCursorType.Block; + + constructor(modeHandler: ModeHandler) { + super(ModeName.Normal); + + this._modeHandler = modeHandler; + } +} diff --git a/src/mode/modeSearchInProgress.ts b/src/mode/modeSearchInProgress.ts index 9fb42056ea4..541045a1b3a 100644 --- a/src/mode/modeSearchInProgress.ts +++ b/src/mode/modeSearchInProgress.ts @@ -1,11 +1,11 @@ -import { ModeName, Mode } from './mode'; -import { VSCodeVimCursorType } from './mode'; - -export class SearchInProgressMode extends Mode { - public text = 'Search In Progress'; - public cursorType = VSCodeVimCursorType.Block; - - constructor() { - super(ModeName.SearchInProgressMode); - } -} +import { ModeName, Mode } from './mode'; +import { VSCodeVimCursorType } from './mode'; + +export class SearchInProgressMode extends Mode { + public text = 'Search In Progress'; + public cursorType = VSCodeVimCursorType.Block; + + constructor() { + super(ModeName.SearchInProgressMode); + } +} diff --git a/src/mode/modeVisualBlock.ts b/src/mode/modeVisualBlock.ts index 07271c1d3ee..0279cc0afc6 100644 --- a/src/mode/modeVisualBlock.ts +++ b/src/mode/modeVisualBlock.ts @@ -1,33 +1,33 @@ -import { ModeName, Mode } from './mode'; -import { VSCodeVimCursorType } from './mode'; -import { Position } from './../common/motion/position'; - -export class VisualBlockMode extends Mode { - public text = 'Visual Block Mode'; - public cursorType = VSCodeVimCursorType.TextDecoration; - public isVisualMode = true; - - constructor() { - super(ModeName.VisualBlock); - } - - public static getTopLeftPosition(start: Position, stop: Position): Position { - return new Position(Math.min(start.line, stop.line), Math.min(start.character, stop.character)); - } - - public static getBottomRightPosition(start: Position, stop: Position): Position { - return new Position(Math.max(start.line, stop.line), Math.max(start.character, stop.character)); - } -} - -export enum VisualBlockInsertionType { - /** - * Triggered with I - */ - Insert, - - /** - * Triggered with A - */ - Append, -} +import { ModeName, Mode } from './mode'; +import { VSCodeVimCursorType } from './mode'; +import { Position } from './../common/motion/position'; + +export class VisualBlockMode extends Mode { + public text = 'Visual Block Mode'; + public cursorType = VSCodeVimCursorType.TextDecoration; + public isVisualMode = true; + + constructor() { + super(ModeName.VisualBlock); + } + + public static getTopLeftPosition(start: Position, stop: Position): Position { + return new Position(Math.min(start.line, stop.line), Math.min(start.character, stop.character)); + } + + public static getBottomRightPosition(start: Position, stop: Position): Position { + return new Position(Math.max(start.line, stop.line), Math.max(start.character, stop.character)); + } +} + +export enum VisualBlockInsertionType { + /** + * Triggered with I + */ + Insert, + + /** + * Triggered with A + */ + Append, +} diff --git a/test/mode/modeReplace.test.ts b/test/mode/modeReplace.test.ts index 252b8e7300c..2b9e270cc80 100644 --- a/test/mode/modeReplace.test.ts +++ b/test/mode/modeReplace.test.ts @@ -1,98 +1,98 @@ -import { setupWorkspace, cleanUpWorkspace } from './../testUtils'; -import { ModeName } from '../../src/mode/mode'; -import { ModeHandler } from '../../src/mode/modeHandler'; -import { getTestingFunctions } from '../testSimplifier'; - -suite('Mode Replace', () => { - let { newTest, newTestOnly } = getTestingFunctions(); - - setup(async () => { - await setupWorkspace(); - }); - - teardown(cleanUpWorkspace); - - newTest({ - title: 'Can handle R', - start: ['123|456'], - keysPressed: 'Rab', - end: ['123ab|6'], - }); - - newTest({ - title: 'Can handle R', - start: ['123|456'], - keysPressed: 'Rabcd', - end: ['123abcd|'], - }); - - newTest({ - title: 'Can handle R and quit Replace Mode', - start: ['|123456'], - keysPressed: 'Rabc', - end: ['ab|c456'], - }); - - newTest({ - title: 'Can handle R across lines', - start: ['123|456', '789'], - keysPressed: 'Rabcd\nefg', - end: ['123abcd', 'efg|', '789'], - }); - - newTest({ - title: 'Can handle R across lines and quit Replace Mode', - start: ['123|456', '789'], - keysPressed: 'Rabcd\nefg', - end: ['123abcd', 'ef|g', '789'], - }); - - newTest({ - title: 'Can handle R with {count}', - start: ['123|456', '789'], - keysPressed: '3Rabc\ndef', - end: ['123abc', 'defabc', 'defabc', 'de|f', '789'], - }); - - newTest({ - title: 'Can handle backspace', - start: ['123|456'], - keysPressed: 'Rabc', - end: ['123|456'], - }); - - newTest({ - title: 'Can handle backspace', - start: ['123|456'], - keysPressed: 'Rabcd', - end: ['12|3456'], - }); - - newTest({ - title: 'Can handle backspace across lines', - start: ['123|456'], - keysPressed: 'Rabcd\nef', - end: ['123ab|6'], - }); - - newTest({ - title: 'Can handle arrows', - start: ['123|456'], - keysPressed: 'Rabc', - end: ['123|abc'], - }); - - newTest({ - title: 'Can handle .', - start: ['123|456', '123456'], - keysPressed: 'Rabcj0.', - end: ['123abc', 'ab|c456'], - }); - - newTest({ - title: 'Can handle . across lines', - start: ['123|456', '123456'], - keysPressed: 'Rabc\ndefj0.', - end: ['123abc', 'def', 'abc', 'de|f'], - }); -}); +import { setupWorkspace, cleanUpWorkspace } from './../testUtils'; +import { ModeName } from '../../src/mode/mode'; +import { ModeHandler } from '../../src/mode/modeHandler'; +import { getTestingFunctions } from '../testSimplifier'; + +suite('Mode Replace', () => { + let { newTest, newTestOnly } = getTestingFunctions(); + + setup(async () => { + await setupWorkspace(); + }); + + teardown(cleanUpWorkspace); + + newTest({ + title: 'Can handle R', + start: ['123|456'], + keysPressed: 'Rab', + end: ['123ab|6'], + }); + + newTest({ + title: 'Can handle R', + start: ['123|456'], + keysPressed: 'Rabcd', + end: ['123abcd|'], + }); + + newTest({ + title: 'Can handle R and quit Replace Mode', + start: ['|123456'], + keysPressed: 'Rabc', + end: ['ab|c456'], + }); + + newTest({ + title: 'Can handle R across lines', + start: ['123|456', '789'], + keysPressed: 'Rabcd\nefg', + end: ['123abcd', 'efg|', '789'], + }); + + newTest({ + title: 'Can handle R across lines and quit Replace Mode', + start: ['123|456', '789'], + keysPressed: 'Rabcd\nefg', + end: ['123abcd', 'ef|g', '789'], + }); + + newTest({ + title: 'Can handle R with {count}', + start: ['123|456', '789'], + keysPressed: '3Rabc\ndef', + end: ['123abc', 'defabc', 'defabc', 'de|f', '789'], + }); + + newTest({ + title: 'Can handle backspace', + start: ['123|456'], + keysPressed: 'Rabc', + end: ['123|456'], + }); + + newTest({ + title: 'Can handle backspace', + start: ['123|456'], + keysPressed: 'Rabcd', + end: ['12|3456'], + }); + + newTest({ + title: 'Can handle backspace across lines', + start: ['123|456'], + keysPressed: 'Rabcd\nef', + end: ['123ab|6'], + }); + + newTest({ + title: 'Can handle arrows', + start: ['123|456'], + keysPressed: 'Rabc', + end: ['123|abc'], + }); + + newTest({ + title: 'Can handle .', + start: ['123|456', '123456'], + keysPressed: 'Rabcj0.', + end: ['123abc', 'ab|c456'], + }); + + newTest({ + title: 'Can handle . across lines', + start: ['123|456', '123456'], + keysPressed: 'Rabc\ndefj0.', + end: ['123abc', 'def', 'abc', 'de|f'], + }); +}); diff --git a/tsconfig.json b/tsconfig.json index b0829f7d25d..9c54164725f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,22 @@ -{ - "compilerOptions": { - "allowUnusedLabels": false, - "module": "commonjs", - "target": "es6", - "outDir": "out", - "noImplicitAny": true, - "noImplicitReturns": true, - "suppressImplicitAnyIndexErrors": true, - "lib": [ - "es6" - ], - "sourceMap": true, - "strictNullChecks": true, - "experimentalDecorators": true, - "alwaysStrict": true - }, - "exclude": [ - "node_modules", - ".vscode-test" - ] -} +{ + "compilerOptions": { + "allowUnusedLabels": false, + "module": "commonjs", + "target": "es6", + "outDir": "out", + "noImplicitAny": true, + "noImplicitReturns": true, + "suppressImplicitAnyIndexErrors": true, + "lib": [ + "es6" + ], + "sourceMap": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "alwaysStrict": true + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} diff --git a/tslint.json b/tslint.json index ba2f674ca5d..5b0ec93c792 100644 --- a/tslint.json +++ b/tslint.json @@ -1,88 +1,88 @@ -{ - "rules": { - "align": false, - "ban": false, - "class-name": true, - "comment-format": [ - true, - "check-space" - ], - "curly": true, - "eofline": false, - "forin": true, - "indent": [ - true, - "spaces" - ], - "interface-name": false, - "jsdoc-format": true, - "label-position": true, - "max-line-length": [ - true, - 140 - ], - "member-access": false, - "member-ordering": false, - "no-any": false, - "no-arg": true, - "no-bitwise": true, - "no-conditional-assignment": true, - "no-consecutive-blank-lines": false, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], - "no-construct": true, - "no-parameter-properties": true, - "no-debugger": true, - "no-duplicate-variable": true, - "no-empty": true, - "no-eval": true, - "no-inferrable-types": false, - "no-internal-module": false, - "no-null-keyword": false, - "no-require-imports": false, - "no-shadowed-variable": true, - "no-string-literal": true, - "no-switch-case-fall-through": true, - "no-trailing-whitespace": true, - "no-unused-expression": true, - "no-unused-variable": true, - "no-use-before-declare": true, - "no-var-keyword": false, - "no-var-requires": false, - "object-literal-sort-keys": false, - "one-line": [ - true, - "check-open-brace", - "check-catch", - "check-else", - "check-finally", - "check-whitespace" - ], - "quotemark": false, - "radix": true, - "semicolon": [true, "always"], - "switch-default": false, - "trailing-comma": false, - "triple-equals": [ - true, - "allow-null-check" - ], - "typedef": false, - "typedef-whitespace": false, - "variable-name": false, - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type" - ] - } +{ + "rules": { + "align": false, + "ban": false, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": false, + "forin": true, + "indent": [ + true, + "spaces" + ], + "interface-name": false, + "jsdoc-format": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": false, + "no-any": false, + "no-arg": true, + "no-bitwise": true, + "no-conditional-assignment": true, + "no-consecutive-blank-lines": false, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-parameter-properties": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": true, + "no-eval": true, + "no-inferrable-types": false, + "no-internal-module": false, + "no-null-keyword": false, + "no-require-imports": false, + "no-shadowed-variable": true, + "no-string-literal": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-unused-variable": true, + "no-use-before-declare": true, + "no-var-keyword": false, + "no-var-requires": false, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-finally", + "check-whitespace" + ], + "quotemark": false, + "radix": true, + "semicolon": [true, "always"], + "switch-default": false, + "trailing-comma": false, + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef": false, + "typedef-whitespace": false, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } } \ No newline at end of file diff --git a/typings.json b/typings.json index 3788007bda4..a98c04c3047 100644 --- a/typings.json +++ b/typings.json @@ -1,11 +1,11 @@ -{ - "name": "vim", - "dependencies": { - "diff": "registry:npm/diff#2.0.0+20160211003958", - "lodash": "registry:npm/lodash#4.0.0+20160416211519" - }, - "globalDependencies": { - "copy-paste": "registry:dt/copy-paste#1.1.3+20160117130525", - "diff-match-patch": "registry:dt/diff-match-patch#1.0.0+20160821140300" - } -} +{ + "name": "vim", + "dependencies": { + "diff": "registry:npm/diff#2.0.0+20160211003958", + "lodash": "registry:npm/lodash#4.0.0+20160416211519" + }, + "globalDependencies": { + "copy-paste": "registry:dt/copy-paste#1.1.3+20160117130525", + "diff-match-patch": "registry:dt/diff-match-patch#1.0.0+20160821140300" + } +}