Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CamelCaseMotion plugin #3483

Merged
merged 8 commits into from
Feb 18, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/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
jkillian marked this conversation as resolved.
Show resolved Hide resolved

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": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to make this more hierarchic. Can you make this vim.camelCaseMotion.enable instead? Such that if we do end up creating more configs for camel case, they can live under vim.camelCaseMotion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I've switched the setting over as you suggested and pushed the new changes.

"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 && super.doesActionApply(vimState, keysPressed);
}

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

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

public couldActionApply(vimState: VimState, keysPressed: string[]) {
return configuration.camelCaseMotion && 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should move away from having position.ts the dumping ground for all positioning related logic as it's a bit of a mess.

As the changes you introduced to this file are specific to camel case, I wonder if we should create a new file under /plugins camelcase.position.ts?

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to the idea in principle, but if I were to move I'd either have to duplicate or make public all the get*Word*WithRegex functions and maybe the getAllPositions family of functions since they're shared among all the word-based motions. Your call, I'm happy either way!

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ermagod. Thank you for writing comments.

`[^\\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
2 changes: 2 additions & 0 deletions src/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ class Configuration implements IConfiguration {

autoindent = true;

camelCaseMotion = true;

sneak = false;
sneakUseIgnorecaseAndSmartcase = false;

Expand Down
5 changes: 5 additions & 0 deletions src/configuration/iconfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export interface IConfiguration {
*/
autoindent: boolean;

/**
* Use CamelCaseMotion plugin?
*/
camelCaseMotion: boolean;

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