diff --git a/src/actions/actions.ts b/src/actions/actions.ts index 819b3f78f..70217f168 100644 --- a/src/actions/actions.ts +++ b/src/actions/actions.ts @@ -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 { const key = this.keysPressed[0]; - const searchState = vimState.searchState!; + const searchState = vimState.globalState.searchState!; // handle special keys first if (key === "" || key === "") { @@ -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 === "") { - 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 === "") { - 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.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 { - const searchState = vimState.searchState!; + const searchState = vimState.globalState.searchState!; const textFromClipboard = await new Promise((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 { - const searchState = vimState.searchState!; + const searchState = vimState.globalState.searchState!; const textFromClipboard = await new Promise((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 { - 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 { - 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.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.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; diff --git a/src/mode/modeHandler.ts b/src/mode/modeHandler.ts index decfd4cd6..baa979cda 100644 --- a/src/mode/modeHandler.ts +++ b/src/mode/modeHandler.ts @@ -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 and 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 }`); diff --git a/src/state/globalState.ts b/src/state/globalState.ts new file mode 100644 index 000000000..35200b249 --- /dev/null +++ b/src/state/globalState.ts @@ -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 and 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; + } +} diff --git a/src/state/searchState.ts b/src/state/searchState.ts index 22983bca0..6595bd79d 100644 --- a/src/state/searchState.ts +++ b/src/state/searchState.ts @@ -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 = []; /* diff --git a/src/textEditor.ts b/src/textEditor.ts index 60f4ee3aa..42f104160 100644 --- a/src/textEditor.ts +++ b/src/textEditor.ts @@ -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. */ diff --git a/src/util.ts b/src/util.ts index 5c4511085..860d3bdf2 100644 --- a/src/util.ts +++ b/src/util.ts @@ -30,6 +30,24 @@ export async function waitForCursorUpdatesToHappen(): Promise { }); } +/** + * 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 { + await new Promise((resolve, reject) => { + setTimeout(resolve, 100); + + const disposer = vscode.window.onDidChangeActiveTextEditor((textEditor) => { + disposer.dispose(); + + resolve(textEditor); + }); + }); +} + export async function allowVSCodeToPropagateCursorUpdatesAndReturnThem(): Promise { await waitForCursorUpdatesToHappen(); diff --git a/test/mode/normalModeTests/dot.test.ts b/test/mode/normalModeTests/dot.test.ts index 12a8a1f1e..771202db4 100644 --- a/test/mode/normalModeTests/dot.test.ts +++ b/test/mode/normalModeTests/dot.test.ts @@ -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 = ['', 'a'].concat(firstTabContent.split('')); + const secondTabKeys = ['', 'a'].concat(secondTabContent.split('')); + await setupWorkspace(); + setTextEditorOptions(5, false); + await modeHandler.handleMultipleKeyEvents(firstTabKeys.concat([''])); + await modeHandler.handleMultipleKeyEvents(['', 'g', 'T']); + await waitForTabChange(); + await modeHandler.handleMultipleKeyEvents(secondTabKeys.concat([''])); + + // 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(['', 'g', 'g', '.']); + await assertEqualLines(['test', 'abc', 'end']); + }); + newTest({ title: "Can repeat '~' with ", start: ['|teXt'], diff --git a/test/mode/normalModeTests/motions.test.ts b/test/mode/normalModeTests/motions.test.ts index b7e1d9650..8c9a4f16c 100644 --- a/test/mode/normalModeTests/motions.test.ts +++ b/test/mode/normalModeTests/motions.test.ts @@ -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', '']); + + await waitForTabChange(); + + await testIt(modeHandler, { + title: "", + start: ['|three four two one'], + keysPressed: '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', '']); + + 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', '']); + + await waitForTabChange(); + + await testIt(modeHandler, { + title: "", + start: ['three four two one|'], + keysPressed: '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', '']); + + 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'],