@@ -126,6 +126,24 @@ namespace vfs {
126126 return this . _shadowRoot ;
127127 }
128128
129+ /**
130+ * Snapshots the current file system, effectively shadowing itself. This is useful for
131+ * generating file system patches using `.diff()` from one snapshot to the next. Performs
132+ * no action if this file system is read-only.
133+ */
134+ public snapshot ( ) {
135+ if ( this . isReadonly ) return ;
136+ const fs = new FileSystem ( this . ignoreCase , { time : this . _time } ) ;
137+ fs . _lazy = this . _lazy ;
138+ fs . _cwd = this . _cwd ;
139+ fs . _time = this . _time ;
140+ fs . _shadowRoot = this . _shadowRoot ;
141+ fs . _dirStack = this . _dirStack ;
142+ fs . makeReadonly ( ) ;
143+ this . _lazy = { } ;
144+ this . _shadowRoot = fs ;
145+ }
146+
129147 /**
130148 * Gets a shadow copy of this file system. Changes to the shadow copy do not affect the
131149 * original, allowing multiple copies of the same core file system without multiple copies
@@ -671,6 +689,160 @@ namespace vfs {
671689 node . ctimeMs = time ;
672690 }
673691
692+ /**
693+ * Generates a `FileSet` patch containing all the entries in this `FileSystem` that are not in `base`.
694+ * @param base The base file system. If not provided, this file system's `shadowRoot` is used (if present).
695+ */
696+ public diff ( base = this . shadowRoot ) {
697+ const differences : FileSet = { } ;
698+ const hasDifferences = base ? FileSystem . rootDiff ( differences , this , base ) : FileSystem . trackCreatedInodes ( differences , this , this . _getRootLinks ( ) ) ;
699+ return hasDifferences ? differences : undefined ;
700+ }
701+
702+ /**
703+ * Generates a `FileSet` patch containing all the entries in `chagned` that are not in `base`.
704+ */
705+ public static diff ( changed : FileSystem , base : FileSystem ) {
706+ const differences : FileSet = { } ;
707+ return FileSystem . rootDiff ( differences , changed , base ) ? differences : undefined ;
708+ }
709+
710+ private static diffWorker ( container : FileSet , changed : FileSystem , changedLinks : ReadonlyMap < string , Inode > | undefined , base : FileSystem , baseLinks : ReadonlyMap < string , Inode > | undefined ) {
711+ if ( changedLinks && ! baseLinks ) return FileSystem . trackCreatedInodes ( container , changed , changedLinks ) ;
712+ if ( baseLinks && ! changedLinks ) return FileSystem . trackDeletedInodes ( container , baseLinks ) ;
713+ if ( changedLinks && baseLinks ) {
714+ let hasChanges = false ;
715+ // track base items missing in changed
716+ baseLinks . forEach ( ( node , basename ) => {
717+ if ( ! changedLinks . has ( basename ) ) {
718+ container [ basename ] = isDirectory ( node ) ? new Rmdir ( ) : new Unlink ( ) ;
719+ hasChanges = true ;
720+ }
721+ } ) ;
722+ // track changed items missing or differing in base
723+ changedLinks . forEach ( ( changedNode , basename ) => {
724+ const baseNode = baseLinks . get ( basename ) ;
725+ if ( baseNode ) {
726+ if ( isDirectory ( changedNode ) && isDirectory ( baseNode ) ) {
727+ return hasChanges = FileSystem . directoryDiff ( container , basename , changed , changedNode , base , baseNode ) || hasChanges ;
728+ }
729+ if ( isFile ( changedNode ) && isFile ( baseNode ) ) {
730+ return hasChanges = FileSystem . fileDiff ( container , basename , changed , changedNode , base , baseNode ) || hasChanges ;
731+ }
732+ if ( isSymlink ( changedNode ) && isSymlink ( baseNode ) ) {
733+ return hasChanges = FileSystem . symlinkDiff ( container , basename , changedNode , baseNode ) || hasChanges ;
734+ }
735+ }
736+ return hasChanges = FileSystem . trackCreatedInode ( container , basename , changed , changedNode ) || hasChanges ;
737+ } ) ;
738+ return hasChanges ;
739+ }
740+ return false ;
741+ }
742+
743+ private static rootDiff ( container : FileSet , changed : FileSystem , base : FileSystem ) {
744+ while ( ! changed . _lazy . links && changed . _shadowRoot ) changed = changed . _shadowRoot ;
745+ while ( ! base . _lazy . links && base . _shadowRoot ) base = base . _shadowRoot ;
746+
747+ // no difference if the file systems are the same reference
748+ if ( changed === base ) return false ;
749+
750+ // no difference if the root links are empty and unshadowed
751+ if ( ! changed . _lazy . links && ! changed . _shadowRoot && ! base . _lazy . links && ! base . _shadowRoot ) return false ;
752+
753+ return FileSystem . diffWorker ( container , changed , changed . _getRootLinks ( ) , base , base . _getRootLinks ( ) ) ;
754+ }
755+
756+ private static directoryDiff ( container : FileSet , basename : string , changed : FileSystem , changedNode : DirectoryInode , base : FileSystem , baseNode : DirectoryInode ) {
757+ while ( ! changedNode . links && changedNode . shadowRoot ) changedNode = changedNode . shadowRoot ;
758+ while ( ! baseNode . links && baseNode . shadowRoot ) baseNode = baseNode . shadowRoot ;
759+
760+ // no difference if the nodes are the same reference
761+ if ( changedNode === baseNode ) return false ;
762+
763+ // no difference if both nodes are non shadowed and have no entries
764+ if ( isEmptyNonShadowedDirectory ( changedNode ) && isEmptyNonShadowedDirectory ( baseNode ) ) return false ;
765+
766+ // no difference if both nodes are unpopulated and point to the same mounted file system
767+ if ( ! changedNode . links && ! baseNode . links &&
768+ changedNode . resolver && changedNode . source !== undefined &&
769+ baseNode . resolver === changedNode . resolver && baseNode . source === changedNode . source ) return false ;
770+
771+ // no difference if both nodes have identical children
772+ const children : FileSet = { } ;
773+ if ( ! FileSystem . diffWorker ( children , changed , changed . _getLinks ( changedNode ) , base , base . _getLinks ( baseNode ) ) ) {
774+ return false ;
775+ }
776+
777+ container [ basename ] = new Directory ( children ) ;
778+ return true ;
779+ }
780+
781+ private static fileDiff ( container : FileSet , basename : string , changed : FileSystem , changedNode : FileInode , base : FileSystem , baseNode : FileInode ) {
782+ while ( ! changedNode . buffer && changedNode . shadowRoot ) changedNode = changedNode . shadowRoot ;
783+ while ( ! baseNode . buffer && baseNode . shadowRoot ) baseNode = baseNode . shadowRoot ;
784+
785+ // no difference if the nodes are the same reference
786+ if ( changedNode === baseNode ) return false ;
787+
788+ // no difference if both nodes are non shadowed and have no entries
789+ if ( isEmptyNonShadowedFile ( changedNode ) && isEmptyNonShadowedFile ( baseNode ) ) return false ;
790+
791+ // no difference if both nodes are unpopulated and point to the same mounted file system
792+ if ( ! changedNode . buffer && ! baseNode . buffer &&
793+ changedNode . resolver && changedNode . source !== undefined &&
794+ baseNode . resolver === changedNode . resolver && baseNode . source === changedNode . source ) return false ;
795+
796+ const changedBuffer = changed . _getBuffer ( changedNode ) ;
797+ const baseBuffer = base . _getBuffer ( baseNode ) ;
798+
799+ // no difference if both buffers are the same reference
800+ if ( changedBuffer === baseBuffer ) return false ;
801+
802+ // no difference if both buffers are identical
803+ if ( Buffer . compare ( changedBuffer , baseBuffer ) === 0 ) return false ;
804+
805+ container [ basename ] = new File ( changedBuffer ) ;
806+ return true ;
807+ }
808+
809+ private static symlinkDiff ( container : FileSet , basename : string , changedNode : SymlinkInode , baseNode : SymlinkInode ) {
810+ // no difference if the nodes are the same reference
811+ if ( changedNode . symlink === baseNode . symlink ) return false ;
812+ container [ basename ] = new Symlink ( changedNode . symlink ) ;
813+ return true ;
814+ }
815+
816+ private static trackCreatedInode ( container : FileSet , basename : string , changed : FileSystem , node : Inode ) {
817+ if ( isDirectory ( node ) ) {
818+ const children : FileSet = { } ;
819+ FileSystem . trackCreatedInodes ( children , changed , changed . _getLinks ( node ) ) ;
820+ container [ basename ] = new Directory ( children ) ;
821+ }
822+ else if ( isSymlink ( node ) ) {
823+ container [ basename ] = new Symlink ( node . symlink ) ;
824+ }
825+ else {
826+ container [ basename ] = new File ( node . buffer || "" ) ;
827+ }
828+ return true ;
829+ }
830+
831+ private static trackCreatedInodes ( container : FileSet , changed : FileSystem , changedLinks : ReadonlyMap < string , Inode > ) {
832+ // no difference if links are empty
833+ if ( ! changedLinks . size ) return false ;
834+
835+ changedLinks . forEach ( ( node , basename ) => { FileSystem . trackCreatedInode ( container , basename , changed , node ) ; } ) ;
836+ return true ;
837+ }
838+
839+ private static trackDeletedInodes ( container : FileSet , baseLinks : ReadonlyMap < string , Inode > ) {
840+ // no difference if links are empty
841+ if ( ! baseLinks . size ) return false ;
842+ baseLinks . forEach ( ( node , basename ) => { container [ basename ] = isDirectory ( node ) ? new Rmdir ( ) : new Unlink ( ) ; } ) ;
843+ return true ;
844+ }
845+
674846 private _mknod ( dev : number , type : typeof S_IFREG , mode : number , time ?: number ) : FileInode ;
675847 private _mknod ( dev : number , type : typeof S_IFDIR , mode : number , time ?: number ) : DirectoryInode ;
676848 private _mknod ( dev : number , type : typeof S_IFLNK , mode : number , time ?: number ) : SymlinkInode ;
@@ -940,10 +1112,10 @@ namespace vfs {
9401112
9411113 private _applyFilesWorker ( files : FileSet , dirname : string , deferred : [ Symlink | Link | Mount , string ] [ ] ) {
9421114 for ( const key of Object . keys ( files ) ) {
943- const value = this . _normalizeFileSetEntry ( files [ key ] ) ;
1115+ const value = normalizeFileSetEntry ( files [ key ] ) ;
9441116 const path = dirname ? vpath . resolve ( dirname , key ) : key ;
9451117 vpath . validate ( path , vpath . ValidationFlags . Absolute ) ;
946- if ( value === null || value === undefined ) {
1118+ if ( value === null || value === undefined || value instanceof Rmdir || value instanceof Unlink ) {
9471119 if ( this . stringComparer ( vpath . dirname ( path ) , path ) === 0 ) {
9481120 throw new TypeError ( "Roots cannot be deleted." ) ;
9491121 }
@@ -967,19 +1139,6 @@ namespace vfs {
9671139 }
9681140 }
9691141 }
970-
971- private _normalizeFileSetEntry ( value : FileSet [ string ] ) {
972- if ( value === undefined ||
973- value === null ||
974- value instanceof Directory ||
975- value instanceof File ||
976- value instanceof Link ||
977- value instanceof Symlink ||
978- value instanceof Mount ) {
979- return value ;
980- }
981- return typeof value === "string" || Buffer . isBuffer ( value ) ? new File ( value ) : new Directory ( value ) ;
982- }
9831142 }
9841143
9851144 export interface FileSystemOptions {
@@ -997,12 +1156,9 @@ namespace vfs {
9971156 meta ?: Record < string , any > ;
9981157 }
9991158
1000- export interface FileSystemCreateOptions {
1159+ export interface FileSystemCreateOptions extends FileSystemOptions {
10011160 // Sets the documents to add to the file system.
10021161 documents ?: ReadonlyArray < documents . TextDocument > ;
1003-
1004- // Sets the initial working directory for the file system.
1005- cwd ?: string ;
10061162 }
10071163
10081164 export type Axis = "ancestors" | "ancestors-or-self" | "self" | "descendants-or-self" | "descendants" ;
@@ -1062,8 +1218,16 @@ namespace vfs {
10621218 *
10631219 * Unless overridden, `/.src` will be the current working directory for the virtual file system.
10641220 */
1065- export function createFromFileSystem ( host : FileSystemResolverHost , ignoreCase : boolean , { documents, cwd } : FileSystemCreateOptions = { } ) {
1221+ export function createFromFileSystem ( host : FileSystemResolverHost , ignoreCase : boolean , { documents, files , cwd, time , meta } : FileSystemCreateOptions = { } ) {
10661222 const fs = getBuiltLocal ( host , ignoreCase ) . shadow ( ) ;
1223+ if ( meta ) {
1224+ for ( const key of Object . keys ( meta ) ) {
1225+ fs . meta . set ( key , meta [ key ] ) ;
1226+ }
1227+ }
1228+ if ( time ) {
1229+ fs . time ( time ) ;
1230+ }
10671231 if ( cwd ) {
10681232 fs . mkdirpSync ( cwd ) ;
10691233 fs . chdir ( cwd ) ;
@@ -1083,6 +1247,9 @@ namespace vfs {
10831247 }
10841248 }
10851249 }
1250+ if ( files ) {
1251+ fs . apply ( files ) ;
1252+ }
10861253 return fs ;
10871254 }
10881255
@@ -1165,7 +1332,7 @@ namespace vfs {
11651332 * A template used to populate files, directories, links, etc. in a virtual file system.
11661333 */
11671334 export interface FileSet {
1168- [ name : string ] : DirectoryLike | FileLike | Link | Symlink | Mount | null | undefined ;
1335+ [ name : string ] : DirectoryLike | FileLike | Link | Symlink | Mount | Rmdir | Unlink | null | undefined ;
11691336 }
11701337
11711338 export type DirectoryLike = FileSet | Directory ;
@@ -1201,6 +1368,16 @@ namespace vfs {
12011368 }
12021369 }
12031370
1371+ /** Removes a directory in a `FileSet` */
1372+ export class Rmdir {
1373+ public _rmdirBrand ?: never ; // brand necessary for proper type guards
1374+ }
1375+
1376+ /** Unlinks a file in a `FileSet` */
1377+ export class Unlink {
1378+ public _unlinkBrand ?: never ; // brand necessary for proper type guards
1379+ }
1380+
12041381 /** Extended options for a symbolic link in a `FileSet` */
12051382 export class Symlink {
12061383 public readonly symlink : string ;
@@ -1273,6 +1450,14 @@ namespace vfs {
12731450 meta ?: collections . Metadata ;
12741451 }
12751452
1453+ function isEmptyNonShadowedDirectory ( node : DirectoryInode ) {
1454+ return ! node . links && ! node . shadowRoot && ! node . resolver && ! node . source ;
1455+ }
1456+
1457+ function isEmptyNonShadowedFile ( node : FileInode ) {
1458+ return ! node . buffer && ! node . shadowRoot && ! node . resolver && ! node . source ;
1459+ }
1460+
12761461 function isFile ( node : Inode | undefined ) : node is FileInode {
12771462 return node !== undefined && ( node . mode & S_IFMT ) === S_IFREG ;
12781463 }
@@ -1324,5 +1509,55 @@ namespace vfs {
13241509 }
13251510 return builtLocalCS ;
13261511 }
1512+
1513+ function normalizeFileSetEntry ( value : FileSet [ string ] ) {
1514+ if ( value === undefined ||
1515+ value === null ||
1516+ value instanceof Directory ||
1517+ value instanceof File ||
1518+ value instanceof Link ||
1519+ value instanceof Symlink ||
1520+ value instanceof Mount ||
1521+ value instanceof Rmdir ||
1522+ value instanceof Unlink ) {
1523+ return value ;
1524+ }
1525+ return typeof value === "string" || Buffer . isBuffer ( value ) ? new File ( value ) : new Directory ( value ) ;
1526+ }
1527+
1528+ export function formatPatch ( patch : FileSet ) {
1529+ return formatPatchWorker ( "" , patch ) ;
1530+ }
1531+
1532+ function formatPatchWorker ( dirname : string , container : FileSet ) : string {
1533+ let text = "" ;
1534+ for ( const name of Object . keys ( container ) ) {
1535+ const entry = normalizeFileSetEntry ( container [ name ] ) ;
1536+ const file = dirname ? vpath . combine ( dirname , name ) : name ;
1537+ if ( entry === null || entry === undefined || entry instanceof Unlink || entry instanceof Rmdir ) {
1538+ text += `//// [${ file } ] unlink\r\n` ;
1539+ }
1540+ else if ( entry instanceof Rmdir ) {
1541+ text += `//// [${ vpath . addTrailingSeparator ( file ) } ] rmdir\r\n` ;
1542+ }
1543+ else if ( entry instanceof Directory ) {
1544+ text += formatPatchWorker ( file , entry . files ) ;
1545+ }
1546+ else if ( entry instanceof File ) {
1547+ const content = typeof entry . data === "string" ? entry . data : entry . data . toString ( "utf8" ) ;
1548+ text += `//// [${ file } ]\r\n${ content } \r\n\r\n` ;
1549+ }
1550+ else if ( entry instanceof Link ) {
1551+ text += `//// [${ file } ] link(${ entry . path } )\r\n` ;
1552+ }
1553+ else if ( entry instanceof Symlink ) {
1554+ text += `//// [${ file } ] symlink(${ entry . symlink } )\r\n` ;
1555+ }
1556+ else if ( entry instanceof Mount ) {
1557+ text += `//// [${ file } ] mount(${ entry . source } )\r\n` ;
1558+ }
1559+ }
1560+ return text ;
1561+ }
13271562}
13281563// tslint:enable:no-null-keyword
0 commit comments