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

Enhance the search function for easymotion and support remote yank. #8860

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,8 @@ Once easymotion is active, initiate motions using the following commands. After
| `<leader><leader><leader> bde` | End of word |
| `<leader><leader><leader> bdjk` | Start of line |
| `<leader><leader><leader> j` | JumpToAnywhere motion; default behavior matches beginning & ending of word, camelCase, after `_` and after `#` |
| `<leader><leader><leader> s` | Search n-character and jump |
| `<leader><leader><leader> yr` | Remote yank |

`<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.
Expand Down
2 changes: 1 addition & 1 deletion extensionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export async function activate(context: vscode.ExtensionContext, handleLocal: bo
return;
}

if (mh.currentMode === Mode.EasyMotionMode) {
if (mh.currentMode === Mode.EasyMotionMode || mh.currentMode === Mode.EasyMotionInputMode) {
return;
}

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,11 @@
"default": "#7fbf00",
"markdownDescription": "Set a custom color for the easymotion search n-character (default `<leader><leader>/`)."
},
"vim.easymotionIncSearchBackgroundColor": {
"type": "string",
"default": "#0000",
"markdownDescription": "Set a custom background color for the easymotion search n-character (default `<leader><leader>/`)."
},
"vim.easymotionDimColor": {
"type": "string",
"default": "#777777",
Expand Down
6 changes: 5 additions & 1 deletion src/actions/commands/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ class CommandEsc extends BaseCommand {
override preservesDesiredColumn = true;

public override async exec(position: Position, vimState: VimState): Promise<void> {
vimState.easyMotion.clearRemoteYank(vimState);
if (vimState.currentMode === Mode.Normal) {
vimState.surround = undefined;

Expand All @@ -451,7 +452,10 @@ class CommandEsc extends BaseCommand {
]);
}
} else {
if (vimState.currentMode === Mode.EasyMotionMode) {
if (
vimState.currentMode === Mode.EasyMotionMode ||
vimState.currentMode === Mode.EasyMotionInputMode
) {
vimState.easyMotion.clearDecorations(vimState.editor);
} else if (vimState.currentMode === Mode.SurroundInputMode) {
vimState.surround = undefined;
Expand Down
11 changes: 7 additions & 4 deletions src/actions/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,13 @@ export class YankOperator extends BaseOperator {

Register.put(vimState, text, this.multicursorIndex, true);

vimState.cursorStopPosition =
vimState.currentMode === Mode.Normal && vimState.currentRegisterMode === RegisterMode.LineWise
? start.with({ character: vimState.cursorStopPosition.character })
: start;
if (!vimState.easyMotion.clearRemoteYank(vimState)) {
vimState.cursorStopPosition =
vimState.currentMode === Mode.Normal &&
vimState.currentRegisterMode === RegisterMode.LineWise
? start.with({ character: vimState.cursorStopPosition.character })
: start;
}

await vimState.setCurrentMode(Mode.Normal);

Expand Down
134 changes: 113 additions & 21 deletions src/actions/plugins/easymotion/easymotion.cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { globalState } from '../../../state/globalState';
import { TextEditor } from '../../../textEditor';
import { MarkerGenerator } from './markerGenerator';
import { Position } from 'vscode';
import { getAndUpdateModeHandler } from '../../../../extensionBase';
import { Jump } from '../../../jumps/jump';

export interface EasymotionTrigger {
key: string;
Expand Down Expand Up @@ -42,12 +44,17 @@ abstract class BaseEasyMotionCommand extends BaseCommand {

public abstract resolveMatchPosition(match: Match): Position;

public processMarkers(matches: Match[], cursorPosition: Position, vimState: VimState) {
public processMarkers(
matches: Match[],
cursorPosition: Position,
vimState: VimState,
nextKeys: Set<string> = new Set<string>(),
) {
// Clear existing markers, just in case
vimState.easyMotion.clearMarkers();

let index = 0;
const markerGenerator = new MarkerGenerator(matches.length);
const markerGenerator = new MarkerGenerator(matches.length, nextKeys);
for (const match of matches) {
const matchPosition = this.resolveMatchPosition(match);
// Skip if the match position equals to cursor position
Expand All @@ -60,14 +67,21 @@ abstract class BaseEasyMotionCommand extends BaseCommand {
}
}

protected searchOptions(position: Position): SearchOptions {
protected searchOptions(position: Position, vimState: VimState): SearchOptions {
let minPosition: Position | undefined;
let maxPosition: Position | undefined;
const visibleRange = vimState.editor.visibleRanges[0];
if (visibleRange) {
minPosition = visibleRange.start.getUp();
maxPosition = visibleRange.end.getDown().getLineEnd();
}
switch (this._baseOptions.searchOptions) {
case 'min':
return { min: position };
return { min: position, max: maxPosition };
case 'max':
return { max: position };
return { min: minPosition, max: position };
default:
return {};
return { min: minPosition, max: maxPosition };
}
}

Expand Down Expand Up @@ -120,7 +134,7 @@ function getMatchesForString(
// Search all occurences of the character pressed

// If the input is not a letter, treating it as regex can cause issues
if (!/[a-zA-Z]/.test(searchString)) {
if (/[^a-zA-Z]/.test(searchString)) {
return vimState.easyMotion.sortedSearch(vimState.document, position, searchString, options);
}

Expand Down Expand Up @@ -151,7 +165,12 @@ export class SearchByCharCommand extends BaseEasyMotionCommand implements EasyMo
}

public getMatches(position: Position, vimState: VimState): Match[] {
return getMatchesForString(position, vimState, this.searchString, this.searchOptions(position));
return getMatchesForString(
position,
vimState,
this.searchString,
this.searchOptions(position, vimState),
);
}

public shouldFire() {
Expand Down Expand Up @@ -197,7 +216,7 @@ export class SearchByNCharCommand extends BaseEasyMotionCommand implements EasyM
position,
vimState,
this.removeTrailingLineBreak(this.searchString),
{},
this.searchOptions(position, vimState),
);
}

Expand All @@ -217,13 +236,60 @@ export class SearchByNCharCommand extends BaseEasyMotionCommand implements EasyM
}
}

export class SearchAroundCommand extends SearchByNCharCommand {
public override shouldFire() {
return true;
}

private getNextKeys(matchs: Match[], vimState: VimState): Set<string> {
const result = new Set<string>();
for (const match of matchs) {
const lineText = vimState.document.lineAt(match.position.line).text;
const index = match.position.character + match.text.length;
if (index < lineText.length) {
result.add(lineText.charAt(index));
}
}
return result;
}

public override async fire(position: Position, vimState: VimState): Promise<void> {
// Only execute the action if the configuration is set
if (configuration.easymotion) {
// Search all occurences of the character pressed
const matches = this.getMatches(position, vimState);

// Stop if there are no matches
if (matches.length > 0) {
const nextKeys = this.getNextKeys(matches, vimState);
this.processMarkers(matches, position, vimState, nextKeys);

// Store mode to return to after performing easy motion
vimState.easyMotion.previousMode = vimState.currentMode;
// Enter the EasyMotion mode and await further keys
await vimState.setCurrentMode(Mode.EasyMotionInputMode);
} else {
vimState.easyMotion.clearDecorations(vimState.editor);
vimState.easyMotion.clearMarkers();
}
}
}
}
export abstract class EasyMotionCharMoveCommandBase extends BaseCommand {
modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];
private _action: EasyMotionSearchAction;

constructor(action: EasyMotionSearchAction) {
private _nCharSearch: boolean;
private _remoteYank: boolean;

constructor(
action: EasyMotionSearchAction,
nCharSearch: boolean = false,
remoteYank: boolean = false,
) {
super();
this._action = action;
this._nCharSearch = nCharSearch;
this._remoteYank = remoteYank;
}

public override async exec(position: Position, vimState: VimState): Promise<void> {
Expand All @@ -232,6 +298,8 @@ export abstract class EasyMotionCharMoveCommandBase extends BaseCommand {
vimState.easyMotion = new EasyMotion();
vimState.easyMotion.previousMode = vimState.currentMode;
vimState.easyMotion.searchAction = this._action;
vimState.easyMotion.nCharSearch = this._nCharSearch;
vimState.easyMotion.remoteYank = this._remoteYank;
globalState.hl = true;

await vimState.setCurrentMode(Mode.EasyMotionInputMode);
Expand All @@ -248,7 +316,7 @@ export abstract class EasyMotionWordMoveCommandBase extends BaseEasyMotionComman
}

public getMatches(position: Position, vimState: VimState): Match[] {
return this.getMatchesForWord(position, vimState, this.searchOptions(position));
return this.getMatchesForWord(position, vimState, this.searchOptions(position, vimState));
}

public resolveMatchPosition(match: Match): Position {
Expand Down Expand Up @@ -286,7 +354,7 @@ export abstract class EasyMotionLineMoveCommandBase extends BaseEasyMotionComman
}

public getMatches(position: Position, vimState: VimState): Match[] {
return this.getMatchesForLineStart(position, vimState, this.searchOptions(position));
return this.getMatchesForLineStart(position, vimState, this.searchOptions(position, vimState));
}

private getMatchesForLineStart(
Expand Down Expand Up @@ -319,14 +387,38 @@ class EasyMotionCharInputMode extends BaseCommand {
public override async exec(position: Position, vimState: VimState): Promise<void> {
const key = this.keysPressed[0];
const action = vimState.easyMotion.searchAction;
action.searchString =
key === '<BS>' || key === '<S-BS>'
? action.searchString.slice(0, -1)
: action.searchString + key;
if (action.shouldFire()) {
// Skip Easymotion input mode to make sure not to back to it
await vimState.setCurrentMode(vimState.easyMotion.previousMode);
await action.fire(vimState.cursorStopPosition, vimState);
const index = vimState.easyMotion.markers.findIndex((m) => m.name === key);
if (index < 0) {
action.searchString =
key === '<BS>' || key === '<S-BS>'
? action.searchString.slice(0, -1)
: action.searchString + key;
if (action.shouldFire()) {
// Skip Easymotion input mode to make sure not to back to it
await vimState.setCurrentMode(vimState.easyMotion.previousMode);
await action.fire(vimState.cursorStopPosition, vimState);
}
return;
}

// Only one found, navigate to it
const marker = vimState.easyMotion.markers[index];
vimState.easyMotion.clearMarkers();
action.searchString = '';
// Set cursor position based on marker entered
vimState.easyMotion.clearDecorations(vimState.editor);
await vimState.setCurrentMode(vimState.easyMotion.previousMode);
if (vimState.easyMotion.remoteYank) {
vimState.easyMotion.remoteYankPosition = vimState.cursorStopPosition;
vimState.cursorStopPosition = marker.position;
const mh = await getAndUpdateModeHandler();
void mh?.handleKeyEvent('y');
} else {
vimState.cursorStopPosition = marker.position;
globalState.jumpTracker.recordJump(
Jump.fromStateBefore(vimState),
Jump.fromStateNow(vimState),
);
}
}
}
Expand Down
34 changes: 28 additions & 6 deletions src/actions/plugins/easymotion/easymotion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Mode } from '../../../mode/mode';
import { configuration } from './../../../configuration/configuration';
import { TextEditor } from './../../../textEditor';
import { EasyMotionSearchAction, IEasyMotion, Marker, Match, SearchOptions } from './types';
import { VimState } from 'src/state/vimState';

export class EasyMotion implements IEasyMotion {
/**
Expand All @@ -15,6 +16,9 @@ export class EasyMotion implements IEasyMotion {
// TODO: is this actually always set?
public searchAction!: EasyMotionSearchAction;

public nCharSearch: boolean = false;
public remoteYank: boolean = false;
public remoteYankPosition: Position | undefined;
/**
* Array of all markers and decorations
*/
Expand Down Expand Up @@ -51,7 +55,14 @@ export class EasyMotion implements IEasyMotion {
this.visibleMarkers = [];
this.decorations = [];
}

public clearRemoteYank(vimState: VimState): boolean {
if (this.remoteYankPosition) {
vimState.cursorStopPosition = this.remoteYankPosition;
this.remoteYankPosition = undefined;
return true;
}
return false;
}
/**
* Create and cache decoration types for different marker lengths
*/
Expand Down Expand Up @@ -227,6 +238,12 @@ export class EasyMotion implements IEasyMotion {
this.visibleMarkers = [];
this.decorations = [];

const offset = this.searchAction?.searchString
? this.searchAction.searchString.length === 0
? 0
: this.searchAction.searchString.length
: 0;

// Set the decorations for all the different marker lengths
const dimmingZones: vscode.DecorationOptions[] = [];
const dimmingRenderOptions: vscode.ThemableDecorationRenderOptions = {
Expand Down Expand Up @@ -291,7 +308,12 @@ export class EasyMotion implements IEasyMotion {
? this.getEasymotionMarkerForegroundColorTwoCharFirst()
: this.getEasymotionMarkerForegroundColorOneChar();
const backgroundColor = this.getEasymotionMarkerBackgroundColor();
const firstCharRange = new vscode.Range(pos.line, pos.character, pos.line, pos.character);
const firstCharRange = new vscode.Range(
pos.line,
pos.character + offset,
pos.line,
pos.character + offset,
);
const firstCharRenderOptions: vscode.ThemableDecorationInstanceRenderOptions = {
before: {
contentText: keystroke.substring(0, 1),
Expand Down Expand Up @@ -345,13 +367,13 @@ export class EasyMotion implements IEasyMotion {
hiddenChars.push(
new vscode.Range(
pos.line,
pos.character,
pos.character + offset,
pos.line,
pos.character + keystroke.length + trim,
pos.character + keystroke.length + trim + offset,
),
);

if (configuration.easymotionDimBackground) {
if (configuration.easymotionDimBackground && offset === 0) {
// This excludes markers from the dimming ranges by using them as anchors
// each marker adds the range between it and previous marker to the dimming zone
// except last marker after which the rest of document is dimmed
Expand Down Expand Up @@ -394,7 +416,7 @@ export class EasyMotion implements IEasyMotion {
}

// for the last marker dim till document end
if (configuration.easymotionDimBackground && markers.length > 0) {
if (configuration.easymotionDimBackground && markers.length > 0 && offset === 0) {
const prevMarker = markers[markers.length - 1];
const prevKeystroke = prevMarker.name.substring(this.accumulation.length);
const prevDimPos = dimmingZones[dimmingZones.length - 1].range.end;
Expand Down
10 changes: 7 additions & 3 deletions src/actions/plugins/easymotion/markerGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ export class MarkerGenerator {
private keyTable: string[];
private prefixKeyTable: string[];

constructor(matchesCount: number) {
constructor(matchesCount: number, nextKeys: Set<string>) {
this.matchesCount = matchesCount;
this.keyTable = this.getKeyTable();
this.prefixKeyTable = this.createPrefixKeyTable();
this.keyTable = this.getKeyTable().filter((key) => !nextKeys.has(key));
if (nextKeys.size > 0) {
this.prefixKeyTable = [];
} else {
this.prefixKeyTable = this.createPrefixKeyTable();
}
}

public generateMarker(index: number, markerPosition: Position): Marker | null {
Expand Down
Loading