import { CancellationToken , Disposable , Event , EventEmitter , FileChangeEvent , FileChangeType , FileSearchOptions , FileSearchProvider , FileSearchQuery , FileStat , FileSystemError , FileSystemProvider , FileType , Position , Progress , Range , TextSearchComplete , TextSearchOptions , TextSearchProvider , TextSearchQuery , TextSearchResult , Uri , workspace , } from 'vscode' ; import { watch , stopWatching , stat , pathIsDirectory , createDirectory , readFile , exists , writeFile , appendToPath , dirname , stripRootPrefix , deletePath , rename , getDescendentPaths , isDirectory , readDirectory , RootDir , readFileSavedContent , writeFileSavedContent , readFileSavedContentAsUTF8 , pathIsFileWithUnsavedContent , } from 'utopia-vscode-common' ; import { fromUtopiaURI , toUtopiaURI } from './path-utils' ; interface EventQueue < T > { queue : T [ ] ; emitter : EventEmitter < T [ ] > ; handle : number | null ; } function newEventQueue < T > ( ) { return { queue : [ ] , emitter : new EventEmitter < T [ ] > ( ) , handle : null , } ; } export class UtopiaFSExtension implements FileSystemProvider , FileSearchProvider , TextSearchProvider , Disposable { private disposable : Disposable ; private fileChangeEventQueue = newEventQueue < FileChangeEvent > ( ) ; private utopiaSavedChangeEventQueue = newEventQueue < Uri > ( ) ; private utopiaUnsavedChangeEventQueue = newEventQueue < Uri > ( ) ; private allFilePaths : string [ ] | null = null ; constructor ( private projectID ) { this . disposable = Disposable . from ( workspace . registerFileSystemProvider ( projectID , this , { isCaseSensitive : true } ) , workspace . registerFileSearchProvider ( projectID , this ) , workspace . registerTextSearchProvider ( projectID , this ) ) ; } dispose ( ) { this . disposable ?. dispose ( ) ; } readonly onDidChangeFile : Event < FileChangeEvent [ ] > = this . fileChangeEventQueue . emitter . event ; readonly onUtopiaDidChangeSavedContent : Event < Uri [ ] > = this . utopiaSavedChangeEventQueue . emitter . event ; readonly onUtopiaDidChangeUnsavedContent : Event < Uri [ ] > = this . utopiaUnsavedChangeEventQueue . emitter . event ; private queueEvent < T > ( event , eventQueue ) { eventQueue . queue . push ( event ) ; if ( eventQueue . handle != null ) { clearTimeout ( eventQueue . handle ) ; } eventQueue . handle = setTimeout ( ( ) => { eventQueue . emitter . fire ( eventQueue . queue ) ; eventQueue . queue = [ ] ; } , 5 ) ; } private queueFileChangeEvent ( event ) { this . clearCachedFiles ( ) ; this . queueEvent ( event , this . fileChangeEventQueue ) ; } private queueUtopiaSavedChangeEvent ( resource ) { this . queueEvent ( resource , this . utopiaSavedChangeEventQueue ) ; } private queueUtopiaUnsavedChangeEvent ( resource ) { this . queueEvent ( resource , this . utopiaUnsavedChangeEventQueue ) ; } private async notifyFileChanged ( path , modifiedBySelf ) { const uri = toUtopiaURI ( this . projectID , path ) ; const hasUnsavedContent = await pathIsFileWithUnsavedContent ( path ) ; const fileWasSaved = ! hasUnsavedContent ; if ( fileWasSaved ) { this . queueFileChangeEvent ( { type : FileChangeType . Changed , uri : uri , } ) ; } if ( ! modifiedBySelf ) { if ( fileWasSaved ) { this . queueUtopiaSavedChangeEvent ( uri ) ; } else { this . queueUtopiaUnsavedChangeEvent ( uri ) ; } } } private notifyFileCreated ( path ) { this . queueFileChangeEvent ( { type : FileChangeType . Created , uri : toUtopiaURI ( this . projectID , path ) , } ) ; } private notifyFileDeleted ( path ) { this . queueFileChangeEvent ( { type : FileChangeType . Deleted , uri : toUtopiaURI ( this . projectID , path ) , } ) ; } watch ( uri , options ) { const path = fromUtopiaURI ( uri ) ; watch ( path , options . recursive , this . notifyFileCreated . bind ( this ) , this . notifyFileChanged . bind ( this ) , this . notifyFileDeleted . bind ( this ) , ( ) => { } ) ; return new Disposable ( ( ) => { stopWatching ( path , options . recursive ) ; } ) ; } async exists ( uri ) { const path = fromUtopiaURI ( uri ) ; return exists ( path ) ; } async stat ( uri ) { const path = fromUtopiaURI ( uri ) ; const stats = await stat ( path ) ; const fileType = isDirectory ( stats ) ? FileType . Directory : FileType . File ; return { type : fileType , ctime : stats . ctime . valueOf ( ) , mtime : stats . lastSavedTime . valueOf ( ) , size : stats . size , } ; } async readDirectory ( uri ) { const path = fromUtopiaURI ( uri ) ; const children = await readDirectory ( path ) ; const result = children . map ( ( childName ) => pathIsDirectory ( appendToPath ( path , childName ) ) . then ( ( resultIsDirectory ) => [ childName , resultIsDirectory ? FileType . Directory : FileType . File , ] ) ) ; return Promise . all ( result ) ; } async createDirectory ( uri ) { const path = fromUtopiaURI ( uri ) ; await createDirectory ( path ) ; } async readFile ( uri ) { const path = fromUtopiaURI ( uri ) ; return readFileSavedContent ( path ) ; } async writeFile ( uri , content , options ) { const path = fromUtopiaURI ( uri ) ; if ( ! options . create || ! options . overwrite ) { const fileExists = await exists ( path ) ; if ( ! fileExists && ! options . create ) { throw FileSystemError . FileNotFound ( uri ) ; } else if ( fileExists && ! options . overwrite ) { throw FileSystemError . FileExists ( uri ) ; } } await writeFileSavedContent ( path , content ) ; } async delete ( uri , options ) { const path = fromUtopiaURI ( uri ) ; await deletePath ( path , options . recursive ) ; } async rename ( oldUri , newUri , options ) { const oldPath = fromUtopiaURI ( oldUri ) ; const newPath = fromUtopiaURI ( newUri ) ; if ( ! options . overwrite ) { const fileExists = await exists ( newPath ) ; if ( fileExists ) { throw FileSystemError . FileExists ( newUri ) ; } } await rename ( oldPath , newPath ) ; } async copy ( source , destination , options ) { const sourcePath = fromUtopiaURI ( source ) ; const destinationPath = fromUtopiaURI ( destination ) ; const destinationParentDir = dirname ( destinationPath ) ; const destinationParentDirExists = await exists ( destinationParentDir ) ; if ( ! destinationParentDirExists ) { throw FileSystemError . FileNotFound ( toUtopiaURI ( this . projectID , destinationParentDir ) ) ; } if ( ! options . overwrite ) { const destinationExists = await exists ( destinationPath ) ; if ( destinationExists && ! options . overwrite ) { throw FileSystemError . FileExists ( destination ) ; } } const { content , unsavedContent } = await readFile ( sourcePath ) ; await writeFile ( destinationPath , content , unsavedContent ) ; } async provideFileSearchResults ( query , options , _token ) { const { result : foundPaths } = await this . filterFilePaths ( query . pattern , options . maxResults ) ; return foundPaths . map ( ( p ) => toUtopiaURI ( this . projectID , p ) ) ; } async provideTextSearchResults ( query , options , progress , token ) { const { result : filePaths , limitHit } = await this . filterFilePaths ( options . includes [ 0 ] ) ; if ( filePaths . length > 0 ) { for ( const filePath of filePaths ) { if ( token . isCancellationRequested ) { break ; } const content = await readFileSavedContentAsUTF8 ( filePath ) ; const lines = splitIntoLines ( content ) ; for ( let i = 0 ; i < lines . length ; i ++ ) { const line = lines [ i ] ; const index = line . indexOf ( query . pattern ) ; if ( index !== - 1 ) { progress . report ( { uri : toUtopiaURI ( this . projectID , filePath ) , ranges : new Range ( new Position ( i , index ) , new Position ( i , index + query . pattern . length ) ) , preview : { text : line , matches : new Range ( new Position ( 0 , index ) , new Position ( 0 , index + query . pattern . length ) ) , } , } ) ; } } } } return { limitHit : limitHit } ; } private async filterFilePaths ( query , maxResults ? ) { const filePaths = await this . getAllPaths ( ) ; let result = [ ] ; let limitHit = false ; let remainingCount = maxResults == null ? Infinity : maxResults ; const pattern = query ? new RegExp ( convertSimple2RegExpPattern ( query ) ) : null ; for ( const path of filePaths ) { if ( remainingCount < 0 ) { break ; } if ( ! pattern || pattern . exec ( stripRootPrefix ( path ) ) ) { if ( remainingCount === 0 ) { limitHit = true ; } else { result . push ( path ) ; } remainingCount -- ; } } return { result : result , limitHit : limitHit , } ; } async getAllPaths ( ) { if ( this . allFilePaths == null ) { const result = await getDescendentPaths ( RootDir ) ; this . allFilePaths = result ; return result ; } else { return this . allFilePaths ; } } private clearCachedFiles ( ) { this . allFilePaths = null ; } } function convertSimple2RegExpPattern ( pattern ) { return pattern . replace ( /[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g , '\\$&' ) . replace ( /[\*]/g , '.*' ) ; } function splitIntoLines ( s ) { return s . split ( /[\r\n]/ ) ; }