@@ -443,73 +443,106 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
443443 }
444444
445445 async move ( source : URI , target : URI , overwrite ?: boolean ) : Promise < IFileStatWithMetadata > {
446- const waitForPromises : Promise < unknown > [ ] = [ ] ;
447446
448- // Event
449- this . _onWillMove . fire ( {
450- oldResource : source ,
451- newResource : target ,
452- waitUntil ( promise : Promise < unknown > ) {
453- waitForPromises . push ( promise . then ( undefined , errors . onUnexpectedError ) ) ;
454- }
455- } ) ;
447+ // await onWillMove event joiners
448+ await this . notifyOnWillMove ( source , target ) ;
456449
457- // prevent async waitUntil-calls
458- Object . freeze ( waitForPromises ) ;
450+ // find all models that related to either source or target (can be many if resource is a folder)
451+ const sourceModels : ITextFileEditorModel [ ] = [ ] ;
452+ const conflictingModels : ITextFileEditorModel [ ] = [ ] ;
453+ for ( const model of this . getFileModels ( ) ) {
454+ const resource = model . getResource ( ) ;
459455
460- await Promise . all ( waitForPromises ) ;
456+ if ( isEqualOrParent ( resource , target , false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */ ) ) {
457+ conflictingModels . push ( model ) ;
458+ }
461459
462- // Handle target models if existing (if target URI is a folder, this can be multiple)
463- const dirtyTargetModels = this . getDirtyFileModels ( ) . filter ( model => isEqualOrParent ( model . getResource ( ) , target , false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */ ) ) ;
464- if ( dirtyTargetModels . length ) {
465- await this . revertAll ( dirtyTargetModels . map ( targetModel => targetModel . getResource ( ) ) , { soft : true } ) ;
460+ if ( isEqualOrParent ( resource , source ) ) {
461+ sourceModels . push ( model ) ;
462+ }
466463 }
467464
468- // Handle dirty source models if existing (if source URI is a folder, this can be multiple)
469- const dirtySourceModels = this . getDirtyFileModels ( ) . filter ( model => isEqualOrParent ( model . getResource ( ) , source ) ) ;
470- const dirtyTargetModelUris : URI [ ] = [ ] ;
471- if ( dirtySourceModels . length ) {
472- await Promise . all ( dirtySourceModels . map ( async sourceModel => {
473- const sourceModelResource = sourceModel . getResource ( ) ;
474- let targetModelResource : URI ;
475-
476- // If the source is the actual model, just use target as new resource
477- if ( isEqual ( sourceModelResource , source ) ) {
478- targetModelResource = target ;
479- }
465+ // remember each source model to load again after move is done
466+ // with optional content to restore if it was dirty
467+ type ModelToRestore = { resource : URI ; snapshot ?: ITextSnapshot } ;
468+ const modelsToRestore : ModelToRestore [ ] = [ ] ;
469+ for ( const sourceModel of sourceModels ) {
470+ const sourceModelResource = sourceModel . getResource ( ) ;
471+
472+ // If the source is the actual model, just use target as new resource
473+ let modelToRestoreResource : URI ;
474+ if ( isEqual ( sourceModelResource , source ) ) {
475+ modelToRestoreResource = target ;
476+ }
480477
481- // Otherwise a parent folder of the source is being moved, so we need
482- // to compute the target resource based on that
483- else {
484- targetModelResource = sourceModelResource . with ( { path : joinPath ( target , sourceModelResource . path . substr ( source . path . length + 1 ) ) . path } ) ;
485- }
478+ // Otherwise a parent folder of the source is being moved, so we need
479+ // to compute the target resource based on that
480+ else {
481+ modelToRestoreResource = joinPath ( target , sourceModelResource . path . substr ( source . path . length + 1 ) ) ;
482+ }
486483
487- // Remember as dirty target model to load after the operation
488- dirtyTargetModelUris . push ( targetModelResource ) ;
484+ const modelToRestore : ModelToRestore = { resource : modelToRestoreResource } ;
485+ if ( sourceModel . isDirty ( ) ) {
486+ modelToRestore . snapshot = sourceModel . createSnapshot ( ) ;
487+ }
489488
490- // Backup dirty source model to the target resource it will become later
491- await sourceModel . backup ( targetModelResource ) ;
492- } ) ) ;
489+ modelsToRestore . push ( modelToRestore ) ;
493490 }
494491
495- // Soft revert the dirty source files if any
496- await this . revertAll ( dirtySourceModels . map ( dirtySourceModel => dirtySourceModel . getResource ( ) ) , { soft : true } ) ;
492+ // in order to move, we need to soft revert all dirty models,
493+ // both from the source as well as the target if any
494+ const dirtyModels = [ ...sourceModels , ...conflictingModels ] . filter ( model => model . isDirty ( ) ) ;
495+ await this . revertAll ( dirtyModels . map ( dirtyModel => dirtyModel . getResource ( ) ) , { soft : true } ) ;
497496
498- // Rename to target
497+ // now we can rename the source to target via file operation
498+ let stat : IFileStatWithMetadata ;
499499 try {
500- const stat = await this . fileService . move ( source , target , overwrite ) ;
501-
502- // Load models that were dirty before
503- await Promise . all ( dirtyTargetModelUris . map ( dirtyTargetModel => this . models . loadOrCreate ( dirtyTargetModel ) ) ) ;
504-
505- return stat ;
500+ stat = await this . fileService . move ( source , target , overwrite ) ;
506501 } catch ( error ) {
507502
508- // In case of an error, discard any dirty target backups that were made
509- await Promise . all ( dirtyTargetModelUris . map ( dirtyTargetModel => this . backupFileService . discardResourceBackup ( dirtyTargetModel ) ) ) ;
503+ // in case of any error, ensure to set dirty flag back
504+ dirtyModels . forEach ( dirtyModel => dirtyModel . makeDirty ( ) ) ;
510505
511506 throw error ;
512507 }
508+
509+ // finally, restore models that we had loaded previously
510+ await Promise . all ( modelsToRestore . map ( async modelToRestore => {
511+
512+ // restore the model, forcing a reload. this is important because
513+ // we know the file has changed on disk after the move and the
514+ // model might have still existed with the previous state. this
515+ // ensures we are not tracking a stale state.
516+ const restoredModel = await this . models . loadOrCreate ( modelToRestore . resource , { reload : { async : false } } ) ;
517+
518+ // restore previous dirty content if any and ensure to mark
519+ // the model as dirty
520+ if ( modelToRestore . snapshot && restoredModel . isResolved ( ) ) {
521+ this . modelService . updateModel ( restoredModel . textEditorModel , createTextBufferFactoryFromSnapshot ( modelToRestore . snapshot ) ) ;
522+
523+ restoredModel . makeDirty ( ) ;
524+ }
525+ } ) ) ;
526+
527+ return stat ;
528+ }
529+
530+ private async notifyOnWillMove ( source : URI , target : URI ) : Promise < void > {
531+ const waitForPromises : Promise < unknown > [ ] = [ ] ;
532+
533+ // fire event
534+ this . _onWillMove . fire ( {
535+ oldResource : source ,
536+ newResource : target ,
537+ waitUntil ( promise : Promise < unknown > ) {
538+ waitForPromises . push ( promise . then ( undefined , errors . onUnexpectedError ) ) ;
539+ }
540+ } ) ;
541+
542+ // prevent async waitUntil-calls
543+ Object . freeze ( waitForPromises ) ;
544+
545+ await Promise . all ( waitForPromises ) ;
513546 }
514547
515548 //#endregion
@@ -857,7 +890,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
857890 return false ;
858891 }
859892
860- // take over encoding, mode (only if more specific) and model value from source model
893+ // take over model value, encoding and mode (only if more specific) from source model
861894 targetModel . updatePreferredEncoding ( sourceModel . getEncoding ( ) ) ;
862895 if ( sourceModel . isResolved ( ) && targetModel . isResolved ( ) ) {
863896 this . modelService . updateModel ( targetModel . textEditorModel , createTextBufferFactoryFromSnapshot ( sourceModel . createSnapshot ( ) ) ) ;
0 commit comments