Skip to content

Commit 6d6bb58

Browse files
authored
Fix partial result transfer in quick search (microsoft#201917)
1 parent 959eac3 commit 6d6bb58

File tree

4 files changed

+203
-153
lines changed

4 files changed

+203
-153
lines changed

src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
5-
import { CancellationToken } from 'vs/base/common/cancellation';
5+
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
66
import { IMatch } from 'vs/base/common/filters';
77
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
88
import { ResourceSet } from 'vs/base/common/map';
@@ -22,12 +22,12 @@ import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspac
2222
import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
2323
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
2424
import { searchDetailsIcon, searchOpenInFileIcon, searchActivityBarIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
25-
import { FileMatch, Match, RenderableMatch, SearchModel, searchComparer } from 'vs/workbench/contrib/search/browser/searchModel';
25+
import { FileMatch, Match, RenderableMatch, SearchModel, SearchModelLocation, searchComparer } from 'vs/workbench/contrib/search/browser/searchModel';
2626
import { SearchView, getEditorSelectionFromMatch } from 'vs/workbench/contrib/search/browser/searchView';
2727
import { IWorkbenchSearchConfiguration, getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search';
2828
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
2929
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder';
30-
import { IPatternInfo, ITextQuery, VIEW_ID } from 'vs/workbench/services/search/common/search';
30+
import { IPatternInfo, ISearchComplete, ITextQuery, VIEW_ID } from 'vs/workbench/services/search/common/search';
3131

3232
export const TEXT_SEARCH_QUICK_ACCESS_PREFIX = '%';
3333

@@ -45,6 +45,10 @@ const MAX_RESULTS_PER_FILE = 10;
4545
export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuickAccessItem> {
4646
private queryBuilder: QueryBuilder;
4747
private searchModel: SearchModel;
48+
private currentAsyncSearch: Promise<ISearchComplete> = Promise.resolve({
49+
results: [],
50+
messages: []
51+
});
4852

4953
private _getTextQueryBuilderOptions(charsPerLine: number): ITextQueryBuilderOptions {
5054
return {
@@ -74,6 +78,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
7478

7579
this.queryBuilder = this._instantiationService.createInstance(QueryBuilder);
7680
this.searchModel = this._instantiationService.createInstance(SearchModel);
81+
this.searchModel.location = SearchModelLocation.QUICK_ACCESS;
7782
}
7883

7984
override dispose(): void {
@@ -89,7 +94,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
8994
picker.customButton = true;
9095
picker.customLabel = '$(link-external)';
9196
picker.onDidCustom(() => {
92-
this.moveToSearchViewlet(this.searchModel, undefined);
97+
this.moveToSearchViewlet(undefined);
9398
picker.hide();
9499
});
95100
disposables.add(super.provide(picker, token, runOptions));
@@ -137,6 +142,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
137142
const result = this.searchModel.search(query, undefined, token);
138143

139144
const getAsyncResults = async () => {
145+
this.currentAsyncSearch = result.asyncResults;
140146
await result.asyncResults;
141147
const syncResultURIs = new ResourceSet(result.syncResults.map(e => e.resource));
142148
return this.searchModel.searchResult.matches().filter(e => !syncResultURIs.has(e.resource));
@@ -147,12 +153,15 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
147153
};
148154
}
149155

150-
private moveToSearchViewlet(model: SearchModel, currentElem: RenderableMatch | undefined) {
151-
// this function takes this._searchModel.searchResult and moves it to the search viewlet's search model.
152-
// then, this._searchModel will construct a new (empty) SearchResult, and the search viewlet's search result will be disposed.
156+
private moveToSearchViewlet(currentElem: RenderableMatch | undefined) {
157+
// this function takes this._searchModel and moves it to the search viewlet's search model.
158+
// then, this._searchModel will construct a new (empty) SearchModel.
153159
this._viewsService.openView(VIEW_ID, false);
154160
const viewlet: SearchView | undefined = this._viewsService.getActiveViewWithId(VIEW_ID) as SearchView;
155-
viewlet.importSearchResult(model);
161+
viewlet.replaceSearchModel(this.searchModel, this.currentAsyncSearch);
162+
163+
this.searchModel = this._instantiationService.createInstance(SearchModel);
164+
this.searchModel.location = SearchModelLocation.QUICK_ACCESS;
156165

157166
const viewer: WorkbenchCompressibleObjectTree<RenderableMatch> | undefined = viewlet?.getControl();
158167
if (currentElem) {
@@ -181,7 +190,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
181190
label: localize('QuickSearchSeeMoreFiles', "See More Files"),
182191
iconClass: ThemeIcon.asClassName(searchDetailsIcon),
183192
accept: async () => {
184-
this.moveToSearchViewlet(this.searchModel, matches[limit]);
193+
this.moveToSearchViewlet(matches[limit]);
185194
}
186195
});
187196
break;
@@ -212,7 +221,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
212221
label: localize('QuickSearchMore', "More"),
213222
iconClass: ThemeIcon.asClassName(searchDetailsIcon),
214223
accept: async () => {
215-
this.moveToSearchViewlet(this.searchModel, element);
224+
this.moveToSearchViewlet(element);
216225
}
217226
});
218227
break;
@@ -243,7 +252,7 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
243252
});
244253
},
245254
trigger: (): TriggerAction => {
246-
this.moveToSearchViewlet(this.searchModel, element);
255+
this.moveToSearchViewlet(element);
247256
return TriggerAction.CLOSE_PICKER;
248257
}
249258
});
@@ -270,11 +279,22 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
270279

