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("#{invisibles.eol}")
+ if invisibles and not @mini
+ if invisibles.cr and screenLine.lineEnding is '\r\n'
+ line.push("#{invisibles.cr}")
+ if invisibles.eol
+ line.push("#{invisibles.eol}")
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) ->
"#{match.replace(/./g, invisibles.space)}"
- if invisibles.cr
- html = html.replace /\r$/, (match) ->
- "#{invisibles.cr}"
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]