import { debounce } from "debounce" ; import * as fs from "fs" ; import * as vscode from "vscode" ; import { setsAreEqual } from "./sets" ; import { FileNode , fileNodeSort } from "./tag-tree/file-node" ; import { TagNode , tagNodeSort } from "./tag-tree/tag-node" ; import { TagTree } from "./tag-tree/tag-tree" ; import * as grayMatter from "gray-matter" ; import { Uri } from "vscode" ; import * as path from "path" ; interface IFileInfo { tags : Set < string > ; filePath : string ; } class TagTreeDataProvider implements vscode . TreeDataProvider < TagNode | FileNode > { private tagTree : TagTree ; private _onDidChangeTreeData : vscode . EventEmitter < TagNode | FileNode | null > = new vscode . EventEmitter < TagNode | FileNode | null > ( ) ; readonly onDidChangeTreeData : vscode . Event < TagNode | FileNode | null > = this . _onDidChangeTreeData . event ; constructor ( ) { vscode . workspace . onDidChangeTextDocument ( debounce ( ( e ) => this . onDocumentChanged ( e ) , 500 ) ) ; vscode . workspace . onWillSaveTextDocument ( e => { this . onWillSaveTextDocument ( e ) ; } ) ; const additionalFileTypes = vscode . workspace . getConfiguration ( ) . get ( "vscode-nested-tags.additionalFileTypes" ) ; const customGlobPattern = additionalFileTypes . length > 0 ? `,${ additionalFileTypes . join ( "," ) }` : "" ; const globPattern = `{md${ customGlobPattern }}` ; const fileWatcher = vscode . workspace . createFileSystemWatcher ( "**/*" ) ; fileWatcher . onDidDelete ( ( ... args ) => this . onDidDelete ( ... args ) ) ; fileWatcher . onDidCreate ( ( ... args ) => this . onDidCreate ( ... args ) ) ; this . tagTree = new TagTree ( ) ; ( async ( ) => { const uris = await vscode . workspace . findFiles ( `**/*.${ globPattern }` ) ; const infos = await Promise . all ( uris . map ( uri => this . getTagsFromFileOnFileSystem ( uri . fsPath ) ) ) ; infos . filter ( info => info . tags . size > 0 ) . forEach ( info => { const displayName = this . getPathRelativeToWorkspaceFolder ( Uri . file ( info . filePath ) ) ; this . tagTree . addFile ( info . filePath , [ ... info . tags ] , displayName ) ; } ) ; this . _onDidChangeTreeData . fire ( ) ; } ) ( ) ; } param { ( TagNode | FileNode ) } element returns memberof public getChildren ( element ) { if ( element instanceof FileNode ) { return [ ] ; } else if ( element === undefined ) { const children = [ ... [ ... this . tagTree . root . tags . values ( ) ] . sort ( tagNodeSort ) , ... [ ... this . tagTree . root . files . values ( ) ] . sort ( fileNodeSort ) ] ; return children ; } else { const children = [ ... [ ... element . tags . values ( ) ] . sort ( tagNodeSort ) , ... [ ... element . files . values ( ) ] . sort ( fileNodeSort ) ] ; return children ; } } param { ( TagNode | FileNode ) } element returns { vscode . TreeItem } memberof public getTreeItem ( element ) { const tagTreeNode = this . tagTree . getNode ( element . pathToNode ) ; const { displayName } = tagTreeNode ; const isFile = tagTreeNode instanceof FileNode ; const collapsibleState = isFile ? vscode . TreeItemCollapsibleState . None : vscode . TreeItemCollapsibleState . Collapsed ; const result = new vscode . TreeItem ( displayName , collapsibleState ) ; if ( isFile ) { result . command = { arguments : [ vscode . Uri . file ( tagTreeNode . filePath ) ] , command : "vscode.open" , title : "Jump to tag reference" } ; } return result ; } param changeEvent private async onWillSaveTextDocument ( changeEvent ) { if ( changeEvent . document . isDirty && this . matchesWatchedFileExtensions ( changeEvent . document . uri ) ) { const filePath = changeEvent . document . fileName ; const fileInfo = await this . getTagsFromFileOnFileSystem ( filePath ) ; const tagsInTreeForFile = this . tagTree . getTagsForFile ( filePath ) ; this . updateTreeForFile ( filePath , tagsInTreeForFile , fileInfo . tags ) ; } } param changeEvent private onDocumentChanged ( changeEvent ) { const filePath = changeEvent . document . fileName ; if ( filePath !== undefined && this . matchesWatchedFileExtensions ( changeEvent . document . uri ) ) { const fileInfo = this . getTagsFromFileText ( changeEvent . document . getText ( ) , filePath ) ; const tagsInTreeForFile = this . tagTree . getTagsForFile ( filePath ) ; const isUpdateNeeded = ! setsAreEqual ( fileInfo . tags , tagsInTreeForFile ) ; if ( isUpdateNeeded ) { this . tagTree . deleteFile ( filePath ) ; const displayName = this . getPathRelativeToWorkspaceFolder ( Uri . file ( filePath ) ) ; this . tagTree . addFile ( filePath , [ ... fileInfo . tags . values ( ) ] , displayName ) ; this . _onDidChangeTreeData . fire ( ) ; } } } private async onDidDelete ( fileUri ) { this . tagTree . deleteFile ( fileUri . fsPath ) ; this . _onDidChangeTreeData . fire ( ) ; } private async onDidCreate ( fileUri ) { if ( ! fs . lstatSync ( fileUri . fsPath ) . isDirectory ( ) ) { const displayName = this . getPathRelativeToWorkspaceFolder ( fileUri ) ; const fileInfo = await this . getTagsFromFileOnFileSystem ( fileUri . fsPath ) ; this . tagTree . addFile ( fileUri . fsPath , [ ... fileInfo . tags . values ( ) ] , displayName ) ; this . _onDidChangeTreeData . fire ( ) ; } } param filePath param tagsBefore param tagsAfter private updateTreeForFile ( filePath , tagsBefore , tagsAfter ) { const isUpdateNeeded = ! setsAreEqual ( tagsBefore , tagsAfter ) ; if ( isUpdateNeeded ) { this . tagTree . deleteFile ( filePath ) ; const displayName = this . getPathRelativeToWorkspaceFolder ( Uri . file ( filePath ) ) ; this . tagTree . addFile ( filePath , [ ... tagsAfter . values ( ) ] , displayName ) ; this . _onDidChangeTreeData . fire ( ) ; } } param fileContents param filePath private getTagsFromFileText ( fileContents , filePath ) { const { data } = grayMatter ( fileContents ) ; let yamlTags = new Set ( ) ; if ( data . tags ) { yamlTags = new Set ( [ data . tags ] ) ; } return fileContents . split ( "\n" ) . reduce ( ( accumulator , currentLine ) => { if ( currentLine . includes ( "@nested-tags:" ) ) { const tagsToAdd = currentLine . split ( "@nested-tags:" ) . pop ( ) . replace ( "-->" , "" ) . replace ( "*/" , "" ) . split ( "," ) ; return { ... accumulator , tags : new Set ( [ ... accumulator . tags , ... tagsToAdd ] ) } ; } return accumulator ; } , { tags : new Set ( ... yamlTags ) , filePath } ) ; } param filePath private async getTagsFromFileOnFileSystem ( filePath ) { const buffer = await fs . promises . readFile ( filePath ) ; return this . getTagsFromFileText ( buffer . toString ( ) , filePath ) ; } param uri private getPathRelativeToWorkspaceFolder ( uri ) { const currentWorkspaceFolder = vscode . workspace . getWorkspaceFolder ( uri ) ; const relativePath = typeof currentWorkspaceFolder !== "undefined" ? path . relative ( currentWorkspaceFolder . uri . path , uri . path ) : uri . path ; return relativePath ; } param uri private matchesWatchedFileExtensions ( uri ) { const supportedFileExtensions = new Set ( [ "md" , ... vscode . workspace . getConfiguration ( ) . get ( "vscode-nested-tags.additionalFileTypes" ) ] ) ; const fileExtension = uri . fsPath . split ( "." ) . pop ( ) ; return supportedFileExtensions . has ( fileExtension ) ; } } export { TagTreeDataProvider } ;