Skip to content

Commit 5b99672

Browse files
authored
feat: add more match types for new Settings search (microsoft#241409)
1 parent 750b94a commit 5b99672

File tree

3 files changed

+125
-82
lines changed

3 files changed

+125
-82
lines changed

src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts

Lines changed: 117 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -98,28 +98,29 @@ export class LocalSearchProvider implements ISearchProvider {
9898
}
9999

100100
let orderedScore = LocalSearchProvider.START_SCORE; // Sort is not stable
101-
const useNewKeyMatchingSearch = this.configurationService.getValue('workbench.settings.useWeightedKeySearch') === true;
101+
const useNewKeyMatchAlgorithm = this.configurationService.getValue('workbench.settings.useWeightedKeySearch') === true;
102102
const settingMatcher = (setting: ISetting) => {
103103
const { matches, matchType, keyMatchScore } = new SettingMatches(
104104
this._filter,
105105
setting,
106106
true,
107107
(filter, setting) => preferencesModel.findValueMatches(filter, setting),
108-
useNewKeyMatchingSearch,
108+
useNewKeyMatchAlgorithm,
109109
this.configurationService
110110
);
111+
if (matchType === SettingMatchType.None || matches.length === 0) {
112+
return null;
113+
}
114+
111115
const score = strings.equalsIgnoreCase(this._filter, setting.key) ?
112116
LocalSearchProvider.EXACT_MATCH_SCORE :
113117
orderedScore--;
114-
115-
return matches.length ?
116-
{
117-
matches,
118-
matchType,
119-
keyMatchScore,
120-
score
121-
} :
122-
null;
118+
return {
119+
matches,
120+
matchType,
121+
keyMatchScore,
122+
score
123+
};
123124
};
124125

125126
const filterMatches = preferencesModel.filterSettings(this._filter, this.getGroupFilter(this._filter), settingMatcher);
@@ -129,6 +130,14 @@ export class LocalSearchProvider implements ISearchProvider {
129130
filterMatches: [exactMatch],
130131
exactMatch: true
131132
});
133+
} else if (useNewKeyMatchAlgorithm) {
134+
// Filter by the top match type.
135+
const topMatchType = Math.max(...filterMatches.map(m => m.matchType));
136+
const filteredMatches = filterMatches.filter(m => m.matchType === topMatchType);
137+
return Promise.resolve({
138+
filterMatches: filteredMatches,
139+
exactMatch: false
140+
});
132141
} else {
133142
return Promise.resolve({
134143
filterMatches: filterMatches,
@@ -159,7 +168,7 @@ export class SettingMatches {
159168
setting: ISetting,
160169
private searchDescription: boolean,
161170
valuesMatcher: (filter: string, setting: ISetting) => IRange[],
162-
private useNewKeyMatchingSearch: boolean,
171+
private useNewKeyMatchAlgorithm: boolean,
163172
private readonly configurationService: IConfigurationService
164173
) {
165174
this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`);
@@ -180,53 +189,82 @@ export class SettingMatches {
180189
return label;
181190
}
182191

192+
private _toAlphaNumeric(s: string): string {
193+
return s.replace(/[^A-Za-z0-9]+/g, '');
194+
}
195+
183196
private _doFindMatchesInSetting(searchString: string, setting: ISetting): IRange[] {
184197
const descriptionMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();
185198
const keyMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();
186199
const valueMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();
187200

188-
// Key search
201+
// Key (ID) search
202+
// First, search by the setting's ID and label.
189203
const settingKeyAsWords: string = this._keyToLabel(setting.key);
190204
const queryWords = new Set<string>(searchString.split(' '));
191205
for (const word of queryWords) {
192206
// Check if the key contains the word.
193207
// Force contiguous matching iff we're using the new algorithm.
194-
const keyMatches = matchesWords(word, settingKeyAsWords, this.useNewKeyMatchingSearch);
208+
const keyMatches = matchesWords(word, settingKeyAsWords, this.useNewKeyMatchAlgorithm);
195209
if (keyMatches?.length) {
196210
keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match)));
197211
}
198212
}
199-
if (this.useNewKeyMatchingSearch) {
213+
if (this.useNewKeyMatchAlgorithm) {
214+
// New key match algorithm
200215
if (keyMatchingWords.size === queryWords.size) {
201216
// All words in the query matched with something in the setting key.
202-
this.matchType |= SettingMatchType.KeyMatch;
203-
// Score based on how many words matched out of the entire key, penalizing longer setting names.
204-
const settingKeyAsWordsCount = settingKeyAsWords.split(' ').length;
205-
this.keyMatchScore = (keyMatchingWords.size / settingKeyAsWordsCount) + (1 / setting.key.length);
206-
}
207-
const keyMatches = matchesSubString(searchString, settingKeyAsWords);
208-
if (keyMatches?.length) {
209-
// Handles cases such as "editor formonpast" with missing letters.
210-
keyMatchingWords.set(searchString, keyMatches.map(match => this.toKeyRange(setting, match)));
211-
this.matchType |= SettingMatchType.KeyMatch;
217+
// Matches "edit format on paste" to "editor.formatOnPaste".
218+
this.matchType |= SettingMatchType.AllWordsInSettingsLabel;
219+
} else if (keyMatchingWords.size >= 2) {
220+
// Matches "edit paste" to "editor.formatOnPaste".
221+
// The if statement reduces noise by preventing "editor formatonpast" from matching all editor settings.
222+
this.matchType |= SettingMatchType.ContiguousWordsInSettingsLabel;
212223
this.keyMatchScore = keyMatchingWords.size;
213224
}
225+
const searchStringAlphaNumeric = this._toAlphaNumeric(searchString);
226+
const keyAlphaNumeric = this._toAlphaNumeric(setting.key);
227+
const keyIdMatches = matchesContiguousSubString(searchStringAlphaNumeric, keyAlphaNumeric);
228+
if (keyIdMatches?.length) {
229+
// Matches "editorformatonp" to "editor.formatonpaste".
230+
keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match)));
231+
this.matchType |= SettingMatchType.ContiguousQueryInSettingId;
232+
}
233+
234+
// Fall back to non-contiguous searches if nothing matched yet.
235+
if (this.matchType === SettingMatchType.None) {
236+
keyMatchingWords.clear();
237+
for (const word of queryWords) {
238+
const keyMatches = matchesWords(word, settingKeyAsWords, false);
239+
if (keyMatches?.length) {
240+
keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match)));
241+
}
242+
}
243+
if (keyMatchingWords.size >= 2 || (keyMatchingWords.size === 1 && queryWords.size === 1)) {
244+
// Matches "edforonpas" to "editor.formatOnPaste".
245+
// The if statement reduces noise by preventing "editor fomonpast" from matching all editor settings.
246+
this.matchType |= SettingMatchType.NonContiguousWordsInSettingsLabel;
247+
this.keyMatchScore = keyMatchingWords.size;
248+
} else {
249+
const keyIdMatches = matchesSubString(searchStringAlphaNumeric, keyAlphaNumeric);
250+
if (keyIdMatches?.length) {
251+
// Matches "edfmonpas" to "editor.formatOnPaste".
252+
keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match)));
253+
this.matchType |= SettingMatchType.NonContiguousQueryInSettingId;
254+
}
255+
}
256+
}
214257
} else {
215-
// Fall back to the old algorithm.
258+
// Old key match algorithm
216259
if (keyMatchingWords.size) {
217-
this.matchType |= SettingMatchType.KeyMatch;
260+
this.matchType |= SettingMatchType.NonContiguousWordsInSettingsLabel;
218261
this.keyMatchScore = keyMatchingWords.size;
219262
}
220-
}
221-
const keyIdMatches = matchesContiguousSubString(searchString, setting.key);
222-
if (keyIdMatches?.length) {
223-
// Handles cases such as "editor.formatonpaste" where the user tries searching for the ID.
224-
keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match)));
225-
if (this.useNewKeyMatchingSearch) {
226-
this.matchType |= SettingMatchType.KeyMatch;
227-
this.keyMatchScore = Math.max(this.keyMatchScore, searchString.length / setting.key.length);
228-
} else {
229-
this.matchType |= SettingMatchType.KeyIdMatch;
263+
const keyIdMatches = matchesContiguousSubString(searchString, setting.key);
264+
if (keyIdMatches?.length) {
265+
// Handles cases such as "editor.formatonpaste" where the user tries searching for the ID.
266+
keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match)));
267+
this.matchType |= SettingMatchType.ContiguousQueryInSettingId;
230268
}
231269
}
232270

@@ -239,15 +277,12 @@ export class SettingMatches {
239277
return [...keyRanges];
240278
}
241279

242-
// New algorithm only: exit early if the key already matched.
243-
if (this.useNewKeyMatchingSearch && (this.matchType !== SettingMatchType.None)) {
244-
const keyRanges = keyMatchingWords.size ?
245-
Array.from(keyMatchingWords.values()).flat() : [];
246-
return [...keyRanges];
247-
}
248-
249280
// Description search
250-
if (this.searchDescription && this.matchType === SettingMatchType.None) {
281+
// Old algorithm: search the description if we haven't matched anything yet.
282+
// New algorithm: search the description if we found non-contiguous key matches at best.
283+
const hasContiguousKeyMatchTypes = this.matchType >= SettingMatchType.ContiguousWordsInSettingsLabel;
284+
const checkDescription = (!this.useNewKeyMatchAlgorithm && this.matchType === SettingMatchType.None) || (this.useNewKeyMatchAlgorithm && !hasContiguousKeyMatchTypes);
285+
if (this.searchDescription && checkDescription) {
251286
for (const word of queryWords) {
252287
// Search the description lines.
253288
for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) {
@@ -267,42 +302,47 @@ export class SettingMatches {
267302

268303
// Value search
269304
// Check if the value contains all the words.
270-
if (setting.enum?.length) {
271-
// Search all string values of enums.
272-
for (const option of setting.enum) {
273-
if (typeof option !== 'string') {
274-
continue;
275-
}
276-
valueMatchingWords.clear();
277-
for (const word of queryWords) {
278-
const valueMatches = matchesContiguousSubString(word, option);
279-
if (valueMatches?.length) {
280-
valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match)));
305+
// Old algorithm: always search the values.
306+
// New algorithm: search the values if we found non-contiguous key matches at best.
307+
const checkValue = !this.useNewKeyMatchAlgorithm || !hasContiguousKeyMatchTypes;
308+
if (checkValue) {
309+
if (setting.enum?.length) {
310+
// Search all string values of enums.
311+
for (const option of setting.enum) {
312+
if (typeof option !== 'string') {
313+
continue;
281314
}
282-
}
283-
if (valueMatchingWords.size === queryWords.size) {
284-
this.matchType |= SettingMatchType.DescriptionOrValueMatch;
285-
break;
286-
} else {
287-
// Clear out the match for now. We want to require all words to match in the value.
288315
valueMatchingWords.clear();
289-
}
290-
}
291-
} else {
292-
// Search single string value.
293-
const settingValue = this.configurationService.getValue(setting.key);
294-
if (typeof settingValue === 'string') {
295-
for (const word of queryWords) {
296-
const valueMatches = matchesContiguousSubString(word, settingValue);
297-
if (valueMatches?.length) {
298-
valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match)));
316+
for (const word of queryWords) {
317+
const valueMatches = matchesContiguousSubString(word, option);
318+
if (valueMatches?.length) {
319+
valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match)));
320+
}
321+
}
322+
if (valueMatchingWords.size === queryWords.size) {
323+
this.matchType |= SettingMatchType.DescriptionOrValueMatch;
324+
break;
325+
} else {
326+
// Clear out the match for now. We want to require all words to match in the value.
327+
valueMatchingWords.clear();
299328
}
300329
}
301-
if (valueMatchingWords.size === queryWords.size) {
302-
this.matchType |= SettingMatchType.DescriptionOrValueMatch;
303-
} else {
304-
// Clear out the match for now. We want to require all words to match in the value.
305-
valueMatchingWords.clear();
330+
} else {
331+
// Search single string value.
332+
const settingValue = this.configurationService.getValue(setting.key);
333+
if (typeof settingValue === 'string') {
334+
for (const word of queryWords) {
335+
const valueMatches = matchesContiguousSubString(word, settingValue);
336+
if (valueMatches?.length) {
337+
valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match)));
338+
}
339+
}
340+
if (valueMatchingWords.size === queryWords.size) {
341+
this.matchType |= SettingMatchType.DescriptionOrValueMatch;
342+
} else {
343+
// Clear out the match for now. We want to require all words to match in the value.
344+
valueMatchingWords.clear();
345+
}
306346
}
307347
}
308348
}

src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -971,8 +971,8 @@ export class SearchResultModel extends SettingsTreeModel {
971971
// Sort by match type if the match types are not the same.
972972
// The priority of the match type is given by the SettingMatchType enum.
973973
return b.matchType - a.matchType;
974-
} else if (a.matchType === SettingMatchType.KeyMatch) {
975-
// The match types are the same and are KeyMatch.
974+
} else if (a.matchType === SettingMatchType.NonContiguousWordsInSettingsLabel || a.matchType === SettingMatchType.ContiguousWordsInSettingsLabel) {
975+
// The match types are the same.
976976
// Sort by the number of words matched in the key.
977977
// If those are the same, sort by the order in the table of contents.
978978
return (b.keyMatchScore - a.keyMatchScore) || compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder);

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,12 @@ export enum SettingMatchType {
137137
None = 0,
138138
LanguageTagSettingMatch = 1 << 0,
139139
RemoteMatch = 1 << 1,
140-
DescriptionOrValueMatch = 1 << 2,
141-
KeyMatch = 1 << 3,
142-
KeyIdMatch = 1 << 4,
140+
NonContiguousQueryInSettingId = 1 << 2,
141+
DescriptionOrValueMatch = 1 << 3,
142+
NonContiguousWordsInSettingsLabel = 1 << 4,
143+
ContiguousWordsInSettingsLabel = 1 << 5,
144+
ContiguousQueryInSettingId = 1 << 6,
145+
AllWordsInSettingsLabel = 1 << 7,
143146
}
144147

145148
export interface ISettingMatch {

0 commit comments

Comments
 (0)