Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/files/common/explorerFileNestingTrie.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
type FilenameAttributes = {
7
// index.test in index.test.json
8
basename: string;
9
// json in index.test.json
10
extname: string;
11
// my-folder in my-folder/index.test.json
12
dirname: string;
13
};
14
15
/**
16
* A sort of double-ended trie, used to efficiently query for matches to "star" patterns, where
17
* a given key represents a parent and may contain a capturing group ("*"), which can then be
18
* referenced via the token "$(capture)" in associated child patterns.
19
*
20
* The generated tree will have at most two levels, as subtrees are flattened rather than nested.
21
*
22
* Example:
23
* The config: [
24
* [ *.ts , [ $(capture).*.ts ; $(capture).js ] ]
25
* [ *.js , [ $(capture).min.js ] ] ]
26
* Nests the files: [ a.ts ; a.d.ts ; a.js ; a.min.js ; b.ts ; b.min.js ]
27
* As:
28
* - a.ts => [ a.d.ts ; a.js ; a.min.js ]
29
* - b.ts => [ ]
30
* - b.min.ts => [ ]
31
*/
32
export class ExplorerFileNestingTrie {
33
private root = new PreTrie();
34
35
constructor(config: [string, string[]][]) {
36
for (const [parentPattern, childPatterns] of config) {
37
for (const childPattern of childPatterns) {
38
this.root.add(parentPattern, childPattern);
39
}
40
}
41
}
42
43
toString() {
44
return this.root.toString();
45
}
46
47
private getAttributes(filename: string, dirname: string): FilenameAttributes {
48
const lastDot = filename.lastIndexOf('.');
49
if (lastDot < 1) {
50
return {
51
dirname,
52
basename: filename,
53
extname: ''
54
};
55
} else {
56
return {
57
dirname,
58
basename: filename.substring(0, lastDot),
59
extname: filename.substring(lastDot + 1)
60
};
61
}
62
}
63
64
nest(files: string[], dirname: string): Map<string, Set<string>> {
65
const parentFinder = new PreTrie();
66
67
for (const potentialParent of files) {
68
const attributes = this.getAttributes(potentialParent, dirname);
69
const children = this.root.get(potentialParent, attributes);
70
for (const child of children) {
71
parentFinder.add(child, potentialParent);
72
}
73
}
74
75
const findAllRootAncestors = (file: string, seen: Set<string> = new Set()): string[] => {
76
if (seen.has(file)) { return []; }
77
seen.add(file);
78
const attributes = this.getAttributes(file, dirname);
79
const ancestors = parentFinder.get(file, attributes);
80
if (ancestors.length === 0) {
81
return [file];
82
}
83
84
if (ancestors.length === 1 && ancestors[0] === file) {
85
return [file];
86
}
87
88
return ancestors.flatMap(a => findAllRootAncestors(a, seen));
89
};
90
91
const result = new Map<string, Set<string>>();
92
for (const file of files) {
93
let ancestors = findAllRootAncestors(file);
94
if (ancestors.length === 0) { ancestors = [file]; }
95
for (const ancestor of ancestors) {
96
let existing = result.get(ancestor);
97
if (!existing) { result.set(ancestor, existing = new Set()); }
98
if (file !== ancestor) {
99
existing.add(file);
100
}
101
}
102
}
103
return result;
104
}
105
}
106
107
/** Export for test only. */
108
export class PreTrie {
109
private value: SufTrie = new SufTrie();
110
111
private map: Map<string, PreTrie> = new Map();
112
113
constructor() { }
114
115
add(key: string, value: string) {
116
if (key === '') {
117
this.value.add(key, value);
118
} else if (key[0] === '*') {
119
this.value.add(key, value);
120
} else {
121
const head = key[0];
122
const rest = key.slice(1);
123
let existing = this.map.get(head);
124
if (!existing) {
125
this.map.set(head, existing = new PreTrie());
126
}
127
existing.add(rest, value);
128
}
129
}
130
131
get(key: string, attributes: FilenameAttributes): string[] {
132
const results: string[] = [];
133
results.push(...this.value.get(key, attributes));
134
135
const head = key[0];
136
const rest = key.slice(1);
137
const existing = this.map.get(head);
138
if (existing) {
139
results.push(...existing.get(rest, attributes));
140
}
141
142
return results;
143
}
144
145
toString(indentation = ''): string {
146
const lines = [];
147
if (this.value.hasItems) {
148
lines.push('* => \n' + this.value.toString(indentation + ' '));
149
}
150
[...this.map.entries()].map(([key, trie]) =>
151
lines.push('^' + key + ' => \n' + trie.toString(indentation + ' ')));
152
return lines.map(l => indentation + l).join('\n');
153
}
154
}
155
156
/** Export for test only. */
157
export class SufTrie {
158
private star: SubstitutionString[] = [];
159
private epsilon: SubstitutionString[] = [];
160
161
private map: Map<string, SufTrie> = new Map();
162
hasItems: boolean = false;
163
164
constructor() { }
165
166
add(key: string, value: string) {
167
this.hasItems = true;
168
if (key === '*') {
169
this.star.push(new SubstitutionString(value));
170
} else if (key === '') {
171
this.epsilon.push(new SubstitutionString(value));
172
} else {
173
const tail = key[key.length - 1];
174
const rest = key.slice(0, key.length - 1);
175
if (tail === '*') {
176
throw Error('Unexpected star in SufTrie key: ' + key);
177
} else {
178
let existing = this.map.get(tail);
179
if (!existing) {
180
this.map.set(tail, existing = new SufTrie());
181
}
182
existing.add(rest, value);
183
}
184
}
185
}
186
187
get(key: string, attributes: FilenameAttributes): string[] {
188
const results: string[] = [];
189
if (key === '') {
190
results.push(...this.epsilon.map(ss => ss.substitute(attributes)));
191
}
192
if (this.star.length) {
193
results.push(...this.star.map(ss => ss.substitute(attributes, key)));
194
}
195
196
const tail = key[key.length - 1];
197
const rest = key.slice(0, key.length - 1);
198
const existing = this.map.get(tail);
199
if (existing) {
200
results.push(...existing.get(rest, attributes));
201
}
202
203
return results;
204
}
205
206
toString(indentation = ''): string {
207
const lines = [];
208
if (this.star.length) {
209
lines.push('* => ' + this.star.join('; '));
210
}
211
212
if (this.epsilon.length) {
213
// allow-any-unicode-next-line
214
lines.push('ε => ' + this.epsilon.join('; '));
215
}
216
217
[...this.map.entries()].map(([key, trie]) =>
218
lines.push(key + '$' + ' => \n' + trie.toString(indentation + ' ')));
219
220
return lines.map(l => indentation + l).join('\n');
221
}
222
}
223
224
const enum SubstitutionType {
225
capture = 'capture',
226
basename = 'basename',
227
dirname = 'dirname',
228
extname = 'extname',
229
}
230
231
const substitutionStringTokenizer = /\$[({](capture|basename|dirname|extname)[)}]/g;
232
233
class SubstitutionString {
234
235
private tokens: (string | { capture: SubstitutionType })[] = [];
236
237
constructor(pattern: string) {
238
substitutionStringTokenizer.lastIndex = 0;
239
let token;
240
let lastIndex = 0;
241
while (token = substitutionStringTokenizer.exec(pattern)) {
242
const prefix = pattern.slice(lastIndex, token.index);
243
this.tokens.push(prefix);
244
245
const type = token[1];
246
switch (type) {
247
case SubstitutionType.basename:
248
case SubstitutionType.dirname:
249
case SubstitutionType.extname:
250
case SubstitutionType.capture:
251
this.tokens.push({ capture: type });
252
break;
253
default: throw Error('unknown substitution type: ' + type);
254
}
255
lastIndex = token.index + token[0].length;
256
}
257
258
if (lastIndex !== pattern.length) {
259
const suffix = pattern.slice(lastIndex, pattern.length);
260
this.tokens.push(suffix);
261
}
262
}
263
264
substitute(attributes: FilenameAttributes, capture?: string): string {
265
return this.tokens.map(t => {
266
if (typeof t === 'string') { return t; }
267
switch (t.capture) {
268
case SubstitutionType.basename: return attributes.basename;
269
case SubstitutionType.dirname: return attributes.dirname;
270
case SubstitutionType.extname: return attributes.extname;
271
case SubstitutionType.capture: return capture || '';
272
}
273
}).join('');
274
}
275
}
276
277