Skip to content

Commit dd2ff08

Browse files
authored
Merge pull request microsoft#250016 from microsoft/osortega/support-async-keyword-suggestions
Support for async keyword suggestions
2 parents 73944df + d473e46 commit dd2ff08

File tree

5 files changed

+64
-47
lines changed

5 files changed

+64
-47
lines changed

src/vs/workbench/api/browser/mainThreadSearch.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class SearchOperation {
9393
private static _idPool = 0;
9494

9595
constructor(
96-
readonly progress?: (match: IFileMatch) => any,
96+
readonly progress?: (match: IFileMatch | AISearchKeyword) => any,
9797
readonly id: number = ++SearchOperation._idPool,
9898
readonly matches = new Map<string, IFileMatch>(),
9999
readonly keywords: AISearchKeyword[] = []
@@ -119,6 +119,7 @@ class SearchOperation {
119119

120120
addKeyword(result: AISearchKeyword): void {
121121
this.keywords.push(result);
122+
this.progress?.(result);
122123
}
123124
}
124125

@@ -209,7 +210,7 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable {
209210
searchOp.addKeyword(data);
210211
}
211212

212-
private _provideSearchResults(query: ISearchQuery, session: number, token: CancellationToken, onKeywordResult?: (keyword: AISearchKeyword) => void): Promise<ISearchCompleteStats> {
213+
private _provideSearchResults(query: ISearchQuery, session: number, token: CancellationToken): Promise<ISearchCompleteStats> {
213214
switch (query.type) {
214215
case QueryType.File:
215216
return this._proxy.$provideFileSearchResults(this._handle, session, query, token);

src/vs/workbench/contrib/search/browser/searchTreeModel/searchTreeCommon.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export interface ISearchModel {
9797
replaceString: string;
9898
preserveCase: boolean;
9999
searchResult: ISearchResult;
100-
aiSearch(onResultReported: () => void): Promise<ISearchComplete>;
100+
aiSearch(onResultReported: (result: ISearchProgressItem) => void): Promise<ISearchComplete>;
101101
hasAIResults: boolean;
102102
hasPlainResults: boolean;
103103
search(query: ITextQuery, onProgress?: (result: ISearchProgressItem) => void, callerToken?: CancellationToken): {

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

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import { createEditorFromSearchResult } from '../../searchEditor/browser/searchE
7070
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
7171
import { IPreferencesService, ISettingsEditorOptions } from '../../../services/preferences/common/preferences.js';
7272
import { ITextQueryBuilderOptions, QueryBuilder } from '../../../services/search/common/queryBuilder.js';
73-
import { SemanticSearchBehavior, IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchService, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from '../../../services/search/common/search.js';
73+
import { SemanticSearchBehavior, IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchService, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode, isAIKeyword } from '../../../services/search/common/search.js';
7474
import { AISearchKeyword, TextSearchCompleteMessage } from '../../../services/search/common/searchExtTypes.js';
7575
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
7676
import { INotebookService } from '../../notebook/common/notebookService.js';
@@ -174,6 +174,7 @@ export class SearchView extends ViewPane {
174174
private refreshTreeController: RefreshTreeController;
175175

176176
private _cachedResults: ISearchComplete | undefined;
177+
private _cachedKeywords: string[] = [];
177178
public _pendingSemanticSearchPromise: Promise<ISearchComplete> | undefined;
178179
constructor(
179180
options: IViewPaneOptions,
@@ -1715,9 +1716,6 @@ export class SearchView extends ViewPane {
17151716
return;
17161717
}
17171718

1718-
if (this.configurationService.getValue<ISearchConfigurationProperties>('search').searchView.keywordSuggestions) {
1719-
this.updateKeywordSuggestion(keywords);
1720-
}
17211719

17221720
if (this.shouldShowAIResults() && !allResults) {
17231721
const messageEl = this.clearMessage();
@@ -1847,6 +1845,7 @@ export class SearchView extends ViewPane {
18471845
this.model.searchResult.aiTextSearchResult.hidden = true;
18481846
if (!this._pendingSemanticSearchPromise) {
18491847
this._cachedResults = undefined;
1848+
this._cachedKeywords = [];
18501849
this.model.cancelAISearch(true);
18511850
this.model.clearAiSearchResults();
18521851
}
@@ -1927,6 +1926,10 @@ export class SearchView extends ViewPane {
19271926
this.viewModel.searchResult.setAIQueryUsingTextQuery(query);
19281927
}
19291928

1929+
if (this.configurationService.getValue<ISearchConfigurationProperties>('search').searchView.keywordSuggestions) {
1930+
this.getKeywordSuggestions();
1931+
}
1932+
19301933
return result.asyncResults.then((complete) => {
19311934
clearTimeout(slowTimer);
19321935
const config = this.configurationService.getValue<ISearchConfigurationProperties>('search').searchView.semanticSearchBehavior;
@@ -1976,6 +1979,9 @@ export class SearchView extends ViewPane {
19761979
}
19771980

19781981
private updateSearchResultCount(disregardExcludesAndIgnores?: boolean, onlyOpenEditors?: boolean, clear: boolean = false): void {
1982+
if (this._cachedKeywords.length > 0) {
1983+
return;
1984+
}
19791985
const fileCount = this.viewModel.searchResult.fileCount(this.viewModel.searchResult.aiTextSearchResult.hidden);
19801986
const resultCount = this.viewModel.searchResult.count(this.viewModel.searchResult.aiTextSearchResult.hidden);
19811987
this.hasSearchResultsKey.set(fileCount > 0);
@@ -2017,7 +2023,7 @@ export class SearchView extends ViewPane {
20172023
}
20182024
}
20192025

2020-
private handleKeywordClick(keyword: string, index: number, maxKeywords: number) {
2026+
private handleKeywordClick(keyword: string, index: number) {
20212027
this.searchWidget.searchInput?.setValue(keyword);
20222028
this.triggerQueryChange({ preserveFocus: false, triggeredOnType: false, shouldKeepAIResults: false });
20232029
type KeywordClickClassification = {
@@ -2032,54 +2038,60 @@ export class SearchView extends ViewPane {
20322038
};
20332039
this.telemetryService.publicLog2<KeywordClickEvent, KeywordClickClassification>('searchKeywordClick', {
20342040
index,
2035-
maxKeywords
2041+
maxKeywords: this._cachedKeywords.length
20362042
});
20372043
}
20382044

2039-
private async updateKeywordSuggestion(keywords?: AISearchKeyword[]) {
2040-
if (!keywords || keywords.length === 0) {
2041-
this.viewModel.replaceString = this.searchWidget.getReplaceValue();
2042-
// Reuse pending aiSearch if available
2043-
let aiSearchPromise = this._pendingSemanticSearchPromise;
2044-
if (!aiSearchPromise) {
2045-
this.viewModel.searchResult.setAIQueryUsingTextQuery();
2046-
aiSearchPromise = this._pendingSemanticSearchPromise = this.viewModel.aiSearch(() => {
2047-
// Clear pending promise when first result comes in
2048-
if (this._pendingSemanticSearchPromise === aiSearchPromise) {
2049-
this._pendingSemanticSearchPromise = undefined;
2050-
}
2051-
});
2052-
}
2053-
this._cachedResults = await aiSearchPromise;
2054-
keywords = this._cachedResults.aiKeywords;
2055-
if (!keywords || keywords.length === 0) {
2045+
private updateKeywordSuggestionUI(keyword: AISearchKeyword) {
2046+
const element = this.messagesElement.firstChild as HTMLDivElement;
2047+
if (this._cachedKeywords.length > 0) {
2048+
if (this._cachedKeywords.length >= 3) {
2049+
// If we already have 3 keywords, just return
20562050
return;
20572051
}
2058-
}
2059-
const messageEl = this.clearMessage();
2060-
messageEl.classList.add('ai-keywords');
2061-
2062-
if (keywords.length === 0) {
2063-
// Do not display anything if there are no keywords
2064-
return;
2065-
}
2052+
dom.append(element, ', ');
2053+
const index = this._cachedKeywords.length;
2054+
const button = this.messageDisposables.add(new SearchLinkButton(
2055+
keyword.keyword,
2056+
() => this.handleKeywordClick(keyword.keyword, index),
2057+
this.hoverService
2058+
));
2059+
dom.append(element, button.element);
2060+
} else {
2061+
const messageEl = this.clearMessage();
2062+
messageEl.classList.add('ai-keywords');
20662063

2067-
// Add unclickable message
2068-
const resultMsg = nls.localize('keywordSuggestion.message', "Search instead for: ");
2069-
dom.append(messageEl, resultMsg);
2064+
// Add unclickable message
2065+
const resultMsg = nls.localize('keywordSuggestion.message', "Search instead for: ");
2066+
dom.append(messageEl, resultMsg);
20702067

2071-
const topKeywords = keywords.slice(0, 3);
2072-
topKeywords.forEach((keyword, index) => {
2073-
if (index > 0 && index < topKeywords.length) {
2074-
dom.append(messageEl, ', ');
2075-
}
20762068
const button = this.messageDisposables.add(new SearchLinkButton(
20772069
keyword.keyword,
2078-
() => this.handleKeywordClick(keyword.keyword, index, topKeywords.length),
2070+
() => this.handleKeywordClick(keyword.keyword, 0),
20792071
this.hoverService
20802072
));
20812073
dom.append(messageEl, button.element);
2082-
});
2074+
}
2075+
this._cachedKeywords.push(keyword.keyword);
2076+
}
2077+
2078+
private async getKeywordSuggestions() {
2079+
// Reuse pending aiSearch if available
2080+
let aiSearchPromise = this._pendingSemanticSearchPromise;
2081+
if (!aiSearchPromise) {
2082+
this.viewModel.searchResult.setAIQueryUsingTextQuery();
2083+
aiSearchPromise = this._pendingSemanticSearchPromise = this.viewModel.aiSearch(result => {
2084+
if (isAIKeyword(result)) {
2085+
this.updateKeywordSuggestionUI(result);
2086+
return;
2087+
}
2088+
// Clear pending promise when first result comes in
2089+
if (this._pendingSemanticSearchPromise === aiSearchPromise) {
2090+
this._pendingSemanticSearchPromise = undefined;
2091+
}
2092+
});
2093+
}
2094+
this._cachedResults = await aiSearchPromise;
20832095
}
20842096

20852097
private addMessage(message: TextSearchCompleteMessage) {

src/vs/workbench/services/search/common/search.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,16 @@ export interface IProgressMessage {
238238
message: string;
239239
}
240240

241-
export type ISearchProgressItem = IFileMatch | IProgressMessage;
241+
export type ISearchProgressItem = IFileMatch | IProgressMessage | AISearchKeyword;
242242

243243
export function isFileMatch(p: ISearchProgressItem): p is IFileMatch {
244244
return !!(<IFileMatch>p).resource;
245245
}
246246

247+
export function isAIKeyword(p: ISearchProgressItem): p is AISearchKeyword {
248+
return !!(<AISearchKeyword>p).keyword;
249+
}
250+
247251
export function isProgressMessage(p: ISearchProgressItem | ISerializedSearchProgressItem): p is IProgressMessage {
248252
return !!(p as IProgressMessage).message;
249253
}

src/vs/workbench/services/search/common/searchService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri
2222
import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js';
2323
import { IEditorService } from '../../editor/common/editorService.js';
2424
import { IExtensionService } from '../../extensions/common/extensions.js';
25-
import { DEFAULT_MAX_SEARCH_RESULTS, deserializeSearchError, FileMatch, IAITextQuery, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, isFileMatch, isProgressMessage, ITextQuery, pathIncludedInQuery, QueryType, SEARCH_RESULT_LANGUAGE_ID, SearchError, SearchErrorCode, SearchProviderType } from './search.js';
25+
import { DEFAULT_MAX_SEARCH_RESULTS, deserializeSearchError, FileMatch, IAITextQuery, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, isAIKeyword, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, isFileMatch, isProgressMessage, ITextQuery, pathIncludedInQuery, QueryType, SEARCH_RESULT_LANGUAGE_ID, SearchError, SearchErrorCode, SearchProviderType } from './search.js';
2626
import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from './searchHelpers.js';
2727

2828
export class SearchService extends Disposable implements ISearchService {
@@ -94,7 +94,7 @@ export class SearchService extends Disposable implements ISearchService {
9494
const onProviderProgress = (progress: ISearchProgressItem) => {
9595
// Match
9696
if (onProgress) { // don't override open editor results
97-
if (isFileMatch(progress)) {
97+
if (isFileMatch(progress) || isAIKeyword(progress)) {
9898
onProgress(progress);
9999
} else {
100100
onProgress(<IProgressMessage>progress);

0 commit comments

Comments
 (0)