diff --git a/package.json b/package.json index 77b89f65c..28e4eacc7 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "clear-cut": "0.2.0", "coffee-script": "1.6.3", "coffeestack": "0.6.0", + "diff": "git://github.com/benogle/jsdiff.git", "emissary": "0.19.0", "first-mate": "0.5.0", "fs-plus": "0.11.0", diff --git a/spec/text-buffer-spec.coffee b/spec/text-buffer-spec.coffee index 31066cd5b..0aaf9a3b5 100644 --- a/spec/text-buffer-spec.coffee +++ b/spec/text-buffer-spec.coffee @@ -113,10 +113,17 @@ describe 'TextBuffer', -> runs -> [event] = changeHandler.argsForCall[0] - expect(event.oldRange).toEqual [[0, 0], [0, 5]] + expect(event.oldRange).toEqual [[0, 0], [0, 0]] expect(event.newRange).toEqual [[0, 0], [0, 6]] - expect(event.oldText).toBe "first" + expect(event.oldText).toBe "" expect(event.newText).toBe "second" + + [event] = changeHandler.argsForCall[1] + expect(event.oldRange).toEqual [[0, 6], [0, 11]] + expect(event.newRange).toEqual [[0, 6], [0, 6]] + expect(event.oldText).toBe "first" + expect(event.newText).toBe "" + expect(buffer.isModified()).toBeFalsy() describe "when the buffer's memory contents differ from the *previous* disk contents", -> @@ -454,6 +461,68 @@ describe 'TextBuffer', -> expect(event.oldRange).toEqual expectedPreRange expect(event.newRange).toEqual [[0, 0], [1, 14]] + describe ".setTextViaDiff(text)", -> + it "can change the entire contents of the buffer when there are no newlines", -> + buffer.setText('BUFFER CHANGE') + newText = 'DISK CHANGE' + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + describe "with standard newlines", -> + it "can change the entire contents of the buffer with no newline at the end", -> + newText = "I know you are.\nBut what am I?" + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + it "can change the entire contents of the buffer with a newline at the end", -> + newText = "I know you are.\nBut what am I?\n" + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + it "can change a few lines at the beginning in the buffer", -> + newText = buffer.getText().replace(/function/g, 'omgwow') + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + it "can change a few lines in the middle of the buffer", -> + newText = buffer.getText().replace(/shift/g, 'omgwow') + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + it "can adds a newline at the end", -> + newText = buffer.getText() + '\n' + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + describe "with windows newlines", -> + beforeEach -> + buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + + it "adds a newline at the end", -> + newText = buffer.getText() + '\r\n' + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + it "changes the entire contents of the buffer with smaller content with no newline at the end", -> + newText = "I know you are.\r\nBut what am I?" + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + it "changes the entire contents of the buffer with smaller content with newline at the end", -> + newText = "I know you are.\r\nBut what am I?\r\n" + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + it "changes a few lines at the beginning in the buffer", -> + newText = buffer.getText().replace(/function/g, 'omgwow') + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + it "changes a few lines in the middle of the buffer", -> + newText = buffer.getText().replace(/shift/g, 'omgwow') + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + describe ".save()", -> saveBuffer = null diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 665b7d02d..76f897e3c 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -1,4 +1,5 @@ _ = require 'underscore-plus' +diff = require 'diff' Q = require 'q' {P} = require 'scandal' telepath = require 'telepath' @@ -133,7 +134,7 @@ class TextBuffer extends telepath.Model # Sets the buffer's content to the cached disk contents reload: -> @emit 'will-reload' - @setText(@cachedDiskContents) + @setTextViaDiff(@cachedDiskContents) @emitModifiedStatusChanged(false) @emit 'reloaded' @@ -198,6 +199,52 @@ class TextBuffer extends telepath.Model setText: (text) -> @change(@getRange(), text, normalizeLineEndings: false) + # Private: Replaces the current buffer contents. Only apply the differences. + # + # text - A {String} containing the new buffer contents. + setTextViaDiff: (text) -> + currentText = @getText() + return if currentText == text + + endsWithNewline = (str) -> + /[\r\n]+$/g.test(str) + + computeBufferColumn = (str) -> + newlineIndex = Math.max(str.lastIndexOf('\n'), str.lastIndexOf('\r')) + if endsWithNewline(str) + 0 + else if newlineIndex == -1 + str.length + else + str.length - newlineIndex - 1 + + @transact => + row = 0 + column = 0 + currentPosition = [0, 0] + + lineDiff = diff.diffLines(currentText, text) + changeOptions = normalizeLineEndings: false + + for change in lineDiff + lineCount = change.value.match(/\n/g)?.length ? 0 + currentPosition[0] = row + currentPosition[1] = column + + if change.added + @change([currentPosition, currentPosition], change.value, changeOptions) + row += lineCount + column = computeBufferColumn(change.value) + + else if change.removed + endRow = row + lineCount + endColumn = column + computeBufferColumn(change.value) + @change([currentPosition, [endRow, endColumn]], '', changeOptions) + + else + row += lineCount + column = computeBufferColumn(change.value) + # Gets the range of the buffer contents. # # Returns a new {Range}, from `[0, 0]` to the end of the buffer.