Skip to content

Commit

Permalink
Merge branch 'master' into multicursor-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
J-Fields authored Feb 26, 2020
2 parents 7c3096f + 38b49c5 commit a51334f
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 23 deletions.
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.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -959,7 +959,7 @@
"@types/lodash": "4.14.149",
"@types/mocha": "7.0.1",
"@types/node": "12.12.21",
"@types/sinon": "7.5.1",
"@types/sinon": "7.5.2",
"gulp": "4.0.2",
"gulp-bump": "3.1.3",
"gulp-git": "2.10.0",
Expand Down
5 changes: 5 additions & 0 deletions src/actions/commands/insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,11 @@ class CommandCtrlVInInsertMode extends BaseCommand {
public async exec(position: Position, vimState: VimState): Promise<VimState> {
const textFromClipboard = await Clipboard.Paste();

vimState.recordedState.transformations.push({
type: 'deleteRange',
range: new Range(vimState.cursorStartPosition, vimState.cursorStopPosition),
});

if (vimState.isMultiCursor) {
vimState.recordedState.transformations.push({
type: 'insertText',
Expand Down
26 changes: 21 additions & 5 deletions src/actions/motion.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as vscode from 'vscode';

import { ChangeOperator, DeleteOperator, YankOperator, BaseOperator } from './operator';
import { ChangeOperator, DeleteOperator, YankOperator } from './operator';
import { CursorMoveByUnit, CursorMovePosition, TextEditor } from './../textEditor';
import { Mode } from './../mode/mode';
import { PairMatcher } from './../common/matching/matcher';
import { Position, PositionDiff } from './../common/motion/position';
import { Position } from './../common/motion/position';
import { QuoteMatcher } from './../common/matching/quoteMatcher';
import { RegisterAction } from './base';
import { RegisterMode } from './../register/register';
Expand All @@ -14,11 +14,11 @@ import { VimState } from './../state/vimState';
import { configuration } from './../configuration/configuration';
import { shouldWrapKey } from './wrapping';
import { VimError, ErrorCode } from '../error';
import { reportSearch } from '../util/statusBarTextUtils';
import { Notation } from '../configuration/notation';
import { BaseMovement, SelectionType, IMovement, isIMovement } from './baseMotion';
import { globalState } from '../state/globalState';
import { BaseMovement, IMovement, isIMovement, SelectionType } from './baseMotion';
import { reportSearch } from '../util/statusBarTextUtils';
import { SneakForward, SneakBackward } from './plugins/sneak';
import { Notation } from '../configuration/notation';
import { SearchDirection } from '../state/searchState';

/**
Expand Down Expand Up @@ -424,6 +424,11 @@ export class MarkMovementBOL extends BaseMovement {
if (mark == null) {
throw VimError.fromCode(ErrorCode.MarkNotSet);
}

if (mark.isUppercaseMark && mark.editor !== undefined) {
await ensureEditorIsActive(mark.editor);
}

return mark.position.getFirstLineNonBlankChar();
}
}
Expand All @@ -440,10 +445,21 @@ export class MarkMovement extends BaseMovement {
if (mark == null) {
throw VimError.fromCode(ErrorCode.MarkNotSet);
}

if (mark.isUppercaseMark && mark.editor !== undefined) {
await ensureEditorIsActive(mark.editor);
}

return mark.position;
}
}

async function ensureEditorIsActive(editor: vscode.TextEditor) {
if (editor !== vscode.window.activeTextEditor) {
await vscode.window.showTextDocument(editor.document);
}
}

@RegisterAction
export class MoveLeft extends BaseMovement {
keys = ['h'];
Expand Down
86 changes: 73 additions & 13 deletions src/history/historyTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface IMark {
name: string;
position: Position;
isUppercaseMark: boolean;
editor?: vscode.TextEditor; // only required when using global marks (isUppercaseMark is true)
}

class HistoryStep {
Expand Down Expand Up @@ -116,6 +117,11 @@ class HistoryStep {
*/
marks: IMark[] = [];

/**
* "global" marks which operate across files. (when IMark.name is uppercase)
*/
static globalMarks: IMark[] = [];

constructor(init: {
changes?: DocumentChange[];
isFinished?: boolean;
Expand Down Expand Up @@ -303,16 +309,12 @@ export class HistoryTracker {
* text that was marked.
*/
private updateAndReturnMarks(): IMark[] {
const previousMarks = this.currentHistoryStep.marks;
const previousMarks = this.getAllCurrentDocumentMarks();
let newMarks: IMark[] = [];

// clone old marks into new marks
for (const mark of previousMarks) {
newMarks.push({
name: mark.name,
position: mark.position,
isUppercaseMark: mark.isUppercaseMark,
});
newMarks.push({ ...mark });
}

for (const change of this.currentHistoryStep.changes) {
Expand Down Expand Up @@ -395,33 +397,91 @@ export class HistoryTracker {
return newMarks;
}

/**
* Updates all marks affecting the active text editor.
* Since all currentHistoryStep's marks are affected, just update the
* array. Global marks might not be from the active editor, so the
* global mark collection is mutated with the new element in place.
*/
private updateMarks(): void {
const newMarks = this.updateAndReturnMarks();
this.currentHistoryStep.marks = newMarks.filter(mark => !mark.isUppercaseMark);

newMarks.filter(mark => mark.isUppercaseMark).forEach(this.putMarkInList.bind);
}

/**
* Returns the shared static list if isFileMark is true,
* otherwise returns the currentHistoryStep.marks.
*/
private getMarkList(isFileMark: boolean): IMark[] {
return isFileMark ? HistoryStep.globalMarks : this.currentHistoryStep.marks;
}

/**
* Gets all local and global marks targeting the current editor.
*/
private getAllCurrentDocumentMarks(): IMark[] {
const globalMarks = HistoryStep.globalMarks.filter(
mark => mark.editor === vscode.window.activeTextEditor
);
return [...this.currentHistoryStep.marks, ...globalMarks];
}

/**
* Adds a mark.
*/
public addMark(position: Position, markName: string): void {
const isUppercaseMark = markName.toUpperCase() === markName;
const newMark: IMark = {
position,
name: markName,
isUppercaseMark: markName === markName.toUpperCase(),
isUppercaseMark: isUppercaseMark,
editor: isUppercaseMark ? vscode.window.activeTextEditor : undefined,
};
const previousIndex = this.currentHistoryStep.marks.findIndex(mark => mark.name === markName);
this.putMarkInList(newMark);
}

/**
* Puts the mark into either the global or local marks array depending on
* mark.isUppercaseMark.
*/
private putMarkInList(mark: IMark): void {
const marks = this.getMarkList(mark.isUppercaseMark);
const previousIndex = marks.findIndex(existingMark => existingMark.name === mark.name);
if (previousIndex !== -1) {
this.currentHistoryStep.marks[previousIndex] = newMark;
marks[previousIndex] = mark;
} else {
this.currentHistoryStep.marks.push(newMark);
marks.push(mark);
}
}

/**
* Retrieves a mark.
* Retrieves a mark from either the global or local array depending on
* mark.isUppercaseMark.
*/
public getMark(markName: string): IMark {
return <IMark>this.currentHistoryStep.marks.find(mark => mark.name === markName);
const marks = this.getMarkList(markName.toUpperCase() === markName);
return <IMark>marks.find(mark => mark.name === markName);
}

/**
* Gets all local marks. I.e., marks that are specific for the current
* editor.
*/
public getLocalMarks(): IMark[] {
return [...this.currentHistoryStep.marks];
}

/**
* Gets all global marks. I.e., marks that are shared among all editors.
*/
public getGlobalMarks(): IMark[] {
return [...HistoryStep.globalMarks];
}

public getMarks(): IMark[] {
return this.currentHistoryStep.marks;
return [...this.currentHistoryStep.marks, ...HistoryStep.globalMarks];
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/mode/modeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export class ModeHandler implements vscode.Disposable {
public syncCursors() {
setImmediate(() => {
if (this.vimState.editor) {
this.vimState.cursors = this.vimState.editor.selections.map(
({ start, end }) =>
new Range(Position.FromVSCodePosition(start), Position.FromVSCodePosition(end))
);

this.vimState.cursorStartPosition = Position.FromVSCodePosition(
this.vimState.editor.selection.start
);
Expand Down
2 changes: 1 addition & 1 deletion src/neovim/neovim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class NeovimWrapper implements vscode.Disposable {
"'>",
[0, rangeEnd.line + 1, rangeEnd.character, false],
]);
for (const mark of vimState.historyTracker.getMarks()) {
for (const mark of vimState.historyTracker.getLocalMarks()) {
await this.nvim.callFunction('setpos', [
`'${mark.name}`,
[0, mark.position.line + 1, mark.position.character, false],
Expand Down
128 changes: 128 additions & 0 deletions test/historyTracker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import * as vscode from 'vscode';

import { HistoryTracker, IMark } from '../src/history/historyTracker';
import { VimState } from '../src/state/vimState';
import { Position } from '../src/common/motion/position';

suite('historyTracker unit tests', () => {
let sandbox: sinon.SinonSandbox;
let historyTracker: HistoryTracker;
let activeTextEditor: vscode.TextEditor;

const retrieveLocalMark = (markName: string): IMark | undefined =>
historyTracker.getLocalMarks().find(mark => mark.name === markName);

const retrieveFileMark = (markName: string): IMark | undefined =>
historyTracker.getGlobalMarks().find(mark => mark.name === markName);

const setupVimState = () => <VimState>(<any>sandbox.createStubInstance(VimState));

const setupHistoryTracker = (vimState = setupVimState()) => new HistoryTracker(vimState);

const setupVSCode = () => {
activeTextEditor = sandbox.createStubInstance<vscode.TextEditor>(TextEditorStub);
sandbox.stub(vscode, 'window').value({ activeTextEditor });
};

const buildMockPosition = (): Position => <any>sandbox.createStubInstance(Position);

setup(() => {
sandbox = sinon.createSandbox();
});

teardown(() => {
sandbox.restore();
});

suite('addMark', () => {
setup(() => {
setupVSCode();
historyTracker = setupHistoryTracker();
});

test('can create lowercase mark', () => {
const position = buildMockPosition();
historyTracker.addMark(position, 'a');
const mark = retrieveLocalMark('a');
assert.notStrictEqual(mark, undefined, 'failed to store lowercase mark');
if (mark !== undefined) {
assert.strictEqual(mark.position, position);
assert.strictEqual(mark.isUppercaseMark, false);
assert.strictEqual(mark.editor, undefined);
}
});

test('can create uppercase mark', () => {
const position = buildMockPosition();
historyTracker.addMark(position, 'A');
const mark = retrieveFileMark('A');
assert.notStrictEqual(mark, undefined, 'failed to store file mark');
if (mark !== undefined) {
assert.strictEqual(mark.position, position);
assert.strictEqual(mark.isUppercaseMark, true);
assert.strictEqual(mark.editor, activeTextEditor);
}
});

test('shares uppercase marks between editor instances', () => {
const position = buildMockPosition();
const firstHistoryTrackerInstance = historyTracker;
const otherHistoryTrackerInstance = setupHistoryTracker(setupVimState());
assert.notStrictEqual(firstHistoryTrackerInstance, otherHistoryTrackerInstance);
otherHistoryTrackerInstance.addMark(position, 'A');
const mark = retrieveFileMark('A');
assert.notStrictEqual(mark, undefined);
if (mark !== undefined) {
assert.strictEqual(position, mark.position);
}
});

test('does not share lower marks between editor instances', () => {
const position = buildMockPosition();
const firstHistoryTrackerInstance = historyTracker;
const otherHistoryTrackerInstance = setupHistoryTracker(setupVimState());
assert.notStrictEqual(firstHistoryTrackerInstance, otherHistoryTrackerInstance);
otherHistoryTrackerInstance.addMark(position, 'a');
const mark = retrieveLocalMark('a');
assert.strictEqual(mark, undefined);
});
});
});

// tslint:disable: no-empty
class TextEditorStub implements vscode.TextEditor {
readonly document: vscode.TextDocument;
selection: vscode.Selection;
selections: vscode.Selection[];
readonly visibleRanges: vscode.Range[];
options: vscode.TextEditorOptions;
viewColumn?: vscode.ViewColumn;

constructor() {}
async edit(
callback: (editBuilder: vscode.TextEditorEdit) => void,
options?: { undoStopBefore: boolean; undoStopAfter: boolean }
) {
return true;
}
async insertSnippet(
snippet: vscode.SnippetString,
location?:
| vscode.Position
| vscode.Range
| ReadonlyArray<Position>
| ReadonlyArray<vscode.Range>,
options?: { undoStopBefore: boolean; undoStopAfter: boolean }
) {
return true;
}
setDecorations(
decorationType: vscode.TextEditorDecorationType,
rangesOrOptions: vscode.Range[] | vscode.DecorationOptions[]
) {}
revealRange(range: vscode.Range, revealType?: vscode.TextEditorRevealType) {}
show(column?: vscode.ViewColumn) {}
hide() {}
}
Loading

0 comments on commit a51334f

Please sign in to comment.