Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/common/comparers.ts
3291 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 { safeIntl } from './date.js';
7
import { Lazy } from './lazy.js';
8
import { sep } from './path.js';
9
10
// When comparing large numbers of strings it's better for performance to create an
11
// Intl.Collator object and use the function provided by its compare property
12
// than it is to use String.prototype.localeCompare()
13
14
// A collator with numeric sorting enabled, and no sensitivity to case, accents or diacritics.
15
const intlFileNameCollatorBaseNumeric: Lazy<{ collator: Intl.Collator; collatorIsNumeric: boolean }> = new Lazy(() => {
16
const collator = safeIntl.Collator(undefined, { numeric: true, sensitivity: 'base' }).value;
17
return {
18
collator,
19
collatorIsNumeric: collator.resolvedOptions().numeric
20
};
21
});
22
23
// A collator with numeric sorting enabled.
24
const intlFileNameCollatorNumeric: Lazy<{ collator: Intl.Collator }> = new Lazy(() => {
25
const collator = safeIntl.Collator(undefined, { numeric: true }).value;
26
return {
27
collator
28
};
29
});
30
31
// A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case.
32
const intlFileNameCollatorNumericCaseInsensitive: Lazy<{ collator: Intl.Collator }> = new Lazy(() => {
33
const collator = safeIntl.Collator(undefined, { numeric: true, sensitivity: 'accent' }).value;
34
return {
35
collator
36
};
37
});
38
39
/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */
40
export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
41
const a = one || '';
42
const b = other || '';
43
const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b);
44
45
// Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate.
46
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) {
47
return a < b ? -1 : 1;
48
}
49
50
return result;
51
}
52
53
/** Compares full filenames without grouping by case. */
54
export function compareFileNamesDefault(one: string | null, other: string | null): number {
55
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
56
one = one || '';
57
other = other || '';
58
59
return compareAndDisambiguateByLength(collatorNumeric, one, other);
60
}
61
62
/** Compares full filenames grouping uppercase names before lowercase. */
63
export function compareFileNamesUpper(one: string | null, other: string | null) {
64
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
65
one = one || '';
66
other = other || '';
67
68
return compareCaseUpperFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
69
}
70
71
/** Compares full filenames grouping lowercase names before uppercase. */
72
export function compareFileNamesLower(one: string | null, other: string | null) {
73
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
74
one = one || '';
75
other = other || '';
76
77
return compareCaseLowerFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
78
}
79
80
/** Compares full filenames by unicode value. */
81
export function compareFileNamesUnicode(one: string | null, other: string | null) {
82
one = one || '';
83
other = other || '';
84
85
if (one === other) {
86
return 0;
87
}
88
89
return one < other ? -1 : 1;
90
}
91
92
/** Compares filenames by extension, then by name. Disambiguates by unicode comparison. */
93
export function compareFileExtensions(one: string | null, other: string | null): number {
94
const [oneName, oneExtension] = extractNameAndExtension(one);
95
const [otherName, otherExtension] = extractNameAndExtension(other);
96
97
let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension);
98
99
if (result === 0) {
100
// Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate.
101
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) {
102
return oneExtension < otherExtension ? -1 : 1;
103
}
104
105
// Extensions are equal, compare filenames
106
result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneName, otherName);
107
108
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && oneName !== otherName) {
109
return oneName < otherName ? -1 : 1;
110
}
111
}
112
113
return result;
114
}
115
116
/** Compares filenames by extension, then by full filename. Mixes uppercase and lowercase names together. */
117
export function compareFileExtensionsDefault(one: string | null, other: string | null): number {
118
one = one || '';
119
other = other || '';
120
const oneExtension = extractExtension(one);
121
const otherExtension = extractExtension(other);
122
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
123
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;
124
125
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
126
compareAndDisambiguateByLength(collatorNumeric, one, other);
127
}
128
129
/** Compares filenames by extension, then case, then full filename. Groups uppercase names before lowercase. */
130
export function compareFileExtensionsUpper(one: string | null, other: string | null): number {
131
one = one || '';
132
other = other || '';
133
const oneExtension = extractExtension(one);
134
const otherExtension = extractExtension(other);
135
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
136
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;
137
138
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
139
compareCaseUpperFirst(one, other) ||
140
compareAndDisambiguateByLength(collatorNumeric, one, other);
141
}
142
143
/** Compares filenames by extension, then case, then full filename. Groups lowercase names before uppercase. */
144
export function compareFileExtensionsLower(one: string | null, other: string | null): number {
145
one = one || '';
146
other = other || '';
147
const oneExtension = extractExtension(one);
148
const otherExtension = extractExtension(other);
149
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
150
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;
151
152
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
153
compareCaseLowerFirst(one, other) ||
154
compareAndDisambiguateByLength(collatorNumeric, one, other);
155
}
156
157
/** Compares filenames by case-insensitive extension unicode value, then by full filename unicode value. */
158
export function compareFileExtensionsUnicode(one: string | null, other: string | null) {
159
one = one || '';
160
other = other || '';
161
const oneExtension = extractExtension(one).toLowerCase();
162
const otherExtension = extractExtension(other).toLowerCase();
163
164
// Check for extension differences
165
if (oneExtension !== otherExtension) {
166
return oneExtension < otherExtension ? -1 : 1;
167
}
168
169
// Check for full filename differences.
170
if (one !== other) {
171
return one < other ? -1 : 1;
172
}
173
174
return 0;
175
}
176
177
const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
178
179
/** Extracts the name and extension from a full filename, with optional special handling for dotfiles */
180
function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] {
181
const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
182
183
let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || ''];
184
185
// if the dotfilesAsNames option is selected, treat an empty filename with an extension
186
// or a filename that starts with a dot, as a dotfile name
187
if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) {
188
result = [result[0] + '.' + result[1], ''];
189
}
190
191
return result;
192
}
193
194
/** Extracts the extension from a full filename. Treats dotfiles as names, not extensions. */
195
function extractExtension(str?: string | null): string {
196
const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
197
198
return (match && match[1] && match[1].charAt(0) !== '.' && match[3]) || '';
199
}
200
201
function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) {
202
// Check for differences
203
const result = collator.compare(one, other);
204
if (result !== 0) {
205
return result;
206
}
207
208
// In a numeric comparison, `foo1` and `foo01` will compare as equivalent.
209
// Disambiguate by sorting the shorter string first.
210
if (one.length !== other.length) {
211
return one.length < other.length ? -1 : 1;
212
}
213
214
return 0;
215
}
216
217
/** @returns `true` if the string is starts with a lowercase letter. Otherwise, `false`. */
218
function startsWithLower(string: string) {
219
const character = string.charAt(0);
220
221
return (character.toLocaleUpperCase() !== character) ? true : false;
222
}
223
224
/** @returns `true` if the string starts with an uppercase letter. Otherwise, `false`. */
225
function startsWithUpper(string: string) {
226
const character = string.charAt(0);
227
228
return (character.toLocaleLowerCase() !== character) ? true : false;
229
}
230
231
/**
232
* Compares the case of the provided strings - lowercase before uppercase
233
*
234
* @returns
235
* ```text
236
* -1 if one is lowercase and other is uppercase
237
* 1 if one is uppercase and other is lowercase
238
* 0 otherwise
239
* ```
240
*/
241
function compareCaseLowerFirst(one: string, other: string): number {
242
if (startsWithLower(one) && startsWithUpper(other)) {
243
return -1;
244
}
245
return (startsWithUpper(one) && startsWithLower(other)) ? 1 : 0;
246
}
247
248
/**
249
* Compares the case of the provided strings - uppercase before lowercase
250
*
251
* @returns
252
* ```text
253
* -1 if one is uppercase and other is lowercase
254
* 1 if one is lowercase and other is uppercase
255
* 0 otherwise
256
* ```
257
*/
258
function compareCaseUpperFirst(one: string, other: string): number {
259
if (startsWithUpper(one) && startsWithLower(other)) {
260
return -1;
261
}
262
return (startsWithLower(one) && startsWithUpper(other)) ? 1 : 0;
263
}
264
265
function comparePathComponents(one: string, other: string, caseSensitive = false): number {
266
if (!caseSensitive) {
267
one = one && one.toLowerCase();
268
other = other && other.toLowerCase();
269
}
270
271
if (one === other) {
272
return 0;
273
}
274
275
return one < other ? -1 : 1;
276
}
277
278
export function comparePaths(one: string, other: string, caseSensitive = false): number {
279
const oneParts = one.split(sep);
280
const otherParts = other.split(sep);
281
282
const lastOne = oneParts.length - 1;
283
const lastOther = otherParts.length - 1;
284
let endOne: boolean, endOther: boolean;
285
286
for (let i = 0; ; i++) {
287
endOne = lastOne === i;
288
endOther = lastOther === i;
289
290
if (endOne && endOther) {
291
return compareFileNames(oneParts[i], otherParts[i], caseSensitive);
292
} else if (endOne) {
293
return -1;
294
} else if (endOther) {
295
return 1;
296
}
297
298
const result = comparePathComponents(oneParts[i], otherParts[i], caseSensitive);
299
300
if (result !== 0) {
301
return result;
302
}
303
}
304
}
305
306
export function compareAnything(one: string, other: string, lookFor: string): number {
307
const elementAName = one.toLowerCase();
308
const elementBName = other.toLowerCase();
309
310
// Sort prefix matches over non prefix matches
311
const prefixCompare = compareByPrefix(one, other, lookFor);
312
if (prefixCompare) {
313
return prefixCompare;
314
}
315
316
// Sort suffix matches over non suffix matches
317
const elementASuffixMatch = elementAName.endsWith(lookFor);
318
const elementBSuffixMatch = elementBName.endsWith(lookFor);
319
if (elementASuffixMatch !== elementBSuffixMatch) {
320
return elementASuffixMatch ? -1 : 1;
321
}
322
323
// Understand file names
324
const r = compareFileNames(elementAName, elementBName);
325
if (r !== 0) {
326
return r;
327
}
328
329
// Compare by name
330
return elementAName.localeCompare(elementBName);
331
}
332
333
export function compareByPrefix(one: string, other: string, lookFor: string): number {
334
const elementAName = one.toLowerCase();
335
const elementBName = other.toLowerCase();
336
337
// Sort prefix matches over non prefix matches
338
const elementAPrefixMatch = elementAName.startsWith(lookFor);
339
const elementBPrefixMatch = elementBName.startsWith(lookFor);
340
if (elementAPrefixMatch !== elementBPrefixMatch) {
341
return elementAPrefixMatch ? -1 : 1;
342
}
343
344
// Same prefix: Sort shorter matches to the top to have those on top that match more precisely
345
else if (elementAPrefixMatch && elementBPrefixMatch) {
346
if (elementAName.length < elementBName.length) {
347
return -1;
348
}
349
350
if (elementAName.length > elementBName.length) {
351
return 1;
352
}
353
}
354
355
return 0;
356
}
357
358