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

Easymotion improvements #2017

Merged
merged 12 commits into from
Sep 16, 2017
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,12 @@ Motion Command | Description
`<leader><leader> j`|Start of line forwards
`<leader><leader> k`|Start of line backwards
`<leader><leader> / <char>... <CR>`|Search n-character
`<leader><leader><leader> bdt`|Til character
`<leader><leader><leader> bdw`|Start of word
`<leader><leader><leader> bde`|End of word
`<leader><leader><leader> bdjk`|Start of line

`<leader><leader> (2s|2f|2F|2t|2T) <char><char>` are also available.
`<leader><leader> (2s|2f|2F|2t|2T) <char><char>` and `<leader><leader><leader> bd2t <char>char>` are also available.
The difference is character count required for search.
For example, `<leader><leader> 2s <char><char>` requires two characters, and search by two characters.
This mapping is not a standard mapping, so it is recommended to use your custom mapping.
Expand Down
13 changes: 5 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"url": "https://github.com/VSCodeVim/Vim/issues"
},
"engines": {
"vscode": "^1.12.0"
"vscode": "^1.13.0"
},
"categories": [
"Other",
Expand Down Expand Up @@ -422,18 +422,15 @@
},
"vim.easymotionMarkerBackgroundColor": {
"type": "string",
"description": "Set a custom background color for EasyMotion markers.",
"default": "#000000"
"description": "Set a custom background color for EasyMotion markers."
},
"vim.easymotionMarkerForegroundColorOneChar": {
"type": "string",
"description": "Set a custom color for the text on one character long markers.",
"default": "#ff0000"
"description": "Set a custom color for the text on one character long markers."
},
"vim.easymotionMarkerForegroundColorTwoChar": {
"type": "string",
"description": "Set a custom color for the text on two character long markers.",
"default": "#ffa500"
"description": "Set a custom color for the text on two character long markers."
},
"vim.easymotionMarkerWidthPerChar": {
"type": "number",
Expand Down Expand Up @@ -463,7 +460,7 @@
"vim.easymotionMarkerYOffset": {
"type": "number",
"description": "Set the Y offset of the marker text (the distance from the top).",
"default": 11
"default": 0
},
"vim.easymotionKeys": {
"type": "string",
Expand Down
57 changes: 42 additions & 15 deletions src/actions/plugins/easymotion/easymotion.cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,31 @@ import {
EasyMotionCharMoveOpions,
} from './types';

export interface EasymotionTrigger {
key: string;
leaderCount?: number;
}

export function buildTriggerKeys(trigger: EasymotionTrigger) {
return [
...Array.from({ length: trigger.leaderCount || 2 }, () => '<leader>'),
...trigger.key.split(''),
];
}

abstract class BaseEasyMotionCommand extends BaseCommand {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock];

private _baseOptions: EasyMotionMoveOptionsBase;

public abstract getMatches(position: Position, vimState: VimState): EasyMotion.Match[];

constructor(baseOptions: EasyMotionMoveOptionsBase) {
constructor(baseOptions: EasyMotionMoveOptionsBase, trigger?: EasymotionTrigger) {
super();
this._baseOptions = baseOptions;
if (trigger) {
this.keys = buildTriggerKeys(trigger);
}
}

public abstract resolveMatchPosition(match: EasyMotion.Match): Position;
Expand Down Expand Up @@ -126,12 +141,17 @@ export interface EasyMotionSearchAction {
updateSearchString(s: string): void;
getSearchString(): string;
getMatches(position: Position, vimState: VimState): EasyMotion.Match[];
readonly searchCharCount: number;
}

export class SearchByCharCommand extends BaseEasyMotionCommand implements EasyMotionSearchAction {
private _searchString: string = '';
private _options: EasyMotionCharMoveOpions;

get searchCharCount() {
return this._options.charCount;
}

constructor(options: EasyMotionCharMoveOpions) {
super(options);
this._options = options;
Expand Down Expand Up @@ -179,6 +199,10 @@ export class SearchByCharCommand extends BaseEasyMotionCommand implements EasyMo
export class SearchByNCharCommand extends BaseEasyMotionCommand implements EasyMotionSearchAction {
private _searchString: string = '';

get searchCharCount() {
return -1;
}

constructor() {
super({});
}
Expand Down Expand Up @@ -226,30 +250,34 @@ export class EasyMotionCharMoveCommandBase extends BaseCommand {
modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine, ModeName.VisualBlock];
private _action: EasyMotionSearchAction;

constructor(trigger: string, action: EasyMotionSearchAction) {
constructor(trigger: EasymotionTrigger, action: EasyMotionSearchAction) {
super();
this._action = action;
this.keys = ['<leader>', '<leader>', ...trigger.split('')];
this.keys = buildTriggerKeys(trigger);
}

public async exec(position: Position, vimState: VimState): Promise<VimState> {
vimState.easyMotion = new EasyMotion();
vimState.easyMotion.previousMode = vimState.currentMode;
vimState.easyMotion.searchAction = this._action;
vimState.globalState.hl = true;
// Only execute the action if easymotion is enabled
if (!Configuration.easymotion) {
return vimState;
} else {
vimState.easyMotion = new EasyMotion();
vimState.easyMotion.previousMode = vimState.currentMode;
vimState.easyMotion.searchAction = this._action;
vimState.globalState.hl = true;

vimState.currentMode = ModeName.EasyMotionInputMode;
return vimState;
vimState.currentMode = ModeName.EasyMotionInputMode;
return vimState;
}
}
}

export class EasyMotionWordMoveCommandBase extends BaseEasyMotionCommand {
private _options: EasyMotionWordMoveOpions;

constructor(trigger: string, options: EasyMotionWordMoveOpions = {}) {
super(options);
constructor(trigger: EasymotionTrigger, options: EasyMotionWordMoveOpions = {}) {
super(options, trigger);
this._options = options;
this.keys = ['<leader>', '<leader>', ...trigger.split('')];
}

public getMatches(position: Position, vimState: VimState): EasyMotion.Match[] {
Expand Down Expand Up @@ -279,10 +307,9 @@ export class EasyMotionWordMoveCommandBase extends BaseEasyMotionCommand {
export class EasyMotionLineMoveCommandBase extends BaseEasyMotionCommand {
private _options: EasyMotionMoveOptionsBase;

constructor(trigger: string, options: EasyMotionMoveOptionsBase) {
super(options);
constructor(trigger: EasymotionTrigger, options: EasyMotionMoveOptionsBase = {}) {
super(options, trigger);
this._options = options;
this.keys = ['<leader>', '<leader>', ...trigger.split('')];
}

public resolveMatchPosition(match: EasyMotion.Match): Position {
Expand Down
130 changes: 60 additions & 70 deletions src/actions/plugins/easymotion/easymotion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ export class EasyMotion {
* Caches for decorations
*/
private static decorationTypeCache: vscode.TextEditorDecorationType[] = [];
private static svgCache: { [code: string]: vscode.Uri } = {};
private static cachedBackgroundColor: string = '';
private static cachedOneFontColor: string = '';
private static cachedTwoFontColor: string = '';
private static cachedWidthPerChar: number = -1;
private static cachedHeight: number = -1;

public get markers() {
return this._markers;
Expand Down Expand Up @@ -80,60 +74,6 @@ export class EasyMotion {
}
}

/**
* Create and cache the SVG data URI for different marker codes and colors
*/
private static getSvgDataUri(
code: string,
backgroundColor: string | undefined = 'black',
fontFamily: string | undefined = 'Consolas',
fontColor: string | undefined = 'white',
fontSize: string | undefined = '14',
fontWeight: string | undefined = 'normal'
): vscode.Uri {
// Clear cache if the backgroundColor or fontColor has changed
if (this.cachedBackgroundColor !== backgroundColor) {
this.svgCache = {};
this.cachedBackgroundColor = backgroundColor;
}

if (this.cachedOneFontColor !== Configuration.easymotionMarkerForegroundColorOneChar) {
this.svgCache = {};
this.cachedOneFontColor = Configuration.easymotionMarkerForegroundColorOneChar;
}

if (this.cachedTwoFontColor !== Configuration.easymotionMarkerForegroundColorTwoChar) {
this.svgCache = {};
this.cachedTwoFontColor = Configuration.easymotionMarkerForegroundColorTwoChar;
}

const widthPerChar = Configuration.easymotionMarkerWidthPerChar;
const width = code.length * widthPerChar + 1;
const height = Configuration.easymotionMarkerHeight;

if (this.cachedWidthPerChar !== widthPerChar || this.cachedHeight !== height) {
this.svgCache = {};
this.cachedWidthPerChar = width;
this.cachedHeight = height;
}

const cache = this.svgCache[code];
if (cache) {
return cache;
} else {
const uri = vscode.Uri.parse(
`data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ` +
`${height}" height="${height}" width="${width}"><rect width="${width}" height="${height}" rx="2" ry="2" ` +
`style="fill: ${backgroundColor}"></rect><text font-family="${fontFamily}" font-size="${fontSize}" ` +
`font-weight="${fontWeight}" fill="${fontColor}" x="1" y="${Configuration.easymotionMarkerYOffset}">${code}</text></svg>`
);

this.svgCache[code] = uri;

return uri;
}
}

/**
* Clear all decorations
*/
Expand Down Expand Up @@ -244,6 +184,52 @@ export class EasyMotion {
return matches;
}

private themeColorApiSupported(): boolean {
// Theme color is available from version 1.12.
const vscodeVersionAsNumber = parseInt(vscode.version.replace(/\./g, ''), 10);
return vscodeVersionAsNumber >= 1120;
}

private getMarkerColor(
customizedValue: string,
defaultValue: string | vscode.ThemeColor,
themeColorId: string
): string | vscode.ThemeColor {
if (!this.themeColorApiSupported()) {
return customizedValue || defaultValue;
} else {
if (customizedValue) {
return customizedValue;
} else {
return new vscode.ThemeColor(themeColorId);
}
}
}

private getEasymotionMarkerBackgroundColor() {
return this.getMarkerColor(
Configuration.easymotionMarkerBackgroundColor,
'#000',
'activityBarBadge.background'
);
}

private getEasymotionMarkerForegroundColorOneChar() {
return this.getMarkerColor(
Configuration.easymotionMarkerForegroundColorOneChar,
'#f00',
'activityBarBadge.foreground'
);
}

private getEasymotionMarkerForegroundColorTwoChar() {
return this.getMarkerColor(
Configuration.easymotionMarkerForegroundColorTwoChar,
'#ffa500',
'activityBarBadge.foreground'
);
}

public updateDecorations() {
this.clearDecorations();

Expand All @@ -262,19 +248,23 @@ export class EasyMotion {

const fontColor =
keystroke.length > 1
? Configuration.easymotionMarkerForegroundColorTwoChar
: Configuration.easymotionMarkerForegroundColorOneChar;
? this.getEasymotionMarkerForegroundColorTwoChar()
: this.getEasymotionMarkerForegroundColorOneChar();

const renderOptions: vscode.ThemableDecorationInstanceRenderOptions = {
after: {
contentIconPath: EasyMotion.getSvgDataUri(
keystroke,
Configuration.easymotionMarkerBackgroundColor,
Configuration.easymotionMarkerFontFamily,
fontColor,
Configuration.easymotionMarkerFontSize,
Configuration.easymotionMarkerFontWeight
),
contentText: keystroke,
backgroundColor: this.getEasymotionMarkerBackgroundColor(),
height: `${Configuration.easymotionMarkerHeight}px`,
width: `${keystroke.length * Configuration.easymotionMarkerWidthPerChar}px`,
color: fontColor,
textDecoration: `none;
font-family: ${Configuration.easymotionMarkerFontFamily};
font-size: ${Configuration.easymotionMarkerFontSize}px;
font-weight: ${Configuration.easymotionMarkerFontWeight};
position: absolute;
z-index: 99;
bottom: ${Configuration.easymotionMarkerYOffset}px`,
},
};
// Position should be offsetted by the length of the keystroke to prevent hiding behind the gutter
Expand Down
2 changes: 1 addition & 1 deletion src/actions/plugins/easymotion/markerGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class MarkerGenerator {
private createPrefixKeyTable(): string[] {
const keyTable = this.keyTable;
const totalRemainder = Math.max(this.matchesCount - keyTable.length, 0);
const totalSteps = Math.floor(totalRemainder / keyTable.length) + 1;
const totalSteps = Math.ceil(totalRemainder / keyTable.length);
const reversed = this.keyTable.slice().reverse();
const count = Math.min(totalSteps, reversed.length);
return reversed.slice(0, count);
Expand Down
Loading