Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"dependencies": {
"@pulsar-edit/pathwatcher": "^9.0.4",
"@pulsar-edit/superstring": "^3.0.4",
"@pulsar-edit/superstring": "^3.0.8",
"delegato": "^1.0.0",
"diff": "^2.2.1",
"event-kit": "^2.4.0",
Expand Down
71 changes: 66 additions & 5 deletions spec/text-buffer-io-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -785,17 +785,78 @@ describe('TextBuffer IO', () => {
// Nothing was reloaded: the whole point of this scenario is that the
// buffer's uncommitted changes are preserved.
expect(reloadedCount).toBe(0)
expect(conflictedCount).toBe(1)

// The buffer stays conflicted; the status must not flip back on its
// own once the load finishes.
expect(conflictedStatusChanges).toEqual([true])
expect(buffer.isInConflict()).toBe(true)
// The file came back with exactly the contents our changes are
// based on, so there's nothing to conflict with.
expect(conflictedCount).toBe(0)
expect(conflictedStatusChanges).toEqual([])
expect(buffer.isInConflict()).toBe(false)

expect(buffer.isDeleted()).toBe(false)
expect(buffer.isModified()).toBe(true)
})

describe('while the buffer has unsaved changes', () => {
async function deleteAndResurrect (restoredText) {
fs.unlinkSync(filePath)
await wait(500)
expect(buffer.isDeleted()).toBe(true)

const events = {conflictedCount: 0, conflictedStatusChanges: []}
buffer.onDidConflict(() => events.conflictedCount++)
buffer.onDidChangeConflicted((status) => events.conflictedStatusChanges.push(status))
fs.writeFileSync(filePath, restoredText)
await buffer.resurrect()
return events
}

beforeEach(async () => {
buffer.setText(`lorem ipsum`)
await buffer.save()
buffer.setText(`lorem ipsum dolor`)
})

it('reports a conflict if the file is restored with different contents', async () => {
const events = await deleteAndResurrect(`dolor sit amet`)
expect(events.conflictedCount).toBe(1)
expect(buffer.isInConflict()).toBe(true)
expect(buffer.isModified()).toBe(true)
expect(buffer.isDeleted()).toBe(false)
expect(buffer.getText()).toBe(`lorem ipsum dolor`)
})

it('reports a conflict if the file is restored empty', async () => {
const events = await deleteAndResurrect(``)
expect(events.conflictedCount).toBe(1)
expect(buffer.isInConflict()).toBe(true)
expect(buffer.getText()).toBe(`lorem ipsum dolor`)
})

it('does not report a conflict if the file is restored with the contents it had before', async () => {
const events = await deleteAndResurrect(`lorem ipsum`)
expect(events.conflictedCount).toBe(0)
expect(events.conflictedStatusChanges).toEqual([])
expect(buffer.isInConflict()).toBe(false)
expect(buffer.isModified()).toBe(true)
expect(buffer.isDeleted()).toBe(false)
expect(buffer.getText()).toBe(`lorem ipsum dolor`)
})

it('stays in conflict if it was already in conflict before the file was deleted', async () => {
const conflicted = new Promise((resolve) => buffer.onDidConflict(resolve))
fs.writeFileSync(filePath, `dolor sit amet`)
await conflicted
expect(buffer.isInConflict()).toBe(true)

// Even a file restored with the buffer's base text doesn't resolve
// a conflict that predates the deletion.
await deleteAndResurrect(`lorem ipsum`)
expect(buffer.isInConflict()).toBe(true)
expect(buffer.isModified()).toBe(true)
expect(buffer.isDeleted()).toBe(false)
})
})

describe('and re-saved', () => {
it('results in isModified and isDeleted no longer returning true', async () => {
buffer.setText(`lorem ipsum`)
Expand Down
21 changes: 20 additions & 1 deletion src/text-buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2085,7 +2085,7 @@ class TextBuffer {
// Instead, this method exists as an easy way for an external file-watcher to
// signal to `TextBuffer` that this buffer's backing file has been
// resurrected, and that certain events ought to be emitted.
resurrect () {
async resurrect () {
// Either the file came back but was gone again by the time we ran… or
// something is calling this method speculatively (perhaps via polling) in
// lieu of a file-watcher.
Expand All @@ -2098,6 +2098,25 @@ class TextBuffer {
this.subscribeToFile()

this.emitDeletedStatusChanged(false)

// A modified buffer won't reload, and a refused load is recorded as a
// conflict. That's correct only if the file came back with different
// contents than what our changes are based on — or if we were already in
// conflict before the file was deleted.
if (this.buffer.isModified() && !this.fileHasChangedSinceLastLoad) {
const source = this.file instanceof File
? this.file.getPath()
: this.file.createReadStream()
let matches = false
try {
matches = await this.buffer.baseTextMatchesFile(source, this.getEncoding())
} catch (error) {
// If the file vanished again, `load` will handle it.
if (error.code !== 'ENOENT') throw error
}
if (matches) return
}

// This is like calling `reload`, but less aggressive; it will preserve
// uncommitted buffer contents and emit a `did-conflict` event instead of
// clobbering those changes.
Expand Down
Loading