@@ -126,6 +126,24 @@ namespace vfs {
126
126
return this . _shadowRoot ;
127
127
}
128
128
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
+
129
147
/**
130
148
* Gets a shadow copy of this file system. Changes to the shadow copy do not affect the
131
149
* original, allowing multiple copies of the same core file system without multiple copies
@@ -671,6 +689,160 @@ namespace vfs {
671
689
node . ctimeMs = time ;
672
690
}
673
691
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
+
674
846
private _mknod ( dev : number , type : typeof S_IFREG , mode : number , time ?: number ) : FileInode ;
675
847
private _mknod ( dev : number , type : typeof S_IFDIR , mode : number , time ?: number ) : DirectoryInode ;
676
848
private _mknod ( dev : number , type : typeof S_IFLNK , mode : number , time ?: number ) : SymlinkInode ;
@@ -940,10 +1112,10 @@ namespace vfs {
940
1112
941
1113
private _applyFilesWorker ( files : FileSet , dirname : string , deferred : [ Symlink | Link | Mount , string ] [ ] ) {
942
1114
for ( const key of Object . keys ( files ) ) {
943
- const value = this . _normalizeFileSetEntry ( files [ key ] ) ;
1115
+ const value = normalizeFileSetEntry ( files [ key ] ) ;
944
1116
const path = dirname ? vpath . resolve ( dirname , key ) : key ;
945
1117
vpath . validate ( path , vpath . ValidationFlags . Absolute ) ;
946
- if ( value === null || value === undefined ) {
1118
+ if ( value === null || value === undefined || value instanceof Rmdir || value instanceof Unlink ) {
947
1119
if ( this . stringComparer ( vpath . dirname ( path ) , path ) === 0 ) {
948
1120
throw new TypeError ( "Roots cannot be deleted." ) ;
949
1121
}
@@ -967,19 +1139,6 @@ namespace vfs {
967
1139
}
968
1140
}
969
1141
}
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
- }
983
1142
}
984
1143
985
1144
export interface FileSystemOptions {
@@ -997,12 +1156,9 @@ namespace vfs {
997
1156
meta ?: Record < string , any > ;
998
1157
}
999
1158
1000
- export interface FileSystemCreateOptions {
1159
+ export interface FileSystemCreateOptions extends FileSystemOptions {
1001
1160
// Sets the documents to add to the file system.
1002
1161
documents ?: ReadonlyArray < documents . TextDocument > ;
1003
-
1004
- // Sets the initial working directory for the file system.
1005
- cwd ?: string ;
1006
1162
}
1007
1163
1008
1164
export type Axis = "ancestors" | "ancestors-or-self" | "self" | "descendants-or-self" | "descendants" ;
@@ -1062,8 +1218,16 @@ namespace vfs {
1062
1218
*
1063
1219
* Unless overridden, `/.src` will be the current working directory for the virtual file system.
1064
1220
*/
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 = { } ) {
1066
1222
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
+ }
1067
1231
if ( cwd ) {
1068
1232
fs . mkdirpSync ( cwd ) ;
1069
1233
fs . chdir ( cwd ) ;
@@ -1083,6 +1247,9 @@ namespace vfs {
1083
1247
}
1084
1248
}
1085
1249
}
1250
+ if ( files ) {
1251
+ fs . apply ( files ) ;
1252
+ }
1086
1253
return fs ;
1087
1254
}
1088
1255
@@ -1165,7 +1332,7 @@ namespace vfs {
1165
1332
* A template used to populate files, directories, links, etc. in a virtual file system.
1166
1333
*/
1167
1334
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 ;
1169
1336
}
1170
1337
1171
1338
export type DirectoryLike = FileSet | Directory ;
@@ -1201,6 +1368,16 @@ namespace vfs {
1201
1368
}
1202
1369
}
1203
1370
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
+
1204
1381
/** Extended options for a symbolic link in a `FileSet` */
1205
1382
export class Symlink {
1206
1383
public readonly symlink : string ;
@@ -1273,6 +1450,14 @@ namespace vfs {
1273
1450
meta ?: collections . Metadata ;
1274
1451
}
1275
1452
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
+
1276
1461
function isFile ( node : Inode | undefined ) : node is FileInode {
1277
1462
return node !== undefined && ( node . mode & S_IFMT ) === S_IFREG ;
1278
1463
}
@@ -1324,5 +1509,55 @@ namespace vfs {
1324
1509
}
1325
1510
return builtLocalCS ;
1326
1511
}
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
+ }
1327
1562
}
1328
1563
// tslint:enable:no-null-keyword
0 commit comments