diff --git a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts index d24db9e808..9f303e03c2 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts @@ -266,7 +266,6 @@ export class NotebookEditor extends BaseEditor implements IFindNotebookControlle return this._actionMap[id]; } - private async _onFindStateChange(e: FindReplaceStateChangedEvent): Promise { if (!this._notebookModel) { await this.setNotebookModel(); @@ -312,27 +311,26 @@ export class NotebookEditor extends BaseEditor implements IFindNotebookControlle } else { this.notebookInput.notebookFindModel.clearDecorations(); this.notebookFindModel.findExpression = this._findState.searchString; - this.notebookInput.notebookFindModel.find(this._findState.searchString, this._findState.matchCase, this._findState.wholeWord, NOTEBOOK_MAX_MATCHES).then(findRange => { - if (findRange) { - this.setSelection(findRange); - } else if (this.notebookFindModel.findMatches.length > 0) { - this.setSelection(this.notebookFindModel.findMatches[0].range); - } else { - this.notebookInput.notebookFindModel.clearFind(); - this._updateFinderMatchState(); - this._finder.focusFindInput(); - return; - } + const findRange = await this.notebookInput.notebookFindModel.find(this._findState.searchString, this._findState.matchCase, this._findState.wholeWord, NOTEBOOK_MAX_MATCHES); + if (findRange) { + this.setSelection(findRange); + } else if (this.notebookFindModel.findMatches.length > 0) { + this.setSelection(this.notebookFindModel.findMatches[0].range); + } else { + this.notebookInput.notebookFindModel.clearFind(); this._updateFinderMatchState(); this._finder.focusFindInput(); - this._findDecorations.set(this.notebookFindModel.findMatches, this._currentMatch); - this._findState.changeMatchInfo( - this.notebookFindModel.getFindIndex(), - this._findDecorations.getCount(), - this._currentMatch - ); - this._setCurrentFindMatch(this._currentMatch); - }); + return; + } + this._updateFinderMatchState(); + this._finder.focusFindInput(); + this._findDecorations.set(this.notebookFindModel.findMatches, this._currentMatch); + this._findState.changeMatchInfo( + this.notebookFindModel.getFindIndex(), + this._findDecorations.getCount(), + this._currentMatch + ); + this._setCurrentFindMatch(this._currentMatch); } } else { this.notebookFindModel.clearFind(); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts index 3c83490e52..420132ddd1 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts @@ -6,7 +6,9 @@ import * as assert from 'assert'; import { nb } from 'azdata'; import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor'; +import { NotebookFindMatch } from 'sql/workbench/contrib/notebook/browser/find/notebookFindDecorations'; import { ACTION_IDS } from 'sql/workbench/contrib/notebook/browser/find/notebookFindWidget'; +import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; import { UntitledNotebookInput } from 'sql/workbench/contrib/notebook/browser/models/untitledNotebookInput'; import { NotebookFindNextAction, NotebookFindPreviousAction } from 'sql/workbench/contrib/notebook/browser/notebookActions'; import { NotebookEditor } from 'sql/workbench/contrib/notebook/browser/notebookEditor'; @@ -15,8 +17,9 @@ import * as stubs from 'sql/workbench/contrib/notebook/test/stubs'; import { NotebookEditorStub } from 'sql/workbench/contrib/notebook/test/testCommon'; import { CellModel } from 'sql/workbench/services/notebook/browser/models/cell'; import { ICellModel, NotebookContentChange } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; -import { INotebookService, NotebookRange } from 'sql/workbench/services/notebook/browser/notebookService'; +import { INotebookEditor, INotebookService, NotebookRange } from 'sql/workbench/services/notebook/browser/notebookService'; import { NotebookService } from 'sql/workbench/services/notebook/browser/notebookServiceImpl'; +import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts'; import * as TypeMoq from 'typemoq'; import * as DOM from 'vs/base/browser/dom'; import { errorHandler, onUnexpectedError } from 'vs/base/common/errors'; @@ -27,7 +30,8 @@ import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { INewFindReplaceState } from 'vs/editor/contrib/find/findState'; +import { FindReplaceStateChangedEvent, INewFindReplaceState } from 'vs/editor/contrib/find/findState'; +import { getRandomString } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; @@ -56,14 +60,15 @@ import { workbenchInstantiationService } from 'vs/workbench/test/browser/workben class NotebookModelStub extends stubs.NotebookModelStub { private _cells: Array = [new CellModel(undefined, undefined)]; - private _contentChangedEmitter = new Emitter(); + public contentChangedEmitter = new Emitter(); private _kernelChangedEmitter = new Emitter(); private _onActiveCellChanged = new Emitter(); + get cells(): ReadonlyArray { return this._cells; } public get contentChanged(): Event { - return this._contentChangedEmitter.event; + return this.contentChangedEmitter.event; } get kernelChanged(): Event { @@ -81,79 +86,23 @@ class NotebookModelStub extends stubs.NotebookModelStub { } } -suite('Test class NotebookEditor', () => { - +suite('Test class NotebookEditor:', () => { + let instantiationService = workbenchInstantiationService(); + let workbenchThemeService = instantiationService.createInstance(WorkbenchThemeService); let notebookEditor: NotebookEditor; - - const installEvent: Emitter = new Emitter(); - const didInstallEvent = new Emitter(); - const uninstallEvent = new Emitter(); - const didUninstallEvent = new Emitter(); - - const instantiationService = workbenchInstantiationService(); - const workbenchThemeService = instantiationService.createInstance(WorkbenchThemeService); - instantiationService.stub(IWorkbenchThemeService, workbenchThemeService); - - const queryManagementService = new NBTestQueryManagementService(); - - instantiationService.stub(IExtensionManagementService, ExtensionManagementService); - instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); - - const extensionService = instantiationService.get(IExtensionService); - const notebookService = new NotebookService( - instantiationService.get(ILifecycleService), - instantiationService.get(IStorageService), - extensionService, - instantiationService.get(IExtensionManagementService), - instantiationService, - instantiationService.get(IFileService), - instantiationService.get(ILogService), - queryManagementService, - instantiationService.get(IContextKeyService) - ); - - instantiationService.stub(INotebookService, notebookService); - - const testTitle = 'NotebookEditor.Test-Title'; - const untitledUri = URI.from({ scheme: Schemas.untitled, path: 'NotebookEditor.Test-TestPath' }); - const untitledTextEditorService = instantiationService.get(IUntitledTextEditorService); - const untitledTextInput = instantiationService.createInstance(UntitledTextEditorInput, untitledTextEditorService.create({ associatedResource: untitledUri })); - const untitledNotebookInput = new UntitledNotebookInput( - testTitle, untitledUri, untitledTextInput, - undefined, instantiationService, notebookService, extensionService - ); - - const cellTextEditorGuid = generateUuid(); - const queryTextEditor = new QueryTextEditor( - instantiationService.get(ITelemetryService), - instantiationService, - instantiationService.get(IStorageService), - instantiationService.get(ITextResourceConfigurationService), - instantiationService.get(IThemeService), - instantiationService.get(IEditorGroupsService), - instantiationService.get(IEditorService), - instantiationService.get(IConfigurationService) - ); - const notebookEditorStub = new NotebookEditorStub({ cellGuid: cellTextEditorGuid, editor: queryTextEditor, model: new NotebookModelStub() }); - notebookEditorStub.id = untitledNotebookInput.notebookUri.toString(); - notebookService.addNotebookEditor(notebookEditorStub); + let testTitle: string; + let notebookService: NotebookService; + let extensionService: IExtensionService; + let cellTextEditorGuid: string; + let queryTextEditor: QueryTextEditor; + let untitledNotebookInput: UntitledNotebookInput; + let notebookEditorStub: NotebookEditorStub; setup(async () => { + // setup services + ({ instantiationService, workbenchThemeService, notebookService, testTitle, extensionService, cellTextEditorGuid, queryTextEditor, untitledNotebookInput, notebookEditorStub } = setupServices({ instantiationService, workbenchThemeService })); // Create notebookEditor - notebookEditor = new NotebookEditor( - instantiationService.get(ITelemetryService), - instantiationService.get(IThemeService), - instantiationService, - instantiationService.get(IStorageService), - new ContextViewService(instantiationService.get(ILayoutService)), - instantiationService.get(IKeybindingService), - instantiationService.get(IContextKeyService), - workbenchThemeService, - notebookService - ); + notebookEditor = createNotebookEditor(instantiationService, workbenchThemeService, notebookService); }); test('Verifies that create() calls createEditor() and sets the provided parent object as the \'_overlay\' field', () => { @@ -165,7 +114,7 @@ suite('Test class NotebookEditor', () => { }); for (const notebookModel of [new NotebookModelStub(), undefined]) { - test.skip(`Tests that notebookModel='${notebookModel}' set indirectly by setInput -> setNotebookModel is returned by getNotebookModel()`, async () => { + test(`Tests that notebookModel='${notebookModel}' set indirectly by setInput -> setNotebookModel is returned by getNotebookModel()`, async () => { createEditor(notebookEditor); const untitledUri = URI.from({ scheme: Schemas.untitled, path: `NotebookEditor.Test-TestPath-${notebookModel}` }); const untitledTextEditorService = instantiationService.get(IUntitledTextEditorService); @@ -179,6 +128,7 @@ suite('Test class NotebookEditor', () => { notebookService.addNotebookEditor(testNotebookEditor); notebookEditor.clearInput(); await notebookEditor.setInput(untitledNotebookInput, EditorOptions.create({ pinned: true })); + untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel const result = await notebookEditor.getNotebookModel(); assert.strictEqual(result, notebookModel, `getNotebookModel() should return the model set in the INotebookEditor object`); }); @@ -193,7 +143,7 @@ suite('Test class NotebookEditor', () => { assert.ok(isDisposed, 'notebookEditor\'s disposable store must be disposed'); }); - test('Tests that getPosition and getLastPosition correctly return the range set by setSelection', async () => { + test('Tests that getPosition() and getLastPosition() correctly return the range set by setSelection', async () => { await setupNotebookEditor(notebookEditor, untitledNotebookInput); let currentPosition = notebookEditor.getPosition(); let lastPosition = notebookEditor.getLastPosition(); @@ -334,8 +284,7 @@ suite('Test class NotebookEditor', () => { test('Verifies toggleSearch changes isRevealed state with and without a notebookModel', async () => { await setupNotebookEditor(notebookEditor, untitledNotebookInput); - const notebookModel = await notebookEditor.getNotebookModel(); - for (const model of [notebookModel, undefined]) { + for (const model of [new NotebookModelStub(), undefined]) { notebookEditor['_notebookModel'] = model; for (let i: number = 1; i <= 2; i++) { //Do it twice so that two toggles return back to original state verifying both transitions let isRevealed = notebookEditor['_findState']['_isRevealed']; @@ -346,11 +295,10 @@ suite('Test class NotebookEditor', () => { }); for (const action of [ACTION_IDS.FIND_NEXT, ACTION_IDS.FIND_PREVIOUS]) { - test.skip(`Tests that ${action} raises 'no search running' error when findArray is empty`, async () => { + test(`Tests that ${action} raises 'no search running' error when findArray is empty`, async () => { await setupNotebookEditor(notebookEditor, untitledNotebookInput); let unexpectedErrorCalled = false; const onUnexpectedErrorVerifier = (error: any) => { - //console.log(`Verifies that: ${onUnexpectedError} is passed an instance of ${Error}`); unexpectedErrorCalled = true; assert.ok(error instanceof Error, `${onUnexpectedError} must be passed an instance of ${Error}`); assert.strictEqual((error as Error).message, 'no search running', `Error text must be 'no search running' when findArray is empty`); @@ -366,8 +314,9 @@ suite('Test class NotebookEditor', () => { for (const action of [ACTION_IDS.FIND_NEXT, ACTION_IDS.FIND_PREVIOUS]) { for (const range of [{}, new NotebookRange({}, 0, 0, 0, 0)]) { - test.skip(`Tests ${action} returns the NotebookRange with cell: '${JSON.stringify(range.cell)}' that is as expected given the findArray`, async () => { + test(`Tests ${action} returns the NotebookRange with cell: '${JSON.stringify(range.cell)}' that is as expected given the findArray`, async () => { await setupNotebookEditor(notebookEditor, untitledNotebookInput); + untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel const notebookModel = await notebookEditor.getNotebookModel(); const mockModel = TypeMoq.Mock.ofInstance(notebookModel); mockModel.callBase = true; //forward calls to the base object @@ -393,15 +342,18 @@ suite('Test class NotebookEditor', () => { } } - test.skip(`Verifies visibility and decorations are set correctly when FindStateChange callbacks happen`, async () => { + test(`Verifies visibility and decorations are set correctly when _onFindStateChange callback happens`, async () => { await setupNotebookEditor(notebookEditor, untitledNotebookInput); let currentPosition = new NotebookRange({}, 0, 0, 0, 0); notebookEditor.setSelection(currentPosition); notebookEditor.notebookFindModel['_findArray'] = [currentPosition]; //set some pending finds. + untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel + await notebookEditor.setNotebookModel(); await notebookEditor.findNext(); const findState = notebookEditor['_findState']; const finder = notebookEditor['_finder']; const newState: INewFindReplaceState = {}; + const findDecorations = notebookEditor.notebookInput.notebookFindModel.findDecorations; const findDecorationsMock = TypeMoq.Mock.ofInstance(findDecorations); findDecorationsMock.callBase = true; //forward to base object by default. @@ -422,11 +374,370 @@ suite('Test class NotebookEditor', () => { } else { assert.strictEqual(finder.getDomNode().style.visibility, 'hidden', 'finder node should be hidden when newState.isNotRevealed'); assert.ok(clearDecorationsCalled, 'decorations are cleared when finder isNotRevealed'); + //verify that clearDecorations was called only once. + findDecorationsMock.verify(x => x.clearDecorations(), TypeMoq.Times.once()); } } }); + + for (const currentMatch of [null, new NotebookRange({}, 1, 1, 1, 1)]) { + const searchString = getRandomString(1, 10); // a random string 1 to 10 characters long. + for (const modelFindExpression of [searchString, getRandomString(1, 10)]) { + for (const matchCase of [false, true]) { + for (const wholeWord of [false, true]) { + for (const findMatches of [[], [new NotebookFindMatch(currentMatch || {}, null)]]) { + test(`Verifies _onFindStateChange callback when searchString='${searchString}', currentMatch='${currentMatch}', findExpression='${modelFindExpression}', matchCase='${matchCase}', wholeWord='${wholeWord}', findMatches='${findMatches}'`, async () => { + await verifyFindCallsWhenFindStateChangeCallbackFires(instantiationService, workbenchThemeService, notebookService, untitledNotebookInput, modelFindExpression, currentMatch, searchString, wholeWord, matchCase, findMatches); + }); + } + } + } + } + } + + + for (const searchString of ['', undefined]) { + for (const matchCase of [true, false]) { + for (const wholeWord of [true, false]) { + test(`Verifies _onFindStateChange callback when searchString='${searchString}', matchCase='${matchCase}', wholeWord='${wholeWord}'`, async () => { + await verifyClearDeocorationsAndClearFindCallsWhenFindStateChangeCallbackFires(instantiationService, workbenchThemeService, notebookService, untitledNotebookInput, searchString, wholeWord, matchCase); + }); + } + } + } + + + for (const visibility of ['hidden', 'visible']) { + const searchString = getRandomString(1, 10); + const matchCase = true; + const wholeWord = true; + const searchScope = new NotebookRange({}, 1, 1, 1, 1); + const currentMatch = {}; + test(`Verifies _onFindStateChange callback when searchScope is defined, visibility='${visibility}', searchString='${searchString}', matchCase='${matchCase}', wholeWord='${wholeWord}'`, async () => { + const { findReplaceStateChangedEvent, notebookFindModelMock, findDecorationsMock, notebookFindModel, notebookEditor } = await findStateChangeSetup(instantiationService, workbenchThemeService, notebookService, untitledNotebookInput, undefined, currentMatch, searchString, wholeWord, matchCase, searchScope); + notebookFindModelMock.setup(x => x.getIndexByRange(TypeMoq.It.isAny())).returns((_range: NotebookRange) => { + assert.strictEqual(_range, currentMatch, `getIndexByRange must be called with the same NotebookRange that we set for '_currentMatch' property of notebookEditor`); + return 0; + }); + findReplaceStateChangedEvent.searchString = false; + findReplaceStateChangedEvent.matchCase = false; + findReplaceStateChangedEvent.wholeWord = false; + const findMatches = [new NotebookFindMatch(currentMatch, null)]; + notebookFindModel['_findMatches'] = findMatches; + notebookFindModel['_findArray'] = findMatches.map(x => x?.range); + await notebookEditor['_onFindStateChange'](findReplaceStateChangedEvent); + notebookEditor['_finder'].getDomNode().style.visibility = visibility; + notebookFindModelMock.verify(x => x.find( + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAnyNumber() + ), TypeMoq.Times.once()); + findDecorationsMock.verify(x => x.clearDecorations(), TypeMoq.Times.never()); + notebookFindModelMock.verify(x => x.clearFind(), TypeMoq.Times.never()); + if (visibility === 'visible') { + assert.strictEqual(notebookEditor.getPosition(), currentMatch, `position must be set to the same NotebookRange that we set for '_currentMatch' property of notebookEditor`); + } + }); + } + + + test(`Verifies callbacks registered by registerModelChanges`, async () => { + const searchString = getRandomString(1, 10); + const matchCase = true; + const wholeWord = true; + const searchScope = new NotebookRange({}, 1, 1, 1, 1); + const currentMatch = {}; + const { notebookFindModelMock, notebookEditor } = await findStateChangeSetup(instantiationService, workbenchThemeService, notebookService, untitledNotebookInput, undefined, currentMatch, searchString, wholeWord, matchCase, searchScope); + notebookFindModelMock.setup(x => x.getIndexByRange(TypeMoq.It.isAny())).returns((_range: NotebookRange) => { + assert.strictEqual(_range, currentMatch, `getIndexByRange must be called with the same NotebookRange that we set for '_currentMatch' property of notebookEditor`); + return 0; + }); + untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel + const notebookModel = await notebookEditor.getNotebookModel(); + notebookEditor['registerModelChanges'](); + notebookModel.cells[0]['_onCellModeChanged'].fire(true); //fire cellModeChanged event on the first sell of our test notebookModel + notebookModel.contentChangedEmitter.fire({ changeType: NotebookChangeType.Saved }); + (notebookService)['_onNotebookEditorAdd'].fire({}); + notebookFindModelMock.verify(x => x.find( + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAnyNumber() + ), TypeMoq.Times.exactly(3)); + }); + + test(`Verifies _triggerInputChange raises error and does not throw'`, async () => { + const searchString = getRandomString(1, 10); + const matchCase = true; + const wholeWord = true; + const searchScope = new NotebookRange({}, 1, 1, 1, 1); + const currentMatch = {}; + const { notebookEditor } = await findStateChangeSetup(instantiationService, workbenchThemeService, notebookService, untitledNotebookInput, undefined, currentMatch, searchString, wholeWord, matchCase, searchScope); + untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel + await notebookEditor.setNotebookModel(); + let unexpectedErrorCalled = false; + const unexpectedErrorMessage = 'unexpected Error occurred'; + const onUnexpectedErrorVerifier = (error: any) => { + unexpectedErrorCalled = true; + assert.ok(error instanceof Error, `${onUnexpectedError} must be passed an instance of ${Error}`); + assert.strictEqual((error as Error).message, unexpectedErrorMessage, `Error text must be '${unexpectedErrorMessage}' when exception occurs within _triggerInputChange method`); + }; + errorHandler.setUnexpectedErrorHandler(onUnexpectedErrorVerifier); + notebookEditor['_onFindStateChange'] = async (changeEvent: FindReplaceStateChangedEvent) => { + try { + throw new Error(unexpectedErrorMessage); + } finally { } + }; + notebookEditor['_triggerInputChange'](); + assert.notStrictEqual(unexpectedErrorCalled, true, '_triggerInputChange did not raise an error when an exception occurred Notebook model should be defined after findState.change->notebookEditor._onFindReplaceStateChange call'); + }); + + test(`Verifies _onFindStateChange callback sets notebookModel when it was not previously set'`, async () => { + await setupNotebookEditor(notebookEditor, untitledNotebookInput); + notebookEditor['_notebookModel'] = undefined; + const findState = notebookEditor['_findState']; + const newState: INewFindReplaceState = { + searchString: getRandomString(1, 10), + matchCase: true, + wholeWord: true, + searchScope: {} + }; + findState.change(newState, false); + untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel + const model = await notebookEditor.getNotebookModel(); + assert.notStrictEqual(model, undefined, 'Notebook model should be defined after findState.change->notebookEditor._onFindReplaceStateChange call'); + }); + + test(`Verifies _updateFinderMatchState with no notebookInput or notebookFindModel`, async () => { + await setupNotebookEditor(notebookEditor, untitledNotebookInput); + const findState = notebookEditor['_findState']; + findState['changeMatchInfo'] = (matchPosition: number, matchCount: number, currentMatch: NotebookRange) => { + assert.strictEqual(matchPosition, 0, `matchPosition parameter to changeMatchInfo call when notebookInput or notebookFindModel is not defined should be 0`); + assert.strictEqual(matchCount, 0, `matchCount parameter to changeMatchInfo call when notebookInput or notebookFindModel is not defined should be 0`); + assert.strictEqual(currentMatch, undefined, `currentMatch parameter to changeMatchInfo call when notebookInput or notebookFindModel is not defined should be undefined`); + }; + notebookEditor['_input'] = {}; + notebookEditor['_updateFinderMatchState'](); + notebookEditor['_input'] = undefined; + notebookEditor['_updateFinderMatchState'](); + }); + + test(`Verifies onFindCountChange.fire invokes _updateFinderMatchState`, async () => { + await setupNotebookEditor(notebookEditor, untitledNotebookInput); + untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel + await notebookEditor.setNotebookModel(); + const findState = notebookEditor['_findState']; + const newState: INewFindReplaceState = { + searchString: getRandomString(1, 10), + matchCase: true, + wholeWord: true, + searchScope: {} + }; + findState.change(newState, false); //installs _updateFinderMatchState as event handler for onFindCountChange event + let updateFinderMatchStateCalled = false; + notebookEditor['_updateFinderMatchState'] = () => { + updateFinderMatchStateCalled = true; + }; + notebookEditor.notebookInput.notebookFindModel['_onFindCountChange'].fire(null); + assert.strictEqual(updateFinderMatchStateCalled, true, `_updateFinderMatchState() should have been called`); + }); }); +async function verifyClearDeocorationsAndClearFindCallsWhenFindStateChangeCallbackFires(instantiationService: TestInstantiationService, workbenchThemeService: any, notebookService: NotebookService, untitledNotebookInput: UntitledNotebookInput, searchString: string, wholeWord: boolean, matchCase: boolean) { + const { findReplaceStateChangedEvent, notebookFindModelMock, findDecorationsMock, notebookEditor } = await findStateChangeSetup(instantiationService, workbenchThemeService, notebookService, untitledNotebookInput, undefined, undefined, searchString, wholeWord, matchCase); + await notebookEditor['_onFindStateChange'](findReplaceStateChangedEvent); + notebookFindModelMock.verify(x => x.find( + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAnyNumber() + ), TypeMoq.Times.never()); + if (searchString === '' || findReplaceStateChangedEvent.matchCase || findReplaceStateChangedEvent.wholeWord) { + findDecorationsMock.verify(x => x.clearDecorations(), TypeMoq.Times.once()); + notebookFindModelMock.verify(x => x.clearFind(), TypeMoq.Times.once()); + } + else { + findDecorationsMock.verify(x => x.clearDecorations(), TypeMoq.Times.never()); + notebookFindModelMock.verify(x => x.clearFind(), TypeMoq.Times.never()); + } +} + +async function verifyFindCallsWhenFindStateChangeCallbackFires(instantiationService: TestInstantiationService, workbenchThemeService: any, notebookService: NotebookService, untitledNotebookInput: UntitledNotebookInput, modelFindExpression: string, currentMatch: NotebookRange, searchString: string, wholeWord: boolean, matchCase: boolean, findMatches: NotebookFindMatch[]) { + const { findReplaceStateChangedEvent, notebookFindModelMock, findDecorationsMock, notebookEditor } = await findStateChangeSetup(instantiationService, workbenchThemeService, notebookService, untitledNotebookInput, modelFindExpression, currentMatch, searchString, wholeWord, matchCase, null, findMatches); + await notebookEditor['_onFindStateChange'](findReplaceStateChangedEvent); + if (currentMatch) { + assert.strictEqual(notebookEditor.getPosition(), currentMatch, `position must be set to the same NotebookRange that we set for '_currentMatch' property of notebookEditor`); + } + if (searchString === modelFindExpression && currentMatch && !matchCase && !wholeWord) { + notebookFindModelMock.verify(x => x.find( + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAnyNumber() + ), TypeMoq.Times.never()); + findDecorationsMock.verify(x => x.set(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + } + else { + notebookFindModelMock.verify(x => x.find( + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAnyNumber() + ), TypeMoq.Times.once()); + assert.strictEqual(notebookEditor.notebookFindModel.findExpression, searchString, `findExpression should get set to the provided searchString:${searchString}`); + if (!currentMatch && notebookEditor.notebookFindModel.findMatches.length === 0) { + notebookFindModelMock.verify(x => x.clearFind(), TypeMoq.Times.once()); + findDecorationsMock.verify(x => x.set(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + assert.strictEqual(notebookEditor.notebookFindModel.findArray?.length ?? 0, 0, 'The find array should be cleared or undefined if there were no findMatches'); + } + else { + findDecorationsMock.verify(x => x.set(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + } + } + findDecorationsMock.verify(x => x.clearDecorations(), TypeMoq.Times.once()); +} + +async function findStateChangeSetup(instantiationService: TestInstantiationService, workbenchThemeService: any, notebookService: NotebookService, untitledNotebookInput: UntitledNotebookInput, modelFindExpression: string, currentMatch: NotebookRange, searchString: string, wholeWord: boolean, matchCase: boolean, searchScope: NotebookRange | null = undefined, findMatches: Array = []) { + const findReplaceStateChangedEvent: FindReplaceStateChangedEvent = { + searchString: searchString !== undefined, + matchCase: matchCase, + wholeWord: wholeWord, + searchScope: !!searchScope, + isRevealed: true, + + moveCursor: false, + updateHistory: false, + replaceString: false, + isReplaceRevealed: false, + isRegex: false, + preserveCase: false, + matchesPosition: false, + matchesCount: false, + currentMatch: false, + loop: false + }; + + const notebookEditor = createNotebookEditor(instantiationService, workbenchThemeService, notebookService); + await setupNotebookEditor(notebookEditor, untitledNotebookInput); + untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel + await notebookEditor.setNotebookModel(); + const findState = notebookEditor['_findState']; + findState['_searchString'] = searchString; + findState['_isRevealed'] = findReplaceStateChangedEvent.isRevealed; + findState['_wholeWord'] = findReplaceStateChangedEvent.wholeWord; + findState['_matchCase'] = findReplaceStateChangedEvent.matchCase; + findState['_searchScope'] = searchScope; + const notebookFindModel = notebookEditor.notebookInput.notebookFindModel; + notebookFindModel['_findMatches'] = findMatches; + notebookFindModel['_findArray'] = findMatches.map(x => x?.range); + const notebookFindModelMock = TypeMoq.Mock.ofInstance(notebookFindModel); + notebookFindModelMock.callBase = true; + const spiedNotebookFindModel = notebookFindModelMock.object; + notebookEditor.notebookInput['_notebookFindModel'] = spiedNotebookFindModel; + + spiedNotebookFindModel.findExpression = modelFindExpression; + const findDecorations = notebookFindModel.findDecorations; + findDecorations['_currentMatch'] = currentMatch; + + const findDecorationsMock = TypeMoq.Mock.ofInstance(findDecorations); + const spiedFindDecorations = findDecorationsMock.object; + findDecorationsMock.callBase = true; //forward to base object by default. + notebookFindModel['_findDecorations'] = spiedFindDecorations; + notebookEditor['_currentMatch'] = currentMatch; + + findDecorationsMock.setup(x => x.clearDecorations()); + findDecorationsMock.setup(x => x.set(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_findMatches, _currentMatch) => { + // install a no-op method + }); + notebookFindModelMock.setup(x => x.find( + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAnyNumber() + )).returns((_searchString: string, _matchCase: boolean, _wholeWord: boolean, _maxMatches: number) => { + assert.strictEqual(_searchString, searchString, `find method should be called with the test search string:'${searchString}'`); + assert.strictEqual(_wholeWord, wholeWord, `find method should be called with the test value of wholeWord:'${wholeWord}'`); + assert.strictEqual(_matchCase, matchCase, `find method should be called with the test value of matchCase:'${matchCase}'`); + return Promise.resolve(currentMatch); + }); + notebookFindModelMock.setup(x => x.clearFind()); + return { findReplaceStateChangedEvent, notebookFindModelMock, findDecorationsMock, notebookFindModel, notebookEditor }; +} + +function setupServices(arg: { workbenchThemeService?: WorkbenchThemeService, instantiationService?: TestInstantiationService } = {}) { + const installEvent: Emitter = new Emitter(); + const didInstallEvent = new Emitter(); + const uninstallEvent = new Emitter(); + const didUninstallEvent = new Emitter(); + + const instantiationService = arg.instantiationService ?? workbenchInstantiationService(); + const workbenchThemeService = arg.workbenchThemeService ?? instantiationService.createInstance(WorkbenchThemeService); + instantiationService.stub(IWorkbenchThemeService, workbenchThemeService); + + const queryManagementService = new NBTestQueryManagementService(); + + instantiationService.stub(IExtensionManagementService, ExtensionManagementService); + instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); + instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); + instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); + instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + + const extensionService = instantiationService.get(IExtensionService); + const notebookService = new NotebookService( + instantiationService.get(ILifecycleService), + instantiationService.get(IStorageService), + extensionService, + instantiationService.get(IExtensionManagementService), + instantiationService, + instantiationService.get(IFileService), + instantiationService.get(ILogService), + queryManagementService, + instantiationService.get(IContextKeyService) + ); + + instantiationService.stub(INotebookService, notebookService); + + const testTitle = 'NotebookEditor.Test-Title'; + const untitledUri = URI.from({ scheme: Schemas.untitled, path: 'NotebookEditor.Test-TestPath' }); + const untitledTextEditorService = instantiationService.get(IUntitledTextEditorService); + const untitledTextInput = instantiationService.createInstance(UntitledTextEditorInput, untitledTextEditorService.create({ associatedResource: untitledUri })); + const untitledNotebookInput = new UntitledNotebookInput( + testTitle, untitledUri, untitledTextInput, + undefined, instantiationService, notebookService, extensionService + ); + + const cellTextEditorGuid = generateUuid(); + const queryTextEditor = new QueryTextEditor( + instantiationService.get(ITelemetryService), + instantiationService, + instantiationService.get(IStorageService), + instantiationService.get(ITextResourceConfigurationService), + instantiationService.get(IThemeService), + instantiationService.get(IEditorGroupsService), + instantiationService.get(IEditorService), + instantiationService.get(IConfigurationService) + ); + const notebookEditorStub = new NotebookEditorStub({ cellGuid: cellTextEditorGuid, editor: queryTextEditor, model: new NotebookModelStub() }); + notebookEditorStub.id = untitledNotebookInput.notebookUri.toString(); + notebookService.addNotebookEditor(notebookEditorStub); + return { instantiationService, workbenchThemeService, notebookService, testTitle, extensionService, cellTextEditorGuid, queryTextEditor, untitledNotebookInput, notebookEditorStub }; +} + +function createNotebookEditor(instantiationService: TestInstantiationService, workbenchThemeService: WorkbenchThemeService, notebookService: NotebookService) { + return new NotebookEditor( + instantiationService.get(ITelemetryService), + instantiationService.get(IThemeService), + instantiationService, + instantiationService.get(IStorageService), + new ContextViewService(instantiationService.get(ILayoutService)), + instantiationService.get(IKeybindingService), + instantiationService.get(IContextKeyService), + workbenchThemeService, + notebookService + ); +} + async function setupNotebookEditor(notebookEditor: NotebookEditor, untitledNotebookInput: UntitledNotebookInput): Promise { createEditor(notebookEditor); await setInputDocument(notebookEditor, untitledNotebookInput);