Skip to content
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
240 changes: 207 additions & 33 deletions src/actions/motion.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import * as vscode from 'vscode';

import { ChangeOperator, DeleteOperator, YankOperator } from './operator';
import { CursorMoveByUnit, CursorMovePosition, TextEditor } from './../textEditor';
import { isVisualMode, Mode } from './../mode/mode';
import { PairMatcher } from './../common/matching/matcher';
import { QuoteMatcher } from './../common/matching/quoteMatcher';
import { RegisterAction } from './base';
import { RegisterMode } from './../register/register';
import { TagMatcher } from './../common/matching/tagMatcher';
import { VimState } from './../state/vimState';
import { configuration } from './../configuration/configuration';
import { shouldWrapKey } from './wrapping';
import { Position } from 'vscode';
import { sorted } from '../common/motion/position';
import { Notation } from '../configuration/notation';
import { VimError } from '../error';
import { BaseMovement, SelectionType, IMovement, isIMovement, failedMovement } from './baseMotion';
import { ModeDataFor } from '../mode/modeData';
import { globalState } from '../state/globalState';
import { reportSearch } from '../util/statusBarTextUtils';
import { SneakForward, SneakBackward } from './plugins/sneak';
import { Notation } from '../configuration/notation';
import { StatusBar } from '../statusBar';
import { clamp, isHighSurrogate, isLowSurrogate } from '../util/util';
import { getCurrentParagraphBeginning, getCurrentParagraphEnd } from '../textobject/paragraph';
import { PythonDocument } from './languages/python/motion';
import { Position } from 'vscode';
import { sorted } from '../common/motion/position';
import { WordType } from '../textobject/word';
import { CommandInsertAtCursor } from './commands/actions';
import { reportSearch } from '../util/statusBarTextUtils';
import { clamp, isHighSurrogate, isLowSurrogate } from '../util/util';
import { SearchDirection } from '../vimscript/pattern';
import { PairMatcher } from './../common/matching/matcher';
import { QuoteMatcher } from './../common/matching/quoteMatcher';
import { TagMatcher } from './../common/matching/tagMatcher';
import { configuration } from './../configuration/configuration';
import { isVisualMode, Mode } from './../mode/mode';
import { RegisterMode } from './../register/register';
import { VimState } from './../state/vimState';
import { CursorMoveByUnit, CursorMovePosition, TextEditor } from './../textEditor';
import { RegisterAction } from './base';
import { BaseMovement, failedMovement, IMovement, isIMovement, SelectionType } from './baseMotion';
import { CommandInsertAtCursor } from './commands/actions';
import { PythonDocument } from './languages/python/motion';
import { ChangeOperator, DeleteOperator, YankOperator } from './operator';
import { SneakBackward, SneakForward } from './plugins/sneak';
import { SmartQuoteMatcher, WhichQuotes } from './plugins/targets/smartQuotesMatcher';
import { useSmartQuotes } from './plugins/targets/targetsConfig';
import { ModeDataFor } from '../mode/modeData';
import { shouldWrapKey } from './wrapping';

