From 98614592af81712bd0626682811ecbf04b2d88c8 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Sun, 27 Jan 2013 15:45:53 -0800 Subject: [PATCH] Store line endings on a per-line basis in Buffer The line ending for each line is recorded and reused when lines are modified or inserted. Closes #166 --- spec/app/buffer-spec.coffee | 48 ++++++++++++++++++++++++++ spec/app/editor-spec.coffee | 2 +- spec/app/language-mode-spec.coffee | 6 ++-- src/app/buffer-change-operation.coffee | 44 ++++++++++++++++------- src/app/buffer.coffee | 26 ++++++++------ src/app/editor.coffee | 7 ++-- src/app/screen-line.coffee | 2 +- src/app/token.coffee | 3 -- src/app/tokenized-buffer.coffee | 3 +- 9 files changed, 108 insertions(+), 33 deletions(-) diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index a93bae30a..ea10907cf 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -833,3 +833,51 @@ describe 'Buffer', -> expect(buffer.getText()).toBe "a" buffer.append("b\nc"); expect(buffer.getText()).toBe "ab\nc" + + describe "line ending support", -> + describe ".lineEndingForRow(line)", -> + it "return the line ending for each buffer line", -> + buffer.setText("a\r\nb\nc") + expect(buffer.lineEndingForRow(0)).toBe '\r\n' + expect(buffer.lineEndingForRow(1)).toBe '\n' + expect(buffer.lineEndingForRow(2)).toBeUndefined() + + describe ".lineForRow(line)", -> + it "returns the line text without the line ending for both lf and crlf lines", -> + buffer.setText("a\r\nb\nc") + expect(buffer.lineForRow(0)).toBe 'a' + expect(buffer.lineForRow(1)).toBe 'b' + expect(buffer.lineForRow(2)).toBe 'c' + + describe ".getText()", -> + it "returns the text with the corrent line endings for each row", -> + buffer.setText("a\r\nb\nc") + expect(buffer.getText()).toBe "a\r\nb\nc" + buffer.setText("a\r\nb\nc\n") + expect(buffer.getText()).toBe "a\r\nb\nc\n" + + describe "when editing a line", -> + it "preserves the existing line ending", -> + buffer.setText("a\r\nb\nc") + buffer.insert([0, 1], "1") + expect(buffer.getText()).toBe "a1\r\nb\nc" + + describe "when inserting text with multiple lines", -> + describe "when the current line has a line ending", -> + it "uses the same line ending as the line where the text is inserted", -> + buffer.setText("a\r\n") + buffer.insert([0,1], "hello\n1\n\n2") + expect(buffer.getText()).toBe "ahello\r\n1\r\n\r\n2\r\n" + + describe "when the current line has no line ending (because it's the last line of the buffer)", -> + describe "when the buffer contains only a single line", -> + it "honors the line endings in the inserted text", -> + buffer.setText("initialtext") + buffer.append("hello\n1\r\n2\n") + expect(buffer.getText()).toBe "initialtexthello\n1\r\n2\n" + + describe "when the buffer contains a preceding line", -> + it "uses the line ending of the preceding line", -> + buffer.setText("\ninitialtext") + buffer.append("hello\n1\r\n2\n") + expect(buffer.getText()).toBe "\ninitialtexthello\n1\n2\n" diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index f5a86ccbf..445f2faf1 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1609,7 +1609,7 @@ describe "Editor", -> editor.attachToDom() expect(config.get("editor.showInvisibles")).toBeFalsy() - expect(editor.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return\n" + expect(editor.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return" config.set("editor.showInvisibles", true) cr = editor.invisibles?.cr diff --git a/spec/app/language-mode-spec.coffee b/spec/app/language-mode-spec.coffee index 9e1cd8b71..775baefa1 100644 --- a/spec/app/language-mode-spec.coffee +++ b/spec/app/language-mode-spec.coffee @@ -308,16 +308,16 @@ describe "LanguageMode", -> expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;" it "uncomments lines with leading whitespace", -> - buffer.replaceLines(2, 2, " /*width: 110%;*/") + buffer.change([[2, 0], [2, Infinity]], " /*width: 110%;*/") languageMode.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe " width: 110%;" it "uncomments lines with trailing whitespace", -> - buffer.replaceLines(2, 2, "/*width: 110%;*/ ") + buffer.change([[2, 0], [2, Infinity]], "/*width: 110%;*/ ") languageMode.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe "width: 110%; " it "uncomments lines with leading and trailing whitespace", -> - buffer.replaceLines(2, 2, " /*width: 110%;*/ ") + buffer.change([[2, 0], [2, Infinity]], " /*width: 110%;*/ ") languageMode.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe " width: 110%; " diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index 6fd0f2654..c50bb2308 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -1,4 +1,5 @@ Range = require 'range' +_ = require 'underscore' module.exports = class BufferChangeOperation @@ -26,18 +27,37 @@ class BufferChangeOperation oldText: @newText newText: @oldText + splitLines: (text) -> + lines = text.split('\n') + lineEndings = [] + for line, index in lines + if _.endsWith(line, '\r') + lines[index] = line[0..-2] + lineEndings[index] = '\r\n' + else + lineEndings[index] = '\n' + {lines, lineEndings} + changeBuffer: ({ oldRange, newRange, newText, oldText }) -> { prefix, suffix } = @buffer.prefixAndSuffixForRange(oldRange) - newTextLines = newText.split('\n') - if newTextLines.length == 1 - newTextLines = [prefix + newText + suffix] - else - lastLineIndex = newTextLines.length - 1 - newTextLines[0] = prefix + newTextLines[0] - newTextLines[lastLineIndex] += suffix + {lines, lineEndings} = @splitLines(newText) + lastLineIndex = lines.length - 1 - @buffer.replaceLines(oldRange.start.row, oldRange.end.row, newTextLines) + if lines.length == 1 + lines = [prefix + newText + suffix] + else + lines[0] = prefix + lines[0] + lines[lastLineIndex] += suffix + + startRow = oldRange.start.row + endRow = oldRange.end.row + if suggestedLineEnding = @buffer.suggestedLineEndingForRow(startRow) + lineEndings[index] = suggestedLineEnding for index in [0..lastLineIndex] + @buffer.lines[startRow..endRow] = lines + @buffer.lineEndings[startRow..endRow] = lineEndings + @buffer.cachedMemoryContents = null + @buffer.conflict = false if @buffer.conflict and !@buffer.isModified() event = { oldRange, newRange, oldText, newText } @buffer.trigger 'changed', event @@ -47,11 +67,11 @@ class BufferChangeOperation calculateNewRange: (oldRange, newText) -> newRange = new Range(oldRange.start.copy(), oldRange.start.copy()) - newTextLines = newText.split('\n') - if newTextLines.length == 1 + {lines} = @splitLines(newText) + if lines.length == 1 newRange.end.column += newText.length else - lastLineIndex = newTextLines.length - 1 + lastLineIndex = lines.length - 1 newRange.end.row += lastLineIndex - newRange.end.column = newTextLines[lastLineIndex].length + newRange.end.column = lines[lastLineIndex].length newRange diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 9b205b7e0..cbff0b2d9 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -19,6 +19,7 @@ class Buffer cachedMemoryContents: null conflict: false lines: null + lineEndings: null file: null anchors: null anchorRanges: null @@ -29,6 +30,7 @@ class Buffer @anchors = [] @anchorRanges = [] @lines = [''] + @lineEndings = [] if path throw "Path '#{path}' does not exist" unless fs.exists(path) @@ -104,9 +106,10 @@ class Buffer null getText: -> - @cachedMemoryContents ?= @lines.join('\n') + @cachedMemoryContents ?= @getTextInRange(@getRange()) setText: (text) -> + @lineEndings = [] @change(@getRange(), text) getRange: -> @@ -118,12 +121,14 @@ class Buffer return @lines[range.start.row][range.start.column...range.end.column] multipleLines = [] - multipleLines.push @lines[range.start.row][range.start.column..] # first line + multipleLines.push @lineForRow(range.start.row)[range.start.column..] # first line + multipleLines.push @lineEndingForRow(range.start.row) for row in [range.start.row + 1...range.end.row] - multipleLines.push @lines[row] # middle lines - multipleLines.push @lines[range.end.row][0...range.end.column] # last line + multipleLines.push @lineForRow(row) # middle lines + multipleLines.push @lineEndingForRow(row) + multipleLines.push @lineForRow(range.end.row)[0...range.end.column] # last line - return multipleLines.join '\n' + return multipleLines.join '' getLines: -> @lines @@ -131,6 +136,12 @@ class Buffer lineForRow: (row) -> @lines[row] + lineEndingForRow: (row) -> + @lineEndings[row] unless row is @getLastRow() + + suggestedLineEndingForRow: (row) -> + @lineEndingForRow(row) ? @lineEndingForRow(row - 1) + lineLengthForRow: (row) -> @lines[row].length @@ -214,11 +225,6 @@ class Buffer prefix: @lines[range.start.row][0...range.start.column] suffix: @lines[range.end.row][range.end.column..] - replaceLines: (startRow, endRow, newLines) -> - @lines[startRow..endRow] = newLines - @cachedMemoryContents = null - @conflict = false if @conflict and !@isModified() - pushOperation: (operation, editSession) -> if @undoManager @undoManager.pushOperation(operation, editSession) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index bd4f93934..b986c1e6a 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -1076,8 +1076,11 @@ class Editor extends View position += token.value.length popScope() while scopeStack.length > 0 - if not @mini and invisibles?.eol - line.push("") + if invisibles and not @mini + if invisibles.cr and screenLine.lineEnding is '\r\n' + line.push("") + if invisibles.eol + line.push("") line.push('') line.join('') diff --git a/src/app/screen-line.coffee b/src/app/screen-line.coffee index 31d93e879..2d01a07b1 100644 --- a/src/app/screen-line.coffee +++ b/src/app/screen-line.coffee @@ -2,7 +2,7 @@ _ = require 'underscore' module.exports = class ScreenLine - constructor: ({tokens, @ruleStack, @bufferRows, @startBufferColumn, @fold, tabLength}) -> + constructor: ({tokens, @lineEnding, @ruleStack, @bufferRows, @startBufferColumn, @fold, tabLength}) -> @tokens = @breakOutAtomicTokens(tokens, tabLength) @bufferRows ?= 1 @startBufferColumn ?= 0 diff --git a/src/app/token.coffee b/src/app/token.coffee index 0c595c908..e141dd7bf 100644 --- a/src/app/token.coffee +++ b/src/app/token.coffee @@ -80,8 +80,5 @@ class Token if hasTrailingWhitespace html = html.replace /[ ]+$/, (match) -> "" - if invisibles.cr - html = html.replace /\r$/, (match) -> - "" html diff --git a/src/app/tokenized-buffer.coffee b/src/app/tokenized-buffer.coffee index 6646fd54f..9e0930bf0 100644 --- a/src/app/tokenized-buffer.coffee +++ b/src/app/tokenized-buffer.coffee @@ -137,8 +137,9 @@ class TokenizedBuffer buildTokenizedScreenLineForRow: (row, ruleStack) -> line = @buffer.lineForRow(row) + lineEnding = @buffer.lineEndingForRow(row) { tokens, ruleStack } = @languageMode.tokenizeLine(line, ruleStack, row is 0) - new ScreenLine({tokens, ruleStack, @tabLength}) + new ScreenLine({tokens, ruleStack, @tabLength, lineEnding}) lineForScreenRow: (row) -> @linesForScreenRows(row, row)[0]