diff --git a/spec/text-buffer-replication-spec.coffee b/spec/text-buffer-replication-spec.coffee index 4bbff2e5e..eb8da8c7c 100644 --- a/spec/text-buffer-replication-spec.coffee +++ b/spec/text-buffer-replication-spec.coffee @@ -5,7 +5,6 @@ describe "TextBuffer replication", -> beforeEach -> buffer1 = project.buildBufferSync('sample.js') - buffer1.insert([0, 0], 'changed\n') doc1 = buffer1.getState() doc2 = doc1.clone(new Site(2)) doc1.connect(doc2) @@ -14,6 +13,9 @@ describe "TextBuffer replication", -> waitsFor -> buffer1.loaded and buffer2.loaded + runs -> + buffer1.insert([0, 0], 'changed\n') + afterEach -> buffer1.destroy() buffer2.destroy() diff --git a/spec/text-buffer-spec.coffee b/spec/text-buffer-spec.coffee index 660309386..e2911d087 100644 --- a/spec/text-buffer-spec.coffee +++ b/spec/text-buffer-spec.coffee @@ -949,24 +949,48 @@ describe 'TextBuffer', -> expect(buffer2.getText()).toBe(buffer.getText()) describe "when the serialized buffer had unsaved changes", -> - it "restores the previous unsaved state of the buffer", -> - previousText = buffer.getText() - buffer.setText("abc") + describe "when the disk contents were changed since serialization", -> + it "loads the disk contents instead of the previous unsaved state", -> + buffer.release() - state = buffer.serialize() - expect(state.getObject('text')).toBe 'abc' + filePath = temp.openSync('atom').path + fs.writeSync(filePath, "words") + {buffer} = project.openSync(filePath) + buffer.setText("BUFFER CHANGE") - buffer2 = deserialize(state, {project}) + state = buffer.serialize() + expect(state.getObject('text')).toBe 'BUFFER CHANGE' + fs.writeSync(filePath, "DISK CHANGE") - waitsFor -> - buffer2.cachedDiskContents + buffer2 = deserialize(state, {project}) - runs -> - expect(buffer2.getPath()).toBe(buffer.getPath()) - expect(buffer2.getText()).toBe(buffer.getText()) - expect(buffer2.isModified()).toBeTruthy() - buffer2.setText(previousText) - expect(buffer2.isModified()).toBeFalsy() + waitsFor -> + buffer2.cachedDiskContents + + runs -> + expect(buffer2.getPath()).toBe(buffer.getPath()) + expect(buffer2.getText()).toBe("DISK CHANGE") + expect(buffer2.isModified()).toBeFalsy() + + describe "when the disk contents are the same since serialization", -> + it "restores the previous unsaved state of the buffer", -> + previousText = buffer.getText() + buffer.setText("abc") + + state = buffer.serialize() + expect(state.getObject('text')).toBe 'abc' + + buffer2 = deserialize(state, {project}) + + waitsFor -> + buffer2.cachedDiskContents + + runs -> + expect(buffer2.getPath()).toBe(buffer.getPath()) + expect(buffer2.getText()).toBe(buffer.getText()) + expect(buffer2.isModified()).toBeTruthy() + buffer2.setText(previousText) + expect(buffer2.isModified()).toBeFalsy() describe "when the serialized buffer was unsaved and had no path", -> it "restores the previous unsaved state of the buffer", -> diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index dd36ec316..096060bec 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -350,7 +350,8 @@ describe "TokenizedBuffer", -> describe "when the buffer contains surrogate pairs", -> beforeEach -> atom.activatePackage('language-javascript', sync: true) - buffer = project.buildBufferSync 'sample-with-pairs.js', """ + buffer = project.buildBufferSync 'sample-with-pairs.js' + buffer.setText """ 'abc\uD835\uDF97def' //\uD835\uDF97xyz """ @@ -389,7 +390,8 @@ describe "TokenizedBuffer", -> atom.activatePackage('language-ruby-on-rails', sync: true) atom.activatePackage('language-ruby', sync: true) - buffer = project.bufferForPathSync(null, "
<%= User.find(2).full_name %>
") + buffer = project.bufferForPathSync() + buffer.setText "
<%= User.find(2).full_name %>
" tokenizedBuffer = new TokenizedBuffer({buffer}) tokenizedBuffer.setGrammar(syntax.selectGrammar('test.erb')) fullyTokenize(tokenizedBuffer) diff --git a/src/file.coffee b/src/file.coffee index 5e7895410..2a671ed35 100644 --- a/src/file.coffee +++ b/src/file.coffee @@ -1,3 +1,4 @@ +crypto = require 'crypto' path = require 'path' pathWatcher = require 'pathwatcher' Q = require 'q' @@ -68,6 +69,9 @@ class File else @cachedContents + @setDigest(@cachedContents) + @cachedContents + # Public: Reads the contents of the file. # # * flushCache: @@ -101,12 +105,19 @@ class File promise = Q(@cachedContents) promise.then (contents) => + @setDigest(contents) @cachedContents = contents # Public: Returns whether the file exists. exists: -> fsUtils.exists(@getPath()) + setDigest: (contents) -> + @digest = crypto.createHash('sha1').update(contents ? '').digest('hex') + + getDigest: -> + @digest ? @setDigest(@readSync()) + # Private: handleNativeChangeEvent: (eventType, path) -> if eventType is "delete" diff --git a/src/project.coffee b/src/project.coffee index c1769121e..6e821e2c6 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -224,13 +224,13 @@ class Project existingBuffer?.isModified() # Private: Only to be used in specs - bufferForPathSync: (filePath, text) -> + bufferForPathSync: (filePath) -> absoluteFilePath = @resolve(filePath) if filePath existingBuffer = _.find @buffers, (buffer) -> buffer.getPath() == absoluteFilePath - existingBuffer ? @buildBufferSync(absoluteFilePath, text) + existingBuffer ? @buildBufferSync(absoluteFilePath) # Private: Given a file path, this retrieves or creates a new {TextBuffer}. # @@ -238,23 +238,22 @@ class Project # `text` is used as the contents of the new buffer. # # filePath - A {String} representing a path. If `null`, an "Untitled" buffer is created. - # text - The {String} text to use as a buffer, if the file doesn't have any contents # # Returns a promise that resolves to the {TextBuffer}. - bufferForPath: (filePath, text) -> + bufferForPath: (filePath) -> absoluteFilePath = @resolve(filePath) if absoluteFilePath existingBuffer = _.find @buffers, (buffer) -> buffer.getPath() == absoluteFilePath - Q(existingBuffer ? @buildBuffer(absoluteFilePath, text)) + Q(existingBuffer ? @buildBuffer(absoluteFilePath)) # Private: bufferForId: (id) -> _.find @buffers, (buffer) -> buffer.id is id # Private: DEPRECATED - buildBufferSync: (absoluteFilePath, initialText) -> - buffer = new TextBuffer({project: this, filePath: absoluteFilePath, initialText}) + buildBufferSync: (absoluteFilePath) -> + buffer = new TextBuffer({project: this, filePath: absoluteFilePath}) buffer.loadSync() @addBuffer(buffer) buffer @@ -265,8 +264,8 @@ class Project # text - The {String} text to use as a buffer # # Returns a promise that resolves to the {TextBuffer}. - buildBuffer: (absoluteFilePath, initialText) -> - buffer = new TextBuffer({project: this, filePath: absoluteFilePath, initialText}) + buildBuffer: (absoluteFilePath) -> + buffer = new TextBuffer({project: this, filePath: absoluteFilePath}) buffer.load().then (buffer) => @addBuffer(buffer) buffer diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 99fa698a7..d4cc854f8 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -1,3 +1,4 @@ +crypto = require 'crypto' {Emitter, Subscriber} = require 'emissary' guid = require 'guid' Q = require 'q' @@ -38,8 +39,8 @@ class TextBuffer # Creates a new buffer. # - # path - A {String} representing the file path - # initialText - A {String} setting the starting text + # * optionsOrState - An {Object} or a telepath.Document + # + filePath - A {String} representing the file path constructor: (optionsOrState={}, params={}) -> if optionsOrState instanceof telepath.Document {@project} = params @@ -47,9 +48,9 @@ class TextBuffer @id = @state.get('id') filePath = @state.get('relativePath') @text = @state.get('text') - @loadFromDisk = @state.get('isModified') == false + @useSerializedText = @state.get('isModified') != false else - {@project, filePath, initialText} = optionsOrState + {@project, filePath} = optionsOrState @text = site.createDocument(initialText ? '', shareStrings: true) @id = guid.create().toString() @state = site.createDocument @@ -57,7 +58,6 @@ class TextBuffer deserializer: @constructor.name version: @constructor.version text: @text - @loadFromDisk = not initialText @loaded = false @subscribe @text, 'changed', @handleTextChange @@ -68,14 +68,19 @@ class TextBuffer loadSync: -> @updateCachedDiskContentsSync() - @reload() if @loadFromDisk - @text.clearUndoStack() + @finishLoading() load: -> - @updateCachedDiskContents().then => - @reload() if @loadFromDisk - @text.clearUndoStack() - this + @updateCachedDiskContents().then => @finishLoading() + + finishLoading: -> + @loaded = true + if @useSerializedText and @state.get('diskContentsDigest') == @file?.getDigest() + @emitModifiedStatusChanged(true) + else + @reload() + @text.clearUndoStack() + this ### Internal ### @@ -108,6 +113,7 @@ class TextBuffer serialize: -> state = @state.clone() state.set('isModified', @isModified()) + state.set('diskContentsDigest', @file.getDigest()) if @file for marker in state.get('text').getMarkers() when marker.isRemote() marker.destroy() state @@ -158,13 +164,11 @@ class TextBuffer # Private: Rereads the contents of the file, and stores them in the cache. updateCachedDiskContentsSync: -> - @loaded = true @cachedDiskContents = @file?.readSync() ? "" # Private: Rereads the contents of the file, and stores them in the cache. updateCachedDiskContents: -> Q(@file?.read() ? "").then (contents) => - @loaded = true @cachedDiskContents = contents # Gets the file's basename--that is, the file without any directory information.