Path: blob/main/src/vs/editor/contrib/find/test/browser/findController.test.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { Delayer } from '../../../../../base/common/async.js';7import * as platform from '../../../../../base/common/platform.js';8import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';9import { ICodeEditor } from '../../../../browser/editorBrowser.js';10import { EditorAction } from '../../../../browser/editorExtensions.js';11import { EditOperation } from '../../../../common/core/editOperation.js';12import { Position } from '../../../../common/core/position.js';13import { Range } from '../../../../common/core/range.js';14import { Selection } from '../../../../common/core/selection.js';15import { CommonFindController, FindStartFocusAction, IFindStartOptions, NextMatchFindAction, NextSelectionMatchFindAction, StartFindAction, StartFindReplaceAction, StartFindWithSelectionAction } from '../../browser/findController.js';16import { CONTEXT_FIND_INPUT_FOCUSED } from '../../browser/findModel.js';17import { withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';18import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';19import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';20import { IHoverService } from '../../../../../platform/hover/browser/hover.js';21import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';22import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';23import { INotificationService } from '../../../../../platform/notification/common/notification.js';24import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';2526class TestFindController extends CommonFindController {2728public hasFocus: boolean;29public delayUpdateHistory: boolean = false;3031private _findInputFocused: IContextKey<boolean>;3233constructor(34editor: ICodeEditor,35@IContextKeyService contextKeyService: IContextKeyService,36@IStorageService storageService: IStorageService,37@IClipboardService clipboardService: IClipboardService,38@INotificationService notificationService: INotificationService,39@IHoverService hoverService: IHoverService40) {41super(editor, contextKeyService, storageService, clipboardService, notificationService, hoverService);42this._findInputFocused = CONTEXT_FIND_INPUT_FOCUSED.bindTo(contextKeyService);43this._updateHistoryDelayer = new Delayer<void>(50);44this.hasFocus = false;45}4647protected override async _start(opts: IFindStartOptions): Promise<void> {48await super._start(opts);4950if (opts.shouldFocus !== FindStartFocusAction.NoFocusChange) {51this.hasFocus = true;52}5354const inputFocused = opts.shouldFocus === FindStartFocusAction.FocusFindInput;55this._findInputFocused.set(inputFocused);56}57}5859function fromSelection(slc: Selection): number[] {60return [slc.startLineNumber, slc.startColumn, slc.endLineNumber, slc.endColumn];61}6263function executeAction(instantiationService: IInstantiationService, editor: ICodeEditor, action: EditorAction, args?: any): Promise<void> {64return instantiationService.invokeFunction((accessor) => {65return Promise.resolve(action.runEditorCommand(accessor, editor, args));66});67}6869suite('FindController', () => {7071ensureNoDisposablesAreLeakedInTestSuite();7273let clipboardState = '';74const serviceCollection = new ServiceCollection();75serviceCollection.set(IStorageService, new InMemoryStorageService());7677if (platform.isMacintosh) {78// eslint-disable-next-line local/code-no-any-casts79serviceCollection.set(IClipboardService, <any>{80readFindText: () => clipboardState,81writeFindText: (value: any) => { clipboardState = value; }82});83}8485/* test('stores to the global clipboard buffer on start find action', async () => {86await withAsyncTestCodeEditor([87'ABC',88'ABC',89'XYZ',90'ABC'91], { serviceCollection: serviceCollection }, async (editor) => {92clipboardState = '';93if (!platform.isMacintosh) {94assert.ok(true);95return;96}97let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);98let startFindAction = new StartFindAction();99// I select ABC on the first line100editor.setSelection(new Selection(1, 1, 1, 4));101// I hit Ctrl+F to show the Find dialog102startFindAction.run(null, editor);103104assert.deepStrictEqual(findController.getGlobalBufferTerm(), findController.getState().searchString);105findController.dispose();106});107});108109test('reads from the global clipboard buffer on next find action if buffer exists', async () => {110await withAsyncTestCodeEditor([111'ABC',112'ABC',113'XYZ',114'ABC'115], { serviceCollection: serviceCollection }, async (editor) => {116clipboardState = 'ABC';117118if (!platform.isMacintosh) {119assert.ok(true);120return;121}122123let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);124let findState = findController.getState();125let nextMatchFindAction = new NextMatchFindAction();126127nextMatchFindAction.run(null, editor);128assert.strictEqual(findState.searchString, 'ABC');129130assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]);131132findController.dispose();133});134});135136test('writes to the global clipboard buffer when text changes', async () => {137await withAsyncTestCodeEditor([138'ABC',139'ABC',140'XYZ',141'ABC'142], { serviceCollection: serviceCollection }, async (editor) => {143clipboardState = '';144if (!platform.isMacintosh) {145assert.ok(true);146return;147}148149let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);150let findState = findController.getState();151152findState.change({ searchString: 'ABC' }, true);153154assert.deepStrictEqual(findController.getGlobalBufferTerm(), 'ABC');155156findController.dispose();157});158}); */159160test('issue #1857: F3, Find Next, acts like "Find Under Cursor"', async () => {161await withAsyncTestCodeEditor([162'ABC',163'ABC',164'XYZ',165'ABC'166], { serviceCollection: serviceCollection }, async (editor, _, instantiationService) => {167clipboardState = '';168// The cursor is at the very top, of the file, at the first ABC169const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);170const findState = findController.getState();171const nextMatchFindAction = NextMatchFindAction;172173// I hit Ctrl+F to show the Find dialog174await executeAction(instantiationService, editor, StartFindAction);175176// I type ABC.177findState.change({ searchString: 'A' }, true);178findState.change({ searchString: 'AB' }, true);179findState.change({ searchString: 'ABC' }, true);180181// The first ABC is highlighted.182assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]);183184// I hit Esc to exit the Find dialog.185findController.closeFindWidget();186findController.hasFocus = false;187188// The cursor is now at end of the first line, with ABC on that line highlighted.189assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]);190191// I hit delete to remove it and change the text to XYZ.192editor.pushUndoStop();193editor.executeEdits('test', [EditOperation.delete(new Range(1, 1, 1, 4))]);194editor.executeEdits('test', [EditOperation.insert(new Position(1, 1), 'XYZ')]);195editor.pushUndoStop();196197// At this point the text editor looks like this:198// XYZ199// ABC200// XYZ201// ABC202assert.strictEqual(editor.getModel()!.getLineContent(1), 'XYZ');203204// The cursor is at end of the first line.205assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 4, 1, 4]);206207// I hit F3 to "Find Next" to find the next occurrence of ABC, but instead it searches for XYZ.208await editor.runAction(nextMatchFindAction);209210assert.strictEqual(findState.searchString, 'ABC');211assert.strictEqual(findController.hasFocus, false);212213findController.dispose();214});215});216217test('issue #3090: F3 does not loop with two matches on a single line', async () => {218await withAsyncTestCodeEditor([219'import nls = require(\'vs/nls\');'220], { serviceCollection: serviceCollection }, async (editor) => {221clipboardState = '';222const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);223const nextMatchFindAction = NextMatchFindAction;224225editor.setPosition({226lineNumber: 1,227column: 9228});229230await editor.runAction(nextMatchFindAction);231assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 26, 1, 29]);232233await editor.runAction(nextMatchFindAction);234assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 8, 1, 11]);235236findController.dispose();237});238});239240test('issue #6149: Auto-escape highlighted text for search and replace regex mode', async () => {241await withAsyncTestCodeEditor([242'var x = (3 * 5)',243'var y = (3 * 5)',244'var z = (3 * 5)',245], { serviceCollection: serviceCollection }, async (editor, _, instantiationService) => {246clipboardState = '';247const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);248const nextMatchFindAction = NextMatchFindAction;249250editor.setSelection(new Selection(1, 9, 1, 13));251252findController.toggleRegex();253await executeAction(instantiationService, editor, StartFindAction);254255await editor.runAction(nextMatchFindAction);256assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 9, 2, 13]);257258await editor.runAction(nextMatchFindAction);259assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 9, 1, 13]);260261findController.dispose();262});263});264265test('issue #41027: Don\'t replace find input value on replace action if find input is active', async () => {266await withAsyncTestCodeEditor([267'test',268], { serviceCollection: serviceCollection }, async (editor, _, instantiationService) => {269const testRegexString = 'tes.';270const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);271const nextMatchFindAction = NextMatchFindAction;272273findController.toggleRegex();274findController.setSearchString(testRegexString);275await findController.start({276forceRevealReplace: false,277seedSearchStringFromSelection: 'none',278seedSearchStringFromNonEmptySelection: false,279seedSearchStringFromGlobalClipboard: false,280shouldFocus: FindStartFocusAction.FocusFindInput,281shouldAnimate: false,282updateSearchScope: false,283loop: true284});285await editor.runAction(nextMatchFindAction);286await executeAction(instantiationService, editor, StartFindReplaceAction);287288assert.strictEqual(findController.getState().searchString, testRegexString);289290findController.dispose();291});292});293294test('issue #9043: Clear search scope when find widget is hidden', async () => {295await withAsyncTestCodeEditor([296'var x = (3 * 5)',297'var y = (3 * 5)',298'var z = (3 * 5)',299], { serviceCollection: serviceCollection }, async (editor) => {300clipboardState = '';301const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);302await findController.start({303forceRevealReplace: false,304seedSearchStringFromSelection: 'none',305seedSearchStringFromNonEmptySelection: false,306seedSearchStringFromGlobalClipboard: false,307shouldFocus: FindStartFocusAction.NoFocusChange,308shouldAnimate: false,309updateSearchScope: false,310loop: true311});312313assert.strictEqual(findController.getState().searchScope, null);314315findController.getState().change({316searchScope: [new Range(1, 1, 1, 5)]317}, false);318319assert.deepStrictEqual(findController.getState().searchScope, [new Range(1, 1, 1, 5)]);320321findController.closeFindWidget();322assert.strictEqual(findController.getState().searchScope, null);323});324});325326test('issue #18111: Regex replace with single space replaces with no space', async () => {327await withAsyncTestCodeEditor([328'HRESULT OnAmbientPropertyChange(DISPID dispid);'329], { serviceCollection: serviceCollection }, async (editor, _, instantiationService) => {330clipboardState = '';331const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);332333await executeAction(instantiationService, editor, StartFindAction);334335findController.getState().change({ searchString: '\\b\\s{3}\\b', replaceString: ' ', isRegex: true }, false);336findController.moveToNextMatch();337338assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [339[1, 39, 1, 42]340]);341342findController.replace();343344assert.deepStrictEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);');345346findController.dispose();347});348});349350test('issue #24714: Regular expression with ^ in search & replace', async () => {351await withAsyncTestCodeEditor([352'',353'line2',354'line3'355], { serviceCollection: serviceCollection }, async (editor, _, instantiationService) => {356clipboardState = '';357const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);358359await executeAction(instantiationService, editor, StartFindAction);360361findController.getState().change({ searchString: '^', replaceString: 'x', isRegex: true }, false);362findController.moveToNextMatch();363364assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [365[2, 1, 2, 1]366]);367368findController.replace();369370assert.deepStrictEqual(editor.getValue(), '\nxline2\nline3');371372findController.dispose();373});374});375376test('issue #38232: Find Next Selection, regex enabled', async () => {377await withAsyncTestCodeEditor([378'([funny]',379'',380'([funny]'381], { serviceCollection: serviceCollection }, async (editor) => {382clipboardState = '';383const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);384const nextSelectionMatchFindAction = new NextSelectionMatchFindAction();385386// toggle regex387findController.getState().change({ isRegex: true }, false);388389// change selection390editor.setSelection(new Selection(1, 1, 1, 9));391392// cmd+f3393await editor.runAction(nextSelectionMatchFindAction);394395assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [396[3, 1, 3, 9]397]);398399findController.dispose();400});401});402403test('issue #38232: Find Next Selection, regex enabled, find widget open', async () => {404await withAsyncTestCodeEditor([405'([funny]',406'',407'([funny]'408], { serviceCollection: serviceCollection }, async (editor, _, instantiationService) => {409clipboardState = '';410const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);411const nextSelectionMatchFindAction = new NextSelectionMatchFindAction();412413// cmd+f - open find widget414await executeAction(instantiationService, editor, StartFindAction);415416// toggle regex417findController.getState().change({ isRegex: true }, false);418419// change selection420editor.setSelection(new Selection(1, 1, 1, 9));421422// cmd+f3423await editor.runAction(nextSelectionMatchFindAction);424425assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [426[3, 1, 3, 9]427]);428429findController.dispose();430});431});432433test('issue #47400, CMD+E supports feeding multiple line of text into the find widget', async () => {434await withAsyncTestCodeEditor([435'ABC',436'ABC',437'XYZ',438'ABC',439'ABC'440], { serviceCollection: serviceCollection }, async (editor, _, instantiationService) => {441clipboardState = '';442const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);443444// change selection445editor.setSelection(new Selection(1, 1, 1, 1));446447// cmd+f - open find widget448await executeAction(instantiationService, editor, StartFindAction);449450editor.setSelection(new Selection(1, 1, 2, 4));451const startFindWithSelectionAction = new StartFindWithSelectionAction();452await editor.runAction(startFindWithSelectionAction);453const findState = findController.getState();454455assert.deepStrictEqual(findState.searchString.split(/\r\n|\r|\n/g), ['ABC', 'ABC']);456457editor.setSelection(new Selection(3, 1, 3, 1));458await editor.runAction(startFindWithSelectionAction);459460findController.dispose();461});462});463464test('issue #109756, CMD+E with empty cursor should always work', async () => {465await withAsyncTestCodeEditor([466'ABC',467'ABC',468'XYZ',469'ABC',470'ABC'471], { serviceCollection: serviceCollection }, async (editor) => {472clipboardState = '';473const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);474editor.setSelection(new Selection(1, 2, 1, 2));475476const startFindWithSelectionAction = new StartFindWithSelectionAction();477editor.runAction(startFindWithSelectionAction);478479const findState = findController.getState();480assert.deepStrictEqual(findState.searchString, 'ABC');481findController.dispose();482});483});484});485486suite('FindController query options persistence', () => {487488ensureNoDisposablesAreLeakedInTestSuite();489490const serviceCollection = new ServiceCollection();491const storageService = new InMemoryStorageService();492storageService.store('editor.isRegex', false, StorageScope.WORKSPACE, StorageTarget.USER);493storageService.store('editor.matchCase', false, StorageScope.WORKSPACE, StorageTarget.USER);494storageService.store('editor.wholeWord', false, StorageScope.WORKSPACE, StorageTarget.USER);495serviceCollection.set(IStorageService, storageService);496497test('matchCase', async () => {498await withAsyncTestCodeEditor([499'abc',500'ABC',501'XYZ',502'ABC'503], { serviceCollection: serviceCollection }, async (editor, _, instantiationService) => {504storageService.store('editor.matchCase', true, StorageScope.WORKSPACE, StorageTarget.USER);505// The cursor is at the very top, of the file, at the first ABC506const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);507const findState = findController.getState();508509// I hit Ctrl+F to show the Find dialog510await executeAction(instantiationService, editor, StartFindAction);511512// I type ABC.513findState.change({ searchString: 'ABC' }, true);514// The second ABC is highlighted as matchCase is true.515assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 4]);516517findController.dispose();518});519});520521storageService.store('editor.matchCase', false, StorageScope.WORKSPACE, StorageTarget.USER);522storageService.store('editor.wholeWord', true, StorageScope.WORKSPACE, StorageTarget.USER);523524test('wholeWord', async () => {525await withAsyncTestCodeEditor([526'ABC',527'AB',528'XYZ',529'ABC'530], { serviceCollection: serviceCollection }, async (editor, _, instantiationService) => {531// The cursor is at the very top, of the file, at the first ABC532const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);533const findState = findController.getState();534535// I hit Ctrl+F to show the Find dialog536await executeAction(instantiationService, editor, StartFindAction);537538// I type AB.539findState.change({ searchString: 'AB' }, true);540// The second AB is highlighted as wholeWord is true.541assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 3]);542543findController.dispose();544});545});546547test('toggling options is saved', async () => {548await withAsyncTestCodeEditor([549'ABC',550'AB',551'XYZ',552'ABC'553], { serviceCollection: serviceCollection }, async (editor) => {554// The cursor is at the very top, of the file, at the first ABC555const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);556findController.toggleRegex();557assert.strictEqual(storageService.getBoolean('editor.isRegex', StorageScope.WORKSPACE), true);558559findController.dispose();560});561});562563test('issue #27083: Update search scope once find widget becomes visible', async () => {564await withAsyncTestCodeEditor([565'var x = (3 * 5)',566'var y = (3 * 5)',567'var z = (3 * 5)',568], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, async (editor) => {569// clipboardState = '';570const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);571const findConfig: IFindStartOptions = {572forceRevealReplace: false,573seedSearchStringFromSelection: 'none',574seedSearchStringFromNonEmptySelection: false,575seedSearchStringFromGlobalClipboard: false,576shouldFocus: FindStartFocusAction.NoFocusChange,577shouldAnimate: false,578updateSearchScope: true,579loop: true580};581582editor.setSelection(new Range(1, 1, 2, 1));583findController.start(findConfig);584assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]);585586findController.closeFindWidget();587588editor.setSelections([new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]);589findController.start(findConfig);590assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]);591});592});593594test('issue #58604: Do not update searchScope if it is empty', async () => {595await withAsyncTestCodeEditor([596'var x = (3 * 5)',597'var y = (3 * 5)',598'var z = (3 * 5)',599], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, async (editor) => {600// clipboardState = '';601editor.setSelection(new Range(1, 2, 1, 2));602const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);603604await findController.start({605forceRevealReplace: false,606seedSearchStringFromSelection: 'none',607seedSearchStringFromNonEmptySelection: false,608seedSearchStringFromGlobalClipboard: false,609shouldFocus: FindStartFocusAction.NoFocusChange,610shouldAnimate: false,611updateSearchScope: true,612loop: true613});614615assert.deepStrictEqual(findController.getState().searchScope, null);616});617});618619test('issue #58604: Update searchScope if it is not empty', async () => {620await withAsyncTestCodeEditor([621'var x = (3 * 5)',622'var y = (3 * 5)',623'var z = (3 * 5)',624], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, async (editor) => {625// clipboardState = '';626editor.setSelection(new Range(1, 2, 1, 3));627const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);628629await findController.start({630forceRevealReplace: false,631seedSearchStringFromSelection: 'none',632seedSearchStringFromNonEmptySelection: false,633seedSearchStringFromGlobalClipboard: false,634shouldFocus: FindStartFocusAction.NoFocusChange,635shouldAnimate: false,636updateSearchScope: true,637loop: true638});639640assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]);641});642});643644645test('issue #27083: Find in selection when multiple lines are selected', async () => {646await withAsyncTestCodeEditor([647'var x = (3 * 5)',648'var y = (3 * 5)',649'var z = (3 * 5)',650], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'multiline', globalFindClipboard: false } }, async (editor) => {651// clipboardState = '';652editor.setSelection(new Range(1, 6, 2, 1));653const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);654655await findController.start({656forceRevealReplace: false,657seedSearchStringFromSelection: 'none',658seedSearchStringFromNonEmptySelection: false,659seedSearchStringFromGlobalClipboard: false,660shouldFocus: FindStartFocusAction.NoFocusChange,661shouldAnimate: false,662updateSearchScope: true,663loop: true664});665666assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]);667});668});669});670671672