├── .tool-versions ├── index.d.ts ├── .npmrc ├── res └── icon.png ├── src ├── simple_range_types.ts ├── modes_types.ts ├── actions │ ├── index.ts │ ├── keymaps.ts │ ├── unimpared.ts │ ├── spaceMode.ts │ ├── viewMode.ts │ ├── windowMode.ts │ ├── gotoMode.ts │ ├── matchMode.ts │ ├── operators.ts │ ├── motions.ts │ ├── actions.ts │ └── operator_ranges.ts ├── array_utils.ts ├── action_types.ts ├── yank_highlight.ts ├── file_utils.ts ├── type_subscription.ts ├── scroll_commands.ts ├── visual_line_utils.ts ├── parse_keys_types.ts ├── helix_state_types.ts ├── quote_utils.ts ├── type_handler.ts ├── visual_utils.ts ├── commandLine.ts ├── eventHandlers.ts ├── position_utils.ts ├── selection_utils.ts ├── paragraph_utils.ts ├── word_utils.ts ├── escape_handler.ts ├── statusBar.ts ├── put_utils │ ├── put_before.ts │ ├── common.ts │ └── put_after.ts ├── search_utils.ts ├── tag_utils.ts ├── block_utils.ts ├── SymbolProvider.ts ├── modes.ts ├── indent_utils.ts ├── index.ts ├── parse_keys.ts └── search.ts ├── .gitignore ├── docs └── img │ ├── helixLogo.png │ ├── Visual_Studio_Code_1.35_icon.png │ ├── helixLogo.svg │ └── Visual_Studio_Code_1.35_icon.svg ├── .vscodeignore ├── .prettierrc ├── tsup.config.ts ├── tsconfig.json ├── .vscode ├── tasks.json ├── settings.json └── launch.json ├── .eslintrc.cjs ├── LICENSE.txt ├── README.md └── package.json /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 16.14.2 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'string.prototype.matchall' 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | node-linker=hoisted -------------------------------------------------------------------------------- /res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwilliams/vscode-helix/HEAD/res/icon.png -------------------------------------------------------------------------------- /src/simple_range_types.ts: -------------------------------------------------------------------------------- 1 | export type SimpleRange = { 2 | start: number 3 | end: number 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | node_modules 4 | .vscode-test/ 5 | .vsix 6 | *.log 7 | *.vsix 8 | .env -------------------------------------------------------------------------------- /docs/img/helixLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwilliams/vscode-helix/HEAD/docs/img/helixLogo.png -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | dist/**/*.map 4 | src/** 5 | .gitignore 6 | tsconfig.json 7 | tslint.json 8 | -------------------------------------------------------------------------------- /docs/img/Visual_Studio_Code_1.35_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwilliams/vscode-helix/HEAD/docs/img/Visual_Studio_Code_1.35_icon.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "arrowParens": "always", 6 | "pluginSearchDirs": ["."] 7 | } 8 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs'], 6 | shims: false, 7 | dts: false, 8 | external: ['vscode'], 9 | }) 10 | -------------------------------------------------------------------------------- /src/modes_types.ts: -------------------------------------------------------------------------------- 1 | export enum Mode { 2 | Disabled, 3 | Insert, 4 | Normal, 5 | Visual, 6 | VisualLine, 7 | Occurrence, 8 | Window, 9 | SearchInProgress, 10 | CommandlineInProgress, 11 | Select, 12 | View, 13 | } 14 | -------------------------------------------------------------------------------- /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 { HelixState } from './helix_state_types'; 4 | import { ParseKeysStatus } from './parse_keys_types'; 5 | 6 | export type Action = (vimState: HelixState, keys: string[], editor: vscode.TextEditor) => ParseKeysStatus; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "lib": ["ES2022"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "skipDefaultLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/actions/keymaps.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | Motions: { 3 | MoveLeft: 'h', 4 | MoveRight: 'l', 5 | MoveDown: 'j', 6 | MoveUp: 'k', 7 | MoveLineEnd: 'o', 8 | MoveLineStart: 'u', 9 | }, 10 | Actions: { 11 | InsertMode: 'i', 12 | InsertAtLineStart: 'I', 13 | InsertAtLineEnd: 'A', 14 | NewLineAbove: 'O', 15 | NewLineBelow: 'o', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "isBackground": true, 10 | "presentation": { 11 | "reveal": "never" 12 | }, 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /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 6 | .getConfiguration('helixKeymap') 7 | .get('yankHighlightBackgroundColor'), 8 | }) 9 | 10 | editor.setDecorations(decoration, ranges) 11 | setTimeout(() => decoration.dispose(), 200) 12 | } 13 | -------------------------------------------------------------------------------- /src/file_utils.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, window } from 'vscode' 2 | 3 | let currentEditor: TextEditor | undefined 4 | let previousEditor: TextEditor | undefined 5 | 6 | export const registerActiveTextEditorChangeListener = () => { 7 | window.onDidChangeActiveTextEditor((textEditor) => { 8 | previousEditor = currentEditor 9 | currentEditor = textEditor 10 | }) 11 | } 12 | 13 | export const getPreviousEditor = () => { 14 | return previousEditor 15 | } 16 | 17 | export const getCurrentEditor = () => { 18 | return currentEditor 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // { 2 | // "files.exclude": { 3 | // "out": true, 4 | // }, 5 | // "typescript.tsdk": "node_modules/typescript/lib", 6 | // } 7 | { 8 | "files.exclude": { 9 | "out": true // set this to true to hide the "out" folder with the compiled JS files 10 | }, 11 | "search.exclude": { 12 | "out": true // set this to false to include "out" folder in search results 13 | }, 14 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 15 | "typescript.tsc.autoDetect": "off" 16 | } 17 | -------------------------------------------------------------------------------- /src/type_subscription.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { HelixState } from './helix_state_types'; 4 | 5 | export function addTypeSubscription( 6 | vimState: HelixState, 7 | typeHandler: (vimState: HelixState, 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: HelixState): 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 | revealCursor: true, 8 | value: 1, 9 | }); 10 | } 11 | 12 | export function scrollDownHalfPage(): void { 13 | editorScroll('down', 'halfPage'); 14 | } 15 | 16 | export function scrollUpHalfPage(): void { 17 | editorScroll('up', 'halfPage'); 18 | } 19 | 20 | export function scrollDownPage(): void { 21 | editorScroll('down', 'page'); 22 | } 23 | 24 | export function scrollUpPage(): void { 25 | editorScroll('up', 'page'); 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | parserOptions: { 8 | sourceType: 'module', 9 | ecmaVersion: 2020, 10 | }, 11 | env: { 12 | browser: true, 13 | es2017: true, 14 | node: true, 15 | }, 16 | rules: { 17 | 'no-unused-vars': 'off', 18 | '@typescript-eslint/no-unused-vars': [ 19 | 'warn', // or "error" 20 | { 21 | argsIgnorePattern: '^_', 22 | varsIgnorePattern: '^_', 23 | caughtErrorsIgnorePattern: '^_', 24 | }, 25 | ], 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /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/parse_keys_types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { HelixState } from './helix_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: HelixState, 34 | keys: string[], 35 | editor: vscode.TextEditor, 36 | ) => ParseFailure | ParseOperatorRangeSuccess; 37 | -------------------------------------------------------------------------------- /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/helix_state_types.ts: -------------------------------------------------------------------------------- 1 | import type { Disposable, TextDocument } from 'vscode'; 2 | import { Range, TextEditor } from 'vscode'; 3 | import type { SymbolProvider } from './SymbolProvider'; 4 | import { CommandLine } from './commandLine'; 5 | import type { Mode } from './modes_types'; 6 | import { SearchState } from './search'; 7 | 8 | /** This represents the global Helix state used across the board */ 9 | export type HelixState = { 10 | typeSubscription: Disposable | undefined; 11 | mode: Mode; 12 | keysPressed: string[]; 13 | numbersPressed: string[]; 14 | resolveCount: () => number; 15 | registers: { 16 | contentsList: (string | undefined)[]; 17 | linewise: boolean; 18 | }; 19 | symbolProvider: SymbolProvider; 20 | editorState: { 21 | activeEditor: TextEditor | undefined; 22 | previousEditor: TextEditor | undefined; 23 | lastModifiedDocument: TextDocument | undefined; 24 | }; 25 | commandLine: CommandLine; 26 | searchState: SearchState; 27 | /** 28 | * The current range we're searching in when calling select 29 | * This is better kept on the global state as it's used for multiple things 30 | */ 31 | currentSelection: Range | null; 32 | repeatLastMotion: (vimState: HelixState, editor: TextEditor) => void; 33 | lastPutRanges: { 34 | ranges: (Range | undefined)[]; 35 | linewise: boolean; 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/quote_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { SimpleRange } from './simple_range_types' 3 | 4 | export function findQuoteRange( 5 | ranges: SimpleRange[], 6 | position: vscode.Position, 7 | ): SimpleRange | undefined { 8 | const insideResult = ranges.find( 9 | (x) => x.start <= position.character && x.end >= position.character, 10 | ) 11 | 12 | if (insideResult) { 13 | return insideResult 14 | } 15 | 16 | const outsideResult = ranges.find((x) => x.start > position.character) 17 | 18 | if (outsideResult) { 19 | return outsideResult 20 | } 21 | 22 | return undefined 23 | } 24 | 25 | export function quoteRanges(quoteChar: string, s: string): SimpleRange[] { 26 | let stateInQuote = false 27 | let stateStartIndex = 0 28 | let backslashCount = 0 29 | const ranges = [] 30 | 31 | for (let i = 0; i < s.length; ++i) { 32 | if (s[i] === quoteChar && backslashCount % 2 === 0) { 33 | if (stateInQuote) { 34 | ranges.push({ 35 | start: stateStartIndex, 36 | end: i, 37 | }) 38 | 39 | stateInQuote = false 40 | } else { 41 | stateInQuote = true 42 | stateStartIndex = i 43 | } 44 | } 45 | 46 | if (s[i] === '\\') { 47 | ++backslashCount 48 | } else { 49 | backslashCount = 0 50 | } 51 | } 52 | 53 | return ranges 54 | } 55 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 14 | "outFiles": ["${workspaceFolder}/dist/**/*.js"] 15 | }, 16 | { 17 | "name": "Run Extension (Web)", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentKind=web", "--extensionDevelopmentPath=${workspaceFolder}"], 22 | "outFiles": ["${workspaceFolder}/dist/**/*.js"] 23 | }, 24 | { 25 | "name": "Extension Tests", 26 | "type": "extensionHost", 27 | "request": "launch", 28 | "runtimeExecutable": "${execPath}", 29 | "args": [ 30 | "--extensionDevelopmentPath=${workspaceFolder}", 31 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 32 | ], 33 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"] 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/type_handler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { actions } from './actions'; 4 | import { HelixState } from './helix_state_types'; 5 | import { Mode } from './modes_types'; 6 | import { ParseKeysStatus } from './parse_keys_types'; 7 | 8 | export function typeHandler(helixState: HelixState, char: string): void { 9 | if (helixState.mode === Mode.SearchInProgress || helixState.mode === Mode.Select) { 10 | helixState.searchState.addChar(helixState, char); 11 | return; 12 | } 13 | if (helixState.mode == Mode.CommandlineInProgress){ 14 | helixState.commandLine.addChar(helixState, char); 15 | return; 16 | } 17 | const editor = vscode.window.activeTextEditor; 18 | if (!editor) return; 19 | 20 | // Handle number prefixes 21 | if (/[0-9]/.test(char) && helixState.keysPressed.length === 0) { 22 | helixState.numbersPressed.push(char); 23 | return; 24 | } 25 | 26 | helixState.keysPressed.push(char); 27 | 28 | try { 29 | let could = false; 30 | for (const action of actions) { 31 | const result = action(helixState, helixState.keysPressed, editor); 32 | 33 | if (result === ParseKeysStatus.YES) { 34 | helixState.keysPressed = []; 35 | break; 36 | } else if (result === ParseKeysStatus.MORE_INPUT) { 37 | could = true; 38 | } 39 | } 40 | 41 | if (!could) { 42 | helixState.keysPressed = []; 43 | helixState.numbersPressed = []; 44 | } 45 | } catch (error) { 46 | console.error(error); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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: readonly 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/commandLine.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { HelixState } from './helix_state_types'; 3 | import { StatusBar } from './statusBar'; 4 | import { enterNormalMode } from './modes'; 5 | // Create a class which has access to the VSCode status bar 6 | 7 | export class CommandLine { 8 | 9 | // Buffer to save text written in the command-line 10 | commandLineText = ''; 11 | 12 | // concatenate each keystroke to a buffer 13 | addChar(helixState: HelixState, char: string): void{ 14 | if (char==='\n'){ 15 | // when the `enter` key is pressed 16 | this.enter(helixState) 17 | return; 18 | } 19 | this.commandLineText += char; 20 | // display what the user has written in command mode 21 | this.setText(this.commandLineText, helixState); 22 | } 23 | public clearCommandString(helixState: HelixState): void { 24 | this.commandLineText = ''; 25 | this.setText(this.commandLineText, helixState); 26 | } 27 | public setText(text: string, helixState: HelixState): void { 28 | StatusBar.setText(helixState, text); 29 | } 30 | enter(helixState: HelixState): void{ 31 | if (this.commandLineText === "w") { 32 | vscode.window.activeTextEditor?.document.save(); 33 | } else if (this.commandLineText === "wa"){ 34 | vscode.workspace.saveAll(true); 35 | } 36 | this.commandLineText = ''; 37 | this.setText(this.commandLineText, helixState); 38 | enterNormalMode(helixState); 39 | } 40 | 41 | backspace(helixState: HelixState): void { 42 | this.commandLineText = this.commandLineText.slice(0, -1) 43 | this.setText(this.commandLineText, helixState); 44 | } 45 | 46 | } 47 | 48 | export const commandLine = new CommandLine(); 49 | -------------------------------------------------------------------------------- /src/actions/unimpared.ts: -------------------------------------------------------------------------------- 1 | import { Selection, TextEditorRevealType, commands } from 'vscode'; 2 | import { Action } from '../action_types'; 3 | import { Mode } from '../modes_types'; 4 | import { parseKeysExact } from '../parse_keys'; 5 | 6 | export const unimparedActions: Action[] = [ 7 | parseKeysExact([']', 'D'], [Mode.Normal], () => { 8 | commands.executeCommand('editor.action.marker.next'); 9 | }), 10 | 11 | parseKeysExact(['[', 'D'], [Mode.Normal], () => { 12 | commands.executeCommand('editor.action.marker.prev'); 13 | }), 14 | 15 | parseKeysExact([']', 'd'], [Mode.Normal], () => { 16 | commands.executeCommand('editor.action.marker.nextInFiles'); 17 | }), 18 | 19 | parseKeysExact(['[', 'd'], [Mode.Normal], () => { 20 | commands.executeCommand('editor.action.marker.prevInFiles'); 21 | }), 22 | 23 | parseKeysExact(['[', 'g'], [Mode.Normal], () => { 24 | // There is no way to check if we're in compare editor mode or not so i need to call both commands 25 | commands.executeCommand('workbench.action.compareEditor.previousChange'); 26 | commands.executeCommand('workbench.action.editor.previousChange'); 27 | }), 28 | 29 | parseKeysExact([']', 'g'], [Mode.Normal], () => { 30 | // There is no way to check if we're in compare editor mode or not so i need to call both commands 31 | commands.executeCommand('workbench.action.compareEditor.nextChange'); 32 | commands.executeCommand('workbench.action.editor.nextChange'); 33 | }), 34 | 35 | parseKeysExact([']', 'f'], [Mode.Normal], (helixState, editor) => { 36 | const range = helixState.symbolProvider.getNextFunctionRange(editor); 37 | if (range) { 38 | editor.revealRange(range, TextEditorRevealType.InCenter); 39 | editor.selection = new Selection(range.start, range.end); 40 | } 41 | }), 42 | 43 | parseKeysExact(['[', 'f'], [Mode.Normal], (helixState, editor) => { 44 | const range = helixState.symbolProvider.getPreviousFunctionRange(editor); 45 | if (range) { 46 | editor.revealRange(range, TextEditorRevealType.InCenter); 47 | editor.selection = new Selection(range.start, range.end); 48 | } 49 | }), 50 | ]; 51 | -------------------------------------------------------------------------------- /src/eventHandlers.ts: -------------------------------------------------------------------------------- 1 | import type { TextDocumentChangeEvent, TextEditor, TextEditorSelectionChangeEvent } from 'vscode'; 2 | import { TextEditorSelectionChangeKind } from 'vscode'; 3 | import { HelixState } from './helix_state_types'; 4 | import { enterNormalMode, enterVisualMode, setModeCursorStyle, setRelativeLineNumbers } from './modes'; 5 | import { Mode } from './modes_types'; 6 | 7 | /** Currently this handler is used for implementing "g", "m" (go to last modified file) */ 8 | export function onDidChangeTextDocument(HelixState: HelixState, e: TextDocumentChangeEvent) { 9 | HelixState.editorState.lastModifiedDocument = e.document; 10 | HelixState.symbolProvider.refreshTree(e.document.uri); 11 | } 12 | 13 | /** Currently this handler is used for implementing "g", "a" (go to last accessed file) */ 14 | export function onDidChangeActiveTextEditor(helixState: HelixState, editor: TextEditor | undefined) { 15 | if (!editor) return; 16 | 17 | // The user has switched editors, re-set the editor state so we can go back 18 | helixState.editorState.previousEditor = helixState.editorState.activeEditor; 19 | helixState.editorState.activeEditor = editor; 20 | helixState.symbolProvider.refreshTree(editor.document.uri); 21 | 22 | // Ensure new editors always have the correct cursor style and line numbering 23 | // applied according to the current mode 24 | setModeCursorStyle(helixState.mode, editor); 25 | } 26 | 27 | export function onSelectionChange(helixState: HelixState, e: TextEditorSelectionChangeEvent): void { 28 | if (helixState.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 | (helixState.mode === Mode.Visual || helixState.mode === Mode.VisualLine) && 36 | e.kind === TextEditorSelectionChangeKind.Mouse 37 | ) { 38 | enterNormalMode(helixState); 39 | setModeCursorStyle(helixState.mode, e.textEditor); 40 | setRelativeLineNumbers(helixState.mode, e.textEditor); 41 | } 42 | } else { 43 | if (helixState.mode === Mode.Normal) { 44 | enterVisualMode(helixState); 45 | // setModeCursorStyle(helixState.mode, e.textEditor); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/actions/spaceMode.ts: -------------------------------------------------------------------------------- 1 | import { commands } from 'vscode'; 2 | import { Action } from '../action_types'; 3 | import { enterWindowMode } from '../modes'; 4 | import { Mode } from '../modes_types'; 5 | import { parseKeysExact } from '../parse_keys'; 6 | 7 | // https://docs.helix-editor.com/keymap.html#space-mode 8 | export const spaceActions: Action[] = [ 9 | // Open File Picker 10 | parseKeysExact([' ', 'f'], [Mode.Normal], () => { 11 | commands.executeCommand('workbench.action.quickOpen'); 12 | }), 13 | 14 | parseKeysExact([' ', 'g'], [Mode.Normal], () => { 15 | commands.executeCommand('workbench.debug.action.focusBreakpointsView'); 16 | }), 17 | 18 | parseKeysExact([' ', 'k'], [Mode.Normal], () => { 19 | commands.executeCommand('editor.action.showHover'); 20 | }), 21 | 22 | parseKeysExact([' ', 's'], [Mode.Normal], () => { 23 | commands.executeCommand('workbench.action.gotoSymbol'); 24 | }), 25 | 26 | parseKeysExact([' ', 'S'], [Mode.Normal], () => { 27 | commands.executeCommand('workbench.action.showAllSymbols'); 28 | }), 29 | 30 | // View problems in current file 31 | parseKeysExact([' ', 'd'], [Mode.Normal], () => { 32 | commands.executeCommand('workbench.actions.view.problems'); 33 | // It's not possible to set active file on and off, you can only toggle it, which makes implementing this difficult 34 | // For now both d and D will do the same thing and search all of the workspace 35 | 36 | // Leaving this here for future reference 37 | // commands.executeCommand('workbench.actions.workbench.panel.markers.view.toggleActiveFile'); 38 | }), 39 | 40 | // View problems in workspace 41 | parseKeysExact([' ', 'D'], [Mode.Normal], () => { 42 | // alias of 'd'. See above 43 | commands.executeCommand('workbench.actions.view.problems'); 44 | }), 45 | 46 | parseKeysExact([' ', 'r'], [Mode.Normal], () => { 47 | commands.executeCommand('editor.action.rename'); 48 | }), 49 | 50 | parseKeysExact([' ', 'a'], [Mode.Normal], () => { 51 | commands.executeCommand('editor.action.quickFix'); 52 | }), 53 | 54 | parseKeysExact([' ', 'w'], [Mode.Normal], (helixState) => { 55 | enterWindowMode(helixState); 56 | }), 57 | 58 | parseKeysExact([' ', '/'], [Mode.Normal], () => { 59 | commands.executeCommand('workbench.action.findInFiles'); 60 | }), 61 | 62 | parseKeysExact([' ', '?'], [Mode.Normal], () => { 63 | commands.executeCommand('workbench.action.showCommands'); 64 | }), 65 | ]; 66 | -------------------------------------------------------------------------------- /src/position_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | 3 | export function left(position: vscode.Position, count = 1): vscode.Position { 4 | return position.with({ 5 | character: Math.max(position.character - count, 0), 6 | }) 7 | } 8 | 9 | export function right( 10 | document: vscode.TextDocument, 11 | position: vscode.Position, 12 | count = 1, 13 | ): vscode.Position { 14 | const lineLength = document.lineAt(position.line).text.length 15 | return position.with({ 16 | character: Math.min(position.character + count, lineLength), 17 | }) 18 | } 19 | 20 | export function rightNormal( 21 | document: vscode.TextDocument, 22 | position: vscode.Position, 23 | count = 1, 24 | ): vscode.Position { 25 | const lineLength = document.lineAt(position.line).text.length 26 | 27 | if (lineLength === 0) { 28 | return position.with({ character: 0 }) 29 | } else { 30 | return position.with({ 31 | character: Math.min(position.character + count, lineLength - 1), 32 | }) 33 | } 34 | } 35 | 36 | export function leftWrap( 37 | document: vscode.TextDocument, 38 | position: vscode.Position, 39 | ): vscode.Position { 40 | if (position.character <= 0) { 41 | if (position.line <= 0) { 42 | return position 43 | } else { 44 | const previousLineLength = document.lineAt(position.line - 1).text.length 45 | return new vscode.Position(position.line - 1, previousLineLength) 46 | } 47 | } else { 48 | return position.with({ character: position.character - 1 }) 49 | } 50 | } 51 | 52 | export function rightWrap( 53 | document: vscode.TextDocument, 54 | position: vscode.Position, 55 | ): vscode.Position { 56 | const lineLength = document.lineAt(position.line).text.length 57 | 58 | if (position.character >= lineLength) { 59 | if (position.line >= document.lineCount - 1) { 60 | return position 61 | } else { 62 | return new vscode.Position(position.line + 1, 0) 63 | } 64 | } else { 65 | return position.with({ character: position.character + 1 }) 66 | } 67 | } 68 | 69 | export function lineEnd(document: vscode.TextDocument, position: vscode.Position): vscode.Position { 70 | const lineLength = document.lineAt(position.line).text.length 71 | return position.with({ 72 | character: lineLength, 73 | }) 74 | } 75 | 76 | export function lastChar(document: vscode.TextDocument): vscode.Position { 77 | return new vscode.Position( 78 | document.lineCount - 1, 79 | document.lineAt(document.lineCount - 1).text.length, 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /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(positionUtils.left(vscodeSelection.anchor), vscodeSelection.active); 11 | } else { 12 | return new vscode.Selection(vscodeSelection.anchor, positionUtils.left(vscodeSelection.active)); 13 | } 14 | } 15 | 16 | export function vimToVscodeVisualSelection( 17 | document: vscode.TextDocument, 18 | vimSelection: vscode.Selection, 19 | ): vscode.Selection { 20 | if (vimSelection.active.isBefore(vimSelection.anchor)) { 21 | return new vscode.Selection(positionUtils.right(document, vimSelection.anchor), vimSelection.active); 22 | } else { 23 | return new vscode.Selection(vimSelection.anchor, positionUtils.right(document, vimSelection.active)); 24 | } 25 | } 26 | 27 | export function vscodeToVimVisualLineSelection( 28 | document: vscode.TextDocument, 29 | vscodeSelection: vscode.Selection, 30 | ): vscode.Selection { 31 | return new vscode.Selection( 32 | vscodeSelection.anchor.with({ character: 0 }), 33 | vscodeSelection.active.with({ character: 0 }), 34 | ); 35 | } 36 | 37 | export function vimToVscodeVisualLineSelection( 38 | document: vscode.TextDocument, 39 | vimSelection: vscode.Selection, 40 | ): vscode.Selection { 41 | const anchorLineLength = document.lineAt(vimSelection.anchor.line).text.length; 42 | const activeLineLength = document.lineAt(vimSelection.active.line).text.length; 43 | 44 | if (vimSelection.active.isBefore(vimSelection.anchor)) { 45 | return new vscode.Selection( 46 | vimSelection.anchor.with({ character: anchorLineLength }), 47 | vimSelection.active.with({ character: 0 }), 48 | ); 49 | } else { 50 | return new vscode.Selection( 51 | vimSelection.anchor.with({ character: 0 }), 52 | vimSelection.active.with({ character: activeLineLength }), 53 | ); 54 | } 55 | } 56 | 57 | export function flipSelection(editor: vscode.TextEditor | undefined) { 58 | if (!editor) { 59 | return; 60 | } 61 | 62 | editor.selections = editor.selections.map((s) => new vscode.Selection(s.active, s.anchor)); 63 | // When flipping selection the new active position may be off screen, so reveal line to the active location 64 | vscode.commands.executeCommand('revealLine', { 65 | lineNumber: editor.selection.active.line, 66 | at: 'center', 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /docs/img/helixLogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/paragraph_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Range } from 'vscode'; 3 | import { SimpleRange } from './simple_range_types'; 4 | 5 | export function paragraphForward(document: vscode.TextDocument, line: number): number { 6 | let visitedNonEmptyLine = false; 7 | 8 | for (let i = line; i < document.lineCount; ++i) { 9 | if (visitedNonEmptyLine) { 10 | if (document.lineAt(i).isEmptyOrWhitespace) { 11 | return i; 12 | } 13 | } else { 14 | if (!document.lineAt(i).isEmptyOrWhitespace) { 15 | visitedNonEmptyLine = true; 16 | } 17 | } 18 | } 19 | 20 | return document.lineCount - 1; 21 | } 22 | 23 | export function paragraphBackward(document: vscode.TextDocument, line: number): number { 24 | let visitedNonEmptyLine = false; 25 | 26 | for (let i = line; i >= 0; --i) { 27 | if (visitedNonEmptyLine) { 28 | if (document.lineAt(i).isEmptyOrWhitespace) { 29 | return i; 30 | } 31 | } else { 32 | if (!document.lineAt(i).isEmptyOrWhitespace) { 33 | visitedNonEmptyLine = true; 34 | } 35 | } 36 | } 37 | 38 | return 0; 39 | } 40 | 41 | export function paragraphRangeOuter(document: vscode.TextDocument, line: number): SimpleRange | undefined { 42 | if (document.lineAt(line).isEmptyOrWhitespace) return undefined; 43 | 44 | return { 45 | start: paragraphRangeBackward(document, line - 1), 46 | end: paragraphRangeForwardOuter(document, line + 1), 47 | }; 48 | } 49 | 50 | function paragraphRangeForwardOuter(document: vscode.TextDocument, line: number): number { 51 | let seenWhitespace = false; 52 | 53 | for (let i = line; i < document.lineCount; ++i) { 54 | if (document.lineAt(i).isEmptyOrWhitespace) { 55 | seenWhitespace = true; 56 | } else if (seenWhitespace) { 57 | return i - 1; 58 | } 59 | } 60 | 61 | return document.lineCount - 1; 62 | } 63 | 64 | function paragraphRangeBackward(document: vscode.TextDocument, line: number): number { 65 | for (let i = line; i >= 0; --i) { 66 | if (document.lineAt(i).isEmptyOrWhitespace) { 67 | return i + 1; 68 | } 69 | } 70 | 71 | return 0; 72 | } 73 | 74 | export function paragraphRangeInner(document: vscode.TextDocument, line: number): SimpleRange | undefined { 75 | if (document.lineAt(line).isEmptyOrWhitespace) return undefined; 76 | 77 | return { 78 | start: paragraphRangeBackward(document, line - 1), 79 | end: paragraphRangeForwardInner(document, line + 1), 80 | }; 81 | } 82 | 83 | function paragraphRangeForwardInner(document: vscode.TextDocument, line: number): number { 84 | for (let i = line; i < document.lineCount; ++i) { 85 | if (document.lineAt(i).isEmptyOrWhitespace) { 86 | return i - 1; 87 | } 88 | } 89 | 90 | return document.lineCount - 1; 91 | } 92 | -------------------------------------------------------------------------------- /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.includes(char) 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/escape_handler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { commandLine } from './commandLine'; 4 | import { HelixState } from './helix_state_types'; 5 | import { enterNormalMode, setModeCursorStyle, setRelativeLineNumbers } from './modes'; 6 | import { Mode } from './modes_types'; 7 | import * as positionUtils from './position_utils'; 8 | import { typeHandler } from './type_handler'; 9 | import { addTypeSubscription } from './type_subscription'; 10 | 11 | export function escapeHandler(vimState: HelixState): void { 12 | const editor = vscode.window.activeTextEditor; 13 | 14 | if (!editor) return; 15 | 16 | if (vimState.mode === Mode.Insert || vimState.mode === Mode.Occurrence) { 17 | editor.selections = editor.selections.map((selection) => { 18 | return new vscode.Selection(selection.active, selection.active); 19 | 20 | }); 21 | 22 | enterNormalMode(vimState); 23 | setModeCursorStyle(vimState.mode, editor); 24 | setRelativeLineNumbers(vimState.mode, editor); 25 | addTypeSubscription(vimState, typeHandler); 26 | } else if (vimState.mode === Mode.Normal) { 27 | // Clear multiple cursors 28 | if (editor.selections.length > 1) { 29 | editor.selections = [editor.selections[0]]; 30 | } 31 | // There is no way to check if find widget is open, so just close it 32 | vscode.commands.executeCommand('closeFindWidget'); 33 | } else if (vimState.mode === Mode.Visual) { 34 | editor.selections = editor.selections.map((selection) => { 35 | const newPosition = new vscode.Position(selection.active.line, Math.max(selection.active.character - 1, 0)); 36 | return new vscode.Selection(newPosition, newPosition); 37 | }); 38 | 39 | enterNormalMode(vimState); 40 | setModeCursorStyle(vimState.mode, editor); 41 | setRelativeLineNumbers(vimState.mode, editor); 42 | } else if (vimState.mode === Mode.VisualLine) { 43 | editor.selections = editor.selections.map((selection) => { 44 | const newPosition = selection.active.with({ 45 | character: Math.max(selection.active.character - 1, 0), 46 | }); 47 | return new vscode.Selection(newPosition, newPosition); 48 | }); 49 | 50 | enterNormalMode(vimState); 51 | setModeCursorStyle(vimState.mode, editor); 52 | setRelativeLineNumbers(vimState.mode, editor); 53 | } else if (vimState.mode === Mode.SearchInProgress || vimState.mode === Mode.Select) { 54 | enterNormalMode(vimState); 55 | vimState.searchState.clearSearchString(vimState); 56 | // To match Helix UI go back to the last active position on escape 57 | if (vimState.searchState.lastActivePosition) { 58 | editor.selection = new vscode.Selection( 59 | vimState.searchState.lastActivePosition, 60 | vimState.searchState.lastActivePosition, 61 | ); 62 | vimState.editorState.activeEditor?.revealRange( 63 | editor.selection, 64 | vscode.TextEditorRevealType.InCenterIfOutsideViewport, 65 | ); 66 | } 67 | } else if (vimState.mode === Mode.View || vimState.mode === Mode.CommandlineInProgress) { 68 | commandLine.clearCommandString(vimState); 69 | enterNormalMode(vimState); 70 | } 71 | 72 | vimState.keysPressed = []; 73 | } 74 | -------------------------------------------------------------------------------- /src/statusBar.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { HelixState } from './helix_state_types'; 3 | import { Mode } from './modes_types'; 4 | 5 | class StatusBarImpl implements vscode.Disposable { 6 | // Displays the current state (mode, recording macro, etc.) and messages to the user 7 | private readonly statusBarItem: vscode.StatusBarItem; 8 | 9 | private previousMode: Mode | undefined = undefined; 10 | private showingDefaultMessage = true; 11 | private themeBackgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); 12 | 13 | public lastMessageTime: Date | undefined; 14 | 15 | constructor() { 16 | this.statusBarItem = vscode.window.createStatusBarItem( 17 | 'primary', 18 | vscode.StatusBarAlignment.Left, 19 | Number.MIN_SAFE_INTEGER, // Furthest right on the left 20 | ); 21 | this.statusBarItem.name = 'Helix Command Line'; 22 | this.statusBarItem.text = 'NOR'; 23 | this.statusBarItem.show(); 24 | } 25 | 26 | dispose() { 27 | this.statusBarItem.dispose(); 28 | } 29 | 30 | /** 31 | * Updates the status bar text 32 | * @param isError If true, text rendered in red 33 | */ 34 | public setText(helixState: HelixState, text: string) { 35 | // Text 36 | text = text.replace(/\n/g, '^M'); 37 | if (this.statusBarItem.text !== text) { 38 | this.statusBarItem.text = `${this.statusBarPrefix(helixState)} ${text}`; 39 | } 40 | 41 | this.previousMode = helixState.mode; 42 | this.showingDefaultMessage = false; 43 | this.lastMessageTime = new Date(); 44 | } 45 | 46 | public displayError(vimState: HelixState, error: string | Error) { 47 | StatusBar.setText(vimState, error.toString()); 48 | } 49 | 50 | public getText() { 51 | return this.statusBarItem.text.replace(/\^M/g, '\n'); 52 | } 53 | 54 | /** 55 | * Clears any messages from the status bar, leaving the default info, such as 56 | * the current mode and macro being recorded. 57 | * @param force If true, will clear even high priority messages like errors. 58 | */ 59 | public clear(helixState: HelixState, force = true) { 60 | if (!this.showingDefaultMessage && !force) { 61 | return; 62 | } 63 | 64 | StatusBar.setText(helixState, ''); 65 | this.showingDefaultMessage = true; 66 | } 67 | 68 | statusBarPrefix(helixState: HelixState) { 69 | switch (helixState.mode) { 70 | case Mode.Normal: 71 | this.statusBarItem.backgroundColor = undefined; 72 | return 'NOR'; 73 | case Mode.Visual: 74 | return 'NOR (V)'; 75 | case Mode.Insert: 76 | return 'INS'; 77 | case Mode.Disabled: 78 | return '-- HELIX DISABLED --'; 79 | case Mode.SearchInProgress: 80 | this.statusBarItem.backgroundColor = this.themeBackgroundColor; 81 | return 'SER:'; 82 | case Mode.Select: 83 | this.statusBarItem.backgroundColor = this.themeBackgroundColor; 84 | return 'SEL:'; 85 | case Mode.Window: 86 | return 'WIN'; 87 | case Mode.CommandlineInProgress: 88 | return ':'; 89 | case Mode.View: 90 | return 'VIEW'; 91 | default: 92 | return ''; 93 | } 94 | } 95 | } 96 | 97 | export const StatusBar = new StatusBarImpl(); 98 | -------------------------------------------------------------------------------- /src/put_utils/put_before.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as positionUtils from '../position_utils'; 3 | import type { HelixState } from '../helix_state_types'; 4 | import { adjustInsertPositions, getInsertRangesFromBeginning, getRegisterContentsList } from './common'; 5 | 6 | export function putBefore(vimState: HelixState, editor: vscode.TextEditor) { 7 | const registerContentsList = getRegisterContentsList(vimState, editor); 8 | if (registerContentsList === undefined) return; 9 | 10 | if (vimState.registers.linewise) { 11 | normalModeLinewise(vimState, editor, registerContentsList); 12 | } else { 13 | normalModeCharacterwise(vimState, editor, registerContentsList); 14 | } 15 | } 16 | 17 | function normalModeLinewise( 18 | vimState: HelixState, 19 | editor: vscode.TextEditor, 20 | registerContentsList: (string | undefined)[], 21 | ) { 22 | const insertContentsList = registerContentsList.map((contents) => { 23 | if (contents === undefined) return undefined; 24 | else return `${contents}\n`; 25 | }); 26 | 27 | const insertPositions = editor.selections.map((selection) => { 28 | return new vscode.Position(selection.active.line, 0); 29 | }); 30 | 31 | const adjustedInsertPositions = adjustInsertPositions(insertPositions, insertContentsList); 32 | 33 | editor 34 | .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 | }) 42 | .then(() => { 43 | editor.selections = editor.selections.map((selection, i) => { 44 | const position = adjustedInsertPositions[i]; 45 | if (position === undefined) return selection; 46 | 47 | return new vscode.Selection(position, position); 48 | }); 49 | }); 50 | 51 | vimState.lastPutRanges = { 52 | ranges: getInsertRangesFromBeginning(adjustedInsertPositions, registerContentsList), 53 | linewise: true, 54 | }; 55 | } 56 | 57 | function normalModeCharacterwise( 58 | vimState: HelixState, 59 | editor: vscode.TextEditor, 60 | registerContentsList: (string | undefined)[], 61 | ) { 62 | const insertPositions = editor.selections.map((selection) => selection.start); 63 | const adjustedInsertPositions = adjustInsertPositions(insertPositions, registerContentsList); 64 | const insertRanges = getInsertRangesFromBeginning(adjustedInsertPositions, registerContentsList); 65 | 66 | editor 67 | .edit((editBuilder) => { 68 | insertPositions.forEach((insertPosition, i) => { 69 | const registerContents = registerContentsList[i]; 70 | if (registerContents === undefined) return; 71 | 72 | editBuilder.insert(insertPosition, registerContents); 73 | }); 74 | }) 75 | .then(() => { 76 | editor.selections = editor.selections.map((selection, i) => { 77 | const range = insertRanges[i]; 78 | if (range === undefined) return selection; 79 | 80 | const position = positionUtils.left(range.end); 81 | return new vscode.Selection(position, position); 82 | }); 83 | }); 84 | 85 | vimState.lastPutRanges = { 86 | ranges: insertRanges, 87 | linewise: false, 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /src/actions/viewMode.ts: -------------------------------------------------------------------------------- 1 | import { commands } from 'vscode'; 2 | import { Action } from '../action_types'; 3 | import { enterViewMode } from '../modes'; 4 | import { Mode } from '../modes_types'; 5 | import { parseKeysExact } from '../parse_keys'; 6 | 7 | // https://docs.helix-editor.com/keymap.html#view-mode 8 | export const viewActions: Action[] = [ 9 | parseKeysExact(['Z'], [Mode.Normal, Mode.Visual], (helixState) => { 10 | enterViewMode(helixState); 11 | }), 12 | 13 | // align view center 14 | parseKeysExact(['z', 'c'], [Mode.Normal, Mode.Visual], (_, editor) => { 15 | commands.executeCommand('revealLine', { 16 | lineNumber: editor.selection.active.line, 17 | at: 'center', 18 | }); 19 | }), 20 | 21 | parseKeysExact(['c'], [Mode.View], (_, editor) => { 22 | commands.executeCommand('revealLine', { 23 | lineNumber: editor.selection.active.line, 24 | at: 'center', 25 | }); 26 | }), 27 | 28 | // align view top 29 | parseKeysExact(['z', 't'], [Mode.Normal, Mode.Visual], (_, editor) => { 30 | commands.executeCommand('revealLine', { 31 | lineNumber: editor.selection.active.line, 32 | at: 'top', 33 | }); 34 | }), 35 | 36 | parseKeysExact(['t'], [Mode.View], (_, editor) => { 37 | commands.executeCommand('revealLine', { 38 | lineNumber: editor.selection.active.line, 39 | at: 'top', 40 | }); 41 | }), 42 | 43 | // align view bottom 44 | parseKeysExact(['z', 'b'], [Mode.Normal, Mode.Visual], (_, editor) => { 45 | commands.executeCommand('revealLine', { 46 | lineNumber: editor.selection.active.line, 47 | at: 'bottom', 48 | }); 49 | }), 50 | 51 | parseKeysExact(['b'], [Mode.View], (_, editor) => { 52 | commands.executeCommand('revealLine', { 53 | lineNumber: editor.selection.active.line, 54 | at: 'bottom', 55 | }); 56 | }), 57 | 58 | parseKeysExact(['z', 't'], [Mode.Normal, Mode.Visual], (_, editor) => { 59 | commands.executeCommand('revealLine', { 60 | lineNumber: editor.selection.active.line, 61 | at: 'top', 62 | }); 63 | }), 64 | 65 | parseKeysExact(['t'], [Mode.View], (_, editor) => { 66 | commands.executeCommand('revealLine', { 67 | lineNumber: editor.selection.active.line, 68 | at: 'top', 69 | }); 70 | }), 71 | 72 | parseKeysExact(['z', 'j'], [Mode.Normal, Mode.Visual], () => { 73 | commands.executeCommand('scrollLineDown'); 74 | }), 75 | 76 | parseKeysExact(['j'], [Mode.View], () => { 77 | commands.executeCommand('scrollLineDown'); 78 | }), 79 | 80 | parseKeysExact(['z', 'k'], [Mode.Normal, Mode.Visual], () => { 81 | commands.executeCommand('scrollLineUp'); 82 | }), 83 | 84 | parseKeysExact(['k'], [Mode.View], () => { 85 | commands.executeCommand('scrollLineUp'); 86 | }), 87 | 88 | parseKeysExact(['z', 'z'], [Mode.Normal, Mode.Visual], (_, editor) => { 89 | commands.executeCommand('revealLine', { 90 | lineNumber: editor.selection.active.line, 91 | at: 'center', 92 | }); 93 | }), 94 | 95 | parseKeysExact(['z'], [Mode.View], (_, editor) => { 96 | commands.executeCommand('revealLine', { 97 | lineNumber: editor.selection.active.line, 98 | at: 'center', 99 | }); 100 | }), 101 | ]; 102 | -------------------------------------------------------------------------------- /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 tillForward(document: vscode.TextDocument, needle: string, fromPosition: vscode.Position) { 22 | for (let i = fromPosition.line; i < document.lineCount; ++i) { 23 | const lineText = document.lineAt(i).text; 24 | const fromIndex = i === fromPosition.line ? fromPosition.character : 0; 25 | const matchIndex = lineText.indexOf(needle, fromIndex); 26 | 27 | if (matchIndex >= 0) { 28 | return new vscode.Position(i, matchIndex); 29 | } 30 | } 31 | 32 | return undefined; 33 | } 34 | 35 | export function searchBackward( 36 | document: vscode.TextDocument, 37 | needle: string, 38 | fromPosition: vscode.Position, 39 | ): vscode.Position | undefined { 40 | for (let i = fromPosition.line; i >= 0; --i) { 41 | const lineText = document.lineAt(i).text; 42 | const fromIndex = i === fromPosition.line ? fromPosition.character : +Infinity; 43 | const matchIndex = lineText.lastIndexOf(needle, fromIndex); 44 | 45 | if (matchIndex >= 0) { 46 | return new vscode.Position(i, matchIndex); 47 | } 48 | } 49 | 50 | return undefined; 51 | } 52 | 53 | export function searchForwardBracket( 54 | document: vscode.TextDocument, 55 | openingChar: string, 56 | closingChar: string, 57 | fromPosition: vscode.Position, 58 | offset?: number, 59 | ): vscode.Position | undefined { 60 | let n = offset ? offset - 1 : 0; 61 | 62 | for (let i = fromPosition.line; i < document.lineCount; ++i) { 63 | const lineText = document.lineAt(i).text; 64 | const fromIndex = i === fromPosition.line ? fromPosition.character : 0; 65 | 66 | for (let j = fromIndex; j < lineText.length; ++j) { 67 | // If closing and opening are the same, don't bother deducting n 68 | // However if they are different, we need to deduct n when we see an opening char 69 | if (lineText[j] === openingChar && openingChar !== closingChar) { 70 | ++n; 71 | } else if (lineText[j] === closingChar) { 72 | if (n === 0) { 73 | return new vscode.Position(i, j); 74 | } else { 75 | --n; 76 | } 77 | } 78 | } 79 | } 80 | 81 | return undefined; 82 | } 83 | 84 | export function searchBackwardBracket( 85 | document: vscode.TextDocument, 86 | openingChar: string, 87 | closingChar: string, 88 | fromPosition: vscode.Position, 89 | offset?: number, 90 | ): vscode.Position | undefined { 91 | let n = offset ? offset - 1 : 0; 92 | 93 | for (let i = fromPosition.line; i >= 0; --i) { 94 | const lineText = document.lineAt(i).text; 95 | const fromIndex = i === fromPosition.line ? fromPosition.character : lineText.length - 1; 96 | 97 | for (let j = fromIndex; j >= 0; --j) { 98 | // If closing and opening are the same, don't bother deducting n 99 | // However if they are different, we need to deduct n when we see an opening char 100 | if (lineText[j] === closingChar && closingChar !== openingChar) { 101 | ++n; 102 | } else if (lineText[j] === openingChar) { 103 | if (n === 0) { 104 | return new vscode.Position(i, j); 105 | } else { 106 | --n; 107 | } 108 | } 109 | } 110 | } 111 | 112 | return undefined; 113 | } 114 | -------------------------------------------------------------------------------- /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/actions/windowMode.ts: -------------------------------------------------------------------------------- 1 | import { commands } from 'vscode'; 2 | import { Action } from '../action_types'; 3 | import { enterNormalMode } from '../modes'; 4 | import { Mode } from '../modes_types'; 5 | import { parseKeysExact } from '../parse_keys'; 6 | 7 | // https://docs.helix-editor.com/keymap.html#window-mode 8 | export const windowActions: Action[] = [ 9 | // New window modes (moving existing windows) 10 | parseKeysExact(['m', 'v'], [Mode.Window], (helixState) => { 11 | commands.executeCommand('workbench.action.moveEditorToNextGroup'); 12 | enterNormalMode(helixState); 13 | }), 14 | 15 | parseKeysExact(['m', 's'], [Mode.Window], (helixState) => { 16 | commands.executeCommand('workbench.action.moveEditorToBelowGroup'); 17 | enterNormalMode(helixState); 18 | }), 19 | 20 | parseKeysExact(['m', 'p'], [Mode.Window], (helixState) => { 21 | commands.executeCommand('workbench.action.moveEditorToPreviousGroup'); 22 | enterNormalMode(helixState); 23 | }), 24 | 25 | parseKeysExact(['m', 'w'], [Mode.Window], (helixState) => { 26 | commands.executeCommand('workbench.action.moveEditorToNewWindow'); 27 | enterNormalMode(helixState); 28 | }), 29 | 30 | parseKeysExact(['m', 'j'], [Mode.Window], (helixState) => { 31 | commands.executeCommand('workbench.action.restoreEditorsToMainWindow'); 32 | enterNormalMode(helixState); 33 | }), 34 | 35 | // Crtl+w actions 36 | parseKeysExact(['w'], [Mode.Window], (helixState) => { 37 | commands.executeCommand('workbench.action.navigateEditorGroups'); 38 | enterNormalMode(helixState); 39 | }), 40 | 41 | parseKeysExact(['v'], [Mode.Window], (helixState) => { 42 | commands.executeCommand('workbench.action.splitEditor'); 43 | enterNormalMode(helixState); 44 | }), 45 | 46 | parseKeysExact(['s'], [Mode.Window], (helixState) => { 47 | commands.executeCommand('workbench.action.splitEditorDown'); 48 | enterNormalMode(helixState); 49 | }), 50 | 51 | parseKeysExact(['F'], [Mode.Window], (helixState) => { 52 | commands.executeCommand('editor.action.revealDefinitionAside'); 53 | enterNormalMode(helixState); 54 | }), 55 | 56 | parseKeysExact(['f'], [Mode.Window], (helixState) => { 57 | commands.executeCommand('editor.action.revealDefinitionAside'); 58 | enterNormalMode(helixState); 59 | }), 60 | 61 | parseKeysExact(['h'], [Mode.Window], (helixState) => { 62 | commands.executeCommand('workbench.action.focusLeftGroup'); 63 | enterNormalMode(helixState); 64 | }), 65 | 66 | parseKeysExact(['l'], [Mode.Window], (helixState) => { 67 | commands.executeCommand('workbench.action.focusRightGroup'); 68 | enterNormalMode(helixState); 69 | }), 70 | 71 | parseKeysExact(['j'], [Mode.Window], (helixState) => { 72 | commands.executeCommand('workbench.action.focusBelowGroup'); 73 | enterNormalMode(helixState); 74 | }), 75 | 76 | parseKeysExact(['k'], [Mode.Window], (helixState) => { 77 | commands.executeCommand('workbench.action.focusAboveGroup'); 78 | enterNormalMode(helixState); 79 | }), 80 | 81 | parseKeysExact(['q'], [Mode.Window], (helixState) => { 82 | commands.executeCommand('workbench.action.closeActiveEditor'); 83 | enterNormalMode(helixState); 84 | }), 85 | 86 | // Alias q (for vim compatibility) 87 | parseKeysExact(['c'], [Mode.Window], (helixState) => { 88 | commands.executeCommand('workbench.action.closeActiveEditor'); 89 | enterNormalMode(helixState); 90 | }), 91 | 92 | parseKeysExact(['o'], [Mode.Window], (helixState) => { 93 | commands.executeCommand('workbench.action.closeOtherEditors'); 94 | enterNormalMode(helixState); 95 | }), 96 | 97 | parseKeysExact(['H'], [Mode.Window], (helixState) => { 98 | commands.executeCommand('workbench.action.moveActiveEditorGroupLeft'); 99 | enterNormalMode(helixState); 100 | }), 101 | 102 | parseKeysExact(['L'], [Mode.Window], (helixState) => { 103 | commands.executeCommand('workbench.action.moveActiveEditorGroupRight'); 104 | enterNormalMode(helixState); 105 | }), 106 | 107 | parseKeysExact(['n'], [Mode.Window], (helixState) => { 108 | commands.executeCommand('workbench.action.files.newUntitledFile'); 109 | enterNormalMode(helixState); 110 | }), 111 | 112 | parseKeysExact(['b'], [Mode.Window], (helixState) => { 113 | commands.executeCommand('workbench.action.toggleSidebarVisibility'); 114 | enterNormalMode(helixState); 115 | }), 116 | ]; 117 | -------------------------------------------------------------------------------- /docs/img/Visual_Studio_Code_1.35_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/block_utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | 3 | enum MatchType { 4 | start = 'start', 5 | end = 'end', 6 | } 7 | 8 | interface BlockMatch { 9 | type: MatchType 10 | match: RegExpMatchArray 11 | } 12 | 13 | const startRegex = (startWords: string[]) => RegExp(`(^|\\s)(${startWords.join('|')})($|\\s)`, 'g') 14 | 15 | const endRegex = (endWords: string[]) => RegExp(`(^|\\s)(${endWords.join('|')})($|\\W)`, 'g') 16 | 17 | export function blockRange(document: vscode.TextDocument, position: vscode.Position): vscode.Range { 18 | let startWords: string[] = [] 19 | let endWords: string[] = [] 20 | 21 | console.log(`LanguageID=${document.languageId}`) 22 | if (document.languageId === 'elixir') { 23 | startWords = ['case', 'cond', 'fn', 'def'] 24 | endWords = ['end'] 25 | } else { 26 | console.log(`Unsupported language: ${document.languageId}`) 27 | return new vscode.Range(position, position) 28 | } 29 | 30 | const start = findBlockStart(document, position, startWords, endWords) 31 | const end = findBlockEnd(document, position, startWords, endWords) 32 | 33 | if (start && end) { 34 | return new vscode.Range(start, end) 35 | } 36 | return new vscode.Range(position, position) 37 | } 38 | 39 | function findBlockStart( 40 | document: vscode.TextDocument, 41 | position: vscode.Position, 42 | startWords: string[], 43 | endWords: string[], 44 | ): vscode.Position | undefined { 45 | const closedBlocks: boolean[] = [] 46 | 47 | for (let i = position.line; i >= 0; --i) { 48 | const lineText = 49 | i === position.line 50 | ? document.lineAt(i).text.substr(position.character) 51 | : document.lineAt(i).text 52 | 53 | let blockMatches: BlockMatch[] = [] 54 | for (const m of lineText.matchAll(startRegex(startWords))) { 55 | blockMatches.push({ type: MatchType.start, match: m }) 56 | } 57 | 58 | for (const m of lineText.matchAll(endRegex(endWords))) { 59 | blockMatches.push({ type: MatchType.end, match: m }) 60 | } 61 | 62 | blockMatches = blockMatches.sort((a, b) => 63 | (a.match.index as number) > (b.match.index as number) ? 1 : -1, 64 | ) 65 | 66 | for (let idx = 0; idx < blockMatches.length; idx++) { 67 | const blockMatch = blockMatches[idx] 68 | if (blockMatch.type === MatchType.end) { 69 | closedBlocks.push(true) 70 | } else if (blockMatch.type === MatchType.start) { 71 | if (closedBlocks.length === 0) { 72 | const [fullText, , matchText] = blockMatch.match 73 | const offset = fullText.indexOf(matchText) 74 | return new vscode.Position(i, (blockMatch.match.index as number) + offset) 75 | } else { 76 | closedBlocks.pop() 77 | } 78 | } 79 | } 80 | } 81 | return undefined 82 | } 83 | 84 | function findBlockEnd( 85 | document: vscode.TextDocument, 86 | position: vscode.Position, 87 | startWords: string[], 88 | endWords: string[], 89 | ): vscode.Position | undefined { 90 | const openedBlocks: boolean[] = [true] 91 | 92 | for (let i = position.line; i < document.lineCount; ++i) { 93 | const lineText = 94 | i === position.line 95 | ? document.lineAt(i).text.substr(position.character) 96 | : document.lineAt(i).text 97 | 98 | let blockMatches: BlockMatch[] = [] 99 | for (const m of lineText.matchAll(startRegex(startWords))) { 100 | blockMatches.push({ type: MatchType.start, match: m }) 101 | } 102 | 103 | for (const m of lineText.matchAll(endRegex(endWords))) { 104 | blockMatches.push({ type: MatchType.end, match: m }) 105 | } 106 | 107 | blockMatches = blockMatches.sort((a, b) => 108 | (a.match.index as number) > (b.match.index as number) ? 1 : -1, 109 | ) 110 | 111 | for (let idx = 0; idx < blockMatches.length; idx++) { 112 | const blockMatch = blockMatches[idx] 113 | if (blockMatch.type === MatchType.start) { 114 | openedBlocks.push(true) 115 | } else if (blockMatch.type === MatchType.end) { 116 | openedBlocks.pop() 117 | if (openedBlocks.length === 0) { 118 | const [fullText, , matchText] = blockMatch.match 119 | const offset = fullText.indexOf(matchText) 120 | return new vscode.Position( 121 | i, 122 | (blockMatch.match.index as number) + offset + matchText.length, 123 | ) 124 | } 125 | } 126 | } 127 | } 128 | return undefined 129 | } 130 | -------------------------------------------------------------------------------- /src/put_utils/common.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { HelixState } from '../helix_state_types'; 4 | 5 | export function getRegisterContentsList(vimState: HelixState, 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(() => 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({ 38 | character: position.character - lines[0].length, 39 | }); 40 | } 41 | 42 | return new vscode.Range(beginningPosition, position); 43 | }); 44 | } 45 | 46 | // Given positions and contents inserted at those positions, return the range that will 47 | // select that contents 48 | export function getInsertRangesFromBeginning(positions: vscode.Position[], contentsList: (string | undefined)[]) { 49 | return positions.map((position, i) => { 50 | const contents = contentsList[i]; 51 | if (!contents) return undefined; 52 | 53 | const lines = contents.split(/\r?\n/); 54 | const endLine = position.line + lines.length - 1; 55 | const endCharacter = lines.length === 1 ? position.character + lines[0].length : lines[lines.length - 1].length; 56 | 57 | return new vscode.Range(position, new vscode.Position(endLine, endCharacter)); 58 | }); 59 | } 60 | 61 | // Given positions and contents inserted at those positions, figure out how the positions will move 62 | // when the contents is inserted. For example inserting a line above a position will increase its 63 | // line number by one. 64 | export function adjustInsertPositions(positions: vscode.Position[], contentsList: (string | undefined)[]) { 65 | const indexPositions = positions.map((position, i) => ({ 66 | originalIndex: i, 67 | position: position, 68 | })); 69 | 70 | indexPositions.sort((a, b) => { 71 | if (a.position.isBefore(b.position)) return -1; 72 | else if (a.position.isEqual(b.position)) return 0; 73 | else return 1; 74 | }); 75 | 76 | const adjustedIndexPositions = []; 77 | let lineOffset = 0; 78 | let characterOffset = 0; 79 | let lineNumber = 0; 80 | 81 | for (const indexPosition of indexPositions) { 82 | // Adjust position 83 | 84 | const adjustedLine = indexPosition.position.line + lineOffset; 85 | 86 | let adjustedCharacter = indexPosition.position.character; 87 | if (indexPosition.position.line === lineNumber) { 88 | adjustedCharacter += characterOffset; 89 | } 90 | 91 | adjustedIndexPositions.push({ 92 | originalIndex: indexPosition.originalIndex, 93 | position: new vscode.Position(adjustedLine, adjustedCharacter), 94 | }); 95 | 96 | // Increase offsets 97 | 98 | const contents = contentsList[indexPosition.originalIndex]; 99 | 100 | if (contents !== undefined) { 101 | const contentsLines = contents.split(/\r?\n/); 102 | 103 | lineOffset += contentsLines.length - 1; 104 | 105 | if (indexPosition.position.line === lineNumber) { 106 | if (contentsLines.length === 1) { 107 | characterOffset += contentsLines[0].length; 108 | } else { 109 | characterOffset += contentsLines[contentsLines.length - 1].length - indexPosition.position.character; 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/SymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export class SymbolProvider { 4 | /** The array of symbols in the current document */ 5 | tree: vscode.DocumentSymbol[] = []; 6 | /** Current index in tree */ 7 | symbolIndex = 0; 8 | /** Flag for if the tree is dirty or not */ 9 | dirtyTree = false; 10 | 11 | static checkSymbolKindPermitted(symbolKind: vscode.SymbolKind): boolean { 12 | // https://code.visualstudio.com/api/references/vscode-api#SymbolKind 13 | return ( 14 | symbolKind === vscode.SymbolKind.Constructor || 15 | symbolKind === vscode.SymbolKind.Enum || 16 | symbolKind === vscode.SymbolKind.EnumMember || 17 | symbolKind === vscode.SymbolKind.Event || 18 | symbolKind === vscode.SymbolKind.Function || 19 | symbolKind === vscode.SymbolKind.Interface || 20 | symbolKind === vscode.SymbolKind.Method 21 | ); 22 | } 23 | 24 | async refreshTree(uri: vscode.Uri) { 25 | const results = await vscode.commands.executeCommand( 26 | 'vscode.executeDocumentSymbolProvider', 27 | uri, 28 | ); 29 | if (!results) { 30 | return []; 31 | } 32 | 33 | const flattenedSymbols: vscode.DocumentSymbol[] = []; 34 | const addSymbols = (flattenedSymbols: vscode.DocumentSymbol[], results: vscode.DocumentSymbol[]) => { 35 | results.forEach((symbol: vscode.DocumentSymbol) => { 36 | if (SymbolProvider.checkSymbolKindPermitted(symbol.kind)) { 37 | flattenedSymbols.push(symbol); 38 | } 39 | if (symbol.children && symbol.children.length > 0) { 40 | addSymbols(flattenedSymbols, symbol.children); 41 | } 42 | }); 43 | }; 44 | 45 | addSymbols(flattenedSymbols, results); 46 | 47 | this.tree = flattenedSymbols.sort((x: vscode.DocumentSymbol, y: vscode.DocumentSymbol) => { 48 | const lineDiff = x.selectionRange.start.line - y.selectionRange.start.line; 49 | if (lineDiff === 0) { 50 | return x.selectionRange.start.character - y.selectionRange.start.character; 51 | } 52 | return lineDiff; 53 | }); 54 | 55 | this.dirtyTree = false; 56 | } 57 | 58 | getContainingSymbolIndex(position: vscode.Position): number | undefined { 59 | if (this.tree.length === 0 || this.dirtyTree) { 60 | return; 61 | } 62 | 63 | const symbolIndex = this.tree.findIndex((symbol: vscode.DocumentSymbol) => { 64 | return symbol.range.contains(position); 65 | }); 66 | 67 | return symbolIndex; 68 | } 69 | 70 | getContainingSymbolRange(position: vscode.Position): vscode.Range | undefined { 71 | if (this.tree.length === 0 || this.dirtyTree) { 72 | return; 73 | } 74 | 75 | const symbolIndex = this.getContainingSymbolIndex(position); 76 | if (symbolIndex === -1 || symbolIndex === undefined) { 77 | return; 78 | } 79 | 80 | const symbol = this.tree[symbolIndex]; 81 | 82 | if (symbol) { 83 | return symbol.range; 84 | } 85 | } 86 | 87 | getNextFunctionRange(editor: vscode.TextEditor): vscode.Range | undefined { 88 | if (this.tree.length === 0 || this.dirtyTree) { 89 | return; 90 | } 91 | 92 | const activeCursor = editor.selection.active; 93 | const currentSymbolIndex = this.getContainingSymbolIndex(activeCursor); 94 | if (currentSymbolIndex === undefined) { 95 | return; 96 | } 97 | 98 | // Iterate forward until we find the next function on the same level 99 | for (let i = currentSymbolIndex + 1; i < this.tree.length; i++) { 100 | if (this.tree[i].kind === vscode.SymbolKind.Function) { 101 | this.symbolIndex = i; 102 | break; 103 | } 104 | } 105 | 106 | const symbol = this.tree[this.symbolIndex]; 107 | if (symbol) { 108 | return symbol.range; 109 | } 110 | } 111 | 112 | getPreviousFunctionRange(editor: vscode.TextEditor): vscode.Range | undefined { 113 | if (this.tree.length === 0 || this.dirtyTree) { 114 | return; 115 | } 116 | 117 | const activeCursor = editor.selection.active; 118 | const currentSymbolIndex = this.getContainingSymbolIndex(activeCursor); 119 | if (currentSymbolIndex === undefined) { 120 | return; 121 | } 122 | 123 | // Iterate backwards until we find the previouis function on the same level 124 | for (let i = currentSymbolIndex - 1; i > 0; i--) { 125 | if (this.tree[i].kind === vscode.SymbolKind.Function) { 126 | this.symbolIndex = i; 127 | break; 128 | } 129 | } 130 | 131 | const symbol = this.tree[this.symbolIndex]; 132 | if (symbol) { 133 | return symbol.range; 134 | } 135 | } 136 | } 137 | 138 | export const symbolProvider = new SymbolProvider(); 139 | -------------------------------------------------------------------------------- /src/modes.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { HelixState } from './helix_state_types'; 4 | import { Mode } from './modes_types'; 5 | import { removeTypeSubscription } from './type_subscription'; 6 | 7 | export function enterInsertMode(helixState: HelixState, before = true): void { 8 | // To fix https://github.com/jasonwilliams/vscode-helix/issues/14 we should clear selections on entering insert mode 9 | // Helix doesn't clear selections on insert but doesn't overwrite the selection either, so our best option is to just clear them 10 | const editor = helixState.editorState.activeEditor!; 11 | 12 | editor.selections = editor.selections.map((selection) => { 13 | const position = before ? selection.start : selection.end; 14 | return new vscode.Selection(position, position); 15 | }); 16 | 17 | helixState.mode = Mode.Insert; 18 | setModeContext('extension.helixKeymap.insertMode'); 19 | helixState.commandLine.setText('', helixState); 20 | } 21 | 22 | export function enterNormalMode(helixState: HelixState): void { 23 | helixState.mode = Mode.Normal; 24 | setModeContext('extension.helixKeymap.normalMode'); 25 | helixState.commandLine.setText('', helixState); 26 | } 27 | 28 | export function enterSearchMode(helixState: HelixState): void { 29 | helixState.mode = Mode.SearchInProgress; 30 | setModeContext('extension.helixKeymap.searchMode'); 31 | helixState.commandLine.setText('', helixState); 32 | } 33 | 34 | export function enterCommandMode(helixState: HelixState): void { 35 | helixState.mode = Mode.CommandlineInProgress; 36 | setModeContext('extension.helixKeymap.commandMode'); 37 | helixState.commandLine.setText('', helixState); 38 | } 39 | 40 | export function enterSelectMode(helixState: HelixState): void { 41 | helixState.mode = Mode.Select; 42 | setModeContext('extension.helixKeymap.selectMode'); 43 | helixState.commandLine.setText('', helixState); 44 | } 45 | 46 | export function enterWindowMode(helixState: HelixState): void { 47 | helixState.mode = Mode.Window; 48 | setModeContext('extension.helixKeymap.windowMode'); 49 | helixState.commandLine.setText('', helixState); 50 | } 51 | 52 | export function enterVisualMode(helixState: HelixState): void { 53 | helixState.mode = Mode.Visual; 54 | setModeContext('extension.helixKeymap.visualMode'); 55 | helixState.commandLine.setText('', helixState); 56 | } 57 | 58 | export function enterVisualLineMode(helixState: HelixState): void { 59 | helixState.mode = Mode.VisualLine; 60 | setModeContext('extension.helixKeymap.visualLineMode'); 61 | } 62 | 63 | export function enterViewMode(helixState: HelixState): void { 64 | helixState.mode = Mode.View; 65 | setModeContext('extension.helixKeymap.viewMode'); 66 | helixState.commandLine.setText('', helixState); 67 | } 68 | 69 | export function enterDisabledMode(helixState: HelixState): void { 70 | helixState.mode = Mode.Disabled; 71 | setModeCursorStyle(helixState.mode, helixState.editorState.activeEditor!); 72 | setRelativeLineNumbers(helixState.mode, helixState.editorState.activeEditor!); 73 | removeTypeSubscription(helixState); 74 | setModeContext('extension.helixKeymap.disabledMode'); 75 | helixState.commandLine.setText('', helixState); 76 | } 77 | 78 | function setModeContext(key: string) { 79 | const modeKeys = [ 80 | 'extension.helixKeymap.insertMode', 81 | 'extension.helixKeymap.normalMode', 82 | 'extension.helixKeymap.visualMode', 83 | 'extension.helixKeymap.visualLineMode', 84 | 'extension.helixKeymap.searchMode', 85 | 'extension.helixKeymap.selectMode', 86 | 'extension.helixKeymap.viewMode', 87 | 'extension.helixKeymap.disabledMode', 88 | 'extension.helixKeymap.commandMode', 89 | ]; 90 | 91 | modeKeys.forEach((modeKey) => { 92 | vscode.commands.executeCommand('setContext', modeKey, key === modeKey); 93 | }); 94 | } 95 | 96 | export function setModeCursorStyle(mode: Mode, editor: vscode.TextEditor): void { 97 | if (mode === Mode.Insert || mode === Mode.Occurrence || mode === Mode.Disabled) { 98 | editor.options.cursorStyle = vscode.TextEditorCursorStyle.Line; 99 | } else if (mode === Mode.Normal) { 100 | editor.options.cursorStyle = vscode.TextEditorCursorStyle.Block; 101 | } else if (mode === Mode.Visual || mode === Mode.VisualLine) { 102 | editor.options.cursorStyle = vscode.TextEditorCursorStyle.Block; 103 | } 104 | } 105 | 106 | export function setRelativeLineNumbers(mode: Mode, editor: vscode.TextEditor): void { 107 | const config = vscode.workspace.getConfiguration('helixKeymap'); 108 | const isEnabledToggleRelativeLineNumbers = config.get('toggleRelativeLineNumbers', false); 109 | 110 | if (!isEnabledToggleRelativeLineNumbers) { 111 | return; 112 | } 113 | 114 | if (mode === Mode.Insert) { 115 | editor.options.lineNumbers = vscode.TextEditorLineNumbersStyle.On; 116 | } else { 117 | editor.options.lineNumbers = vscode.TextEditorLineNumbersStyle.Relative; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/actions/gotoMode.ts: -------------------------------------------------------------------------------- 1 | import { Selection, commands, window } from 'vscode'; 2 | import { Action } from '../action_types'; 3 | import { Mode } from '../modes_types'; 4 | import { parseKeysExact } from '../parse_keys'; 5 | 6 | // https://docs.helix-editor.com/keymap.html#goto-mode 7 | export const gotoActions: Action[] = [ 8 | // G actions 9 | parseKeysExact(['g', '.'], [Mode.Normal], () => { 10 | commands.executeCommand('workbench.action.navigateToLastEditLocation'); 11 | }), 12 | 13 | parseKeysExact(['g', 'e'], [Mode.Normal], () => { 14 | commands.executeCommand('cursorBottom'); 15 | }), 16 | 17 | parseKeysExact(['g', 'e'], [Mode.Visual], () => { 18 | commands.executeCommand('cursorBottomSelect'); 19 | }), 20 | 21 | parseKeysExact(['g', 'g'], [Mode.Normal], (helixState, editor) => { 22 | const count = helixState.resolveCount(); 23 | if (count !== 1) { 24 | const range = editor.document.lineAt(count - 1).range; 25 | editor.selection = new Selection(range.start, range.end); 26 | editor.revealRange(range); 27 | return; 28 | } 29 | 30 | commands.executeCommand('cursorTop'); 31 | }), 32 | 33 | parseKeysExact(['g', 'g'], [Mode.Visual], (helixState, editor) => { 34 | const count = helixState.resolveCount(); 35 | if (count !== 1) { 36 | const position = editor.selection.active; 37 | const range = editor.document.lineAt(count - 1).range; 38 | if (position.isBefore(range.start)) { 39 | editor.selection = new Selection(position, range.end); 40 | } else { 41 | editor.selection = new Selection(position, range.start); 42 | } 43 | return; 44 | } 45 | 46 | commands.executeCommand('cursorTopSelect'); 47 | }), 48 | 49 | parseKeysExact(['g', 'h'], [Mode.Normal], () => { 50 | commands.executeCommand('cursorLineStart'); 51 | }), 52 | 53 | parseKeysExact(['g', 'h'], [Mode.Visual], () => { 54 | commands.executeCommand('cursorLineStartSelect'); 55 | }), 56 | 57 | parseKeysExact(['g', 'l'], [Mode.Normal], () => { 58 | commands.executeCommand('cursorLineEnd'); 59 | }), 60 | 61 | parseKeysExact(['g', 'l'], [Mode.Visual], () => { 62 | commands.executeCommand('cursorLineEndSelect'); 63 | }), 64 | 65 | parseKeysExact(['g', 's'], [Mode.Normal], () => { 66 | commands.executeCommand('cursorHome'); 67 | }), 68 | 69 | parseKeysExact(['g', 's'], [Mode.Visual], () => { 70 | commands.executeCommand('cursorHomeSelect'); 71 | }), 72 | 73 | parseKeysExact(['g', 'd'], [Mode.Normal], () => { 74 | commands.executeCommand('editor.action.revealDefinition'); 75 | }), 76 | 77 | parseKeysExact(['g', 'i'], [Mode.Normal], () => { 78 | commands.executeCommand('editor.action.goToImplementation'); 79 | }), 80 | 81 | parseKeysExact(['g', 'y'], [Mode.Normal], () => { 82 | commands.executeCommand('editor.action.goToTypeDefinition'); 83 | }), 84 | 85 | parseKeysExact(['g', 'r'], [Mode.Normal], () => { 86 | commands.executeCommand('editor.action.goToReferences'); 87 | }), 88 | 89 | parseKeysExact(['g', 't'], [Mode.Normal], () => { 90 | commands.executeCommand('cursorPageUp'); 91 | }), 92 | 93 | parseKeysExact(['g', 't'], [Mode.Visual], () => { 94 | commands.executeCommand('cursorPageUpSelect'); 95 | }), 96 | 97 | parseKeysExact(['g', 'b'], [Mode.Normal], () => { 98 | commands.executeCommand('cursorPageDown'); 99 | }), 100 | 101 | parseKeysExact(['g', 'b'], [Mode.Visual], () => { 102 | commands.executeCommand('cursorPageDownSelect'); 103 | }), 104 | 105 | parseKeysExact(['g', 'c'], [Mode.Normal], () => { 106 | commands.executeCommand('cursorMove', { 107 | to: 'viewPortCenter', 108 | }); 109 | }), 110 | 111 | parseKeysExact(['g', 'c'], [Mode.Visual], () => { 112 | commands.executeCommand('cursorMove', { 113 | to: 'viewPortCenter', 114 | select: true, 115 | }); 116 | }), 117 | 118 | parseKeysExact(['g', 'k'], [Mode.Normal], () => { 119 | commands.executeCommand('scrollLineUp'); 120 | }), 121 | 122 | parseKeysExact(['g', 'j'], [Mode.Normal], () => { 123 | commands.executeCommand('scrollLineDown'); 124 | }), 125 | 126 | parseKeysExact(['g', 'a'], [Mode.Normal], (helixState) => { 127 | // VS Code has no concept of "last accessed file" so instead we'll need to keep track of previous text editors 128 | const editor = helixState.editorState.previousEditor; 129 | if (!editor) return; 130 | 131 | window.showTextDocument(editor.document); 132 | }), 133 | 134 | parseKeysExact(['g', 'm'], [Mode.Normal], (helixState) => { 135 | // VS Code has no concept of "last accessed file" so instead we'll need to keep track of previous text editors 136 | const document = helixState.editorState.lastModifiedDocument; 137 | if (!document) return; 138 | 139 | window.showTextDocument(document); 140 | }), 141 | 142 | parseKeysExact(['g', 'n'], [Mode.Normal], () => { 143 | commands.executeCommand('workbench.action.nextEditor'); 144 | }), 145 | 146 | parseKeysExact(['g', 'p'], [Mode.Normal], () => { 147 | commands.executeCommand('workbench.action.previousEditor'); 148 | }), 149 | ]; 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Helix for VS Code 2 | 3 |
4 | 5 |   6 |   7 | 8 |
9 |
10 | This is a Visual Studio Code implementation of Helix Keybindings and Commands. I created this because I wanted to use Helix in VS Code and was frustrated with the built-in commands not being ergonomic. I didn't want to move fully unto helix and lose the benefits of VS Code, so I decided to create this extension. 11 | 12 | It is a work in progress, feel free to raise issues or contribute on the issue tracker. 13 | 14 | ## Installation 15 | 16 | You can install the extension from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=jasew.vscode-helix-emulation) or [Open VSX](https://open-vsx.org/extension/jasew/vscode-helix-emulation). 17 | 18 | ## Commands 19 | 20 | The main commands should work but selections currently do not. 21 | 22 | Right now commands are hardcoded to the [default keymap](https://docs.helix-editor.com/keymap.html), hopefully this can be adjusted to the user's keymap in the future. 23 | 24 | If something doesn't work, please open an issue. 25 | 26 | ## Performance 27 | 28 | For added performance, you can try adding the following [setting](https://github.com/microsoft/vscode/issues/75627#issuecomment-1078827311), and reload/restart VSCode: 29 | 30 | ```json 31 | "extensions.experimental.affinity": { 32 | "jasew.vscode-helix-emulation": 1 33 | } 34 | ``` 35 | 36 | ## Differences 37 | 38 | There will be some differences between this extension and native Helix, these will be due: 39 | 40 | - Both are visually different and offer different features (such as VS Code having multiple windows and tabs rather than buffers) 41 | - VS Code not having TreeSitter or an AST, so we need to find other ways of achieving the same action. 42 | - Additional keybindings to match more how VS Code works 43 | 44 | ### Window Mode 45 | 46 | Due to VS Code having tabs I've needed to add some new windows modes on top of the current Helix ones, these commands are based 47 | around movements, (i.e moving the editor from one window to another). 48 | 49 | | Command | Description | 50 | | ---------------- | ------------------------------------------------------- | 51 | | `ctrl + w, m, v` | Move editor to the next group vertically (to the right) | 52 | | `ctrl + w, m, s` | Move editor to the next group horizontally (below) | 53 | | `ctrl + w, m, p` | Move editor back to the previous group | 54 | | `ctrl + w, m, w` | Move editor out into a new window (experimental) | 55 | | `ctrl + w, m, j` | Rejoin editor with main window (experimental) | 56 | | `ctrl + w, c` | Close window (alias to ctrl + w, q) | 57 | | `ctrl + w, n` | New untitled editor (prev ctrl+n/cmd+n in VS Code) | 58 | | `ctrl + w, b` | Toggle sidebar visibility (prev ctrl+b in VS Code) | 59 | 60 | Most of the differences will be related to the fact VSCode doesn't have TreeSitter or have access to an AST. So we often need to find other ways of achieving the same action. 61 | 62 | - `mif/maf` both do the same, they will select the outer function range. Getting the inner function body isn't achievable because LSP doesn't give us that, and we can't hardcode blocks (incompatibilty with python for example) 63 | 64 | ### Movements 65 | 66 | | Command | Description | 67 | | --------- | --------------------------------------------- | 68 | | `alt + k` | Move lines or selection up | 69 | | `alt + j` | Move lines or selection down | 70 | | `alt + d` | Add selection to next match (same as ctrl+d) | 71 | | `alt + m` | Move selection to next match (same as ctrl+k) | 72 | 73 | ### Line Number Toggle 74 | 75 | The extension includes a line number toggle feature that automatically switches between relative and absolute line numbers based on the current editor mode: 76 | 77 | - **Normal, Visual, and Visual Line modes**: Shows relative line numbers for easier navigation with motions like `10j` or `5k` 78 | - **Insert mode**: Shows absolute line numbers for clearer positioning context 79 | 80 | To enable this feature, add the following setting to your VS Code configuration: 81 | 82 | ```json 83 | "helixKeymap.toggleRelativeLineNumbers": true 84 | ``` 85 | 86 | This behaviour matches Helix's approach to line numbers, helping you take advantage of relative line numbers for efficient movement commands while providing absolute numbers when editing text. 87 | 88 | ## Outstanding 89 | 90 | Feel free to pick up any of these if you wanted to contribute. 91 | 92 | - [#3](https://github.com/jasonwilliams/vscode-helix/issues/3) Commit checkpoints, extensions don't have access to VS Code's back/forward stack. So it would need to have its own stack in memory which can be manipulated. 93 | - [#4](https://github.com/jasonwilliams/vscode-helix/issues/4) Custom keymaps, currently keymaps are matching Helix 94 | 95 | ## Insert Mode 96 | 97 | - `ctrl + k` (delete to end of line) needs to be `ctrl + l` instead due to it conflicting with VS Code's chord mode which extensions can't turn off. 98 | -------------------------------------------------------------------------------- /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 | export function aroundIndentLevelRange( 23 | document: vscode.TextDocument, 24 | lineNumber: number, 25 | ): SimpleRange { 26 | const indentLevel = findIndentLevel(document, lineNumber) 27 | const rangeStart = aroundIndentLevelRangeBefore(document, lineNumber, indentLevel) 28 | const rangeEnd = aroundIndentLevelRangeAfter(document, lineNumber + 1, indentLevel) 29 | 30 | if (rangeStart && rangeEnd) { 31 | return { start: rangeStart.start, end: rangeEnd.end } 32 | } else if (rangeStart) { 33 | return rangeStart 34 | } else if (rangeEnd) { 35 | return rangeEnd 36 | } else { 37 | // This will never happen but the typechecker can't know that 38 | return { start: lineNumber, end: lineNumber } 39 | } 40 | } 41 | 42 | function aroundIndentLevelRangeBefore( 43 | document: vscode.TextDocument, 44 | lineNumber: number, 45 | indentLevel: number, 46 | ): SimpleRange | undefined { 47 | let result 48 | const insideRange = indentLevelRangeBefore(document, lineNumber, indentLevel) 49 | if (typeof insideRange !== 'undefined') { 50 | result = insideRange 51 | for (let i = Math.max(0, insideRange.start - 1); i >= 0; --i) { 52 | const line = document.lineAt(i) 53 | 54 | if (!line.isEmptyOrWhitespace) { 55 | result.start = i 56 | } else { 57 | return result 58 | } 59 | } 60 | } 61 | 62 | return result 63 | } 64 | 65 | function indentLevelRangeBefore( 66 | document: vscode.TextDocument, 67 | lineNumber: number, 68 | indentLevel: number, 69 | ): SimpleRange | undefined { 70 | let result 71 | 72 | for (let i = lineNumber; i >= 0; --i) { 73 | const line = document.lineAt(i) 74 | 75 | if (line.firstNonWhitespaceCharacterIndex >= indentLevel) { 76 | // if (!line.isEmptyOrWhitespace) { 77 | if (result) { 78 | result.start = i 79 | } else { 80 | result = { start: i, end: i } 81 | } 82 | // } 83 | } else { 84 | if (!line.isEmptyOrWhitespace) { 85 | return result 86 | } 87 | } 88 | } 89 | 90 | return result 91 | } 92 | 93 | function aroundIndentLevelRangeAfter( 94 | document: vscode.TextDocument, 95 | lineNumber: number, 96 | indentLevel: number, 97 | ): SimpleRange | undefined { 98 | let result 99 | const insideRange = indentLevelRangeAfter(document, lineNumber, indentLevel) 100 | if (typeof insideRange !== 'undefined') { 101 | result = insideRange 102 | for (let i = insideRange.end + 1; i < document.lineCount; ++i) { 103 | const line = document.lineAt(i) 104 | 105 | if (!line.isEmptyOrWhitespace) { 106 | result.end = i 107 | } else { 108 | return result 109 | } 110 | } 111 | } 112 | 113 | return result 114 | } 115 | 116 | function indentLevelRangeAfter( 117 | document: vscode.TextDocument, 118 | lineNumber: number, 119 | indentLevel: number, 120 | ): SimpleRange | undefined { 121 | let result 122 | 123 | for (let i = lineNumber; i < document.lineCount; ++i) { 124 | const line = document.lineAt(i) 125 | 126 | if (line.firstNonWhitespaceCharacterIndex >= indentLevel) { 127 | // if (!line.isEmptyOrWhitespace) { 128 | if (result) { 129 | result.end = i 130 | } else { 131 | result = { start: i, end: i } 132 | } 133 | // } 134 | } else { 135 | if (!line.isEmptyOrWhitespace) { 136 | return result 137 | } 138 | } 139 | } 140 | 141 | return result 142 | } 143 | 144 | function findIndentLevel(document: vscode.TextDocument, lineNumber: number) { 145 | const line = document.lineAt(lineNumber) 146 | 147 | if (!line.isEmptyOrWhitespace) { 148 | return line.firstNonWhitespaceCharacterIndex 149 | } 150 | 151 | return Math.max( 152 | findIndentLevelForward(document, lineNumber + 1), 153 | findIndentLevelBackward(document, lineNumber - 1), 154 | ) 155 | } 156 | 157 | function findIndentLevelForward(document: vscode.TextDocument, lineNumber: number): number { 158 | for (let i = lineNumber; i < document.lineCount; ++i) { 159 | const line = document.lineAt(i) 160 | 161 | if (!line.isEmptyOrWhitespace) { 162 | return line.firstNonWhitespaceCharacterIndex 163 | } 164 | } 165 | 166 | return 0 167 | } 168 | 169 | function findIndentLevelBackward(document: vscode.TextDocument, lineNumber: number): number { 170 | for (let i = lineNumber; i >= 0; --i) { 171 | const line = document.lineAt(i) 172 | 173 | if (!line.isEmptyOrWhitespace) { 174 | return line.firstNonWhitespaceCharacterIndex 175 | } 176 | } 177 | 178 | return 0 179 | } 180 | -------------------------------------------------------------------------------- /src/actions/matchMode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Action } from '../action_types'; 3 | import { enterInsertMode, setModeCursorStyle, setRelativeLineNumbers } from '../modes'; 4 | import { Mode } from '../modes_types'; 5 | import { parseKeysExact, parseKeysRegex } from '../parse_keys'; 6 | import { searchBackwardBracket, searchForwardBracket } from '../search_utils'; 7 | import { removeTypeSubscription } from '../type_subscription'; 8 | import { delete_, yank } from './operators'; 9 | 10 | export const matchActions: Action[] = [ 11 | // Implemenent jump to bracket 12 | parseKeysExact(['m', 'm'], [Mode.Normal], () => { 13 | vscode.commands.executeCommand('editor.action.jumpToBracket'); 14 | }), 15 | 16 | parseKeysExact(['m', 'm'], [Mode.Visual], () => { 17 | vscode.commands.executeCommand('editor.action.selectToBracket'); 18 | }), 19 | 20 | // Delete match 21 | parseKeysExact(['d'], [Mode.Normal, Mode.Visual], (helixState, editor) => { 22 | const ranges = editor.selections.map((selection) => selection.with()); 23 | yank(helixState, editor, ranges, false); 24 | delete_(editor, ranges, false); 25 | }), 26 | 27 | // edit match 28 | parseKeysExact(['c'], [Mode.Normal, Mode.Visual], (helixState, editor) => { 29 | const ranges = editor.selections.map((selection) => selection.with()); 30 | delete_(editor, ranges, false); 31 | enterInsertMode(helixState); 32 | setModeCursorStyle(helixState.mode, editor); 33 | setRelativeLineNumbers(helixState.mode, editor); 34 | removeTypeSubscription(helixState); 35 | }), 36 | 37 | // implement match add to selection 38 | parseKeysRegex(/^ms(.)$/, /^ms/, [Mode.Normal, Mode.Visual], (helixState, editor, match) => { 39 | const char = match[1]; 40 | const [startChar, endChar] = getMatchPairs(char); 41 | // Add char to both ends of each selection 42 | editor.edit((editBuilder) => { 43 | // Add char to both ends of each selection 44 | editor.selections.forEach((selection) => { 45 | const start = selection.start; 46 | const end = selection.end; 47 | editBuilder.insert(start, startChar); 48 | editBuilder.insert(end, endChar); 49 | }); 50 | }); 51 | }), 52 | 53 | // implement match replace to selection 54 | parseKeysRegex(/^mr(.)(.)$/, /^mr(.)?/, [Mode.Normal, Mode.Visual], (helixState, editor, match) => { 55 | const original = match[1]; 56 | const replacement = match[2]; 57 | const [startCharOrig, endCharOrig] = getMatchPairs(original); 58 | const [startCharNew, endCharNew] = getMatchPairs(replacement); 59 | const num = helixState.resolveCount(); 60 | 61 | const forwardPosition = searchForwardBracket( 62 | editor.document, 63 | startCharOrig, 64 | endCharOrig, 65 | editor.selection.active, 66 | num, 67 | ); 68 | const backwardPosition = searchBackwardBracket( 69 | editor.document, 70 | startCharOrig, 71 | endCharOrig, 72 | editor.selection.active, 73 | num, 74 | ); 75 | 76 | if (forwardPosition === undefined || backwardPosition === undefined) return; 77 | 78 | // Add char to both ends of each selection 79 | editor.edit((editBuilder) => { 80 | // Add char to both ends of each selection 81 | editBuilder.replace( 82 | new vscode.Range(forwardPosition, forwardPosition.with({ character: forwardPosition.character + 1 })), 83 | endCharNew, 84 | ); 85 | editBuilder.replace( 86 | new vscode.Range(backwardPosition, backwardPosition.with({ character: backwardPosition.character + 1 })), 87 | startCharNew, 88 | ); 89 | }); 90 | }), 91 | 92 | // implement match delete character 93 | parseKeysRegex(/^md(.)$/, /^md/, [Mode.Normal, Mode.Visual], (helixState, editor, match) => { 94 | const char = match[1]; 95 | const [startChar, endChar] = getMatchPairs(char); 96 | const num = helixState.resolveCount(); 97 | 98 | const forwardPosition = searchForwardBracket(editor.document, startChar, endChar, editor.selection.active, num); 99 | const backwardPosition = searchBackwardBracket(editor.document, startChar, endChar, editor.selection.active, num); 100 | 101 | if (forwardPosition === undefined || backwardPosition === undefined) return; 102 | 103 | // Add char to both ends of each selection 104 | editor.edit((editBuilder) => { 105 | // Add char to both ends of each selection 106 | editBuilder.delete( 107 | new vscode.Range(forwardPosition, forwardPosition.with({ character: forwardPosition.character + 1 })), 108 | ); 109 | editBuilder.delete( 110 | new vscode.Range(backwardPosition, backwardPosition.with({ character: backwardPosition.character + 1 })), 111 | ); 112 | }); 113 | }), 114 | ]; 115 | 116 | const getMatchPairs = (char: string) => { 117 | let startChar: string; 118 | let endChar: string; 119 | if (['{', '}'].includes(char)) { 120 | startChar = '{'; 121 | endChar = '}'; 122 | } else if (['[', ']'].includes(char)) { 123 | startChar = '['; 124 | endChar = ']'; 125 | } else if (['(', ')'].includes(char)) { 126 | startChar = '('; 127 | endChar = ')'; 128 | } else if (['<', '>'].includes(char)) { 129 | startChar = '<'; 130 | endChar = '>'; 131 | } else { 132 | // Otherwise, startChar and endChar should be the same character 133 | startChar = char; 134 | endChar = char; 135 | } 136 | 137 | return [startChar, endChar]; 138 | }; 139 | -------------------------------------------------------------------------------- /src/actions/operators.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Action } from '../action_types'; 4 | import { HelixState } from '../helix_state_types'; 5 | import { enterNormalMode, setModeCursorStyle, setRelativeLineNumbers } from '../modes'; 6 | import { Mode } from '../modes_types'; 7 | import { parseKeysOperator } from '../parse_keys'; 8 | import { operatorRanges } from './operator_ranges'; 9 | 10 | export const operators: Action[] = [ 11 | parseKeysOperator(['d'], operatorRanges, (vimState, editor, ranges, linewise) => { 12 | if (ranges.every((x) => x === undefined)) return; 13 | 14 | cursorsToRangesStart(editor, ranges); 15 | 16 | delete_(editor, ranges, linewise); 17 | 18 | if (vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) { 19 | enterNormalMode(vimState); 20 | setModeCursorStyle(vimState.mode, editor); 21 | setRelativeLineNumbers(vimState.mode, editor); 22 | } 23 | }), 24 | 25 | // Match Mode 26 | parseKeysOperator(['m'], operatorRanges, (vimState, editor, ranges) => { 27 | if (ranges.every((x) => x === undefined)) { 28 | return; 29 | } 30 | 31 | editor.selections = ranges.map((range, i) => { 32 | if (range) { 33 | const start = range.start; 34 | const end = range.end; 35 | return new vscode.Selection(start, end); 36 | } else { 37 | return editor.selections[i]; 38 | } 39 | }); 40 | setModeCursorStyle(vimState.mode, editor); 41 | setRelativeLineNumbers(vimState.mode, editor); 42 | }), 43 | 44 | parseKeysOperator(['q'], operatorRanges, (vimState, editor, ranges, _linewise) => { 45 | if (ranges.every((x) => x === undefined) || vimState.mode === Mode.Visual || vimState.mode === Mode.VisualLine) { 46 | return; 47 | } 48 | 49 | editor.selections = ranges.map((range, i) => { 50 | if (range) { 51 | const start = range.start; 52 | const end = range.end; 53 | return new vscode.Selection(start, end); 54 | } else { 55 | return editor.selections[i]; 56 | } 57 | }); 58 | 59 | vscode.commands.executeCommand('editor.action.copyLinesDownAction'); 60 | }), 61 | ]; 62 | 63 | function cursorsToRangesStart(editor: vscode.TextEditor, ranges: readonly (vscode.Range | undefined)[]) { 64 | editor.selections = editor.selections.map((selection, i) => { 65 | const range = ranges[i]; 66 | 67 | if (range) { 68 | const newPosition = range.start; 69 | return new vscode.Selection(newPosition, newPosition); 70 | } else { 71 | return selection; 72 | } 73 | }); 74 | } 75 | 76 | export function delete_(editor: vscode.TextEditor, ranges: readonly (vscode.Range | undefined)[], linewise: boolean) { 77 | if (ranges.length === 1 && ranges[0] && isEmptyRange(ranges[0])) { 78 | vscode.commands.executeCommand('deleteRight'); 79 | return; 80 | } 81 | 82 | editor 83 | .edit((editBuilder) => { 84 | ranges.forEach((range) => { 85 | if (!range) return; 86 | 87 | let deleteRange = range; 88 | 89 | if (linewise) { 90 | const start = range.start; 91 | const end = range.end; 92 | 93 | if (end.line === editor.document.lineCount - 1) { 94 | if (start.line === 0) { 95 | deleteRange = new vscode.Range(start.with({ character: 0 }), end); 96 | } else { 97 | deleteRange = new vscode.Range( 98 | new vscode.Position(start.line - 1, editor.document.lineAt(start.line - 1).text.length), 99 | end, 100 | ); 101 | } 102 | } else { 103 | deleteRange = new vscode.Range(range.start, new vscode.Position(end.line + 1, 0)); 104 | } 105 | } 106 | 107 | editBuilder.delete(deleteRange); 108 | }); 109 | }) 110 | .then(() => { 111 | // For linewise deletions, make sure cursor is at beginning of line 112 | editor.selections = editor.selections.map((selection, i) => { 113 | const range = ranges[i]; 114 | 115 | if (range && linewise) { 116 | const newPosition = selection.start.with({ character: 0 }); 117 | return new vscode.Selection(newPosition, newPosition); 118 | } else { 119 | return selection; 120 | } 121 | }); 122 | }); 123 | } 124 | 125 | export function yank( 126 | vimState: HelixState, 127 | editor: vscode.TextEditor, 128 | ranges: (vscode.Range | undefined)[], 129 | linewise: boolean, 130 | ) { 131 | vimState.registers = { 132 | contentsList: ranges.map((range, i) => { 133 | if (range) { 134 | return editor.document.getText(range); 135 | } else { 136 | return vimState.registers.contentsList[i]; 137 | } 138 | }), 139 | linewise: linewise, 140 | }; 141 | } 142 | 143 | // detect if a range is covering just a single character 144 | function isEmptyRange(range: vscode.Range) { 145 | return range.start.line === range.end.line && range.start.character === range.end.character; 146 | } 147 | 148 | // detect if the range spans a whole line and only one line 149 | // Theres a weird issue where the cursor jumps to the next line when doing expand line selection 150 | // https://github.com/microsoft/vscode/issues/118015#issuecomment-854964022 151 | export function isSingleLineRange(range: vscode.Range): boolean { 152 | return range.start.line === range.end.line && range.start.character === 0 && range.end.character === 0; 153 | } 154 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { symbolProvider } from './SymbolProvider'; 4 | import { decrement, incremenet, switchToUppercase } from './actions/actions'; 5 | import { commandLine } from './commandLine'; 6 | import { escapeHandler } from './escape_handler'; 7 | import { onDidChangeActiveTextEditor, onDidChangeTextDocument } from './eventHandlers'; 8 | import { HelixState } from './helix_state_types'; 9 | import { 10 | enterDisabledMode, 11 | enterNormalMode, 12 | enterSearchMode, 13 | enterWindowMode, 14 | setModeCursorStyle, 15 | setRelativeLineNumbers, 16 | } from './modes'; 17 | import { Mode } from './modes_types'; 18 | import * as scrollCommands from './scroll_commands'; 19 | import { searchState } from './search'; 20 | import { flipSelection } from './selection_utils'; 21 | import { typeHandler } from './type_handler'; 22 | import { addTypeSubscription, removeTypeSubscription } from './type_subscription'; 23 | 24 | const globalhelixState: HelixState = { 25 | typeSubscription: undefined, 26 | mode: Mode.Normal, 27 | keysPressed: [], 28 | numbersPressed: [], 29 | resolveCount: function () { 30 | // We can resolve this lazily as not every function will need it 31 | // So we don't want it running on every keystroke or every command 32 | return parseInt(this.numbersPressed.join(''), 10) || 1; 33 | }, 34 | registers: { 35 | contentsList: [], 36 | linewise: true, 37 | }, 38 | symbolProvider, 39 | editorState: { 40 | activeEditor: undefined, 41 | previousEditor: undefined, 42 | lastModifiedDocument: undefined, 43 | }, 44 | commandLine, 45 | searchState, 46 | currentSelection: null, 47 | repeatLastMotion: () => undefined, 48 | lastPutRanges: { 49 | ranges: [], 50 | linewise: true, 51 | }, 52 | }; 53 | 54 | /** This is the main entry point into the Helix VSCode extension */ 55 | export function activate(context: vscode.ExtensionContext): void { 56 | context.subscriptions.push( 57 | // vscode.window.onDidChangeTextEditorSelection((e) => onSelectionChange(globalhelixState, e)), 58 | vscode.window.onDidChangeActiveTextEditor((editor) => onDidChangeActiveTextEditor(globalhelixState, editor)), 59 | vscode.workspace.onDidChangeTextDocument((e) => onDidChangeTextDocument(globalhelixState, e)), 60 | vscode.commands.registerCommand('extension.helixKeymap.escapeKey', () => escapeHandler(globalhelixState)), 61 | vscode.commands.registerCommand('extension.helixKeymap.scrollDownHalfPage', scrollCommands.scrollDownHalfPage), 62 | vscode.commands.registerCommand('extension.helixKeymap.scrollUpHalfPage', scrollCommands.scrollUpHalfPage), 63 | vscode.commands.registerCommand('extension.helixKeymap.scrollDownPage', scrollCommands.scrollDownPage), 64 | vscode.commands.registerCommand('extension.helixKeymap.scrollUpPage', scrollCommands.scrollUpPage), 65 | vscode.commands.registerCommand('extension.helixKeymap.enterSearchMode', () => enterSearchMode(globalhelixState)), 66 | vscode.commands.registerCommand('extension.helixKeymap.exitSearchMode', () => 67 | globalhelixState.searchState.enter(globalhelixState), 68 | ), 69 | vscode.commands.registerCommand('extension.helixKeymap.enterWindowMode', () => enterWindowMode(globalhelixState)), 70 | vscode.commands.registerCommand('extension.helixKeymap.backspaceSearchMode', () => { 71 | globalhelixState.searchState.backspace(globalhelixState); 72 | }), 73 | vscode.commands.registerCommand('extension.helixKeymap.backspaceCommandMode', () => { 74 | globalhelixState.commandLine.backspace(globalhelixState); 75 | }), 76 | vscode.commands.registerCommand('extension.helixKeymap.nextSearchResult', () => 77 | globalhelixState.searchState.nextSearchResult(globalhelixState), 78 | ), 79 | vscode.commands.registerCommand('extension.helixKeymap.previousSearchResult', () => 80 | globalhelixState.searchState.previousSearchResult(globalhelixState), 81 | ), 82 | vscode.commands.registerCommand('extension.helixKeymap.enterDisabledMode', () => { 83 | enterDisabledMode(globalhelixState); 84 | }), 85 | vscode.commands.registerCommand('extension.helixKeymap.enableHelix', () => { 86 | enterNormalMode(globalhelixState); 87 | setModeCursorStyle(globalhelixState.mode, vscode.window.activeTextEditor!); 88 | setRelativeLineNumbers(globalhelixState.mode, vscode.window.activeTextEditor!); 89 | addTypeSubscription(globalhelixState, typeHandler); 90 | }), 91 | vscode.commands.registerCommand('extension.helixKeymap.flipSelection', () => { 92 | flipSelection(vscode.window.activeTextEditor); 93 | }), 94 | vscode.commands.registerCommand('extension.helixKeymap.clipboardPasteAction', () => { 95 | vscode.env.clipboard.readText().then((text) => { 96 | globalhelixState.searchState.addText(globalhelixState, text); 97 | }); 98 | }), 99 | vscode.commands.registerCommand('extension.helixKeymap.repeatLastMotion', () => { 100 | globalhelixState.repeatLastMotion(globalhelixState, vscode.window.activeTextEditor!); 101 | }), 102 | vscode.commands.registerCommand('extension.helixKeymap.switchToUppercase', () => { 103 | switchToUppercase(vscode.window.activeTextEditor!); 104 | }), 105 | vscode.commands.registerCommand('extension.helixKeymap.increment', () => { 106 | incremenet(vscode.window.activeTextEditor!); 107 | }), 108 | vscode.commands.registerCommand('extension.helixKeymap.decrement', () => { 109 | decrement(vscode.window.activeTextEditor!); 110 | }), 111 | ); 112 | 113 | enterNormalMode(globalhelixState); 114 | addTypeSubscription(globalhelixState, typeHandler); 115 | 116 | if (vscode.window.activeTextEditor) { 117 | setModeCursorStyle(globalhelixState.mode, vscode.window.activeTextEditor); 118 | setRelativeLineNumbers(globalhelixState.mode, vscode.window.activeTextEditor); 119 | onDidChangeActiveTextEditor(globalhelixState, vscode.window.activeTextEditor); 120 | } 121 | } 122 | 123 | export function deactivate(): void { 124 | removeTypeSubscription(globalhelixState); 125 | } 126 | -------------------------------------------------------------------------------- /src/put_utils/put_after.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { HelixState } from '../helix_state_types'; 4 | import { enterNormalMode, setModeCursorStyle, setRelativeLineNumbers } from '../modes'; 5 | import { Mode } from '../modes_types'; 6 | import * as positionUtils from '../position_utils'; 7 | import { 8 | adjustInsertPositions, 9 | getInsertRangesFromBeginning, 10 | getInsertRangesFromEnd, 11 | getRegisterContentsList, 12 | } from './common'; 13 | 14 | export function putAfter(vimState: HelixState, 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: HelixState, 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 50 | .edit((editBuilder) => { 51 | insertPositions.forEach((position, i) => { 52 | const contents = insertContentsList[i]; 53 | if (contents === undefined) return; 54 | 55 | editBuilder.insert(position, contents); 56 | }); 57 | }) 58 | .then(() => { 59 | editor.selections = rangeBeginnings.map((position) => new vscode.Selection(position, position)); 60 | }); 61 | 62 | vimState.lastPutRanges = { 63 | ranges: getInsertRangesFromBeginning(rangeBeginnings, registerContentsList), 64 | linewise: true, 65 | }; 66 | } 67 | 68 | function normalModeCharacterwise( 69 | vimState: HelixState, 70 | editor: vscode.TextEditor, 71 | registerContentsList: (string | undefined)[], 72 | ) { 73 | const insertPositions = editor.selections.map((selection) => selection.end); 74 | const adjustedInsertPositions = adjustInsertPositions(insertPositions, registerContentsList); 75 | const insertRanges = getInsertRangesFromBeginning(adjustedInsertPositions, registerContentsList); 76 | 77 | editor 78 | .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 | }) 86 | .then(() => { 87 | editor.selections = editor.selections.map((selection, i) => { 88 | const range = insertRanges[i]; 89 | if (range === undefined) return selection; 90 | 91 | const position = positionUtils.left(range.end); 92 | return new vscode.Selection(position, position); 93 | }); 94 | }); 95 | 96 | vimState.lastPutRanges = { 97 | ranges: insertRanges, 98 | linewise: false, 99 | }; 100 | } 101 | 102 | function visualMode(vimState: HelixState, editor: vscode.TextEditor, registerContentsList: (string | undefined)[]) { 103 | const insertContentsList = vimState.registers.linewise 104 | ? registerContentsList.map((contents) => { 105 | if (!contents) return undefined; 106 | else return '\n' + contents + '\n'; 107 | }) 108 | : registerContentsList; 109 | 110 | editor 111 | .edit((editBuilder) => { 112 | editor.selections.forEach((selection, i) => { 113 | const contents = insertContentsList[i]; 114 | if (contents === undefined) return; 115 | 116 | editBuilder.delete(selection); 117 | editBuilder.insert(selection.start, contents); 118 | }); 119 | }) 120 | .then(() => { 121 | vimState.lastPutRanges = { 122 | ranges: getInsertRangesFromEnd( 123 | editor.document, 124 | editor.selections.map((selection) => selection.active), 125 | insertContentsList, 126 | ), 127 | linewise: vimState.registers.linewise, 128 | }; 129 | 130 | editor.selections = editor.selections.map((selection) => { 131 | const newPosition = positionUtils.left(selection.active); 132 | return new vscode.Selection(newPosition, newPosition); 133 | }); 134 | }); 135 | 136 | enterNormalMode(vimState); 137 | setModeCursorStyle(vimState.mode, editor); 138 | setRelativeLineNumbers(vimState.mode, editor); 139 | } 140 | 141 | function visualLineMode(vimState: HelixState, editor: vscode.TextEditor, registerContentsList: (string | undefined)[]) { 142 | editor 143 | .edit((editBuilder) => { 144 | editor.selections.forEach((selection, i) => { 145 | const registerContents = registerContentsList[i]; 146 | if (registerContents === undefined) return; 147 | 148 | editBuilder.replace(selection, registerContents); 149 | }); 150 | }) 151 | .then(() => { 152 | vimState.lastPutRanges = { 153 | ranges: editor.selections.map((selection) => new vscode.Range(selection.start, selection.end)), 154 | linewise: vimState.registers.linewise, 155 | }; 156 | 157 | editor.selections = editor.selections.map((selection) => { 158 | return new vscode.Selection(selection.start, selection.start); 159 | }); 160 | 161 | enterNormalMode(vimState); 162 | setModeCursorStyle(vimState.mode, editor); 163 | setRelativeLineNumbers(vimState.mode, editor); 164 | }); 165 | } 166 | -------------------------------------------------------------------------------- /src/parse_keys.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Action } from './action_types'; 4 | import { HelixState } from './helix_state_types'; 5 | import { Mode } from './modes_types'; 6 | import { 7 | OperatorRange, 8 | ParseFailure, 9 | ParseKeysStatus, 10 | ParseOperatorPartSuccess, 11 | ParseOperatorRangeSuccess, 12 | } from './parse_keys_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: HelixState, 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: HelixState, 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: HelixState, 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: HelixState, 142 | editor: vscode.TextEditor, 143 | ranges: readonly (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: readonly (vscode.Range | undefined)[]; 154 | let linewise = true; 155 | if (vimState.mode === Mode.Window) { 156 | return ParseKeysStatus.NO; 157 | } 158 | 159 | if (vimState.mode === Mode.Normal || vimState.mode === Mode.Visual) { 160 | if (operatorResult.rest.length === 0) { 161 | return ParseKeysStatus.MORE_INPUT; 162 | } 163 | 164 | const motionResult = parseOperatorRangePart(vimState, operatorResult.rest, editor, motions); 165 | if (motionResult.kind === 'failure') { 166 | return motionResult.status; 167 | } 168 | 169 | ranges = motionResult.ranges; 170 | linewise = motionResult.linewise; 171 | } else if (vimState.mode === Mode.VisualLine) { 172 | ranges = editor.selections; 173 | linewise = true; 174 | } else { 175 | ranges = editor.selections; 176 | linewise = false; 177 | } 178 | 179 | operator(vimState, editor, ranges, linewise); 180 | return ParseKeysStatus.YES; 181 | }; 182 | } 183 | 184 | export function createOperatorRangeExactKeys( 185 | matchKeys: string[], 186 | linewise: boolean, 187 | f: (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined, 188 | ): OperatorRange { 189 | return (vimState, keys, editor) => { 190 | if (arrayEquals(keys, matchKeys)) { 191 | const ranges = editor.selections.map((selection) => { 192 | return f(vimState, editor.document, selection.active); 193 | }); 194 | return { 195 | kind: 'success', 196 | ranges: ranges, 197 | linewise: linewise, 198 | }; 199 | } else if (arrayStartsWith(keys, matchKeys)) { 200 | return { 201 | kind: 'failure', 202 | status: ParseKeysStatus.MORE_INPUT, 203 | }; 204 | } else { 205 | return { 206 | kind: 'failure', 207 | status: ParseKeysStatus.NO, 208 | }; 209 | } 210 | }; 211 | } 212 | 213 | export function createOperatorRangeRegex( 214 | doesPattern: RegExp, 215 | couldPattern: RegExp, 216 | linewise: boolean, 217 | f: ( 218 | vimState: HelixState, 219 | document: vscode.TextDocument, 220 | position: vscode.Position, 221 | match: RegExpMatchArray, 222 | ) => vscode.Range | undefined, 223 | ): OperatorRange { 224 | return (vimState, keys, editor) => { 225 | const keysStr = keys.join(''); 226 | const doesMatch = keysStr.match(doesPattern); 227 | 228 | if (doesMatch) { 229 | const ranges = editor.selections.map((selection) => { 230 | return f(vimState, editor.document, selection.active, doesMatch); 231 | }); 232 | return { 233 | kind: 'success', 234 | ranges: ranges, 235 | linewise: linewise, 236 | }; 237 | } else if (keysStr.match(couldPattern)) { 238 | return { 239 | kind: 'failure', 240 | status: ParseKeysStatus.MORE_INPUT, 241 | }; 242 | } else { 243 | return { 244 | kind: 'failure', 245 | status: ParseKeysStatus.NO, 246 | }; 247 | } 248 | }; 249 | } 250 | -------------------------------------------------------------------------------- /src/search.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { HelixState } from './helix_state_types'; 3 | import { enterNormalMode } from './modes'; 4 | import { Mode } from './modes_types'; 5 | 6 | // class which handles the search & select functionality 7 | export class SearchState { 8 | /** Current search string */ 9 | searchString = ''; 10 | /** List of previous search strings */ 11 | searchHistory: string[] = []; 12 | /** Index of the current search string in the search history */ 13 | searchHistoryIndex: number = this.searchHistory.length - 1; // Add this line 14 | /** Have we just come out of select mode? */ 15 | selectModeActive: boolean = false; 16 | /** Last active position before search */ 17 | lastActivePosition: vscode.Position | undefined; 18 | /** Initial position when search started */ 19 | initialSearchPosition: vscode.Position | undefined; 20 | 21 | // https://github.com/helix-editor/helix/issues/4978 22 | getFlags(): string { 23 | if (this.searchString.startsWith('(?i)')) { 24 | return 'gi'; 25 | } else if (this.searchString.startsWith('(?-i)')) { 26 | return 'g'; 27 | } 28 | 29 | return this.searchString === this.searchString.toLowerCase() ? 'gi' : 'g'; 30 | } 31 | 32 | getNormalisedSearchString(): string { 33 | if (this.searchString.startsWith('(?i)')) { 34 | return this.searchString.slice(4); 35 | } else if (this.searchString.startsWith('(?-i)')) { 36 | return this.searchString.slice(5); 37 | } 38 | 39 | return this.searchString; 40 | } 41 | 42 | clearSearchString(helixState: HelixState): void { 43 | this.searchString = ''; 44 | helixState.commandLine.setText(this.searchString, helixState); 45 | } 46 | 47 | /** Add character to search string */ 48 | addChar(helixState: HelixState, char: string): void { 49 | if (char === '\n') { 50 | this.enter(helixState); 51 | return; 52 | } 53 | 54 | // If we've just started a search, set a marker where we were so we can go back on escape 55 | if (this.searchString === '') { 56 | this.lastActivePosition = helixState.editorState.activeEditor?.selection.active; 57 | this.initialSearchPosition = this.lastActivePosition; 58 | } 59 | 60 | this.searchString += char; 61 | helixState.commandLine.setText(this.searchString, helixState); 62 | if (helixState.mode === Mode.Select) { 63 | this.findInstancesInRange(helixState); 64 | } else { 65 | this.findInstancesInDocument(helixState); 66 | } 67 | } 68 | 69 | addText(helixState: HelixState, text: string): void { 70 | // If we've just started a search, set a marker where we were so we can go back on escape 71 | if (this.searchString === '') { 72 | this.lastActivePosition = helixState.editorState.activeEditor?.selection.active; 73 | this.initialSearchPosition = this.lastActivePosition; 74 | } 75 | 76 | this.searchString += text; 77 | helixState.commandLine.setText(this.searchString, helixState); 78 | if (helixState.mode === Mode.Select) { 79 | this.findInstancesInRange(helixState); 80 | } else { 81 | this.findInstancesInDocument(helixState); 82 | } 83 | } 84 | 85 | /** The "type" event handler doesn't pick up backspace so it needs to be dealt with separately */ 86 | backspace(helixState: HelixState): void { 87 | this.searchString = this.searchString.slice(0, -1); 88 | helixState.commandLine.setText(this.searchString, helixState); 89 | if (this.searchString && helixState.mode === Mode.Select) { 90 | this.findInstancesInRange(helixState); 91 | } else if (this.searchString) { 92 | this.findInstancesInDocument(helixState); 93 | } 94 | } 95 | 96 | /** Clear search string and return to Normal mode */ 97 | enter(helixState: HelixState): void { 98 | this.searchHistory.push(this.searchString); 99 | this.searchString = ''; 100 | this.initialSearchPosition = undefined; 101 | helixState.commandLine.setText(this.searchString, helixState); 102 | 103 | // Upstream Bug 104 | // Annoyingly, addSelectionToNextFindMatch actually does 2 things. 105 | // For normal search it will put the selection into the search buffer (which is fine) 106 | // But for selection (ctrl+d), it will select the next matching selection (which we don't want) 107 | // We will need to compare the selections, and if they've changed remove the last one 108 | // Cache what we have before calling the commmand 109 | 110 | // Add the current selection to the next find match 111 | if (helixState.mode === Mode.SearchInProgress) { 112 | vscode.commands.executeCommand('actions.findWithSelection'); 113 | } 114 | 115 | if (helixState.mode === Mode.Select) { 116 | // Set a flag to signal we're in select mode, so when we go to search we can search the current selection 117 | // This is a mitigation around https://github.com/jasonwilliams/vscode-helix/issues/5 118 | this.selectModeActive = true; 119 | } 120 | // reset search history index 121 | this.searchHistoryIndex = this.searchHistory.length - 1; 122 | enterNormalMode(helixState); 123 | } 124 | 125 | findInstancesInDocument(helixState: HelixState): void { 126 | const editor = helixState.editorState.activeEditor; 127 | if (editor) { 128 | const document = editor.document; 129 | const flags = this.getFlags(); 130 | const searchRegex = new RegExp(this.getNormalisedSearchString(), flags); 131 | const searchStartPosition = this.initialSearchPosition || editor.selection.active; 132 | const textFromCursor = document.getText().slice(document.offsetAt(searchStartPosition)); 133 | const matchAfterCursor = searchRegex.exec(textFromCursor); 134 | const matchInDocument = searchRegex.exec(document.getText()); 135 | let startPos: vscode.Position | undefined; 136 | let endPos: vscode.Position | undefined; 137 | if (matchAfterCursor) { 138 | const matchStartOffset = document.offsetAt(searchStartPosition) + matchAfterCursor.index; 139 | const matchEndOffset = matchStartOffset + matchAfterCursor[0].length; 140 | startPos = document.positionAt(matchStartOffset); 141 | endPos = document.positionAt(matchEndOffset); 142 | editor.selection = new vscode.Selection(startPos, endPos); 143 | // We should also move the viewport to our match if there is one 144 | editor.revealRange(new vscode.Range(startPos, endPos)); 145 | } else if (matchInDocument) { 146 | // If no match after cursor has been found, search entire document. 147 | startPos = document.positionAt(matchInDocument.index); 148 | endPos = document.positionAt(matchInDocument.index + matchInDocument[0].length); 149 | editor.selection = new vscode.Selection(startPos, endPos); 150 | // We should also move the viewport to our match if there is one 151 | editor.revealRange(new vscode.Range(startPos, endPos)); 152 | } else { 153 | // If we can't find a match view the last saved position 154 | if (this.lastActivePosition) { 155 | editor.selection = new vscode.Selection(this.lastActivePosition, this.lastActivePosition); 156 | editor.revealRange(new vscode.Range(this.lastActivePosition, this.lastActivePosition)); 157 | } 158 | } 159 | } 160 | } 161 | 162 | findInstancesInRange(helixState: HelixState): void { 163 | const editor = helixState.editorState.activeEditor; 164 | if (editor) { 165 | const document = editor.document; 166 | const foundRanges: vscode.Range[] = []; 167 | const flags = this.getFlags(); 168 | const searchRegex = new RegExp(this.getNormalisedSearchString(), flags); 169 | let match; 170 | while ((match = searchRegex.exec(document.getText()))) { 171 | const startPos = document.positionAt(match.index); 172 | const endPos = document.positionAt(match.index + match[0].length); 173 | const foundRange = new vscode.Range(startPos, endPos); 174 | if (helixState.currentSelection?.contains(foundRange)) { 175 | foundRanges.push(foundRange); 176 | } 177 | } 178 | editor.selections = foundRanges.map((range) => new vscode.Selection(range.start, range.end)); 179 | } 180 | } 181 | 182 | /** Go to the previous search result in our search history */ 183 | previousSearchResult(helixState: HelixState): void { 184 | if (this.searchHistory.length > 0) { 185 | this.searchString = this.searchHistory[this.searchHistoryIndex] || ''; 186 | this.searchHistoryIndex = Math.max(this.searchHistoryIndex - 1, 0); // Add this line 187 | helixState.commandLine.setText(this.searchString, helixState); 188 | this.findInstancesInDocument(helixState); 189 | } 190 | } 191 | 192 | nextSearchResult(helixState: HelixState): void { 193 | if (this.searchHistory.length > 0) { 194 | this.searchString = this.searchHistory[this.searchHistoryIndex] || ''; 195 | this.searchHistoryIndex = Math.min(this.searchHistoryIndex + 1, this.searchHistory.length - 1); // Add this line 196 | helixState.commandLine.setText(this.searchString, helixState); 197 | this.findInstancesInDocument(helixState); 198 | } 199 | } 200 | } 201 | 202 | export const searchState = new SearchState(); 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-helix-emulation", 3 | "version": "0.7.0", 4 | "displayName": "Helix For VS Code", 5 | "description": "Helix emulation for Visual Studio Code", 6 | "publisher": "jasew", 7 | "author": "Jason Williams", 8 | "license": "MIT", 9 | "homepage": "https://github.com/jasonwilliams/vscode-helix", 10 | "keywords": [ 11 | "vim", 12 | "vi", 13 | "helix" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/jasonwilliams/vscode-helix" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/jasonwilliams/vscode-helix/issues" 21 | }, 22 | "categories": [ 23 | "Other", 24 | "Keymaps" 25 | ], 26 | "main": "./dist/index.js", 27 | "browser": "./dist/browser.js", 28 | "icon": "docs/img/helixLogo.png", 29 | "engines": { 30 | "vscode": "^1.83.1" 31 | }, 32 | "activationEvents": [ 33 | "onStartupFinished" 34 | ], 35 | "contributes": { 36 | "commands": [ 37 | { 38 | "command": "extension.helixKeymap.enterDisabledMode", 39 | "title": "Disable Helix", 40 | "enablement": "!extension.helixKeymap.disabledMode" 41 | }, 42 | { 43 | "command": "extension.helixKeymap.enableHelix", 44 | "title": "Enable Helix", 45 | "enablement": "extension.helixKeymap.disabledMode" 46 | } 47 | ], 48 | "keybindings": [ 49 | { 50 | "key": "shift+5", 51 | "command": "editor.action.selectAll", 52 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 53 | }, 54 | { 55 | "key": "shift+j", 56 | "command": "editor.action.joinLines", 57 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 58 | }, 59 | { 60 | "key": "Escape", 61 | "command": "extension.helixKeymap.escapeKey", 62 | "when": "editorTextFocus" 63 | }, 64 | { 65 | "key": "Escape", 66 | "command": "closeMarkersNavigation", 67 | "when": "editorFocus && markersNavigationVisible" 68 | }, 69 | { 70 | "key": "ctrl+f", 71 | "command": "extension.helixKeymap.scrollDownPage", 72 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 73 | }, 74 | { 75 | "key": "ctrl+b", 76 | "command": "extension.helixKeymap.scrollUpPage", 77 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 78 | }, 79 | { 80 | "key": "ctrl+w", 81 | "command": "extension.helixKeymap.enterWindowMode", 82 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 83 | }, 84 | { 85 | "key": "backspace", 86 | "command": "extension.helixKeymap.backspaceSearchMode", 87 | "when": "editorTextFocus && (extension.helixKeymap.searchMode || extension.helixKeymap.selectMode)" 88 | }, 89 | { 90 | "key": "backspace", 91 | "command": "extension.helixKeymap.backspaceCommandMode", 92 | "when": "extension.helixKeymap.commandMode" 93 | }, 94 | { 95 | "key": "enter", 96 | "command": "extension.helixKeymap.exitSearchMode", 97 | "when": "editorTextFocus && (extension.helixKeymap.searchMode || extension.helixKeymap.selectMode)" 98 | }, 99 | { 100 | "key": "up", 101 | "command": "extension.helixKeymap.previousSearchResult", 102 | "when": "editorTextFocus && (extension.helixKeymap.searchMode || extension.helixKeymap.selectMode)" 103 | }, 104 | { 105 | "key": "down", 106 | "command": "extension.helixKeymap.nextSearchResult", 107 | "when": "editorTextFocus && (extension.helixKeymap.searchMode || extension.helixKeymap.selectMode)" 108 | }, 109 | { 110 | "key": "alt+o", 111 | "command": "editor.action.smartSelect.expand", 112 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 113 | }, 114 | { 115 | "key": "alt+i", 116 | "command": "editor.action.smartSelect.shrink", 117 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 118 | }, 119 | { 120 | "key": "ctrl+i", 121 | "command": "workbench.action.navigateForward", 122 | "when": "(extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 123 | }, 124 | { 125 | "key": "ctrl+d", 126 | "command": "extension.helixKeymap.scrollDownHalfPage", 127 | "when": "(extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 128 | }, 129 | { 130 | "key": "ctrl+u", 131 | "command": "extension.helixKeymap.scrollUpHalfPage", 132 | "when": "(extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 133 | }, 134 | { 135 | "key": "ctrl+o", 136 | "command": "workbench.action.navigateBack", 137 | "when": "(extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 138 | }, 139 | { 140 | "key": "ctrl+n", 141 | "command": "workbench.action.quickOpenSelectNext", 142 | "when": "inQuickOpen && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 143 | }, 144 | { 145 | "key": "ctrl+p", 146 | "command": "workbench.action.quickOpenSelectPrevious", 147 | "when": "inQuickOpen && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 148 | }, 149 | { 150 | "key": "ctrl+o", 151 | "command": "-workbench.action.files.openFile", 152 | "when": "true" 153 | }, 154 | { 155 | "key": "ctrl+i", 156 | "command": "-editor.action.triggerSuggest", 157 | "when": "true" 158 | }, 159 | { 160 | "key": "ctrl+n", 161 | "command": "-workbench.action.files.newUntitledFile", 162 | "when": "true" 163 | }, 164 | { 165 | "key": "ctrl+n", 166 | "command": "selectNextCodeAction", 167 | "when": "codeActionMenuVisible" 168 | }, 169 | { 170 | "key": "ctrl+p", 171 | "command": "selectPrevCodeAction", 172 | "when": "codeActionMenuVisible" 173 | }, 174 | { 175 | "key": "ctrl+n", 176 | "command": "selectNextSuggestion", 177 | "when": "suggestWidgetMultipleSuggestions && suggestWidgetVisible && textInputFocus" 178 | }, 179 | { 180 | "key": "ctrl+p", 181 | "command": "selectPrevSuggestion", 182 | "when": "suggestWidgetMultipleSuggestions && suggestWidgetVisible && textInputFocus" 183 | }, 184 | { 185 | "key": "alt+;", 186 | "command": "extension.helixKeymap.flipSelection", 187 | "when": "!extension.helixKeymap.insertMode && editorTextFocus" 188 | }, 189 | { 190 | "key": "alt+k", 191 | "command": "editor.action.moveLinesUpAction", 192 | "when": "editorTextFocus && !extension.helixKeymap.insertMode" 193 | }, 194 | { 195 | "key": "alt+j", 196 | "command": "editor.action.moveLinesDownAction", 197 | "when": "editorTextFocus && !extension.helixKeymap.insertMode" 198 | }, 199 | { 200 | "key": "alt+d", 201 | "command": "editor.action.addSelectionToNextFindMatch", 202 | "when": "editorTextFocus && !extension.helixKeymap.insertMode" 203 | }, 204 | { 205 | "key": "alt+m", 206 | "command": "editor.action.moveSelectionToNextFindMatch", 207 | "when": "editorTextFocus && !extension.helixKeymap.insertMode" 208 | }, 209 | { 210 | "key": "ctrl+v", 211 | "command": "extension.helixKeymap.clipboardPasteAction", 212 | "when": "editorTextFocus && extension.helixKeymap.searchMode" 213 | }, 214 | { 215 | "key": "j", 216 | "command": "list.focusDown", 217 | "when": "listFocus && !inputFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 218 | }, 219 | { 220 | "key": "k", 221 | "command": "list.focusUp", 222 | "when": "listFocus && !inputFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 223 | }, 224 | { 225 | "key": "l", 226 | "command": "list.expand", 227 | "when": "listFocus && !inputFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 228 | }, 229 | { 230 | "key": "h", 231 | "command": "list.collapse", 232 | "when": "listFocus && !inputFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 233 | }, 234 | { 235 | "key": "alt+.", 236 | "command": "extension.helixKeymap.repeatLastMotion", 237 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 238 | }, 239 | { 240 | "key": "ctrl+w", 241 | "command": "deleteWordLeft", 242 | "when": "editorTextFocus && extension.helixKeymap.insertMode" 243 | }, 244 | { 245 | "key": "alt+d", 246 | "command": "deleteWordRight", 247 | "when": "editorTextFocus && extension.helixKeymap.insertMode" 248 | }, 249 | { 250 | "key": "alt+d", 251 | "command": "deleteWordRight", 252 | "when": "editorTextFocus && extension.helixKeymap.insertMode" 253 | }, 254 | { 255 | "key": "ctrl+u", 256 | "command": "deleteAllLeft", 257 | "when": "editorTextFocus && extension.helixKeymap.insertMode" 258 | }, 259 | { 260 | "key": "ctrl+l", 261 | "command": "deleteAllRight", 262 | "when": "editorTextFocus && extension.helixKeymap.insertMode" 263 | }, 264 | { 265 | "key": "ctrl+d", 266 | "command": "deleteRight", 267 | "when": "editorTextFocus && extension.helixKeymap.insertMode" 268 | }, 269 | { 270 | "key": "ctrl+h", 271 | "command": "deleteLeft", 272 | "when": "editorTextFocus && extension.helixKeymap.insertMode" 273 | }, 274 | { 275 | "key": "ctrl+x", 276 | "command": "acceptSelectedSuggestion", 277 | "when": "suggestWidgetHasFocusedSuggestion && suggestWidgetVisible && extension.helixKeymap.insertMode" 278 | }, 279 | { 280 | "key": "alt+OEM_8", 281 | "command": "extension.helixKeymap.switchToUppercase", 282 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 283 | }, 284 | { 285 | "key": "ctrl+a", 286 | "command": "extension.helixKeymap.increment", 287 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 288 | }, 289 | { 290 | "key": "ctrl+x", 291 | "command": "extension.helixKeymap.decrement", 292 | "when": "editorTextFocus && (extension.helixKeymap.normalMode || extension.helixKeymap.visualMode)" 293 | } 294 | ], 295 | "configuration": { 296 | "type": "object", 297 | "title": "Helix Keymap Configuration", 298 | "properties": { 299 | "helixKeymap.yankHighlightBackgroundColor": { 300 | "type": "string", 301 | "default": "#F8F3AB", 302 | "description": "Background color that flashes to show the range when yanking." 303 | }, 304 | "helixKeymap.toggleRelativeLineNumbers": { 305 | "type": "boolean", 306 | "default": false, 307 | "description": "Enable toggling relative line numbers based on editor mode. When enabled, relative line numbers are shown in all modes except Insert mode, where they toggle to absolute line numbers." 308 | } 309 | } 310 | } 311 | }, 312 | "scripts": { 313 | "build": "node build.mjs", 314 | "build:prod": "node build.mjs --production", 315 | "watch": "node build.mjs --watch", 316 | "pack": "vsce package --no-dependencies", 317 | "publish": "vsce publish --no-dependencies", 318 | "vscode:prepublish": "npm run build:prod", 319 | "lint": "prettier --check --plugin-search-dir=. src && eslint src", 320 | "format": "prettier --write --plugin-search-dir=. src", 321 | "typecheck": "tsc --noEmit", 322 | "release": "bumpp && npm run publish" 323 | }, 324 | "devDependencies": { 325 | "@types/http-errors": "^1.8.0", 326 | "@types/node": "^20.8.9", 327 | "@types/vscode": "^1.83.1", 328 | "@typescript-eslint/eslint-plugin": "^6.10.0", 329 | "@typescript-eslint/parser": "^6.10.0", 330 | "bumpp": "^8.2.1", 331 | "eslint": "^8.27.0", 332 | "eslint-config-prettier": "^9.0.0", 333 | "esno": "^0.16.3", 334 | "prettier": "^3.0.3", 335 | "rimraf": "^3.0.2", 336 | "string.prototype.matchall": "^4.0.2", 337 | "typescript": "^5.2.2", 338 | "esbuild": "^0.19.5", 339 | "vsce": "^2.11.0" 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/actions/motions.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Action } from '../action_types'; 4 | import { HelixState } from '../helix_state_types'; 5 | import { Mode } from '../modes_types'; 6 | import { paragraphBackward, paragraphForward } from '../paragraph_utils'; 7 | import { parseKeysExact, parseKeysRegex } from '../parse_keys'; 8 | import * as positionUtils from '../position_utils'; 9 | import { searchBackward, searchForward } from '../search_utils'; 10 | import { 11 | vimToVscodeVisualLineSelection, 12 | vimToVscodeVisualSelection, 13 | vscodeToVimVisualLineSelection, 14 | vscodeToVimVisualSelection, 15 | } from '../selection_utils'; 16 | import { setVisualLineSelections } from '../visual_line_utils'; 17 | import { setVisualSelections } from '../visual_utils'; 18 | import { whitespaceWordRanges, wordRanges } from '../word_utils'; 19 | import KeyMap from './keymaps'; 20 | 21 | export const motions: Action[] = [ 22 | parseKeysExact([KeyMap.Motions.MoveRight], [Mode.Visual], (vimState, editor) => { 23 | execMotion(vimState, editor, ({ document, position }) => { 24 | return positionUtils.rightNormal(document, position, vimState.resolveCount()); 25 | }); 26 | }), 27 | 28 | parseKeysExact([KeyMap.Motions.MoveLeft], [Mode.Visual], (vimState, editor) => { 29 | execMotion(vimState, editor, ({ position }) => { 30 | return positionUtils.left(position, vimState.resolveCount()); 31 | }); 32 | }), 33 | 34 | parseKeysExact([KeyMap.Motions.MoveRight], [Mode.Normal], () => { 35 | vscode.commands.executeCommand('cursorRight'); 36 | }), 37 | 38 | parseKeysExact([KeyMap.Motions.MoveLeft], [Mode.Normal], () => { 39 | vscode.commands.executeCommand('cursorLeft'); 40 | }), 41 | 42 | parseKeysExact([KeyMap.Motions.MoveUp], [Mode.Normal], (_vimState, _editor) => { 43 | vscode.commands.executeCommand('cursorMove', { 44 | to: 'up', 45 | by: 'wrappedLine', 46 | value: _vimState.resolveCount(), 47 | }); 48 | }), 49 | parseKeysExact([KeyMap.Motions.MoveUp], [Mode.Visual], (vimState, editor) => { 50 | const originalSelections = editor.selections; 51 | 52 | vscode.commands 53 | .executeCommand('cursorMove', { 54 | to: 'up', 55 | by: 'wrappedLine', 56 | select: true, 57 | value: vimState.resolveCount(), 58 | }) 59 | .then(() => { 60 | setVisualSelections(editor, originalSelections); 61 | }); 62 | }), 63 | parseKeysExact([KeyMap.Motions.MoveUp], [Mode.VisualLine], (vimState, editor) => { 64 | vscode.commands 65 | .executeCommand('cursorMove', { to: 'up', by: 'line', select: true, value: vimState.resolveCount() }) 66 | .then(() => { 67 | setVisualLineSelections(editor); 68 | }); 69 | }), 70 | 71 | parseKeysExact([KeyMap.Motions.MoveDown], [Mode.Normal], (_vimState, _editor) => { 72 | vscode.commands.executeCommand('cursorMove', { 73 | to: 'down', 74 | by: 'wrappedLine', 75 | value: _vimState.resolveCount(), 76 | }); 77 | }), 78 | parseKeysExact([KeyMap.Motions.MoveDown], [Mode.Visual], (vimState, editor) => { 79 | const originalSelections = editor.selections; 80 | 81 | vscode.commands 82 | .executeCommand('cursorMove', { 83 | to: 'down', 84 | by: 'wrappedLine', 85 | select: true, 86 | value: vimState.resolveCount(), 87 | }) 88 | .then(() => { 89 | setVisualSelections(editor, originalSelections); 90 | }); 91 | }), 92 | 93 | parseKeysExact([KeyMap.Motions.MoveDown], [Mode.VisualLine], (vimState, editor) => { 94 | vscode.commands.executeCommand('cursorMove', { to: 'down', by: 'line', select: true }).then(() => { 95 | setVisualLineSelections(editor); 96 | }); 97 | }), 98 | 99 | parseKeysExact(['w'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 100 | const count = vimState.resolveCount(); 101 | for (let i = 0; i < count; i++) { 102 | createWordForwardHandler(wordRanges)(vimState, editor); 103 | } 104 | }), 105 | parseKeysExact(['W'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 106 | const count = vimState.resolveCount(); 107 | for (let i = 0; i < count; i++) { 108 | createWordForwardHandler(whitespaceWordRanges)(vimState, editor); 109 | } 110 | }), 111 | 112 | parseKeysExact(['b'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 113 | const count = vimState.resolveCount(); 114 | for (let i = 0; i < count; i++) { 115 | createWordBackwardHandler(wordRanges)(vimState, editor); 116 | } 117 | }), 118 | parseKeysExact(['B'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 119 | const count = vimState.resolveCount(); 120 | for (let i = 0; i < count; i++) { 121 | createWordBackwardHandler(whitespaceWordRanges)(vimState, editor); 122 | } 123 | }), 124 | 125 | parseKeysExact(['e'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 126 | const count = vimState.resolveCount(); 127 | for (let i = 0; i < count; i++) { 128 | createWordEndHandler(wordRanges)(vimState, editor); 129 | } 130 | }), 131 | parseKeysExact(['E'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 132 | const count = vimState.resolveCount(); 133 | for (let i = 0; i < count; i++) { 134 | createWordEndHandler(whitespaceWordRanges)(vimState, editor); 135 | } 136 | }), 137 | 138 | parseKeysRegex(/^f(.)$/, /^(f|f.)$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => { 139 | findForward(vimState, editor, match); 140 | 141 | vimState.repeatLastMotion = (innerVimState, innerEditor) => { 142 | findForward(innerVimState, innerEditor, match); 143 | }; 144 | }), 145 | 146 | parseKeysRegex(/^F(.)$/, /^(F|F.)$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => { 147 | findBackward(vimState, editor, match); 148 | 149 | vimState.repeatLastMotion = (innerVimState, innerEditor) => { 150 | findBackward(innerVimState, innerEditor, match); 151 | }; 152 | }), 153 | 154 | parseKeysRegex(/^t(.)$/, /^t$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => { 155 | tillForward(vimState, editor, match); 156 | 157 | vimState.repeatLastMotion = (innerVimState, innerEditor) => { 158 | tillForward(innerVimState, innerEditor, match); 159 | }; 160 | }), 161 | 162 | parseKeysRegex(/^T(.)$/, /^T$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => { 163 | tillBackward(vimState, editor, match); 164 | 165 | vimState.repeatLastMotion = (innerVimState, innerEditor) => { 166 | tillBackward(innerVimState, innerEditor, match); 167 | }; 168 | }), 169 | 170 | parseKeysExact(['}'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 171 | execMotion(vimState, editor, ({ document, position }) => { 172 | return new vscode.Position(paragraphForward(document, position.line), 0); 173 | }); 174 | }), 175 | 176 | parseKeysExact([']', 'p'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 177 | execMotion(vimState, editor, ({ document, position }) => { 178 | return new vscode.Position(paragraphForward(document, position.line), 0); 179 | }); 180 | }), 181 | 182 | parseKeysExact(['{'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 183 | execMotion(vimState, editor, ({ document, position }) => { 184 | return new vscode.Position(paragraphBackward(document, position.line), 0); 185 | }); 186 | }), 187 | 188 | parseKeysExact(['[', 'p'], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 189 | execMotion(vimState, editor, ({ document, position }) => { 190 | return new vscode.Position(paragraphBackward(document, position.line), 0); 191 | }); 192 | }), 193 | 194 | parseKeysExact([KeyMap.Motions.MoveLineEnd], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 195 | execMotion(vimState, editor, ({ document, position }) => { 196 | const lineLength = document.lineAt(position.line).text.length; 197 | return position.with({ character: Math.max(lineLength - 1, 0) }); 198 | }); 199 | }), 200 | 201 | parseKeysExact([KeyMap.Motions.MoveLineStart], [Mode.Normal, Mode.Visual, Mode.VisualLine], (vimState, editor) => { 202 | execMotion(vimState, editor, ({ document, position }) => { 203 | const line = document.lineAt(position.line); 204 | return position.with({ 205 | character: line.firstNonWhitespaceCharacterIndex, 206 | }); 207 | }); 208 | }), 209 | 210 | parseKeysExact(['H'], [Mode.Normal], (_vimState, _editor) => { 211 | vscode.commands.executeCommand('cursorMove', { 212 | to: 'viewPortTop', 213 | by: 'line', 214 | }); 215 | }), 216 | parseKeysExact(['H'], [Mode.Visual], (vimState, editor) => { 217 | const originalSelections = editor.selections; 218 | 219 | vscode.commands 220 | .executeCommand('cursorMove', { 221 | to: 'viewPortTop', 222 | by: 'line', 223 | select: true, 224 | }) 225 | .then(() => { 226 | setVisualSelections(editor, originalSelections); 227 | }); 228 | }), 229 | parseKeysExact(['H'], [Mode.VisualLine], (vimState, editor) => { 230 | vscode.commands 231 | .executeCommand('cursorMove', { 232 | to: 'viewPortTop', 233 | by: 'line', 234 | select: true, 235 | }) 236 | .then(() => { 237 | setVisualLineSelections(editor); 238 | }); 239 | }), 240 | 241 | parseKeysExact(['M'], [Mode.Normal], (_vimState, _editor) => { 242 | vscode.commands.executeCommand('cursorMove', { 243 | to: 'viewPortCenter', 244 | by: 'line', 245 | }); 246 | }), 247 | parseKeysExact(['M'], [Mode.Visual], (vimState, editor) => { 248 | const originalSelections = editor.selections; 249 | 250 | vscode.commands 251 | .executeCommand('cursorMove', { 252 | to: 'viewPortCenter', 253 | by: 'line', 254 | select: true, 255 | }) 256 | .then(() => { 257 | setVisualSelections(editor, originalSelections); 258 | }); 259 | }), 260 | parseKeysExact(['M'], [Mode.VisualLine], (vimState, editor) => { 261 | vscode.commands 262 | .executeCommand('cursorMove', { 263 | to: 'viewPortCenter', 264 | by: 'line', 265 | select: true, 266 | }) 267 | .then(() => { 268 | setVisualLineSelections(editor); 269 | }); 270 | }), 271 | 272 | parseKeysExact(['L'], [Mode.Normal], (_vimState, _editor) => { 273 | vscode.commands.executeCommand('cursorMove', { 274 | to: 'viewPortBottom', 275 | by: 'line', 276 | }); 277 | }), 278 | parseKeysExact(['L'], [Mode.Visual], (vimState, editor) => { 279 | const originalSelections = editor.selections; 280 | 281 | vscode.commands 282 | .executeCommand('cursorMove', { 283 | to: 'viewPortBottom', 284 | by: 'line', 285 | select: true, 286 | }) 287 | .then(() => { 288 | setVisualSelections(editor, originalSelections); 289 | }); 290 | }), 291 | parseKeysExact(['L'], [Mode.VisualLine], (vimState, editor) => { 292 | vscode.commands 293 | .executeCommand('cursorMove', { 294 | to: 'viewPortBottom', 295 | by: 'line', 296 | select: true, 297 | }) 298 | .then(() => { 299 | setVisualLineSelections(editor); 300 | }); 301 | }), 302 | ]; 303 | 304 | type MotionArgs = { 305 | document: vscode.TextDocument; 306 | position: vscode.Position; 307 | selectionIndex: number; 308 | vimState: HelixState; 309 | }; 310 | 311 | type RegexMotionArgs = { 312 | document: vscode.TextDocument; 313 | position: vscode.Position; 314 | selectionIndex: number; 315 | vimState: HelixState; 316 | match: RegExpMatchArray; 317 | }; 318 | 319 | function execRegexMotion( 320 | vimState: HelixState, 321 | editor: vscode.TextEditor, 322 | match: RegExpMatchArray, 323 | regexMotion: (args: RegexMotionArgs) => vscode.Position, 324 | ) { 325 | return execMotion(vimState, editor, (motionArgs) => { 326 | return regexMotion({ 327 | ...motionArgs, 328 | match: match, 329 | }); 330 | }); 331 | } 332 | 333 | function execMotion(vimState: HelixState, editor: vscode.TextEditor, motion: (args: MotionArgs) => vscode.Position) { 334 | const document = editor.document; 335 | 336 | const newSelections = editor.selections.map((selection, i) => { 337 | if (vimState.mode === Mode.Normal) { 338 | const newPosition = motion({ 339 | document: document, 340 | position: selection.active, 341 | selectionIndex: i, 342 | vimState: vimState, 343 | }); 344 | return new vscode.Selection(selection.active, newPosition); 345 | } else if (vimState.mode === Mode.Visual) { 346 | const vimSelection = vscodeToVimVisualSelection(document, selection); 347 | const motionPosition = motion({ 348 | document: document, 349 | position: vimSelection.active, 350 | selectionIndex: i, 351 | vimState: vimState, 352 | }); 353 | 354 | return vimToVscodeVisualSelection(document, new vscode.Selection(vimSelection.anchor, motionPosition)); 355 | } else if (vimState.mode === Mode.VisualLine) { 356 | const vimSelection = vscodeToVimVisualLineSelection(document, selection); 357 | const motionPosition = motion({ 358 | document: document, 359 | position: vimSelection.active, 360 | selectionIndex: i, 361 | vimState: vimState, 362 | }); 363 | 364 | return vimToVscodeVisualLineSelection(document, new vscode.Selection(vimSelection.anchor, motionPosition)); 365 | } else { 366 | return selection; 367 | } 368 | }); 369 | 370 | editor.selections = newSelections; 371 | 372 | editor.revealRange( 373 | new vscode.Range(newSelections[0].active, newSelections[0].active), 374 | vscode.TextEditorRevealType.InCenterIfOutsideViewport, 375 | ); 376 | } 377 | 378 | function findForward(vimState: HelixState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void { 379 | execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => { 380 | const fromPosition = position.with({ character: position.character + 1 }); 381 | const result = searchForward(document, match[1], fromPosition); 382 | 383 | if (result) { 384 | return result.with({ character: result.character + 1 }); 385 | } else { 386 | return position; 387 | } 388 | }); 389 | } 390 | 391 | function findBackward(vimState: HelixState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void { 392 | execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => { 393 | const fromPosition = positionLeftWrap(document, position); 394 | const result = searchBackward(document, match[1], fromPosition); 395 | 396 | if (result) { 397 | return result; 398 | } else { 399 | return position; 400 | } 401 | }); 402 | } 403 | 404 | function tillForward(vimState: HelixState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void { 405 | execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => { 406 | const fromPosition = position.with({ character: position.character + 1 }); 407 | const result = searchForward(document, match[1], fromPosition); 408 | 409 | if (result) { 410 | return result.with({ character: result.character }); 411 | } else { 412 | return position; 413 | } 414 | }); 415 | } 416 | 417 | function tillBackward(vimState: HelixState, editor: vscode.TextEditor, outerMatch: RegExpMatchArray): void { 418 | execRegexMotion(vimState, editor, outerMatch, ({ document, position, match }) => { 419 | const fromPosition = positionLeftWrap(document, position); 420 | const result = searchBackward(document, match[1], fromPosition); 421 | 422 | if (result) { 423 | return result; 424 | } else { 425 | return position; 426 | } 427 | }); 428 | } 429 | 430 | function positionLeftWrap(document: vscode.TextDocument, position: vscode.Position): vscode.Position { 431 | if (position.character === 0) { 432 | if (position.line === 0) { 433 | return position; 434 | } else { 435 | const lineLength = document.lineAt(position.line - 1).text.length; 436 | return new vscode.Position(position.line - 1, lineLength); 437 | } 438 | } else { 439 | return position.with({ character: position.character - 1 }); 440 | } 441 | } 442 | 443 | function createWordForwardHandler( 444 | wordRangesFunction: (text: string) => { start: number; end: number }[], 445 | ): (vimState: HelixState, editor: vscode.TextEditor) => void { 446 | return (vimState, editor) => { 447 | execMotion(vimState, editor, ({ document, position }) => { 448 | let character = position.character; 449 | // Try the current line and if we're at the end go to the next line 450 | // This way we're only keeping one line of text in memory at a time 451 | // i is representing the relative line number we're on from where we started 452 | for (let i = 0; i < document.lineCount; i++) { 453 | const lineText = document.lineAt(position.line + i).text; 454 | const ranges = wordRangesFunction(lineText); 455 | 456 | const result = ranges.find((x) => x.start > character); 457 | 458 | if (result) { 459 | if (vimState.mode === Mode.Normal) { 460 | return position.with({ character: result.start, line: position.line + i }); 461 | } else { 462 | return position.with({ character: result.start-1, line: position.line + i }); 463 | } 464 | } 465 | // If we don't find anything on this line, search the next and reset the character to 0 466 | character = 0; 467 | } 468 | 469 | // We may be at the end of the document or nothing else matches 470 | return position; 471 | }); 472 | }; 473 | } 474 | 475 | function createWordBackwardHandler( 476 | wordRangesFunction: (text: string) => { start: number; end: number }[], 477 | ): (vimState: HelixState, editor: vscode.TextEditor) => void { 478 | return (vimState, editor) => { 479 | execMotion(vimState, editor, ({ document, position }) => { 480 | let character = position.character; 481 | // Try the current line and if we're at the end go to the next line 482 | // This way we're only keeping one line of text in memory at a time 483 | // i is representing the relative line number we're on from where we started 484 | for (let i = position.line; i >= 0; i--) { 485 | const lineText = document.lineAt(i).text; 486 | const ranges = wordRangesFunction(lineText); 487 | 488 | const result = ranges.reverse().find((x) => x.start < character); 489 | 490 | if (result) { 491 | return position.with({ character: result.start, line: i }); 492 | } 493 | 494 | // If we don't find anything on this line, search the next and reset the character to 0 495 | character = Infinity; 496 | } 497 | // We may be at the end of the document or nothing else matches 498 | return position; 499 | }); 500 | }; 501 | } 502 | 503 | function createWordEndHandler( 504 | wordRangesFunction: (text: string) => { start: number; end: number }[], 505 | ): (vimState: HelixState, editor: vscode.TextEditor) => void { 506 | return (vimState, editor) => { 507 | execMotion(vimState, editor, ({ document, position }) => { 508 | const lineText = document.lineAt(position.line).text; 509 | const ranges = wordRangesFunction(lineText); 510 | 511 | const result = ranges.find((x) => x.end > position.character); 512 | 513 | if (result) { 514 | if (vimState.mode === Mode.Normal) { 515 | return position.with({ character: result.end + 1 }); 516 | } else { 517 | return position.with({ character: result.end }); 518 | } 519 | } else { 520 | return position; 521 | } 522 | }); 523 | }; 524 | } 525 | -------------------------------------------------------------------------------- /src/actions/actions.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Action } from '../action_types'; 3 | import { HelixState } from '../helix_state_types'; 4 | import { 5 | enterCommandMode, 6 | enterInsertMode, 7 | enterNormalMode, 8 | enterSearchMode, 9 | enterSelectMode, 10 | enterVisualLineMode, 11 | enterVisualMode, 12 | setModeCursorStyle, 13 | setRelativeLineNumbers, 14 | } from '../modes'; 15 | import { Mode } from '../modes_types'; 16 | import { parseKeysExact, parseKeysRegex } from '../parse_keys'; 17 | import * as positionUtils from '../position_utils'; 18 | import { putAfter } from '../put_utils/put_after'; 19 | import { putBefore } from '../put_utils/put_before'; 20 | import { removeTypeSubscription } from '../type_subscription'; 21 | import { flashYankHighlight } from '../yank_highlight'; 22 | import { gotoActions } from './gotoMode'; 23 | import KeyMap from './keymaps'; 24 | import { matchActions } from './matchMode'; 25 | import { isSingleLineRange, yank } from './operators'; 26 | import { spaceActions } from './spaceMode'; 27 | import { unimparedActions } from './unimpared'; 28 | import { viewActions } from './viewMode'; 29 | import { windowActions } from './windowMode'; 30 | 31 | enum Direction { 32 | Up, 33 | Down, 34 | } 35 | 36 | export const actions: Action[] = [ 37 | parseKeysExact(['p'], [Mode.Occurrence], () => { 38 | vscode.commands.executeCommand('editor.action.addSelectionToPreviousFindMatch'); 39 | }), 40 | 41 | parseKeysExact(['a'], [Mode.Occurrence], () => { 42 | vscode.commands.executeCommand('editor.action.selectHighlights'); 43 | }), 44 | 45 | parseKeysExact(['n'], [Mode.Normal], (helixState) => { 46 | if (helixState.searchState.selectModeActive) { 47 | vscode.commands.executeCommand('actions.findWithSelection'); 48 | helixState.searchState.selectModeActive = false; 49 | return; 50 | } 51 | 52 | vscode.commands.executeCommand('editor.action.nextMatchFindAction'); 53 | }), 54 | 55 | parseKeysExact(['N'], [Mode.Normal], (helixState) => { 56 | if (helixState.searchState.selectModeActive) { 57 | vscode.commands.executeCommand('actions.findWithSelection'); 58 | helixState.searchState.selectModeActive = false; 59 | return; 60 | } 61 | vscode.commands.executeCommand('editor.action.previousMatchFindAction'); 62 | }), 63 | 64 | parseKeysExact(['?'], [Mode.Normal], (helixState) => { 65 | enterSearchMode(helixState); 66 | helixState.searchState.previousSearchResult(helixState); 67 | }), 68 | 69 | // Selection Stuff 70 | parseKeysExact(['s'], [Mode.Normal, Mode.Visual], (helixState, editor) => { 71 | enterSelectMode(helixState); 72 | // if we enter select mode we should save the current selection 73 | helixState.currentSelection = editor.selection; 74 | }), 75 | 76 | parseKeysExact([','], [Mode.Normal, Mode.Visual], (_, editor) => { 77 | // Keep primary selection only 78 | editor.selections = editor.selections.slice(0, 1); 79 | }), 80 | 81 | parseKeysExact(['/'], [Mode.Normal], (helixState) => { 82 | enterSearchMode(helixState); 83 | }), 84 | 85 | parseKeysExact([':'], [Mode.Normal], (helixState) => { 86 | enterCommandMode(helixState); 87 | }), 88 | 89 | parseKeysExact(['*'], [Mode.Normal], (_) => { 90 | vscode.commands.executeCommand('actions.findWithSelection'); 91 | }), 92 | 93 | parseKeysExact(['>'], [Mode.Normal, Mode.Visual], (_) => { 94 | vscode.commands.executeCommand('editor.action.indentLines'); 95 | }), 96 | 97 | parseKeysExact(['<'], [Mode.Normal, Mode.Visual], (_) => { 98 | vscode.commands.executeCommand('editor.action.outdentLines'); 99 | }), 100 | 101 | parseKeysExact(['='], [Mode.Normal, Mode.Visual], (_) => { 102 | vscode.commands.executeCommand('editor.action.formatSelection'); 103 | }), 104 | 105 | parseKeysExact(['`'], [Mode.Normal], (vimState, editor) => { 106 | // Take the selection and make it all lowercase 107 | editor.edit((editBuilder) => { 108 | editor.selections.forEach((selection) => { 109 | const text = editor.document.getText(selection); 110 | editBuilder.replace(selection, text.toLowerCase()); 111 | }); 112 | }); 113 | }), 114 | 115 | parseKeysExact(['~'], [Mode.Normal], (vimState, editor) => { 116 | // Switch the case of the selection (so if upper case make lower case and vice versa) 117 | editor.edit((editBuilder) => { 118 | editor.selections.forEach((selection) => { 119 | const text = editor.document.getText(selection); 120 | editBuilder.replace( 121 | selection, 122 | text.replace(/./g, (c) => (c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase())), 123 | ); 124 | }); 125 | }); 126 | }), 127 | 128 | // replace 129 | parseKeysRegex(/^r(.)/, /^r/, [Mode.Normal], (helixState, editor, match) => { 130 | const position = editor.selection.active; 131 | editor.edit((builder) => { 132 | builder.replace(new vscode.Range(position, position.with({ character: position.character + 1 })), match[1]); 133 | }); 134 | }), 135 | 136 | // existing 137 | parseKeysExact( 138 | [KeyMap.Actions.InsertMode], 139 | [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.Occurrence], 140 | (vimState, editor) => { 141 | enterInsertMode(vimState); 142 | setModeCursorStyle(vimState.mode, editor); 143 | setRelativeLineNumbers(vimState.mode, editor); 144 | removeTypeSubscription(vimState); 145 | }, 146 | ), 147 | 148 | parseKeysExact([KeyMap.Actions.InsertAtLineStart], [Mode.Normal], (vimState, editor) => { 149 | editor.selections = editor.selections.map((selection) => { 150 | const character = editor.document.lineAt(selection.active.line).firstNonWhitespaceCharacterIndex; 151 | const newPosition = selection.active.with({ character: character }); 152 | return new vscode.Selection(newPosition, newPosition); 153 | }); 154 | 155 | enterInsertMode(vimState); 156 | setModeCursorStyle(vimState.mode, editor); 157 | setRelativeLineNumbers(vimState.mode, editor); 158 | removeTypeSubscription(vimState); 159 | }), 160 | 161 | parseKeysExact(['a'], [Mode.Normal], (vimState, editor) => { 162 | enterInsertMode(vimState, false); 163 | setModeCursorStyle(vimState.mode, editor); 164 | setRelativeLineNumbers(vimState.mode, editor); 165 | removeTypeSubscription(vimState); 166 | }), 167 | 168 | parseKeysExact([KeyMap.Actions.InsertAtLineEnd], [Mode.Normal], (vimState, editor) => { 169 | editor.selections = editor.selections.map((selection) => { 170 | const lineLength = editor.document.lineAt(selection.active.line).text.length; 171 | const newPosition = selection.active.with({ character: lineLength }); 172 | return new vscode.Selection(newPosition, newPosition); 173 | }); 174 | 175 | enterInsertMode(vimState); 176 | setModeCursorStyle(vimState.mode, editor); 177 | setRelativeLineNumbers(vimState.mode, editor); 178 | removeTypeSubscription(vimState); 179 | }), 180 | 181 | parseKeysExact(['v'], [Mode.Normal, Mode.VisualLine], (vimState, editor) => { 182 | enterVisualMode(vimState); 183 | setModeCursorStyle(vimState.mode, editor); 184 | setRelativeLineNumbers(vimState.mode, editor); 185 | }), 186 | 187 | parseKeysExact(['x'], [Mode.Normal, Mode.Visual], () => { 188 | vscode.commands.executeCommand('expandLineSelection'); 189 | }), 190 | 191 | parseKeysExact([KeyMap.Actions.NewLineBelow], [Mode.Normal], (vimState, editor) => { 192 | enterInsertMode(vimState); 193 | vscode.commands.executeCommand('editor.action.insertLineAfter'); 194 | setModeCursorStyle(vimState.mode, editor); 195 | setRelativeLineNumbers(vimState.mode, editor); 196 | removeTypeSubscription(vimState); 197 | }), 198 | 199 | parseKeysExact([KeyMap.Actions.NewLineAbove], [Mode.Normal], (vimState, editor) => { 200 | enterInsertMode(vimState); 201 | vscode.commands.executeCommand('editor.action.insertLineBefore'); 202 | setModeCursorStyle(vimState.mode, editor); 203 | setRelativeLineNumbers(vimState.mode, editor); 204 | removeTypeSubscription(vimState); 205 | }), 206 | 207 | parseKeysExact(['p'], [Mode.Normal, Mode.Visual, Mode.VisualLine], putAfter), 208 | parseKeysExact(['P'], [Mode.Normal], putBefore), 209 | 210 | parseKeysExact(['u'], [Mode.Normal, Mode.Visual, Mode.VisualLine], () => { 211 | vscode.commands.executeCommand('undo'); 212 | }), 213 | 214 | parseKeysExact(['U'], [Mode.Normal, Mode.Visual, Mode.VisualLine], () => { 215 | vscode.commands.executeCommand('redo'); 216 | }), 217 | 218 | parseKeysExact(['d', 'd'], [Mode.Normal], (vimState, editor) => { 219 | deleteLine(vimState, editor); 220 | }), 221 | 222 | parseKeysExact(['D'], [Mode.Normal], () => { 223 | vscode.commands.executeCommand('deleteAllRight'); 224 | }), 225 | 226 | parseKeysRegex(/(\\d+)g/, /^g$/, [Mode.Normal, Mode.Visual], (helixState, editor, match) => { 227 | new vscode.Position(parseInt(match[1]), 0); 228 | }), 229 | 230 | // add 1 character swap 231 | parseKeysRegex(/^x(.)$/, /^x$/, [Mode.Normal, Mode.Visual], (vimState, editor, match) => { 232 | editor.edit((builder) => { 233 | editor.selections.forEach((s) => { 234 | const oneChar = s.with({ 235 | end: s.active.with({ 236 | character: s.active.character + 1, 237 | }), 238 | }); 239 | builder.replace(oneChar, match[1]); 240 | }); 241 | }); 242 | }), 243 | 244 | // same for rip command 245 | parseKeysRegex( 246 | RegExp(`^r(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`), 247 | /^(r|r\d+)$/, 248 | [Mode.Normal, Mode.Visual], 249 | (vimState, editor, match) => { 250 | const lineCount = parseInt(match[1]); 251 | const direction = match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down; 252 | // console.log(`delete ${lineCount} lines up`); 253 | const selections = makeMultiLineSelection(vimState, editor, lineCount, direction); 254 | 255 | yank(vimState, editor, selections, true); 256 | 257 | deleteLines(vimState, editor, lineCount, direction); 258 | }, 259 | ), 260 | 261 | // same for duplicate command 262 | parseKeysRegex( 263 | RegExp(`^q(\\d+)(${KeyMap.Motions.MoveUp}|${KeyMap.Motions.MoveDown})$`), 264 | /^(q|q\d+)$/, 265 | [Mode.Normal, Mode.Visual], 266 | (vimState, editor, match) => { 267 | const lineCount = parseInt(match[1]); 268 | const direction = match[2] == KeyMap.Motions.MoveUp ? Direction.Up : Direction.Down; 269 | // console.log(`delete ${lineCount} lines up`); 270 | editor.selections = makeMultiLineSelection(vimState, editor, lineCount, direction); 271 | vscode.commands.executeCommand('editor.action.copyLinesDownAction'); 272 | }, 273 | ), 274 | 275 | parseKeysExact(['c', 'c'], [Mode.Normal], (vimState, editor) => { 276 | editor.edit((editBuilder) => { 277 | editor.selections.forEach((selection) => { 278 | const line = editor.document.lineAt(selection.active.line); 279 | editBuilder.delete( 280 | new vscode.Range( 281 | selection.active.with({ 282 | character: line.firstNonWhitespaceCharacterIndex, 283 | }), 284 | selection.active.with({ character: line.text.length }), 285 | ), 286 | ); 287 | }); 288 | }); 289 | 290 | enterInsertMode(vimState); 291 | setModeCursorStyle(vimState.mode, editor); 292 | setRelativeLineNumbers(vimState.mode, editor); 293 | removeTypeSubscription(vimState); 294 | }), 295 | 296 | parseKeysExact(['C'], [Mode.Normal], (vimState, editor) => { 297 | vscode.commands.executeCommand('deleteAllRight'); 298 | enterInsertMode(vimState); 299 | setModeCursorStyle(vimState.mode, editor); 300 | setRelativeLineNumbers(vimState.mode, editor); 301 | removeTypeSubscription(vimState); 302 | }), 303 | 304 | parseKeysExact(['y', 'y'], [Mode.Normal], (vimState, editor) => { 305 | yankLine(vimState, editor); 306 | 307 | // Yank highlight 308 | const highlightRanges = editor.selections.map((selection) => { 309 | const lineLength = editor.document.lineAt(selection.active.line).text.length; 310 | return new vscode.Range( 311 | selection.active.with({ character: 0 }), 312 | selection.active.with({ character: lineLength }), 313 | ); 314 | }); 315 | flashYankHighlight(editor, highlightRanges); 316 | }), 317 | 318 | parseKeysExact(['y'], [Mode.Normal, Mode.Visual], (vimState, editor) => { 319 | // Yank highlight 320 | const highlightRanges = editor.selections.map((selection) => selection.with()); 321 | 322 | // We need to detect if the ranges are lines because we need to handle them differently 323 | highlightRanges.every((range) => isSingleLineRange(range)); 324 | yank(vimState, editor, highlightRanges, false); 325 | flashYankHighlight(editor, highlightRanges); 326 | if (vimState.mode === Mode.Visual) { 327 | enterNormalMode(vimState); 328 | } 329 | }), 330 | 331 | parseKeysExact(['q', 'q'], [Mode.Normal, Mode.Visual], () => { 332 | vscode.commands.executeCommand('editor.action.copyLinesDownAction'); 333 | }), 334 | 335 | parseKeysExact(['Q', 'Q'], [Mode.Normal, Mode.Visual], () => { 336 | vscode.commands.executeCommand('editor.action.copyLinesUpAction'); 337 | }), 338 | 339 | parseKeysExact(['r', 'r'], [Mode.Normal], (vimState, editor) => { 340 | yankLine(vimState, editor); 341 | deleteLine(vimState, editor); 342 | }), 343 | 344 | parseKeysExact(['s', 's'], [Mode.Normal], (vimState, editor) => { 345 | editor.selections = editor.selections.map((selection) => { 346 | return new vscode.Selection( 347 | selection.active.with({ character: 0 }), 348 | positionUtils.lineEnd(editor.document, selection.active), 349 | ); 350 | }); 351 | 352 | enterVisualLineMode(vimState); 353 | setModeCursorStyle(vimState.mode, editor); 354 | setRelativeLineNumbers(vimState.mode, editor); 355 | }), 356 | 357 | parseKeysExact(['S'], [Mode.Normal], (vimState, editor) => { 358 | editor.selections = editor.selections.map((selection) => { 359 | return new vscode.Selection(selection.active, positionUtils.lineEnd(editor.document, selection.active)); 360 | }); 361 | 362 | enterVisualMode(vimState); 363 | setModeCursorStyle(vimState.mode, editor); 364 | setRelativeLineNumbers(vimState.mode, editor); 365 | }), 366 | 367 | // parseKeysExact(['h'], [Mode.Normal], (vimState, editor) => { 368 | // vscode.commands.executeCommand('deleteLeft') 369 | // }), 370 | 371 | parseKeysExact([';'], [Mode.Normal], (vimState, editor) => { 372 | const active = editor.selection.active; 373 | editor.selection = new vscode.Selection(active, active); 374 | }), 375 | 376 | ...gotoActions, 377 | ...windowActions, 378 | ...viewActions, 379 | ...spaceActions, 380 | ...matchActions, 381 | ...unimparedActions, 382 | ]; 383 | 384 | function makeMultiLineSelection( 385 | vimState: HelixState, 386 | editor: vscode.TextEditor, 387 | lineCount: number, 388 | direction: Direction, 389 | ): vscode.Selection[] { 390 | return editor.selections.map((selection) => { 391 | if (direction == Direction.Up) { 392 | const endLine = selection.active.line - lineCount + 1; 393 | const startPos = positionUtils.lineEnd(editor.document, selection.active); 394 | const endPos = endLine >= 0 ? new vscode.Position(endLine, 0) : new vscode.Position(0, 0); 395 | return new vscode.Selection(startPos, endPos); 396 | } else { 397 | const endLine = selection.active.line + lineCount - 1; 398 | const startPos = new vscode.Position(selection.active.line, 0); 399 | const endPos = 400 | endLine < editor.document.lineCount 401 | ? new vscode.Position(endLine, editor.document.lineAt(endLine).text.length) 402 | : positionUtils.lastChar(editor.document); 403 | 404 | return new vscode.Selection(startPos, endPos); 405 | } 406 | }); 407 | } 408 | 409 | function deleteLines( 410 | vimState: HelixState, 411 | editor: vscode.TextEditor, 412 | lineCount: number, 413 | direction: Direction = Direction.Down, 414 | ): void { 415 | const selections = editor.selections.map((selection) => { 416 | if (direction == Direction.Up) { 417 | const endLine = selection.active.line - lineCount; 418 | if (endLine >= 0) { 419 | const startPos = positionUtils.lineEnd(editor.document, selection.active); 420 | const endPos = new vscode.Position(endLine, editor.document.lineAt(endLine).text.length); 421 | return new vscode.Selection(startPos, endPos); 422 | } else { 423 | const startPos = 424 | selection.active.line + 1 <= editor.document.lineCount 425 | ? new vscode.Position(selection.active.line + 1, 0) 426 | : positionUtils.lineEnd(editor.document, selection.active); 427 | 428 | const endPos = new vscode.Position(0, 0); 429 | return new vscode.Selection(startPos, endPos); 430 | } 431 | } else { 432 | const endLine = selection.active.line + lineCount; 433 | if (endLine <= editor.document.lineCount - 1) { 434 | const startPos = new vscode.Position(selection.active.line, 0); 435 | const endPos = new vscode.Position(endLine, 0); 436 | return new vscode.Selection(startPos, endPos); 437 | } else { 438 | const startPos = 439 | selection.active.line - 1 >= 0 440 | ? new vscode.Position( 441 | selection.active.line - 1, 442 | editor.document.lineAt(selection.active.line - 1).text.length, 443 | ) 444 | : new vscode.Position(selection.active.line, 0); 445 | 446 | const endPos = positionUtils.lastChar(editor.document); 447 | return new vscode.Selection(startPos, endPos); 448 | } 449 | } 450 | }); 451 | 452 | editor 453 | .edit((builder) => { 454 | selections.forEach((sel) => builder.replace(sel, '')); 455 | }) 456 | .then(() => { 457 | editor.selections = editor.selections.map((selection) => { 458 | const character = editor.document.lineAt(selection.active.line).firstNonWhitespaceCharacterIndex; 459 | const newPosition = selection.active.with({ character: character }); 460 | return new vscode.Selection(newPosition, newPosition); 461 | }); 462 | }); 463 | } 464 | 465 | function deleteLine(vimState: HelixState, editor: vscode.TextEditor, direction: Direction = Direction.Down): void { 466 | deleteLines(vimState, editor, 1, direction); 467 | } 468 | 469 | function yankLine(vimState: HelixState, editor: vscode.TextEditor): void { 470 | vimState.registers = { 471 | contentsList: editor.selections.map((selection) => { 472 | return editor.document.lineAt(selection.active.line).text; 473 | }), 474 | linewise: true, 475 | }; 476 | } 477 | 478 | export function switchToUppercase(editor: vscode.TextEditor): void { 479 | editor.edit((editBuilder) => { 480 | editor.selections.forEach((selection) => { 481 | const text = editor.document.getText(selection); 482 | editBuilder.replace(selection, text.toUpperCase()); 483 | }); 484 | }); 485 | } 486 | 487 | export function incremenet(editor: vscode.TextEditor): void { 488 | // Move the cursor to the first number and incremene the number 489 | // If the cursor is not on a number, then do nothing 490 | editor.edit((editBuilder) => { 491 | editor.selections.forEach((selection) => { 492 | const translatedSelection = selection.with(selection.start, selection.start.translate(0, 1)); 493 | const text = editor.document.getText(translatedSelection); 494 | const number = parseInt(text, 10); 495 | if (!isNaN(number)) { 496 | editBuilder.replace(translatedSelection, (number + 1).toString()); 497 | } 498 | }); 499 | }); 500 | } 501 | 502 | export function decrement(editor: vscode.TextEditor): void { 503 | // Move the cursor to the first number and incremene the number 504 | // If the cursor is not on a number, then do nothing 505 | editor.edit((editBuilder) => { 506 | editor.selections.forEach((selection) => { 507 | const translatedSelection = selection.with(selection.start, selection.start.translate(0, 1)); 508 | const text = editor.document.getText(translatedSelection); 509 | const number = parseInt(text, 10); 510 | if (!isNaN(number)) { 511 | editBuilder.replace(translatedSelection, (number - 1).toString()); 512 | } 513 | }); 514 | }); 515 | } 516 | -------------------------------------------------------------------------------- /src/actions/operator_ranges.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { arrayFindLast } from '../array_utils'; 4 | import { blockRange } from '../block_utils'; 5 | import { HelixState } from '../helix_state_types'; 6 | import { indentLevelRange } from '../indent_utils'; 7 | import { paragraphBackward, paragraphForward, paragraphRangeInner, paragraphRangeOuter } from '../paragraph_utils'; 8 | import { createOperatorRangeExactKeys, createOperatorRangeRegex } from '../parse_keys'; 9 | import { OperatorRange } from '../parse_keys_types'; 10 | import * as positionUtils from '../position_utils'; 11 | import { findQuoteRange, quoteRanges } from '../quote_utils'; 12 | import { searchBackward, searchBackwardBracket, searchForward, searchForwardBracket } from '../search_utils'; 13 | import { getTags } from '../tag_utils'; 14 | import { whitespaceWordRanges, wordRanges } from '../word_utils'; 15 | // import KeyMap from "./keymap"; 16 | 17 | export const operatorRanges: OperatorRange[] = [ 18 | // createOperatorRangeExactKeys( 19 | // [KeyMap.Motions.MoveRight], 20 | // false, 21 | // (vimState, document, position) => { 22 | // const right = positionUtils.right(document, position); 23 | 24 | // if (right.isEqual(position)) { 25 | // return undefined; 26 | // } else { 27 | // return new vscode.Range(position, right); 28 | // } 29 | // } 30 | // ), 31 | // createOperatorRangeExactKeys( 32 | // [KeyMap.Motions.MoveLeft], 33 | // false, 34 | // (vimState, document, position) => { 35 | // const left = positionUtils.left(position); 36 | 37 | // if (left.isEqual(position)) { 38 | // return undefined; 39 | // } else { 40 | // return new vscode.Range(position, left); 41 | // } 42 | // } 43 | // ), 44 | // createOperatorRangeExactKeys( 45 | // [KeyMap.Motions.MoveUp], 46 | // true, 47 | // (vimState, document, position) => { 48 | // if (position.line === 0) { 49 | // return new vscode.Range( 50 | // new vscode.Position(0, 0), 51 | // positionUtils.lineEnd(document, position) 52 | // ); 53 | // } else { 54 | // return new vscode.Range( 55 | // new vscode.Position(position.line - 1, 0), 56 | // positionUtils.lineEnd(document, position) 57 | // ); 58 | // } 59 | // } 60 | // ), 61 | 62 | // createOperatorRangeExactKeys( 63 | // [KeyMap.Motions.MoveDown], 64 | // true, 65 | // (vimState, document, position) => { 66 | // if (position.line === document.lineCount - 1) { 67 | // return new vscode.Range( 68 | // new vscode.Position(position.line, 0), 69 | // positionUtils.lineEnd(document, position) 70 | // ); 71 | // } else { 72 | // return new vscode.Range( 73 | // new vscode.Position(position.line, 0), 74 | // positionUtils.lineEnd( 75 | // document, 76 | // position.with({ line: position.line + 1 }) 77 | // ) 78 | // ); 79 | // } 80 | // } 81 | // ), 82 | 83 | createOperatorRangeExactKeys(['w'], false, createWordForwardHandler(wordRanges)), 84 | createOperatorRangeExactKeys(['W'], false, createWordForwardHandler(whitespaceWordRanges)), 85 | 86 | createOperatorRangeExactKeys(['b'], false, createWordBackwardHandler(wordRanges)), 87 | createOperatorRangeExactKeys(['B'], false, createWordBackwardHandler(whitespaceWordRanges)), 88 | 89 | createOperatorRangeExactKeys(['e'], false, createWordEndHandler(wordRanges)), 90 | createOperatorRangeExactKeys(['E'], false, createWordEndHandler(whitespaceWordRanges)), 91 | 92 | createOperatorRangeExactKeys(['i', 'w'], false, createInnerWordHandler(wordRanges)), 93 | createOperatorRangeExactKeys(['i', 'W'], false, createInnerWordHandler(whitespaceWordRanges)), 94 | 95 | createOperatorRangeExactKeys(['a', 'w'], false, createOuterWordHandler(wordRanges)), 96 | createOperatorRangeExactKeys(['a', 'W'], false, createOuterWordHandler(whitespaceWordRanges)), 97 | 98 | createOperatorRangeRegex(/^f(..)$/, /^(f|f.)$/, false, (vimState, document, position, match) => { 99 | const fromPosition = position.with({ character: position.character + 1 }); 100 | const result = searchForward(document, match[1], fromPosition); 101 | 102 | if (result) { 103 | return new vscode.Range(position, result); 104 | } else { 105 | return undefined; 106 | } 107 | }), 108 | 109 | createOperatorRangeRegex(/^F(..)$/, /^(F|F.)$/, false, (vimState, document, position, match) => { 110 | const fromPosition = position.with({ character: position.character - 1 }); 111 | const result = searchBackward(document, match[1], fromPosition); 112 | 113 | if (result) { 114 | return new vscode.Range(position, result); 115 | } else { 116 | return undefined; 117 | } 118 | }), 119 | 120 | createOperatorRangeRegex(/^t(.)$/, /^t$/, false, (vimState, document, position, match) => { 121 | const lineText = document.lineAt(position.line).text; 122 | const result = lineText.indexOf(match[1], position.character + 1); 123 | 124 | if (result >= 0) { 125 | return new vscode.Range(position, position.with({ character: result })); 126 | } else { 127 | return undefined; 128 | } 129 | }), 130 | 131 | createOperatorRangeRegex(/^T(.)$/, /^T$/, false, (vimState, document, position, match) => { 132 | const lineText = document.lineAt(position.line).text; 133 | const result = lineText.lastIndexOf(match[1], position.character - 1); 134 | 135 | if (result >= 0) { 136 | const newPosition = positionUtils.right(document, position.with({ character: result })); 137 | return new vscode.Range(newPosition, position); 138 | } else { 139 | return undefined; 140 | } 141 | }), 142 | 143 | createOperatorRangeExactKeys(['g', 'g'], true, (vimState, document, position) => { 144 | const lineLength = document.lineAt(position.line).text.length; 145 | 146 | return new vscode.Range(new vscode.Position(0, 0), position.with({ character: lineLength })); 147 | }), 148 | 149 | createOperatorRangeExactKeys(['G'], true, (vimState, document, position) => { 150 | const lineLength = document.lineAt(document.lineCount - 1).text.length; 151 | 152 | return new vscode.Range(position.with({ character: 0 }), new vscode.Position(document.lineCount - 1, lineLength)); 153 | }), 154 | 155 | // TODO: return undefined? 156 | createOperatorRangeExactKeys(['}'], true, (vimState, document, position) => { 157 | return new vscode.Range( 158 | position.with({ character: 0 }), 159 | new vscode.Position(paragraphForward(document, position.line), 0), 160 | ); 161 | }), 162 | 163 | // TODO: return undefined? 164 | createOperatorRangeExactKeys(['{'], true, (vimState, document, position) => { 165 | return new vscode.Range( 166 | new vscode.Position(paragraphBackward(document, position.line), 0), 167 | position.with({ character: 0 }), 168 | ); 169 | }), 170 | 171 | createOperatorRangeExactKeys(['i', 'p'], true, (vimState, document, position) => { 172 | const result = paragraphRangeInner(document, position.line); 173 | 174 | if (result) { 175 | return new vscode.Range( 176 | new vscode.Position(result.start, 0), 177 | new vscode.Position(result.end, document.lineAt(result.end).text.length), 178 | ); 179 | } else { 180 | return undefined; 181 | } 182 | }), 183 | 184 | createOperatorRangeExactKeys(['a', 'p'], true, (vimState, document, position) => { 185 | const result = paragraphRangeOuter(document, position.line); 186 | 187 | if (result) { 188 | return new vscode.Range( 189 | new vscode.Position(result.start, 0), 190 | new vscode.Position(result.end, document.lineAt(result.end).text.length), 191 | ); 192 | } else { 193 | return undefined; 194 | } 195 | }), 196 | 197 | createOperatorRangeExactKeys(['i', "'"], false, createInnerQuoteHandler("'")), 198 | createOperatorRangeExactKeys(['a', "'"], false, createOuterQuoteHandler("'")), 199 | 200 | createOperatorRangeExactKeys(['i', '"'], false, createInnerQuoteHandler('"')), 201 | createOperatorRangeExactKeys(['a', '"'], false, createOuterQuoteHandler('"')), 202 | 203 | createOperatorRangeExactKeys(['i', '`'], false, createInnerQuoteHandler('`')), 204 | createOperatorRangeExactKeys(['a', '`'], false, createOuterQuoteHandler('`')), 205 | 206 | createOperatorRangeExactKeys(['i', '('], false, createInnerBracketHandler('(', ')')), 207 | createOperatorRangeExactKeys(['a', '('], false, createOuterBracketHandler('(', ')')), 208 | 209 | createOperatorRangeExactKeys(['i', '{'], false, createInnerBracketHandler('{', '}')), 210 | createOperatorRangeExactKeys(['a', '{'], false, createOuterBracketHandler('{', '}')), 211 | 212 | createOperatorRangeExactKeys(['i', '['], false, createInnerBracketHandler('[', ']')), 213 | createOperatorRangeExactKeys(['a', '['], false, createOuterBracketHandler('[', ']')), 214 | 215 | createOperatorRangeExactKeys(['i', '<'], false, createInnerBracketHandler('<', '>')), 216 | createOperatorRangeExactKeys(['a', '<'], false, createOuterBracketHandler('<', '>')), 217 | 218 | createOperatorRangeExactKeys(['i', 'm'], false, createInnerMatchHandler()), 219 | createOperatorRangeExactKeys(['a', 'm'], false, createOuterMatchHandler()), 220 | 221 | createOperatorRangeExactKeys(['i', 'f'], false, createInnerFunctionHandler()), 222 | createOperatorRangeExactKeys(['a', 'f'], false, createInnerFunctionHandler()), 223 | 224 | createOperatorRangeExactKeys(['i', 't'], false, (vimState, document, position) => { 225 | const tags = getTags(document); 226 | 227 | const closestTag = arrayFindLast(tags, (tag) => { 228 | if (tag.closing) { 229 | return position.isAfterOrEqual(tag.opening.start) && position.isBeforeOrEqual(tag.closing.end); 230 | } else { 231 | // Self-closing tags have no inside 232 | return false; 233 | } 234 | }); 235 | 236 | if (closestTag) { 237 | if (closestTag.closing) { 238 | return new vscode.Range( 239 | closestTag.opening.end.with({ 240 | character: closestTag.opening.end.character + 1, 241 | }), 242 | closestTag.closing.start, 243 | ); 244 | } else { 245 | throw new Error('We should have already filtered out self-closing tags above'); 246 | } 247 | } else { 248 | return undefined; 249 | } 250 | }), 251 | 252 | createOperatorRangeExactKeys(['a', 't'], false, (vimState, document, position) => { 253 | const tags = getTags(document); 254 | 255 | const closestTag = arrayFindLast(tags, (tag) => { 256 | const afterStart = position.isAfterOrEqual(tag.opening.start); 257 | 258 | if (tag.closing) { 259 | return afterStart && position.isBeforeOrEqual(tag.closing.end); 260 | } else { 261 | return afterStart && position.isBeforeOrEqual(tag.opening.end); 262 | } 263 | }); 264 | 265 | if (closestTag) { 266 | if (closestTag.closing) { 267 | return new vscode.Range( 268 | closestTag.opening.start, 269 | closestTag.closing.end.with({ 270 | character: closestTag.closing.end.character + 1, 271 | }), 272 | ); 273 | } else { 274 | return new vscode.Range( 275 | closestTag.opening.start, 276 | closestTag.opening.end.with({ 277 | character: closestTag.opening.end.character + 1, 278 | }), 279 | ); 280 | } 281 | } else { 282 | return undefined; 283 | } 284 | }), 285 | 286 | // TODO: return undefined? 287 | createOperatorRangeExactKeys(['i', 'i'], true, (vimState, document, position) => { 288 | const simpleRange = indentLevelRange(document, position.line); 289 | 290 | return new vscode.Range( 291 | new vscode.Position(simpleRange.start, 0), 292 | new vscode.Position(simpleRange.end, document.lineAt(simpleRange.end).text.length), 293 | ); 294 | }), 295 | 296 | createOperatorRangeExactKeys(['a', 'b'], true, (vimState, document, position) => { 297 | const range = blockRange(document, position); 298 | 299 | return range; 300 | }), 301 | ]; 302 | 303 | function createInnerBracketHandler( 304 | openingChar: string, 305 | closingChar: string, 306 | ): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 307 | return (helixState, document, position) => { 308 | const count = helixState.resolveCount(); 309 | const bracketRange = getBracketRange(document, position, openingChar, closingChar, count); 310 | 311 | if (bracketRange) { 312 | return new vscode.Range( 313 | bracketRange.start.with({ 314 | character: bracketRange.start.character + 1, 315 | }), 316 | bracketRange.end, 317 | ); 318 | } else { 319 | return undefined; 320 | } 321 | }; 322 | } 323 | 324 | function createOuterBracketHandler( 325 | openingChar: string, 326 | closingChar: string, 327 | ): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 328 | return (helixState, document, position) => { 329 | const count = helixState.resolveCount(); 330 | const bracketRange = getBracketRange(document, position, openingChar, closingChar, count); 331 | 332 | if (bracketRange) { 333 | return new vscode.Range(bracketRange.start, bracketRange.end.with({ character: bracketRange.end.character + 1 })); 334 | } else { 335 | return undefined; 336 | } 337 | }; 338 | } 339 | 340 | function getBracketRange( 341 | document: vscode.TextDocument, 342 | position: vscode.Position, 343 | openingChar: string, 344 | closingChar: string, 345 | offset?: number, 346 | ): vscode.Range | undefined { 347 | const lineText = document.lineAt(position.line).text; 348 | const currentChar = lineText[position.character]; 349 | 350 | let start; 351 | let end; 352 | if (currentChar === openingChar) { 353 | start = position; 354 | end = searchForwardBracket(document, openingChar, closingChar, positionUtils.rightWrap(document, position), offset); 355 | } else if (currentChar === closingChar) { 356 | start = searchBackwardBracket( 357 | document, 358 | openingChar, 359 | closingChar, 360 | positionUtils.leftWrap(document, position), 361 | offset, 362 | ); 363 | end = position; 364 | } else { 365 | start = searchBackwardBracket(document, openingChar, closingChar, position, offset); 366 | end = searchForwardBracket(document, openingChar, closingChar, position, offset); 367 | } 368 | 369 | if (start && end) { 370 | return new vscode.Range(start, end); 371 | } else { 372 | return undefined; 373 | } 374 | } 375 | 376 | function createInnerQuoteHandler( 377 | quoteChar: string, 378 | ): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 379 | return (vimState, document, position) => { 380 | const lineText = document.lineAt(position.line).text; 381 | const ranges = quoteRanges(quoteChar, lineText); 382 | const result = findQuoteRange(ranges, position); 383 | 384 | if (result) { 385 | return new vscode.Range(position.with({ character: result.start + 1 }), position.with({ character: result.end })); 386 | } else { 387 | return undefined; 388 | } 389 | }; 390 | } 391 | 392 | function createOuterQuoteHandler( 393 | quoteChar: string, 394 | ): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 395 | return (vimState, document, position) => { 396 | const lineText = document.lineAt(position.line).text; 397 | const ranges = quoteRanges(quoteChar, lineText); 398 | const result = findQuoteRange(ranges, position); 399 | 400 | if (result) { 401 | return new vscode.Range(position.with({ character: result.start }), position.with({ character: result.end + 1 })); 402 | } else { 403 | return undefined; 404 | } 405 | }; 406 | } 407 | 408 | function createWordForwardHandler( 409 | wordRangesFunction: (text: string) => { start: number; end: number }[], 410 | ): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range { 411 | return (vimState, document, position) => { 412 | const lineText = document.lineAt(position.line).text; 413 | const ranges = wordRangesFunction(lineText); 414 | 415 | const result = ranges.find((x) => x.start > position.character); 416 | 417 | if (result) { 418 | return new vscode.Range(position, position.with({ character: result.start })); 419 | } else { 420 | return new vscode.Range(position, position.with({ character: lineText.length })); 421 | } 422 | }; 423 | } 424 | 425 | function createWordBackwardHandler( 426 | wordRangesFunction: (text: string) => { start: number; end: number }[], 427 | ): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 428 | return (vimState, document, position) => { 429 | const lineText = document.lineAt(position.line).text; 430 | const ranges = wordRangesFunction(lineText); 431 | 432 | const result = ranges.reverse().find((x) => x.start < position.character); 433 | 434 | if (result) { 435 | return new vscode.Range(position.with({ character: result.start }), position); 436 | } else { 437 | return undefined; 438 | } 439 | }; 440 | } 441 | 442 | function createWordEndHandler( 443 | wordRangesFunction: (text: string) => { start: number; end: number }[], 444 | ): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 445 | return (vimState, document, position) => { 446 | const lineText = document.lineAt(position.line).text; 447 | const ranges = wordRangesFunction(lineText); 448 | 449 | const result = ranges.find((x) => x.end > position.character); 450 | 451 | if (result) { 452 | return new vscode.Range(position, positionUtils.right(document, position.with({ character: result.end }))); 453 | } else { 454 | return undefined; 455 | } 456 | }; 457 | } 458 | 459 | function createInnerWordHandler( 460 | wordRangesFunction: (text: string) => { start: number; end: number }[], 461 | ): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 462 | return (vimState, document, position) => { 463 | const lineText = document.lineAt(position.line).text; 464 | const ranges = wordRangesFunction(lineText); 465 | 466 | const result = ranges.find((x) => x.start <= position.character && position.character <= x.end); 467 | 468 | if (result) { 469 | return new vscode.Range( 470 | position.with({ character: result.start }), 471 | positionUtils.right(document, position.with({ character: result.end })), 472 | ); 473 | } else { 474 | return undefined; 475 | } 476 | }; 477 | } 478 | 479 | function createOuterWordHandler( 480 | wordRangesFunction: (text: string) => { start: number; end: number }[], 481 | ): (vimState: HelixState, document: vscode.TextDocument, position: vscode.Position) => vscode.Range | undefined { 482 | return (vimState, document, position) => { 483 | const lineText = document.lineAt(position.line).text; 484 | const ranges = wordRangesFunction(lineText); 485 | 486 | for (let i = 0; i < ranges.length; ++i) { 487 | const range = ranges[i]; 488 | 489 | if (range.start <= position.character && position.character <= range.end) { 490 | if (i < ranges.length - 1) { 491 | return new vscode.Range( 492 | position.with({ character: range.start }), 493 | position.with({ character: ranges[i + 1].start }), 494 | ); 495 | } else if (i > 0) { 496 | return new vscode.Range( 497 | positionUtils.right(document, position.with({ character: ranges[i - 1].end })), 498 | positionUtils.right(document, position.with({ character: range.end })), 499 | ); 500 | } else { 501 | return new vscode.Range( 502 | position.with({ character: range.start }), 503 | positionUtils.right(document, position.with({ character: range.end })), 504 | ); 505 | } 506 | } 507 | } 508 | 509 | return undefined; 510 | }; 511 | } 512 | 513 | /* 514 | * Implements going to nearest matching brackets from the cursor. 515 | * This will need to call the other `createInnerBracketHandler` functions and get the smallest range from them. 516 | * This should ensure that we're fetching the nearest bracket pair. 517 | **/ 518 | function createInnerMatchHandler(): ( 519 | helixState: HelixState, 520 | document: vscode.TextDocument, 521 | position: vscode.Position, 522 | ) => vscode.Range | undefined { 523 | return (helixState, document, position) => { 524 | const count = helixState.resolveCount(); 525 | // Get all ranges from our position then reduce down to the shortest one 526 | const bracketRange = [ 527 | getBracketRange(document, position, '(', ')', count), 528 | getBracketRange(document, position, '{', '}', count), 529 | getBracketRange(document, position, '<', '>', count), 530 | getBracketRange(document, position, '[', ']', count), 531 | ].reduce((acc, range) => { 532 | if (range) { 533 | if (!acc) { 534 | return range; 535 | } else { 536 | return range.contains(acc) ? acc : range; 537 | } 538 | } else { 539 | return acc; 540 | } 541 | }, undefined); 542 | 543 | return bracketRange?.with(new vscode.Position(bracketRange.start.line, bracketRange.start.character + 1)); 544 | }; 545 | } 546 | 547 | /* 548 | * Implements going to nearest matching brackets from the cursor. 549 | * This will need to call the other `createInnerBracketHandler` functions and get the smallest range from them. 550 | * This should ensure that we're fetching the nearest bracket pair. 551 | **/ 552 | function createOuterMatchHandler(): ( 553 | vimState: HelixState, 554 | document: vscode.TextDocument, 555 | position: vscode.Position, 556 | ) => vscode.Range | undefined { 557 | return (_, document, position) => { 558 | // Get all ranges from our position then reduce down to the shortest one 559 | const bracketRange = [ 560 | getBracketRange(document, position, '(', ')'), 561 | getBracketRange(document, position, '{', '}'), 562 | getBracketRange(document, position, '<', '>'), 563 | getBracketRange(document, position, '[', ']'), 564 | ].reduce((acc, range) => { 565 | if (range) { 566 | if (!acc) { 567 | return range; 568 | } else { 569 | return range.contains(acc) ? acc : range; 570 | } 571 | } else { 572 | return acc; 573 | } 574 | }, undefined); 575 | 576 | return bracketRange?.with(undefined, new vscode.Position(bracketRange.end.line, bracketRange.end.character + 1)); 577 | }; 578 | } 579 | 580 | function createInnerFunctionHandler(): ( 581 | helixState: HelixState, 582 | document: vscode.TextDocument, 583 | position: vscode.Position, 584 | ) => vscode.Range | undefined { 585 | return (helixState, _, position) => { 586 | return helixState.symbolProvider.getContainingSymbolRange(position); 587 | }; 588 | } 589 | --------------------------------------------------------------------------------