Implements Global state (#1179)

* Made GlobalState hold search and . action values

* Adds a test to check . action across editors

* Added util helper that waits for tab changes

* Added all global search history tests

* Fixed global search tests, removed timeout, sleeps

* Moved searchState and searchStateIndex to global

* Tab change recalculates search ranges

* Global . test to await tab change not timeouts

* Moved getters, setters to globalState + cleanup

* Fixed waitForTabChange case of synchronous tab change
This commit is contained in:
Vikram Thyagarajan 2016-12-21 02:50:29 +05:30 committed by Grant Mathews
parent ca7bef095b
commit ac871462d1
8 changed files with 263 additions and 70 deletions

View File

@ -730,7 +730,7 @@ class CommandInsertRegisterContentInSearchMode extends BaseCommand {
text += "\n";
}
const searchState = vimState.searchState!;
const searchState = vimState.globalState.searchState!;
searchState.searchString += text;
return vimState;
}
@ -882,8 +882,8 @@ class CommandEsc extends BaseCommand {
}
if (vimState.currentMode === ModeName.SearchInProgressMode) {
if (vimState.searchState) {
vimState.cursorPosition = vimState.searchState.searchCursorStartPosition;
if (vimState.globalState.searchState) {
vimState.cursorPosition = vimState.globalState.searchState.searchCursorStartPosition;
}
}
@ -1425,7 +1425,7 @@ class CommandInsertInSearchMode extends BaseCommand {
public async exec(position: Position, vimState: VimState): Promise<VimState> {
const key = this.keysPressed[0];
const searchState = vimState.searchState!;
const searchState = vimState.globalState.searchState!;
// handle special keys first
if (key === "<BS>" || key === "<shift+BS>") {
@ -1435,55 +1435,56 @@ class CommandInsertInSearchMode extends BaseCommand {
// Repeat the previous search if no new string is entered
if (searchState.searchString === "") {
const prevSearchList = vimState.searchStatePrevious!;
const prevSearchList = vimState.globalState.searchStatePrevious!;
if (prevSearchList.length > 0) {
searchState.searchString = prevSearchList[prevSearchList.length - 1].searchString;
}
}
// Store this search if different than previous
if (vimState.searchStatePrevious.length !== 0) {
if (searchState.searchString !== vimState.searchStatePrevious[vimState.searchStatePrevious.length - 1]!.searchString) {
vimState.searchStatePrevious.push(searchState);
if (vimState.globalState.searchStatePrevious.length !== 0) {
let previousSearchState = vimState.globalState.searchStatePrevious;
if (searchState.searchString !== previousSearchState[previousSearchState.length - 1]!.searchString) {
previousSearchState.push(searchState);
}
} else {
vimState.searchStatePrevious.push(searchState);
vimState.globalState.searchStatePrevious.push(searchState);
}
// Make sure search history does not exceed configuration option
if (vimState.searchStatePrevious.length > Configuration.history) {
vimState.searchStatePrevious.splice(0, 1);
if (vimState.globalState.searchStatePrevious.length > Configuration.history) {
vimState.globalState.searchStatePrevious.splice(0, 1);
}
// Update the index to the end of the search history
vimState.searchStateIndex = vimState.searchStatePrevious.length - 1;
vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length - 1;
// Move cursor to next match
vimState.cursorPosition = searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos;
return vimState;
} else if (key === "<up>") {
const prevSearchList = vimState.searchStatePrevious!;
if (prevSearchList[vimState.searchStateIndex] !== undefined) {
searchState.searchString = prevSearchList[vimState.searchStateIndex].searchString;
vimState.searchStateIndex -= 1;
const prevSearchList = vimState.globalState.searchStatePrevious!;
if (prevSearchList[vimState.globalState.searchStateIndex] !== undefined) {
searchState.searchString = prevSearchList[vimState.globalState.searchStateIndex].searchString;
vimState.globalState.searchStateIndex -= 1;
}
} else if (key === "<down>") {
const prevSearchList = vimState.searchStatePrevious!;
if (prevSearchList[vimState.searchStateIndex] !== undefined) {
searchState.searchString = prevSearchList[vimState.searchStateIndex].searchString;
vimState.searchStateIndex += 1;
const prevSearchList = vimState.globalState.searchStatePrevious!;
if (prevSearchList[vimState.globalState.searchStateIndex] !== undefined) {
searchState.searchString = prevSearchList[vimState.globalState.searchStateIndex].searchString;
vimState.globalState.searchStateIndex += 1;
}
} else {
searchState.searchString += this.keysPressed[0];
}
// Clamp the history index to stay within bounds of search history length
if (vimState.searchStateIndex > vimState.searchStatePrevious.length - 1) {
vimState.searchStateIndex = vimState.searchStatePrevious.length - 1;
if (vimState.globalState.searchStateIndex > vimState.globalState.searchStatePrevious.length - 1) {
vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length - 1;
}
if (vimState.searchStateIndex < 0) {
vimState.searchStateIndex = 0;
if (vimState.globalState.searchStateIndex < 0) {
vimState.globalState.searchStateIndex = 0;
}
return vimState;
@ -1498,7 +1499,7 @@ class CommandEscInSearchMode extends BaseCommand {
public async exec(position: Position, vimState: VimState): Promise<VimState> {
vimState.currentMode = ModeName.Normal;
vimState.searchState = undefined;
vimState.globalState.searchState = undefined;
return vimState;
}
@ -1511,7 +1512,7 @@ class CommandCtrlVInSearchMode extends BaseCommand {
runsOnceForEveryCursor() { return this.keysPressed[0] === '\n'; }
public async exec(position: Position, vimState: VimState): Promise<VimState> {
const searchState = vimState.searchState!;
const searchState = vimState.globalState.searchState!;
const textFromClipboard = await new Promise<string>((resolve, reject) =>
clipboard.paste((err, text) => err ? reject(err) : resolve(text))
);
@ -1528,7 +1529,7 @@ class CommandCmdVInSearchMode extends BaseCommand {
runsOnceForEveryCursor() { return this.keysPressed[0] === '\n'; }
public async exec(position: Position, vimState: VimState): Promise<VimState> {
const searchState = vimState.searchState!;
const searchState = vimState.globalState.searchState!;
const textFromClipboard = await new Promise<string>((resolve, reject) =>
clipboard.paste((err, text) => err ? reject(err) : resolve(text))
);
@ -1543,7 +1544,7 @@ class CommandNextSearchMatch extends BaseMovement {
keys = ["n"];
public async execAction(position: Position, vimState: VimState): Promise<Position> {
const searchState = vimState.searchState;
const searchState = vimState.globalState.searchState;
if (!searchState || searchState.searchString === "") {
return position;
@ -1583,10 +1584,10 @@ class CommandStar extends BaseCommand {
return vimState;
}
vimState.searchState = new SearchState(SearchDirection.Forward, vimState.cursorPosition, currentWord);
vimState.globalState.searchState = new SearchState(SearchDirection.Forward, vimState.cursorPosition, currentWord);
do {
vimState.cursorPosition = vimState.searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos;
vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos;
} while (TextEditor.getWord(vimState.cursorPosition) !== currentWord);
// Turn one of the highlighting flags back on (turned off with :nohl)
@ -1609,13 +1610,13 @@ class CommandHash extends BaseCommand {
return vimState;
}
vimState.searchState = new SearchState(SearchDirection.Backward, vimState.cursorPosition, currentWord);
vimState.globalState.searchState = new SearchState(SearchDirection.Backward, vimState.cursorPosition, currentWord);
do {
// use getWordLeft() on position to start at the beginning of the word.
// this ensures that any matches happen ounside of the word currently selected,
// which are the desired semantics for this motion.
vimState.cursorPosition = vimState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos;
vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos;
} while (TextEditor.getWord(vimState.cursorPosition) !== currentWord);
// Turn one of the highlighting flags back on (turned off with :nohl)
@ -1630,7 +1631,7 @@ class CommandPreviousSearchMatch extends BaseMovement {
keys = ["N"];
public async execAction(position: Position, vimState: VimState): Promise<Position> {
const searchState = vimState.searchState;
const searchState = vimState.globalState.searchState;
if (!searchState || searchState.searchString === "") {
return position;
@ -1705,11 +1706,11 @@ export class CommandSearchForwards extends BaseCommand {
isMotion = true;
public async exec(position: Position, vimState: VimState): Promise<VimState> {
vimState.searchState = new SearchState(SearchDirection.Forward, vimState.cursorPosition, "", { isRegex: true });
vimState.globalState.searchState = new SearchState(SearchDirection.Forward, vimState.cursorPosition, "", { isRegex: true });
vimState.currentMode = ModeName.SearchInProgressMode;
// Reset search history index
vimState.searchStateIndex = vimState.searchStatePrevious.length - 1;
vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length - 1;
Configuration.hl = true;
@ -1724,11 +1725,11 @@ export class CommandSearchBackwards extends BaseCommand {
isMotion = true;
public async exec(position: Position, vimState: VimState): Promise<VimState> {
vimState.searchState = new SearchState(SearchDirection.Backward, vimState.cursorPosition, "", { isRegex: true });
vimState.globalState.searchState = new SearchState(SearchDirection.Backward, vimState.cursorPosition, "", { isRegex: true });
vimState.currentMode = ModeName.SearchInProgressMode;
// Reset search history index
vimState.searchStateIndex = vimState.searchStatePrevious.length - 1;
vimState.globalState.searchStateIndex = vimState.globalState.searchStatePrevious.length - 1;
Configuration.hl = true;

View File

@ -36,8 +36,8 @@ import { showCmdLine } from '../../src/cmd_line/main';
import { Configuration } from '../../src/configuration/configuration';
import { PairMatcher } from './../matching/matcher';
import { Globals } from '../../src/globals';
import { SearchState } from './../state/searchState';
import { ReplaceState } from './../state/replaceState';
import { GlobalState } from './../state/globalState';
export class ViewChange {
public command: string;
@ -88,12 +88,6 @@ export class VimState {
public lastMovementFailed: boolean = false;
/**
* The keystroke sequence that made up our last complete action (that can be
* repeated with '.').
*/
public previousFullAction: RecordedState | undefined = undefined;
public alteredHistory = false;
public isRunningDotCommand = false;
@ -123,6 +117,8 @@ export class VimState {
*/
public currentFullAction: string[] = [];
public globalState: GlobalState = new GlobalState;
/**
* The position the cursor will be when this action finishes.
*/
@ -168,18 +164,6 @@ export class VimState {
public cursorPositionJustBeforeAnythingHappened = [ new Position(0, 0) ];
public searchState: SearchState | undefined = undefined;
/**
* Index used for navigating search history with <up> and <down> when searching
*/
public searchStateIndex: number = 0;
/**
* Previous searches performed
*/
public searchStatePrevious: SearchState[] = [];
public isRecordingMacro: boolean = false;
public replaceState: ReplaceState | undefined = undefined;
@ -861,7 +845,7 @@ export class ModeHandler implements vscode.Disposable {
if (vimState.cursorPositionJustBeforeAnythingHappened.line !== prevPos.line ||
vimState.cursorPositionJustBeforeAnythingHappened.character !== prevPos.character) {
vimState.previousFullAction = recordedState;
vimState.globalState.previousFullAction = recordedState;
vimState.historyTracker.finishCurrentStep();
}
}
@ -931,7 +915,7 @@ export class ModeHandler implements vscode.Disposable {
// Record down previous action and flush temporary state
if (ranRepeatableAction) {
vimState.previousFullAction = vimState.recordedState;
vimState.globalState.previousFullAction = vimState.recordedState;
if (recordedState.isInsertion) {
Register.putByKey(recordedState, '.');
@ -1324,15 +1308,15 @@ export class ModeHandler implements vscode.Disposable {
break;
case "dot":
if (!vimState.previousFullAction) {
if (!vimState.globalState.previousFullAction) {
return vimState; // TODO(bell)
}
const clonedAction = vimState.previousFullAction.clone();
const clonedAction = vimState.globalState.previousFullAction.clone();
await this.rerunRecordedState(vimState, vimState.previousFullAction);
await this.rerunRecordedState(vimState, vimState.globalState.previousFullAction);
vimState.previousFullAction = clonedAction;
vimState.globalState.previousFullAction = clonedAction;
break;
case "macro":
let recordedMacro = (await Register.getByKey(command.register)).text as RecordedState;
@ -1572,7 +1556,7 @@ export class ModeHandler implements vscode.Disposable {
// Scroll to position of cursor
if (this._vimState.currentMode === ModeName.SearchInProgressMode) {
const nextMatch = vimState.searchState!.getNextSearchMatchPosition(vimState.cursorPosition).pos;
const nextMatch = vimState.globalState.searchState!.getNextSearchMatchPosition(vimState.cursorPosition).pos;
vscode.window.activeTextEditor.revealRange(new vscode.Range(nextMatch, nextMatch));
} else {
@ -1631,9 +1615,9 @@ export class ModeHandler implements vscode.Disposable {
if (
(Configuration.incsearch && this.currentMode.name === ModeName.SearchInProgressMode) ||
((Configuration.hlsearch && Configuration.hl) && vimState.searchState)) {
((Configuration.hlsearch && Configuration.hl) && vimState.globalState.searchState)) {
const searchState = vimState.searchState!;
const searchState = vimState.globalState.searchState!;
searchRanges.push.apply(searchRanges, searchState.matchRanges);
@ -1656,7 +1640,7 @@ export class ModeHandler implements vscode.Disposable {
this.vimState.postponedCodeViewChanges = [];
if (this.currentMode.name === ModeName.SearchInProgressMode) {
this.setStatusBarText(`Searching for: ${ this.vimState.searchState!.searchString }`);
this.setStatusBarText(`Searching for: ${ this.vimState.globalState.searchState!.searchString }`);
} else if (this.currentMode.name === ModeName.EasyMotionMode) {
// Update all EasyMotion decorations
this._vimState.easyMotion.updateDecorations();
@ -1680,7 +1664,7 @@ export class ModeHandler implements vscode.Disposable {
}
if (this._vimState.currentMode === ModeName.SearchInProgressMode) {
currentCommandText = ` ${ this._vimState.searchState!.searchString }`;
currentCommandText = ` ${ this._vimState.globalState.searchState!.searchString }`;
}
this.setStatusBarText(`${ modeText }${ currentCommandText }${ macroText }`);

63
src/state/globalState.ts Normal file
View File

@ -0,0 +1,63 @@
import { SearchState } from './searchState';
import { RecordedState } from '../mode/modeHandler';
/**
* State which stores global state (across editors)
*/
export class GlobalState {
/**
* The keystroke sequence that made up our last complete action (that can be
* repeated with '.').
*/
private static _previousFullAction: RecordedState | undefined = undefined;
/**
* Previous searches performed
*/
private static _searchStatePrevious: SearchState[] = [];
/**
* Last search state for running n and N commands
*/
private static _searchState: SearchState | undefined = undefined;
/**
* Index used for navigating search history with <up> and <down> when searching
*/
private static _searchStateIndex: number = 0;
/**
* Getters and setters for changing global state
*/
public get searchStatePrevious(): SearchState[]{
return GlobalState._searchStatePrevious;
}
public set searchStatePrevious(states: SearchState[]) {
GlobalState._searchStatePrevious = GlobalState._searchStatePrevious.concat(states);
}
public get previousFullAction(): RecordedState | undefined {
return GlobalState._previousFullAction;
}
public set previousFullAction(state : RecordedState | undefined) {
GlobalState._previousFullAction = state;
}
public get searchState(): SearchState | undefined {
return GlobalState._searchState;
}
public set searchState(state : SearchState | undefined) {
GlobalState._searchState = state;
}
public get searchStateIndex(): number {
return GlobalState._searchStateIndex;
}
public set searchStateIndex(state : number) {
GlobalState._searchStateIndex = state;
}
}

View File

@ -30,6 +30,7 @@ export class SearchState {
}
private _cachedDocumentVersion: number;
private _cachedDocumentName: String;
private _searchDirection: SearchDirection = SearchDirection.Forward;
private isRegex: boolean;
@ -52,9 +53,13 @@ export class SearchState {
if (search === "") { return; }
if (this._cachedDocumentVersion !== TextEditor.getDocumentVersion() || forceRecalc) {
// checking if the tab that is worked on has changed, or the file version has changed
const shouldRecalculate = (this._cachedDocumentName !== TextEditor.getDocumentName()) ||
(this._cachedDocumentVersion !== TextEditor.getDocumentVersion()) || forceRecalc;
if (shouldRecalculate) {
// Calculate and store all matching ranges
this._cachedDocumentVersion = TextEditor.getDocumentVersion();
this._cachedDocumentName = TextEditor.getDocumentName();
this._matchRanges = [];
/*

View File

@ -56,6 +56,10 @@ export class TextEditor {
return vscode.window.activeTextEditor.document.version;
}
static getDocumentName(): String {
return vscode.window.activeTextEditor.document.fileName;
}
/**
* Removes all text in the entire document.
*/

View File

@ -30,6 +30,24 @@ export async function waitForCursorUpdatesToHappen(): Promise<void> {
});
}
/**
* Waits for the tabs to change after a command like 'gt' or 'gT' is run.
* Sometimes it is not immediate, so we must busy wait
* On certain versions, the tab changes are synchronous
* For those, a timeout is given
*/
export async function waitForTabChange(): Promise<void> {
await new Promise((resolve, reject) => {
setTimeout(resolve, 100);
const disposer = vscode.window.onDidChangeActiveTextEditor((textEditor) => {
disposer.dispose();
resolve(textEditor);
});
});
}
export async function allowVSCodeToPropagateCursorUpdatesAndReturnThem(): Promise<Range[]> {
await waitForCursorUpdatesToHappen();

View File

@ -1,7 +1,9 @@
"use strict";
import { setupWorkspace, cleanUpWorkspace, setTextEditorOptions } from './../../testUtils';
import { setupWorkspace, cleanUpWorkspace, setTextEditorOptions, assertEqualLines } from './../../testUtils';
import { ModeHandler } from '../../../src/mode/modeHandler';
import { waitForTabChange } from '../../../src/util';
import * as assert from 'assert';
import { getTestingFunctions } from '../../testSimplifier';
suite("Dot Operator", () => {
@ -19,6 +21,28 @@ suite("Dot Operator", () => {
teardown(cleanUpWorkspace);
test('repeats actions across editors ', async () => {
// setting the content of the first 2 tabs
const firstTabContent = 'some\ntest\nabc\nend';
const secondTabContent = 'another\ntest\ndef\nend';
const firstTabKeys = ['<Esc>', 'a'].concat(firstTabContent.split(''));
const secondTabKeys = ['<Esc>', 'a'].concat(secondTabContent.split(''));
await setupWorkspace();
setTextEditorOptions(5, false);
await modeHandler.handleMultipleKeyEvents(firstTabKeys.concat(['<Esc>']));
await modeHandler.handleMultipleKeyEvents(['<Esc>', 'g', 'T']);
await waitForTabChange();
await modeHandler.handleMultipleKeyEvents(secondTabKeys.concat(['<Esc>']));
// running an action in second tab and repeating in first tab
await modeHandler.handleMultipleKeyEvents(['g', 'g', 'd' , 'd']);
await assertEqualLines(['test', 'def', 'end']);
await modeHandler.handleMultipleKeyEvents(['g', 't']);
await waitForTabChange();
await modeHandler.handleMultipleKeyEvents(['<Esc>', 'g', 'g', '.']);
await assertEqualLines(['test', 'abc', 'end']);
});
newTest({
title: "Can repeat '~' with <num>",
start: ['|teXt'],

View File

@ -2,7 +2,8 @@
import { setupWorkspace, cleanUpWorkspace } from './../../testUtils';
import { ModeHandler } from '../../../src/mode/modeHandler';
import { getTestingFunctions } from '../../testSimplifier';
import { getTestingFunctions, testIt } from '../../testSimplifier';
import { waitForTabChange } from '../../../src/util';
suite("Motions in Normal Mode", () => {
let modeHandler: ModeHandler = new ModeHandler();
@ -277,6 +278,52 @@ suite("Motions in Normal Mode", () => {
end: ['one two |two two'],
});
test('Remembers a forward search from another editor', async function() {
// adding another editor
await setupWorkspace();
await testIt(modeHandler, {
title: "",
start: ['|one two two two'],
keysPressed: '/two\n',
end: ['one |two two two'],
});
await modeHandler.handleMultipleKeyEvents(['g', 'T', '<Esc>']);
await waitForTabChange();
await testIt(modeHandler, {
title: "",
start: ['|three four two one'],
keysPressed: '<Esc>n',
end: ['three four |two one'],
});
});
test('Shares forward search history from another editor', async () => {
// adding another editor
await setupWorkspace();
await testIt(modeHandler, {
title: "",
start: ['|one two two two'],
keysPressed: '/two\n',
end: ['one |two two two'],
});
await modeHandler.handleMultipleKeyEvents(['g', 'T', '<Esc>']);
await waitForTabChange();
await testIt(modeHandler, {
title: "",
start: ['|three four two one'],
keysPressed: '/\n',
end: ['three four |two one'],
});
});
newTest({
title: "Can run a reverse search",
start: ['one two thre|e'],
@ -291,6 +338,53 @@ suite("Motions in Normal Mode", () => {
end: ['one |two two three'],
});
test('Remembers a reverse search from another editor', async () => {
// adding another editor
await setupWorkspace();
await testIt(modeHandler, {
title: "",
start: ['one two two two|'],
keysPressed: '?two\n',
end: ['one two two |two'],
});
await modeHandler.handleMultipleKeyEvents(['g', 'T', '<Esc>']);
await waitForTabChange();
await testIt(modeHandler, {
title: "",
start: ['three four two one|'],
keysPressed: '<Esc>n',
end: ['three four |two one'],
});
});
test('Shares reverse search history from another editor', async () => {
// adding another editor
await setupWorkspace();
await testIt(modeHandler, {
title: "",
start: ['one two two two|'],
keysPressed: '?two\n',
end: ['one two two |two'],
});
await modeHandler.handleMultipleKeyEvents(['g', 'T', '<Esc>']);
await waitForTabChange();
await testIt(modeHandler, {
title: "",
start: ['three four two one|'],
keysPressed: '?\n',
end: ['three four |two one'],
});
});
newTest({
title: "maintains column position correctly",
start: ['|one one one', 'two', 'three'],