Skip to content

Commit

Permalink
Add CamelCaseMotion plugin (#3483)
Browse files Browse the repository at this point in the history
* Add CamelCaseMotion Plugin

Fixes #1220

* update @node/types

* adjust tests to only load config once

* nest camelCaseMotion configuration

* fix merge error
  • Loading branch information
jkillian authored and jpoon committed Feb 18, 2019
1 parent fa0285d commit d8564f9
Show file tree
Hide file tree
Showing 11 changed files with 519 additions and 6 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ VSCodeVim is a Vim emulator for [Visual Studio Code](https://code.visualstudio.c
- [vim-commentary](#vim-commentary)
- [vim-indent-object](#vim-indent-object)
- [vim-sneak](#vim-sneak)
- [CamelCaseMotion](#camelcasemotion)
- [Input Method](#input-method)
- [VSCodeVim tricks](#-vscodevim-tricks)
- [F.A.Q / Troubleshooting](#-faq)
Expand Down Expand Up @@ -505,6 +506,25 @@ Once sneak is active, initiate motions using the following commands. For operato
| `<operator>z<char><char>` | Perform `<operator>` forward to the first occurence of `<char><char>` |
| `<operator>Z<char><char>` | Perform `<operator>` backward to the first occurence of `<char><char>` |

### CamelCaseMotion

Based on [CamelCaseMotion](https://github.com/bkad/CamelCaseMotion), though not an exact emulation. This plugin provides an easier way to move through camelCase and snake_case words.

| Setting | Description | Type | Default Value |
| -------------------------- | ------------------------------ | ------- | ------------- |
| vim.camelCaseMotion.enable | Enable/disable CamelCaseMotion | Boolean | false |

Once CamelCaseMotion is enabled, the following motions are available:

| Motion Command | Description |
| ---------------------- | -------------------------------------------------------------------------- |
| `<leader>w` | Move forward to the start of the next camelCase or snake_case word segment |
| `<leader>e` | Move forward to the next end of a camelCase or snake_case word segment |
| `<leader>b` | Move back to the prior beginning of a camelCase or snake_case word segment |
| `<operator>i<leader>w` | Select/change/delete/etc. the current camelCase or snake_case word segment |

By default, `<leader>` is mapped to `\`, so for example, `d2i\w` would delete the current and next camelCase word segment.

### Input Method

Disable input method when exiting Insert Mode.
Expand Down
2 changes: 1 addition & 1 deletion build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:8.15
FROM node:10.15

ARG DEBIAN_FRONTEND=noninteractive

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,11 @@
"description": "Override the 'ignorecase' option if the search pattern contains upper case characters.",
"default": true
},
"vim.camelCaseMotion.enable": {
"type": "boolean",
"description": "Enable the CamelCaseMotion plugin for Vim.",
"default": false
},
"vim.easymotion": {
"type": "boolean",
"description": "Enable the EasyMotion plugin for Vim.",
Expand Down Expand Up @@ -711,7 +716,7 @@
"@types/diff-match-patch": "1.0.32",
"@types/lodash": "4.14.121",
"@types/mocha": "5.2.5",
"@types/node": "9.6.42",
"@types/node": "10.12.25",
"@types/sinon": "7.0.5",
"gulp": "4.0.0",
"gulp-bump": "3.1.3",
Expand Down
1 change: 1 addition & 0 deletions src/actions/include-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import './commands/insert';
import './commands/actions';

// plugin
import './plugins/camelCaseMotion';
import './plugins/easymotion/easymotion.cmd';
import './plugins/easymotion/registerMoveActions';
import './plugins/sneak';
Expand Down
117 changes: 117 additions & 0 deletions src/actions/plugins/camelCaseMotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { TextObjectMovement } from '../textobject';
import { RegisterAction } from '../base';
import { ModeName } from '../../mode/mode';
import { Position } from '../../common/motion/position';
import { VimState } from '../../state/vimState';
import { IMovement, BaseMovement } from '../motion';
import { TextEditor } from '../../textEditor';
import { configuration } from '../../configuration/configuration';
import { ChangeOperator } from '../operator';

class CamelCaseBaseMovement extends BaseMovement {
public doesActionApply(vimState: VimState, keysPressed: string[]) {
return configuration.camelCaseMotion.enable && super.doesActionApply(vimState, keysPressed);
}

public couldActionApply(vimState: VimState, keysPressed: string[]) {
return configuration.camelCaseMotion.enable && super.couldActionApply(vimState, keysPressed);
}
}

class CamelCaseTextObjectMovement extends TextObjectMovement {
public doesActionApply(vimState: VimState, keysPressed: string[]) {
return configuration.camelCaseMotion.enable && super.doesActionApply(vimState, keysPressed);
}

public couldActionApply(vimState: VimState, keysPressed: string[]) {
return configuration.camelCaseMotion.enable && super.couldActionApply(vimState, keysPressed);
}
}

// based off of `MoveWordBegin`
@RegisterAction
class MoveCamelCaseWordBegin extends CamelCaseBaseMovement {
keys = ['<leader>', 'w'];

public async execAction(position: Position, vimState: VimState): Promise<Position> {
if (
!configuration.changeWordIncludesWhitespace &&
vimState.recordedState.operator instanceof ChangeOperator
) {
// TODO use execForOperator? Or maybe dont?

// See note for w
return position.getCurrentCamelCaseWordEnd().getRight();
} else {
return position.getCamelCaseWordRight();
}
}
}

// based off of `MoveWordEnd`
@RegisterAction
class MoveCamelCaseWordEnd extends CamelCaseBaseMovement {
keys = ['<leader>', 'e'];

public async execAction(position: Position, vimState: VimState): Promise<Position> {
return position.getCurrentCamelCaseWordEnd();
}

public async execActionForOperator(position: Position, vimState: VimState): Promise<Position> {
let end = position.getCurrentCamelCaseWordEnd();

return new Position(end.line, end.character + 1);
}
}

// based off of `MoveBeginningWord`
@RegisterAction
class MoveBeginningCamelCaseWord extends CamelCaseBaseMovement {
keys = ['<leader>', 'b'];

public async execAction(position: Position, vimState: VimState): Promise<Position> {
return position.getCamelCaseWordLeft();
}
}

// based off of `SelectInnerWord`
@RegisterAction
export class SelectInnerCamelCaseWord extends CamelCaseTextObjectMovement {
modes = [ModeName.Normal, ModeName.Visual];
keys = ['i', '<leader>', 'w'];

public async execAction(position: Position, vimState: VimState): Promise<IMovement> {
let start: Position;
let stop: Position;
const currentChar = TextEditor.getLineAt(position).text[position.character];

if (/\s/.test(currentChar)) {
start = position.getLastCamelCaseWordEnd().getRight();
stop = position.getCamelCaseWordRight().getLeftThroughLineBreaks();
} else {
start = position.getCamelCaseWordLeft(true);
stop = position.getCurrentCamelCaseWordEnd(true);
}

if (
vimState.currentMode === ModeName.Visual &&
!vimState.cursorStopPosition.isEqual(vimState.cursorStartPosition)
) {
start = vimState.cursorStartPosition;

if (vimState.cursorStopPosition.isBefore(vimState.cursorStartPosition)) {
// If current cursor postion is before cursor start position, we are selecting words in reverser order.
if (/\s/.test(currentChar)) {
stop = position.getLastCamelCaseWordEnd().getRight();
} else {
stop = position.getCamelCaseWordLeft(true);
}
}
}

return {
start: start,
stop: stop,
};
}
}
56 changes: 55 additions & 1 deletion src/common/motion/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export class Position extends vscode.Position {

private _nonWordCharRegex: RegExp;
private _nonBigWordCharRegex: RegExp;
private _nonCamelCaseWordCharRegex: RegExp;
private _sentenceEndRegex: RegExp;
private _nonFileNameRegex: RegExp;

Expand All @@ -109,6 +110,7 @@ export class Position extends vscode.Position {

this._nonWordCharRegex = this.makeWordRegex(Position.NonWordCharacters);
this._nonBigWordCharRegex = this.makeWordRegex(Position.NonBigWordCharacters);
this._nonCamelCaseWordCharRegex = this.makeCamelCaseWordRegex(Position.NonWordCharacters);
this._sentenceEndRegex = /[\.!\?]{1}([ \n\t]+|$)/g;
this._nonFileNameRegex = this.makeWordRegex(Position.NonFileCharacters);
}
Expand Down Expand Up @@ -516,6 +518,10 @@ export class Position extends vscode.Position {
return this.getWordLeftWithRegex(this._nonBigWordCharRegex, inclusive);
}

public getCamelCaseWordLeft(inclusive: boolean = false): Position {
return this.getWordLeftWithRegex(this._nonCamelCaseWordCharRegex, inclusive);
}

public getFilePathLeft(inclusive: boolean = false): Position {
return this.getWordLeftWithRegex(this._nonFileNameRegex, inclusive);
}
Expand All @@ -531,6 +537,10 @@ export class Position extends vscode.Position {
return this.getWordRightWithRegex(this._nonBigWordCharRegex);
}

public getCamelCaseWordRight(inclusive: boolean = false): Position {
return this.getWordRightWithRegex(this._nonCamelCaseWordCharRegex);
}

public getFilePathRight(inclusive: boolean = false): Position {
return this.getWordRightWithRegex(this._nonFileNameRegex, inclusive);
}
Expand All @@ -543,6 +553,10 @@ export class Position extends vscode.Position {
return this.getLastWordEndWithRegex(this._nonBigWordCharRegex);
}

public getLastCamelCaseWordEnd(): Position {
return this.getLastWordEndWithRegex(this._nonCamelCaseWordCharRegex);
}

/**
* Inclusive is true if we consider the current position a valid result, false otherwise.
*/
Expand All @@ -557,6 +571,13 @@ export class Position extends vscode.Position {
return this.getCurrentWordEndWithRegex(this._nonBigWordCharRegex, inclusive);
}

/**
* Inclusive is true if we consider the current position a valid result, false otherwise.
*/
public getCurrentCamelCaseWordEnd(inclusive: boolean = false): Position {
return this.getCurrentWordEndWithRegex(this._nonCamelCaseWordCharRegex, inclusive);
}

/**
* Get the boundary position of the section.
*/
Expand Down Expand Up @@ -831,6 +852,39 @@ export class Position extends vscode.Position {
return result;
}

private makeCamelCaseWordRegex(characterSet: string): RegExp {
const escaped = characterSet && _.escapeRegExp(characterSet).replace(/-/g, '\\-');
const segments: string[] = [];

// prettier-ignore
const firstSegment =
'(' + // OPEN: group for matching camel case words
`[^\\s${escaped}]` + // words can start with any word character
'(?:' + // OPEN: group for characters after initial char
`(?:(?<=[A-Z_])[A-Z](?=[\\sA-Z0-9${escaped}_]))+` + // If first char was a capital
// the word can continue with all caps
'|' + // OR
`(?:(?<=[0-9_])[0-9](?=[\\sA-Z0-9${escaped}_]))+` + // If first char was a digit
// the word can continue with all digits
'|' + // OR
`(?:(?<=[_])[_](?=[\\s${escaped}_]))+` + // Continue with all underscores
'|' + // OR
`[^\\sA-Z0-9${escaped}_]*` + // Continue with regular characters
')' + // END: group for characters after initial char
')' + // END: group for matching camel case words
'';

segments.push(firstSegment);
segments.push(`[${escaped}]+`);
segments.push(`$^`);

// it can be difficult to grok the behavior of the above regex
// feel free to check out https://regex101.com/r/mkVeiH/1 as a live example
const result = new RegExp(segments.join('|'), 'g');

return result;
}

private getAllPositions(line: string, regex: RegExp): number[] {
let positions: number[] = [];
let result = regex.exec(line);
Expand Down Expand Up @@ -987,7 +1041,7 @@ export class Position extends vscode.Position {
.getRightThroughLineBreaks()
.compareTo(this);

return (newPositionBeforeThis && (index < this.character || currentLine < this.line));
return newPositionBeforeThis && (index < this.character || currentLine < this.line);
});

if (newCharacter !== undefined) {
Expand Down
5 changes: 5 additions & 0 deletions src/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
IAutoSwitchInputMethod,
IDebugConfiguration,
Digraph,
ICamelCaseMotionConfiguration,
} from './iconfiguration';

const packagejson: {
Expand Down Expand Up @@ -170,6 +171,10 @@ class Configuration implements IConfiguration {

autoindent = true;

camelCaseMotion: ICamelCaseMotionConfiguration = {
enable: true,
};

sneak = false;
sneakUseIgnorecaseAndSmartcase = false;

Expand Down
12 changes: 12 additions & 0 deletions src/configuration/iconfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export interface IDebugConfiguration {
loggingLevelForConsole: 'error' | 'warn' | 'info' | 'verbose' | 'debug';
}

export interface ICamelCaseMotionConfiguration {
/**
* Enable CamelCaseMotion plugin or not
*/
enable: boolean;
}

export interface IConfiguration {
/**
* Use the system's clipboard when copying.
Expand Down Expand Up @@ -84,6 +91,11 @@ export interface IConfiguration {
*/
autoindent: boolean;

/**
* CamelCaseMotion plugin options
*/
camelCaseMotion: ICamelCaseMotionConfiguration;

/**
* Use EasyMotion plugin?
*/
Expand Down

0 comments on commit d8564f9

Please sign in to comment.