271280
protected _getPicks(contentPattern: string, disposables: DisposableStore, token: CancellationToken): Picks<IQuickPickItem> | Promise<Picks<IQuickPickItem> | FastAndSlowPicks<IQuickPickItem>> | FastAndSlowPicks<IQuickPickItem> | null {
272281

282+
const conditionalTokenCts = disposables.add(new CancellationTokenSource());
283+
284+
const searchModelAtTimeOfSearch = this.searchModel;
285+
286+
disposables.add(token.onCancellationRequested(() => {
287+
if (searchModelAtTimeOfSearch.location === SearchModelLocation.QUICK_ACCESS) {
288+
// if the search model has not been imported to the panel, you can cancel
289+
conditionalTokenCts.cancel();
290+
}
291+
}));
292+
273293
if (contentPattern === '') {
274294
this.searchModel.searchResult.clear();
275295
return [];
276296
}
277-
const allMatches = this.doSearch(contentPattern, token);
297+
const allMatches = this.doSearch(contentPattern, conditionalTokenCts.token);
278298

279299
if (!allMatches) {
280300
return null;

src/vs/workbench/contrib/search/browser/searchModel.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,7 @@ function createParentList(element: RenderableMatch): RenderableMatch[] {
15801580

15811581
return parentArray;
15821582
}
1583+
15831584
export class SearchResult extends Disposable {
15841585

15851586
private _onChange = this._register(new PauseableEmitter<IChangeEvent>({
@@ -1598,7 +1599,7 @@ export class SearchResult extends Disposable {
15981599
private _onDidChangeModelListener: IDisposable | undefined;
15991600

16001601
constructor(
1601-
public searchModel: SearchModel,
1602+
public readonly searchModel: SearchModel,
16021603
@IReplaceService private readonly replaceService: IReplaceService,
16031604
@IInstantiationService private readonly instantiationService: IInstantiationService,
16041605
@IModelService private readonly modelService: IModelService,
@@ -1607,7 +1608,6 @@ export class SearchResult extends Disposable {
16071608
) {
16081609
super();
16091610
this._rangeHighlightDecorations = this.instantiationService.createInstance(RangeHighlightDecorations);
1610-
16111611
this.modelService.getModels().forEach(model => this.onModelAdded(model));
16121612
this._register(this.modelService.onModelAdded(model => this.onModelAdded(model)));
16131613

@@ -1936,6 +1936,11 @@ export class SearchResult extends Disposable {
19361936
}
19371937
}
19381938

1939+
export enum SearchModelLocation {
1940+
PANEL,
1941+
QUICK_ACCESS
1942+
}
1943+
19391944
export class SearchModel extends Disposable {
19401945

19411946
private _searchResult: SearchResult;
@@ -1957,7 +1962,7 @@ export class SearchModel extends Disposable {
19571962

19581963
private currentCancelTokenSource: CancellationTokenSource | null = null;
19591964
private searchCancelledForNewSearch: boolean = false;
1960-
private _searchResultChangedListener: IDisposable;
1965+
public location: SearchModelLocation = SearchModelLocation.PANEL;
19611966

19621967
constructor(
19631968
@ISearchService private readonly searchService: ISearchService,
@@ -1969,7 +1974,7 @@ export class SearchModel extends Disposable {
19691974
) {
19701975
super();
19711976
this._searchResult = this.instantiationService.createInstance(SearchResult, this);
1972-
this._searchResultChangedListener = this._register(this._searchResult.onChange((e) => this._onSearchResultChanged.fire(e)));
1977+
this._register(this._searchResult.onChange((e) => this._onSearchResultChanged.fire(e)));
19731978
}
19741979

19751980
isReplaceActive(): boolean {
@@ -2008,15 +2013,6 @@ export class SearchModel extends Disposable {
20082013
return this._searchResult;
20092014
}
20102015

2011-
set searchResult(searchResult: SearchResult) {
2012-
this._searchResult.dispose();
2013-
this._searchResultChangedListener.dispose();
2014-
2015-
this._searchResult = searchResult;
2016-
this._searchResult.searchModel = this;
2017-
this._searchResultChangedListener = this._register(this._searchResult.onChange((e) => this._onSearchResultChanged.fire(e)));
2018-
}
2019-
20202016

20212017
private doSearch(query: ITextQuery, progressEmitter: Emitter<void>, searchQuery: ITextQuery, searchInstanceID: string, onProgress?: (result: ISearchProgressItem) => void, callerToken?: CancellationToken): {
20222018
asyncResults: Promise<ISearchComplete>;
@@ -2145,7 +2141,7 @@ export class SearchModel extends Disposable {
21452141
}
21462142
}
21472143

2148-
private onSearchCompleted(completed: ISearchComplete | null, duration: number, searchInstanceID: string): ISearchComplete | null {
2144+
private onSearchCompleted(completed: ISearchComplete | undefined, duration: number, searchInstanceID: string): ISearchComplete | undefined {
21492145
if (!this._searchQuery) {
21502146
throw new Error('onSearchCompleted must be called after a search is started');
21512147
}
@@ -2193,7 +2189,7 @@ export class SearchModel extends Disposable {
21932189
this.onSearchCompleted(
21942190
this.searchCancelledForNewSearch
21952191
? { exit: SearchCompletionExitCode.NewSearchStarted, results: [], messages: [] }
2196-
: null,
2192+
: undefined,
21972193
duration, '');
21982194
this.searchCancelledForNewSearch = false;
21992195
}
@@ -2238,10 +2234,6 @@ export class SearchModel extends Disposable {
22382234
super.dispose();
22392235
}
22402236

2241-
transferSearchResult(other: SearchModel): void {
2242-
other.searchResult = this._searchResult;
2243-
this._searchResult = this.instantiationService.createInstance(SearchResult, this);
2244-
}
22452237
}
22462238

22472239
export type FileMatchOrMatch = FileMatch | Match;
@@ -2262,14 +2254,19 @@ export class SearchViewModelWorkbenchService implements ISearchViewModelWorkbenc
22622254
}
22632255
return this._searchModel;
22642256
}
2257+
2258+
set searchModel(searchModel: SearchModel) {
2259+
this._searchModel?.dispose();
2260+
this._searchModel = searchModel;
2261+
}
22652262
}
22662263

22672264
export const ISearchViewModelWorkbenchService = createDecorator<ISearchViewModelWorkbenchService>('searchViewModelWorkbenchService');
22682265

22692266
export interface ISearchViewModelWorkbenchService {
22702267
readonly _serviceBrand: undefined;
22712268

2272-
readonly searchModel: SearchModel;
2269+
searchModel: SearchModel;
22732270
}
22742271

22752272
/**

src/vs/workbench/contrib/search/browser/searchResultsView.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,6 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender
294294
readonly templateId = MatchRenderer.TEMPLATE_ID;
295295

296296
constructor(
297-
private searchModel: SearchModel,
298297
private searchView: SearchView,
299298
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
300299
@IConfigurationService private readonly configurationService: IConfigurationService,
@@ -352,8 +351,8 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender
352351
renderElement(node: ITreeNode<Match, any>, index: number, templateData: IMatchTemplate): void {
353352
const match = node.element;
354353
const preview = match.preview();
355-
const replace = this.searchModel.isReplaceActive() &&
356-
!!this.searchModel.replaceString &&
354+
const replace = this.searchView.model.isReplaceActive() &&
355+
!!this.searchView.model.replaceString &&
357356
!(match instanceof MatchInNotebook && match.isReadonly());
358357

359358
templateData.before.textContent = preview.before;
@@ -402,7 +401,7 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender
402401
export class SearchAccessibilityProvider implements IListAccessibilityProvider<RenderableMatch> {
403402

404403
constructor(
405-
private searchModel: SearchModel,
404+
private searchView: SearchView,
406405
@ILabelService private readonly labelService: ILabelService
407406
) {
408407
}
@@ -427,7 +426,7 @@ export class SearchAccessibilityProvider implements IListAccessibilityProvider<R
427426

428427
if (element instanceof Match) {
429428
const match = <Match>element;
430-
const searchModel: SearchModel = this.searchModel;
429+
const searchModel: SearchModel = this.searchView.model;
431430
const replace = searchModel.isReplaceActive() && !!searchModel.replaceString;
432431
const matchString = match.getMatchString();
433432
const range = match.range();

0 commit comments

Comments
 (0)