import * as pathlib from "path" ; import { IFileMeta , IDirectory , IDirEntryMeta , getFlattenedFiles } from "@common/files" ; import { IDisposable } from "@common/types" ; import { ChokidarEvents } from "@common/events" ; import { IDoc } from "@common/doctypes/doctypes" ; import hash from "@common/util/hash" ; import { WorkspacePlugin } from "@main/plugins/plugin" ; import { CrossRefPlugin } from "@main/plugins/crossref-plugin" ; import { parseAST } from "@common/doctypes/parse-doc" ; import { OutlinePlugin } from "@main/plugins/outline-plugin" ; import { MetadataPlugin } from "@main/plugins/metadata-plugin" ; import { WorkspaceService } from "./workspace-service" ; import { FSAL } from "@main/fsal/fsal" ; export interface IWorkspaceData { path : string ; files : { [ hash ] : IFileMeta ; } ; pluginData : { [ name ] : any ; } ; } export class Workspace implements IDisposable { todo /** plugins which need to know about changes to the workspace */ private _plugins : WorkspacePlugin [ ] ; constructor ( private _dir , private _files = { } , plugins = [ ] , private _workspaceService , /** TODO (2021-05-30) workspace shouldn't need FSAL -- move to WorkspaceService */ private _fsal ) { this . _plugins = [ ] ; for ( let plugin of plugins ) { this . registerPlugin ( plugin ) ; } } dispose ( ) { for ( let plugin of this . _plugins ) { this . unregisterWorkspacePlugin ( plugin ) ; plugin . dispose ( ) ; } } get dataPath ( ) { return Workspace . getDataPath ( this . _dir . path ) ; } get dir ( ) { return this . _dir ; } ; getFileByHash ( hash ) { return this . _files [ hash ] || null ; } getFileTree ( ) { let result = [ ] ; todo for ( let hash of Object . keys ( this . _files ) ) { let file = this . getFileByHash ( hash ) ; if ( file !== null ) { result . push ( file ) ; } } return result ; } /**
     * Called when the workspace should be re-synchronized
     * with files on disk.  Notifies plugins of any changes.
     *
     * TODO (2021-05-30) does workspace.update() work as intended?
     *     it is currently only called immediately after workspace opens
     */ async update ( ) { let currentFiles = getFlattenedFiles ( this . _dir ) ; let fileChanges = this . compareFiles ( currentFiles ) ; for ( let hash of fileChanges . deleted ) { let file = this . getFileByHash ( hash ) ; if ( file ) { this . handleFileDeleted ( file ) ; } } for ( let hash of fileChanges . added ) { let file = currentFiles [ hash ] ; await this . handleFileChanged ( file , true ) ; } for ( let hash of fileChanges . changed ) { let file = currentFiles [ hash ] ; await this . handleFileChanged ( file , false ) ; } return this . _workspaceService . writeJSON ( ) ; } /**
     * Updates the workspace entry for the given file.
     */ updatePath ( fileMeta ) { this . _files [ fileMeta . hash ] = fileMeta ; } /**
     * Called when a watched folder/file has changed.
     */ async handleChangeDetected ( event , info ) { let file = { path : info . path , hash : hash ( info . path ) } ; console . log ( "workspace :: handle chokidar ::" , event , info . path ) ; if ( event == ChokidarEvents . ADD_DIR ) { todo return ; } else if ( event == ChokidarEvents . UNLINK_DIR ) { todo return ; } if ( event == ChokidarEvents . UNLINK_FILE ) { this . handleFileDeleted ( file ) ; } else if ( event == ChokidarEvents . CHANGE_FILE ) { await this . handleFileChanged ( file , false ) ; } else if ( event == ChokidarEvents . ADD_FILE ) { await this . handleFileChanged ( file , true ) ; } } param file handleFileDeleted ( file ) { todo for ( let plugin of this . _plugins ) { plugin . handleFileDeleted ( file . path , file . hash ) ; } delete this . _files [ file . hash ] ; } param file param created async handleFileChanged ( file , created ) { todo let fileMeta = await this . _workspaceService . updatePath ( file . path ) ; if ( fileMeta == null ) { console . error ( `workspace :: handleFileCreated() :: error reading file :: ${ file . path }` ) ; return this . handleFileDeleted ( file ) ; } todo let fileContents = this . _fsal . readFile ( fileMeta . path ) ; if ( fileContents === null || fileContents === undefined ) { console . error ( `workspace :: handleFileCreated() :: error reading file :: ${ file . path }` ) ; return this . handleFileDeleted ( file ) ; } let fileAst = parseAST ( fileMeta . ext , fileContents ) ; if ( fileAst === null || fileAst === undefined ) { console . error ( `workspace :: handleFileCreated() :: error parsing file :: ${ file . path }` ) ; return this . handleFileDeleted ( file ) ; } for ( let plugin of this . _plugins ) { if ( created ) { plugin . handleFileCreated ( fileMeta , fileAst ) ; } else { plugin . handleFileChanged ( fileMeta , fileAst ) ; } } } registerPlugin ( plugin ) { this . _plugins . push ( plugin ) ; } unregisterWorkspacePlugin ( plugin ) { let index = this . _plugins . indexOf ( plugin ) ; if ( index > - 1 ) { this . _plugins . splice ( index , 1 ) ; } } todo getPluginByName ( name ) ; getPluginByName ( name ) ; getPluginByName ( name ) ; getPluginByName ( name ) ; getPluginByName ( name ) { todo return this . _plugins . find ( plugin => ( plugin . plugin_name == name ) ) || null ; } static getDataPath ( workspacePath ) { return pathlib . join ( workspacePath , ".noteworthy" , "workspace.json" ) ; } getData ( ) { let pluginData = { } ; for ( let plugin of this . _plugins ) { pluginData [ plugin . plugin_name ] = plugin . serialize ( ) ; } return { path : this . _dir . path , files : this . _files , pluginData } ; } toJSON ( ) { return JSON . stringify ( this . getData ( ) , undefined , 2 ) ; } /**
     * Compare two flattened file lists and report any changes.
     * (based on https://stackoverflow.com/revisions/33233053/6)
     */ compareFiles ( filesB ) { let filesA = this . _files ; let added = [ ] ; let deleted = [ ] ; let changed = [ ] ; let error = [ ] ; for ( let hash of new Set ( [ ... Object . keys ( filesA ) , ... Object . keys ( filesB ) ] ) ) { let a = filesA [ hash ] ; if ( a === undefined ) { added . push ( hash ) ; continue ; } let b = filesB [ hash ] ; if ( b === undefined ) { deleted . push ( hash ) ; continue ; } let modA = a . modTime ; let modB = b . modTime ; if ( modA > modB ) { error . push ( hash ) ; } else if ( modA < modB ) { changed . push ( hash ) ; } } return { added , deleted , changed , error } ; } }