/**
* A movement is something like 'h', 'k', 'w', 'b', 'gg', etc.
Expand Down Expand Up @@ -286,35 +286,54 @@ class MoveDown extends BaseMovement {
override preservesDesiredColumn = true;

public override async execAction(position: Position, vimState: VimState): Promise<Position> {
return this.execActionWithCount(position, vimState, 1) as Promise<Position>;
}

public override async execActionForOperator(
position: Position,
vimState: VimState,
): Promise<Position> {
vimState.currentRegisterMode = RegisterMode.LineWise;
return position.getDown();
}

public override async execActionWithCount(
Copy link
Member

Choose a reason for hiding this comment

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

For each of these, can't we make the implementation of execAction simply execActionWithCount(..., 1)?

Copy link
Author

Choose a reason for hiding this comment

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

Excellent suggestion! I've refactored execAction to simply delegate to execActionWithCount(position, vimState, 1). This eliminates code duplication and centralizes the logic in one place. The method now returns Promise<Position> (with a cast) since execActionWithCount with count=1 always returns a Position in practice, never an IMovement.

position: Position,
vimState: VimState,
count: number,
): Promise<Position | IMovement> {
// Optimize: jump directly to target line instead of looping
count = clamp(count, 1, 99999);

// Handle special cases with single iteration
if (
vimState.currentMode === Mode.Insert &&
this.keysPressed[0] === '<down>' &&
vimState.editor.document.uri.scheme === 'vscode-interactive-input' &&
position.line === vimState.document.lineCount - 1 &&
vimState.editor.selection.isEmpty
) {
// navigate history in interactive window
await vscode.commands.executeCommand('interactive.history.next');
return vimState.editor.selection.active;
}

if (configuration.foldfix && vimState.currentMode !== Mode.VisualBlock) {
return new MoveDownFoldFix().execAction(position, vimState);
return new MoveDownFoldFix().execActionWithCount(position, vimState, count);
}

if (position.line < vimState.document.lineCount - 1) {
return position.with({ character: vimState.desiredColumn }).getDown();
} else {
// For operators, set register mode and calculate target position directly
if (vimState.recordedState.operator) {
vimState.currentRegisterMode = RegisterMode.LineWise;
return position.getDown(count);
}

// Calculate target line directly
const targetLine = Math.min(position.line + count, vimState.document.lineCount - 1);
if (targetLine === position.line) {
return position;
}
}

public override async execActionForOperator(
position: Position,
vimState: VimState,
): Promise<Position> {
vimState.currentRegisterMode = RegisterMode.LineWise;
return position.getDown();
return new Position(targetLine, vimState.desiredColumn);
}
}

Expand Down Expand Up @@ -354,6 +373,45 @@ class MoveUp extends BaseMovement {
vimState.currentRegisterMode = RegisterMode.LineWise;
return position.getUp();
}

public override async execActionWithCount(
position: Position,
vimState: VimState,
count: number,
): Promise<Position | IMovement> {
// Optimize: jump directly to target line instead of looping
count = clamp(count, 1, 99999);

// Handle special cases with single iteration
if (
vimState.currentMode === Mode.Insert &&
this.keysPressed[0] === '<up>' &&
vimState.editor.document.uri.scheme === 'vscode-interactive-input' &&
position.line === 0 &&
vimState.editor.selection.isEmpty
) {
await vscode.commands.executeCommand('interactive.history.previous');
return vimState.editor.selection.active;
}

if (configuration.foldfix && vimState.currentMode !== Mode.VisualBlock) {
return new MoveUpFoldFix().execActionWithCount(position, vimState, count);
}

// For operators, set register mode and calculate target position directly
if (vimState.recordedState.operator) {
vimState.currentRegisterMode = RegisterMode.LineWise;
return position.getUp(count);
}

// Calculate target line directly
const targetLine = Math.max(position.line - count, 0);
if (targetLine === position.line) {
return position;
}

return new Position(targetLine, vimState.desiredColumn);
}
}

@RegisterAction
Expand Down Expand Up @@ -725,6 +783,65 @@ class MoveLeft extends BaseMovement {
)
: getLeftWhile(position);
}

public override async execActionWithCount(
position: Position,
vimState: VimState,
count: number,
): Promise<Position | IMovement> {
// Optimize: calculate target position directly instead of looping
count = clamp(count, 1, 99999);

const shouldWrap = shouldWrapKey(vimState.currentMode, this.keysPressed[0]);
const includeEol = [Mode.Insert, Mode.Replace].includes(vimState.currentMode);

if (shouldWrap) {
// For wrapping mode, we need to iterate since it can cross line boundaries
// But we can optimize by doing it more efficiently
let currentPos = position;
for (let i = 0; i < count; i++) {
currentPos = currentPos.getLeftThroughLineBreaks(includeEol);
if (currentPos.isEqual(position) && i === 0) {
// Can't move left (at document start)
break;
}
}
return currentPos;
} else {
// For non-wrapping mode, we can calculate directly
const line = vimState.document.lineAt(position.line).text;
let targetChar = position.character;

// Move left count times, respecting surrogate pairs
for (let i = 0; i < count; i++) {
if (targetChar === 0) {
break;
}

// Check for surrogate pair at current position
if (
isLowSurrogate(line.charCodeAt(targetChar)) &&
targetChar > 0 &&
isHighSurrogate(line.charCodeAt(targetChar - 1))
) {
targetChar--;
}

targetChar--;

// Check for surrogate pair at new position
if (
targetChar > 0 &&
isLowSurrogate(line.charCodeAt(targetChar)) &&
isHighSurrogate(line.charCodeAt(targetChar - 1))
) {
targetChar--;
}
}

return new Position(position.line, Math.max(0, targetChar));
}
}
}

@RegisterAction
Expand Down Expand Up @@ -753,6 +870,63 @@ class MoveRight extends BaseMovement {
)
: getRightWhile(position);
}

public override async execActionWithCount(
position: Position,
vimState: VimState,
count: number,
): Promise<Position | IMovement> {
// Optimize: calculate target position directly instead of looping
count = clamp(count, 1, 99999);

const shouldWrap = shouldWrapKey(vimState.currentMode, this.keysPressed[0]);
const includeEol = [Mode.Insert, Mode.Replace].includes(vimState.currentMode);

if (shouldWrap) {
// For wrapping mode, we need to iterate since it can cross line boundaries
// But we can optimize by doing it more efficiently
let currentPos = position;
for (let i = 0; i < count; i++) {
const prevPos = currentPos;
currentPos = currentPos.getRightThroughLineBreaks(includeEol);
if (currentPos.isEqual(prevPos)) {
// Can't move right (at document end)
break;
}
}
return currentPos;
} else {
// For non-wrapping mode, we can calculate directly
const line = vimState.document.lineAt(position.line).text;
let targetChar = position.character;
const lineLength = line.length;

// Move right count times, respecting surrogate pairs
for (let i = 0; i < count; i++) {
if (targetChar >= lineLength) {
break;
}

const newTargetChar = targetChar + 1;
if (newTargetChar >= lineLength) {
targetChar = newTargetChar;
break;
}

// Check for surrogate pair at new position
if (
isLowSurrogate(line.charCodeAt(newTargetChar)) &&
isHighSurrogate(line.charCodeAt(targetChar))
) {
targetChar = newTargetChar + 1;
} else {
targetChar = newTargetChar;
}
}

return new Position(position.line, Math.min(lineLength, targetChar));
}
}
}

@RegisterAction
Expand Down
44 changes: 20 additions & 24 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// host can call to run the tests. The test runner is expected to use console.log
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.
import glob from 'glob';
import { glob } from 'glob';
import Mocha from 'mocha';
import * as path from 'path';

Expand All @@ -19,7 +19,7 @@ import { Configuration } from './testConfiguration';
Globals.isTesting = true;
Globals.mockConfiguration = new Configuration();

export function run(): Promise<void> {
export async function run(): Promise<void> {
const mochaGrep = new RegExp(process.env.MOCHA_GREP || '');

// Create the mocha test
Expand All @@ -32,28 +32,24 @@ export function run(): Promise<void> {

const testsRoot = path.resolve(__dirname, '.');

const files = await glob('**/**.test.js', { cwd: testsRoot });

// Add files to the test suite
files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f)));

// Run the mocha test
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}

// Add files to the test suite
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));

try {
// Run the mocha test
mocha.run((failures) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (error) {
console.error(error);
e(error as Error);
}
});
try {
mocha.run((failures) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (error) {
console.error(error);
e(error as Error);
}
});
}
Loading