├── .gitignore ├── src ├── simple_range_types.ts ├── modes_types.ts ├── actions │ ├── index.ts │ ├── operators.ts │ ├── actions.ts │ ├── motions.ts │ └── operator_ranges.ts ├── array_utils.ts ├── action_types.ts ├── yank_highlight.ts ├── type_subscription.ts ├── scroll_commands.ts ├── vim_state_types.ts ├── parse_keys_types.ts ├── visual_line_utils.ts ├── type_handler.ts ├── quote_utils.ts ├── visual_utils.ts ├── modes.ts ├── escape_handler.ts ├── position_utils.ts ├── selection_utils.ts ├── paragraph_utils.ts ├── search_utils.ts ├── put_utils │ ├── put_before.ts │ ├── common.ts │ └── put_after.ts ├── word_utils.ts ├── indent_utils.ts ├── extension.ts ├── tag_utils.ts └── parse_keys.ts ├── .vscodeignore ├── .vscode ├── settings.json └── launch.json ├── tsconfig.json ├── tslint.json ├── LICENSE.txt ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | .vsix 5 | -------------------------------------------------------------------------------- /src/simple_range_types.ts: -------------------------------------------------------------------------------- 1 | export type SimpleRange = { 2 | start: number; 3 | end: number; 4 | }; 5 | -------------------------------------------------------------------------------- /src/modes_types.ts: -------------------------------------------------------------------------------- 1 | export enum Mode { 2 | Insert, 3 | Normal, 4 | Visual, 5 | VisualLine, 6 | } 7 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | out/**/*.map 5 | src/** 6 | .gitignore 7 | tsconfig.json 8 | tslint.json 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": true, 4 | }, 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '../action_types'; 2 | import { actions as subActions } from './actions'; 3 | import { operators } from './operators'; 4 | import { motions } from './motions'; 5 | 6 | export const actions: Action[] = subActions.concat(operators, motions); 7 | -------------------------------------------------------------------------------- /src/array_utils.ts: -------------------------------------------------------------------------------- 1 | export function arrayFindLast(xs: T[], p: (x: T) => boolean): T | undefined { 2 | const filtered = xs.filter(p); 3 | 4 | if (filtered.length === 0) { 5 | return undefined; 6 | } else { 7 | return filtered[filtered.length - 1]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/action_types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { VimState } from './vim_state_types'; 4 | import { ParseKeysStatus } from './parse_keys_types'; 5 | 6 | export type Action = ( 7 | vimState: VimState, 8 | keys: string[], 9 | editor: vscode.TextEditor, 10 | ) => ParseKeysStatus; 11 | -------------------------------------------------------------------------------- /src/yank_highlight.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function flashYankHighlight(editor: vscode.TextEditor, ranges: vscode.Range[]) { 4 | const decoration = vscode.window.createTextEditorDecorationType({ 5 | backgroundColor: vscode.workspace.getConfiguration('simpleVim').get('yankHighlightBackgroundColor'), 6 | }); 7 | 8 | editor.setDecorations(decoration, ranges); 9 | setTimeout(() => decoration.dispose(), 200); 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 10 | "outFiles": [ "${workspaceRoot}/out/**/*.js" ], 11 | "preLaunchTask": "tsc: watch - tsconfig.json", 12 | }, 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "quotemark": [true, "single", "avoid-escape"], 9 | "ordered-imports": false, 10 | "curly": false, 11 | "object-literal-shorthand": false, 12 | "object-literal-sort-keys": false, 13 | "arrow-parens": false, 14 | "no-console": false, 15 | "interface-over-type-literal": false, 16 | "array-type": false 17 | }, 18 | "rulesDirectory": [] 19 | } 20 | -------------------------------------------------------------------------------- /src/type_subscription.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { VimState } from './vim_state_types'; 4 | 5 | export function addTypeSubscription( 6 | vimState: VimState, 7 | typeHandler: (vimState: VimState, char: string) => void, 8 | ): void { 9 | vimState.typeSubscription = vscode.commands.registerCommand('type', e => { 10 | typeHandler(vimState, e.text); 11 | }); 12 | } 13 | 14 | export function removeTypeSubscription(vimState: VimState): void { 15 | if (vimState.typeSubscription) { 16 | vimState.typeSubscription.dispose(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/scroll_commands.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | function editorScroll(to: string, by: string) { 4 | vscode.commands.executeCommand('editorScroll', { 5 | to: to, 6 | by: by, 7 | }); 8 | } 9 | 10 | export function scrollDownHalfPage(): void { 11 | editorScroll('down', 'halfPage'); 12 | } 13 | 14 | export function scrollUpHalfPage(): void { 15 | editorScroll('up', 'halfPage'); 16 | } 17 | 18 | export function scrollDownPage(): void { 19 | editorScroll('down', 'page'); 20 | } 21 | 22 | export function scrollUpPage(): void { 23 | editorScroll('up', 'page'); 24 | } 25 | -------------------------------------------------------------------------------- /src/vim_state_types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Mode } from './modes_types'; 4 | 5 | export type VimState = { 6 | typeSubscription: vscode.Disposable | undefined; 7 | mode: Mode; 8 | keysPressed: string[]; 9 | registers: { 10 | contentsList: (string | undefined)[]; 11 | linewise: boolean; 12 | }; 13 | semicolonAction: (vimState: VimState, editor: vscode.TextEditor) => void; 14 | commaAction: (vimState: VimState, editor: vscode.TextEditor) => void; 15 | lastPutRanges: { 16 | ranges: (vscode.Range | undefined)[]; 17 | linewise: boolean; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/parse_keys_types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { VimState } from './vim_state_types'; 4 | 5 | export enum ParseKeysStatus { 6 | YES, 7 | NO, 8 | MORE_INPUT, 9 | } 10 | 11 | export type ParseFailure = { 12 | kind: 'failure'; 13 | status: ParseKeysStatus; 14 | }; 15 | 16 | export type ParseOperatorPartSuccess = { 17 | kind: 'success'; 18 | rest: string[]; 19 | }; 20 | 21 | export type ParseOperatorRangeSuccess = { 22 | kind: 'success'; 23 | ranges: (vscode.Range | undefined)[]; 24 | linewise: boolean; 25 | }; 26 | 27 | export type ParseOperatorSuccess = { 28 | kind: 'success'; 29 | motion: OperatorRange | undefined; 30 | }; 31 | 32 | export type OperatorRange = ( 33 | vimState: VimState, 34 | keys: string[], 35 | editor: vscode.TextEditor, 36 | ) => ParseFailure | ParseOperatorRangeSuccess; 37 | -------------------------------------------------------------------------------- /src/visual_line_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function setVisualLineSelections(editor: vscode.TextEditor): void { 4 | editor.selections = editor.selections.map(selection => { 5 | if (!selection.isReversed || selection.isSingleLine) { 6 | const activeLineLength = editor.document.lineAt(selection.active.line).text.length; 7 | return new vscode.Selection( 8 | selection.anchor.with({ character: 0 }), 9 | selection.active.with({ character: activeLineLength }), 10 | ); 11 | } else { 12 | const anchorLineLength = editor.document.lineAt(selection.anchor.line).text.length; 13 | return new vscode.Selection( 14 | selection.anchor.with({ character: anchorLineLength }), 15 | selection.active.with({ character: 0 }), 16 | ); 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/type_handler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { ParseKeysStatus } from './parse_keys_types'; 4 | import { actions } from './actions'; 5 | import { VimState } from './vim_state_types'; 6 | 7 | export function typeHandler(vimState: VimState, char: string): void { 8 | const editor = vscode.window.activeTextEditor; 9 | 10 | if (!editor) return; 11 | 12 | vimState.keysPressed.push(char); 13 | 14 | try { 15 | let could = false; 16 | for (const action of actions) { 17 | const result = action(vimState, vimState.keysPressed, editor); 18 | 19 | if (result === ParseKeysStatus.YES) { 20 | vimState.keysPressed = []; 21 | break; 22 | } else if (result === ParseKeysStatus.MORE_INPUT) { 23 | could = true; 24 | } 25 | } 26 | 27 | if (!could) { 28 | vimState.keysPressed = []; 29 | } 30 | } catch (error) { 31 | console.error(error); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018 Jonathan Potter 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /src/quote_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { SimpleRange } from './simple_range_types'; 4 | 5 | export function findQuoteRange(ranges: SimpleRange[], position: vscode.Position): SimpleRange | undefined { 6 | const insideResult = ranges.find(x => x.start <= position.character && x.end >= position.character); 7 | 8 | if (insideResult) { 9 | return insideResult; 10 | } 11 | 12 | const outsideResult = ranges.find(x => x.start > position.character); 13 | 14 | if (outsideResult) { 15 | return outsideResult; 16 | } 17 | 18 | return undefined; 19 | } 20 | 21 | export function quoteRanges(quoteChar: string, s: string): SimpleRange[] { 22 | let stateInQuote = false; 23 | let stateStartIndex = 0; 24 | let backslashCount = 0; 25 | const ranges = []; 26 | 27 | for (let i = 0; i < s.length; ++i) { 28 | if (s[i] === quoteChar && backslashCount % 2 === 0) { 29 | if (stateInQuote) { 30 | ranges.push({ 31 | start: stateStartIndex, 32 | end: i, 33 | }); 34 | 35 | stateInQuote = false; 36 | } else { 37 | stateInQuote = true; 38 | stateStartIndex = i; 39 | } 40 | } 41 | 42 | if (s[i] === '\\') { 43 | ++backslashCount; 44 | } else { 45 | backslashCount = 0; 46 | } 47 | } 48 | 49 | return ranges; 50 | } 51 | -------------------------------------------------------------------------------- /src/visual_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as positionUtils from './position_utils'; 4 | 5 | // This fixes the selection anchor when selection is changed so that active and anchor are reversed. 6 | // For most motions we use execMotion for perfect visual mode emulation, but for some we need to 7 | // use VSCode's cursorMove instead and this function allows us to fix the selection after the fact. 8 | export function setVisualSelections(editor: vscode.TextEditor, originalSelections: vscode.Selection[]): void { 9 | editor.selections = editor.selections.map((selection, i) => { 10 | const originalSelection = originalSelections[i]; 11 | 12 | let activePosition = selection.active; 13 | if (!selection.isReversed && selection.active.character === 0) { 14 | activePosition = positionUtils.right(editor.document, selection.active); 15 | } 16 | 17 | if ( 18 | originalSelection.active.isBefore(originalSelection.anchor) && 19 | selection.active.isAfterOrEqual(selection.anchor) 20 | ) { 21 | return new vscode.Selection(positionUtils.left(selection.anchor), activePosition); 22 | } else if ( 23 | originalSelection.active.isAfter(originalSelection.anchor) && 24 | selection.active.isBeforeOrEqual(selection.anchor) 25 | ) { 26 | return new vscode.Selection(positionUtils.right(editor.document, selection.anchor), activePosition); 27 | } else { 28 | return new vscode.Selection(selection.anchor, activePosition); 29 | } 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/modes.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Mode } from './modes_types'; 4 | import { VimState } from './vim_state_types'; 5 | 6 | export function enterInsertMode(vimState: VimState): void { 7 | vimState.mode = Mode.Insert; 8 | setModeContext('extension.simpleVim.insertMode'); 9 | } 10 | 11 | export function enterNormalMode(vimState: VimState): void { 12 | vimState.mode = Mode.Normal; 13 | setModeContext('extension.simpleVim.normalMode'); 14 | } 15 | 16 | export function enterVisualMode(vimState: VimState): void { 17 | vimState.mode = Mode.Visual; 18 | setModeContext('extension.simpleVim.visualMode'); 19 | } 20 | 21 | export function enterVisualLineMode(vimState: VimState): void { 22 | vimState.mode = Mode.VisualLine; 23 | setModeContext('extension.simpleVim.visualLineMode'); 24 | } 25 | 26 | function setModeContext(key: string) { 27 | const modeKeys = [ 28 | 'extension.simpleVim.insertMode', 29 | 'extension.simpleVim.normalMode', 30 | 'extension.simpleVim.visualMode', 31 | 'extension.simpleVim.visualLineMode', 32 | ]; 33 | 34 | modeKeys.forEach(modeKey => { 35 | vscode.commands.executeCommand('setContext', modeKey, key === modeKey); 36 | }); 37 | } 38 | 39 | export function setModeCursorStyle(mode: Mode, editor: vscode.TextEditor): void { 40 | if (mode === Mode.Insert) { 41 | editor.options.cursorStyle = vscode.TextEditorCursorStyle.Line; 42 | } else if (mode === Mode.Normal) { 43 | editor.options.cursorStyle = vscode.TextEditorCursorStyle.Underline; 44 | } else if (mode === Mode.Visual || mode === Mode.VisualLine) { 45 | editor.options.cursorStyle = vscode.TextEditorCursorStyle.LineThin; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/escape_handler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { VimState } from './vim_state_types'; 4 | import { enterNormalMode, setModeCursorStyle } from './modes'; 5 | import { addTypeSubscription } from './type_subscription'; 6 | import { typeHandler } from './type_handler'; 7 | import * as positionUtils from './position_utils'; 8 | import { Mode } from './modes_types'; 9 | 10 | export function escapeHandler(vimState: VimState): void { 11 | const editor = vscode.window.activeTextEditor; 12 | 13 | if (!editor) return; 14 | 15 | if (vimState.mode === Mode.Insert) { 16 | editor.selections = editor.selections.map(selection => { 17 | const newPosition = positionUtils.left(selection.active); 18 | return new vscode.Selection(newPosition, newPosition); 19 | }); 20 | 21 | enterNormalMode(vimState); 22 | setModeCursorStyle(vimState.mode, editor); 23 | addTypeSubscription(vimState, typeHandler); 24 | } else if (vimState.mode === Mode.Normal) { 25 | // Clear multiple cursors 26 | if (editor.selections.length > 1) { 27 | editor.selections = [editor.selections[0]]; 28 | } 29 | } else if (vimState.mode === Mode.Visual) { 30 | editor.selections = editor.selections.map(selection => { 31 | const newPosition = new vscode.Position(selection.active.line, Math.max(selection.active.character - 1, 0)); 32 | return new vscode.Selection(newPosition, newPosition); 33 | }); 34 | 35 | enterNormalMode(vimState); 36 | setModeCursorStyle(vimState.mode, editor); 37 | } else if (vimState.mode === Mode.VisualLine) { 38 | editor.selections = editor.selections.map(selection => { 39 | const newPosition = selection.active.with({ 40 | character: Math.max(selection.active.character - 1, 0), 41 | }); 42 | return new vscode.Selection(newPosition, newPosition); 43 | }); 44 | 45 | enterNormalMode(vimState); 46 | setModeCursorStyle(vimState.mode, editor); 47 | } 48 | 49 | vimState.keysPressed = []; 50 | } 51 | -------------------------------------------------------------------------------- /src/position_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function left(position: vscode.Position, count: number = 1): vscode.Position { 4 | return position.with({ 5 | character: Math.max(position.character - count, 0), 6 | }); 7 | } 8 | 9 | export function right(document: vscode.TextDocument, position: vscode.Position, count: number = 1): vscode.Position { 10 | const lineLength = document.lineAt(position.line).text.length; 11 | return position.with({ 12 | character: Math.min(position.character + count, lineLength), 13 | }); 14 | } 15 | 16 | export function rightNormal( 17 | document: vscode.TextDocument, 18 | position: vscode.Position, 19 | count: number = 1, 20 | ): vscode.Position { 21 | const lineLength = document.lineAt(position.line).text.length; 22 | 23 | if (lineLength === 0) { 24 | return position.with({ character: 0 }); 25 | } else { 26 | return position.with({ 27 | character: Math.min(position.character + count, lineLength - 1), 28 | }); 29 | } 30 | } 31 | 32 | export function leftWrap(document: vscode.TextDocument, position: vscode.Position): vscode.Position { 33 | if (position.character <= 0) { 34 | if (position.line <= 0) { 35 | return position; 36 | } else { 37 | const previousLineLength = document.lineAt(position.line - 1).text.length; 38 | return new vscode.Position(position.line - 1, previousLineLength); 39 | } 40 | } else { 41 | return position.with({ character: position.character - 1 }); 42 | } 43 | } 44 | 45 | export function rightWrap(document: vscode.TextDocument, position: vscode.Position): vscode.Position { 46 | const lineLength = document.lineAt(position.line).text.length; 47 | 48 | if (position.character >= lineLength) { 49 | if (position.line >= document.lineCount - 1) { 50 | return position; 51 | } else { 52 | return new vscode.Position(position.line + 1, 0); 53 | } 54 | } else { 55 | return position.with({ character: position.character + 1 }); 56 | } 57 | } 58 | 59 | export function lineEnd(document: vscode.TextDocument, position: vscode.Position): vscode.Position { 60 | const lineLength = document.lineAt(position.line).text.length; 61 | return position.with({ 62 | character: lineLength, 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/selection_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as positionUtils from './position_utils'; 4 | 5 | export function vscodeToVimVisualSelection( 6 | document: vscode.TextDocument, 7 | vscodeSelection: vscode.Selection, 8 | ): vscode.Selection { 9 | if (vscodeSelection.active.isBefore(vscodeSelection.anchor)) { 10 | return new vscode.Selection( 11 | positionUtils.left(vscodeSelection.anchor), 12 | vscodeSelection.active, 13 | ); 14 | } else { 15 | return new vscode.Selection( 16 | vscodeSelection.anchor, 17 | positionUtils.left(vscodeSelection.active), 18 | ); 19 | } 20 | } 21 | 22 | export function vimToVscodeVisualSelection( 23 | document: vscode.TextDocument, 24 | vimSelection: vscode.Selection, 25 | ): vscode.Selection { 26 | if (vimSelection.active.isBefore(vimSelection.anchor)) { 27 | return new vscode.Selection( 28 | positionUtils.right(document, vimSelection.anchor), 29 | vimSelection.active, 30 | ); 31 | } else { 32 | return new vscode.Selection( 33 | vimSelection.anchor, 34 | positionUtils.right(document, vimSelection.active), 35 | ); 36 | } 37 | } 38 | 39 | export function vscodeToVimVisualLineSelection( 40 | document: vscode.TextDocument, 41 | vscodeSelection: vscode.Selection, 42 | ): vscode.Selection { 43 | return new vscode.Selection( 44 | vscodeSelection.anchor.with({ character: 0 }), 45 | vscodeSelection.active.with({ character: 0 }), 46 | ); 47 | } 48 | 49 | export function vimToVscodeVisualLineSelection( 50 | document: vscode.TextDocument, 51 | vimSelection: vscode.Selection, 52 | ): vscode.Selection { 53 | const anchorLineLength = document.lineAt(vimSelection.anchor.line).text.length; 54 | const activeLineLength = document.lineAt(vimSelection.active.line).text.length; 55 | 56 | if (vimSelection.active.isBefore(vimSelection.anchor)) { 57 | return new vscode.Selection( 58 | vimSelection.anchor.with({ character: anchorLineLength }), 59 | vimSelection.active.with({ character: 0 }), 60 | ); 61 | } else { 62 | return new vscode.Selection( 63 | vimSelection.anchor.with({ character: 0 }), 64 | vimSelection.active.with({ character: activeLineLength }), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-vim", 3 | "displayName": "Simple Vim", 4 | "description": "Vim extension for VSCode", 5 | "version": "0.0.5", 6 | "license": "MIT", 7 | "publisher": "jpotterm", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/jpotterm/vscode-simple-vim.git" 11 | }, 12 | "engines": { 13 | "vscode": "^1.22.2" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | "*" 20 | ], 21 | "main": "./out/extension", 22 | "contributes": { 23 | "keybindings": [ 24 | { 25 | "key": "Escape", 26 | "command": "extension.simpleVim.escapeKey", 27 | "when": "editorTextFocus" 28 | }, 29 | { 30 | "key": "ctrl+r", 31 | "command": "redo", 32 | "when": "editorTextFocus && !extension.simpleVim.insertMode" 33 | }, 34 | { 35 | "key": "ctrl+d", 36 | "command": "extension.simpleVim.scrollDownHalfPage", 37 | "when": "editorTextFocus && !extension.simpleVim.insertMode" 38 | }, 39 | { 40 | "key": "ctrl+u", 41 | "command": "extension.simpleVim.scrollUpHalfPage", 42 | "when": "editorTextFocus && !extension.simpleVim.insertMode" 43 | }, 44 | { 45 | "key": "ctrl+f", 46 | "command": "extension.simpleVim.scrollDownPage", 47 | "when": "editorTextFocus && !extension.simpleVim.insertMode" 48 | }, 49 | { 50 | "key": "ctrl+b", 51 | "command": "extension.simpleVim.scrollUpPage", 52 | "when": "editorTextFocus && !extension.simpleVim.insertMode" 53 | } 54 | ], 55 | "configuration": { 56 | "type": "object", 57 | "title": "SimpleVim Configuration", 58 | "properties": { 59 | "simpleVim.yankHighlightBackgroundColor": { 60 | "type": "string", 61 | "default": "#F8F3AB", 62 | "description": "Background color that flashes to show the range when yanking." 63 | } 64 | } 65 | } 66 | }, 67 | "scripts": { 68 | "vscode:prepublish": "npm run compile", 69 | "compile": "tsc -p ./", 70 | "watch": "tsc -watch -p ./", 71 | "postinstall": "node ./node_modules/vscode/bin/install", 72 | "lint": "tslint -c tslint.json 'src/**/*.ts' --exclude 'src/test/*.ts'" 73 | }, 74 | "devDependencies": { 75 | "@types/node": "^10.5.1", 76 | "tslint": "^5.10.0", 77 | "typescript": "^2.9.2", 78 | "vscode": "^1.1.18" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/paragraph_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { SimpleRange } from './simple_range_types'; 3 | 4 | export function paragraphForward(document: vscode.TextDocument, line: number): number { 5 | let visitedNonEmptyLine = false; 6 | 7 | for (let i = line; i < document.lineCount; ++i) { 8 | if (visitedNonEmptyLine) { 9 | if (document.lineAt(i).isEmptyOrWhitespace) { 10 | return i; 11 | } 12 | } else { 13 | if (!document.lineAt(i).isEmptyOrWhitespace) { 14 | visitedNonEmptyLine = true; 15 | } 16 | } 17 | } 18 | 19 | return document.lineCount - 1; 20 | } 21 | 22 | export function paragraphBackward(document: vscode.TextDocument, line: number): number { 23 | let visitedNonEmptyLine = false; 24 | 25 | for (let i = line; i >= 0; --i) { 26 | if (visitedNonEmptyLine) { 27 | if (document.lineAt(i).isEmptyOrWhitespace) { 28 | return i; 29 | } 30 | } else { 31 | if (!document.lineAt(i).isEmptyOrWhitespace) { 32 | visitedNonEmptyLine = true; 33 | } 34 | } 35 | } 36 | 37 | return 0; 38 | } 39 | 40 | export function paragraphRangeOuter(document: vscode.TextDocument, line: number): SimpleRange | undefined { 41 | if (document.lineAt(line).isEmptyOrWhitespace) return undefined; 42 | 43 | return { 44 | start: paragraphRangeBackward(document, line - 1), 45 | end: paragraphRangeForwardOuter(document, line + 1), 46 | }; 47 | } 48 | 49 | function paragraphRangeForwardOuter(document: vscode.TextDocument, line: number): number { 50 | let seenWhitespace = false; 51 | 52 | for (let i = line; i < document.lineCount; ++i) { 53 | if (document.lineAt(i).isEmptyOrWhitespace) { 54 | seenWhitespace = true; 55 | } else if (seenWhitespace) { 56 | return i - 1; 57 | } 58 | } 59 | 60 | return document.lineCount - 1; 61 | } 62 | 63 | function paragraphRangeBackward(document: vscode.TextDocument, line: number): number { 64 | for (let i = line; i >= 0; --i) { 65 | if (document.lineAt(i).isEmptyOrWhitespace) { 66 | return i + 1; 67 | } 68 | } 69 | 70 | return 0; 71 | } 72 | 73 | export function paragraphRangeInner(document: vscode.TextDocument, line: number): SimpleRange | undefined { 74 | if (document.lineAt(line).isEmptyOrWhitespace) return undefined; 75 | 76 | return { 77 | start: paragraphRangeBackward(document, line - 1), 78 | end: paragraphRangeForwardInner(document, line + 1), 79 | }; 80 | } 81 | 82 | function paragraphRangeForwardInner(document: vscode.TextDocument, line: number): number { 83 | for (let i = line; i < document.lineCount; ++i) { 84 | if (document.lineAt(i).isEmptyOrWhitespace) { 85 | return i - 1; 86 | } 87 | } 88 | 89 | return document.lineCount - 1; 90 | } 91 | -------------------------------------------------------------------------------- /src/search_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function searchForward( 4 | document: vscode.TextDocument, 5 | needle: string, 6 | fromPosition: vscode.Position, 7 | ): vscode.Position | undefined { 8 | for (let i = fromPosition.line; i < document.lineCount; ++i) { 9 | const lineText = document.lineAt(i).text; 10 | const fromIndex = i === fromPosition.line ? fromPosition.character : 0; 11 | const matchIndex = lineText.indexOf(needle, fromIndex); 12 | 13 | if (matchIndex >= 0) { 14 | return new vscode.Position(i, matchIndex); 15 | } 16 | } 17 | 18 | return undefined; 19 | } 20 | 21 | export function searchBackward( 22 | document: vscode.TextDocument, 23 | needle: string, 24 | fromPosition: vscode.Position, 25 | ): vscode.Position | undefined { 26 | for (let i = fromPosition.line; i >= 0; --i) { 27 | const lineText = document.lineAt(i).text; 28 | const fromIndex = i === fromPosition.line ? fromPosition.character : +Infinity; 29 | const matchIndex = lineText.lastIndexOf(needle, fromIndex); 30 | 31 | if (matchIndex >= 0) { 32 | return new vscode.Position(i, matchIndex); 33 | } 34 | } 35 | 36 | return undefined; 37 | } 38 | 39 | export function searchForwardBracket( 40 | document: vscode.TextDocument, 41 | openingChar: string, 42 | closingChar: string, 43 | fromPosition: vscode.Position, 44 | ): vscode.Position | undefined { 45 | let n = 0; 46 | 47 | for (let i = fromPosition.line; i < document.lineCount; ++i) { 48 | const lineText = document.lineAt(i).text; 49 | const fromIndex = i === fromPosition.line ? fromPosition.character : 0; 50 | 51 | for (let j = fromIndex; j < lineText.length; ++j) { 52 | if (lineText[j] === openingChar) { 53 | ++n; 54 | } else if (lineText[j] === closingChar) { 55 | if (n === 0) { 56 | return new vscode.Position(i, j); 57 | } else { 58 | --n; 59 | } 60 | } 61 | } 62 | } 63 | 64 | return undefined; 65 | } 66 | 67 | export function searchBackwardBracket( 68 | document: vscode.TextDocument, 69 | openingChar: string, 70 | closingChar: string, 71 | fromPosition: vscode.Position, 72 | ): vscode.Position | undefined { 73 | let n = 0; 74 | 75 | for (let i = fromPosition.line; i >= 0; --i) { 76 | const lineText = document.lineAt(i).text; 77 | const fromIndex = i === fromPosition.line ? fromPosition.character : lineText.length - 1; 78 | 79 | for (let j = fromIndex; j >= 0; --j) { 80 | if (lineText[j] === closingChar) { 81 | ++n; 82 | } else if (lineText[j] === openingChar) { 83 | if (n === 0) { 84 | return new vscode.Position(i, j); 85 | } else { 86 | --n; 87 | } 88 | } 89 | } 90 | } 91 | 92 | return undefined; 93 | } 94 | -------------------------------------------------------------------------------- /src/put_utils/put_before.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as positionUtils from '../position_utils'; 4 | import { VimState } from '../vim_state_types'; 5 | import { getRegisterContentsList, adjustInsertPositions, getInsertRangesFromBeginning } from './common'; 6 | 7 | export function putBefore(vimState: VimState, editor: vscode.TextEditor) { 8 | const registerContentsList = getRegisterContentsList(vimState, editor); 9 | if (registerContentsList === undefined) return; 10 | 11 | if (vimState.registers.linewise) { 12 | normalModeLinewise(vimState, editor, registerContentsList); 13 | } else { 14 | normalModeCharacterwise(vimState, editor, registerContentsList); 15 | } 16 | } 17 | 18 | function normalModeLinewise( 19 | vimState: VimState, 20 | editor: vscode.TextEditor, 21 | registerContentsList: (string | undefined)[], 22 | ) { 23 | const insertContentsList = registerContentsList.map(contents => { 24 | if (contents === undefined) return undefined; 25 | else return contents + '\n'; 26 | }); 27 | 28 | const insertPositions = editor.selections.map(selection => { 29 | return new vscode.Position(selection.active.line, 0); 30 | }); 31 | 32 | const adjustedInsertPositions = adjustInsertPositions(insertPositions, insertContentsList); 33 | 34 | editor.edit(editBuilder => { 35 | insertPositions.forEach((position, i) => { 36 | const contents = insertContentsList[i]; 37 | if (contents === undefined) return; 38 | 39 | editBuilder.insert(position, contents); 40 | }); 41 | }).then(() => { 42 | editor.selections = editor.selections.map((selection, i) => { 43 | const position = adjustedInsertPositions[i]; 44 | if (position === undefined) return selection; 45 | 46 | return new vscode.Selection(position, position); 47 | }); 48 | }); 49 | 50 | vimState.lastPutRanges = { 51 | ranges: getInsertRangesFromBeginning(adjustedInsertPositions, registerContentsList), 52 | linewise: true, 53 | }; 54 | } 55 | 56 | function normalModeCharacterwise( 57 | vimState: VimState, 58 | editor: vscode.TextEditor, 59 | registerContentsList: (string | undefined)[], 60 | ) { 61 | const insertPositions = editor.selections.map(selection => selection.active); 62 | const adjustedInsertPositions = adjustInsertPositions(insertPositions, registerContentsList); 63 | const insertRanges = getInsertRangesFromBeginning(adjustedInsertPositions, registerContentsList); 64 | 65 | editor.edit(editBuilder => { 66 | insertPositions.forEach((insertPosition, i) => { 67 | const registerContents = registerContentsList[i]; 68 | if (registerContents === undefined) return; 69 | 70 | editBuilder.insert(insertPosition, registerContents); 71 | }); 72 | }).then(() => { 73 | editor.selections = editor.selections.map((selection, i) => { 74 | const range = insertRanges[i]; 75 | if (range === undefined) return selection; 76 | 77 | const position = positionUtils.left(range.end); 78 | return new vscode.Selection(position, position); 79 | }); 80 | }); 81 | 82 | vimState.lastPutRanges = { 83 | ranges: insertRanges, 84 | linewise: false, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /src/word_utils.ts: -------------------------------------------------------------------------------- 1 | const NON_WORD_CHARACTERS = '/\\()"\':,.;<>~!@#$%^&*|+=[]{}`?-'; 2 | 3 | export function whitespaceWordRanges(text: string): { start: number, end: number }[] { 4 | enum State { 5 | Whitespace, 6 | Word, 7 | } 8 | 9 | let state = State.Whitespace; 10 | let startIndex = 0; 11 | const ranges = []; 12 | 13 | for (let i = 0; i < text.length; ++i) { 14 | const char = text[i]; 15 | 16 | if (state === State.Whitespace) { 17 | if (!isWhitespaceCharacter(char)) { 18 | startIndex = i; 19 | state = State.Word; 20 | } 21 | } else { 22 | if (isWhitespaceCharacter(char)) { 23 | ranges.push({ 24 | start: startIndex, 25 | end: i - 1, 26 | }); 27 | 28 | state = State.Whitespace; 29 | } 30 | } 31 | } 32 | 33 | if (state === State.Word) { 34 | ranges.push({ 35 | start: startIndex, 36 | end: text.length - 1, 37 | }); 38 | } 39 | 40 | return ranges; 41 | } 42 | 43 | export function wordRanges(text: string): { start: number, end: number }[] { 44 | enum State { 45 | Whitespace, 46 | Word, 47 | NonWord, 48 | } 49 | 50 | let state = State.Whitespace; 51 | let startIndex = 0; 52 | const ranges = []; 53 | 54 | for (let i = 0; i < text.length; ++i) { 55 | const char = text[i]; 56 | 57 | if (state === State.Whitespace) { 58 | if (!isWhitespaceCharacter(char)) { 59 | startIndex = i; 60 | state = isWordCharacter(char) ? State.Word : State.NonWord; 61 | } 62 | } else if (state === State.Word) { 63 | if (!isWordCharacter(char)) { 64 | ranges.push({ 65 | start: startIndex, 66 | end: i - 1, 67 | }); 68 | 69 | if (isWhitespaceCharacter(char)) { 70 | state = State.Whitespace; 71 | } else { 72 | state = State.NonWord; 73 | startIndex = i; 74 | } 75 | } 76 | } else { 77 | if (!isNonWordCharacter(char)) { 78 | ranges.push({ 79 | start: startIndex, 80 | end: i - 1, 81 | }); 82 | 83 | if (isWhitespaceCharacter(char)) { 84 | state = State.Whitespace; 85 | } else { 86 | state = State.Word; 87 | startIndex = i; 88 | } 89 | } 90 | } 91 | } 92 | 93 | if (state !== State.Whitespace) { 94 | ranges.push({ 95 | start: startIndex, 96 | end: text.length - 1, 97 | }); 98 | } 99 | 100 | return ranges; 101 | } 102 | 103 | function isNonWordCharacter(char: string): boolean { 104 | return NON_WORD_CHARACTERS.indexOf(char) >= 0; 105 | } 106 | 107 | function isWhitespaceCharacter(char: string): boolean { 108 | return char === ' ' || char === '\t'; 109 | } 110 | 111 | function isWordCharacter(char: string): boolean { 112 | return !isWhitespaceCharacter(char) && !isNonWordCharacter(char); 113 | } 114 | -------------------------------------------------------------------------------- /src/indent_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { SimpleRange } from './simple_range_types'; 4 | 5 | export function indentLevelRange(document: vscode.TextDocument, lineNumber: number): SimpleRange { 6 | const indentLevel = findIndentLevel(document, lineNumber); 7 | const rangeStart = indentLevelRangeBefore(document, lineNumber, indentLevel); 8 | const rangeEnd = indentLevelRangeAfter(document, lineNumber + 1, indentLevel); 9 | 10 | if (rangeStart && rangeEnd) { 11 | return { start: rangeStart.start, end: rangeEnd.end }; 12 | } else if (rangeStart) { 13 | return rangeStart; 14 | } else if (rangeEnd) { 15 | return rangeEnd; 16 | } else { 17 | // This will never happen but the typechecker can't know that 18 | return { start: lineNumber, end: lineNumber }; 19 | } 20 | } 21 | 22 | function indentLevelRangeBefore( 23 | document: vscode.TextDocument, 24 | lineNumber: number, 25 | indentLevel: number, 26 | ): SimpleRange | undefined { 27 | let result; 28 | 29 | for (let i = lineNumber; i >= 0; --i) { 30 | const line = document.lineAt(i); 31 | 32 | if (line.firstNonWhitespaceCharacterIndex >= indentLevel) { 33 | if (!line.isEmptyOrWhitespace) { 34 | if (result) { 35 | result.start = i; 36 | } else { 37 | result = { start: i, end: i }; 38 | } 39 | } 40 | } else { 41 | if (!line.isEmptyOrWhitespace) { 42 | return result; 43 | } 44 | } 45 | } 46 | 47 | return result; 48 | } 49 | 50 | function indentLevelRangeAfter( 51 | document: vscode.TextDocument, 52 | lineNumber: number, 53 | indentLevel: number, 54 | ): SimpleRange | undefined { 55 | let result; 56 | 57 | for (let i = lineNumber; i < document.lineCount; ++i) { 58 | const line = document.lineAt(i); 59 | 60 | if (line.firstNonWhitespaceCharacterIndex >= indentLevel) { 61 | if (!line.isEmptyOrWhitespace) { 62 | if (result) { 63 | result.end = i; 64 | } else { 65 | result = { start: i, end: i }; 66 | } 67 | } 68 | } else { 69 | if (!line.isEmptyOrWhitespace) { 70 | return result; 71 | } 72 | } 73 | } 74 | 75 | return result; 76 | } 77 | 78 | function findIndentLevel(document: vscode.TextDocument, lineNumber: number) { 79 | const line = document.lineAt(lineNumber); 80 | 81 | if (!line.isEmptyOrWhitespace) { 82 | return line.firstNonWhitespaceCharacterIndex; 83 | } 84 | 85 | return Math.max( 86 | findIndentLevelForward(document, lineNumber + 1), 87 | findIndentLevelBackward(document, lineNumber - 1), 88 | ); 89 | } 90 | 91 | function findIndentLevelForward(document: vscode.TextDocument, lineNumber: number): number { 92 | for (let i = lineNumber; i < document.lineCount; ++i) { 93 | const line = document.lineAt(i); 94 | 95 | if (!line.isEmptyOrWhitespace) { 96 | return line.firstNonWhitespaceCharacterIndex; 97 | } 98 | } 99 | 100 | return 0; 101 | } 102 | 103 | function findIndentLevelBackward(document: vscode.TextDocument, lineNumber: number): number { 104 | for (let i = lineNumber; i >= 0; --i) { 105 | const line = document.lineAt(i); 106 | 107 | if (!line.isEmptyOrWhitespace) { 108 | return line.firstNonWhitespaceCharacterIndex; 109 | } 110 | } 111 | 112 | return 0; 113 | } 114 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Mode } from './modes_types'; 4 | import * as scrollCommands from './scroll_commands'; 5 | import { enterNormalMode, enterVisualMode, setModeCursorStyle } from './modes'; 6 | import { typeHandler } from './type_handler'; 7 | import { addTypeSubscription, removeTypeSubscription } from './type_subscription'; 8 | import { VimState } from './vim_state_types'; 9 | import { escapeHandler } from './escape_handler'; 10 | 11 | const globalVimState: VimState = { 12 | typeSubscription: undefined, 13 | mode: Mode.Insert, 14 | keysPressed: [], 15 | registers: { 16 | contentsList: [], 17 | linewise: true, 18 | }, 19 | semicolonAction: () => undefined, 20 | commaAction: () => undefined, 21 | lastPutRanges: { 22 | ranges: [], 23 | linewise: true, 24 | }, 25 | }; 26 | 27 | function onSelectionChange(vimState: VimState, e: vscode.TextEditorSelectionChangeEvent): void { 28 | if (vimState.mode === Mode.Insert) return; 29 | 30 | if (e.selections.every(selection => selection.isEmpty)) { 31 | // It would be nice if we could always go from visual to normal mode when all selections are empty 32 | // but visual mode on an empty line will yield an empty selection and there's no good way of 33 | // distinguishing that case from the rest. So we only do it for mouse events. 34 | if ( 35 | (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) && 36 | e.kind === vscode.TextEditorSelectionChangeKind.Mouse 37 | ) { 38 | enterNormalMode(vimState); 39 | setModeCursorStyle(vimState.mode, e.textEditor); 40 | } 41 | } else { 42 | if (vimState.mode === Mode.Normal) { 43 | enterVisualMode(vimState); 44 | setModeCursorStyle(vimState.mode, e.textEditor); 45 | } 46 | } 47 | } 48 | 49 | function onDidChangeActiveTextEditor(vimState: VimState, editor: vscode.TextEditor | undefined) { 50 | if (!editor) return; 51 | 52 | if (editor.selections.every(selection => selection.isEmpty)) { 53 | if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) { 54 | enterNormalMode(vimState); 55 | } 56 | } else { 57 | if (vimState.mode === Mode.Normal) { 58 | enterVisualMode(vimState); 59 | } 60 | } 61 | 62 | setModeCursorStyle(vimState.mode, editor); 63 | 64 | vimState.keysPressed = []; 65 | } 66 | 67 | export function activate(context: vscode.ExtensionContext): void { 68 | context.subscriptions.push( 69 | vscode.window.onDidChangeActiveTextEditor((editor) => onDidChangeActiveTextEditor(globalVimState, editor)), 70 | vscode.window.onDidChangeTextEditorSelection((e) => onSelectionChange(globalVimState, e)), 71 | vscode.commands.registerCommand( 72 | 'extension.simpleVim.escapeKey', 73 | () => escapeHandler(globalVimState), 74 | ), 75 | vscode.commands.registerCommand( 76 | 'extension.simpleVim.scrollDownHalfPage', 77 | scrollCommands.scrollDownHalfPage, 78 | ), 79 | vscode.commands.registerCommand( 80 | 'extension.simpleVim.scrollUpHalfPage', 81 | scrollCommands.scrollUpHalfPage, 82 | ), 83 | vscode.commands.registerCommand( 84 | 'extension.simpleVim.scrollDownPage', 85 | scrollCommands.scrollDownPage, 86 | ), 87 | vscode.commands.registerCommand( 88 | 'extension.simpleVim.scrollUpPage', 89 | scrollCommands.scrollUpPage, 90 | ), 91 | ); 92 | 93 | enterNormalMode(globalVimState); 94 | addTypeSubscription(globalVimState, typeHandler); 95 | 96 | if (vscode.window.activeTextEditor) { 97 | onDidChangeActiveTextEditor(globalVimState, vscode.window.activeTextEditor); 98 | } 99 | } 100 | 101 | export function deactivate(): void { 102 | removeTypeSubscription(globalVimState); 103 | } 104 | -------------------------------------------------------------------------------- /src/tag_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { SimpleRange } from './simple_range_types'; 4 | 5 | type PartialTagOpening = { 6 | kind: 'opening'; 7 | name: string; 8 | range: SimpleRange; 9 | }; 10 | 11 | type PartialTagClosing = { 12 | kind: 'closing'; 13 | name: string; 14 | range: SimpleRange; 15 | }; 16 | 17 | type PartialTagSelfClosing = { 18 | kind: 'self_closing'; 19 | name: string; 20 | range: SimpleRange; 21 | }; 22 | 23 | type PartialTag = PartialTagOpening | PartialTagClosing | PartialTagSelfClosing; 24 | 25 | type OffsetTag = { 26 | name: string; 27 | opening: SimpleRange; 28 | closing?: SimpleRange; // Doesn't exist for self-closing tags 29 | }; 30 | 31 | type PositionTag = { 32 | name: string; 33 | opening: vscode.Range; 34 | closing?: vscode.Range; // Doesn't exist for self-closing tags 35 | }; 36 | 37 | const OPEN_SLASH_GROUP = 1; 38 | const TAG_NAME_GROUP = 2; 39 | const CLOSE_SLASH_GROUP = 3; 40 | 41 | export function getTags(document: vscode.TextDocument): PositionTag[] { 42 | return positionTags(document, matchTags(getPartialTags(document.getText()))); 43 | } 44 | 45 | function positionTags(document: vscode.TextDocument, offsetTags: OffsetTag[]): PositionTag[] { 46 | return offsetTags.map(tag => { 47 | const openingRange = new vscode.Range( 48 | document.positionAt(tag.opening.start), 49 | document.positionAt(tag.opening.end), 50 | ); 51 | 52 | if (tag.closing) { 53 | return { 54 | name: tag.name, 55 | opening: openingRange, 56 | closing: new vscode.Range( 57 | document.positionAt(tag.closing.start), 58 | document.positionAt(tag.closing.end), 59 | ), 60 | }; 61 | } else { 62 | return { 63 | name: tag.name, 64 | opening: openingRange, 65 | }; 66 | } 67 | }); 68 | } 69 | 70 | function matchTags(partialTags: PartialTag[]): OffsetTag[] { 71 | const tags: OffsetTag[] = []; 72 | const openingStack: PartialTagOpening[] = []; 73 | 74 | partialTags.forEach(partialTag => { 75 | if (partialTag.kind === 'opening') { 76 | openingStack.push(partialTag); 77 | } else if (partialTag.kind === 'self_closing') { 78 | tags.push({ 79 | name: partialTag.name, 80 | opening: partialTag.range, 81 | }); 82 | } else if (partialTag.kind === 'closing') { 83 | let stackTag = openingStack.pop(); 84 | 85 | while (stackTag) { 86 | if (stackTag.name === partialTag.name) { 87 | tags.push({ 88 | name: stackTag.name, 89 | opening: stackTag.range, 90 | closing: partialTag.range, 91 | }); 92 | 93 | break; 94 | } else { 95 | // Treat unclosed tags as self-closing because that's often the case in HTML 96 | tags.push({ 97 | name: stackTag.name, 98 | opening: stackTag.range, 99 | }); 100 | } 101 | 102 | stackTag = openingStack.pop(); 103 | } 104 | } 105 | }); 106 | 107 | return tags.sort((a, b) => a.opening.start - b.opening.start); 108 | } 109 | 110 | function getPartialTags(text: string): PartialTag[] { 111 | const regex = /\<(\/)?([^\>\<\s]+)[^\>\<]*?(\/?)\>/g; 112 | const tagRanges: PartialTag[] = []; 113 | let match = regex.exec(text); 114 | 115 | while (match) { 116 | const name = match[TAG_NAME_GROUP]; 117 | const range = { start: match.index, end: regex.lastIndex - 1 }; 118 | 119 | if (match[CLOSE_SLASH_GROUP]) { 120 | tagRanges.push({ kind: 'self_closing', name: name, range: range }); 121 | } else if (match[OPEN_SLASH_GROUP]) { 122 | tagRanges.push({ kind: 'closing', name: name, range: range }); 123 | } else { 124 | tagRanges.push({ kind: 'opening', name: name, range: range }); 125 | } 126 | 127 | match = regex.exec(text); 128 | } 129 | 130 | return tagRanges; 131 | } 132 | -------------------------------------------------------------------------------- /src/put_utils/common.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { VimState } from '../vim_state_types'; 4 | 5 | export function getRegisterContentsList(vimState: VimState, editor: vscode.TextEditor) { 6 | if (vimState.registers.contentsList.length === 0) return undefined; 7 | 8 | let registerContentsList = vimState.registers.contentsList; 9 | 10 | // Handle putting with a different number of cursors than when you yanked 11 | if (vimState.registers.contentsList.length !== editor.selections.length) { 12 | const combinedContents = vimState.registers.contentsList.join('\n'); 13 | registerContentsList = editor.selections.map(selection => combinedContents); 14 | } 15 | 16 | return registerContentsList; 17 | } 18 | 19 | // Given contents and positions at the end of the contents, return the position at the beginning of the contents 20 | export function getInsertRangesFromEnd( 21 | document: vscode.TextDocument, 22 | positions: vscode.Position[], 23 | contentsList: (string | undefined)[], 24 | ) { 25 | return positions.map((position, i) => { 26 | const contents = contentsList[i]; 27 | if (!contents) return undefined; 28 | 29 | const lines = contents.split(/\r?\n/); 30 | 31 | let beginningPosition; 32 | if (lines.length > 1) { 33 | const beginningLine = position.line - (lines.length - 1); 34 | const beginningCharacter = document.lineAt(beginningLine).text.length - lines[0].length; 35 | beginningPosition = new vscode.Position(beginningLine, beginningCharacter); 36 | } else { 37 | beginningPosition = position.with({ character: position.character - lines[0].length }); 38 | } 39 | 40 | return new vscode.Range(beginningPosition, position); 41 | }); 42 | } 43 | 44 | // Given positions and contents inserted at those positions, return the range that will 45 | // select that contents 46 | export function getInsertRangesFromBeginning(positions: vscode.Position[], contentsList: (string | undefined)[]) { 47 | return positions.map((position, i) => { 48 | const contents = contentsList[i]; 49 | if (!contents) return undefined; 50 | 51 | const lines = contents.split(/\r?\n/); 52 | const endLine = position.line + lines.length - 1; 53 | const endCharacter = (lines.length === 1 ? 54 | position.character + lines[0].length : 55 | lines[lines.length - 1].length 56 | ); 57 | 58 | return new vscode.Range(position, new vscode.Position(endLine, endCharacter)); 59 | }); 60 | } 61 | 62 | // Given positions and contents inserted at those positions, figure out how the positions will move 63 | // when the contents is inserted. For example inserting a line above a position will increase its 64 | // line number by one. 65 | export function adjustInsertPositions(positions: vscode.Position[], contentsList: (string | undefined)[]) { 66 | const indexPositions = positions.map((position, i) => ({ originalIndex: i, position: position })); 67 | 68 | indexPositions.sort((a, b) => { 69 | if (a.position.isBefore(b.position)) return -1; 70 | else if (a.position.isEqual(b.position)) return 0; 71 | else return 1; 72 | }); 73 | 74 | const adjustedIndexPositions = []; 75 | let lineOffset = 0; 76 | let characterOffset = 0; 77 | let lineNumber = 0; 78 | 79 | for (const indexPosition of indexPositions) { 80 | // Adjust position 81 | 82 | const adjustedLine = indexPosition.position.line + lineOffset; 83 | 84 | let adjustedCharacter = indexPosition.position.character; 85 | if (indexPosition.position.line === lineNumber) { 86 | adjustedCharacter += characterOffset; 87 | } 88 | 89 | adjustedIndexPositions.push({ 90 | originalIndex: indexPosition.originalIndex, 91 | position: new vscode.Position(adjustedLine, adjustedCharacter), 92 | }); 93 | 94 | // Increase offsets 95 | 96 | const contents = contentsList[indexPosition.originalIndex]; 97 | 98 | if (contents !== undefined) { 99 | const contentsLines = contents.split(/\r?\n/); 100 | 101 | lineOffset += contentsLines.length - 1; 102 | 103 | if (indexPosition.position.line === lineNumber) { 104 | if (contentsLines.length === 1) { 105 | characterOffset += contentsLines[0].length; 106 | } else { 107 | characterOffset += ( 108 | contentsLines[contentsLines.length - 1].length - indexPosition.position.character 109 | ); 110 | } 111 | } else { 112 | characterOffset = 0; 113 | lineNumber = indexPosition.position.line; 114 | } 115 | } 116 | } 117 | 118 | adjustedIndexPositions.sort((a, b) => (a.originalIndex - b.originalIndex)); 119 | return adjustedIndexPositions.map(indexPosition => indexPosition.position); 120 | } 121 | -------------------------------------------------------------------------------- /src/put_utils/put_after.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as positionUtils from '../position_utils'; 4 | import { VimState } from '../vim_state_types'; 5 | import { Mode } from '../modes_types'; 6 | import { enterNormalMode, setModeCursorStyle } from '../modes'; 7 | import { 8 | getRegisterContentsList, 9 | adjustInsertPositions, 10 | getInsertRangesFromBeginning, 11 | getInsertRangesFromEnd, 12 | } from './common'; 13 | 14 | export function putAfter(vimState: VimState, editor: vscode.TextEditor) { 15 | const registerContentsList = getRegisterContentsList(vimState, editor); 16 | if (registerContentsList === undefined) return; 17 | 18 | if (vimState.mode === Mode.Normal) { 19 | if (vimState.registers.linewise) { 20 | normalModeLinewise(vimState, editor, registerContentsList); 21 | } else { 22 | normalModeCharacterwise(vimState, editor, registerContentsList); 23 | } 24 | } else if (vimState.mode === Mode.Visual) { 25 | visualMode(vimState, editor, registerContentsList); 26 | } else { 27 | visualLineMode(vimState, editor, registerContentsList); 28 | } 29 | } 30 | 31 | function normalModeLinewise( 32 | vimState: VimState, 33 | editor: vscode.TextEditor, 34 | registerContentsList: (string | undefined)[], 35 | ) { 36 | const insertContentsList = registerContentsList.map(contents => { 37 | if (contents === undefined) return undefined; 38 | else return '\n' + contents; 39 | }); 40 | 41 | const insertPositions = editor.selections.map(selection => { 42 | const lineLength = editor.document.lineAt(selection.active.line).text.length; 43 | return new vscode.Position(selection.active.line, lineLength); 44 | }); 45 | 46 | const adjustedInsertPositions = adjustInsertPositions(insertPositions, insertContentsList); 47 | const rangeBeginnings = adjustedInsertPositions.map(position => new vscode.Position(position.line + 1, 0)); 48 | 49 | editor.edit(editBuilder => { 50 | insertPositions.forEach((position, i) => { 51 | const contents = insertContentsList[i]; 52 | if (contents === undefined) return; 53 | 54 | editBuilder.insert(position, contents); 55 | }); 56 | }).then(() => { 57 | editor.selections = rangeBeginnings.map(position => new vscode.Selection(position, position)); 58 | }); 59 | 60 | vimState.lastPutRanges = { 61 | ranges: getInsertRangesFromBeginning(rangeBeginnings, registerContentsList), 62 | linewise: true, 63 | }; 64 | } 65 | 66 | function normalModeCharacterwise( 67 | vimState: VimState, 68 | editor: vscode.TextEditor, 69 | registerContentsList: (string | undefined)[], 70 | ) { 71 | const insertPositions = editor.selections.map(selection => { 72 | return positionUtils.right(editor.document, selection.active); 73 | }); 74 | 75 | const adjustedInsertPositions = adjustInsertPositions(insertPositions, registerContentsList); 76 | const insertRanges = getInsertRangesFromBeginning(adjustedInsertPositions, registerContentsList); 77 | 78 | editor.edit(editBuilder => { 79 | insertPositions.forEach((insertPosition, i) => { 80 | const registerContents = registerContentsList[i]; 81 | if (registerContents === undefined) return; 82 | 83 | editBuilder.insert(insertPosition, registerContents); 84 | }); 85 | }).then(() => { 86 | editor.selections = editor.selections.map((selection, i) => { 87 | const range = insertRanges[i]; 88 | if (range === undefined) return selection; 89 | 90 | const position = positionUtils.left(range.end); 91 | return new vscode.Selection(position, position); 92 | }); 93 | }); 94 | 95 | vimState.lastPutRanges = { 96 | ranges: insertRanges, 97 | linewise: false, 98 | }; 99 | } 100 | 101 | function visualMode(vimState: VimState, editor: vscode.TextEditor, registerContentsList: (string | undefined)[]) { 102 | const insertContentsList = (vimState.registers.linewise ? 103 | registerContentsList.map(contents => { 104 | if (!contents) return undefined; 105 | else return '\n' + contents + '\n'; 106 | }) : 107 | registerContentsList 108 | ); 109 | 110 | editor.edit(editBuilder => { 111 | editor.selections.forEach((selection, i) => { 112 | const contents = insertContentsList[i]; 113 | if (contents === undefined) return; 114 | 115 | editBuilder.delete(selection); 116 | editBuilder.insert(selection.start, contents); 117 | }); 118 | }).then(() => { 119 | vimState.lastPutRanges = { 120 | ranges: getInsertRangesFromEnd( 121 | editor.document, 122 | editor.selections.map(selection => selection.active), 123 | insertContentsList, 124 | ), 125 | linewise: vimState.registers.linewise, 126 | }; 127 | 128 | editor.selections = editor.selections.map(selection => { 129 | const newPosition = positionUtils.left(selection.active); 130 | return new vscode.Selection(newPosition, newPosition); 131 | }); 132 | }); 133 | 134 | enterNormalMode(vimState); 135 | setModeCursorStyle(vimState.mode, editor); 136 | } 137 | 138 | function visualLineMode(vimState: VimState, editor: vscode.TextEditor, registerContentsList: (string | undefined)[]) { 139 | editor.edit(editBuilder => { 140 | editor.selections.forEach((selection, i) => { 141 | const registerContents = registerContentsList[i]; 142 | if (registerContents === undefined) return; 143 | 144 | editBuilder.replace(selection, registerContents); 145 | }); 146 | }).then(() => { 147 | vimState.lastPutRanges = { 148 | ranges: editor.selections.map(selection => new vscode.Range(selection.start, selection.end)), 149 | linewise: vimState.registers.linewise, 150 | }; 151 | 152 | editor.selections = editor.selections.map(selection => { 153 | return new vscode.Selection(selection.start, selection.start); 154 | }); 155 | 156 | enterNormalMode(vimState); 157 | setModeCursorStyle(vimState.mode, editor); 158 | }); 159 | } 160 | -------------------------------------------------------------------------------- /src/actions/operators.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Action } from '../action_types'; 4 | import { operatorRanges } from './operator_ranges'; 5 | import { parseKeysOperator } from '../parse_keys'; 6 | import { enterInsertMode, enterNormalMode, setModeCursorStyle, enterVisualLineMode, enterVisualMode } from '../modes'; 7 | import { removeTypeSubscription } from '../type_subscription'; 8 | import { VimState } from '../vim_state_types'; 9 | import { Mode } from '../modes_types'; 10 | import { flashYankHighlight } from '../yank_highlight'; 11 | 12 | export const operators: Action[] = [ 13 | parseKeysOperator(['d'], operatorRanges, (vimState, editor, ranges, linewise) => { 14 | if (ranges.every(x => x === undefined)) return; 15 | 16 | cursorsToRangesStart(editor, ranges); 17 | 18 | delete_(editor, ranges, linewise); 19 | 20 | if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) { 21 | enterNormalMode(vimState); 22 | setModeCursorStyle(vimState.mode, editor); 23 | } 24 | }), 25 | parseKeysOperator(['c'], operatorRanges, (vimState, editor, ranges, linewise) => { 26 | if (ranges.every(x => x === undefined)) return; 27 | 28 | cursorsToRangesStart(editor, ranges); 29 | 30 | editor.edit(editBuilder => { 31 | ranges.forEach(range => { 32 | if (!range) return; 33 | editBuilder.delete(range); 34 | }); 35 | 36 | }); 37 | 38 | enterInsertMode(vimState); 39 | setModeCursorStyle(vimState.mode, editor); 40 | removeTypeSubscription(vimState); 41 | }), 42 | parseKeysOperator(['y'], operatorRanges, (vimState, editor, ranges, linewise) => { 43 | if (ranges.every(x => x === undefined)) return; 44 | 45 | yank(vimState, editor, ranges, linewise); 46 | 47 | if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) { 48 | // Move cursor to start of yanked text 49 | editor.selections = editor.selections.map(selection => { 50 | return new vscode.Selection(selection.start, selection.start); 51 | }); 52 | 53 | enterNormalMode(vimState); 54 | setModeCursorStyle(vimState.mode, editor); 55 | } else { 56 | // Yank highlight 57 | const highlightRanges: vscode.Range[] = []; 58 | ranges.forEach(range => { 59 | if (range) { 60 | highlightRanges.push(new vscode.Range(range.start, range.end)); 61 | } 62 | }); 63 | flashYankHighlight(editor, highlightRanges); 64 | } 65 | }), 66 | parseKeysOperator(['r'], operatorRanges, (vimState, editor, ranges, linewise) => { 67 | if (ranges.every(x => x === undefined)) return; 68 | 69 | cursorsToRangesStart(editor, ranges); 70 | 71 | yank(vimState, editor, ranges, linewise); 72 | delete_(editor, ranges, linewise); 73 | 74 | if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) { 75 | enterNormalMode(vimState); 76 | setModeCursorStyle(vimState.mode, editor); 77 | } 78 | }), 79 | parseKeysOperator(['s'], operatorRanges, (vimState, editor, ranges, linewise) => { 80 | if ( 81 | ranges.every(x => x === undefined) || 82 | vimState.mode === Mode.Visual || 83 | vimState.mode === Mode.VisualLine 84 | ) { 85 | return; 86 | } 87 | 88 | editor.selections = ranges.map((range, i) => { 89 | if (range) { 90 | const start = range.start; 91 | const end = range.end; 92 | return new vscode.Selection(start, end); 93 | } else { 94 | return editor.selections[i]; 95 | } 96 | }); 97 | 98 | if (linewise) { 99 | enterVisualLineMode(vimState); 100 | } else { 101 | enterVisualMode(vimState); 102 | } 103 | 104 | setModeCursorStyle(vimState.mode, editor); 105 | }), 106 | ]; 107 | 108 | function cursorsToRangesStart(editor: vscode.TextEditor, ranges: (vscode.Range | undefined)[]) { 109 | editor.selections = editor.selections.map((selection, i) => { 110 | const range = ranges[i]; 111 | 112 | if (range) { 113 | const newPosition = range.start; 114 | return new vscode.Selection(newPosition, newPosition); 115 | } else { 116 | return selection; 117 | } 118 | }); 119 | } 120 | 121 | function delete_(editor: vscode.TextEditor, ranges: (vscode.Range | undefined)[], linewise: boolean) { 122 | editor.edit(editBuilder => { 123 | ranges.forEach(range => { 124 | if (!range) return; 125 | 126 | let deleteRange = range; 127 | 128 | if (linewise) { 129 | const start = range.start; 130 | const end = range.end; 131 | 132 | if (end.line === editor.document.lineCount - 1) { 133 | if (start.line === 0) { 134 | deleteRange = new vscode.Range(start.with({ character: 0 }), end); 135 | } else { 136 | deleteRange = new vscode.Range( 137 | new vscode.Position(start.line - 1, editor.document.lineAt(start.line - 1).text.length), 138 | end, 139 | ); 140 | } 141 | } else { 142 | deleteRange = new vscode.Range( 143 | range.start, 144 | new vscode.Position(end.line + 1, 0), 145 | ); 146 | } 147 | } 148 | 149 | editBuilder.delete(deleteRange); 150 | }); 151 | }).then(() => { 152 | // For linewise deletions, make sure cursor is at beginning of line 153 | editor.selections = editor.selections.map((selection, i) => { 154 | const range = ranges[i]; 155 | 156 | if (range && linewise) { 157 | const newPosition = selection.start.with({ character: 0 }); 158 | return new vscode.Selection(newPosition, newPosition); 159 | } else { 160 | return selection; 161 | } 162 | }); 163 | }); 164 | } 165 | 166 | function yank(vimState: VimState, editor: vscode.TextEditor, ranges: (vscode.Range | undefined)[], linewise: boolean) { 167 | vimState.registers = { 168 | contentsList: ranges.map((range, i) => { 169 | if (range) { 170 | return editor.document.getText(range); 171 | } else { 172 | return vimState.registers.contentsList[i]; 173 | } 174 | }), 175 | linewise: linewise, 176 | }; 177 | } 178 | -------------------------------------------------------------------------------- /src/parse_keys.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { VimState } from './vim_state_types'; 4 | import { Mode } from './modes_types'; 5 | import { 6 | ParseKeysStatus, 7 | OperatorRange, 8 | ParseFailure, 9 | ParseOperatorPartSuccess, 10 | ParseOperatorRangeSuccess, 11 | } from './parse_keys_types'; 12 | import { Action } from './action_types'; 13 | 14 | export function arrayStartsWith(prefix: T[], xs: T[]) { 15 | if (xs.length < prefix.length) { 16 | return false; 17 | } 18 | 19 | for (let i = 0; i < prefix.length; ++i) { 20 | if (prefix[i] !== xs[i]) { 21 | return false; 22 | } 23 | } 24 | 25 | return true; 26 | } 27 | 28 | export function arrayEquals(xs: T[], ys: T[]) { 29 | if (xs.length !== ys.length) { 30 | return false; 31 | } 32 | 33 | for (let i = 0; i < xs.length; ++i) { 34 | if (xs[i] !== ys[i]) { 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | } 41 | 42 | export function parseKeysExact( 43 | matchKeys: string[], 44 | modes: Mode[], 45 | action: (vimState: VimState, editor: vscode.TextEditor) => void, 46 | ): Action { 47 | return (vimState, keys, editor) => { 48 | if (modes && modes.indexOf(vimState.mode) < 0) { 49 | return ParseKeysStatus.NO; 50 | } 51 | 52 | if (arrayEquals(keys, matchKeys)) { 53 | action(vimState, editor); 54 | return ParseKeysStatus.YES; 55 | } else if (arrayStartsWith(keys, matchKeys)) { 56 | return ParseKeysStatus.MORE_INPUT; 57 | } else { 58 | return ParseKeysStatus.NO; 59 | } 60 | }; 61 | } 62 | 63 | export function parseKeysRegex( 64 | doesPattern: RegExp, 65 | couldPattern: RegExp, 66 | modes: Mode[], 67 | action: (vimState: VimState, editor: vscode.TextEditor, match: RegExpMatchArray) => void, 68 | ): Action { 69 | return (vimState, keys, editor) => { 70 | if (modes && modes.indexOf(vimState.mode) < 0) { 71 | return ParseKeysStatus.NO; 72 | } 73 | 74 | const keysStr = keys.join(''); 75 | const doesMatch = keysStr.match(doesPattern); 76 | 77 | if (doesMatch) { 78 | action(vimState, editor, doesMatch); 79 | return ParseKeysStatus.YES; 80 | } else if (keysStr.match(couldPattern)) { 81 | return ParseKeysStatus.MORE_INPUT; 82 | } else { 83 | return ParseKeysStatus.NO; 84 | } 85 | }; 86 | } 87 | 88 | function parseOperatorPart(keys: string[], operatorKeys: string[]): ParseFailure | ParseOperatorPartSuccess { 89 | if (arrayStartsWith(operatorKeys, keys)) { 90 | return { 91 | kind: 'success', 92 | rest: keys.slice(operatorKeys.length), 93 | }; 94 | } else if (arrayStartsWith(keys, operatorKeys)) { 95 | return { 96 | kind: 'failure', 97 | status: ParseKeysStatus.MORE_INPUT, 98 | }; 99 | } else { 100 | return { 101 | kind: 'failure', 102 | status: ParseKeysStatus.NO, 103 | }; 104 | } 105 | } 106 | 107 | function parseOperatorRangePart( 108 | vimState: VimState, 109 | keys: string[], 110 | editor: vscode.TextEditor, 111 | motions: OperatorRange[], 112 | ): ParseFailure | ParseOperatorRangeSuccess { 113 | let could = false; 114 | for (const motion of motions) { 115 | const result = motion(vimState, keys, editor); 116 | 117 | if (result.kind === 'success') { 118 | return result; 119 | } else if (result.status === ParseKeysStatus.MORE_INPUT) { 120 | could = true; 121 | } 122 | } 123 | 124 | if (could) { 125 | return { 126 | kind: 'failure', 127 | status: ParseKeysStatus.MORE_INPUT, 128 | }; 129 | } else { 130 | return { 131 | kind: 'failure', 132 | status: ParseKeysStatus.NO, 133 | }; 134 | } 135 | } 136 | 137 | export function parseKeysOperator( 138 | operatorKeys: string[], 139 | motions: OperatorRange[], 140 | operator: ( 141 | vimState: VimState, 142 | editor: vscode.TextEditor, 143 | ranges: (vscode.Range | undefined)[], 144 | linewise: boolean, 145 | ) => void, 146 | ): Action { 147 | return (vimState, keys, editor) => { 148 | const operatorResult = parseOperatorPart(keys, operatorKeys); 149 | if (operatorResult.kind === 'failure') { 150 | return operatorResult.status; 151 | } 152 | 153 | let ranges: (vscode.Range | undefined)[]; 154 | let linewise = true; 155 | if (vimState.mode === Mode.Normal) { 156 | if (operatorResult.rest.length === 0) { 157 | return ParseKeysStatus.MORE_INPUT; 158 | } 159 | 160 | const motionResult = parseOperatorRangePart(vimState, operatorResult.rest, editor, motions); 161 | if (motionResult.kind === 'failure') { 162 | return motionResult.status; 163 | } 164 | 165 | ranges = motionResult.ranges; 166 | linewise = motionResult.linewise; 167 | } else if (vimState.mode === Mode.VisualLine) { 168 | ranges = editor.selections; 169 | linewise = true; 170 | } else { 171 | ranges = editor.selections; 172 | linewise = false; 173 | } 174 | 175 | operator(vimState, editor, ranges, linewise); 176 | return ParseKeysStatus.YES; 177 | }; 178 | } 179 | 180 | export function createOperatorRangeExactKeys( 181 | matchKeys: string[], 182 | linewise: boolean, 183 | f: (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined, 184 | ): OperatorRange { 185 | return (vimState, keys, editor) => { 186 | if (arrayEquals(keys, matchKeys)) { 187 | const ranges = editor.selections.map(selection => { 188 | return f(vimState, editor.document, selection.active); 189 | }); 190 | return { 191 | kind: 'success', 192 | ranges: ranges, 193 | linewise: linewise, 194 | }; 195 | } else if (arrayStartsWith(keys, matchKeys)) { 196 | return { 197 | kind: 'failure', 198 | status: ParseKeysStatus.MORE_INPUT, 199 | }; 200 | } else { 201 | return { 202 | kind: 'failure', 203 | status: ParseKeysStatus.NO, 204 | }; 205 | } 206 | }; 207 | } 208 | 209 | export function createOperatorRangeRegex( 210 | doesPattern: RegExp, 211 | couldPattern: RegExp, 212 | linewise: boolean, 213 | f: ( 214 | vimState: VimState, 215 | document: vscode.TextDocument, 216 | position: vscode.Position, 217 | match: RegExpMatchArray, 218 | ) => vscode.Range | undefined, 219 | ): OperatorRange { 220 | return (vimState, keys, editor) => { 221 | const keysStr = keys.join(''); 222 | const doesMatch = keysStr.match(doesPattern); 223 | 224 | if (doesMatch) { 225 | const ranges = editor.selections.map(selection => { 226 | return f(vimState, editor.document, selection.active, doesMatch); 227 | }); 228 | return { 229 | kind: 'success', 230 | ranges: ranges, 231 | linewise: linewise, 232 | }; 233 | } else if (keysStr.match(couldPattern)) { 234 | return { 235 | kind: 'failure', 236 | status: ParseKeysStatus.MORE_INPUT, 237 | }; 238 | } else { 239 | return { 240 | kind: 'failure', 241 | status: ParseKeysStatus.NO, 242 | }; 243 | } 244 | }; 245 | } 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleVim 2 | 3 | An opinionated Vim-like extension for VSCode that prioritizes simplicity (of use and implementation) and integration with native VSCode features. 4 | 5 | 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. 6 | 7 | 8 | ## Operators 9 | 10 | 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. 11 | 12 | | Keys | Description | 13 | |-|-| 14 | | `d` | Delete range. | 15 | | `c` | Delete range and enter insert mode. | 16 | | `y` | Yank range. | 17 | | `r` | Yank and delete range. | 18 | | `s` | Select range and enter Visual mode. | 19 | 20 | 21 | ## OperatorRanges 22 | 23 | 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. 24 | 25 | | Keys | Description | 26 | |-|-| 27 | | `l` | Character under cursor. | 28 | | `h` | Character to the left of cursor. | 29 | | `k` | Current line and line above. | 30 | | `j` | Current line and line below. | 31 | | `w` | From cursor to beginning of next word. | 32 | | `W` | From cursor to beginning of next word (including punctuation). | 33 | | `b` | From cursor to beginning of previous word. | 34 | | `B` | From cursor to beginning of previous word (including punctuation). | 35 | | `e` | From cursor to end of next word. | 36 | | `E` | From cursor to end of next word (including punctuation). | 37 | | `iw` | Word under cursor. | 38 | | `iW` | Word (including punctuation) under cursor. | 39 | | `aw` | Word under cursor and whitespace after. | 40 | | `aW` | Word (including punctuation) under cursor and whitespace after. | 41 | | `f` | From cursor to next occurrence (case sensitive) of . | 42 | | `F` | From cursor to previous occurrence (case sensitive) of . | 43 | | `t` | From cursor to next occurrence (case sensitive) of . | 44 | | `T` | From cursor to previous occurrence (case sensitive) of . | 45 | | `gg` | From current line to first line of the document. | 46 | | `G` | From current line to last line of the document. | 47 | | `}` | From current line to beginning of next paragraph. | 48 | | `{` | From current line to beginning of previous paragraph. | 49 | | `ip` | Current paragraph. | 50 | | `ap` | Current paragraph and whitespace after. | 51 | | `i` | Inside the matching ``s. Where `` is a quote or opening bracket character (any of ``'"`({[<``). | 52 | | `a` | Outside the matching ``s. Where `` is a quote or opening bracket character (any of ``'"`({[<``). | 53 | | `it` | Inside XML tag. | 54 | | `at` | Outside XML tag. | 55 | | `ii` | Inside indentation level. | 56 | 57 | 58 | ## Motions 59 | 60 | 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. 61 | 62 | | Keys | Description | 63 | |-|-| 64 | | `l` | Character right. | 65 | | `h` | Character left. | 66 | | `k` | Line up. | 67 | | `j` | Line down. | 68 | | `w` | Word right. | 69 | | `W` | Word (including punctuation) right. | 70 | | `b` | Word left. | 71 | | `B` | Word (including punctuation) left. | 72 | | `e` | Word end right. | 73 | | `E` | Word end (including punctuation) right. | 74 | | `f` | Next occurrence (case sensitive) of . | 75 | | `F` | Previous occurrence (case sensitive) of . | 76 | | `t` | Next occurrence (case sensitive) of . | 77 | | `T` | Previous occurrence (case sensitive) of . | 78 | | `gg` | First line of the document. | 79 | | `G` | Last line of the document. | 80 | | `}` | Down a paragraph. | 81 | | `{` | Up a paragraph. | 82 | | `$` | End of line. | 83 | | `_` | Beginning of line. | 84 | | `H` | Top of screen. | 85 | | `M` | Middle of screen. | 86 | | `L` | Bottom of screen. | 87 | 88 | 89 | ## Actions 90 | 91 | Actions are miscellaneous commands that don't follow the well-defined patterns of Operators, OperatorRanges, or Motions. 92 | 93 | | Keys | Description | 94 | |-|-| 95 | | `i` | Enter Insert mode. | 96 | | `I` | Move to beginning of line and enter Insert mode. | 97 | | `a` | Move one character to the right and enter Insert mode. | 98 | | `A` | Move to end of line and enter Insert mode. | 99 | | `v` | Enter VisualCharacter mode. | 100 | | `V` | Enter VisualLine mode. | 101 | | `Escape` | Enter Normal mode. | 102 | | `o` | Insert line below and enter insert mode. | 103 | | `O` | Insert line above and enter insert mode. | 104 | | `p` | Put yanked text after cursor. | 105 | | `P` | Put yanked text before cursor. | 106 | | `gp` | Select the result of the last `p` or `P` actions and enter Visual mode. | 107 | | `u` | Undo. | 108 | | `Ctrl+r` | Redo. | 109 | | `dd` | Delete current line. | 110 | | `D` | Delete to the end of the line. | 111 | | `cc` | Delete current line and enter Insert mode. | 112 | | `C` | Delete to the end of the line and enter Insert mode. | 113 | | `yy` | Yank current line. | 114 | | `Y` | Yank to the end of the line. | 115 | | `rr` | Yank current line and delete it. | 116 | | `R` | Yank to the end of the line and delete it. | 117 | | `ss` | Select current line. | 118 | | `S` | Select to the end of the line. | 119 | | `x` | Delete character. | 120 | | `zt` | Scroll so that cursor is at the top of the screen. | 121 | | `zz` | Scroll so that cursor is in the middle of the screen. | 122 | | `zb` | Scroll so that cursor is at the bottom of the screen. | 123 | | `Ctrl+d` | Scroll down half page. | 124 | | `Ctrl+u` | Scroll up half page. | 125 | | `Ctrl+f` | Scroll down full page. | 126 | | `Ctrl+b` | Scroll up full page. | 127 | | `;` | Repeat the last `f`, `F`, `t` or `T` motion forward. | 128 | | `,` | Repeat the last `f`, `F`, `t` or `T` motion backward. | 129 | 130 | 131 | ## Differences From Vim 132 | 133 | 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. 134 | 135 | - 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. 136 | 137 | - SimpleVim has no `.` (repeat) command. Use multiple cursors instead (see previous bullet). 138 | 139 | - SimpleVim has no count. In Vim you can prefix commands with a number and it will run them that many times. In SimpleVim that is not supported. Instead you can just type the command again or use a command that accomplishes your goal with fewer repetitions. 140 | 141 | - 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. 142 | 143 | - 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. 144 | 145 | - 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. 146 | 147 | - 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. 148 | 149 | - SimpleVim has no `>` (indent) command. Instead you can use VSCode's `Cmd+]`. 150 | 151 | - SimpleVim has no `gU` (uppercase) command. Instead you can use VSCode's `Transform to Uppercase` from the Command Palette. 152 | 153 | - 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+_`. 154 | 155 | - 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+-`. 156 | 157 | 158 | ## Settings 159 | 160 | 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. 161 | 162 | ```json 163 | { 164 | "simpleVim.yankHighlightBackgroundColor": "#F8F3AB" 165 | } 166 | ``` 167 | -------------------------------------------------------------------------------- /src/actions/actions.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Mode } from '../modes_types'; 4 | import { Action } from '../action_types'; 5 | import { parseKeysExact } from '../parse_keys'; 6 | import { enterInsertMode, enterVisualMode, enterVisualLineMode, setModeCursorStyle } from '../modes'; 7 | import * as positionUtils from '../position_utils'; 8 | import { removeTypeSubscription } from '../type_subscription'; 9 | import { VimState } from '../vim_state_types'; 10 | import { setVisualLineSelections } from '../visual_line_utils'; 11 | import { flashYankHighlight } from '../yank_highlight'; 12 | import { putAfter } from '../put_utils/put_after'; 13 | import { putBefore } from '../put_utils/put_before'; 14 | 15 | export const actions: Action[] = [ 16 | parseKeysExact(['i'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 17 | enterInsertMode(vimState); 18 | setModeCursorStyle(vimState.mode, editor); 19 | removeTypeSubscription(vimState); 20 | }), 21 | 22 | parseKeysExact(['I'], [Mode.Normal], (vimState, editor) => { 23 | editor.selections = editor.selections.map(selection => { 24 | const character = editor.document.lineAt(selection.active.line).firstNonWhitespaceCharacterIndex; 25 | const newPosition = selection.active.with({ character: character }); 26 | return new vscode.Selection(newPosition, newPosition); 27 | }); 28 | 29 | enterInsertMode(vimState); 30 | setModeCursorStyle(vimState.mode, editor); 31 | removeTypeSubscription(vimState); 32 | }), 33 | 34 | parseKeysExact(['a'], [Mode.Normal], (vimState, editor) => { 35 | editor.selections = editor.selections.map(selection => { 36 | const newPosition = positionUtils.right(editor.document, selection.active); 37 | return new vscode.Selection(newPosition, newPosition); 38 | }); 39 | 40 | enterInsertMode(vimState); 41 | setModeCursorStyle(vimState.mode, editor); 42 | removeTypeSubscription(vimState); 43 | }), 44 | 45 | parseKeysExact(['A'], [Mode.Normal], (vimState, editor) => { 46 | editor.selections = editor.selections.map(selection => { 47 | const lineLength = editor.document.lineAt(selection.active.line).text.length; 48 | const newPosition = selection.active.with({ character: lineLength }); 49 | return new vscode.Selection(newPosition, newPosition); 50 | }); 51 | 52 | enterInsertMode(vimState); 53 | setModeCursorStyle(vimState.mode, editor); 54 | removeTypeSubscription(vimState); 55 | }), 56 | 57 | parseKeysExact(['v'], [Mode.Normal, Mode.VisualLine], (vimState, editor) => { 58 | if (vimState.mode === Mode.Normal) { 59 | editor.selections = editor.selections.map(selection => { 60 | const lineLength = editor.document.lineAt(selection.active.line).text.length; 61 | 62 | if (lineLength === 0) return selection; 63 | 64 | return new vscode.Selection(selection.active, positionUtils.right(editor.document, selection.active)); 65 | }); 66 | } 67 | 68 | enterVisualMode(vimState); 69 | setModeCursorStyle(vimState.mode, editor); 70 | }), 71 | 72 | parseKeysExact(['V'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 73 | enterVisualLineMode(vimState); 74 | setModeCursorStyle(vimState.mode, editor); 75 | setVisualLineSelections(editor); 76 | }), 77 | 78 | parseKeysExact(['o'], [Mode.Normal], (vimState, editor) => { 79 | vscode.commands.executeCommand('editor.action.insertLineAfter'); 80 | enterInsertMode(vimState); 81 | setModeCursorStyle(vimState.mode, editor); 82 | removeTypeSubscription(vimState); 83 | }), 84 | 85 | parseKeysExact(['O'], [Mode.Normal], (vimState, editor) => { 86 | vscode.commands.executeCommand('editor.action.insertLineBefore'); 87 | enterInsertMode(vimState); 88 | setModeCursorStyle(vimState.mode, editor); 89 | removeTypeSubscription(vimState); 90 | }), 91 | 92 | parseKeysExact(['p'], [Mode.Normal, Mode.Visual, Mode.VisualLine], putAfter), 93 | parseKeysExact(['P'], [Mode.Normal], putBefore), 94 | 95 | parseKeysExact(['g', 'p'], [Mode.Normal], (vimState, editor) => { 96 | editor.selections = editor.selections.map((selection, i) => { 97 | const putRange = vimState.lastPutRanges.ranges[i]; 98 | 99 | if (putRange) { 100 | return new vscode.Selection(putRange.start, putRange.end); 101 | } else { 102 | return selection; 103 | } 104 | }); 105 | 106 | if (vimState.lastPutRanges.linewise) { 107 | enterVisualLineMode(vimState); 108 | setModeCursorStyle(vimState.mode, editor); 109 | } else { 110 | enterVisualMode(vimState); 111 | setModeCursorStyle(vimState.mode, editor); 112 | } 113 | }), 114 | 115 | parseKeysExact(['u'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 116 | vscode.commands.executeCommand('undo'); 117 | }), 118 | 119 | parseKeysExact(['d', 'd'], [Mode.Normal], (vimState, editor) => { 120 | deleteLine(vimState, editor); 121 | }), 122 | 123 | parseKeysExact(['D'], [Mode.Normal], (vimState, editor) => { 124 | vscode.commands.executeCommand('deleteAllRight'); 125 | }), 126 | 127 | parseKeysExact(['c', 'c'], [Mode.Normal], (vimState, editor) => { 128 | editor.edit(editBuilder => { 129 | editor.selections.forEach(selection => { 130 | const line = editor.document.lineAt(selection.active.line); 131 | editBuilder.delete(new vscode.Range( 132 | selection.active.with({ character: line.firstNonWhitespaceCharacterIndex }), 133 | selection.active.with({ character: line.text.length }), 134 | )); 135 | }); 136 | }); 137 | 138 | enterInsertMode(vimState); 139 | setModeCursorStyle(vimState.mode, editor); 140 | removeTypeSubscription(vimState); 141 | }), 142 | 143 | parseKeysExact(['C'], [Mode.Normal], (vimState, editor) => { 144 | vscode.commands.executeCommand('deleteAllRight'); 145 | enterInsertMode(vimState); 146 | setModeCursorStyle(vimState.mode, editor); 147 | removeTypeSubscription(vimState); 148 | }), 149 | 150 | parseKeysExact(['y', 'y'], [Mode.Normal], (vimState, editor) => { 151 | yankLine(vimState, editor); 152 | 153 | // Yank highlight 154 | const highlightRanges = editor.selections.map(selection => { 155 | const lineLength = editor.document.lineAt(selection.active.line).text.length; 156 | return new vscode.Range( 157 | selection.active.with({ character: 0 }), 158 | selection.active.with({ character: lineLength }), 159 | ); 160 | }); 161 | flashYankHighlight(editor, highlightRanges); 162 | }), 163 | 164 | parseKeysExact(['Y'], [Mode.Normal], (vimState, editor) => { 165 | yankToEndOfLine(vimState, editor); 166 | 167 | // Yank highlight 168 | const highlightRanges = editor.selections.map(selection => { 169 | const lineLength = editor.document.lineAt(selection.active.line).text.length; 170 | return new vscode.Range( 171 | selection.active, 172 | selection.active.with({ character: lineLength }), 173 | ); 174 | }); 175 | flashYankHighlight(editor, highlightRanges); 176 | }), 177 | 178 | parseKeysExact(['r', 'r'], [Mode.Normal], (vimState, editor) => { 179 | yankLine(vimState, editor); 180 | deleteLine(vimState, editor); 181 | }), 182 | 183 | parseKeysExact(['R'], [Mode.Normal], (vimState, editor) => { 184 | yankToEndOfLine(vimState, editor); 185 | vscode.commands.executeCommand('deleteAllRight'); 186 | }), 187 | 188 | parseKeysExact(['s', 's'], [Mode.Normal], (vimState, editor) => { 189 | editor.selections = editor.selections.map(selection => { 190 | return new vscode.Selection( 191 | selection.active.with({ character: 0 }), 192 | positionUtils.lineEnd(editor.document, selection.active), 193 | ); 194 | }); 195 | 196 | enterVisualLineMode(vimState); 197 | setModeCursorStyle(vimState.mode, editor); 198 | }), 199 | 200 | parseKeysExact(['S'], [Mode.Normal], (vimState, editor) => { 201 | editor.selections = editor.selections.map(selection => { 202 | return new vscode.Selection( 203 | selection.active, 204 | positionUtils.lineEnd(editor.document, selection.active), 205 | ); 206 | }); 207 | 208 | enterVisualMode(vimState); 209 | setModeCursorStyle(vimState.mode, editor); 210 | }), 211 | 212 | parseKeysExact(['x'], [Mode.Normal], (vimState, editor) => { 213 | vscode.commands.executeCommand('deleteRight'); 214 | }), 215 | 216 | parseKeysExact(['z', 't'], [Mode.Normal], (vimState, editor) => { 217 | vscode.commands.executeCommand('revealLine', { 218 | lineNumber: editor.selection.active.line, 219 | at: 'top', 220 | }); 221 | }), 222 | 223 | parseKeysExact(['z', 'z'], [Mode.Normal], (vimState, editor) => { 224 | vscode.commands.executeCommand('revealLine', { 225 | lineNumber: editor.selection.active.line, 226 | at: 'center', 227 | }); 228 | }), 229 | 230 | parseKeysExact(['z', 'b'], [Mode.Normal], (vimState, editor) => { 231 | vscode.commands.executeCommand('revealLine', { 232 | lineNumber: editor.selection.active.line, 233 | at: 'bottom', 234 | }); 235 | }), 236 | 237 | parseKeysExact([';'], [Mode.Normal], (vimState, editor) => { 238 | vimState.semicolonAction(vimState, editor); 239 | }), 240 | 241 | parseKeysExact([','], [Mode.Normal], (vimState, editor) => { 242 | vimState.commaAction(vimState, editor); 243 | }), 244 | ]; 245 | 246 | function deleteLine(vimState: VimState, editor: vscode.TextEditor): void { 247 | vscode.commands.executeCommand('editor.action.deleteLines').then(() => { 248 | editor.selections = editor.selections.map(selection => { 249 | const character = editor.document.lineAt(selection.active.line).firstNonWhitespaceCharacterIndex; 250 | const newPosition = selection.active.with({ character: character }); 251 | return new vscode.Selection(newPosition, newPosition); 252 | }); 253 | }); 254 | } 255 | 256 | function yankLine(vimState: VimState, editor: vscode.TextEditor): void { 257 | vimState.registers = { 258 | contentsList: editor.selections.map(selection => { 259 | return editor.document.lineAt(selection.active.line).text; 260 | }), 261 | linewise: true, 262 | }; 263 | } 264 | 265 | function yankToEndOfLine(vimState: VimState, editor: vscode.TextEditor): void { 266 | vimState.registers = { 267 | contentsList: editor.selections.map(selection => { 268 | return editor.document.lineAt(selection.active.line).text.substring(selection.active.character); 269 | }), 270 | linewise: false, 271 | }; 272 | } 273 | -------------------------------------------------------------------------------- /src/actions/motions.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Mode } from '../modes_types'; 4 | import { Action } from '../action_types'; 5 | import { 6 | parseKeysExact, 7 | parseKeysRegex, 8 | } from '../parse_keys'; 9 | import { 10 | vscodeToVimVisualSelection, 11 | vimToVscodeVisualLineSelection, 12 | vimToVscodeVisualSelection, 13 | vscodeToVimVisualLineSelection, 14 | } from '../selection_utils'; 15 | import * as positionUtils from '../position_utils'; 16 | import { VimState } from '../vim_state_types'; 17 | import { wordRanges, whitespaceWordRanges } from '../word_utils'; 18 | import { searchForward, searchBackward } from '../search_utils'; 19 | import { paragraphForward, paragraphBackward } from '../paragraph_utils'; 20 | import { setVisualLineSelections } from '../visual_line_utils'; 21 | import { setVisualSelections } from '../visual_utils'; 22 | 23 | export const motions: Action[] = [ 24 | parseKeysExact(['l'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 25 | execMotion(vimState, editor, ({ document, position }) => { 26 | return positionUtils.rightNormal(document, position); 27 | }); 28 | }), 29 | 30 | parseKeysExact(['h'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 31 | execMotion(vimState, editor, ({ document, position }) => { 32 | return positionUtils.left(position); 33 | }); 34 | }), 35 | 36 | parseKeysExact(['k'], [Mode.Normal], (vimState, editor) => { 37 | vscode.commands.executeCommand('cursorMove', { to: 'up', by: 'wrappedLine' }); 38 | }), 39 | parseKeysExact(['k'], [Mode.Visual], (vimState, editor) => { 40 | const originalSelections = editor.selections; 41 | 42 | vscode.commands.executeCommand('cursorMove', { to: 'up', by: 'wrappedLine', select: true }).then(() => { 43 | setVisualSelections(editor, originalSelections); 44 | }); 45 | }), 46 | parseKeysExact(['k'], [Mode.VisualLine], (vimState, editor) => { 47 | vscode.commands.executeCommand('cursorMove', { to: 'up', by: 'line', select: true }).then(() => { 48 | setVisualLineSelections(editor); 49 | }); 50 | }), 51 | 52 | parseKeysExact(['j'], [Mode.Normal], (vimState, editor) => { 53 | vscode.commands.executeCommand('cursorMove', { to: 'down', by: 'wrappedLine' }); 54 | }), 55 | parseKeysExact(['j'], [Mode.Visual], (vimState, editor) => { 56 | const originalSelections = editor.selections; 57 | 58 | vscode.commands.executeCommand('cursorMove', { to: 'down', by: 'wrappedLine', select: true }).then(() => { 59 | setVisualSelections(editor, originalSelections); 60 | }); 61 | }), 62 | parseKeysExact(['j'], [Mode.VisualLine], (vimState, editor) => { 63 | vscode.commands.executeCommand('cursorMove', { to: 'down', by: 'line', select: true }).then(() => { 64 | setVisualLineSelections(editor); 65 | }); 66 | }), 67 | 68 | parseKeysExact(['w'], [Mode.Normal, Mode.Visual], createWordForwardHandler(wordRanges)), 69 | parseKeysExact(['W'], [Mode.Normal, Mode.Visual], createWordForwardHandler(whitespaceWordRanges)), 70 | 71 | parseKeysExact(['b'], [Mode.Normal, Mode.Visual], createWordBackwardHandler(wordRanges)), 72 | parseKeysExact(['B'], [Mode.Normal, Mode.Visual], createWordBackwardHandler(whitespaceWordRanges)), 73 | 74 | parseKeysExact(['e'], [Mode.Normal, Mode.Visual], createWordEndHandler(wordRanges)), 75 | parseKeysExact(['E'], [Mode.Normal, Mode.Visual], createWordEndHandler(whitespaceWordRanges)), 76 | 77 | parseKeysRegex(/^f(..)$/, /^(f|f.)$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => { 78 | findForward(vimState, editor, match); 79 | 80 | vimState.semicolonAction = (innerVimState, innerEditor) => { 81 | findForward(innerVimState, innerEditor, match); 82 | }; 83 | 84 | vimState.commaAction = (innerVimState, innerEditor) => { 85 | findBackward(innerVimState, innerEditor, match); 86 | }; 87 | }), 88 | 89 | parseKeysRegex(/^F(..)$/, /^(F|F.)$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => { 90 | findBackward(vimState, editor, match); 91 | 92 | vimState.semicolonAction = (innerVimState, innerEditor) => { 93 | findBackward(innerVimState, innerEditor, match); 94 | }; 95 | 96 | vimState.commaAction = (innerVimState, innerEditor) => { 97 | findForward(innerVimState, innerEditor, match); 98 | }; 99 | }), 100 | 101 | parseKeysRegex(/^t(.)$/, /^t$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => { 102 | tillForward(vimState, editor, match); 103 | 104 | vimState.semicolonAction = (innerVimState, innerEditor) => { 105 | tillForward(innerVimState, innerEditor, match); 106 | }; 107 | 108 | vimState.commaAction = (innerVimState, innerEditor) => { 109 | tillBackward(innerVimState, innerEditor, match); 110 | }; 111 | }), 112 | 113 | parseKeysRegex(/^T(.)$/, /^T$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => { 114 | tillBackward(vimState, editor, match); 115 | 116 | vimState.semicolonAction = (innerVimState, innerEditor) => { 117 | tillBackward(innerVimState, innerEditor, match); 118 | }; 119 | 120 | vimState.commaAction = (innerVimState, innerEditor) => { 121 | tillForward(innerVimState, innerEditor, match); 122 | }; 123 | }), 124 | 125 | parseKeysExact(['g', 'g'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 126 | execMotion(vimState, editor, ({ document, position }) => { 127 | return new vscode.Position(0, 0); 128 | }); 129 | }), 130 | 131 | parseKeysExact(['G'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 132 | execMotion(vimState, editor, ({ document, position }) => { 133 | return new vscode.Position(document.lineCount - 1, 0); 134 | }); 135 | }), 136 | 137 | parseKeysExact(['}'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 138 | execMotion(vimState, editor, ({ document, position }) => { 139 | return new vscode.Position(paragraphForward(document, position.line), 0); 140 | }); 141 | }), 142 | 143 | parseKeysExact(['{'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 144 | execMotion(vimState, editor, ({ document, position }) => { 145 | return new vscode.Position(paragraphBackward(document, position.line), 0); 146 | }); 147 | }), 148 | 149 | parseKeysExact(['$'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 150 | execMotion(vimState, editor, ({ document, position }) => { 151 | const lineLength = document.lineAt(position.line).text.length; 152 | return position.with({ character: Math.max(lineLength - 1, 0) }); 153 | }); 154 | }), 155 | 156 | parseKeysExact(['_'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 157 | execMotion(vimState, editor, ({ document, position }) => { 158 | const line = document.lineAt(position.line); 159 | return position.with({ character: line.firstNonWhitespaceCharacterIndex }); 160 | }); 161 | }), 162 | 163 | parseKeysExact(['H'], [Mode.Normal], (vimState, editor) => { 164 | vscode.commands.executeCommand('cursorMove', { to: 'viewPortTop', by: 'line' }); 165 | }), 166 | parseKeysExact(['H'], [Mode.Visual], (vimState, editor) => { 167 | const originalSelections = editor.selections; 168 | 169 | vscode.commands.executeCommand('cursorMove', { to: 'viewPortTop', by: 'line', select: true }).then(() => { 170 | setVisualSelections(editor, originalSelections); 171 | }); 172 | }), 173 | parseKeysExact(['H'], [Mode.VisualLine], (vimState, editor) => { 174 | vscode.commands.executeCommand('cursorMove', { to: 'viewPortTop', by: 'line', select: true }).then(() => { 175 | setVisualLineSelections(editor); 176 | }); 177 | }), 178 | 179 | parseKeysExact(['M'], [Mode.Normal], (vimState, editor) => { 180 | vscode.commands.executeCommand('cursorMove', { to: 'viewPortCenter', by: 'line' }); 181 | }), 182 | parseKeysExact(['M'], [Mode.Visual], (vimState, editor) => { 183 | const originalSelections = editor.selections; 184 | 185 | vscode.commands.executeCommand('cursorMove', { to: 'viewPortCenter', by: 'line', select: true }).then(() => { 186 | setVisualSelections(editor, originalSelections); 187 | }); 188 | }), 189 | parseKeysExact(['M'], [Mode.VisualLine], (vimState, editor) => { 190 | vscode.commands.executeCommand('cursorMove', { to: 'viewPortCenter', by: 'line', select: true }).then(() => { 191 | setVisualLineSelections(editor); 192 | }); 193 | }), 194 | 195 | parseKeysExact(['L'], [Mode.Normal], (vimState, editor) => { 196 | vscode.commands.executeCommand('cursorMove', { to: 'viewPortBottom', by: 'line' }); 197 | }), 198 | parseKeysExact(['L'], [Mode.Visual], (vimState, editor) => { 199 | const originalSelections = editor.selections; 200 | 201 | vscode.commands.executeCommand('cursorMove', { to: 'viewPortBottom', by: 'line', select: true }).then(() => { 202 | setVisualSelections(editor, originalSelections); 203 | }); 204 | }), 205 | parseKeysExact(['L'], [Mode.VisualLine], (vimState, editor) => { 206 | vscode.commands.executeCommand('cursorMove', { to: 'viewPortBottom', by: 'line', select: true }).then(() => { 207 | setVisualLineSelections(editor); 208 | }); 209 | }), 210 | ]; 211 | 212 | type MotionArgs = { 213 | document: vscode.TextDocument, 214 | position: vscode.Position, 215 | selectionIndex: number, 216 | vimState: VimState, 217 | }; 218 | 219 | type RegexMotionArgs = { 220 | document: vscode.TextDocument, 221 | position: vscode.Position, 222 | selectionIndex: number, 223 | vimState: VimState, 224 | match: RegExpMatchArray, 225 | }; 226 | 227 | function execRegexMotion( 228 | vimState: VimState, 229 | editor: vscode.TextEditor, 230 | match: RegExpMatchArray, 231 | regexMotion: (args: RegexMotionArgs) => vscode.Position, 232 | ) { 233 | return execMotion(vimState, editor, motionArgs => { 234 | return regexMotion({ 235 | ...motionArgs, 236 | match: match, 237 | }); 238 | }); 239 | } 240 | 241 | function execMotion(vimState: VimState, editor: vscode.TextEditor, motion: (args: MotionArgs) => vscode.Position) { 242 | const document = editor.document; 243 | 244 | const newSelections = editor.selections.map((selection, i) => { 245 | if (vimState.mode === Mode.Normal) { 246 | const newPosition = motion({ 247 | document: document, 248 | position: selection.active, 249 | selectionIndex: i, 250 | vimState: vimState, 251 | }); 252 | return new vscode.Selection(newPosition, newPosition); 253 | } else if (vimState.mode === Mode.Visual) { 254 | const vimSelection = vscodeToVimVisualSelection(document, selection); 255 | const motionPosition = motion({ 256 | document: document, 257 | position: vimSelection.active, 258 | selectionIndex: i, 259 | vimState: vimState, 260 | }); 261 | 262 | return vimToVscodeVisualSelection(document, new vscode.Selection(vimSelection.anchor, motionPosition)); 263 | } else if (vimState.mode === Mode.VisualLine) { 264 | const vimSelection = vscodeToVimVisualLineSelection(document, selection); 265 | const motionPosition = motion({ 266 | document: document, 267 | position: vimSelection.active, 268 | selectionIndex: i, 269 | vimState: vimState, 270 | }); 271 | 272 | return vimToVscodeVisualLineSelection(document, new vscode.Selection(vimSelection.anchor, motionPosition)); 273 | } else { 274 | return selection; 275 | } 276 | }); 277 | 278 | editor.selections = newSelections; 279 | 280 | editor.revealRange( 281 | new vscode.Range(newSelections[0].active, newSelections[0].active), 282 | vscode.TextEditorRevealType.InCenterIfOutsideViewport, 283 | ); 284 | } 285 | 286 | function findForward(vimState: VimState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void { 287 | execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => { 288 | const fromPosition = position.with({ character: position.character + 1 }); 289 | const result = searchForward(document, match[1], fromPosition); 290 | 291 | if (result) { 292 | return result; 293 | } else { 294 | return position; 295 | } 296 | }); 297 | } 298 | 299 | function findBackward(vimState: VimState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void { 300 | execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => { 301 | const fromPosition = positionLeftWrap(document, position); 302 | const result = searchBackward(document, match[1], fromPosition); 303 | 304 | if (result) { 305 | return result; 306 | } else { 307 | return position; 308 | } 309 | }); 310 | } 311 | 312 | function tillForward(vimState: VimState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void { 313 | execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => { 314 | const lineText = document.lineAt(position.line).text; 315 | const result = lineText.indexOf(match[1], position.character + 1); 316 | 317 | if (result >= 0) { 318 | return position.with({ character: result }); 319 | } else { 320 | return position; 321 | } 322 | }); 323 | } 324 | 325 | function tillBackward(vimState: VimState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void { 326 | execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => { 327 | const lineText = document.lineAt(position.line).text; 328 | const result = lineText.lastIndexOf(match[1], position.character - 1); 329 | 330 | if (result >= 0) { 331 | return position.with({ character: result }); 332 | } else { 333 | return position; 334 | } 335 | }); 336 | } 337 | 338 | function positionLeftWrap(document: vscode.TextDocument, position: vscode.Position): vscode.Position { 339 | if (position.character === 0) { 340 | if (position.line === 0) { 341 | return position; 342 | } else { 343 | const lineLength = document.lineAt(position.line - 1).text.length; 344 | return new vscode.Position(position.line - 1, lineLength); 345 | } 346 | } else { 347 | return position.with({ character: position.character - 1 }); 348 | } 349 | } 350 | 351 | function createWordForwardHandler( 352 | wordRangesFunction: (text: string) => { start: number; end: number }[], 353 | ): (vimState: VimState, editor: vscode.TextEditor) => void { 354 | return (vimState, editor) => { 355 | execMotion(vimState, editor, ({ document, position }) => { 356 | const lineText = document.lineAt(position.line).text; 357 | const ranges = wordRangesFunction(lineText); 358 | 359 | const result = ranges.find(x => x.start > position.character); 360 | 361 | if (result) { 362 | return position.with({ character: result.start }); 363 | } else { 364 | return position; 365 | } 366 | }); 367 | }; 368 | } 369 | 370 | function createWordBackwardHandler( 371 | wordRangesFunction: (text: string) => { start: number; end: number }[], 372 | ): (vimState: VimState, editor: vscode.TextEditor) => void { 373 | return (vimState, editor) => { 374 | execMotion(vimState, editor, ({ document, position }) => { 375 | const lineText = document.lineAt(position.line).text; 376 | const ranges = wordRangesFunction(lineText); 377 | 378 | const result = ranges.reverse().find(x => x.start < position.character); 379 | 380 | if (result) { 381 | return position.with({ character: result.start }); 382 | } else { 383 | return position; 384 | } 385 | }); 386 | }; 387 | } 388 | 389 | function createWordEndHandler( 390 | wordRangesFunction: (text: string) => { start: number; end: number }[], 391 | ): (vimState: VimState, editor: vscode.TextEditor) => void { 392 | return (vimState, editor) => { 393 | execMotion(vimState, editor, ({ document, position }) => { 394 | const lineText = document.lineAt(position.line).text; 395 | const ranges = wordRangesFunction(lineText); 396 | 397 | const result = ranges.find(x => x.end > position.character); 398 | 399 | if (result) { 400 | return position.with({ character: result.end }); 401 | } else { 402 | return position; 403 | } 404 | }); 405 | }; 406 | } 407 | -------------------------------------------------------------------------------- /src/actions/operator_ranges.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { createOperatorRangeExactKeys, createOperatorRangeRegex } from '../parse_keys'; 4 | import { OperatorRange } from '../parse_keys_types'; 5 | import { searchForward, searchBackward, searchBackwardBracket, searchForwardBracket } from '../search_utils'; 6 | import * as positionUtils from '../position_utils'; 7 | import { wordRanges, whitespaceWordRanges } from '../word_utils'; 8 | import { paragraphForward, paragraphBackward, paragraphRangeOuter, paragraphRangeInner } from '../paragraph_utils'; 9 | import { VimState } from '../vim_state_types'; 10 | import { quoteRanges, findQuoteRange } from '../quote_utils'; 11 | import { indentLevelRange } from '../indent_utils'; 12 | import { getTags } from '../tag_utils'; 13 | import { arrayFindLast } from '../array_utils'; 14 | 15 | export const operatorRanges: OperatorRange[] = [ 16 | createOperatorRangeExactKeys(['l'], false, (vimState, document, position) => { 17 | const right = positionUtils.right(document, position); 18 | 19 | if (right.isEqual(position)) { 20 | return undefined; 21 | } else { 22 | return new vscode.Range(position, right); 23 | } 24 | }), 25 | createOperatorRangeExactKeys(['h'], false, (vimState, document, position) => { 26 | const left = positionUtils.left(position); 27 | 28 | if (left.isEqual(position)) { 29 | return undefined; 30 | } else { 31 | return new vscode.Range(position, left); 32 | } 33 | }), 34 | createOperatorRangeExactKeys(['k'], true, (vimState, document, position) => { 35 | if (position.line === 0) { 36 | return new vscode.Range( 37 | new vscode.Position(0, 0), 38 | positionUtils.lineEnd(document, position), 39 | ); 40 | } else { 41 | return new vscode.Range( 42 | new vscode.Position(position.line - 1, 0), 43 | positionUtils.lineEnd(document, position), 44 | ); 45 | } 46 | }), 47 | 48 | createOperatorRangeExactKeys(['j'], true, (vimState, document, position) => { 49 | if (position.line === document.lineCount - 1) { 50 | return new vscode.Range( 51 | new vscode.Position(position.line, 0), 52 | positionUtils.lineEnd(document, position), 53 | ); 54 | } else { 55 | return new vscode.Range( 56 | new vscode.Position(position.line, 0), 57 | positionUtils.lineEnd(document, position.with({ line: position.line + 1 })), 58 | ); 59 | } 60 | }), 61 | 62 | createOperatorRangeExactKeys(['w'], false, createWordForwardHandler(wordRanges)), 63 | createOperatorRangeExactKeys(['W'], false, createWordForwardHandler(whitespaceWordRanges)), 64 | 65 | createOperatorRangeExactKeys(['b'], false, createWordBackwardHandler(wordRanges)), 66 | createOperatorRangeExactKeys(['B'], false, createWordBackwardHandler(whitespaceWordRanges)), 67 | 68 | createOperatorRangeExactKeys(['e'], false, createWordEndHandler(wordRanges)), 69 | createOperatorRangeExactKeys(['E'], false, createWordEndHandler(whitespaceWordRanges)), 70 | 71 | createOperatorRangeExactKeys(['i', 'w'], false, createInnerWordHandler(wordRanges)), 72 | createOperatorRangeExactKeys(['i', 'W'], false, createInnerWordHandler(whitespaceWordRanges)), 73 | 74 | createOperatorRangeExactKeys(['a', 'w'], false, createOuterWordHandler(wordRanges)), 75 | createOperatorRangeExactKeys(['a', 'W'], false, createOuterWordHandler(whitespaceWordRanges)), 76 | 77 | createOperatorRangeRegex(/^f(..)$/, /^(f|f.)$/, false, (vimState, document, position, match) => { 78 | const fromPosition = position.with({ character: position.character + 1 }); 79 | const result = searchForward(document, match[1], fromPosition); 80 | 81 | if (result) { 82 | return new vscode.Range(position, result); 83 | } else { 84 | return undefined; 85 | } 86 | }), 87 | 88 | createOperatorRangeRegex(/^F(..)$/, /^(F|F.)$/, false, (vimState, document, position, match) => { 89 | const fromPosition = position.with({ character: position.character - 1 }); 90 | const result = searchBackward(document, match[1], fromPosition); 91 | 92 | if (result) { 93 | return new vscode.Range(position, result); 94 | } else { 95 | return undefined; 96 | } 97 | }), 98 | 99 | createOperatorRangeRegex(/^t(.)$/, /^t$/, false, (vimState, document, position, match) => { 100 | const lineText = document.lineAt(position.line).text; 101 | const result = lineText.indexOf(match[1], position.character + 1); 102 | 103 | if (result >= 0) { 104 | return new vscode.Range(position, position.with({ character: result })); 105 | } else { 106 | return undefined; 107 | } 108 | }), 109 | 110 | createOperatorRangeRegex(/^T(.)$/, /^T$/, false, (vimState, document, position, match) => { 111 | const lineText = document.lineAt(position.line).text; 112 | const result = lineText.lastIndexOf(match[1], position.character - 1); 113 | 114 | if (result >= 0) { 115 | const newPosition = positionUtils.right(document, position.with({ character: result })); 116 | return new vscode.Range(newPosition, position); 117 | } else { 118 | return undefined; 119 | } 120 | }), 121 | 122 | createOperatorRangeExactKeys(['g', 'g'], true, (vimState, document, position) => { 123 | const lineLength = document.lineAt(position.line).text.length; 124 | 125 | return new vscode.Range( 126 | new vscode.Position(0, 0), 127 | position.with({ character: lineLength }), 128 | ); 129 | }), 130 | 131 | createOperatorRangeExactKeys(['G'], true, (vimState, document, position) => { 132 | const lineLength = document.lineAt(document.lineCount - 1).text.length; 133 | 134 | return new vscode.Range( 135 | position.with({ character: 0 }), 136 | new vscode.Position(document.lineCount - 1, lineLength), 137 | ); 138 | }), 139 | 140 | // TODO: return undefined? 141 | createOperatorRangeExactKeys(['}'], true, (vimState, document, position) => { 142 | return new vscode.Range( 143 | position.with({ character: 0 }), 144 | new vscode.Position(paragraphForward(document, position.line), 0), 145 | ); 146 | }), 147 | 148 | // TODO: return undefined? 149 | createOperatorRangeExactKeys(['{'], true, (vimState, document, position) => { 150 | return new vscode.Range( 151 | new vscode.Position(paragraphBackward(document, position.line), 0), 152 | position.with({ character: 0 }), 153 | ); 154 | }), 155 | 156 | createOperatorRangeExactKeys(['i', 'p'], true, (vimState, document, position) => { 157 | const result = paragraphRangeInner(document, position.line); 158 | 159 | if (result) { 160 | return new vscode.Range( 161 | new vscode.Position(result.start, 0), 162 | new vscode.Position(result.end, document.lineAt(result.end).text.length), 163 | ); 164 | } else { 165 | return undefined; 166 | } 167 | }), 168 | 169 | createOperatorRangeExactKeys(['a', 'p'], true, (vimState, document, position) => { 170 | const result = paragraphRangeOuter(document, position.line); 171 | 172 | if (result) { 173 | return new vscode.Range( 174 | new vscode.Position(result.start, 0), 175 | new vscode.Position(result.end, document.lineAt(result.end).text.length), 176 | ); 177 | } else { 178 | return undefined; 179 | } 180 | }), 181 | 182 | createOperatorRangeExactKeys(['i', "'"], false, createInnerQuoteHandler("'")), 183 | createOperatorRangeExactKeys(['a', "'"], false, createOuterQuoteHandler("'")), 184 | 185 | createOperatorRangeExactKeys(['i', '"'], false, createInnerQuoteHandler('"')), 186 | createOperatorRangeExactKeys(['a', '"'], false, createOuterQuoteHandler('"')), 187 | 188 | createOperatorRangeExactKeys(['i', '`'], false, createInnerQuoteHandler('`')), 189 | createOperatorRangeExactKeys(['a', '`'], false, createOuterQuoteHandler('`')), 190 | 191 | createOperatorRangeExactKeys(['i', '('], false, createInnerBracketHandler('(', ')')), 192 | createOperatorRangeExactKeys(['a', '('], false, createOuterBracketHandler('(', ')')), 193 | 194 | createOperatorRangeExactKeys(['i', '{'], false, createInnerBracketHandler('{', '}')), 195 | createOperatorRangeExactKeys(['a', '{'], false, createOuterBracketHandler('{', '}')), 196 | 197 | createOperatorRangeExactKeys(['i', '['], false, createInnerBracketHandler('[', ']')), 198 | createOperatorRangeExactKeys(['a', '['], false, createOuterBracketHandler('[', ']')), 199 | 200 | createOperatorRangeExactKeys(['i', '<'], false, createInnerBracketHandler('<', '>')), 201 | createOperatorRangeExactKeys(['a', '<'], false, createOuterBracketHandler('<', '>')), 202 | 203 | createOperatorRangeExactKeys(['i', 't'], false, (vimState, document, position) => { 204 | const tags = getTags(document); 205 | 206 | const closestTag = arrayFindLast(tags, tag => { 207 | if (tag.closing) { 208 | return ( 209 | position.isAfterOrEqual(tag.opening.start) && 210 | position.isBeforeOrEqual(tag.closing.end) 211 | ); 212 | } else { 213 | // Self-closing tags have no inside 214 | return false; 215 | } 216 | }); 217 | 218 | if (closestTag) { 219 | if (closestTag.closing) { 220 | return new vscode.Range( 221 | closestTag.opening.end.with({ character: closestTag.opening.end.character + 1 }), 222 | closestTag.closing.start, 223 | ); 224 | } else { 225 | throw new Error('We should have already filtered out self-closing tags above'); 226 | } 227 | } else { 228 | return undefined; 229 | } 230 | }), 231 | 232 | createOperatorRangeExactKeys(['a', 't'], false, (vimState, document, position) => { 233 | const tags = getTags(document); 234 | 235 | const closestTag = arrayFindLast(tags, tag => { 236 | const afterStart = position.isAfterOrEqual(tag.opening.start); 237 | 238 | if (tag.closing) { 239 | return afterStart && position.isBeforeOrEqual(tag.closing.end); 240 | } else { 241 | return afterStart && position.isBeforeOrEqual(tag.opening.end); 242 | } 243 | }); 244 | 245 | if (closestTag) { 246 | if (closestTag.closing) { 247 | return new vscode.Range( 248 | closestTag.opening.start, 249 | closestTag.closing.end.with({ character: closestTag.closing.end.character + 1 }), 250 | ); 251 | } else { 252 | return new vscode.Range( 253 | closestTag.opening.start, 254 | closestTag.opening.end.with({ character: closestTag.opening.end.character + 1 }), 255 | ); 256 | } 257 | } else { 258 | return undefined; 259 | } 260 | }), 261 | 262 | // TODO: return undefined? 263 | createOperatorRangeExactKeys(['i', 'i'], true, (vimState, document, position) => { 264 | const simpleRange = indentLevelRange(document, position.line); 265 | 266 | return new vscode.Range( 267 | new vscode.Position(simpleRange.start, 0), 268 | new vscode.Position(simpleRange.end, document.lineAt(simpleRange.end).text.length), 269 | ); 270 | }), 271 | ]; 272 | 273 | function createInnerBracketHandler( 274 | openingChar: string, 275 | closingChar: string, 276 | ): (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 277 | return (vimState, document, position) => { 278 | const bracketRange = getBracketRange(document, position, openingChar, closingChar); 279 | 280 | if (bracketRange) { 281 | return new vscode.Range( 282 | bracketRange.start.with({ character: bracketRange.start.character + 1 }), 283 | bracketRange.end, 284 | ); 285 | } else { 286 | return undefined; 287 | } 288 | }; 289 | } 290 | 291 | function createOuterBracketHandler( 292 | openingChar: string, 293 | closingChar: string, 294 | ): (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 295 | return (vimState, document, position) => { 296 | const bracketRange = getBracketRange(document, position, openingChar, closingChar); 297 | 298 | if (bracketRange) { 299 | return new vscode.Range( 300 | bracketRange.start, 301 | bracketRange.end.with({ character: bracketRange.end.character + 1 }), 302 | ); 303 | } else { 304 | return undefined; 305 | } 306 | }; 307 | } 308 | 309 | function getBracketRange( 310 | document: vscode.TextDocument, 311 | position: vscode.Position, 312 | openingChar: string, 313 | closingChar: string, 314 | ): vscode.Range | undefined { 315 | const lineText = document.lineAt(position.line).text; 316 | const currentChar = lineText[position.character]; 317 | 318 | let start; 319 | let end; 320 | if (currentChar === openingChar) { 321 | start = position; 322 | end = searchForwardBracket( 323 | document, 324 | openingChar, 325 | closingChar, 326 | positionUtils.rightWrap(document, position), 327 | ); 328 | } else if (currentChar === closingChar) { 329 | start = searchBackwardBracket( 330 | document, 331 | openingChar, 332 | closingChar, 333 | positionUtils.leftWrap(document, position), 334 | ); 335 | end = position; 336 | } else { 337 | start = searchBackwardBracket(document, openingChar, closingChar, position); 338 | end = searchForwardBracket(document, openingChar, closingChar, position); 339 | } 340 | 341 | if (start && end) { 342 | return new vscode.Range(start, end); 343 | } else { 344 | return undefined; 345 | } 346 | } 347 | 348 | function createInnerQuoteHandler( 349 | quoteChar: string, 350 | ): (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 351 | return (vimState, document, position) => { 352 | const lineText = document.lineAt(position.line).text; 353 | const ranges = quoteRanges(quoteChar, lineText); 354 | const result = findQuoteRange(ranges, position); 355 | 356 | if (result) { 357 | return new vscode.Range( 358 | position.with({ character: result.start + 1 }), 359 | position.with({ character: result.end }), 360 | ); 361 | } else { 362 | return undefined; 363 | } 364 | }; 365 | } 366 | 367 | function createOuterQuoteHandler( 368 | quoteChar: string, 369 | ): (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 370 | return (vimState, document, position) => { 371 | const lineText = document.lineAt(position.line).text; 372 | const ranges = quoteRanges(quoteChar, lineText); 373 | const result = findQuoteRange(ranges, position); 374 | 375 | if (result) { 376 | return new vscode.Range( 377 | position.with({ character: result.start }), 378 | position.with({ character: result.end + 1 }), 379 | ); 380 | } else { 381 | return undefined; 382 | } 383 | }; 384 | } 385 | 386 | function createWordForwardHandler( 387 | wordRangesFunction: (text: string) => { start: number; end: number }[], 388 | ): (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range { 389 | return (vimState, document, position) => { 390 | const lineText = document.lineAt(position.line).text; 391 | const ranges = wordRangesFunction(lineText); 392 | 393 | const result = ranges.find(x => x.start > position.character); 394 | 395 | if (result) { 396 | return new vscode.Range(position, position.with({ character: result.start })); 397 | } else { 398 | return new vscode.Range(position, position.with({ character: lineText.length })); 399 | } 400 | }; 401 | } 402 | 403 | function createWordBackwardHandler( 404 | wordRangesFunction: (text: string) => { start: number; end: number }[], 405 | ): (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 406 | return (vimState, document, position) => { 407 | const lineText = document.lineAt(position.line).text; 408 | const ranges = wordRangesFunction(lineText); 409 | 410 | const result = ranges.reverse().find(x => x.start < position.character); 411 | 412 | if (result) { 413 | return new vscode.Range(position.with({ character: result.start }), position); 414 | } else { 415 | return undefined; 416 | } 417 | }; 418 | } 419 | 420 | function createWordEndHandler( 421 | wordRangesFunction: (text: string) => { start: number; end: number }[], 422 | ): (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 423 | return (vimState, document, position) => { 424 | const lineText = document.lineAt(position.line).text; 425 | const ranges = wordRangesFunction(lineText); 426 | 427 | const result = ranges.find(x => x.end > position.character); 428 | 429 | if (result) { 430 | return new vscode.Range( 431 | position, 432 | positionUtils.right(document, position.with({ character: result.end })), 433 | ); 434 | } else { 435 | return undefined; 436 | } 437 | }; 438 | } 439 | 440 | function createInnerWordHandler( 441 | wordRangesFunction: (text: string) => { start: number; end: number }[], 442 | ): (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 443 | return (vimState, document, position) => { 444 | const lineText = document.lineAt(position.line).text; 445 | const ranges = wordRangesFunction(lineText); 446 | 447 | const result = ranges.find(x => x.start <= position.character && position.character <= x.end); 448 | 449 | if (result) { 450 | return new vscode.Range( 451 | position.with({ character: result.start }), 452 | positionUtils.right(document, position.with({ character: result.end })), 453 | ); 454 | } else { 455 | return undefined; 456 | } 457 | }; 458 | } 459 | 460 | function createOuterWordHandler( 461 | wordRangesFunction: (text: string) => { start: number; end: number }[], 462 | ): (vimState: VimState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 463 | return (vimState, document, position) => { 464 | const lineText = document.lineAt(position.line).text; 465 | const ranges = wordRangesFunction(lineText); 466 | 467 | for (let i = 0; i < ranges.length; ++i) { 468 | const range = ranges[i]; 469 | 470 | if (range.start <= position.character && position.character <= range.end) { 471 | if (i < ranges.length - 1) { 472 | return new vscode.Range( 473 | position.with({ character: range.start }), 474 | position.with({ character: ranges[i + 1].start }), 475 | ); 476 | } else if (i > 0) { 477 | return new vscode.Range( 478 | positionUtils.right(document, position.with({ character: ranges[i - 1].end })), 479 | positionUtils.right(document, position.with({ character: range.end })), 480 | ); 481 | } else { 482 | return new vscode.Range( 483 | position.with({ character: range.start }), 484 | positionUtils.right(document, position.with({ character: range.end })), 485 | ); 486 | } 487 | } 488 | } 489 | 490 | return undefined; 491 | }; 492 | } 493 | --------------------------------------------------------------------------------