From b87291e59a2f1ae5cdb4e163e30a4de0878c0c8a Mon Sep 17 00:00:00 2001 From: aidaodedjl Date: Sun, 16 Aug 2026 03:13:41 +0800 Subject: [PATCH 1/2] fix: update find searchScope when the selection changes The searchScope change detection in FindReplaceState.change() had inverted comparison logic since the multi-selection refactor (b9efdab): a new scope was treated as unchanged whenever any existing scope range differed from it, so moving or resizing the selection kept the stale scope and the match count never updated. Compare scopes with a proper set-equality check instead. Fixes #237774 --- .../editor/contrib/find/browser/findState.ts | 16 ++++++--- .../find/test/browser/findController.test.ts | 35 +++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/contrib/find/browser/findState.ts b/src/vs/editor/contrib/find/browser/findState.ts index f48d60c1763ed1..553e10acf2d07b 100644 --- a/src/vs/editor/contrib/find/browser/findState.ts +++ b/src/vs/editor/contrib/find/browser/findState.ts @@ -8,6 +8,16 @@ 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; + } + return a.every(rangeA => b.some(rangeB => Range.equalsRange(rangeA, rangeB))); +} + export interface FindReplaceStateChangedEvent { moveCursor: boolean; updateHistory: boolean; @@ -252,11 +262,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..a555361a2f3b61 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,41 @@ suite('FindController query options persistence', () => { }); }); + test('issue #237774: Update searchScope when the selection 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)]); + + // 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)] }, 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)', From b88d23108148dd82f5395b466b04dcc12ec9578f Mon Sep 17 00:00:00 2001 From: aidaodedjl Date: Sun, 16 Aug 2026 03:21:18 +0800 Subject: [PATCH 2/2] fix: handle duplicate ranges in searchScopesEqual Track matched indices so a multiset comparison is performed: [A, A] must not be considered equal to [A, B]. Also add a regression assertion for duplicate ranges and rename the test to reflect that it exercises scope changes directly. --- src/vs/editor/contrib/find/browser/findState.ts | 10 +++++++++- .../contrib/find/test/browser/findController.test.ts | 8 ++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/find/browser/findState.ts b/src/vs/editor/contrib/find/browser/findState.ts index 553e10acf2d07b..45a8ad377fad01 100644 --- a/src/vs/editor/contrib/find/browser/findState.ts +++ b/src/vs/editor/contrib/find/browser/findState.ts @@ -15,7 +15,15 @@ function searchScopesEqual(a: Range[] | null, b: Range[] | null): boolean { if (!a || !b || a.length !== b.length) { return false; } - return a.every(rangeA => b.some(rangeB => Range.equalsRange(rangeA, rangeB))); + 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 { 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 a555361a2f3b61..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,7 +678,7 @@ suite('FindController query options persistence', () => { }); }); - test('issue #237774: Update searchScope when the selection changes', async () => { + test('issue #237774: Update searchScope when the scope changes', async () => { await withAsyncTestCodeEditor([ 'var x = (3 * 5)', 'var y = (3 * 5)', @@ -700,10 +700,14 @@ suite('FindController query options persistence', () => { 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)] }, false); + findState.change({ searchScope: [new Selection(1, 1, 2, 1), new Selection(1, 1, 2, 1)] }, false); listener.dispose(); assert.strictEqual(changeEventFired, false);