Skip to content

Instantly share code, notes, and snippets.

@bxb100
Last active May 27, 2023 02:12
Show Gist options
  • Save bxb100/66d3a54262f5e3163c1fcccf26ce2335 to your computer and use it in GitHub Desktop.
Save bxb100/66d3a54262f5e3163c1fcccf26ce2335 to your computer and use it in GitHub Desktop.

Revisions

  1. bxb100 revised this gist May 27, 2023. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -35,6 +35,14 @@ IDEA 也有类似的 nest 机制[^1] 但是很显然它的规则及其简单:

    简单容易就能搜索到一个实现 https://grep.app/search?q=ProjectViewNestingRulesProvider

    ### 其它有用的链接

    * https://github.com/pavankjadda/intellij-foldable-project-view
    * https://github.com/antfu/vscode-file-nesting-config
    * https://dzhavat.github.io/2022/02/07/file-nesting-in-vs-code.html
    * https://code.visualstudio.com/updates/v1_67#_explorer-file-nesting
    * https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/files/common/explorerFileNestingTrie.ts#L131
    * https://plugins.jetbrains.com/docs/intellij/tool-windows.html


    [^1]: https://www.jetbrains.com/help/idea/2023.1/file-nesting-dialog.html?utm_source=product&utm_medium=link&utm_campaign=IU&utm_content=2023.1
  2. bxb100 revised this gist May 27, 2023. 1 changed file with 9 additions and 0 deletions.
    9 changes: 9 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,6 @@
    IDEA 也有类似的 nest 机制[^1] 但是很显然它的规则及其简单:

    `com.intellij.ide.projectView.impl.FileNestingBuilder#checkMatchingAsParentOrChild`

    ```java

    @@ -22,9 +23,17 @@ IDEA 也有类似的 nest 机制[^1] 但是很显然它的规则及其简单:

    return Couple.of(matchesParent, matchesChild);
    }
    ```

    并且 IDEA 提供了拓展

    ```xml
    <extensions defaultExtensionNs="com.intellij">
        <projectViewNestingRulesProvider implementation="my.package.MyRulesProvider"/>
    </extensions>
    ```

    简单容易就能搜索到一个实现 https://grep.app/search?q=ProjectViewNestingRulesProvider



  3. bxb100 created this gist May 27, 2023.
    275 changes: 275 additions & 0 deletions ExplorerFileNestingTrie.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,275 @@
    /*---------------------------------------------------------------------------------------------
    * Copyright (c) Microsoft Corporation. All rights reserved.
    * Licensed under the MIT License. See License.txt in the project root for license information.
    *--------------------------------------------------------------------------------------------*/

    type FilenameAttributes = {
    // index.test in index.test.json
    basename: string;
    // json in index.test.json
    extname: string;
    // my-folder in my-folder/index.test.json
    dirname: string;
    };

    /**
    * A sort of double-ended trie, used to efficiently query for matches to "star" patterns, where
    * a given key represents a parent and may contain a capturing group ("*"), which can then be
    * referenced via the token "$(capture)" in associated child patterns.
    *
    * The generated tree will have at most two levels, as subtrees are flattened rather than nested.
    *
    * Example:
    * The config: [
    * [ *.ts , [ $(capture).*.ts ; $(capture).js ] ]
    * [ *.js , [ $(capture).min.js ] ] ]
    * Nests the files: [ a.ts ; a.d.ts ; a.js ; a.min.js ; b.ts ; b.min.js ]
    * As:
    * - a.ts => [ a.d.ts ; a.js ; a.min.js ]
    * - b.ts => [ ]
    * - b.min.ts => [ ]
    */
    export class ExplorerFileNestingTrie {
    private root = new PreTrie();

    constructor(config: [string, string[]][]) {
    for (const [parentPattern, childPatterns] of config) {
    for (const childPattern of childPatterns) {
    this.root.add(parentPattern, childPattern);
    }
    }
    }

    toString() {
    return this.root.toString();
    }

    private getAttributes(filename: string, dirname: string): FilenameAttributes {
    const lastDot = filename.lastIndexOf('.');
    if (lastDot < 1) {
    return {
    dirname,
    basename: filename,
    extname: ''
    };
    } else {
    return {
    dirname,
    basename: filename.substring(0, lastDot),
    extname: filename.substring(lastDot + 1)
    };
    }
    }

    nest(files: string[], dirname: string): Map<string, Set<string>> {
    const parentFinder = new PreTrie();

    for (const potentialParent of files) {
    const attributes = this.getAttributes(potentialParent, dirname);
    const children = this.root.get(potentialParent, attributes);
    for (const child of children) {
    parentFinder.add(child, potentialParent);
    }
    }

    const findAllRootAncestors = (file: string, seen: Set<string> = new Set()): string[] => {
    if (seen.has(file)) { return []; }
    seen.add(file);
    const attributes = this.getAttributes(file, dirname);
    const ancestors = parentFinder.get(file, attributes);
    if (ancestors.length === 0) {
    return [file];
    }

    if (ancestors.length === 1 && ancestors[0] === file) {
    return [file];
    }

    return ancestors.flatMap(a => findAllRootAncestors(a, seen));
    };

    const result = new Map<string, Set<string>>();
    for (const file of files) {
    let ancestors = findAllRootAncestors(file);
    if (ancestors.length === 0) { ancestors = [file]; }
    for (const ancestor of ancestors) {
    let existing = result.get(ancestor);
    if (!existing) { result.set(ancestor, existing = new Set()); }
    if (file !== ancestor) {
    existing.add(file);
    }
    }
    }
    return result;
    }
    }

    /** Export for test only. */
    export class PreTrie {
    private value: SufTrie = new SufTrie();

    private map: Map<string, PreTrie> = new Map();

    constructor() { }

    add(key: string, value: string) {
    if (key === '') {
    this.value.add(key, value);
    } else if (key[0] === '*') {
    this.value.add(key, value);
    } else {
    const head = key[0];
    const rest = key.slice(1);
    let existing = this.map.get(head);
    if (!existing) {
    this.map.set(head, existing = new PreTrie());
    }
    existing.add(rest, value);
    }
    }

    get(key: string, attributes: FilenameAttributes): string[] {
    const results: string[] = [];
    results.push(...this.value.get(key, attributes));

    const head = key[0];
    const rest = key.slice(1);
    const existing = this.map.get(head);
    if (existing) {
    results.push(...existing.get(rest, attributes));
    }

    return results;
    }

    toString(indentation = ''): string {
    const lines = [];
    if (this.value.hasItems) {
    lines.push('* => \n' + this.value.toString(indentation + ' '));
    }
    [...this.map.entries()].map(([key, trie]) =>
    lines.push('^' + key + ' => \n' + trie.toString(indentation + ' ')));
    return lines.map(l => indentation + l).join('\n');
    }
    }

    /** Export for test only. */
    export class SufTrie {
    private star: SubstitutionString[] = [];
    private epsilon: SubstitutionString[] = [];

    private map: Map<string, SufTrie> = new Map();
    hasItems: boolean = false;

    constructor() { }

    add(key: string, value: string) {
    this.hasItems = true;
    if (key === '*') {
    this.star.push(new SubstitutionString(value));
    } else if (key === '') {
    this.epsilon.push(new SubstitutionString(value));
    } else {
    const tail = key[key.length - 1];
    const rest = key.slice(0, key.length - 1);
    if (tail === '*') {
    throw Error('Unexpected star in SufTrie key: ' + key);
    } else {
    let existing = this.map.get(tail);
    if (!existing) {
    this.map.set(tail, existing = new SufTrie());
    }
    existing.add(rest, value);
    }
    }
    }

    get(key: string, attributes: FilenameAttributes): string[] {
    const results: string[] = [];
    if (key === '') {
    results.push(...this.epsilon.map(ss => ss.substitute(attributes)));
    }
    if (this.star.length) {
    results.push(...this.star.map(ss => ss.substitute(attributes, key)));
    }

    const tail = key[key.length - 1];
    const rest = key.slice(0, key.length - 1);
    const existing = this.map.get(tail);
    if (existing) {
    results.push(...existing.get(rest, attributes));
    }

    return results;
    }

    toString(indentation = ''): string {
    const lines = [];
    if (this.star.length) {
    lines.push('* => ' + this.star.join('; '));
    }

    if (this.epsilon.length) {
    // allow-any-unicode-next-line
    lines.push('ε => ' + this.epsilon.join('; '));
    }

    [...this.map.entries()].map(([key, trie]) =>
    lines.push(key + '$' + ' => \n' + trie.toString(indentation + ' ')));

    return lines.map(l => indentation + l).join('\n');
    }
    }

    const enum SubstitutionType {
    capture = 'capture',
    basename = 'basename',
    dirname = 'dirname',
    extname = 'extname',
    }

    const substitutionStringTokenizer = /\$[({](capture|basename|dirname|extname)[)}]/g;

    class SubstitutionString {

    private tokens: (string | { capture: SubstitutionType })[] = [];

    constructor(pattern: string) {
    substitutionStringTokenizer.lastIndex = 0;
    let token;
    let lastIndex = 0;
    while (token = substitutionStringTokenizer.exec(pattern)) {
    const prefix = pattern.slice(lastIndex, token.index);
    this.tokens.push(prefix);

    const type = token[1];
    switch (type) {
    case SubstitutionType.basename:
    case SubstitutionType.dirname:
    case SubstitutionType.extname:
    case SubstitutionType.capture:
    this.tokens.push({ capture: type });
    break;
    default: throw Error('unknown substitution type: ' + type);
    }
    lastIndex = token.index + token[0].length;
    }

    if (lastIndex !== pattern.length) {
    const suffix = pattern.slice(lastIndex, pattern.length);
    this.tokens.push(suffix);
    }
    }

    substitute(attributes: FilenameAttributes, capture?: string): string {
    return this.tokens.map(t => {
    if (typeof t === 'string') { return t; }
    switch (t.capture) {
    case SubstitutionType.basename: return attributes.basename;
    case SubstitutionType.dirname: return attributes.dirname;
    case SubstitutionType.extname: return attributes.extname;
    case SubstitutionType.capture: return capture || '';
    }
    }).join('');
    }
    }
    31 changes: 31 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,31 @@
    IDEA 也有类似的 nest 机制[^1] 但是很显然它的规则及其简单:


    ```java

    public static Couple<Boolean> checkMatchingAsParentOrChild(@NotNull final ProjectViewFileNestingService.NestingRule rule,
    @NotNull final String fileName) {
    String parentFileSuffix = rule.getParentFileSuffix();
    String childFileSuffix = rule.getChildFileSuffix();

    boolean matchesParent = /*!fileName.equalsIgnoreCase(parentFileSuffix) &&*/ StringUtil.endsWithIgnoreCase(fileName, parentFileSuffix);
    boolean matchesChild = /*!fileName.equalsIgnoreCase(childFileSuffix) &&*/ StringUtil.endsWithIgnoreCase(fileName, childFileSuffix);

    if (matchesParent && matchesChild) {
    if (parentFileSuffix.length() > childFileSuffix.length()) {
    matchesChild = false;
    }
    else {
    matchesParent = false;
    }
    }

    return Couple.of(matchesParent, matchesChild);
    }

    ```




    [^1]: https://www.jetbrains.com/help/idea/2023.1/file-nesting-dialog.html?utm_source=product&utm_medium=link&utm_campaign=IU&utm_content=2023.1