@@ -102,6 +102,15 @@ class TextBuffer {
102102 this . cachedHasAstral = null
103103 this . _emittedWillChangeEvent = false
104104
105+ // Whether a buffer has ever had a backing file, whether or not it exists
106+ // now.
107+ this . didHaveFileOnDisk = false
108+
109+ // When a buffer's backing file is deleted while the file is unmodified,
110+ // this trait flips to `true`… and then flips back to `false` if any
111+ // further edits are made.
112+ this . retainsUnmodifiedTraitAfterDeletion = false
113+
105114 this . setEncoding ( params . encoding )
106115 this . setPreferredLineEnding ( params . preferredLineEnding )
107116
@@ -524,15 +533,32 @@ class TextBuffer {
524533 //
525534 // Returns a {Boolean}.
526535 isModified ( ) {
536+ if ( this . isDeleted ( ) ) {
537+ // We typically consider a deleted file to be modified… unless it was
538+ // unmodified at the time of deletion and has not been modified since.
539+ return ! this . retainsUnmodifiedTraitAfterDeletion
540+ }
527541 if ( this . file ) {
528542 return ! this . file . existsSync ( ) || this . buffer . isModified ( )
529543 } else {
530544 return this . buffer . getLength ( ) > 0
531545 }
532546 }
533547
534- // Public: Determine if the in-memory contents of the buffer conflict with the
535- // on-disk contents of its associated file.
548+ // Public: Determine if the buffer is in a deleted state — meaning that it
549+ // was previously backed by a file on disk, but is no longer.
550+ isDeleted ( ) {
551+ let hasNoFile = ! this . file || ! this . file . existsSync ( )
552+ return hasNoFile && this . didHaveFileOnDisk
553+ }
554+
555+ // Public: Determine if the in-memory contents of the buffer conflict with
556+ // the on-disk contents of its associated file.
557+ //
558+ // This happens if the contents of a buffer’s backing file change while the
559+ // editor has uncommitted changes. Those uncommitted changes build upon a
560+ // state that is now stale; if those changes were committed to disk, it could
561+ // clobber the changes made by the external program.
536562 //
537563 // Returns a {Boolean}.
538564 isInConflict ( ) {
@@ -845,6 +871,7 @@ class TextBuffer {
845871 if ( undo != null ) {
846872 Grim . deprecate ( 'The `undo` option is deprecated. Call groupLastChanges() on the TextBuffer afterward instead.' )
847873 }
874+ this . retainsUnmodifiedTraitAfterDeletion = false
848875
849876 if ( this . transactCallDepth === 0 ) {
850877 const newRange = this . transact ( ( ) => this . setTextInRange ( range , newText , { normalizeLineEndings} ) )
@@ -1929,6 +1956,7 @@ class TextBuffer {
19291956
19301957 try {
19311958 await this . buffer . save ( destination , this . getEncoding ( ) )
1959+ this . didHaveFileOnDisk = true
19321960 } catch ( error ) {
19331961 if ( error . code !== 'EACCES' || destination !== filePath ) throw error
19341962
@@ -2101,6 +2129,8 @@ class TextBuffer {
21012129 Grim . deprecate ( 'The .load instance method is deprecated. Create a loaded buffer using TextBuffer.load(filePath) instead.' )
21022130 }
21032131
2132+ this . didHaveFileOnDisk = true
2133+
21042134 const source = this . file instanceof File
21052135 ? this . file . getPath ( )
21062136 : this . file . createReadStream ( )
@@ -2295,6 +2325,7 @@ class TextBuffer {
22952325 if ( this . file . onDidDelete ) {
22962326 this . fileSubscriptions . add ( this . file . onDidDelete ( ( ) => {
22972327 const modified = this . buffer . isModified ( )
2328+ this . retainsUnmodifiedTraitAfterDeletion = ! modified
22982329 this . emitter . emit ( 'did-delete' )
22992330 if ( ! modified && this . shouldDestroyOnFileDelete ( ) ) {
23002331 return this . destroy ( )
0 commit comments