Path: blob/main/src/vs/workbench/contrib/files/common/explorerFileNestingTrie.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45type FilenameAttributes = {6// index.test in index.test.json7basename: string;8// json in index.test.json9extname: string;10// my-folder in my-folder/index.test.json11dirname: string;12};1314/**15* A sort of double-ended trie, used to efficiently query for matches to "star" patterns, where16* a given key represents a parent and may contain a capturing group ("*"), which can then be17* referenced via the token "$(capture)" in associated child patterns.18*19* The generated tree will have at most two levels, as subtrees are flattened rather than nested.20*21* Example:22* The config: [23* [ *.ts , [ $(capture).*.ts ; $(capture).js ] ]24* [ *.js , [ $(capture).min.js ] ] ]25* Nests the files: [ a.ts ; a.d.ts ; a.js ; a.min.js ; b.ts ; b.min.js ]26* As:27* - a.ts => [ a.d.ts ; a.js ; a.min.js ]28* - b.ts => [ ]29* - b.min.ts => [ ]30*/31export class ExplorerFileNestingTrie {32private root = new PreTrie();3334constructor(config: [string, string[]][]) {35for (const [parentPattern, childPatterns] of config) {36for (const childPattern of childPatterns) {37this.root.add(parentPattern, childPattern);38}39}40}4142toString() {43return this.root.toString();44}4546private getAttributes(filename: string, dirname: string): FilenameAttributes {47const lastDot = filename.lastIndexOf('.');48if (lastDot < 1) {49return {50dirname,51basename: filename,52extname: ''53};54} else {55return {56dirname,57basename: filename.substring(0, lastDot),58extname: filename.substring(lastDot + 1)59};60}61}6263nest(files: string[], dirname: string): Map<string, Set<string>> {64const parentFinder = new PreTrie();6566for (const potentialParent of files) {67const attributes = this.getAttributes(potentialParent, dirname);68const children = this.root.get(potentialParent, attributes);69for (const child of children) {70parentFinder.add(child, potentialParent);71}72}7374const findAllRootAncestors = (file: string, seen: Set<string> = new Set()): string[] => {75if (seen.has(file)) { return []; }76seen.add(file);77const attributes = this.getAttributes(file, dirname);78const ancestors = parentFinder.get(file, attributes);79if (ancestors.length === 0) {80return [file];81}8283if (ancestors.length === 1 && ancestors[0] === file) {84return [file];85}8687return ancestors.flatMap(a => findAllRootAncestors(a, seen));88};8990const result = new Map<string, Set<string>>();91for (const file of files) {92let ancestors = findAllRootAncestors(file);93if (ancestors.length === 0) { ancestors = [file]; }94for (const ancestor of ancestors) {95let existing = result.get(ancestor);96if (!existing) { result.set(ancestor, existing = new Set()); }97if (file !== ancestor) {98existing.add(file);99}100}101}102return result;103}104}105106/** Export for test only. */107export class PreTrie {108private value: SufTrie = new SufTrie();109110private map: Map<string, PreTrie> = new Map();111112constructor() { }113114add(key: string, value: string) {115if (key === '') {116this.value.add(key, value);117} else if (key[0] === '*') {118this.value.add(key, value);119} else {120const head = key[0];121const rest = key.slice(1);122let existing = this.map.get(head);123if (!existing) {124this.map.set(head, existing = new PreTrie());125}126existing.add(rest, value);127}128}129130get(key: string, attributes: FilenameAttributes): string[] {131const results: string[] = [];132results.push(...this.value.get(key, attributes));133134const head = key[0];135const rest = key.slice(1);136const existing = this.map.get(head);137if (existing) {138results.push(...existing.get(rest, attributes));139}140141return results;142}143144toString(indentation = ''): string {145const lines = [];146if (this.value.hasItems) {147lines.push('* => \n' + this.value.toString(indentation + ' '));148}149[...this.map.entries()].map(([key, trie]) =>150lines.push('^' + key + ' => \n' + trie.toString(indentation + ' ')));151return lines.map(l => indentation + l).join('\n');152}153}154155/** Export for test only. */156export class SufTrie {157private star: SubstitutionString[] = [];158private epsilon: SubstitutionString[] = [];159160private map: Map<string, SufTrie> = new Map();161hasItems: boolean = false;162163constructor() { }164165add(key: string, value: string) {166this.hasItems = true;167if (key === '*') {168this.star.push(new SubstitutionString(value));169} else if (key === '') {170this.epsilon.push(new SubstitutionString(value));171} else {172const tail = key[key.length - 1];173const rest = key.slice(0, key.length - 1);174if (tail === '*') {175throw Error('Unexpected star in SufTrie key: ' + key);176} else {177let existing = this.map.get(tail);178if (!existing) {179this.map.set(tail, existing = new SufTrie());180}181existing.add(rest, value);182}183}184}185186get(key: string, attributes: FilenameAttributes): string[] {187const results: string[] = [];188if (key === '') {189results.push(...this.epsilon.map(ss => ss.substitute(attributes)));190}191if (this.star.length) {192results.push(...this.star.map(ss => ss.substitute(attributes, key)));193}194195const tail = key[key.length - 1];196const rest = key.slice(0, key.length - 1);197const existing = this.map.get(tail);198if (existing) {199results.push(...existing.get(rest, attributes));200}201202return results;203}204205toString(indentation = ''): string {206const lines = [];207if (this.star.length) {208lines.push('* => ' + this.star.join('; '));209}210211if (this.epsilon.length) {212// allow-any-unicode-next-line213lines.push('ε => ' + this.epsilon.join('; '));214}215216[...this.map.entries()].map(([key, trie]) =>217lines.push(key + '$' + ' => \n' + trie.toString(indentation + ' ')));218219return lines.map(l => indentation + l).join('\n');220}221}222223const enum SubstitutionType {224capture = 'capture',225basename = 'basename',226dirname = 'dirname',227extname = 'extname',228}229230const substitutionStringTokenizer = /\$[({](capture|basename|dirname|extname)[)}]/g;231232class SubstitutionString {233234private tokens: (string | { capture: SubstitutionType })[] = [];235236constructor(pattern: string) {237substitutionStringTokenizer.lastIndex = 0;238let token;239let lastIndex = 0;240while (token = substitutionStringTokenizer.exec(pattern)) {241const prefix = pattern.slice(lastIndex, token.index);242this.tokens.push(prefix);243244const type = token[1];245switch (type) {246case SubstitutionType.basename:247case SubstitutionType.dirname:248case SubstitutionType.extname:249case SubstitutionType.capture:250this.tokens.push({ capture: type });251break;252default: throw Error('unknown substitution type: ' + type);253}254lastIndex = token.index + token[0].length;255}256257if (lastIndex !== pattern.length) {258const suffix = pattern.slice(lastIndex, pattern.length);259this.tokens.push(suffix);260}261}262263substitute(attributes: FilenameAttributes, capture?: string): string {264return this.tokens.map(t => {265if (typeof t === 'string') { return t; }266switch (t.capture) {267case SubstitutionType.basename: return attributes.basename;268case SubstitutionType.dirname: return attributes.dirname;269case SubstitutionType.extname: return attributes.extname;270case SubstitutionType.capture: return capture || '';271}272}).join('');273}274}275276277