initial commit

This commit is contained in:
garlandcrow 2022-11-09 13:28:36 +09:00
commit 352193fc4e
47 changed files with 8577 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
out
node_modules
.vscode-test/
.vsix
*.log
*.vsix
.env

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
nodejs 16.14.2

30
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,30 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": ["${workspaceFolder}/out/test/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
}
]
}

16
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,16 @@
// {
// "files.exclude": {
// "out": true,
// },
// "typescript.tsdk": "node_modules/typescript/lib",
// }
{
"files.exclude": {
"out": true // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}

20
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,20 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

8
.vscodeignore Normal file
View File

@ -0,0 +1,8 @@
.vscode/**
.vscode-test/**
out/test/**
out/**/*.map
src/**
.gitignore
tsconfig.json
tslint.json

9
LICENSE.txt Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright 2018 Jonathan Potter
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

201
README.md Normal file
View File

@ -0,0 +1,201 @@
# SimpleVim
An opinionated Vim-like extension for VSCode that prioritizes simplicity (of use and implementation) and integration with native VSCode features.
Once you enter Insert mode it will be a completely vanilla VSCode experience: the only event SimpleVim will listen for is the `Escape` key to go back to Normal mode.
## Operators
Operators act on a range of text. In Normal mode the range is specified by the OperatorRange typed after the operator. In Visual mode it is the visual selection.
| Keys | Description |
|-|-|
| `d` | Delete range. |
| `c` | Delete range and enter insert mode. |
| `y` | Yank range. |
| `r` | Yank and delete range. |
| `s` | Select range and enter Visual mode. |
| `q` | Duplicate line. |
## OperatorRanges
OperatorRanges select a range for an Operator to act on. They must be used in Normal mode by typing an Operator and then an OperatorRange.
| Keys | Description |
|-|-|
| `l` | Character under cursor. |
| `j` | Character to the left of cursor. |
| `i` | Current line and line above. |
| `k` | Current line and line below. |
| `w` | From cursor to beginning of next word. |
| `W` | From cursor to beginning of next word (including punctuation). |
| `b` | From cursor to beginning of previous word. |
| `B` | From cursor to beginning of previous word (including punctuation). |
| `e` | From cursor to end of next word. |
| `E` | From cursor to end of next word (including punctuation). |
| `iw` | Word under cursor. |
| `iW` | Word (including punctuation) under cursor. |
| `aw` | Word under cursor and whitespace after. |
| `aW` | Word (including punctuation) under cursor and whitespace after. |
| `f[char][char]` | From cursor to next occurrence (case sensitive) of [char][char]. |
| `F[char][char]` | From cursor to previous occurrence (case sensitive) of [char][char]. |
| `t[char]` | From cursor to next occurrence (case sensitive) of [char]. |
| `T[char]` | From cursor to previous occurrence (case sensitive) of [char]. |
| `gg` | From current line to first line of the document. |
| `G` | From current line to last line of the document. |
| `}` | From current line to beginning of next paragraph. |
| `{` | From current line to beginning of previous paragraph. |
| `ip` | Current paragraph. |
| `ap` | Current paragraph and whitespace after. |
| `i[bracket]` | Inside the matching `[bracket]`s. Where `[bracket]` is a quote or opening bracket character (any of ``'"`({[<``). |
| `a[bracket]` | Outside the matching `[bracket]`s. Where `[bracket]` is a quote or opening bracket character (any of ``'"`({[<``). |
| `it` | Inside XML tag. |
| `at` | Outside XML tag. |
| `ii` | Inside indentation level. |
| `[number]i` | Including [number] of lines up. |
| `[number]k` | Including [number] of lines down. |
## Motions
Motions move the cursor and can be used in Normal or Visual mode. In Visual mode they only move one side of the selection; the other side stays anchored to where it was when you entered Visual mode.
| Keys | Description |
|-|-|
| `l` | Character right. |
| `j` | Character left. |
| `i` | Line up. |
| `k` | Line down. |
| `w` | Word right. |
| `W` | Word (including punctuation) right. |
| `b` | Word left. |
| `B` | Word (including punctuation) left. |
| `e` | Word end right. |
| `E` | Word end (including punctuation) right. |
| `o` | End of line. |
| `u` | Beginning of line. |
| `%` | Go to matching `[bracket]`. Where `[bracket]` is a bracket character (any of ({[<>]})). |
| `f[char][char]` | Next occurrence (case sensitive) of [char][char]. |
| `F[char][char]` | Previous occurrence (case sensitive) of [char][char]. |
| `t[char]` | Next occurrence (case sensitive) of [char]. |
| `T[char]` | Previous occurrence (case sensitive) of [char]. |
| `gg` | First line of the document. |
| `G` | Last line of the document. |
| `}` | Down a paragraph. |
| `{` | Up a paragraph. |
| `H` | Top of screen. |
| `M` | Middle of screen. |
| `L` | Bottom of screen. |
## Actions
Actions are miscellaneous commands that don't follow the well-defined patterns of Operators, OperatorRanges, or Motions.
| Keys | Description |
|-|-|
| `~` | Enter Insert mode. |
| `U` | Move to beginning of line and enter Insert mode. |
| `a` | Move one character to the right and enter Insert mode. |
| `O` | Move to end of line and enter Insert mode. |
| `v` | Enter VisualCharacter mode. |
| `V` | Enter VisualLine mode. |
| `Escape` | Enter Normal mode. |
| `K` | Insert line below and enter insert mode. |
| `I` | Insert line above and enter insert mode. |
| `p` | Put yanked text before cursor. |
| `P` | Put yanked text after cursor. |
| `gl` | Go to last edited location. |
| `gd` | Go to definition of symbol under cursor in current pane. |
| `gp` | Peek definition of symbol under cursor. |
| `gD` | Go to definition of symbol under cursor in new pane. |
| `gr` | Peek references of symbol under cursor. |
| `gR` | Show all references of symbol under cursor in sidebar. |
| `gh` | Show hover popup of symbol under cursor. |
| `gs` | Open Dash to lookup symbol under cursor (required Dash extension). |
| `gu` | Transform symbol under cursor to lowercase. |
| `gU` | Transform symbol under cursor to uppercase. |
| `n` | Delete character under cursor. |
| `h` | Delete character left of cursor. |
| `dd` | Delete current line. |
| `D` | Delete to the end of the line. |
| `cc` | Delete current line and enter Insert mode. |
| `C` | Delete to the end of the line and enter Insert mode. |
| `yy` | Yank current line. |
| `Y` | Yank to the end of the line. |
| `rr` | Yank current line and delete it. |
| `R` | Yank to the end of the line and delete it. |
| `ss` | Select current line. |
| `S` | Select to the end of the line. |
| `qq` | Copy line down. |
| `QQ` | Copy line up. |
| `[space]z` | Undo. |
| `[space]r` | Redo. |
| `[space]i` | Scroll half-page up. |
| `[space]k` | Scroll half-page down. |
| `x[char]` | Swap character under the cursor with next [char] typed. |
| `zz` | Scroll so that cursor is in the middle of the screen. |
| `:` | Goto line # |
| `;` | Repeat the last `f`, `F`, `t` or `T` motion forward. |
| `,` | Repeat the last `f`, `F`, `t` or `T` motion backward. |
## Occurance Match Mode
Occurance Match Mode lets you easily add cursors to the symbol your cursor is under.
| Keys | Description |
|-|-|
| `[space][space]` | Enter occurance match mode. |
| `p` | Add cursor to to previous match. |
| `n` | Add cursor to to next match. |
| `a` | Add cursor to to all matches in document. |
## Bookmarks
Bookmarks are quick ways to jump to places in code. This requires `alefragnani.bookmarks` extension installed.
| Keys | Description |
|-|-|
| `mm` | Add bookmark to currnet line. |
| `mi` | Move to previous bookmark. |
| `mk` | Move to next bookmark. |
| `ml` | List bookmarks in current file. |
| `mL` | List bookmarks in all files. |
## Differences From Vim
SimpleVim prioritizes simplicity and integration with native VSCode features over compatability with Vim. If full Vim compatibility is important to you, consider trying a different extension. Here are some of the ways SimpleVim is different from Vim.
- SimpleVim has no macros. Instead it has first class multiple cursor support which you can use to achieve something similar. You can place additional cursors by any of the ways native to VSCode including: `Cmd+d`, `Cmd+Alt+Down` or `Alt+Click`. Simply place cursors everywhere you would have run the macro and see your changes to each place in real time.
- SimpleVim has no `.` (repeat) command. Use multiple cursors instead (see previous bullet).
- SimpleVim lets the cursor go one past the last character of the line in Normal mode. It would be nice to prevent this, but because of VSCode's selection model and extension API there is no good way to do it. It would require ugly hacks and would make other parts of the SimpleVim experience buggy.
- SimpleVim has no registers. Instead the operators have been modified so deleting text does not overwrite the text you yanked. A new `r` operator has been added for when you want to yank and delete text at the same time.
- SimpleVim's `f` and `t` motions work slightly differently from Vim's. `t` and `f` behave like Vim's `/` command, but `t` takes one character and `f` takes two. Or in other words, `t` works like Vim's `t` in Normal mode but Vim's `f` in Visual mode. And `f` behaves like the vim-sneak plugin.
- SimpleVim has no `/` (search) command. Instead you can either use the `f` motion or the native VSCode find. Between them most of the uses for `/` are taken care of.
- SimpleVim has no `]` (indent) command. Instead you can use VSCode's `Cmd+]`.
- SimpleVim has no `gU` (uppercase) command. Instead you can use VSCode's `Transform to Uppercase` from the Command Palette.
- SimpleVim has no jump list (`Ctrl+o` and `Ctrl+i` in Vim). Instead you can use VSCode's native jump list with `Ctrl+-` and `Ctrl+_`.
- SimpleVim does not support marks. If you're jumping back and forth often between two places in a file you can use VSCode's split window feature, and use `Cmd+1` and `Cmd+2` to focus them. If you just need to jump back to where you've been, you can use VSCode's `Ctrl+-`.
## Settings
The `y` (yank) operator temporarily changes the background color of the range being yanked to make it obvious what you're yanking. Otherwise you might not realize you yanked the wrong thing until you tried to put it somewhere else. You can change the background color it uses with the `simpleVim.yankHighlightBackgroundColor` setting.
```json
{
"simpleVim.yankHighlightBackgroundColor": "#F8F3AB"
}
```

1
index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "string.prototype.matchall";

3716
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

93
package.json Normal file
View File

@ -0,0 +1,93 @@
{
"name": "vscode-helix",
"displayName": "Helix",
"description": "Helix-like key emulation for Visual Studio Code",
"version": "0.1.0",
"license": "MIT",
"publisher": "garlandcrow",
"keywords": [
"vim",
"vi",
"helix"
],
"repository": {
"type": "git",
"url": "https://github.com/garlandcrow/vscode-helix.git"
},
"engines": {
"vscode": "^1.22.2"
},
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"main": "./out/extension",
"contributes": {
"keybindings": [
{
"key": "shift+5",
"command": "editor.action.jumpToBracket",
"when": "editorTextFocus && !extension.simpleVim.insertMode"
},
{
"key": "shift+j",
"command": "editor.action.joinLines",
"when": "editorTextFocus && !extension.simpleVim.insertMode"
},
{
"key": "Escape",
"command": "extension.simpleVim.escapeKey",
"when": "editorTextFocus"
},
{
"key": "ctrl+f",
"command": "extension.simpleVim.scrollDownPage",
"when": "editorTextFocus && !extension.simpleVim.insertMode"
},
{
"key": "ctrl+b",
"command": "extension.simpleVim.scrollUpPage",
"when": "editorTextFocus && !extension.simpleVim.insertMode"
}
],
"configuration": {
"type": "object",
"title": "SimpleVim Configuration",
"properties": {
"simpleVim.yankHighlightBackgroundColor": {
"type": "string",
"default": "#F8F3AB",
"description": "Background color that flashes to show the range when yanking."
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"package": "vsce package",
"publish": "vsce publish",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"lint": "tslint -c tslint.json 'src/**/*.ts' --exclude 'src/test/*.ts'"
},
"devDependencies": {
"@types/http-errors": "^1.8.0",
"@types/node": "^12.12.58",
"string.prototype.matchall": "^4.0.2",
"tslint": "^5.10.0",
"typescript": "^4.0.2",
"vscode": "^1.1.18"
},
"dependencies": {
"vsce": "^2.11.0"
},
"__metadata": {
"id": "fb57f80d-708d-4ade-b92b-e420c6b5c04a",
"publisherDisplayName": "GarlandCrow",
"publisherId": "fe4ca483-01fb-4881-9e30-524a40026553",
"isPreReleaseVersion": false
}
}

10
src/action_types.ts Normal file
View File

@ -0,0 +1,10 @@
import * as vscode from "vscode";
import { VimState } from "./vim_state_types";
import { ParseKeysStatus } from "./parse_keys_types";
export type Action = (
vimState: VimState,
keys: string[],
editor: vscode.TextEditor
) => ParseKeysStatus;

659
src/actions/actions.ts Normal file
View File

@ -0,0 +1,659 @@
import * as vscode from "vscode";
import { Mode } from "../modes_types";
import { Action } from "../action_types";
import { parseKeysExact, parseKeysRegex } from "../parse_keys";
import {
enterInsertMode,
enterVisualMode,
enterVisualLineMode,
enterOccurrenceMode,
setModeCursorStyle
} from "../modes";
import * as positionUtils from "../position_utils";
import { removeTypeSubscription } from "../type_subscription";
import { VimState } from "../vim_state_types";
import { setVisualLineSelections } from "../visual_line_utils";
import { flashYankHighlight } from "../yank_highlight";
import { putAfter } from "../put_utils/put_after";
import { putBefore } from "../put_utils/put_before";
import { yank } from "./operators";
import KeyMap from "./keymaps";
enum Direction {
Up,
Down
}
export const actions: Action[] = [
parseKeysExact([":"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("workbench.action.gotoLine");
}),
parseKeysExact(["m", "l"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("bookmarks.list");
}),
parseKeysExact(["m", "L"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("bookmarks.listFromAllFiles");
}),
parseKeysExact(["m", "i"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("bookmarks.jumpToPrevious");
}),
parseKeysExact(["m", "k"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("bookmarks.jumpToNext");
}),
parseKeysExact(["m", "m"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("bookmarks.toggle");
}),
// new space actions
parseKeysExact([" ", " "], [Mode.Normal], (vimState, editor) => {
enterOccurrenceMode(vimState);
vscode.commands.executeCommand("editor.action.addSelectionToNextFindMatch");
}),
parseKeysExact(["p"], [Mode.Occurrence], (vimState, editor) => {
vscode.commands.executeCommand(
"editor.action.addSelectionToPreviousFindMatch"
);
}),
parseKeysExact(["n"], [Mode.Occurrence], (vimState, editor) => {
vscode.commands.executeCommand("editor.action.addSelectionToNextFindMatch");
}),
parseKeysExact(["a"], [Mode.Occurrence], (vimState, editor) => {
vscode.commands.executeCommand("editor.action.selectHighlights");
}),
parseKeysExact([" ", "z"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("undo");
}),
parseKeysExact([" ", "r"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("redo");
}),
parseKeysExact([" ", "i"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("extension.simpleVim.scrollUpHalfPage");
}),
parseKeysExact([" ", "k"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("extension.simpleVim.scrollDownHalfPage");
}),
// new G actions
parseKeysExact(["g", "l"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand(
"workbench.action.navigateToLastEditLocation"
);
}),
parseKeysExact(["g", "R"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("references-view.find");
}),
parseKeysExact(["g", "r"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("editor.action.referenceSearch.trigger");
}),
parseKeysExact(["g", "d"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("editor.action.revealDefinition");
}),
parseKeysExact(["g", "D"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("editor.action.revealDefinitionAside");
}),
parseKeysExact(["g", "p"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("editor.action.peekDefinition");
}),
parseKeysExact(["g", "s"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("extension.dash.specific");
}),
parseKeysExact(["g", "h"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("editor.action.showHover");
}),
parseKeysExact(["g", "U"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("editor.action.transformToUppercase");
}),
parseKeysExact(["g", "u"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("editor.action.transformToLowercase");
}),
// existing
parseKeysExact(
[KeyMap.Actions.InsertMode],
[Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.Occurrence],
(vimState, editor) => {
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}
),
parseKeysExact(
[KeyMap.Actions.InsertAtLineStart],
[Mode.Normal],
(vimState, editor) => {
editor.selections = editor.selections.map(selection => {
const character = editor.document.lineAt(selection.active.line)
.firstNonWhitespaceCharacterIndex;
const newPosition = selection.active.with({ character: character });
return new vscode.Selection(newPosition, newPosition);
});
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}
),
parseKeysExact(["a"], [Mode.Normal], (vimState, editor) => {
editor.selections = editor.selections.map(selection => {
const newPosition = positionUtils.right(
editor.document,
selection.active
);
return new vscode.Selection(newPosition, newPosition);
});
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}),
parseKeysExact(
[KeyMap.Actions.InsertAtLineEnd],
[Mode.Normal],
(vimState, editor) => {
editor.selections = editor.selections.map(selection => {
const lineLength = editor.document.lineAt(selection.active.line).text
.length;
const newPosition = selection.active.with({ character: lineLength });
return new vscode.Selection(newPosition, newPosition);
});
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}
),
parseKeysExact(["v"], [Mode.Normal, Mode.VisualLine], (vimState, editor) => {
if (vimState.mode === Mode.Normal) {
editor.selections = editor.selections.map(selection => {
const lineLength = editor.document.lineAt(selection.active.line).text
.length;
if (lineLength === 0) return selection;
return new vscode.Selection(
selection.active,
positionUtils.right(editor.document, selection.active)
);
});
}
enterVisualMode(vimState);
setModeCursorStyle(vimState.mode, editor);
}),
parseKeysExact(["V"], [Mode.Normal, Mode.Visual], (vimState, editor) => {
enterVisualLineMode(vimState);
setModeCursorStyle(vimState.mode, editor);
setVisualLineSelections(editor);
}),
parseKeysExact(
[KeyMap.Actions.NewLineBelow],
[Mode.Normal],
(vimState, editor) => {
vscode.commands.executeCommand("editor.action.insertLineAfter");
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}
),
parseKeysExact(
[KeyMap.Actions.NewLineAbove],
[Mode.Normal],
(vimState, editor) => {
vscode.commands.executeCommand("editor.action.insertLineBefore");
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}
),
parseKeysExact(["P"], [Mode.Normal, Mode.Visual, Mode.VisualLine], putAfter),
parseKeysExact(["p"], [Mode.Normal], putBefore),
// parseKeysExact(
// ["u"],
// [Mode.Normal, Mode.Visual, Mode.VisualLine],
// (vimState, editor) => {
// vscode.commands.executeCommand("undo");
// }
// ),
parseKeysExact(["d", "d"], [Mode.Normal], (vimState, editor) => {
deleteLine(vimState, editor);
}),
parseKeysExact(["D"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("deleteAllRight");
}),
// add 1 character swap
parseKeysRegex(
/^x(.)$/,
/^x$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
editor.edit(builder => {
editor.selections.forEach(s => {
let oneChar = s.with({
end: s.active.with({
character: s.active.character + 1
})
});
builder.replace(oneChar, match[1]);
});
});
}
),
// these allow you to the delete n lines above/below
// ex. d12i = delete 12 lines up
parseKeysRegex(
RegExp(`^d(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`),
/^(d|d\d+)$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
let lineCount = parseInt(match[1]);
let direction =
match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down;
// console.log(`delete ${lineCount} lines down`);
deleteLines(vimState, editor, lineCount, direction);
}
),
// same for change command
parseKeysRegex(
RegExp(`^c(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`),
/^(c|c\d+)$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
let lineCount = parseInt(match[1]);
let direction =
match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down;
// console.log(`delete ${lineCount} lines down`);
deleteLines(vimState, editor, lineCount, direction);
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}
),
// same for selection command
parseKeysRegex(
RegExp(`^s(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`),
/^(s|s\d+)$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
let lineCount = parseInt(match[1]);
let direction =
match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down;
// console.log(`delete ${lineCount} lines up`);
editor.selections = makeMultiLineSelection(
vimState,
editor,
lineCount,
direction
);
}
),
// same for yank command
parseKeysRegex(
RegExp(`^y(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`),
/^(y|y\d+)$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
let lineCount = parseInt(match[1]);
let direction =
match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down;
// console.log(`delete ${lineCount} lines up`);
let selections = makeMultiLineSelection(
vimState,
editor,
lineCount,
direction
);
yank(vimState, editor, selections, true);
flashYankHighlight(editor, selections);
}
),
// same for rip command
parseKeysRegex(
RegExp(`^r(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`),
/^(r|r\d+)$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
let lineCount = parseInt(match[1]);
let direction =
match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down;
// console.log(`delete ${lineCount} lines up`);
let selections = makeMultiLineSelection(
vimState,
editor,
lineCount,
direction
);
yank(vimState, editor, selections, true);
deleteLines(vimState, editor, lineCount, direction);
}
),
// same for duplicate command
parseKeysRegex(
RegExp(`^q(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`),
/^(q|q\d+)$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
let lineCount = parseInt(match[1]);
let direction =
match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down;
// console.log(`delete ${lineCount} lines up`);
editor.selections = makeMultiLineSelection(
vimState,
editor,
lineCount,
direction
);
vscode.commands.executeCommand("editor.action.copyLinesDownAction");
}
),
parseKeysExact(["c", "c"], [Mode.Normal], (vimState, editor) => {
editor.edit(editBuilder => {
editor.selections.forEach(selection => {
const line = editor.document.lineAt(selection.active.line);
editBuilder.delete(
new vscode.Range(
selection.active.with({
character: line.firstNonWhitespaceCharacterIndex
}),
selection.active.with({ character: line.text.length })
)
);
});
});
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}),
parseKeysExact(["C"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("deleteAllRight");
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}),
parseKeysExact(["y", "y"], [Mode.Normal], (vimState, editor) => {
yankLine(vimState, editor);
// Yank highlight
const highlightRanges = editor.selections.map(selection => {
const lineLength = editor.document.lineAt(selection.active.line).text
.length;
return new vscode.Range(
selection.active.with({ character: 0 }),
selection.active.with({ character: lineLength })
);
});
flashYankHighlight(editor, highlightRanges);
}),
parseKeysExact(["Y"], [Mode.Normal], (vimState, editor) => {
yankToEndOfLine(vimState, editor);
// Yank highlight
const highlightRanges = editor.selections.map(selection => {
const lineLength = editor.document.lineAt(selection.active.line).text
.length;
return new vscode.Range(
selection.active,
selection.active.with({ character: lineLength })
);
});
flashYankHighlight(editor, highlightRanges);
}),
parseKeysExact(["q", "q"], [Mode.Normal, Mode.Visual], () => {
vscode.commands.executeCommand("editor.action.copyLinesDownAction");
}),
parseKeysExact(["Q", "Q"], [Mode.Normal, Mode.Visual], () => {
vscode.commands.executeCommand("editor.action.copyLinesUpAction");
}),
parseKeysExact(["r", "r"], [Mode.Normal], (vimState, editor) => {
yankLine(vimState, editor);
deleteLine(vimState, editor);
}),
parseKeysExact(["R"], [Mode.Normal], (vimState, editor) => {
yankToEndOfLine(vimState, editor);
vscode.commands.executeCommand("deleteAllRight");
}),
parseKeysExact(["s", "s"], [Mode.Normal], (vimState, editor) => {
editor.selections = editor.selections.map(selection => {
return new vscode.Selection(
selection.active.with({ character: 0 }),
positionUtils.lineEnd(editor.document, selection.active)
);
});
enterVisualLineMode(vimState);
setModeCursorStyle(vimState.mode, editor);
}),
parseKeysExact(["S"], [Mode.Normal], (vimState, editor) => {
editor.selections = editor.selections.map(selection => {
return new vscode.Selection(
selection.active,
positionUtils.lineEnd(editor.document, selection.active)
);
});
enterVisualMode(vimState);
setModeCursorStyle(vimState.mode, editor);
}),
parseKeysExact(["h"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("deleteLeft");
}),
parseKeysExact(["n"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("deleteRight");
}),
parseKeysExact(
["z", KeyMap.Motions.MoveUp],
[Mode.Normal],
(vimState, editor) => {
vscode.commands.executeCommand("revealLine", {
lineNumber: editor.selection.active.line,
at: "top"
});
}
),
parseKeysExact(["z", "z"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("revealLine", {
lineNumber: editor.selection.active.line,
at: "center"
});
}),
parseKeysExact(
["z", KeyMap.Motions.MoveDown],
[Mode.Normal],
(vimState, editor) => {
vscode.commands.executeCommand("revealLine", {
lineNumber: editor.selection.active.line,
at: "bottom"
});
}
),
parseKeysExact([";"], [Mode.Normal], (vimState, editor) => {
vimState.semicolonAction(vimState, editor);
}),
parseKeysExact([","], [Mode.Normal], (vimState, editor) => {
vimState.commaAction(vimState, editor);
})
];
function makeMultiLineSelection(
vimState: VimState,
editor: vscode.TextEditor,
lineCount: number,
direction: Direction
): vscode.Selection[] {
return editor.selections.map(selection => {
if (direction == Direction.Up) {
let endLine = selection.active.line - lineCount + 1;
const startPos = positionUtils.lineEnd(editor.document, selection.active);
const endPos =
endLine >= 0
? new vscode.Position(endLine, 0)
: new vscode.Position(0, 0);
return new vscode.Selection(startPos, endPos);
} else {
const endLine = selection.active.line + lineCount - 1;
const startPos = new vscode.Position(selection.active.line, 0);
const endPos =
endLine < editor.document.lineCount
? new vscode.Position(
endLine,
editor.document.lineAt(endLine).text.length
)
: positionUtils.lastChar(editor.document);
return new vscode.Selection(startPos, endPos);
}
});
}
function deleteLines(
vimState: VimState,
editor: vscode.TextEditor,
lineCount: number,
direction: Direction = Direction.Down
): void {
let selections = editor.selections.map(selection => {
if (direction == Direction.Up) {
let endLine = selection.active.line - lineCount;
if (endLine >= 0) {
const startPos = positionUtils.lineEnd(
editor.document,
selection.active
);
const endPos = new vscode.Position(
endLine,
editor.document.lineAt(endLine).text.length
);
return new vscode.Selection(startPos, endPos);
} else {
const startPos =
selection.active.line + 1 <= editor.document.lineCount
? new vscode.Position(selection.active.line + 1, 0)
: positionUtils.lineEnd(editor.document, selection.active);
const endPos = new vscode.Position(0, 0);
return new vscode.Selection(startPos, endPos);
}
} else {
let endLine = selection.active.line + lineCount;
if (endLine <= editor.document.lineCount - 1) {
const startPos = new vscode.Position(selection.active.line, 0);
const endPos = new vscode.Position(endLine, 0);
return new vscode.Selection(startPos, endPos);
} else {
const startPos =
selection.active.line - 1 >= 0
? new vscode.Position(
selection.active.line - 1,
editor.document.lineAt(selection.active.line - 1).text.length
)
: new vscode.Position(selection.active.line, 0);
const endPos = positionUtils.lastChar(editor.document);
return new vscode.Selection(startPos, endPos);
}
}
});
editor
.edit(builder => {
selections.forEach(sel => builder.replace(sel, ""));
})
.then(() => {
editor.selections = editor.selections.map(selection => {
const character = editor.document.lineAt(selection.active.line)
.firstNonWhitespaceCharacterIndex;
const newPosition = selection.active.with({ character: character });
return new vscode.Selection(newPosition, newPosition);
});
});
}
function deleteLine(
vimState: VimState,
editor: vscode.TextEditor,
direction: Direction = Direction.Down
): void {
deleteLines(vimState, editor, 1, direction);
}
function yankLine(vimState: VimState, editor: vscode.TextEditor): void {
vimState.registers = {
contentsList: editor.selections.map(selection => {
return editor.document.lineAt(selection.active.line).text;
}),
linewise: true
};
}
function yankToEndOfLine(vimState: VimState, editor: vscode.TextEditor): void {
vimState.registers = {
contentsList: editor.selections.map(selection => {
return editor.document
.lineAt(selection.active.line)
.text.substring(selection.active.character);
}),
linewise: false
};
}

6
src/actions/index.ts Normal file
View File

@ -0,0 +1,6 @@
import { Action } from "../action_types";
import { actions as subActions } from "./actions";
import { operators } from "./operators";
import { motions } from "./motions";
export const actions: Action[] = subActions.concat(operators, motions);

17
src/actions/keymaps.ts Normal file
View File

@ -0,0 +1,17 @@
export default {
Motions: {
MoveLeft: "j",
MoveRight: "l",
MoveDown: "k",
MoveUp: "i",
MoveLineEnd: "o",
MoveLineStart: "u",
},
Actions: {
InsertMode: "h",
InsertAtLineStart: "U",
InsertAtLineEnd: "O",
NewLineAbove: "I",
NewLineBelow: "K",
},
};

620
src/actions/motions.ts Normal file
View File

@ -0,0 +1,620 @@
import * as vscode from "vscode";
import { Mode } from "../modes_types";
import { Action } from "../action_types";
import { parseKeysExact, parseKeysRegex } from "../parse_keys";
import {
vscodeToVimVisualSelection,
vimToVscodeVisualLineSelection,
vimToVscodeVisualSelection,
vscodeToVimVisualLineSelection
} from "../selection_utils";
import * as positionUtils from "../position_utils";
import { VimState } from "../vim_state_types";
import { wordRanges, whitespaceWordRanges } from "../word_utils";
import { searchForward, searchBackward } from "../search_utils";
import { paragraphForward, paragraphBackward } from "../paragraph_utils";
import { setVisualLineSelections } from "../visual_line_utils";
import { setVisualSelections } from "../visual_utils";
import KeyMap from "./keymaps";
export const motions: Action[] = [
parseKeysExact(
[KeyMap.Motions.MoveRight],
[Mode.Normal, Mode.Visual],
(vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
return positionUtils.rightNormal(document, position);
});
}
),
parseKeysExact(
[KeyMap.Motions.MoveLeft],
[Mode.Normal, Mode.Visual],
(vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
return positionUtils.left(position);
});
}
),
parseKeysExact([KeyMap.Motions.MoveUp], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("cursorMove", {
to: "up",
by: "wrappedLine"
});
}),
parseKeysExact([KeyMap.Motions.MoveUp], [Mode.Visual], (vimState, editor) => {
const originalSelections = editor.selections;
vscode.commands
.executeCommand("cursorMove", {
to: "up",
by: "wrappedLine",
select: true
})
.then(() => {
setVisualSelections(editor, originalSelections);
});
}),
parseKeysExact(
[KeyMap.Motions.MoveUp],
[Mode.VisualLine],
(vimState, editor) => {
vscode.commands
.executeCommand("cursorMove", { to: "up", by: "line", select: true })
.then(() => {
setVisualLineSelections(editor);
});
}
),
parseKeysExact(
[KeyMap.Motions.MoveDown],
[Mode.Normal],
(vimState, editor) => {
vscode.commands.executeCommand("cursorMove", {
to: "down",
by: "wrappedLine"
});
}
),
parseKeysExact(
[KeyMap.Motions.MoveDown],
[Mode.Visual],
(vimState, editor) => {
const originalSelections = editor.selections;
vscode.commands
.executeCommand("cursorMove", {
to: "down",
by: "wrappedLine",
select: true
})
.then(() => {
setVisualSelections(editor, originalSelections);
});
}
),
parseKeysExact(
[KeyMap.Motions.MoveDown],
[Mode.VisualLine],
(vimState, editor) => {
vscode.commands
.executeCommand("cursorMove", { to: "down", by: "line", select: true })
.then(() => {
setVisualLineSelections(editor);
});
}
),
parseKeysExact(
["w"],
[Mode.Normal, Mode.Visual],
createWordForwardHandler(wordRanges)
),
parseKeysExact(
["W"],
[Mode.Normal, Mode.Visual],
createWordForwardHandler(whitespaceWordRanges)
),
parseKeysExact(
["b"],
[Mode.Normal, Mode.Visual],
createWordBackwardHandler(wordRanges)
),
parseKeysExact(
["B"],
[Mode.Normal, Mode.Visual],
createWordBackwardHandler(whitespaceWordRanges)
),
parseKeysExact(
["e"],
[Mode.Normal, Mode.Visual],
createWordEndHandler(wordRanges)
),
parseKeysExact(
["E"],
[Mode.Normal, Mode.Visual],
createWordEndHandler(whitespaceWordRanges)
),
parseKeysRegex(
/^f(..)$/,
/^(f|f.)$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
findForward(vimState, editor, match);
vimState.semicolonAction = (innerVimState, innerEditor) => {
findForward(innerVimState, innerEditor, match);
};
vimState.commaAction = (innerVimState, innerEditor) => {
findBackward(innerVimState, innerEditor, match);
};
}
),
parseKeysRegex(
/^F(..)$/,
/^(F|F.)$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
findBackward(vimState, editor, match);
vimState.semicolonAction = (innerVimState, innerEditor) => {
findBackward(innerVimState, innerEditor, match);
};
vimState.commaAction = (innerVimState, innerEditor) => {
findForward(innerVimState, innerEditor, match);
};
}
),
parseKeysRegex(
/^t(.)$/,
/^t$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
tillForward(vimState, editor, match);
vimState.semicolonAction = (innerVimState, innerEditor) => {
tillForward(innerVimState, innerEditor, match);
};
vimState.commaAction = (innerVimState, innerEditor) => {
tillBackward(innerVimState, innerEditor, match);
};
}
),
parseKeysRegex(
/^T(.)$/,
/^T$/,
[Mode.Normal, Mode.Visual],
(vimState, editor, match) => {
tillBackward(vimState, editor, match);
vimState.semicolonAction = (innerVimState, innerEditor) => {
tillBackward(innerVimState, innerEditor, match);
};
vimState.commaAction = (innerVimState, innerEditor) => {
tillForward(innerVimState, innerEditor, match);
};
}
),
parseKeysExact(
["g", "g"],
[Mode.Normal, Mode.Visual, Mode.VisualLine],
(vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
return new vscode.Position(0, 0);
});
}
),
parseKeysExact(
["G"],
[Mode.Normal, Mode.Visual, Mode.VisualLine],
(vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
return new vscode.Position(document.lineCount - 1, 0);
});
}
),
parseKeysExact(
["}"],
[Mode.Normal, Mode.Visual, Mode.VisualLine],
(vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
return new vscode.Position(
paragraphForward(document, position.line),
0
);
});
}
),
parseKeysExact(
["{"],
[Mode.Normal, Mode.Visual, Mode.VisualLine],
(vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
return new vscode.Position(
paragraphBackward(document, position.line),
0
);
});
}
),
parseKeysExact(
[KeyMap.Motions.MoveLineEnd],
[Mode.Normal, Mode.Visual, Mode.VisualLine],
(vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
const lineLength = document.lineAt(position.line).text.length;
return position.with({ character: Math.max(lineLength - 1, 0) });
});
}
),
parseKeysExact(
[KeyMap.Motions.MoveLineStart],
[Mode.Normal, Mode.Visual, Mode.VisualLine],
(vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
const line = document.lineAt(position.line);
return position.with({
character: line.firstNonWhitespaceCharacterIndex
});
});
}
),
parseKeysExact(["H"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("cursorMove", {
to: "viewPortTop",
by: "line"
});
}),
parseKeysExact(["H"], [Mode.Visual], (vimState, editor) => {
const originalSelections = editor.selections;
vscode.commands
.executeCommand("cursorMove", {
to: "viewPortTop",
by: "line",
select: true
})
.then(() => {
setVisualSelections(editor, originalSelections);
});
}),
parseKeysExact(["H"], [Mode.VisualLine], (vimState, editor) => {
vscode.commands
.executeCommand("cursorMove", {
to: "viewPortTop",
by: "line",
select: true
})
.then(() => {
setVisualLineSelections(editor);
});
}),
parseKeysExact(["M"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("cursorMove", {
to: "viewPortCenter",
by: "line"
});
}),
parseKeysExact(["M"], [Mode.Visual], (vimState, editor) => {
const originalSelections = editor.selections;
vscode.commands
.executeCommand("cursorMove", {
to: "viewPortCenter",
by: "line",
select: true
})
.then(() => {
setVisualSelections(editor, originalSelections);
});
}),
parseKeysExact(["M"], [Mode.VisualLine], (vimState, editor) => {
vscode.commands
.executeCommand("cursorMove", {
to: "viewPortCenter",
by: "line",
select: true
})
.then(() => {
setVisualLineSelections(editor);
});
}),
parseKeysExact(["L"], [Mode.Normal], (vimState, editor) => {
vscode.commands.executeCommand("cursorMove", {
to: "viewPortBottom",
by: "line"
});
}),
parseKeysExact(["L"], [Mode.Visual], (vimState, editor) => {
const originalSelections = editor.selections;
vscode.commands
.executeCommand("cursorMove", {
to: "viewPortBottom",
by: "line",
select: true
})
.then(() => {
setVisualSelections(editor, originalSelections);
});
}),
parseKeysExact(["L"], [Mode.VisualLine], (vimState, editor) => {
vscode.commands
.executeCommand("cursorMove", {
to: "viewPortBottom",
by: "line",
select: true
})
.then(() => {
setVisualLineSelections(editor);
});
})
];
type MotionArgs = {
document: vscode.TextDocument;
position: vscode.Position;
selectionIndex: number;
vimState: VimState;
};
type RegexMotionArgs = {
document: vscode.TextDocument;
position: vscode.Position;
selectionIndex: number;
vimState: VimState;
match: RegExpMatchArray;
};
function execRegexMotion(
vimState: VimState,
editor: vscode.TextEditor,
match: RegExpMatchArray,
regexMotion: (args: RegexMotionArgs) => vscode.Position
) {
return execMotion(vimState, editor, motionArgs => {
return regexMotion({
...motionArgs,
match: match
});
});
}
function execMotion(
vimState: VimState,
editor: vscode.TextEditor,
motion: (args: MotionArgs) => vscode.Position
) {
const document = editor.document;
const newSelections = editor.selections.map((selection, i) => {
if (vimState.mode === Mode.Normal) {
const newPosition = motion({
document: document,
position: selection.active,
selectionIndex: i,
vimState: vimState
});
return new vscode.Selection(newPosition, newPosition);
} else if (vimState.mode === Mode.Visual) {
const vimSelection = vscodeToVimVisualSelection(document, selection);
const motionPosition = motion({
document: document,
position: vimSelection.active,
selectionIndex: i,
vimState: vimState
});
return vimToVscodeVisualSelection(
document,
new vscode.Selection(vimSelection.anchor, motionPosition)
);
} else if (vimState.mode === Mode.VisualLine) {
const vimSelection = vscodeToVimVisualLineSelection(document, selection);
const motionPosition = motion({
document: document,
position: vimSelection.active,
selectionIndex: i,
vimState: vimState
});
return vimToVscodeVisualLineSelection(
document,
new vscode.Selection(vimSelection.anchor, motionPosition)
);
} else {
return selection;
}
});
editor.selections = newSelections;
editor.revealRange(
new vscode.Range(newSelections[0].active, newSelections[0].active),
vscode.TextEditorRevealType.InCenterIfOutsideViewport
);
}
function findForward(
vimState: VimState,
editor: vscode.TextEditor,
outerMatch: RegExpMatchArray
): void {
execRegexMotion(
vimState,
editor,
outerMatch,
({ document, position, match }) => {
const fromPosition = position.with({ character: position.character + 1 });
const result = searchForward(document, match[1], fromPosition);
if (result) {
return result;
} else {
return position;
}
}
);
}
function findBackward(
vimState: VimState,
editor: vscode.TextEditor,
outerMatch: RegExpMatchArray
): void {
execRegexMotion(
vimState,
editor,
outerMatch,
({ document, position, match }) => {
const fromPosition = positionLeftWrap(document, position);
const result = searchBackward(document, match[1], fromPosition);
if (result) {
return result;
} else {
return position;
}
}
);
}
function tillForward(
vimState: VimState,
editor: vscode.TextEditor,
outerMatch: RegExpMatchArray
): void {
execRegexMotion(
vimState,
editor,
outerMatch,
({ document, position, match }) => {
const lineText = document.lineAt(position.line).text;
const result = lineText.indexOf(match[1], position.character + 1);
if (result >= 0) {
return position.with({ character: result });
} else {
return position;
}
}
);
}
function tillBackward(
vimState: VimState,
editor: vscode.TextEditor,
outerMatch: RegExpMatchArray
): void {
execRegexMotion(
vimState,
editor,
outerMatch,
({ document, position, match }) => {
const lineText = document.lineAt(position.line).text;
const result = lineText.lastIndexOf(match[1], position.character - 1);
if (result >= 0) {
return position.with({ character: result });
} else {
return position;
}
}
);
}
function positionLeftWrap(
document: vscode.TextDocument,
position: vscode.Position
): vscode.Position {
if (position.character === 0) {
if (position.line === 0) {
return position;
} else {
const lineLength = document.lineAt(position.line - 1).text.length;
return new vscode.Position(position.line - 1, lineLength);
}
} else {
return position.with({ character: position.character - 1 });
}
}
function createWordForwardHandler(
wordRangesFunction: (text: string) => { start: number; end: number }[]
): (vimState: VimState, editor: vscode.TextEditor) => void {
return (vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
const lineText = document.lineAt(position.line).text;
const ranges = wordRangesFunction(lineText);
const result = ranges.find(x => x.start > position.character);
if (result) {
return position.with({ character: result.start });
} else {
return position;
}
});
};
}
function createWordBackwardHandler(
wordRangesFunction: (text: string) => { start: number; end: number }[]
): (vimState: VimState, editor: vscode.TextEditor) => void {
return (vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
const lineText = document.lineAt(position.line).text;
const ranges = wordRangesFunction(lineText);
const result = ranges.reverse().find(x => x.start < position.character);
if (result) {
return position.with({ character: result.start });
} else {
return position;
}
});
};
}
function createWordEndHandler(
wordRangesFunction: (text: string) => { start: number; end: number }[]
): (vimState: VimState, editor: vscode.TextEditor) => void {
return (vimState, editor) => {
execMotion(vimState, editor, ({ document, position }) => {
const lineText = document.lineAt(position.line).text;
const ranges = wordRangesFunction(lineText);
const result = ranges.find(x => x.end > position.character);
if (result) {
return position.with({ character: result.end });
} else {
return position;
}
});
};
}

View File

@ -0,0 +1,739 @@
import * as vscode from "vscode";
import {
createOperatorRangeExactKeys,
createOperatorRangeRegex,
} from "../parse_keys";
import { OperatorRange } from "../parse_keys_types";
import {
searchForward,
searchBackward,
searchBackwardBracket,
searchForwardBracket,
} from "../search_utils";
import * as positionUtils from "../position_utils";
import { wordRanges, whitespaceWordRanges } from "../word_utils";
import {
paragraphForward,
paragraphBackward,
paragraphRangeOuter,
paragraphRangeInner,
} from "../paragraph_utils";
import { VimState } from "../vim_state_types";
import { quoteRanges, findQuoteRange } from "../quote_utils";
import { indentLevelRange } from "../indent_utils";
import { blockRange } from "../block_utils";
import { getTags } from "../tag_utils";
import { arrayFindLast } from "../array_utils";
// import KeyMap from "./keymap";
export const operatorRanges: OperatorRange[] = [
// createOperatorRangeExactKeys(
// [KeyMap.Motions.MoveRight],
// false,
// (vimState, document, position) => {
// const right = positionUtils.right(document, position);
// if (right.isEqual(position)) {
// return undefined;
// } else {
// return new vscode.Range(position, right);
// }
// }
// ),
// createOperatorRangeExactKeys(
// [KeyMap.Motions.MoveLeft],
// false,
// (vimState, document, position) => {
// const left = positionUtils.left(position);
// if (left.isEqual(position)) {
// return undefined;
// } else {
// return new vscode.Range(position, left);
// }
// }
// ),
// createOperatorRangeExactKeys(
// [KeyMap.Motions.MoveUp],
// true,
// (vimState, document, position) => {
// if (position.line === 0) {
// return new vscode.Range(
// new vscode.Position(0, 0),
// positionUtils.lineEnd(document, position)
// );
// } else {
// return new vscode.Range(
// new vscode.Position(position.line - 1, 0),
// positionUtils.lineEnd(document, position)
// );
// }
// }
// ),
// createOperatorRangeExactKeys(
// [KeyMap.Motions.MoveDown],
// true,
// (vimState, document, position) => {
// if (position.line === document.lineCount - 1) {
// return new vscode.Range(
// new vscode.Position(position.line, 0),
// positionUtils.lineEnd(document, position)
// );
// } else {
// return new vscode.Range(
// new vscode.Position(position.line, 0),
// positionUtils.lineEnd(
// document,
// position.with({ line: position.line + 1 })
// )
// );
// }
// }
// ),
createOperatorRangeExactKeys(
["w"],
false,
createWordForwardHandler(wordRanges)
),
createOperatorRangeExactKeys(
["W"],
false,
createWordForwardHandler(whitespaceWordRanges)
),
createOperatorRangeExactKeys(
["b"],
false,
createWordBackwardHandler(wordRanges)
),
createOperatorRangeExactKeys(
["B"],
false,
createWordBackwardHandler(whitespaceWordRanges)
),
createOperatorRangeExactKeys(["e"], false, createWordEndHandler(wordRanges)),
createOperatorRangeExactKeys(
["E"],
false,
createWordEndHandler(whitespaceWordRanges)
),
createOperatorRangeExactKeys(
["i", "w"],
false,
createInnerWordHandler(wordRanges)
),
createOperatorRangeExactKeys(
["i", "W"],
false,
createInnerWordHandler(whitespaceWordRanges)
),
createOperatorRangeExactKeys(
["a", "w"],
false,
createOuterWordHandler(wordRanges)
),
createOperatorRangeExactKeys(
["a", "W"],
false,
createOuterWordHandler(whitespaceWordRanges)
),
createOperatorRangeRegex(
/^f(..)$/,
/^(f|f.)$/,
false,
(vimState, document, position, match) => {
const fromPosition = position.with({ character: position.character + 1 });
const result = searchForward(document, match[1], fromPosition);
if (result) {
return new vscode.Range(position, result);
} else {
return undefined;
}
}
),
createOperatorRangeRegex(
/^F(..)$/,
/^(F|F.)$/,
false,
(vimState, document, position, match) => {
const fromPosition = position.with({ character: position.character - 1 });
const result = searchBackward(document, match[1], fromPosition);
if (result) {
return new vscode.Range(position, result);
} else {
return undefined;
}
}
),
createOperatorRangeRegex(
/^t(.)$/,
/^t$/,
false,
(vimState, document, position, match) => {
const lineText = document.lineAt(position.line).text;
const result = lineText.indexOf(match[1], position.character + 1);
if (result >= 0) {
return new vscode.Range(position, position.with({ character: result }));
} else {
return undefined;
}
}
),
createOperatorRangeRegex(
/^T(.)$/,
/^T$/,
false,
(vimState, document, position, match) => {
const lineText = document.lineAt(position.line).text;
const result = lineText.lastIndexOf(match[1], position.character - 1);
if (result >= 0) {
const newPosition = positionUtils.right(
document,
position.with({ character: result })
);
return new vscode.Range(newPosition, position);
} else {
return undefined;
}
}
),
createOperatorRangeExactKeys(
["g", "g"],
true,
(vimState, document, position) => {
const lineLength = document.lineAt(position.line).text.length;
return new vscode.Range(
new vscode.Position(0, 0),
position.with({ character: lineLength })
);
}
),
createOperatorRangeExactKeys(["G"], true, (vimState, document, position) => {
const lineLength = document.lineAt(document.lineCount - 1).text.length;
return new vscode.Range(
position.with({ character: 0 }),
new vscode.Position(document.lineCount - 1, lineLength)
);
}),
// TODO: return undefined?
createOperatorRangeExactKeys(["}"], true, (vimState, document, position) => {
return new vscode.Range(
position.with({ character: 0 }),
new vscode.Position(paragraphForward(document, position.line), 0)
);
}),
// TODO: return undefined?
createOperatorRangeExactKeys(["{"], true, (vimState, document, position) => {
return new vscode.Range(
new vscode.Position(paragraphBackward(document, position.line), 0),
position.with({ character: 0 })
);
}),
createOperatorRangeExactKeys(
["i", "p"],
true,
(vimState, document, position) => {
const result = paragraphRangeInner(document, position.line);
if (result) {
return new vscode.Range(
new vscode.Position(result.start, 0),
new vscode.Position(
result.end,
document.lineAt(result.end).text.length
)
);
} else {
return undefined;
}
}
),
createOperatorRangeExactKeys(
["a", "p"],
true,
(vimState, document, position) => {
const result = paragraphRangeOuter(document, position.line);
if (result) {
return new vscode.Range(
new vscode.Position(result.start, 0),
new vscode.Position(
result.end,
document.lineAt(result.end).text.length
)
);
} else {
return undefined;
}
}
),
createOperatorRangeExactKeys(["i", "'"], false, createInnerQuoteHandler("'")),
createOperatorRangeExactKeys(["a", "'"], false, createOuterQuoteHandler("'")),
createOperatorRangeExactKeys(["i", '"'], false, createInnerQuoteHandler('"')),
createOperatorRangeExactKeys(["a", '"'], false, createOuterQuoteHandler('"')),
createOperatorRangeExactKeys(["i", "`"], false, createInnerQuoteHandler("`")),
createOperatorRangeExactKeys(["a", "`"], false, createOuterQuoteHandler("`")),
createOperatorRangeExactKeys(
["i", "("],
false,
createInnerBracketHandler("(", ")")
),
createOperatorRangeExactKeys(
["a", "("],
false,
createOuterBracketHandler("(", ")")
),
createOperatorRangeExactKeys(
["i", "{"],
false,
createInnerBracketHandler("{", "}")
),
createOperatorRangeExactKeys(
["a", "{"],
false,
createOuterBracketHandler("{", "}")
),
createOperatorRangeExactKeys(
["i", "["],
false,
createInnerBracketHandler("[", "]")
),
createOperatorRangeExactKeys(
["a", "["],
false,
createOuterBracketHandler("[", "]")
),
createOperatorRangeExactKeys(
["i", "<"],
false,
createInnerBracketHandler("<", ">")
),
createOperatorRangeExactKeys(
["a", "<"],
false,
createOuterBracketHandler("<", ">")
),
createOperatorRangeExactKeys(
["i", "t"],
false,
(vimState, document, position) => {
const tags = getTags(document);
const closestTag = arrayFindLast(tags, (tag) => {
if (tag.closing) {
return (
position.isAfterOrEqual(tag.opening.start) &&
position.isBeforeOrEqual(tag.closing.end)
);
} else {
// Self-closing tags have no inside
return false;
}
});
if (closestTag) {
if (closestTag.closing) {
return new vscode.Range(
closestTag.opening.end.with({
character: closestTag.opening.end.character + 1,
}),
closestTag.closing.start
);
} else {
throw new Error(
"We should have already filtered out self-closing tags above"
);
}
} else {
return undefined;
}
}
),
createOperatorRangeExactKeys(
["a", "t"],
false,
(vimState, document, position) => {
const tags = getTags(document);
const closestTag = arrayFindLast(tags, (tag) => {
const afterStart = position.isAfterOrEqual(tag.opening.start);
if (tag.closing) {
return afterStart && position.isBeforeOrEqual(tag.closing.end);
} else {
return afterStart && position.isBeforeOrEqual(tag.opening.end);
}
});
if (closestTag) {
if (closestTag.closing) {
return new vscode.Range(
closestTag.opening.start,
closestTag.closing.end.with({
character: closestTag.closing.end.character + 1,
})
);
} else {
return new vscode.Range(
closestTag.opening.start,
closestTag.opening.end.with({
character: closestTag.opening.end.character + 1,
})
);
}
} else {
return undefined;
}
}
),
// TODO: return undefined?
createOperatorRangeExactKeys(
["i", "i"],
true,
(vimState, document, position) => {
const simpleRange = indentLevelRange(document, position.line);
return new vscode.Range(
new vscode.Position(simpleRange.start, 0),
new vscode.Position(
simpleRange.end,
document.lineAt(simpleRange.end).text.length
)
);
}
),
createOperatorRangeExactKeys(
["a", "b"],
true,
(vimState, document, position) => {
const range = blockRange(document, position);
return range;
}
),
];
function createInnerBracketHandler(
openingChar: string,
closingChar: string
): (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range | undefined {
return (vimState, document, position) => {
const bracketRange = getBracketRange(
document,
position,
openingChar,
closingChar
);
if (bracketRange) {
return new vscode.Range(
bracketRange.start.with({
character: bracketRange.start.character + 1,
}),
bracketRange.end
);
} else {
return undefined;
}
};
}
function createOuterBracketHandler(
openingChar: string,
closingChar: string
): (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range | undefined {
return (vimState, document, position) => {
const bracketRange = getBracketRange(
document,
position,
openingChar,
closingChar
);
if (bracketRange) {
return new vscode.Range(
bracketRange.start,
bracketRange.end.with({ character: bracketRange.end.character + 1 })
);
} else {
return undefined;
}
};
}
function getBracketRange(
document: vscode.TextDocument,
position: vscode.Position,
openingChar: string,
closingChar: string
): vscode.Range | undefined {
const lineText = document.lineAt(position.line).text;
const currentChar = lineText[position.character];
let start;
let end;
if (currentChar === openingChar) {
start = position;
end = searchForwardBracket(
document,
openingChar,
closingChar,
positionUtils.rightWrap(document, position)
);
} else if (currentChar === closingChar) {
start = searchBackwardBracket(
document,
openingChar,
closingChar,
positionUtils.leftWrap(document, position)
);
end = position;
} else {
start = searchBackwardBracket(document, openingChar, closingChar, position);
end = searchForwardBracket(document, openingChar, closingChar, position);
}
if (start && end) {
return new vscode.Range(start, end);
} else {
return undefined;
}
}
function createInnerQuoteHandler(
quoteChar: string
): (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range | undefined {
return (vimState, document, position) => {
const lineText = document.lineAt(position.line).text;
const ranges = quoteRanges(quoteChar, lineText);
const result = findQuoteRange(ranges, position);
if (result) {
return new vscode.Range(
position.with({ character: result.start + 1 }),
position.with({ character: result.end })
);
} else {
return undefined;
}
};
}
function createOuterQuoteHandler(
quoteChar: string
): (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range | undefined {
return (vimState, document, position) => {
const lineText = document.lineAt(position.line).text;
const ranges = quoteRanges(quoteChar, lineText);
const result = findQuoteRange(ranges, position);
if (result) {
return new vscode.Range(
position.with({ character: result.start }),
position.with({ character: result.end + 1 })
);
} else {
return undefined;
}
};
}
function createWordForwardHandler(
wordRangesFunction: (text: string) => { start: number; end: number }[]
): (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range {
return (vimState, document, position) => {
const lineText = document.lineAt(position.line).text;
const ranges = wordRangesFunction(lineText);
const result = ranges.find((x) => x.start > position.character);
if (result) {
return new vscode.Range(
position,
position.with({ character: result.start })
);
} else {
return new vscode.Range(
position,
position.with({ character: lineText.length })
);
}
};
}
function createWordBackwardHandler(
wordRangesFunction: (text: string) => { start: number; end: number }[]
): (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range | undefined {
return (vimState, document, position) => {
const lineText = document.lineAt(position.line).text;
const ranges = wordRangesFunction(lineText);
const result = ranges.reverse().find((x) => x.start < position.character);
if (result) {
return new vscode.Range(
position.with({ character: result.start }),
position
);
} else {
return undefined;
}
};
}
function createWordEndHandler(
wordRangesFunction: (text: string) => { start: number; end: number }[]
): (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range | undefined {
return (vimState, document, position) => {
const lineText = document.lineAt(position.line).text;
const ranges = wordRangesFunction(lineText);
const result = ranges.find((x) => x.end > position.character);
if (result) {
return new vscode.Range(
position,
positionUtils.right(document, position.with({ character: result.end }))
);
} else {
return undefined;
}
};
}
function createInnerWordHandler(
wordRangesFunction: (text: string) => { start: number; end: number }[]
): (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range | undefined {
return (vimState, document, position) => {
const lineText = document.lineAt(position.line).text;
const ranges = wordRangesFunction(lineText);
const result = ranges.find(
(x) => x.start <= position.character && position.character <= x.end
);
if (result) {
return new vscode.Range(
position.with({ character: result.start }),
positionUtils.right(document, position.with({ character: result.end }))
);
} else {
return undefined;
}
};
}
function createOuterWordHandler(
wordRangesFunction: (text: string) => { start: number; end: number }[]
): (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range | undefined {
return (vimState, document, position) => {
const lineText = document.lineAt(position.line).text;
const ranges = wordRangesFunction(lineText);
for (let i = 0; i < ranges.length; ++i) {
const range = ranges[i];
if (
range.start <= position.character &&
position.character <= range.end
) {
if (i < ranges.length - 1) {
return new vscode.Range(
position.with({ character: range.start }),
position.with({ character: ranges[i + 1].start })
);
} else if (i > 0) {
return new vscode.Range(
positionUtils.right(
document,
position.with({ character: ranges[i - 1].end })
),
positionUtils.right(
document,
position.with({ character: range.end })
)
);
} else {
return new vscode.Range(
position.with({ character: range.start }),
positionUtils.right(
document,
position.with({ character: range.end })
)
);
}
}
}
return undefined;
};
}

244
src/actions/operators.ts Normal file
View File

@ -0,0 +1,244 @@
import * as vscode from "vscode";
import { Action } from "../action_types";
import { operatorRanges } from "./operator_ranges";
import { parseKeysOperator } from "../parse_keys";
import {
enterInsertMode,
enterNormalMode,
setModeCursorStyle,
enterVisualLineMode,
enterVisualMode
} from "../modes";
import { removeTypeSubscription } from "../type_subscription";
import { VimState } from "../vim_state_types";
import { Mode } from "../modes_types";
import { flashYankHighlight } from "../yank_highlight";
export const operators: Action[] = [
parseKeysOperator(
["d"],
operatorRanges,
(vimState, editor, ranges, linewise) => {
if (ranges.every(x => x === undefined)) return;
cursorsToRangesStart(editor, ranges);
delete_(editor, ranges, linewise);
if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) {
enterNormalMode(vimState);
setModeCursorStyle(vimState.mode, editor);
}
}
),
parseKeysOperator(
["c"],
operatorRanges,
(vimState, editor, ranges, linewise) => {
if (ranges.every(x => x === undefined)) return;
cursorsToRangesStart(editor, ranges);
editor.edit(editBuilder => {
ranges.forEach(range => {
if (!range) return;
editBuilder.delete(range);
});
});
enterInsertMode(vimState);
setModeCursorStyle(vimState.mode, editor);
removeTypeSubscription(vimState);
}
),
parseKeysOperator(
["y"],
operatorRanges,
(vimState, editor, ranges, linewise) => {
if (ranges.every(x => x === undefined)) return;
yank(vimState, editor, ranges, linewise);
if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) {
// Move cursor to start of yanked text
editor.selections = editor.selections.map(selection => {
return new vscode.Selection(selection.start, selection.start);
});
enterNormalMode(vimState);
setModeCursorStyle(vimState.mode, editor);
} else {
// Yank highlight
const highlightRanges: vscode.Range[] = [];
ranges.forEach(range => {
if (range) {
highlightRanges.push(new vscode.Range(range.start, range.end));
}
});
flashYankHighlight(editor, highlightRanges);
}
}
),
parseKeysOperator(
["r"],
operatorRanges,
(vimState, editor, ranges, linewise) => {
if (ranges.every(x => x === undefined)) return;
cursorsToRangesStart(editor, ranges);
yank(vimState, editor, ranges, linewise);
delete_(editor, ranges, linewise);
if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) {
enterNormalMode(vimState);
setModeCursorStyle(vimState.mode, editor);
}
}
),
parseKeysOperator(
["s"],
operatorRanges,
(vimState, editor, ranges, linewise) => {
if (
ranges.every(x => x === undefined) ||
vimState.mode === Mode.Visual ||
vimState.mode === Mode.VisualLine
) {
return;
}
editor.selections = ranges.map((range, i) => {
if (range) {
const start = range.start;
const end = range.end;
return new vscode.Selection(start, end);
} else {
return editor.selections[i];
}
});
if (linewise) {
enterVisualLineMode(vimState);
} else {
enterVisualMode(vimState);
}
setModeCursorStyle(vimState.mode, editor);
}
),
parseKeysOperator(
["q"],
operatorRanges,
(vimState, editor, ranges, linewise) => {
if (
ranges.every(x => x === undefined) ||
vimState.mode === Mode.Visual ||
vimState.mode === Mode.VisualLine
) {
return;
}
editor.selections = ranges.map((range, i) => {
if (range) {
const start = range.start;
const end = range.end;
return new vscode.Selection(start, end);
} else {
return editor.selections[i];
}
});
vscode.commands.executeCommand("editor.action.copyLinesDownAction");
}
)
];
function cursorsToRangesStart(
editor: vscode.TextEditor,
ranges: (vscode.Range | undefined)[]
) {
editor.selections = editor.selections.map((selection, i) => {
const range = ranges[i];
if (range) {
const newPosition = range.start;
return new vscode.Selection(newPosition, newPosition);
} else {
return selection;
}
});
}
function delete_(
editor: vscode.TextEditor,
ranges: (vscode.Range | undefined)[],
linewise: boolean
) {
editor
.edit(editBuilder => {
ranges.forEach(range => {
if (!range) return;
let deleteRange = range;
if (linewise) {
const start = range.start;
const end = range.end;
if (end.line === editor.document.lineCount - 1) {
if (start.line === 0) {
deleteRange = new vscode.Range(start.with({ character: 0 }), end);
} else {
deleteRange = new vscode.Range(
new vscode.Position(
start.line - 1,
editor.document.lineAt(start.line - 1).text.length
),
end
);
}
} else {
deleteRange = new vscode.Range(
range.start,
new vscode.Position(end.line + 1, 0)
);
}
}
editBuilder.delete(deleteRange);
});
})
.then(() => {
// For linewise deletions, make sure cursor is at beginning of line
editor.selections = editor.selections.map((selection, i) => {
const range = ranges[i];
if (range && linewise) {
const newPosition = selection.start.with({ character: 0 });
return new vscode.Selection(newPosition, newPosition);
} else {
return selection;
}
});
});
}
export function yank(
vimState: VimState,
editor: vscode.TextEditor,
ranges: (vscode.Range | undefined)[],
linewise: boolean
) {
vimState.registers = {
contentsList: ranges.map((range, i) => {
if (range) {
return editor.document.getText(range);
} else {
return vimState.registers.contentsList[i];
}
}),
linewise: linewise
};
}

9
src/array_utils.ts Normal file
View File

@ -0,0 +1,9 @@
export function arrayFindLast<T>(xs: T[], p: (x: T) => boolean): T | undefined {
const filtered = xs.filter(p);
if (filtered.length === 0) {
return undefined;
} else {
return filtered[filtered.length - 1];
}
}

137
src/block_utils.ts Normal file
View File

@ -0,0 +1,137 @@
import * as vscode from "vscode";
enum MatchType {
start = "start",
end = "end",
}
interface BlockMatch {
type: MatchType;
match: RegExpMatchArray;
}
const startRegex = (startWords: string[]) =>
RegExp(`(^|\\s)(${startWords.join("|")})($|\\s)`, "g");
const endRegex = (endWords: string[]) =>
RegExp(`(^|\\s)(${endWords.join("|")})($|\\W)`, "g");
export function blockRange(
document: vscode.TextDocument,
position: vscode.Position
): vscode.Range {
let startWords: string[] = [];
let endWords: string[] = [];
console.log(`LanguageID=${document.languageId}`);
if (document.languageId === "elixir") {
startWords = ["case", "cond", "fn", "def"];
endWords = ["end"];
} else {
console.log(`Unsupported language: ${document.languageId}`);
return new vscode.Range(position, position);
}
const start = findBlockStart(document, position, startWords, endWords);
const end = findBlockEnd(document, position, startWords, endWords);
if (start && end) {
return new vscode.Range(start, end);
}
return new vscode.Range(position, position);
}
function findBlockStart(
document: vscode.TextDocument,
position: vscode.Position,
startWords: string[],
endWords: string[]
): vscode.Position | undefined {
let closedBlocks: boolean[] = [];
for (let i = position.line; i >= 0; --i) {
let lineText =
i === position.line
? document.lineAt(i).text.substr(position.character)
: document.lineAt(i).text;
let blockMatches: BlockMatch[] = [];
for (const m of lineText.matchAll(startRegex(startWords))) {
blockMatches.push({ type: MatchType.start, match: m });
}
for (const m of lineText.matchAll(endRegex(endWords))) {
blockMatches.push({ type: MatchType.end, match: m });
}
blockMatches = blockMatches.sort((a, b) =>
(a.match.index as number) > (b.match.index as number) ? 1 : -1
);
for (let idx = 0; idx < blockMatches.length; idx++) {
const blockMatch = blockMatches[idx];
if (blockMatch.type === MatchType.end) {
closedBlocks.push(true);
} else if (blockMatch.type === MatchType.start) {
if (closedBlocks.length === 0) {
const [fullText, , matchText] = blockMatch.match;
const offset = fullText.indexOf(matchText);
return new vscode.Position(
i,
(blockMatch.match.index as number) + offset
);
} else {
closedBlocks.pop();
}
}
}
}
return undefined;
}
function findBlockEnd(
document: vscode.TextDocument,
position: vscode.Position,
startWords: string[],
endWords: string[]
): vscode.Position | undefined {
let openedBlocks: boolean[] = [true];
for (let i = position.line; i < document.lineCount; ++i) {
let lineText =
i === position.line
? document.lineAt(i).text.substr(position.character)
: document.lineAt(i).text;
let blockMatches: BlockMatch[] = [];
for (const m of lineText.matchAll(startRegex(startWords))) {
blockMatches.push({ type: MatchType.start, match: m });
}
for (const m of lineText.matchAll(endRegex(endWords))) {
blockMatches.push({ type: MatchType.end, match: m });
}
blockMatches = blockMatches.sort((a, b) =>
(a.match.index as number) > (b.match.index as number) ? 1 : -1
);
for (let idx = 0; idx < blockMatches.length; idx++) {
const blockMatch = blockMatches[idx];
if (blockMatch.type === MatchType.start) {
openedBlocks.push(true);
} else if (blockMatch.type === MatchType.end) {
openedBlocks.pop();
if (openedBlocks.length === 0) {
const [fullText, , matchText] = blockMatch.match;
const offset = fullText.indexOf(matchText);
return new vscode.Position(
i,
(blockMatch.match.index as number) + offset + matchText.length
);
}
}
}
}
return undefined;
}

53
src/escape_handler.ts Normal file
View File

@ -0,0 +1,53 @@
import * as vscode from "vscode";
import { VimState } from "./vim_state_types";
import { enterNormalMode, setModeCursorStyle } from "./modes";
import { addTypeSubscription } from "./type_subscription";
import { typeHandler } from "./type_handler";
import * as positionUtils from "./position_utils";
import { Mode } from "./modes_types";
export function escapeHandler(vimState: VimState): void {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
if (vimState.mode === Mode.Insert || vimState.mode === Mode.Occurrence) {
editor.selections = editor.selections.map(selection => {
const newPosition = positionUtils.left(selection.active);
return new vscode.Selection(newPosition, newPosition);
});
enterNormalMode(vimState);
setModeCursorStyle(vimState.mode, editor);
addTypeSubscription(vimState, typeHandler);
} else if (vimState.mode === Mode.Normal) {
// Clear multiple cursors
if (editor.selections.length > 1) {
editor.selections = [editor.selections[0]];
}
} else if (vimState.mode === Mode.Visual) {
editor.selections = editor.selections.map(selection => {
const newPosition = new vscode.Position(
selection.active.line,
Math.max(selection.active.character - 1, 0)
);
return new vscode.Selection(newPosition, newPosition);
});
enterNormalMode(vimState);
setModeCursorStyle(vimState.mode, editor);
} else if (vimState.mode === Mode.VisualLine) {
editor.selections = editor.selections.map(selection => {
const newPosition = selection.active.with({
character: Math.max(selection.active.character - 1, 0)
});
return new vscode.Selection(newPosition, newPosition);
});
enterNormalMode(vimState);
setModeCursorStyle(vimState.mode, editor);
}
vimState.keysPressed = [];
}

115
src/extension.ts Normal file
View File

@ -0,0 +1,115 @@
import * as vscode from "vscode";
import { Mode } from "./modes_types";
import * as scrollCommands from "./scroll_commands";
import { enterNormalMode, enterVisualMode, setModeCursorStyle } from "./modes";
import { typeHandler } from "./type_handler";
import {
addTypeSubscription,
removeTypeSubscription
} from "./type_subscription";
import { VimState } from "./vim_state_types";
import { escapeHandler } from "./escape_handler";
const globalVimState: VimState = {
typeSubscription: undefined,
mode: Mode.Insert,
keysPressed: [],
registers: {
contentsList: [],
linewise: true
},
semicolonAction: () => undefined,
commaAction: () => undefined,
lastPutRanges: {
ranges: [],
linewise: true
}
};
function onSelectionChange(
vimState: VimState,
e: vscode.TextEditorSelectionChangeEvent
): void {
if (vimState.mode === Mode.Insert) return;
if (e.selections.every(selection => selection.isEmpty)) {
// It would be nice if we could always go from visual to normal mode when all selections are empty
// but visual mode on an empty line will yield an empty selection and there's no good way of
// distinguishing that case from the rest. So we only do it for mouse events.
if (
(vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) &&
e.kind === vscode.TextEditorSelectionChangeKind.Mouse
) {
enterNormalMode(vimState);
setModeCursorStyle(vimState.mode, e.textEditor);
}
} else {
if (vimState.mode === Mode.Normal) {
enterVisualMode(vimState);
setModeCursorStyle(vimState.mode, e.textEditor);
}
}
}
function onDidChangeActiveTextEditor(
vimState: VimState,
editor: vscode.TextEditor | undefined
) {
if (!editor) return;
if (editor.selections.every(selection => selection.isEmpty)) {
if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) {
enterNormalMode(vimState);
}
} else {
if (vimState.mode === Mode.Normal) {
enterVisualMode(vimState);
}
}
setModeCursorStyle(vimState.mode, editor);
vimState.keysPressed = [];
}
export function activate(context: vscode.ExtensionContext): void {
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(editor =>
onDidChangeActiveTextEditor(globalVimState, editor)
),
vscode.window.onDidChangeTextEditorSelection(e =>
onSelectionChange(globalVimState, e)
),
vscode.commands.registerCommand("extension.simpleVim.escapeKey", () =>
escapeHandler(globalVimState)
),
vscode.commands.registerCommand(
"extension.simpleVim.scrollDownHalfPage",
scrollCommands.scrollDownHalfPage
),
vscode.commands.registerCommand(
"extension.simpleVim.scrollUpHalfPage",
scrollCommands.scrollUpHalfPage
),
vscode.commands.registerCommand(
"extension.simpleVim.scrollDownPage",
scrollCommands.scrollDownPage
),
vscode.commands.registerCommand(
"extension.simpleVim.scrollUpPage",
scrollCommands.scrollUpPage
)
);
enterNormalMode(globalVimState);
addTypeSubscription(globalVimState, typeHandler);
if (vscode.window.activeTextEditor) {
onDidChangeActiveTextEditor(globalVimState, vscode.window.activeTextEditor);
}
}
export function deactivate(): void {
removeTypeSubscription(globalVimState);
}

196
src/indent_utils.ts Normal file
View File

@ -0,0 +1,196 @@
import * as vscode from "vscode";
import { SimpleRange } from "./simple_range_types";
export function indentLevelRange(
document: vscode.TextDocument,
lineNumber: number
): SimpleRange {
const indentLevel = findIndentLevel(document, lineNumber);
const rangeStart = indentLevelRangeBefore(document, lineNumber, indentLevel);
const rangeEnd = indentLevelRangeAfter(document, lineNumber + 1, indentLevel);
if (rangeStart && rangeEnd) {
return { start: rangeStart.start, end: rangeEnd.end };
} else if (rangeStart) {
return rangeStart;
} else if (rangeEnd) {
return rangeEnd;
} else {
// This will never happen but the typechecker can't know that
return { start: lineNumber, end: lineNumber };
}
}
export function aroundIndentLevelRange(
document: vscode.TextDocument,
lineNumber: number
): SimpleRange {
const indentLevel = findIndentLevel(document, lineNumber);
const rangeStart = aroundIndentLevelRangeBefore(
document,
lineNumber,
indentLevel
);
const rangeEnd = aroundIndentLevelRangeAfter(
document,
lineNumber + 1,
indentLevel
);
if (rangeStart && rangeEnd) {
return { start: rangeStart.start, end: rangeEnd.end };
} else if (rangeStart) {
return rangeStart;
} else if (rangeEnd) {
return rangeEnd;
} else {
// This will never happen but the typechecker can't know that
return { start: lineNumber, end: lineNumber };
}
}
function aroundIndentLevelRangeBefore(
document: vscode.TextDocument,
lineNumber: number,
indentLevel: number
): SimpleRange | undefined {
let result;
const insideRange = indentLevelRangeBefore(document, lineNumber, indentLevel);
if (typeof insideRange !== "undefined") {
result = insideRange;
for (let i = Math.max(0, insideRange.start - 1); i >= 0; --i) {
const line = document.lineAt(i);
if (!line.isEmptyOrWhitespace) {
result.start = i;
} else {
return result;
}
}
}
return result;
}
function indentLevelRangeBefore(
document: vscode.TextDocument,
lineNumber: number,
indentLevel: number
): SimpleRange | undefined {
let result;
for (let i = lineNumber; i >= 0; --i) {
const line = document.lineAt(i);
if (line.firstNonWhitespaceCharacterIndex >= indentLevel) {
// if (!line.isEmptyOrWhitespace) {
if (result) {
result.start = i;
} else {
result = { start: i, end: i };
}
// }
} else {
if (!line.isEmptyOrWhitespace) {
return result;
}
}
}
return result;
}
function aroundIndentLevelRangeAfter(
document: vscode.TextDocument,
lineNumber: number,
indentLevel: number
): SimpleRange | undefined {
let result;
const insideRange = indentLevelRangeAfter(document, lineNumber, indentLevel);
if (typeof insideRange !== "undefined") {
result = insideRange;
for (let i = insideRange.end + 1; i < document.lineCount; ++i) {
const line = document.lineAt(i);
if (!line.isEmptyOrWhitespace) {
result.end = i;
} else {
return result;
}
}
}
return result;
}
function indentLevelRangeAfter(
document: vscode.TextDocument,
lineNumber: number,
indentLevel: number
): SimpleRange | undefined {
let result;
for (let i = lineNumber; i < document.lineCount; ++i) {
const line = document.lineAt(i);
if (line.firstNonWhitespaceCharacterIndex >= indentLevel) {
// if (!line.isEmptyOrWhitespace) {
if (result) {
result.end = i;
} else {
result = { start: i, end: i };
}
// }
} else {
if (!line.isEmptyOrWhitespace) {
return result;
}
}
}
return result;
}
function findIndentLevel(document: vscode.TextDocument, lineNumber: number) {
const line = document.lineAt(lineNumber);
if (!line.isEmptyOrWhitespace) {
return line.firstNonWhitespaceCharacterIndex;
}
return Math.max(
findIndentLevelForward(document, lineNumber + 1),
findIndentLevelBackward(document, lineNumber - 1)
);
}
function findIndentLevelForward(
document: vscode.TextDocument,
lineNumber: number
): number {
for (let i = lineNumber; i < document.lineCount; ++i) {
const line = document.lineAt(i);
if (!line.isEmptyOrWhitespace) {
return line.firstNonWhitespaceCharacterIndex;
}
}
return 0;
}
function findIndentLevelBackward(
document: vscode.TextDocument,
lineNumber: number
): number {
for (let i = lineNumber; i >= 0; --i) {
const line = document.lineAt(i);
if (!line.isEmptyOrWhitespace) {
return line.firstNonWhitespaceCharacterIndex;
}
}
return 0;
}

56
src/modes.ts Normal file
View File

@ -0,0 +1,56 @@
import * as vscode from "vscode";
import { Mode } from "./modes_types";
import { VimState } from "./vim_state_types";
export function enterInsertMode(vimState: VimState): void {
vimState.mode = Mode.Insert;
setModeContext("extension.simpleVim.insertMode");
}
export function enterNormalMode(vimState: VimState): void {
vimState.mode = Mode.Normal;
setModeContext("extension.simpleVim.normalMode");
}
export function enterVisualMode(vimState: VimState): void {
vimState.mode = Mode.Visual;
setModeContext("extension.simpleVim.visualMode");
}
export function enterVisualLineMode(vimState: VimState): void {
vimState.mode = Mode.VisualLine;
setModeContext("extension.simpleVim.visualLineMode");
}
export function enterOccurrenceMode(vimState: VimState): void {
vimState.mode = Mode.Occurrence;
setModeContext("extension.simpleVim.occurrenceMode");
}
function setModeContext(key: string) {
const modeKeys = [
"extension.simpleVim.insertMode",
"extension.simpleVim.normalMode",
"extension.simpleVim.visualMode",
"extension.simpleVim.visualLineMode",
"extension.simpleVim.occurrenceMode"
];
modeKeys.forEach(modeKey => {
vscode.commands.executeCommand("setContext", modeKey, key === modeKey);
});
}
export function setModeCursorStyle(
mode: Mode,
editor: vscode.TextEditor
): void {
if (mode === Mode.Insert || mode === Mode.Occurrence) {
editor.options.cursorStyle = vscode.TextEditorCursorStyle.Line;
} else if (mode === Mode.Normal) {
editor.options.cursorStyle = vscode.TextEditorCursorStyle.Block;
} else if (mode === Mode.Visual || mode === Mode.VisualLine) {
editor.options.cursorStyle = vscode.TextEditorCursorStyle.Block;
}
}

7
src/modes_types.ts Normal file
View File

@ -0,0 +1,7 @@
export enum Mode {
Insert,
Normal,
Visual,
VisualLine,
Occurrence
}

111
src/paragraph_utils.ts Normal file
View File

@ -0,0 +1,111 @@
import * as vscode from "vscode";
import { SimpleRange } from "./simple_range_types";
export function paragraphForward(
document: vscode.TextDocument,
line: number
): number {
let visitedNonEmptyLine = false;
for (let i = line; i < document.lineCount; ++i) {
if (visitedNonEmptyLine) {
if (document.lineAt(i).isEmptyOrWhitespace) {
return i;
}
} else {
if (!document.lineAt(i).isEmptyOrWhitespace) {
visitedNonEmptyLine = true;
}
}
}
return document.lineCount - 1;
}
export function paragraphBackward(
document: vscode.TextDocument,
line: number
): number {
let visitedNonEmptyLine = false;
for (let i = line; i >= 0; --i) {
if (visitedNonEmptyLine) {
if (document.lineAt(i).isEmptyOrWhitespace) {
return i;
}
} else {
if (!document.lineAt(i).isEmptyOrWhitespace) {
visitedNonEmptyLine = true;
}
}
}
return 0;
}
export function paragraphRangeOuter(
document: vscode.TextDocument,
line: number
): SimpleRange | undefined {
if (document.lineAt(line).isEmptyOrWhitespace) return undefined;
return {
start: paragraphRangeBackward(document, line - 1),
end: paragraphRangeForwardOuter(document, line + 1)
};
}
function paragraphRangeForwardOuter(
document: vscode.TextDocument,
line: number
): number {
let seenWhitespace = false;
for (let i = line; i < document.lineCount; ++i) {
if (document.lineAt(i).isEmptyOrWhitespace) {
seenWhitespace = true;
} else if (seenWhitespace) {
return i - 1;
}
}
return document.lineCount - 1;
}
function paragraphRangeBackward(
document: vscode.TextDocument,
line: number
): number {
for (let i = line; i >= 0; --i) {
if (document.lineAt(i).isEmptyOrWhitespace) {
return i + 1;
}
}
return 0;
}
export function paragraphRangeInner(
document: vscode.TextDocument,
line: number
): SimpleRange | undefined {
if (document.lineAt(line).isEmptyOrWhitespace) return undefined;
return {
start: paragraphRangeBackward(document, line - 1),
end: paragraphRangeForwardInner(document, line + 1)
};
}
function paragraphRangeForwardInner(
document: vscode.TextDocument,
line: number
): number {
for (let i = line; i < document.lineCount; ++i) {
if (document.lineAt(i).isEmptyOrWhitespace) {
return i - 1;
}
}
return document.lineCount - 1;
}

261
src/parse_keys.ts Normal file
View File

@ -0,0 +1,261 @@
import * as vscode from "vscode";
import { VimState } from "./vim_state_types";
import { Mode } from "./modes_types";
import {
ParseKeysStatus,
OperatorRange,
ParseFailure,
ParseOperatorPartSuccess,
ParseOperatorRangeSuccess
} from "./parse_keys_types";
import { Action } from "./action_types";
export function arrayStartsWith<T>(prefix: T[], xs: T[]) {
if (xs.length < prefix.length) {
return false;
}
for (let i = 0; i < prefix.length; ++i) {
if (prefix[i] !== xs[i]) {
return false;
}
}
return true;
}
export function arrayEquals<T>(xs: T[], ys: T[]) {
if (xs.length !== ys.length) {
return false;
}
for (let i = 0; i < xs.length; ++i) {
if (xs[i] !== ys[i]) {
return false;
}
}
return true;
}
export function parseKeysExact(
matchKeys: string[],
modes: Mode[],
action: (vimState: VimState, editor: vscode.TextEditor) => void
): Action {
return (vimState, keys, editor) => {
if (modes && modes.indexOf(vimState.mode) < 0) {
return ParseKeysStatus.NO;
}
if (arrayEquals(keys, matchKeys)) {
action(vimState, editor);
return ParseKeysStatus.YES;
} else if (arrayStartsWith(keys, matchKeys)) {
return ParseKeysStatus.MORE_INPUT;
} else {
return ParseKeysStatus.NO;
}
};
}
export function parseKeysRegex(
doesPattern: RegExp,
couldPattern: RegExp,
modes: Mode[],
action: (
vimState: VimState,
editor: vscode.TextEditor,
match: RegExpMatchArray
) => void
): Action {
return (vimState, keys, editor) => {
if (modes && modes.indexOf(vimState.mode) < 0) {
return ParseKeysStatus.NO;
}
const keysStr = keys.join("");
const doesMatch = keysStr.match(doesPattern);
if (doesMatch) {
action(vimState, editor, doesMatch);
return ParseKeysStatus.YES;
} else if (keysStr.match(couldPattern)) {
return ParseKeysStatus.MORE_INPUT;
} else {
return ParseKeysStatus.NO;
}
};
}
function parseOperatorPart(
keys: string[],
operatorKeys: string[]
): ParseFailure | ParseOperatorPartSuccess {
if (arrayStartsWith(operatorKeys, keys)) {
return {
kind: "success",
rest: keys.slice(operatorKeys.length)
};
} else if (arrayStartsWith(keys, operatorKeys)) {
return {
kind: "failure",
status: ParseKeysStatus.MORE_INPUT
};
} else {
return {
kind: "failure",
status: ParseKeysStatus.NO
};
}
}
function parseOperatorRangePart(
vimState: VimState,
keys: string[],
editor: vscode.TextEditor,
motions: OperatorRange[]
): ParseFailure | ParseOperatorRangeSuccess {
let could = false;
for (const motion of motions) {
const result = motion(vimState, keys, editor);
if (result.kind === "success") {
return result;
} else if (result.status === ParseKeysStatus.MORE_INPUT) {
could = true;
}
}
if (could) {
return {
kind: "failure",
status: ParseKeysStatus.MORE_INPUT
};
} else {
return {
kind: "failure",
status: ParseKeysStatus.NO
};
}
}
export function parseKeysOperator(
operatorKeys: string[],
motions: OperatorRange[],
operator: (
vimState: VimState,
editor: vscode.TextEditor,
ranges: (vscode.Range | undefined)[],
linewise: boolean
) => void
): Action {
return (vimState, keys, editor) => {
const operatorResult = parseOperatorPart(keys, operatorKeys);
if (operatorResult.kind === "failure") {
return operatorResult.status;
}
let ranges: (vscode.Range | undefined)[];
let linewise = true;
if (vimState.mode === Mode.Normal) {
if (operatorResult.rest.length === 0) {
return ParseKeysStatus.MORE_INPUT;
}
const motionResult = parseOperatorRangePart(
vimState,
operatorResult.rest,
editor,
motions
);
if (motionResult.kind === "failure") {
return motionResult.status;
}
ranges = motionResult.ranges;
linewise = motionResult.linewise;
} else if (vimState.mode === Mode.VisualLine) {
ranges = editor.selections;
linewise = true;
} else {
ranges = editor.selections;
linewise = false;
}
operator(vimState, editor, ranges, linewise);
return ParseKeysStatus.YES;
};
}
export function createOperatorRangeExactKeys(
matchKeys: string[],
linewise: boolean,
f: (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position
) => vscode.Range | undefined
): OperatorRange {
return (vimState, keys, editor) => {
if (arrayEquals(keys, matchKeys)) {
const ranges = editor.selections.map(selection => {
return f(vimState, editor.document, selection.active);
});
return {
kind: "success",
ranges: ranges,
linewise: linewise
};
} else if (arrayStartsWith(keys, matchKeys)) {
return {
kind: "failure",
status: ParseKeysStatus.MORE_INPUT
};
} else {
return {
kind: "failure",
status: ParseKeysStatus.NO
};
}
};
}
export function createOperatorRangeRegex(
doesPattern: RegExp,
couldPattern: RegExp,
linewise: boolean,
f: (
vimState: VimState,
document: vscode.TextDocument,
position: vscode.Position,
match: RegExpMatchArray
) => vscode.Range | undefined
): OperatorRange {
return (vimState, keys, editor) => {
const keysStr = keys.join("");
const doesMatch = keysStr.match(doesPattern);
if (doesMatch) {
const ranges = editor.selections.map(selection => {
return f(vimState, editor.document, selection.active, doesMatch);
});
return {
kind: "success",
ranges: ranges,
linewise: linewise
};
} else if (keysStr.match(couldPattern)) {
return {
kind: "failure",
status: ParseKeysStatus.MORE_INPUT
};
} else {
return {
kind: "failure",
status: ParseKeysStatus.NO
};
}
};
}

36
src/parse_keys_types.ts Normal file
View File

@ -0,0 +1,36 @@
import * as vscode from "vscode";
import { VimState } from "./vim_state_types";
export enum ParseKeysStatus {
YES,
NO,
MORE_INPUT
}
export type ParseFailure = {
kind: "failure";
status: ParseKeysStatus;
};
export type ParseOperatorPartSuccess = {
kind: "success";
rest: string[];
};
export type ParseOperatorRangeSuccess = {
kind: "success";
ranges: (vscode.Range | undefined)[];
linewise: boolean;
};
export type ParseOperatorSuccess = {
kind: "success";
motion: OperatorRange | undefined;
};
export type OperatorRange = (
vimState: VimState,
keys: string[],
editor: vscode.TextEditor
) => ParseFailure | ParseOperatorRangeSuccess;

87
src/position_utils.ts Normal file
View File

@ -0,0 +1,87 @@
import * as vscode from "vscode";
export function left(
position: vscode.Position,
count: number = 1
): vscode.Position {
return position.with({
character: Math.max(position.character - count, 0)
});
}
export function right(
document: vscode.TextDocument,
position: vscode.Position,
count: number = 1
): vscode.Position {
const lineLength = document.lineAt(position.line).text.length;
return position.with({
character: Math.min(position.character + count, lineLength)
});
}
export function rightNormal(
document: vscode.TextDocument,
position: vscode.Position,
count: number = 1
): vscode.Position {
const lineLength = document.lineAt(position.line).text.length;
if (lineLength === 0) {
return position.with({ character: 0 });
} else {
return position.with({
character: Math.min(position.character + count, lineLength - 1)
});
}
}
export function leftWrap(
document: vscode.TextDocument,
position: vscode.Position
): vscode.Position {
if (position.character <= 0) {
if (position.line <= 0) {
return position;
} else {
const previousLineLength = document.lineAt(position.line - 1).text.length;
return new vscode.Position(position.line - 1, previousLineLength);
}
} else {
return position.with({ character: position.character - 1 });
}
}
export function rightWrap(
document: vscode.TextDocument,
position: vscode.Position
): vscode.Position {
const lineLength = document.lineAt(position.line).text.length;
if (position.character >= lineLength) {
if (position.line >= document.lineCount - 1) {
return position;
} else {
return new vscode.Position(position.line + 1, 0);
}
} else {
return position.with({ character: position.character + 1 });
}
}
export function lineEnd(
document: vscode.TextDocument,
position: vscode.Position
): vscode.Position {
const lineLength = document.lineAt(position.line).text.length;
return position.with({
character: lineLength
});
}
export function lastChar(document: vscode.TextDocument): vscode.Position {
return new vscode.Position(
document.lineCount - 1,
document.lineAt(document.lineCount - 1).text.length
);
}

141
src/put_utils/common.ts Normal file
View File

@ -0,0 +1,141 @@
import * as vscode from "vscode";
import { VimState } from "../vim_state_types";
export function getRegisterContentsList(
vimState: VimState,
editor: vscode.TextEditor
) {
if (vimState.registers.contentsList.length === 0) return undefined;
let registerContentsList = vimState.registers.contentsList;
// Handle putting with a different number of cursors than when you yanked
if (vimState.registers.contentsList.length !== editor.selections.length) {
const combinedContents = vimState.registers.contentsList.join("\n");
registerContentsList = editor.selections.map(selection => combinedContents);
}
return registerContentsList;
}
// Given contents and positions at the end of the contents, return the position at the beginning of the contents
export function getInsertRangesFromEnd(
document: vscode.TextDocument,
positions: vscode.Position[],
contentsList: (string | undefined)[]
) {
return positions.map((position, i) => {
const contents = contentsList[i];
if (!contents) return undefined;
const lines = contents.split(/\r?\n/);
let beginningPosition;
if (lines.length > 1) {
const beginningLine = position.line - (lines.length - 1);
const beginningCharacter =
document.lineAt(beginningLine).text.length - lines[0].length;
beginningPosition = new vscode.Position(
beginningLine,
beginningCharacter
);
} else {
beginningPosition = position.with({
character: position.character - lines[0].length
});
}
return new vscode.Range(beginningPosition, position);
});
}
// Given positions and contents inserted at those positions, return the range that will
// select that contents
export function getInsertRangesFromBeginning(
positions: vscode.Position[],
contentsList: (string | undefined)[]
) {
return positions.map((position, i) => {
const contents = contentsList[i];
if (!contents) return undefined;
const lines = contents.split(/\r?\n/);
const endLine = position.line + lines.length - 1;
const endCharacter =
lines.length === 1
? position.character + lines[0].length
: lines[lines.length - 1].length;
return new vscode.Range(
position,
new vscode.Position(endLine, endCharacter)
);
});
}
// Given positions and contents inserted at those positions, figure out how the positions will move
// when the contents is inserted. For example inserting a line above a position will increase its
// line number by one.
export function adjustInsertPositions(
positions: vscode.Position[],
contentsList: (string | undefined)[]
) {
const indexPositions = positions.map((position, i) => ({
originalIndex: i,
position: position
}));
indexPositions.sort((a, b) => {
if (a.position.isBefore(b.position)) return -1;
else if (a.position.isEqual(b.position)) return 0;
else return 1;
});
const adjustedIndexPositions = [];
let lineOffset = 0;
let characterOffset = 0;
let lineNumber = 0;
for (const indexPosition of indexPositions) {
// Adjust position
const adjustedLine = indexPosition.position.line + lineOffset;
let adjustedCharacter = indexPosition.position.character;
if (indexPosition.position.line === lineNumber) {
adjustedCharacter += characterOffset;
}
adjustedIndexPositions.push({
originalIndex: indexPosition.originalIndex,
position: new vscode.Position(adjustedLine, adjustedCharacter)
});
// Increase offsets
const contents = contentsList[indexPosition.originalIndex];
if (contents !== undefined) {
const contentsLines = contents.split(/\r?\n/);
lineOffset += contentsLines.length - 1;
if (indexPosition.position.line === lineNumber) {
if (contentsLines.length === 1) {
characterOffset += contentsLines[0].length;
} else {
characterOffset +=
contentsLines[contentsLines.length - 1].length -
indexPosition.position.character;
}
} else {
characterOffset = 0;
lineNumber = indexPosition.position.line;
}
}
}
adjustedIndexPositions.sort((a, b) => a.originalIndex - b.originalIndex);
return adjustedIndexPositions.map(indexPosition => indexPosition.position);
}

190
src/put_utils/put_after.ts Normal file
View File

@ -0,0 +1,190 @@
import * as vscode from "vscode";
import * as positionUtils from "../position_utils";
import { VimState } from "../vim_state_types";
import { Mode } from "../modes_types";
import { enterNormalMode, setModeCursorStyle } from "../modes";
import {
getRegisterContentsList,
adjustInsertPositions,
getInsertRangesFromBeginning,
getInsertRangesFromEnd
} from "./common";
export function putAfter(vimState: VimState, editor: vscode.TextEditor) {
const registerContentsList = getRegisterContentsList(vimState, editor);
if (registerContentsList === undefined) return;
if (vimState.mode === Mode.Normal) {
if (vimState.registers.linewise) {
normalModeLinewise(vimState, editor, registerContentsList);
} else {
normalModeCharacterwise(vimState, editor, registerContentsList);
}
} else if (vimState.mode === Mode.Visual) {
visualMode(vimState, editor, registerContentsList);
} else {
visualLineMode(vimState, editor, registerContentsList);
}
}
function normalModeLinewise(
vimState: VimState,
editor: vscode.TextEditor,
registerContentsList: (string | undefined)[]
) {
const insertContentsList = registerContentsList.map(contents => {
if (contents === undefined) return undefined;
else return "\n" + contents;
});
const insertPositions = editor.selections.map(selection => {
const lineLength = editor.document.lineAt(selection.active.line).text
.length;
return new vscode.Position(selection.active.line, lineLength);
});
const adjustedInsertPositions = adjustInsertPositions(
insertPositions,
insertContentsList
);
const rangeBeginnings = adjustedInsertPositions.map(
position => new vscode.Position(position.line + 1, 0)
);
editor
.edit(editBuilder => {
insertPositions.forEach((position, i) => {
const contents = insertContentsList[i];
if (contents === undefined) return;
editBuilder.insert(position, contents);
});
})
.then(() => {
editor.selections = rangeBeginnings.map(
position => new vscode.Selection(position, position)
);
});
vimState.lastPutRanges = {
ranges: getInsertRangesFromBeginning(rangeBeginnings, registerContentsList),
linewise: true
};
}
function normalModeCharacterwise(
vimState: VimState,
editor: vscode.TextEditor,
registerContentsList: (string | undefined)[]
) {
const insertPositions = editor.selections.map(selection => {
return positionUtils.right(editor.document, selection.active);
});
const adjustedInsertPositions = adjustInsertPositions(
insertPositions,
registerContentsList
);
const insertRanges = getInsertRangesFromBeginning(
adjustedInsertPositions,
registerContentsList
);
editor
.edit(editBuilder => {
insertPositions.forEach((insertPosition, i) => {
const registerContents = registerContentsList[i];
if (registerContents === undefined) return;
editBuilder.insert(insertPosition, registerContents);
});
})
.then(() => {
editor.selections = editor.selections.map((selection, i) => {
const range = insertRanges[i];
if (range === undefined) return selection;
const position = positionUtils.left(range.end);
return new vscode.Selection(position, position);
});
});
vimState.lastPutRanges = {
ranges: insertRanges,
linewise: false
};
}
function visualMode(
vimState: VimState,
editor: vscode.TextEditor,
registerContentsList: (string | undefined)[]
) {
const insertContentsList = vimState.registers.linewise
? registerContentsList.map(contents => {
if (!contents) return undefined;
else return "\n" + contents + "\n";
})
: registerContentsList;
editor
.edit(editBuilder => {
editor.selections.forEach((selection, i) => {
const contents = insertContentsList[i];
if (contents === undefined) return;
editBuilder.delete(selection);
editBuilder.insert(selection.start, contents);
});
})
.then(() => {
vimState.lastPutRanges = {
ranges: getInsertRangesFromEnd(
editor.document,
editor.selections.map(selection => selection.active),
insertContentsList
),
linewise: vimState.registers.linewise
};
editor.selections = editor.selections.map(selection => {
const newPosition = positionUtils.left(selection.active);
return new vscode.Selection(newPosition, newPosition);
});
});
enterNormalMode(vimState);
setModeCursorStyle(vimState.mode, editor);
}
function visualLineMode(
vimState: VimState,
editor: vscode.TextEditor,
registerContentsList: (string | undefined)[]
) {
editor
.edit(editBuilder => {
editor.selections.forEach((selection, i) => {
const registerContents = registerContentsList[i];
if (registerContents === undefined) return;
editBuilder.replace(selection, registerContents);
});
})
.then(() => {
vimState.lastPutRanges = {
ranges: editor.selections.map(
selection => new vscode.Range(selection.start, selection.end)
),
linewise: vimState.registers.linewise
};
editor.selections = editor.selections.map(selection => {
return new vscode.Selection(selection.start, selection.start);
});
enterNormalMode(vimState);
setModeCursorStyle(vimState.mode, editor);
});
}

106
src/put_utils/put_before.ts Normal file
View File

@ -0,0 +1,106 @@
import * as vscode from "vscode";
import * as positionUtils from "../position_utils";
import { VimState } from "../vim_state_types";
import {
getRegisterContentsList,
adjustInsertPositions,
getInsertRangesFromBeginning
} from "./common";
export function putBefore(vimState: VimState, editor: vscode.TextEditor) {
const registerContentsList = getRegisterContentsList(vimState, editor);
if (registerContentsList === undefined) return;
if (vimState.registers.linewise) {
normalModeLinewise(vimState, editor, registerContentsList);
} else {
normalModeCharacterwise(vimState, editor, registerContentsList);
}
}
function normalModeLinewise(
vimState: VimState,
editor: vscode.TextEditor,
registerContentsList: (string | undefined)[]
) {
const insertContentsList = registerContentsList.map(contents => {
if (contents === undefined) return undefined;
else return contents + "\n";
});
const insertPositions = editor.selections.map(selection => {
return new vscode.Position(selection.active.line, 0);
});
const adjustedInsertPositions = adjustInsertPositions(
insertPositions,
insertContentsList
);
editor
.edit(editBuilder => {
insertPositions.forEach((position, i) => {
const contents = insertContentsList[i];
if (contents === undefined) return;
editBuilder.insert(position, contents);
});
})
.then(() => {
editor.selections = editor.selections.map((selection, i) => {
const position = adjustedInsertPositions[i];
if (position === undefined) return selection;
return new vscode.Selection(position, position);
});
});
vimState.lastPutRanges = {
ranges: getInsertRangesFromBeginning(
adjustedInsertPositions,
registerContentsList
),
linewise: true
};
}
function normalModeCharacterwise(
vimState: VimState,
editor: vscode.TextEditor,
registerContentsList: (string | undefined)[]
) {
const insertPositions = editor.selections.map(selection => selection.active);
const adjustedInsertPositions = adjustInsertPositions(
insertPositions,
registerContentsList
);
const insertRanges = getInsertRangesFromBeginning(
adjustedInsertPositions,
registerContentsList
);
editor
.edit(editBuilder => {
insertPositions.forEach((insertPosition, i) => {
const registerContents = registerContentsList[i];
if (registerContents === undefined) return;
editBuilder.insert(insertPosition, registerContents);
});
})
.then(() => {
editor.selections = editor.selections.map((selection, i) => {
const range = insertRanges[i];
if (range === undefined) return selection;
const position = positionUtils.left(range.end);
return new vscode.Selection(position, position);
});
});
vimState.lastPutRanges = {
ranges: insertRanges,
linewise: false
};
}

55
src/quote_utils.ts Normal file
View File

@ -0,0 +1,55 @@
import * as vscode from "vscode";
import { SimpleRange } from "./simple_range_types";
export function findQuoteRange(
ranges: SimpleRange[],
position: vscode.Position
): SimpleRange | undefined {
const insideResult = ranges.find(
x => x.start <= position.character && x.end >= position.character
);
if (insideResult) {
return insideResult;
}
const outsideResult = ranges.find(x => x.start > position.character);
if (outsideResult) {
return outsideResult;
}
return undefined;
}
export function quoteRanges(quoteChar: string, s: string): SimpleRange[] {
let stateInQuote = false;
let stateStartIndex = 0;
let backslashCount = 0;
const ranges = [];
for (let i = 0; i < s.length; ++i) {
if (s[i] === quoteChar && backslashCount % 2 === 0) {
if (stateInQuote) {
ranges.push({
start: stateStartIndex,
end: i
});
stateInQuote = false;
} else {
stateInQuote = true;
stateStartIndex = i;
}
}
if (s[i] === "\\") {
++backslashCount;
} else {
backslashCount = 0;
}
}
return ranges;
}

24
src/scroll_commands.ts Normal file
View File

@ -0,0 +1,24 @@
import * as vscode from "vscode";
function editorScroll(to: string, by: string) {
vscode.commands.executeCommand("editorScroll", {
to: to,
by: by
});
}
export function scrollDownHalfPage(): void {
editorScroll("down", "halfPage");
}
export function scrollUpHalfPage(): void {
editorScroll("up", "halfPage");
}
export function scrollDownPage(): void {
editorScroll("down", "page");
}
export function scrollUpPage(): void {
editorScroll("up", "page");
}

95
src/search_utils.ts Normal file
View File

@ -0,0 +1,95 @@
import * as vscode from "vscode";
export function searchForward(
document: vscode.TextDocument,
needle: string,
fromPosition: vscode.Position
): vscode.Position | undefined {
for (let i = fromPosition.line; i < document.lineCount; ++i) {
const lineText = document.lineAt(i).text;
const fromIndex = i === fromPosition.line ? fromPosition.character : 0;
const matchIndex = lineText.indexOf(needle, fromIndex);
if (matchIndex >= 0) {
return new vscode.Position(i, matchIndex);
}
}
return undefined;
}
export function searchBackward(
document: vscode.TextDocument,
needle: string,
fromPosition: vscode.Position
): vscode.Position | undefined {
for (let i = fromPosition.line; i >= 0; --i) {
const lineText = document.lineAt(i).text;
const fromIndex =
i === fromPosition.line ? fromPosition.character : +Infinity;
const matchIndex = lineText.lastIndexOf(needle, fromIndex);
if (matchIndex >= 0) {
return new vscode.Position(i, matchIndex);
}
}
return undefined;
}
export function searchForwardBracket(
document: vscode.TextDocument,
openingChar: string,
closingChar: string,
fromPosition: vscode.Position
): vscode.Position | undefined {
let n = 0;
for (let i = fromPosition.line; i < document.lineCount; ++i) {
const lineText = document.lineAt(i).text;
const fromIndex = i === fromPosition.line ? fromPosition.character : 0;
for (let j = fromIndex; j < lineText.length; ++j) {
if (lineText[j] === openingChar) {
++n;
} else if (lineText[j] === closingChar) {
if (n === 0) {
return new vscode.Position(i, j);
} else {
--n;
}
}
}
}
return undefined;
}
export function searchBackwardBracket(
document: vscode.TextDocument,
openingChar: string,
closingChar: string,
fromPosition: vscode.Position
): vscode.Position | undefined {
let n = 0;
for (let i = fromPosition.line; i >= 0; --i) {
const lineText = document.lineAt(i).text;
const fromIndex =
i === fromPosition.line ? fromPosition.character : lineText.length - 1;
for (let j = fromIndex; j >= 0; --j) {
if (lineText[j] === closingChar) {
++n;
} else if (lineText[j] === openingChar) {
if (n === 0) {
return new vscode.Position(i, j);
} else {
--n;
}
}
}
}
return undefined;
}

69
src/selection_utils.ts Normal file
View File

@ -0,0 +1,69 @@
import * as vscode from "vscode";
import * as positionUtils from "./position_utils";
export function vscodeToVimVisualSelection(
document: vscode.TextDocument,
vscodeSelection: vscode.Selection
): vscode.Selection {
if (vscodeSelection.active.isBefore(vscodeSelection.anchor)) {
return new vscode.Selection(
positionUtils.left(vscodeSelection.anchor),
vscodeSelection.active
);
} else {
return new vscode.Selection(
vscodeSelection.anchor,
positionUtils.left(vscodeSelection.active)
);
}
}
export function vimToVscodeVisualSelection(
document: vscode.TextDocument,
vimSelection: vscode.Selection
): vscode.Selection {
if (vimSelection.active.isBefore(vimSelection.anchor)) {
return new vscode.Selection(
positionUtils.right(document, vimSelection.anchor),
vimSelection.active
);
} else {
return new vscode.Selection(
vimSelection.anchor,
positionUtils.right(document, vimSelection.active)
);
}
}
export function vscodeToVimVisualLineSelection(
document: vscode.TextDocument,
vscodeSelection: vscode.Selection
): vscode.Selection {
return new vscode.Selection(
vscodeSelection.anchor.with({ character: 0 }),
vscodeSelection.active.with({ character: 0 })
);
}
export function vimToVscodeVisualLineSelection(
document: vscode.TextDocument,
vimSelection: vscode.Selection
): vscode.Selection {
const anchorLineLength = document.lineAt(vimSelection.anchor.line).text
.length;
const activeLineLength = document.lineAt(vimSelection.active.line).text
.length;
if (vimSelection.active.isBefore(vimSelection.anchor)) {
return new vscode.Selection(
vimSelection.anchor.with({ character: anchorLineLength }),
vimSelection.active.with({ character: 0 })
);
} else {
return new vscode.Selection(
vimSelection.anchor.with({ character: 0 }),
vimSelection.active.with({ character: activeLineLength })
);
}
}

View File

@ -0,0 +1,4 @@
export type SimpleRange = {
start: number;
end: number;
};

134
src/tag_utils.ts Normal file
View File

@ -0,0 +1,134 @@
import * as vscode from "vscode";
import { SimpleRange } from "./simple_range_types";
type PartialTagOpening = {
kind: "opening";
name: string;
range: SimpleRange;
};
type PartialTagClosing = {
kind: "closing";
name: string;
range: SimpleRange;
};
type PartialTagSelfClosing = {
kind: "self_closing";
name: string;
range: SimpleRange;
};
type PartialTag = PartialTagOpening | PartialTagClosing | PartialTagSelfClosing;
type OffsetTag = {
name: string;
opening: SimpleRange;
closing?: SimpleRange; // Doesn't exist for self-closing tags
};
type PositionTag = {
name: string;
opening: vscode.Range;
closing?: vscode.Range; // Doesn't exist for self-closing tags
};
const OPEN_SLASH_GROUP = 1;
const TAG_NAME_GROUP = 2;
const CLOSE_SLASH_GROUP = 3;
export function getTags(document: vscode.TextDocument): PositionTag[] {
return positionTags(document, matchTags(getPartialTags(document.getText())));
}
function positionTags(
document: vscode.TextDocument,
offsetTags: OffsetTag[]
): PositionTag[] {
return offsetTags.map(tag => {
const openingRange = new vscode.Range(
document.positionAt(tag.opening.start),
document.positionAt(tag.opening.end)
);
if (tag.closing) {
return {
name: tag.name,
opening: openingRange,
closing: new vscode.Range(
document.positionAt(tag.closing.start),
document.positionAt(tag.closing.end)
)
};
} else {
return {
name: tag.name,
opening: openingRange
};
}
});
}
function matchTags(partialTags: PartialTag[]): OffsetTag[] {
const tags: OffsetTag[] = [];
const openingStack: PartialTagOpening[] = [];
partialTags.forEach(partialTag => {
if (partialTag.kind === "opening") {
openingStack.push(partialTag);
} else if (partialTag.kind === "self_closing") {
tags.push({
name: partialTag.name,
opening: partialTag.range
});
} else if (partialTag.kind === "closing") {
let stackTag = openingStack.pop();
while (stackTag) {
if (stackTag.name === partialTag.name) {
tags.push({
name: stackTag.name,
opening: stackTag.range,
closing: partialTag.range
});
break;
} else {
// Treat unclosed tags as self-closing because that's often the case in HTML
tags.push({
name: stackTag.name,
opening: stackTag.range
});
}
stackTag = openingStack.pop();
}
}
});
return tags.sort((a, b) => a.opening.start - b.opening.start);
}
function getPartialTags(text: string): PartialTag[] {
const regex = /\<(\/)?([^\>\<\s]+)[^\>\<]*?(\/?)\>/g;
const tagRanges: PartialTag[] = [];
let match = regex.exec(text);
while (match) {
const name = match[TAG_NAME_GROUP];
const range = { start: match.index, end: regex.lastIndex - 1 };
if (match[CLOSE_SLASH_GROUP]) {
tagRanges.push({ kind: "self_closing", name: name, range: range });
} else if (match[OPEN_SLASH_GROUP]) {
tagRanges.push({ kind: "closing", name: name, range: range });
} else {
tagRanges.push({ kind: "opening", name: name, range: range });
}
match = regex.exec(text);
}
return tagRanges;
}

33
src/type_handler.ts Normal file
View File

@ -0,0 +1,33 @@
import * as vscode from "vscode";
import { ParseKeysStatus } from "./parse_keys_types";
import { actions } from "./actions";
import { VimState } from "./vim_state_types";
export function typeHandler(vimState: VimState, char: string): void {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
vimState.keysPressed.push(char);
try {
let could = false;
for (const action of actions) {
const result = action(vimState, vimState.keysPressed, editor);
if (result === ParseKeysStatus.YES) {
vimState.keysPressed = [];
break;
} else if (result === ParseKeysStatus.MORE_INPUT) {
could = true;
}
}
if (!could) {
vimState.keysPressed = [];
}
} catch (error) {
console.error(error);
}
}

18
src/type_subscription.ts Normal file
View File

@ -0,0 +1,18 @@
import * as vscode from "vscode";
import { VimState } from "./vim_state_types";
export function addTypeSubscription(
vimState: VimState,
typeHandler: (vimState: VimState, char: string) => void
): void {
vimState.typeSubscription = vscode.commands.registerCommand("type", e => {
typeHandler(vimState, e.text);
});
}
export function removeTypeSubscription(vimState: VimState): void {
if (vimState.typeSubscription) {
vimState.typeSubscription.dispose();
}
}

19
src/vim_state_types.ts Normal file
View File

@ -0,0 +1,19 @@
import * as vscode from "vscode";
import { Mode } from "./modes_types";
export type VimState = {
typeSubscription: vscode.Disposable | undefined;
mode: Mode;
keysPressed: string[];
registers: {
contentsList: (string | undefined)[];
linewise: boolean;
};
semicolonAction: (vimState: VimState, editor: vscode.TextEditor) => void;
commaAction: (vimState: VimState, editor: vscode.TextEditor) => void;
lastPutRanges: {
ranges: (vscode.Range | undefined)[];
linewise: boolean;
};
};

21
src/visual_line_utils.ts Normal file
View File

@ -0,0 +1,21 @@
import * as vscode from "vscode";
export function setVisualLineSelections(editor: vscode.TextEditor): void {
editor.selections = editor.selections.map(selection => {
if (!selection.isReversed || selection.isSingleLine) {
const activeLineLength = editor.document.lineAt(selection.active.line)
.text.length;
return new vscode.Selection(
selection.anchor.with({ character: 0 }),
selection.active.with({ character: activeLineLength })
);
} else {
const anchorLineLength = editor.document.lineAt(selection.anchor.line)
.text.length;
return new vscode.Selection(
selection.anchor.with({ character: anchorLineLength }),
selection.active.with({ character: 0 })
);
}
});
}

40
src/visual_utils.ts Normal file
View File

@ -0,0 +1,40 @@
import * as vscode from "vscode";
import * as positionUtils from "./position_utils";
// This fixes the selection anchor when selection is changed so that active and anchor are reversed.
// For most motions we use execMotion for perfect visual mode emulation, but for some we need to
// use VSCode's cursorMove instead and this function allows us to fix the selection after the fact.
export function setVisualSelections(
editor: vscode.TextEditor,
originalSelections: vscode.Selection[]
): void {
editor.selections = editor.selections.map((selection, i) => {
const originalSelection = originalSelections[i];
let activePosition = selection.active;
if (!selection.isReversed && selection.active.character === 0) {
activePosition = positionUtils.right(editor.document, selection.active);
}
if (
originalSelection.active.isBefore(originalSelection.anchor) &&
selection.active.isAfterOrEqual(selection.anchor)
) {
return new vscode.Selection(
positionUtils.left(selection.anchor),
activePosition
);
} else if (
originalSelection.active.isAfter(originalSelection.anchor) &&
selection.active.isBeforeOrEqual(selection.anchor)
) {
return new vscode.Selection(
positionUtils.right(editor.document, selection.anchor),
activePosition
);
} else {
return new vscode.Selection(selection.anchor, activePosition);
}
});
}

115
src/word_utils.ts Normal file
View File

@ -0,0 +1,115 @@
const NON_WORD_CHARACTERS = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-";
export function whitespaceWordRanges(
text: string
): { start: number; end: number }[] {
enum State {
Whitespace,
Word
}
let state = State.Whitespace;
let startIndex = 0;
const ranges = [];
for (let i = 0; i < text.length; ++i) {
const char = text[i];
if (state === State.Whitespace) {
if (!isWhitespaceCharacter(char)) {
startIndex = i;
state = State.Word;
}
} else {
if (isWhitespaceCharacter(char)) {
ranges.push({
start: startIndex,
end: i - 1
});
state = State.Whitespace;
}
}
}
if (state === State.Word) {
ranges.push({
start: startIndex,
end: text.length - 1
});
}
return ranges;
}
export function wordRanges(text: string): { start: number; end: number }[] {
enum State {
Whitespace,
Word,
NonWord
}
let state = State.Whitespace;
let startIndex = 0;
const ranges = [];
for (let i = 0; i < text.length; ++i) {
const char = text[i];
if (state === State.Whitespace) {
if (!isWhitespaceCharacter(char)) {
startIndex = i;
state = isWordCharacter(char) ? State.Word : State.NonWord;
}
} else if (state === State.Word) {
if (!isWordCharacter(char)) {
ranges.push({
start: startIndex,
end: i - 1
});
if (isWhitespaceCharacter(char)) {
state = State.Whitespace;
} else {
state = State.NonWord;
startIndex = i;
}
}
} else {
if (!isNonWordCharacter(char)) {
ranges.push({
start: startIndex,
end: i - 1
});
if (isWhitespaceCharacter(char)) {
state = State.Whitespace;
} else {
state = State.Word;
startIndex = i;
}
}
}
}
if (state !== State.Whitespace) {
ranges.push({
start: startIndex,
end: text.length - 1
});
}
return ranges;
}
function isNonWordCharacter(char: string): boolean {
return NON_WORD_CHARACTERS.indexOf(char) >= 0;
}
function isWhitespaceCharacter(char: string): boolean {
return char === " " || char === "\t";
}
function isWordCharacter(char: string): boolean {
return !isWhitespaceCharacter(char) && !isNonWordCharacter(char);
}

15
src/yank_highlight.ts Normal file
View File

@ -0,0 +1,15 @@
import * as vscode from "vscode";
export function flashYankHighlight(
editor: vscode.TextEditor,
ranges: vscode.Range[]
) {
const decoration = vscode.window.createTextEditorDecorationType({
backgroundColor: vscode.workspace
.getConfiguration("simpleVim")
.get("yankHighlightBackgroundColor")
});
editor.setDecorations(decoration, ranges);
setTimeout(() => decoration.dispose(), 200);
}

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"lib": ["es6", "es2020.string"],
"sourceMap": true,
"rootDir": "src",
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"exclude": ["node_modules", ".vscode-test"]
}

17
tslint.json Normal file
View File

@ -0,0 +1,17 @@
{
"defaultSeverity": "error",
"extends": ["tslint:recommended"],
"jsRules": {},
"rules": {
"quotemark": [true, "single", "avoid-escape"],
"ordered-imports": false,
"curly": false,
"object-literal-shorthand": false,
"object-literal-sort-keys": false,
"arrow-parens": false,
"no-console": false,
"interface-over-type-literal": false,
"array-type": false
},
"rulesDirectory": []
}