From 1f69963982d0b93d2f45b6e802a3667aeab3a29d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 11 Dec 2013 17:42:35 -0800 Subject: [PATCH 1/8] Apply text to buffer via diff on reload Previously, it would blindly read from disk on reload, and set the text into the editor. This was problematic as it would mess with markers and folds. No longer. Fixes #1285 and fixes atom/bookmarks#3 --- package.json | 1 + spec/text-buffer-spec.coffee | 72 +++++++++++++++++++++++++++++++++++- src/text-buffer.coffee | 55 ++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 3 deletions(-) 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..963edd932 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,67 @@ describe 'TextBuffer', -> expect(event.oldRange).toEqual expectedPreRange expect(event.newRange).toEqual [[0, 0], [1, 14]] + describe ".setTextViaDiff(text)", -> + it "changes the entire contents of the buffer with smaller content with no newline at the end", -> + newText = "I know you are.\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.\nBut what am I?\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 + + it "adds a newline at the end", -> + newText = buffer.getText() + '\n' + buffer.setTextViaDiff(newText) + expect(buffer.getText()).toBe newText + + it "changes all with no newlines", -> + buffer.setText('BUFFER CHANGE') + newText = 'DISK CHANGE' + 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..51cad7723 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,12 @@ class TextBuffer extends telepath.Model setText: (text) -> @change(@getRange(), text, normalizeLineEndings: false) + # Replaces the current buffer contents. Only apply the differences. + # + # text - A {String} containing the new buffer contents. + setTextViaDiff: (text) -> + @applyDifferences(text) + # Gets the range of the buffer contents. # # Returns a new {Range}, from `[0, 0]` to the end of the buffer. @@ -667,3 +674,49 @@ class TextBuffer extends telepath.Model for row in [start..end] line = @lineForRow(row) console.log row, line, line.length + + applyDifferences: (newText) -> + currentText = @getText() + return if currentText == newText + + 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 => + bufferRow = 0 + bufferColumn = 0 + startPosition = [0, 0] + + lineDiff = diff.diffLines(currentText, newText) + changeOptions = normalizeLineEndings: false + + for change in lineDiff + numberLines = change.value.match(/\n/g)?.length ? 0 + startPosition[0] = bufferRow + startPosition[1] = bufferColumn + + if change.added + @change([startPosition, startPosition], change.value, changeOptions) + bufferRow += numberLines + bufferColumn = computeBufferColumn(change.value) + + else if change.removed + endBufferRow = bufferRow + numberLines + endBufferColumn = bufferColumn + computeBufferColumn(change.value) + @change([startPosition, [endBufferRow, endBufferColumn]], '', changeOptions) + + else + bufferRow += numberLines + bufferColumn = computeBufferColumn(change.value) + + # console.log 'after', bufferRow, bufferColumn, change + # console.log @getText() From ed745d20721c41dc1460aedde7253c5d023f71d9 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 11 Dec 2013 17:44:00 -0800 Subject: [PATCH 2/8] Remove log lines --- src/text-buffer.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 51cad7723..6536b2238 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -717,6 +717,3 @@ class TextBuffer extends telepath.Model else bufferRow += numberLines bufferColumn = computeBufferColumn(change.value) - - # console.log 'after', bufferRow, bufferColumn, change - # console.log @getText() From 60498616b79a995b0e059c5573a167f7c829c5ed Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 11 Dec 2013 17:48:33 -0800 Subject: [PATCH 3/8] numberLines -> lineCount --- src/text-buffer.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 6536b2238..160076c05 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -700,20 +700,20 @@ class TextBuffer extends telepath.Model changeOptions = normalizeLineEndings: false for change in lineDiff - numberLines = change.value.match(/\n/g)?.length ? 0 + lineCount = change.value.match(/\n/g)?.length ? 0 startPosition[0] = bufferRow startPosition[1] = bufferColumn if change.added @change([startPosition, startPosition], change.value, changeOptions) - bufferRow += numberLines + bufferRow += lineCount bufferColumn = computeBufferColumn(change.value) else if change.removed - endBufferRow = bufferRow + numberLines + endBufferRow = bufferRow + lineCount endBufferColumn = bufferColumn + computeBufferColumn(change.value) @change([startPosition, [endBufferRow, endBufferColumn]], '', changeOptions) else - bufferRow += numberLines + bufferRow += lineCount bufferColumn = computeBufferColumn(change.value) From 8d1d64d9d3149636de45cd0995cb37019cf55413 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 11 Dec 2013 17:51:19 -0800 Subject: [PATCH 4/8] bufferRow -> row --- src/text-buffer.coffee | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 160076c05..4d42ba28b 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -692,8 +692,8 @@ class TextBuffer extends telepath.Model str.length - newlineIndex - 1 @transact => - bufferRow = 0 - bufferColumn = 0 + row = 0 + column = 0 startPosition = [0, 0] lineDiff = diff.diffLines(currentText, newText) @@ -701,19 +701,19 @@ class TextBuffer extends telepath.Model for change in lineDiff lineCount = change.value.match(/\n/g)?.length ? 0 - startPosition[0] = bufferRow - startPosition[1] = bufferColumn + startPosition[0] = row + startPosition[1] = column if change.added @change([startPosition, startPosition], change.value, changeOptions) - bufferRow += lineCount - bufferColumn = computeBufferColumn(change.value) + row += lineCount + column = computeBufferColumn(change.value) else if change.removed - endBufferRow = bufferRow + lineCount - endBufferColumn = bufferColumn + computeBufferColumn(change.value) + endBufferRow = row + lineCount + endBufferColumn = column + computeBufferColumn(change.value) @change([startPosition, [endBufferRow, endBufferColumn]], '', changeOptions) else - bufferRow += lineCount - bufferColumn = computeBufferColumn(change.value) + row += lineCount + column = computeBufferColumn(change.value) From 5d46d7a88118b80df24cf9b3d7ada168f813fc21 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 11 Dec 2013 17:53:54 -0800 Subject: [PATCH 5/8] :lipstick: --- src/text-buffer.coffee | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 4d42ba28b..b3a60c09f 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -694,25 +694,25 @@ class TextBuffer extends telepath.Model @transact => row = 0 column = 0 - startPosition = [0, 0] + currentPosition = [0, 0] lineDiff = diff.diffLines(currentText, newText) changeOptions = normalizeLineEndings: false for change in lineDiff lineCount = change.value.match(/\n/g)?.length ? 0 - startPosition[0] = row - startPosition[1] = column + currentPosition[0] = row + currentPosition[1] = column if change.added - @change([startPosition, startPosition], change.value, changeOptions) + @change([currentPosition, currentPosition], change.value, changeOptions) row += lineCount column = computeBufferColumn(change.value) else if change.removed - endBufferRow = row + lineCount - endBufferColumn = column + computeBufferColumn(change.value) - @change([startPosition, [endBufferRow, endBufferColumn]], '', changeOptions) + endRow = row + lineCount + endColumn = column + computeBufferColumn(change.value) + @change([currentPosition, [endRow, endColumn]], '', changeOptions) else row += lineCount From 7111961929cdbda901aa4c5a7b71ab8dfcab0ffa Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 11 Dec 2013 17:56:38 -0800 Subject: [PATCH 6/8] Move function into setTextViaDiff() --- src/text-buffer.coffee | 85 ++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index b3a60c09f..6d3542eec 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -203,7 +203,47 @@ class TextBuffer extends telepath.Model # # text - A {String} containing the new buffer contents. setTextViaDiff: (text) -> - @applyDifferences(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. # @@ -674,46 +714,3 @@ class TextBuffer extends telepath.Model for row in [start..end] line = @lineForRow(row) console.log row, line, line.length - - applyDifferences: (newText) -> - currentText = @getText() - return if currentText == newText - - 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, newText) - 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) From 4dbca94d32a29055cbe4b8c573a4c062599aaa08 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 11 Dec 2013 18:00:13 -0800 Subject: [PATCH 7/8] spec :lipstick: --- spec/text-buffer-spec.coffee | 53 ++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/spec/text-buffer-spec.coffee b/spec/text-buffer-spec.coffee index 963edd932..0aaf9a3b5 100644 --- a/spec/text-buffer-spec.coffee +++ b/spec/text-buffer-spec.coffee @@ -462,37 +462,38 @@ describe 'TextBuffer', -> expect(event.newRange).toEqual [[0, 0], [1, 14]] describe ".setTextViaDiff(text)", -> - it "changes the entire contents of the buffer with smaller content with no newline at the end", -> - newText = "I know you are.\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.\nBut what am I?\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 - - it "adds a newline at the end", -> - newText = buffer.getText() + '\n' - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "changes all with no newlines", -> + 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')) From 4ffa5bb90e1b6b9708cb989ad6b4d2d6115e3af7 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 11 Dec 2013 18:22:14 -0800 Subject: [PATCH 8/8] Make this fn private. --- src/text-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 6d3542eec..76f897e3c 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -199,7 +199,7 @@ class TextBuffer extends telepath.Model setText: (text) -> @change(@getRange(), text, normalizeLineEndings: false) - # Replaces the current buffer contents. Only apply the differences. + # Private: Replaces the current buffer contents. Only apply the differences. # # text - A {String} containing the new buffer contents. setTextViaDiff: (text) ->