diff --git a/src/vs/editor/contrib/find/browser/findState.ts b/src/vs/editor/contrib/find/browser/findState.ts index f48d60c1763ed1..45a8ad377fad01 100644 --- a/src/vs/editor/contrib/find/browser/findState.ts +++ b/src/vs/editor/contrib/find/browser/findState.ts @@ -8,6 +8,24 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { Range } from '../../../common/core/range.js'; import { MATCHES_LIMIT } from './findModel.js'; +function searchScopesEqual(a: Range[] | null, b: Range[] | null): boolean { + if (a === b) { + return true; + } + if (!a || !b || a.length !== b.length) { + return false; + } + const matched = new Array(b.length).fill(false); + return a.every(rangeA => { + const index = b.findIndex((rangeB, i) => !matched[i] && Range.equalsRange(rangeA, rangeB)); + if (index === -1) { + return false; + } + matched[index] = true; + return true; + }); +} + export interface FindReplaceStateChangedEvent { moveCursor: boolean; updateHistory: boolean; @@ -252,11 +270,7 @@ export class FindReplaceState void } = { updat this._preserveCase = newState.preserveCase; } if (typeof newState.searchScope !== 'undefined') { - if (!newState.searchScope?.every((newSearchScope) => { - return this._searchScope?.some(existingSearchScope => { - return !Range.equalsRange(existingSearchScope, newSearchScope); - }); - })) { + if (!searchScopesEqual(newState.searchScope, this._searchScope)) { this._searchScope = newState.searchScope; changeEvent.searchScope = true; somethingChanged = true; diff --git a/src/vs/editor/contrib/find/test/browser/findController.test.ts b/src/vs/editor/contrib/find/test/browser/findController.test.ts index 1823dd31e1ab99..fdc96e8fdcee6b 100644 --- a/src/vs/editor/contrib/find/test/browser/findController.test.ts +++ b/src/vs/editor/contrib/find/test/browser/findController.test.ts @@ -678,6 +678,45 @@ suite('FindController query options persistence', () => { }); }); + test('issue #237774: Update searchScope when the scope changes', async () => { + await withAsyncTestCodeEditor([ + 'var x = (3 * 5)', + 'var y = (3 * 5)', + 'var z = (3 * 5)', + ], { serviceCollection: serviceCollection }, async (editor) => { + const findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); + const findState = findController.getState(); + + findState.change({ searchScope: [new Selection(1, 1, 2, 1)] }, false); + assert.deepStrictEqual(findState.searchScope, [new Selection(1, 1, 2, 1)]); + + // Move the single selection: the scope must follow the new selection + findState.change({ searchScope: [new Selection(2, 1, 3, 1)] }, false); + assert.deepStrictEqual(findState.searchScope, [new Selection(2, 1, 3, 1)]); + + // Shrink from two selections to one: the scope must shrink too + findState.change({ searchScope: [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)] }, false); + assert.deepStrictEqual(findState.searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); + findState.change({ searchScope: [new Selection(1, 1, 2, 1)] }, false); + assert.deepStrictEqual(findState.searchScope, [new Selection(1, 1, 2, 1)]); + + // Duplicate ranges must not be treated as equal to distinct ranges + findState.change({ searchScope: [new Selection(1, 1, 2, 1), new Selection(1, 1, 2, 1)] }, false); + assert.deepStrictEqual(findState.searchScope, [new Selection(1, 1, 2, 1), new Selection(1, 1, 2, 1)]); + + // Identical scope must not fire a change event + let changeEventFired = false; + const listener = findState.onFindReplaceStateChange(() => changeEventFired = true); + findState.change({ searchScope: [new Selection(1, 1, 2, 1), new Selection(1, 1, 2, 1)] }, false); + listener.dispose(); + assert.strictEqual(changeEventFired, false); + + // Clearing the scope must be honored + findState.change({ searchScope: null }, false); + assert.strictEqual(findState.searchScope, null); + }); + }); + test('issue #58604: Update searchScope if it is not empty', async () => { await withAsyncTestCodeEditor([ 'var x = (3 * 5)',