Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts
5241 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 { IMatch, IFilter, or, matchesCamelCase, matchesWords, matchesBaseContiguousSubString } from '../../../../../base/common/filters.js';
8
import { Emitter } from '../../../../../base/common/event.js';
9
import { ILanguageModelsService, ILanguageModelProviderDescriptor, ILanguageModelChatMetadataAndIdentifier } from '../../../chat/common/languageModels.js';
10
import { localize } from '../../../../../nls.js';
11
import { Disposable } from '../../../../../base/common/lifecycle.js';
12
import { ILanguageModelsProviderGroup } from '../../common/languageModelsConfiguration.js';
13
import Severity from '../../../../../base/common/severity.js';
14
15
export const MODEL_ENTRY_TEMPLATE_ID = 'model.entry.template';
16
export const VENDOR_ENTRY_TEMPLATE_ID = 'vendor.entry.template';
17
export const GROUP_ENTRY_TEMPLATE_ID = 'group.entry.template';
18
19
const wordFilter = or(matchesBaseContiguousSubString, matchesWords);
20
const CAPABILITY_REGEX = /@capability:\s*([^\s]+)/gi;
21
const VISIBLE_REGEX = /@visible:\s*(true|false)/i;
22
const PROVIDER_REGEX = /@provider:\s*((".+?")|([^\s]+))/gi;
23
24
export const SEARCH_SUGGESTIONS = {
25
FILTER_TYPES: [
26
'@provider:',
27
'@capability:',
28
'@visible:'
29
],
30
CAPABILITIES: [
31
'@capability:tools',
32
'@capability:vision',
33
'@capability:agent'
34
],
35
VISIBILITY: [
36
'@visible:true',
37
'@visible:false'
38
]
39
};
40
41
export interface ILanguageModelProvider {
42
vendor: ILanguageModelProviderDescriptor;
43
group: ILanguageModelsProviderGroup;
44
}
45
46
export interface ILanguageModel extends ILanguageModelChatMetadataAndIdentifier {
47
provider: ILanguageModelProvider;
48
visible: boolean;
49
}
50
51
export interface ILanguageModelEntry {
52
type: 'model';
53
id: string;
54
templateId: string;
55
model: ILanguageModel;
56
providerMatches?: IMatch[];
57
modelNameMatches?: IMatch[];
58
modelIdMatches?: IMatch[];
59
capabilityMatches?: string[];
60
}
61
62
export interface ILanguageModelGroupEntry {
63
type: 'group';
64
id: string;
65
label: string;
66
collapsed: boolean;
67
templateId: string;
68
}
69
70
export interface ILanguageModelProviderEntry {
71
type: 'vendor';
72
id: string;
73
label: string;
74
templateId: string;
75
collapsed: boolean;
76
vendorEntry: ILanguageModelProvider;
77
}
78
79
export interface IStatusEntry {
80
type: 'status';
81
id: string;
82
message: string;
83
severity: Severity;
84
}
85
86
export interface ILanguageModelEntriesGroup {
87
group: ILanguageModelGroupEntry | ILanguageModelProviderEntry;
88
models: ILanguageModel[];
89
status?: IStatusEntry;
90
}
91
92
export function isLanguageModelProviderEntry(entry: IViewModelEntry): entry is ILanguageModelProviderEntry {
93
return entry.type === 'vendor';
94
}
95
96
export function isLanguageModelGroupEntry(entry: IViewModelEntry): entry is ILanguageModelGroupEntry {
97
return entry.type === 'group';
98
}
99
100
export function isStatusEntry(entry: IViewModelEntry): entry is IStatusEntry {
101
return entry.type === 'status';
102
}
103
104
export type IViewModelEntry = ILanguageModelEntry | ILanguageModelProviderEntry | ILanguageModelGroupEntry | IStatusEntry;
105
106
export interface IViewModelChangeEvent {
107
at: number;
108
removed: number;
109
added: IViewModelEntry[];
110
}
111
112
export const enum ChatModelGroup {
113
Vendor = 'vendor',
114
Visibility = 'visibility'
115
}
116
117
export class ChatModelsViewModel extends Disposable {
118
119
private readonly _onDidChange = this._register(new Emitter<IViewModelChangeEvent>());
120
readonly onDidChange = this._onDidChange.event;
121
122
private readonly _onDidChangeGrouping = this._register(new Emitter<ChatModelGroup>());
123
readonly onDidChangeGrouping = this._onDidChangeGrouping.event;
124
125
private languageModels: ILanguageModel[];
126
private languageModelGroupStatuses: Array<{ provider: ILanguageModelProvider; status: { severity: Severity; message: string } }> = [];
127
private languageModelGroups: ILanguageModelEntriesGroup[] = [];
128
129
private readonly collapsedGroups = new Set<string>();
130
private searchValue: string = '';
131
private modelsSorted: boolean = false;
132
133
private _groupBy: ChatModelGroup = ChatModelGroup.Vendor;
134
get groupBy(): ChatModelGroup { return this._groupBy; }
135
set groupBy(groupBy: ChatModelGroup) {
136
if (this._groupBy !== groupBy) {
137
this._groupBy = groupBy;
138
this.collapsedGroups.clear();
139
this.languageModelGroups = this.groupModels(this.languageModels);
140
this.doFilter();
141
this._onDidChangeGrouping.fire(groupBy);
142
}
143
}
144
145
constructor(
146
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
147
) {
148
super();
149
this.languageModels = [];
150
this._register(this.languageModelsService.onDidChangeLanguageModels(vendor => this.refreshVendor(vendor)));
151
}
152
153
private readonly _viewModelEntries: IViewModelEntry[] = [];
154
get viewModelEntries(): readonly IViewModelEntry[] {
155
return this._viewModelEntries;
156
}
157
private splice(at: number, removed: number, added: IViewModelEntry[]): void {
158
this._viewModelEntries.splice(at, removed, ...added);
159
if (this.selectedEntry) {
160
this.selectedEntry = this._viewModelEntries.find(entry => entry.id === this.selectedEntry?.id);
161
}
162
this._onDidChange.fire({ at, removed, added });
163
}
164
165
selectedEntry: IViewModelEntry | undefined;
166
167
public shouldRefilter(): boolean {
168
return !this.modelsSorted;
169
}
170
171
filter(searchValue: string): readonly IViewModelEntry[] {
172
if (searchValue !== this.searchValue) {
173
this.searchValue = searchValue;
174
this.collapsedGroups.clear();
175
if (!this.modelsSorted) {
176
this.languageModelGroups = this.groupModels(this.languageModels);
177
}
178
this.doFilter();
179
}
180
return this.viewModelEntries;
181
}
182
183
private doFilter(): void {
184
const viewModelEntries: IViewModelEntry[] = [];
185
const shouldShowGroupHeaders = this.languageModelGroups.length > 1;
186
187
for (const group of this.languageModelGroups) {
188
if (this.collapsedGroups.has(group.group.id)) {
189
group.group.collapsed = true;
190
if (shouldShowGroupHeaders) {
191
viewModelEntries.push(group.group);
192
}
193
continue;
194
}
195
196
const groupEntries: IViewModelEntry[] = [];
197
if (group.status) {
198
groupEntries.push(group.status);
199
}
200
201
groupEntries.push(...this.filterModels(group.models, this.searchValue));
202
203
if (groupEntries.length > 0) {
204
group.group.collapsed = false;
205
if (shouldShowGroupHeaders) {
206
viewModelEntries.push(group.group);
207
}
208
viewModelEntries.push(...groupEntries);
209
}
210
}
211
this.splice(0, this._viewModelEntries.length, viewModelEntries);
212
}
213
214
private filterModels(modelEntries: ILanguageModel[], searchValue: string): IViewModelEntry[] {
215
let visible: boolean | undefined;
216
217
const visibleMatches = VISIBLE_REGEX.exec(searchValue);
218
if (visibleMatches && visibleMatches[1]) {
219
visible = visibleMatches[1].toLowerCase() === 'true';
220
searchValue = searchValue.replace(VISIBLE_REGEX, '');
221
}
222
223
const providerNames: string[] = [];
224
let providerMatch: RegExpExecArray | null;
225
PROVIDER_REGEX.lastIndex = 0;
226
while ((providerMatch = PROVIDER_REGEX.exec(searchValue)) !== null) {
227
const providerName = providerMatch[2] ? providerMatch[2].substring(1, providerMatch[2].length - 1) : providerMatch[3];
228
providerNames.push(providerName);
229
}
230
if (providerNames.length > 0) {
231
searchValue = searchValue.replace(PROVIDER_REGEX, '');
232
}
233
234
const capabilities: string[] = [];
235
let capabilityMatch: RegExpExecArray | null;
236
CAPABILITY_REGEX.lastIndex = 0;
237
while ((capabilityMatch = CAPABILITY_REGEX.exec(searchValue)) !== null) {
238
capabilities.push(capabilityMatch[1].toLowerCase());
239
}
240
if (capabilities.length > 0) {
241
searchValue = searchValue.replace(CAPABILITY_REGEX, '');
242
}
243
244
const quoteAtFirstChar = searchValue.charAt(0) === '"';
245
const quoteAtLastChar = searchValue.charAt(searchValue.length - 1) === '"';
246
const completeMatch = quoteAtFirstChar && quoteAtLastChar;
247
if (quoteAtFirstChar) {
248
searchValue = searchValue.substring(1);
249
}
250
if (quoteAtLastChar) {
251
searchValue = searchValue.substring(0, searchValue.length - 1);
252
}
253
searchValue = searchValue.trim();
254
255
const result: IViewModelEntry[] = [];
256
const words = searchValue.split(' ');
257
const lowerProviders = providerNames.map(p => p.toLowerCase().trim());
258
259
for (const modelEntry of modelEntries) {
260
if (visible !== undefined) {
261
if (modelEntry.visible !== visible) {
262
continue;
263
}
264
}
265
266
if (lowerProviders.length > 0) {
267
const matchesProvider = lowerProviders.some(provider =>
268
modelEntry.provider.vendor.vendor.toLowerCase() === provider ||
269
modelEntry.provider.vendor.displayName.toLowerCase() === provider
270
);
271
if (!matchesProvider) {
272
continue;
273
}
274
}
275
276
// Filter by capabilities
277
let matchedCapabilities: string[] = [];
278
if (capabilities.length > 0) {
279
if (!modelEntry.metadata.capabilities) {
280
continue;
281
}
282
let matchesAll = true;
283
for (const capability of capabilities) {
284
const matchedForThisCapability = this.getMatchingCapabilities(modelEntry, capability);
285
if (matchedForThisCapability.length === 0) {
286
matchesAll = false;
287
break;
288
}
289
matchedCapabilities.push(...matchedForThisCapability);
290
}
291
if (!matchesAll) {
292
continue;
293
}
294
matchedCapabilities = distinct(matchedCapabilities);
295
}
296
297
// Filter by text
298
let modelMatches: ModelItemMatches | undefined;
299
if (searchValue) {
300
modelMatches = new ModelItemMatches(modelEntry, searchValue, words, completeMatch);
301
if (!modelMatches.modelNameMatches && !modelMatches.modelIdMatches && !modelMatches.providerMatches && !modelMatches.capabilityMatches) {
302
continue;
303
}
304
}
305
306
const modelId = this.getModelId(modelEntry);
307
result.push({
308
type: 'model',
309
id: modelId,
310
templateId: MODEL_ENTRY_TEMPLATE_ID,
311
model: modelEntry,
312
modelNameMatches: modelMatches?.modelNameMatches || undefined,
313
modelIdMatches: modelMatches?.modelIdMatches || undefined,
314
providerMatches: modelMatches?.providerMatches || undefined,
315
capabilityMatches: matchedCapabilities.length ? matchedCapabilities : undefined,
316
});
317
}
318
return result;
319
}
320
321
private getMatchingCapabilities(modelEntry: ILanguageModel, capability: string): string[] {
322
const matchedCapabilities: string[] = [];
323
if (!modelEntry.metadata.capabilities) {
324
return matchedCapabilities;
325
}
326
327
switch (capability) {
328
case 'tools':
329
case 'toolcalling':
330
if (modelEntry.metadata.capabilities.toolCalling === true) {
331
matchedCapabilities.push('toolCalling');
332
}
333
break;
334
case 'vision':
335
if (modelEntry.metadata.capabilities.vision === true) {
336
matchedCapabilities.push('vision');
337
}
338
break;
339
case 'agent':
340
case 'agentmode':
341
if (modelEntry.metadata.capabilities.agentMode === true) {
342
matchedCapabilities.push('agentMode');
343
}
344
break;
345
default:
346
// Check edit tools
347
if (modelEntry.metadata.capabilities.editTools) {
348
for (const tool of modelEntry.metadata.capabilities.editTools) {
349
if (tool.toLowerCase().includes(capability)) {
350
matchedCapabilities.push(tool);
351
}
352
}
353
}
354
break;
355
}
356
return matchedCapabilities;
357
}
358
359
private groupModels(languageModels: ILanguageModel[]): ILanguageModelEntriesGroup[] {
360
const result: ILanguageModelEntriesGroup[] = [];
361
if (this.groupBy === ChatModelGroup.Visibility) {
362
const visible = [], hidden = [];
363
for (const model of languageModels) {
364
if (model.visible) {
365
visible.push(model);
366
} else {
367
hidden.push(model);
368
}
369
}
370
result.push({
371
group: {
372
type: 'group',
373
id: 'visible',
374
label: localize('visible', "Visible"),
375
templateId: GROUP_ENTRY_TEMPLATE_ID,
376
collapsed: this.collapsedGroups.has('visible')
377
},
378
models: visible
379
});
380
result.push({
381
group: {
382
type: 'group',
383
id: 'hidden',
384
label: localize('hidden', "Hidden"),
385
templateId: GROUP_ENTRY_TEMPLATE_ID,
386
collapsed: this.collapsedGroups.has('hidden'),
387
},
388
models: hidden
389
});
390
}
391
else if (this.groupBy === ChatModelGroup.Vendor) {
392
for (const model of languageModels) {
393
const groupId = this.getProviderGroupId(model.provider.group);
394
let group = result.find(group => group.group.id === groupId);
395
if (!group) {
396
group = {
397
group: this.createLanguageModelProviderEntry(model.provider),
398
models: [],
399
};
400
result.push(group);
401
}
402
group.models.push(model);
403
}
404
for (const statusGroup of this.languageModelGroupStatuses) {
405
const groupId = this.getProviderGroupId(statusGroup.provider.group);
406
let group = result.find(group => group.group.id === groupId);
407
if (!group) {
408
group = {
409
group: this.createLanguageModelProviderEntry(statusGroup.provider),
410
models: [],
411
};
412
result.push(group);
413
}
414
group.status = {
415
id: `status.${group.group.id}`,
416
type: 'status',
417
...statusGroup.status,
418
};
419
}
420
result.sort((a, b) => {
421
if (a.models[0]?.provider.vendor.isDefault) { return -1; }
422
if (b.models[0]?.provider.vendor.isDefault) { return 1; }
423
return a.group.label.localeCompare(b.group.label);
424
});
425
}
426
for (const group of result) {
427
group.models.sort((a, b) => {
428
if (a.provider.vendor.isDefault && b.provider.vendor.isDefault) {
429
return a.metadata.name.localeCompare(b.metadata.name);
430
}
431
if (a.provider.vendor.isDefault) { return -1; }
432
if (b.provider.vendor.isDefault) { return 1; }
433
if (a.provider.group.name === b.provider.group.name) {
434
return a.metadata.name.localeCompare(b.metadata.name);
435
}
436
return a.provider.group.name.localeCompare(b.provider.group.name);
437
});
438
}
439
this.modelsSorted = true;
440
return result;
441
}
442
443
private createLanguageModelProviderEntry(provider: ILanguageModelProvider): ILanguageModelProviderEntry {
444
const id = this.getProviderGroupId(provider.group);
445
return {
446
type: 'vendor',
447
id,
448
label: provider.group.name,
449
templateId: VENDOR_ENTRY_TEMPLATE_ID,
450
collapsed: this.collapsedGroups.has(id),
451
vendorEntry: {
452
group: provider.group,
453
vendor: provider.vendor
454
},
455
};
456
}
457
458
getVendors(): ILanguageModelProviderDescriptor[] {
459
return [...this.languageModelsService.getVendors()].sort((a, b) => {
460
if (a.isDefault) { return -1; }
461
if (b.isDefault) { return 1; }
462
return a.displayName.localeCompare(b.displayName);
463
});
464
}
465
466
async refresh(): Promise<void> {
467
await this.languageModelsService.selectLanguageModels({});
468
await this.refreshAllVendors();
469
}
470
471
private async refreshAllVendors(): Promise<void> {
472
this.languageModels = [];
473
this.languageModelGroupStatuses = [];
474
for (const vendor of this.getVendors()) {
475
this.addVendorModels(vendor);
476
}
477
this.languageModelGroups = this.groupModels(this.languageModels);
478
this.doFilter();
479
}
480
481
private refreshVendor(vendorId: string): void {
482
const vendor = this.getVendors().find(v => v.vendor === vendorId);
483
if (!vendor) {
484
return;
485
}
486
487
// Remove existing models for this vendor
488
this.languageModels = this.languageModels.filter(m => m.provider.vendor.vendor !== vendorId);
489
this.languageModelGroupStatuses = this.languageModelGroupStatuses.filter(s => s.provider.vendor.vendor !== vendorId);
490
491
// Add updated models for this vendor
492
this.addVendorModels(vendor);
493
this.languageModelGroups = this.groupModels(this.languageModels);
494
this.doFilter();
495
}
496
497
private addVendorModels(vendor: ILanguageModelProviderDescriptor): void {
498
const models: ILanguageModel[] = [];
499
const languageModelsGroups = this.languageModelsService.getLanguageModelGroups(vendor.vendor);
500
for (const group of languageModelsGroups) {
501
const provider: ILanguageModelProvider = {
502
group: group.group ?? {
503
vendor: vendor.vendor,
504
name: vendor.displayName
505
},
506
vendor
507
};
508
if (group.status) {
509
this.languageModelGroupStatuses.push({
510
provider,
511
status: {
512
message: group.status.message,
513
severity: group.status.severity
514
}
515
});
516
}
517
for (const identifier of group.modelIdentifiers) {
518
const metadata = this.languageModelsService.lookupLanguageModel(identifier);
519
if (!metadata) {
520
continue;
521
}
522
if (vendor.isDefault && metadata.id === 'auto') {
523
continue;
524
}
525
models.push({
526
identifier,
527
metadata,
528
provider,
529
visible: metadata.isUserSelectable ?? false,
530
});
531
}
532
}
533
this.languageModels.push(...models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)));
534
}
535
536
toggleVisibility(model: ILanguageModelEntry): void {
537
const newVisibility = !model.model.visible;
538
this.languageModelsService.updateModelPickerPreference(model.model.identifier, newVisibility);
539
const metadata = this.languageModelsService.lookupLanguageModel(model.model.identifier);
540
const index = this.viewModelEntries.indexOf(model);
541
if (metadata && index !== -1) {
542
model.model.visible = newVisibility;
543
model.model.metadata = metadata;
544
model.id = this.getModelId(model.model);
545
if (this.groupBy === ChatModelGroup.Visibility) {
546
this.modelsSorted = false;
547
}
548
this.splice(index, 1, [model]);
549
}
550
}
551
552
setModelsVisibility(models: ILanguageModelEntry[], visible: boolean): void {
553
for (const model of models) {
554
this.languageModelsService.updateModelPickerPreference(model.model.identifier, visible);
555
model.model.visible = visible;
556
}
557
// Refresh to update the UI
558
this.languageModelGroups = this.groupModels(this.languageModels);
559
this.doFilter();
560
}
561
562
setGroupVisibility(group: ILanguageModelProviderEntry | ILanguageModelGroupEntry, visible: boolean): void {
563
const models = this.getModelsForGroup(group);
564
for (const model of models) {
565
this.languageModelsService.updateModelPickerPreference(model.identifier, visible);
566
model.visible = visible;
567
}
568
// Refresh to update the UI
569
this.languageModelGroups = this.groupModels(this.languageModels);
570
this.doFilter();
571
}
572
573
getModelsForGroup(group: ILanguageModelProviderEntry | ILanguageModelGroupEntry): ILanguageModel[] {
574
if (isLanguageModelProviderEntry(group)) {
575
return this.languageModels.filter(m =>
576
this.getProviderGroupId(m.provider.group) === group.id
577
);
578
} else {
579
// Group by visibility
580
return this.languageModels.filter(m =>
581
(group.id === 'visible' && m.visible) ||
582
(group.id === 'hidden' && !m.visible)
583
);
584
}
585
}
586
587
private getModelId(modelEntry: ILanguageModel): string {
588
return `${modelEntry.provider.group.name}.${modelEntry.identifier}.${modelEntry.metadata.version}-visible:${modelEntry.visible}`;
589
}
590
591
private getProviderGroupId(group: ILanguageModelsProviderGroup): string {
592
return `${group.vendor}-${group.name}`;
593
}
594
595
toggleCollapsed(viewModelEntry: IViewModelEntry): void {
596
const id = isLanguageModelGroupEntry(viewModelEntry) ? viewModelEntry.id : isLanguageModelProviderEntry(viewModelEntry) ? viewModelEntry.id : undefined;
597
if (!id) {
598
return;
599
}
600
this.selectedEntry = viewModelEntry;
601
if (!this.collapsedGroups.delete(id)) {
602
this.collapsedGroups.add(id);
603
}
604
this.doFilter();
605
}
606
607
collapseAll(): void {
608
this.collapsedGroups.clear();
609
for (const entry of this.viewModelEntries) {
610
if (isLanguageModelProviderEntry(entry) || isLanguageModelGroupEntry(entry)) {
611
this.collapsedGroups.add(entry.id);
612
}
613
}
614
this.doFilter();
615
}
616
617
getConfiguredVendors(): ILanguageModelProvider[] {
618
const result: ILanguageModelProvider[] = [];
619
const seenVendors = new Set<string>();
620
for (const modelEntry of this.languageModels) {
621
if (!seenVendors.has(modelEntry.provider.group.name)) {
622
seenVendors.add(modelEntry.provider.group.name);
623
result.push(modelEntry.provider);
624
}
625
}
626
return result;
627
}
628
}
629
630
class ModelItemMatches {
631
632
readonly modelNameMatches: IMatch[] | null = null;
633
readonly modelIdMatches: IMatch[] | null = null;
634
readonly providerMatches: IMatch[] | null = null;
635
readonly capabilityMatches: IMatch[] | null = null;
636
637
constructor(modelEntry: ILanguageModel, searchValue: string, words: string[], completeMatch: boolean) {
638
if (!completeMatch) {
639
// Match against model name
640
this.modelNameMatches = modelEntry.metadata.name ?
641
this.matches(searchValue, modelEntry.metadata.name, (word, wordToMatchAgainst) => matchesWords(word, wordToMatchAgainst, true), words) :
642
null;
643
644
this.modelIdMatches = this.matches(searchValue, modelEntry.metadata.id, or(matchesWords, matchesCamelCase), words);
645
646
// Match against vendor display name
647
this.providerMatches = this.matches(searchValue, modelEntry.provider.group.name, (word, wordToMatchAgainst) => matchesWords(word, wordToMatchAgainst, true), words);
648
649
// Match against capabilities
650
if (modelEntry.metadata.capabilities) {
651
const capabilityStrings: string[] = [];
652
if (modelEntry.metadata.capabilities.toolCalling) {
653
capabilityStrings.push('tools', 'toolCalling');
654
}
655
if (modelEntry.metadata.capabilities.vision) {
656
capabilityStrings.push('vision');
657
}
658
if (modelEntry.metadata.capabilities.agentMode) {
659
capabilityStrings.push('agent', 'agentMode');
660
}
661
if (modelEntry.metadata.capabilities.editTools) {
662
capabilityStrings.push(...modelEntry.metadata.capabilities.editTools);
663
}
664
665
const capabilityString = capabilityStrings.join(' ');
666
if (capabilityString) {
667
this.capabilityMatches = this.matches(searchValue, capabilityString, or(matchesWords, matchesCamelCase), words);
668
}
669
}
670
}
671
}
672
673
private matches(searchValue: string | null, wordToMatchAgainst: string, wordMatchesFilter: IFilter, words: string[]): IMatch[] | null {
674
let matches = searchValue ? wordFilter(searchValue, wordToMatchAgainst) : null;
675
if (!matches) {
676
matches = this.matchesWords(words, wordToMatchAgainst, wordMatchesFilter);
677
}
678
if (matches) {
679
matches = this.filterAndSort(matches);
680
}
681
return matches;
682
}
683
684
private matchesWords(words: string[], wordToMatchAgainst: string, wordMatchesFilter: IFilter): IMatch[] | null {
685
let matches: IMatch[] | null = [];
686
for (const word of words) {
687
const wordMatches = wordMatchesFilter(word, wordToMatchAgainst);
688
if (wordMatches) {
689
matches = [...(matches || []), ...wordMatches];
690
} else {
691
matches = null;
692
break;
693
}
694
}
695
return matches;
696
}
697
698
private filterAndSort(matches: IMatch[]): IMatch[] {
699
return distinct(matches, (a => a.start + '.' + a.end))
700
.filter(match => !matches.some(m => !(m.start === match.start && m.end === match.end) && (m.start <= match.start && m.end >= match.end)))
701
.sort((a, b) => a.start - b.start);
702
}
703
}
704
705