From 5f6ef5fadaf1448c79e1943bd2a976eeb89746fb Mon Sep 17 00:00:00 2001 From: Jason Fields Date: Thu, 26 Dec 2019 01:36:27 -0500 Subject: [PATCH] `g?` (rot13) support (#4367) Fixes #4363 --- ROADMAP.md | 4 +-- src/actions/commands/actions.ts | 7 +++++ src/actions/operator.ts | 55 +++++++++++++++++++++++++++++++- src/actions/plugins/sneak.ts | 6 ++-- src/configuration/vimrc.ts | 5 +-- src/state/recordedState.ts | 9 +++--- test/cmd_line/split.test.ts | 6 +++- test/cmd_line/vsplit.test.ts | 6 +++- test/operator/rot13.test.ts | 56 +++++++++++++++++++++++++++++++++ 9 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 test/operator/rot13.test.ts diff --git a/ROADMAP.md b/ROADMAP.md index cd5ef95a..75975967 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -355,8 +355,8 @@ moving around: | :white_check_mark: | g~{motion} | switch case for the text that is moved over with {motion} | | :white_check_mark: | gu{motion} | make the text that is moved over with {motion} lowercase | | :white_check_mark: | gU{motion} | make the text that is moved over with {motion} uppercase | -| :arrow_down: | {visual}g? | perform rot13 encoding on highlighted text | -| :arrow_down: | g?{motion} | perform rot13 encoding on the text that is moved over with {motion} | +| :white_check_mark: | {visual}g? | perform rot13 encoding on highlighted text | +| :white_check_mark: | g?{motion} | perform rot13 encoding on the text that is moved over with {motion} | | :white_check_mark: | :1234: CTRL-A | add N to the number at or after the cursor | | :white_check_mark: | :1234: CTRL-X | subtract N from the number at or after the cursor | | :white_check_mark: | :1234: <{motion} | move the lines that are moved over with {motion} one shiftwidth left | diff --git a/src/actions/commands/actions.ts b/src/actions/commands/actions.ts index 9812ee41..cab9387e 100644 --- a/src/actions/commands/actions.ts +++ b/src/actions/commands/actions.ts @@ -1397,6 +1397,13 @@ export class CommandSearchBackwards extends BaseCommand { isMotion = true; isJump = true; + public doesActionApply(vimState: VimState, keysPressed: string[]): boolean { + // Prevent collision with `g?` (rot13 operator) + return ( + super.doesActionApply(vimState, keysPressed) && vimState.recordedState.operator === undefined + ); + } + public async exec(position: Position, vimState: VimState): Promise { globalState.searchState = new SearchState( SearchDirection.Backward, diff --git a/src/actions/operator.ts b/src/actions/operator.ts index b4dfe9c8..a9e80e16 100644 --- a/src/actions/operator.ts +++ b/src/actions/operator.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import { Position, PositionDiff } from './../common/motion/position'; import { Range } from './../common/motion/range'; import { configuration } from './../configuration/configuration'; -import { Mode } from './../mode/mode'; +import { Mode, isVisualMode } from './../mode/mode'; import { Register, RegisterMode } from './../register/register'; import { VimState } from './../state/vimState'; import { TextEditor } from './../textEditor'; @@ -783,6 +783,59 @@ export class CommentOperator extends BaseOperator { } } +@RegisterAction +export class ROT13Operator extends BaseOperator { + public keys = ['g', '?']; + public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock]; + + public async run(vimState: VimState, start: Position, end: Position): Promise { + let selections: vscode.Selection[]; + if (isVisualMode(vimState.currentMode)) { + selections = vimState.editor.selections; + } else if (vimState.currentRegisterMode === RegisterMode.LineWise) { + selections = [new vscode.Selection(start.getLineBegin(), end.getLineEnd())]; + } else { + selections = [new vscode.Selection(start, end.getRight())]; + } + + for (const range of selections) { + const original = TextEditor.getText(range); + vimState.recordedState.transformations.push({ + type: 'replaceText', + text: ROT13Operator.rot13(original), + start: Position.FromVSCodePosition(range.start), + end: Position.FromVSCodePosition(range.end), + }); + } + + return vimState; + } + + /** + * https://en.wikipedia.org/wiki/ROT13 + */ + public static rot13(str: string) { + return str + .split('') + .map((char: string) => { + let charCode = char.charCodeAt(0); + + if (char >= 'a' && char <= 'z') { + const a = 'a'.charCodeAt(0); + charCode = ((charCode - a + 13) % 26) + a; + } + + if (char >= 'A' && char <= 'Z') { + const A = 'A'.charCodeAt(0); + charCode = ((charCode - A + 13) % 26) + A; + } + + return String.fromCharCode(charCode); + }) + .join(''); + } +} + @RegisterAction export class CommentBlockOperator extends BaseOperator { public keys = ['g', 'C']; diff --git a/src/actions/plugins/sneak.ts b/src/actions/plugins/sneak.ts index e72283c5..e6347273 100644 --- a/src/actions/plugins/sneak.ts +++ b/src/actions/plugins/sneak.ts @@ -49,7 +49,8 @@ export class SneakForward extends BaseMovement { const ignorecase = configuration.sneakUseIgnorecaseAndSmartcase && - configuration.ignorecase && !(configuration.smartcase && /[A-Z]/.test(searchString)); + configuration.ignorecase && + !(configuration.smartcase && /[A-Z]/.test(searchString)); // Check for matches if (ignorecase) { @@ -112,7 +113,8 @@ export class SneakBackward extends BaseMovement { const ignorecase = configuration.sneakUseIgnorecaseAndSmartcase && - configuration.ignorecase && !(configuration.smartcase && /[A-Z]/.test(searchString)); + configuration.ignorecase && + !(configuration.smartcase && /[A-Z]/.test(searchString)); // Check for matches if (ignorecase) { diff --git a/src/configuration/vimrc.ts b/src/configuration/vimrc.ts index 1650f53b..643f32a4 100644 --- a/src/configuration/vimrc.ts +++ b/src/configuration/vimrc.ts @@ -45,10 +45,7 @@ class VimrcImpl { const mappings = (() => { switch (remap.keyRemappingType) { case 'map': - return [ - config.normalModeKeyBindings, - config.visualModeKeyBindings, - ]; + return [config.normalModeKeyBindings, config.visualModeKeyBindings]; case 'nmap': return [config.normalModeKeyBindings]; case 'vmap': diff --git a/src/state/recordedState.ts b/src/state/recordedState.ts index 9e8aad74..5d0810aa 100644 --- a/src/state/recordedState.ts +++ b/src/state/recordedState.ts @@ -113,11 +113,11 @@ export class RecordedState { */ public get operator(): BaseOperator { let list = this.actionsRun.filter(a => a instanceof BaseOperator).reverse(); - return list[0] as any; + return list[0] as BaseOperator; } public get operators(): BaseOperator[] { - return this.actionsRun.filter(a => a instanceof BaseOperator).reverse() as any; + return this.actionsRun.filter(a => a instanceof BaseOperator).reverse() as BaseOperator[]; } /** @@ -128,7 +128,7 @@ export class RecordedState { // TODO - disregard , then assert this is of length 1. - return list[0] as any; + return list[0] as BaseCommand; } public get hasRunAMovement(): boolean { @@ -167,7 +167,8 @@ export class RecordedState { mode !== Mode.SearchInProgressMode && mode !== Mode.CommandlineInProgress && (this.hasRunAMovement || - mode === Mode.Visual || mode === Mode.VisualLine || + mode === Mode.Visual || + mode === Mode.VisualLine || (this.operators.length > 1 && this.operators.reverse()[0].constructor === this.operators.reverse()[1].constructor)) ); diff --git a/test/cmd_line/split.test.ts b/test/cmd_line/split.test.ts index 0367607c..73009647 100644 --- a/test/cmd_line/split.test.ts +++ b/test/cmd_line/split.test.ts @@ -21,7 +21,11 @@ suite('Horizontal split', () => { await commandLine.Run(cmd, modeHandler.vimState); await WaitForEditorsToClose(2); - assert.strictEqual(vscode.window.visibleTextEditors.length, 2, 'Editor did not split in 1 sec'); + assert.strictEqual( + vscode.window.visibleTextEditors.length, + 2, + 'Editor did not split in 1 sec' + ); }); } }); diff --git a/test/cmd_line/vsplit.test.ts b/test/cmd_line/vsplit.test.ts index 8a52f3c5..137b0f64 100644 --- a/test/cmd_line/vsplit.test.ts +++ b/test/cmd_line/vsplit.test.ts @@ -21,7 +21,11 @@ suite('Vertical split', () => { await commandLine.Run(cmd, modeHandler.vimState); await WaitForEditorsToClose(2); - assert.strictEqual(vscode.window.visibleTextEditors.length, 2, 'Editor did not split in 1 sec'); + assert.strictEqual( + vscode.window.visibleTextEditors.length, + 2, + 'Editor did not split in 1 sec' + ); }); } }); diff --git a/test/operator/rot13.test.ts b/test/operator/rot13.test.ts new file mode 100644 index 00000000..495d623c --- /dev/null +++ b/test/operator/rot13.test.ts @@ -0,0 +1,56 @@ +import * as assert from 'assert'; + +import { getTestingFunctions } from '../testSimplifier'; + +import { setupWorkspace, cleanUpWorkspace } from '../testUtils'; +import { ROT13Operator } from '../../src/actions/operator'; + +suite('rot13 operator', () => { + const { newTest, newTestOnly, newTestSkip } = getTestingFunctions(); + + setup(async () => { + await setupWorkspace(); + }); + + teardown(cleanUpWorkspace); + + test('rot13() unit test', () => { + const testCases = [ + ['abcdefghijklmnopqrstuvwxyz', 'nopqrstuvwxyzabcdefghijklm'], + ['ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'NOPQRSTUVWXYZABCDEFGHIJKLM'], + ['!@#$%^&*()', '!@#$%^&*()'], + ['âéü', 'âéü'], + ]; + for (const [input, output] of testCases) { + assert.strictEqual(ROT13Operator.rot13(input), output); + } + }); + + newTest({ + title: 'g?j works', + start: ['a|bc', 'def', 'ghi'], + keysPressed: 'g?j', + end: ['n|op', 'qrs', 'ghi'], + }); + + newTest({ + title: 'g? in visual mode works', + start: ['a|bc', 'def', 'ghi'], + keysPressed: 'vj$g?', + end: ['a|op', 'qrs', 'ghi'], + }); + + newTest({ + title: 'g? in visual line mode works', + start: ['a|bc', 'def', 'ghi'], + keysPressed: 'Vj$g?', + end: ['|nop', 'qrs', 'ghi'], + }); + + newTest({ + title: 'g? in visual block mode works', + start: ['a|bc', 'def', 'ghi'], + keysPressed: 'j$g?', + end: ['a|op', 'drs', 'ghi'], + }); +});