diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 8f2914b7ca4..9b0969d648e 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -12,9 +12,9 @@ export interface IIterator { export class ArrayIterator implements IIterator { private items: T[]; - private start: number; - private end: number; - private index: number; + protected start: number; + protected end: number; + protected index: number; constructor(items: T[], start: number = 0, end: number = items.length) { this.items = items; @@ -25,8 +25,11 @@ export class ArrayIterator implements IIterator { public next(): T { this.index = Math.min(this.index + 1, this.end); + return this.current(); + } - if (this.index === this.end) { + protected current(): T { + if (this.index === this.start - 1 || this.index === this.end) { return null; } @@ -34,6 +37,37 @@ export class ArrayIterator implements IIterator { } } +export class ArrayNavigator extends ArrayIterator implements INavigator { + + constructor(items: T[], start: number = 0, end: number = items.length) { + super(items, start, end); + } + + public current(): T { + return super.current(); + } + + public previous(): T { + this.index = Math.max(this.index - 1, this.start - 1); + return this.current(); + } + + public first(): T { + this.index = this.start; + return this.current(); + } + + public last(): T { + this.index = this.end - 1; + return this.current(); + } + + public parent(): T { + return null; + } + +} + export class MappedIterator implements IIterator { constructor(protected iterator: IIterator, protected fn: (item:T)=>R) { diff --git a/src/vs/test/utils/instantiationTestUtils.ts b/src/vs/test/utils/instantiationTestUtils.ts index b9fc6d08040..14004266902 100644 --- a/src/vs/test/utils/instantiationTestUtils.ts +++ b/src/vs/test/utils/instantiationTestUtils.ts @@ -5,6 +5,7 @@ import * as sinon from 'sinon'; import { TPromise } from 'vs/base/common/winjs.base'; +import * as types from 'vs/base/common/types'; import { LinkedMap } from 'vs/base/common/map'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -26,6 +27,8 @@ import { MarkerService } from 'vs/platform/markers/common/markerService'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; import { ReplaceService } from 'vs/workbench/parts/search/browser/replaceService'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { WorkbenchKeybindingService } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; interface IServiceMock { id: ServiceIdentifier; @@ -48,6 +51,7 @@ export class TestInstantiationService extends InstantiationService { this._servciesMap.set(IExtensionService, SimpleExtensionService); this._servciesMap.set(IMarkerService, MarkerService); this._servciesMap.set(IReplaceService, ReplaceService); + this._servciesMap.set(IKeybindingService, WorkbenchKeybindingService); } public mock(service:ServiceIdentifier): T | sinon.SinonMock { @@ -136,6 +140,13 @@ export class TestInstantiationService extends InstantiationService { } } +export function stubFunction(ctor: any, fnProperty: string, value: any): T | sinon.SinonStub { + let stub = sinon.createStubInstance(ctor); + stub[fnProperty].restore(); + sinon.stub(stub, fnProperty, types.isFunction(value) ? value : () => { return value; }); + return stub; +} + interface SinonOptions { mock?: boolean; stub?: boolean; diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index 80ac858ba66..5cdf3fd3cbb 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -24,7 +24,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Keybinding, KeyCode, KeyMod, CommonKeybindings } from 'vs/base/common/keyCodes'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; +import { asFileEditorInput } from 'vs/workbench/common/editor'; export function isSearchViewletFocussed(viewletService: IViewletService): boolean { let activeViewlet = viewletService.getActiveViewlet(); @@ -132,7 +132,7 @@ export abstract class AbstractSearchAndReplaceAction extends Action { /** * Returns element to focus after removing the given element */ - protected getElementToFocusAfterRemoved(viewer: ITree, elementToBeRemoved: FileMatchOrMatch): FileMatchOrMatch { + public getElementToFocusAfterRemoved(viewer: ITree, elementToBeRemoved: FileMatchOrMatch): FileMatchOrMatch { let elementToFocus = this.getNextElementAfterRemoved(viewer, elementToBeRemoved); if (!elementToFocus) { elementToFocus = this.getPreviousElementAfterRemoved(viewer, elementToBeRemoved); @@ -140,7 +140,7 @@ export abstract class AbstractSearchAndReplaceAction extends Action { return elementToFocus; } - protected getNextElementAfterRemoved(viewer: ITree, element: FileMatchOrMatch): FileMatchOrMatch { + public getNextElementAfterRemoved(viewer: ITree, element: FileMatchOrMatch): FileMatchOrMatch { let navigator: INavigator = this.getNavigatorAt(element, viewer); if (element instanceof FileMatch) { // If file match is removed then next element is the next file match @@ -151,7 +151,7 @@ export abstract class AbstractSearchAndReplaceAction extends Action { return navigator.current(); } - protected getPreviousElementAfterRemoved(viewer: ITree, element: FileMatchOrMatch): FileMatchOrMatch { + public getPreviousElementAfterRemoved(viewer: ITree, element: FileMatchOrMatch): FileMatchOrMatch { let navigator: INavigator = this.getNavigatorAt(element, viewer); let previousElement = navigator.previous(); if (element instanceof Match && element.parent().matches().length === 1) { @@ -272,9 +272,9 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { } private hasToOpenFile(): boolean { - let activeInput = this.editorService.getActiveEditorInput(); - if (activeInput instanceof FileEditorInput) { - return activeInput.getResource().fsPath === this.element.parent().resource().fsPath; + const editorInput = asFileEditorInput(this.editorService.getActiveEditorInput()); + if (editorInput) { + return editorInput.getResource().fsPath === this.element.parent().resource().fsPath; } return false; } diff --git a/src/vs/workbench/parts/search/test/browser/searchActions.test.ts b/src/vs/workbench/parts/search/test/browser/searchActions.test.ts new file mode 100644 index 00000000000..a25de6e3f7f --- /dev/null +++ b/src/vs/workbench/parts/search/test/browser/searchActions.test.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as assert from 'assert'; +import URI from 'vs/base/common/uri'; +import { TestInstantiationService, stubFunction } from 'vs/test/utils/instantiationTestUtils'; +import { Match, FileMatch, FileMatchOrMatch } from 'vs/workbench/parts/search/common/searchModel'; +import { ReplaceAction } from 'vs/workbench/parts/search/browser/searchActions'; +import { ArrayNavigator } from 'vs/base/common/iterator'; +import { IFileMatch } from 'vs/platform/search/common/search'; +import { createMockModelService } from 'vs/test/utils/servicesTestUtils'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; + +suite('Search Actions', () => { + + let instantiationService: TestInstantiationService; + let counter: number; + + setup(() => { + instantiationService = new TestInstantiationService(); + instantiationService.stub(IModelService, createMockModelService(instantiationService)); + instantiationService.stub(IKeybindingService); + counter = 0; + }); + + test('get next element to focus after removing a match when it has next sibling match', function () { + let fileMatch1 = aFileMatch(); + let fileMatch2 = aFileMatch(); + let data = [fileMatch1, aMatch(fileMatch1), aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2), aMatch(fileMatch2)]; + let tree = aTree(data); + let target = data[2]; + let testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); + + let actual = testObject.getElementToFocusAfterRemoved(tree, target); + + assert.equal(data[3], actual); + }); + + test('get next element to focus after removing a match when it does not have next sibling match', function () { + let fileMatch1 = aFileMatch(); + let fileMatch2 = aFileMatch(); + let data = [fileMatch1, aMatch(fileMatch1), aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2), aMatch(fileMatch2)]; + let tree = aTree(data); + let target = data[5]; + let testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); + + let actual = testObject.getElementToFocusAfterRemoved(tree, target); + + assert.equal(data[4], actual); + }); + + test('get next element to focus after removing a match when it does not have next sibling match and previous match is file match', function () { + let fileMatch1 = aFileMatch(); + let fileMatch2 = aFileMatch(); + let data = [fileMatch1, aMatch(fileMatch1), aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2)]; + let tree = aTree(data); + let target = data[4]; + let testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); + + let actual = testObject.getElementToFocusAfterRemoved(tree, target); + + assert.equal(data[2], actual); + }); + + test('get next element to focus after removing a match when it is the only match', function () { + let fileMatch1 = aFileMatch(); + let data = [fileMatch1, aMatch(fileMatch1)]; + let tree = aTree(data); + let target = data[1]; + let testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); + + let actual = testObject.getElementToFocusAfterRemoved(tree, target); + + assert.equal(void 0, actual); + }); + + test('get next element to focus after removing a file match when it has next sibling', function () { + let fileMatch1 = aFileMatch(); + let fileMatch2 = aFileMatch(); + let fileMatch3 = aFileMatch(); + let data = [fileMatch1, aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2), fileMatch3, aMatch(fileMatch3)]; + let tree = aTree(data); + let target = data[2]; + let testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); + + let actual = testObject.getElementToFocusAfterRemoved(tree, target); + + assert.equal(data[4], actual); + }); + + test('get next element to focus after removing a file match when it has no next sibling', function () { + let fileMatch1 = aFileMatch(); + let fileMatch2 = aFileMatch(); + let fileMatch3 = aFileMatch(); + let data = [fileMatch1, aMatch(fileMatch1), fileMatch2, aMatch(fileMatch2), fileMatch3, aMatch(fileMatch3)]; + let tree = aTree(data); + let target = data[4]; + let testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); + + let actual = testObject.getElementToFocusAfterRemoved(tree, target); + + assert.equal(data[3], actual); + }); + + test('get next element to focus after removing a file match when it is only match', function () { + let fileMatch1 = aFileMatch(); + let data = [fileMatch1, aMatch(fileMatch1)]; + let tree = aTree(data); + let target = data[0]; + let testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); + + let actual = testObject.getElementToFocusAfterRemoved(tree, target); + + assert.equal(void 0, actual); + }); + + function aFileMatch(): FileMatch { + let rawMatch: IFileMatch = { + resource: URI.file('somepath' + ++counter), + lineMatches: [] + }; + return instantiationService.createInstance(FileMatch, null, null, rawMatch); + } + + function aMatch(fileMatch: FileMatch): Match { + let match = new Match(fileMatch, 'some match', ++counter, 0, 2); + fileMatch.add(match); + return match; + } + + function aTree(elements: FileMatchOrMatch[]): any { + return stubFunction(Tree, 'getNavigator', () => { return new ArrayNavigator(elements); }); + } +}); \ No newline at end of file