Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { distinct } from '../../../../base/common/arrays.js';
7
import { CancellationToken } from '../../../../base/common/cancellation.js';
8
import { IStringDictionary } from '../../../../base/common/collections.js';
9
import { IMatch, matchesContiguousSubString, matchesSubString, matchesWords } from '../../../../base/common/filters.js';
10
import { Disposable } from '../../../../base/common/lifecycle.js';
11
import * as strings from '../../../../base/common/strings.js';
12
import { TfIdfCalculator, TfIdfDocument } from '../../../../base/common/tfIdf.js';
13
import { IRange } from '../../../../editor/common/core/range.js';
14
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
15
import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
16
import { ExtensionType } from '../../../../platform/extensions/common/extensions.js';
17
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
18
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
19
import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/common/aiSettingsSearch.js';
20
import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js';
21
import { IGroupFilter, ISearchResult, ISetting, ISettingMatch, ISettingMatcher, ISettingsEditorModel, ISettingsGroup, SettingKeyMatchTypes, SettingMatchType } from '../../../services/preferences/common/preferences.js';
22
import { nullRange } from '../../../services/preferences/common/preferencesModels.js';
23
import { EMBEDDINGS_ONLY_SEARCH_PROVIDER_NAME, EMBEDDINGS_SEARCH_PROVIDER_NAME, IAiSearchProvider, IPreferencesSearchService, IRemoteSearchProvider, ISearchProvider, IWorkbenchSettingsConfiguration, LLM_RANKED_SEARCH_PROVIDER_NAME, STRING_MATCH_SEARCH_PROVIDER_NAME, TF_IDF_SEARCH_PROVIDER_NAME } from '../common/preferences.js';
24
25
export interface IEndpointDetails {
26
urlBase?: string;
27
key?: string;
28
}
29
30
export class PreferencesSearchService extends Disposable implements IPreferencesSearchService {
31
declare readonly _serviceBrand: undefined;
32
33
// @ts-expect-error disable remote search for now, ref https://github.com/microsoft/vscode/issues/172411
34
private _installedExtensions: Promise<ILocalExtension[]>;
35
private _remoteSearchProvider: IRemoteSearchProvider | undefined;
36
private _aiSearchProvider: IAiSearchProvider | undefined;
37
38
constructor(
39
@IInstantiationService private readonly instantiationService: IInstantiationService,
40
@IConfigurationService private readonly configurationService: IConfigurationService,
41
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
42
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService
43
) {
44
super();
45
46
// This request goes to the shared process but results won't change during a window's lifetime, so cache the results.
47
this._installedExtensions = this.extensionManagementService.getInstalled(ExtensionType.User).then(exts => {
48
// Filter to enabled extensions that have settings
49
return exts
50
.filter(ext => this.extensionEnablementService.isEnabled(ext))
51
.filter(ext => ext.manifest && ext.manifest.contributes && ext.manifest.contributes.configuration)
52
.filter(ext => !!ext.identifier.uuid);
53
});
54
}
55
56
getLocalSearchProvider(filter: string): LocalSearchProvider {
57
return this.instantiationService.createInstance(LocalSearchProvider, filter);
58
}
59
60
private get remoteSearchAllowed(): boolean {
61
const workbenchSettings = this.configurationService.getValue<IWorkbenchSettingsConfiguration>().workbench.settings;
62
return workbenchSettings.enableNaturalLanguageSearch;
63
}
64
65
getRemoteSearchProvider(filter: string): IRemoteSearchProvider | undefined {
66
if (!this.remoteSearchAllowed) {
67
return undefined;
68
}
69
70
this._remoteSearchProvider ??= this.instantiationService.createInstance(RemoteSearchProvider);
71
this._remoteSearchProvider.setFilter(filter);
72
return this._remoteSearchProvider;
73
}
74
75
getAiSearchProvider(filter: string): IAiSearchProvider | undefined {
76
if (!this.remoteSearchAllowed) {
77
return undefined;
78
}
79
80
this._aiSearchProvider ??= this.instantiationService.createInstance(AiSearchProvider);
81
this._aiSearchProvider.setFilter(filter);
82
return this._aiSearchProvider;
83
}
84
}
85
86
function cleanFilter(filter: string): string {
87
// Remove " and : which are likely to be copypasted as part of a setting name.
88
// Leave other special characters which the user might want to search for.
89
return filter
90
.replace(/[":]/g, ' ')
91
.replace(/ /g, ' ')
92
.trim();
93
}
94
95
export class LocalSearchProvider implements ISearchProvider {
96
constructor(
97
private _filter: string,
98
@IConfigurationService private readonly configurationService: IConfigurationService
99
) {
100
this._filter = cleanFilter(this._filter);
101
}
102
103
searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise<ISearchResult | null> {
104
if (!this._filter) {
105
return Promise.resolve(null);
106
}
107
108
const settingMatcher: ISettingMatcher = (setting: ISetting) => {
109
let { matches, matchType, keyMatchScore } = new SettingMatches(
110
this._filter,
111
setting,
112
true,
113
this.configurationService
114
);
115
if (matchType === SettingMatchType.None || matches.length === 0) {
116
return null;
117
}
118
if (strings.equalsIgnoreCase(this._filter, setting.key)) {
119
matchType = SettingMatchType.ExactMatch;
120
}
121
return {
122
matches,
123
matchType,
124
keyMatchScore,
125
score: 0 // only used for RemoteSearchProvider matches.
126
};
127
};
128
129
const filterMatches = preferencesModel.filterSettings(this._filter, this.getGroupFilter(this._filter), settingMatcher);
130
131
// Check the top key match type.
132
const topKeyMatchType = Math.max(...filterMatches.map(m => (m.matchType & SettingKeyMatchTypes)));
133
// Always allow description matches as part of https://github.com/microsoft/vscode/issues/239936.
134
const alwaysAllowedMatchTypes = SettingMatchType.DescriptionOrValueMatch | SettingMatchType.LanguageTagSettingMatch;
135
const filteredMatches = filterMatches
136
.filter(m => (m.matchType & topKeyMatchType) || (m.matchType & alwaysAllowedMatchTypes) || m.matchType === SettingMatchType.ExactMatch)
137
.map(m => ({ ...m, providerName: STRING_MATCH_SEARCH_PROVIDER_NAME }));
138
return Promise.resolve({
139
filterMatches: filteredMatches,
140
exactMatch: filteredMatches.some(m => m.matchType === SettingMatchType.ExactMatch)
141
});
142
}
143
144
private getGroupFilter(filter: string): IGroupFilter {
145
const regex = strings.createRegExp(filter, false, { global: true });
146
return (group: ISettingsGroup) => {
147
return group.id !== 'defaultOverrides' && regex.test(group.title);
148
};
149
}
150
}
151
152
export class SettingMatches {
153
readonly matches: IRange[];
154
matchType: SettingMatchType = SettingMatchType.None;
155
/**
156
* A match score for key matches to allow comparing key matches against each other.
157
* Otherwise, all key matches are treated the same, and sorting is done by ToC order.
158
*/
159
keyMatchScore: number = 0;
160
161
constructor(
162
searchString: string,
163
setting: ISetting,
164
private searchDescription: boolean,
165
private readonly configurationService: IConfigurationService
166
) {
167
this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`);
168
}
169
170
private _findMatchesInSetting(searchString: string, setting: ISetting): IRange[] {
171
const result = this._doFindMatchesInSetting(searchString, setting);
172
return result;
173
}
174
175
private _keyToLabel(settingId: string): string {
176
const label = settingId
177
.replace(/[-._]/g, ' ')
178
.replace(/([a-z]+)([A-Z])/g, '$1 $2')
179
.replace(/([A-Za-z]+)(\d+)/g, '$1 $2')
180
.replace(/(\d+)([A-Za-z]+)/g, '$1 $2')
181
.toLowerCase();
182
return label;
183
}
184
185
private _toAlphaNumeric(s: string): string {
186
return s.replace(/[^\p{L}\p{N}]+/gu, '');
187
}
188
189
private _doFindMatchesInSetting(searchString: string, setting: ISetting): IRange[] {
190
const descriptionMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();
191
const keyMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();
192
const valueMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();
193
194
// Key (ID) search
195
// First, search by the setting's ID and label.
196
const settingKeyAsWords: string = this._keyToLabel(setting.key);
197
const queryWords = new Set<string>(searchString.split(' '));
198
for (const word of queryWords) {
199
// Check if the key contains the word. Use contiguous search.
200
const keyMatches = matchesWords(word, settingKeyAsWords, true);
201
if (keyMatches?.length) {
202
keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match)));
203
}
204
}
205
if (keyMatchingWords.size === queryWords.size) {
206
// All words in the query matched with something in the setting key.
207
// Matches "edit format on paste" to "editor.formatOnPaste".
208
this.matchType |= SettingMatchType.AllWordsInSettingsLabel;
209
} else if (keyMatchingWords.size >= 2) {
210
// Matches "edit paste" to "editor.formatOnPaste".
211
// The if statement reduces noise by preventing "editor formatonpast" from matching all editor settings.
212
this.matchType |= SettingMatchType.ContiguousWordsInSettingsLabel;
213
this.keyMatchScore = keyMatchingWords.size;
214
}
215
const searchStringAlphaNumeric = this._toAlphaNumeric(searchString);
216
const keyAlphaNumeric = this._toAlphaNumeric(setting.key);
217
const keyIdMatches = matchesContiguousSubString(searchStringAlphaNumeric, keyAlphaNumeric);
218
if (keyIdMatches?.length) {
219
// Matches "editorformatonp" to "editor.formatonpaste".
220
keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match)));
221
this.matchType |= SettingMatchType.ContiguousQueryInSettingId;
222
}
223
224
// Fall back to non-contiguous key (ID) searches if nothing matched yet.
225
if (this.matchType === SettingMatchType.None) {
226
keyMatchingWords.clear();
227
for (const word of queryWords) {
228
const keyMatches = matchesWords(word, settingKeyAsWords, false);
229
if (keyMatches?.length) {
230
keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match)));
231
}
232
}
233
if (keyMatchingWords.size >= 2 || (keyMatchingWords.size === 1 && queryWords.size === 1)) {
234
// Matches "edforonpas" to "editor.formatOnPaste".
235
// The if statement reduces noise by preventing "editor fomonpast" from matching all editor settings.
236
this.matchType |= SettingMatchType.NonContiguousWordsInSettingsLabel;
237
this.keyMatchScore = keyMatchingWords.size;
238
} else {
239
const keyIdMatches = matchesSubString(searchStringAlphaNumeric, keyAlphaNumeric);
240
if (keyIdMatches?.length) {
241
// Matches "edfmonpas" to "editor.formatOnPaste".
242
keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match)));
243
this.matchType |= SettingMatchType.NonContiguousQueryInSettingId;
244
}
245
}
246
}
247
248
// Check if the match was for a language tag group setting such as [markdown].
249
// In such a case, move that setting to be last.
250
if (setting.overrides?.length && (this.matchType !== SettingMatchType.None)) {
251
this.matchType = SettingMatchType.LanguageTagSettingMatch;
252
const keyRanges = keyMatchingWords.size ?
253
Array.from(keyMatchingWords.values()).flat() : [];
254
return [...keyRanges];
255
}
256
257
// Description search
258
// Search the description if we found non-contiguous key matches at best.
259
const hasContiguousKeyMatchTypes = this.matchType >= SettingMatchType.ContiguousWordsInSettingsLabel;
260
if (this.searchDescription && !hasContiguousKeyMatchTypes) {
261
for (const word of queryWords) {
262
// Search the description lines.
263
for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) {
264
const descriptionMatches = matchesContiguousSubString(word, setting.description[lineIndex]);
265
if (descriptionMatches?.length) {
266
descriptionMatchingWords.set(word, descriptionMatches.map(match => this.toDescriptionRange(setting, match, lineIndex)));
267
}
268
}
269
}
270
if (descriptionMatchingWords.size === queryWords.size) {
271
this.matchType |= SettingMatchType.DescriptionOrValueMatch;
272
} else {
273
// Clear out the match for now. We want to require all words to match in the description.
274
descriptionMatchingWords.clear();
275
}
276
}
277
278
// Value search
279
// Check if the value contains all the words.
280
// Search the values if we found non-contiguous key matches at best.
281
if (!hasContiguousKeyMatchTypes) {
282
if (setting.enum?.length) {
283
// Search all string values of enums.
284
for (const option of setting.enum) {
285
if (typeof option !== 'string') {
286
continue;
287
}
288
valueMatchingWords.clear();
289
for (const word of queryWords) {
290
const valueMatches = matchesContiguousSubString(word, option);
291
if (valueMatches?.length) {
292
valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match)));
293
}
294
}
295
if (valueMatchingWords.size === queryWords.size) {
296
this.matchType |= SettingMatchType.DescriptionOrValueMatch;
297
break;
298
} else {
299
// Clear out the match for now. We want to require all words to match in the value.
300
valueMatchingWords.clear();
301
}
302
}
303
} else {
304
// Search single string value.
305
const settingValue = this.configurationService.getValue(setting.key);
306
if (typeof settingValue === 'string') {
307
for (const word of queryWords) {
308
const valueMatches = matchesContiguousSubString(word, settingValue);
309
if (valueMatches?.length) {
310
valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match)));
311
}
312
}
313
if (valueMatchingWords.size === queryWords.size) {
314
this.matchType |= SettingMatchType.DescriptionOrValueMatch;
315
} else {
316
// Clear out the match for now. We want to require all words to match in the value.
317
valueMatchingWords.clear();
318
}
319
}
320
}
321
}
322
323
const descriptionRanges = descriptionMatchingWords.size ?
324
Array.from(descriptionMatchingWords.values()).flat() : [];
325
const keyRanges = keyMatchingWords.size ?
326
Array.from(keyMatchingWords.values()).flat() : [];
327
const valueRanges = valueMatchingWords.size ?
328
Array.from(valueMatchingWords.values()).flat() : [];
329
return [...descriptionRanges, ...keyRanges, ...valueRanges];
330
}
331
332
private toKeyRange(setting: ISetting, match: IMatch): IRange {
333
return {
334
startLineNumber: setting.keyRange.startLineNumber,
335
startColumn: setting.keyRange.startColumn + match.start,
336
endLineNumber: setting.keyRange.startLineNumber,
337
endColumn: setting.keyRange.startColumn + match.end
338
};
339
}
340
341
private toDescriptionRange(setting: ISetting, match: IMatch, lineIndex: number): IRange {
342
const descriptionRange = setting.descriptionRanges[lineIndex];
343
if (!descriptionRange) {
344
// This case occurs with added settings such as the
345
// manage extension setting.
346
return nullRange;
347
}
348
return {
349
startLineNumber: descriptionRange.startLineNumber,
350
startColumn: descriptionRange.startColumn + match.start,
351
endLineNumber: descriptionRange.endLineNumber,
352
endColumn: descriptionRange.startColumn + match.end
353
};
354
}
355
356
private toValueRange(setting: ISetting, match: IMatch): IRange {
357
return {
358
startLineNumber: setting.valueRange.startLineNumber,
359
startColumn: setting.valueRange.startColumn + match.start + 1,
360
endLineNumber: setting.valueRange.startLineNumber,
361
endColumn: setting.valueRange.startColumn + match.end + 1
362
};
363
}
364
}
365
366
class SettingsRecordProvider {
367
private _settingsRecord: IStringDictionary<ISetting> = {};
368
private _currentPreferencesModel: ISettingsEditorModel | undefined;
369
370
constructor() { }
371
372
updateModel(preferencesModel: ISettingsEditorModel) {
373
if (preferencesModel === this._currentPreferencesModel) {
374
return;
375
}
376
377
this._currentPreferencesModel = preferencesModel;
378
this.refresh();
379
}
380
381
private refresh() {
382
this._settingsRecord = {};
383
384
if (!this._currentPreferencesModel) {
385
return;
386
}
387
388
for (const group of this._currentPreferencesModel.settingsGroups) {
389
if (group.id === 'mostCommonlyUsed') {
390
continue;
391
}
392
for (const section of group.sections) {
393
for (const setting of section.settings) {
394
this._settingsRecord[setting.key] = setting;
395
}
396
}
397
}
398
}
399
400
getSettingsRecord(): IStringDictionary<ISetting> {
401
return this._settingsRecord;
402
}
403
}
404
405
class EmbeddingsSearchProvider implements IRemoteSearchProvider {
406
private static readonly EMBEDDINGS_SETTINGS_SEARCH_MAX_PICKS = 10;
407
408
private readonly _recordProvider: SettingsRecordProvider;
409
private _filter: string = '';
410
411
constructor(
412
private readonly _aiSettingsSearchService: IAiSettingsSearchService,
413
private readonly _excludeSelectionStep: boolean
414
) {
415
this._recordProvider = new SettingsRecordProvider();
416
}
417
418
setFilter(filter: string) {
419
this._filter = cleanFilter(filter);
420
}
421
422
async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise<ISearchResult | null> {
423
if (!this._filter || !this._aiSettingsSearchService.isEnabled()) {
424
return null;
425
}
426
427
this._recordProvider.updateModel(preferencesModel);
428
this._aiSettingsSearchService.startSearch(this._filter, this._excludeSelectionStep, token);
429
430
return {
431
filterMatches: await this.getEmbeddingsItems(token),
432
exactMatch: false
433
};
434
}
435
436
private async getEmbeddingsItems(token: CancellationToken): Promise<ISettingMatch[]> {
437
const settingsRecord = this._recordProvider.getSettingsRecord();
438
const filterMatches: ISettingMatch[] = [];
439
const settings = await this._aiSettingsSearchService.getEmbeddingsResults(this._filter, token);
440
if (!settings) {
441
return [];
442
}
443
444
const providerName = this._excludeSelectionStep ? EMBEDDINGS_ONLY_SEARCH_PROVIDER_NAME : EMBEDDINGS_SEARCH_PROVIDER_NAME;
445
for (const settingKey of settings) {
446
if (filterMatches.length === EmbeddingsSearchProvider.EMBEDDINGS_SETTINGS_SEARCH_MAX_PICKS) {
447
break;
448
}
449
filterMatches.push({
450
setting: settingsRecord[settingKey],
451
matches: [settingsRecord[settingKey].range],
452
matchType: SettingMatchType.RemoteMatch,
453
keyMatchScore: 0,
454
score: 0, // the results are sorted upstream.
455
providerName
456
});
457
}
458
459
return filterMatches;
460
}
461
}
462
463
class TfIdfSearchProvider implements IRemoteSearchProvider {
464
private static readonly TF_IDF_PRE_NORMALIZE_THRESHOLD = 50;
465
private static readonly TF_IDF_POST_NORMALIZE_THRESHOLD = 0.7;
466
private static readonly TF_IDF_MAX_PICKS = 5;
467
468
private _currentPreferencesModel: ISettingsEditorModel | undefined;
469
private _filter: string = '';
470
private _documents: TfIdfDocument[] = [];
471
private _settingsRecord: IStringDictionary<ISetting> = {};
472
473
constructor() {
474
}
475
476
setFilter(filter: string) {
477
this._filter = cleanFilter(filter);
478
}
479
480
keyToLabel(settingId: string): string {
481
const label = settingId
482
.replace(/[-._]/g, ' ')
483
.replace(/([a-z]+)([A-Z])/g, '$1 $2')
484
.replace(/([A-Za-z]+)(\d+)/g, '$1 $2')
485
.replace(/(\d+)([A-Za-z]+)/g, '$1 $2')
486
.toLowerCase();
487
return label;
488
}
489
490
settingItemToEmbeddingString(item: ISetting): string {
491
let result = `Setting Id: ${item.key}\n`;
492
result += `Label: ${this.keyToLabel(item.key)}\n`;
493
result += `Description: ${item.description}\n`;
494
return result;
495
}
496
497
async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise<ISearchResult | null> {
498
if (!this._filter) {
499
return null;
500
}
501
502
if (this._currentPreferencesModel !== preferencesModel) {
503
// Refresh the documents and settings record
504
this._currentPreferencesModel = preferencesModel;
505
this._documents = [];
506
this._settingsRecord = {};
507
for (const group of preferencesModel.settingsGroups) {
508
if (group.id === 'mostCommonlyUsed') {
509
continue;
510
}
511
for (const section of group.sections) {
512
for (const setting of section.settings) {
513
this._documents.push({
514
key: setting.key,
515
textChunks: [this.settingItemToEmbeddingString(setting)]
516
});
517
this._settingsRecord[setting.key] = setting;
518
}
519
}
520
}
521
}
522
523
return {
524
filterMatches: await this.getTfIdfItems(token),
525
exactMatch: false
526
};
527
}
528
529
private async getTfIdfItems(token: CancellationToken): Promise<ISettingMatch[]> {
530
const filterMatches: ISettingMatch[] = [];
531
const tfIdfCalculator = new TfIdfCalculator();
532
tfIdfCalculator.updateDocuments(this._documents);
533
const tfIdfRankings = tfIdfCalculator.calculateScores(this._filter, token);
534
tfIdfRankings.sort((a, b) => b.score - a.score);
535
const maxScore = tfIdfRankings[0].score;
536
537
if (maxScore < TfIdfSearchProvider.TF_IDF_PRE_NORMALIZE_THRESHOLD) {
538
// Reject all the matches.
539
return [];
540
}
541
542
for (const info of tfIdfRankings) {
543
if (info.score / maxScore < TfIdfSearchProvider.TF_IDF_POST_NORMALIZE_THRESHOLD || filterMatches.length === TfIdfSearchProvider.TF_IDF_MAX_PICKS) {
544
break;
545
}
546
const pick = info.key;
547
filterMatches.push({
548
setting: this._settingsRecord[pick],
549
matches: [this._settingsRecord[pick].range],
550
matchType: SettingMatchType.RemoteMatch,
551
keyMatchScore: 0,
552
score: info.score,
553
providerName: TF_IDF_SEARCH_PROVIDER_NAME
554
});
555
}
556
557
return filterMatches;
558
}
559
}
560
561
class RemoteSearchProvider implements IRemoteSearchProvider {
562
private _tfIdfSearchProvider: TfIdfSearchProvider;
563
private _filter: string = '';
564
565
constructor() {
566
this._tfIdfSearchProvider = new TfIdfSearchProvider();
567
}
568
569
setFilter(filter: string): void {
570
this._filter = filter;
571
this._tfIdfSearchProvider.setFilter(filter);
572
}
573
574
async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise<ISearchResult | null> {
575
if (!this._filter) {
576
return null;
577
}
578
579
const results = await this._tfIdfSearchProvider.searchModel(preferencesModel, token);
580
return results;
581
}
582
}
583
584
class AiSearchProvider implements IAiSearchProvider {
585
private readonly _embeddingsSearchProvider: EmbeddingsSearchProvider;
586
private readonly _recordProvider: SettingsRecordProvider;
587
private _filter: string = '';
588
589
constructor(
590
@IAiSettingsSearchService private readonly aiSettingsSearchService: IAiSettingsSearchService
591
) {
592
this._embeddingsSearchProvider = new EmbeddingsSearchProvider(this.aiSettingsSearchService, false);
593
this._recordProvider = new SettingsRecordProvider();
594
}
595
596
setFilter(filter: string): void {
597
this._filter = filter;
598
this._embeddingsSearchProvider.setFilter(filter);
599
}
600
601
async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise<ISearchResult | null> {
602
if (!this._filter || !this.aiSettingsSearchService.isEnabled()) {
603
return null;
604
}
605
606
this._recordProvider.updateModel(preferencesModel);
607
const results = await this._embeddingsSearchProvider.searchModel(preferencesModel, token);
608
return results;
609
}
610
611
async getLLMRankedResults(token: CancellationToken): Promise<ISearchResult | null> {
612
if (!this._filter || !this.aiSettingsSearchService.isEnabled()) {
613
return null;
614
}
615
616
const items = await this.getLLMRankedItems(token);
617
return {
618
filterMatches: items,
619
exactMatch: false
620
};
621
}
622
623
private async getLLMRankedItems(token: CancellationToken): Promise<ISettingMatch[]> {
624
const settingsRecord = this._recordProvider.getSettingsRecord();
625
const filterMatches: ISettingMatch[] = [];
626
const settings = await this.aiSettingsSearchService.getLLMRankedResults(this._filter, token);
627
if (!settings) {
628
return [];
629
}
630
631
for (const settingKey of settings) {
632
if (!settingsRecord[settingKey]) {
633
// Non-existent setting.
634
continue;
635
}
636
filterMatches.push({
637
setting: settingsRecord[settingKey],
638
matches: [settingsRecord[settingKey].range],
639
matchType: SettingMatchType.RemoteMatch,
640
keyMatchScore: 0,
641
score: 0, // the results are sorted upstream.
642
providerName: LLM_RANKED_SEARCH_PROVIDER_NAME
643
});
644
}
645
646
return filterMatches;
647
}
648
}
649
650
registerSingleton(IPreferencesSearchService, PreferencesSearchService, InstantiationType.Delayed);
651
652