diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee
index 56c2dbfb6..d6bddd95d 100644
--- a/spec/editor-component-spec.coffee
+++ b/spec/editor-component-spec.coffee
@@ -90,6 +90,51 @@ describe "EditorComponent", ->
expect(component.lineNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels
expect(component.lineNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels
+ describe "when showInvisibles is enabled", ->
+ invisibles = null
+
+ beforeEach ->
+ invisibles =
+ eol: 'E'
+ space: 'S'
+ tab: 'T'
+ cr: 'C'
+
+ atom.config.set("editor.showInvisibles", true)
+ atom.config.set("editor.invisibles", invisibles)
+
+ it "re-renders the lines when the showInvisibles config option changes", ->
+ editor.setText " a line with tabs\tand spaces "
+
+ expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab} and spaces#{invisibles.space}#{invisibles.eol}"
+ atom.config.set("editor.showInvisibles", false)
+ expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces "
+ atom.config.set("editor.showInvisibles", true)
+ expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab} and spaces#{invisibles.space}#{invisibles.eol}"
+
+ it "displays spaces, tabs, and newlines as visible characters", ->
+ editor.setText " a line with tabs\tand spaces "
+ expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab} and spaces#{invisibles.space}#{invisibles.eol}"
+
+ it "displays newlines as their own token outside of the other tokens' scopes", ->
+ editor.setText "var"
+ expect(component.lineNodeForScreenRow(0).innerHTML).toBe "var#{invisibles.eol}"
+
+ it "displays trailing carriage returns using a visible, non-empty value", ->
+ editor.setText "a line that ends with a carriage return\r\n"
+ expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that ends with a carriage return#{invisibles.cr}#{invisibles.eol}"
+
+ describe "when soft wrapping is enabled", ->
+ beforeEach ->
+ editor.setText "a line that wraps "
+ editor.setSoftWrap(true)
+ node.style.width = 15 * charWidth + 'px'
+ component.measureHeightAndWidth()
+
+ it "doesn't show end of line invisibles at the end of wrapped lines", ->
+ expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that "
+ expect(component.lineNodeForScreenRow(1).textContent).toBe "wraps#{invisibles.space}#{invisibles.eol}"
+
describe "when indent guides are enabled", ->
beforeEach ->
component.setShowIndentGuide(true)
diff --git a/src/editor-component.coffee b/src/editor-component.coffee
index 7ce1e5350..bf5904d53 100644
--- a/src/editor-component.coffee
+++ b/src/editor-component.coffee
@@ -1,6 +1,6 @@
React = require 'react'
{div, span} = require 'reactionary'
-{debounce} = require 'underscore-plus'
+{debounce, defaults} = require 'underscore-plus'
scrollbarStyle = require 'scrollbar-style'
GutterComponent = require './gutter-component'
@@ -31,9 +31,10 @@ EditorComponent = React.createClass
mouseWheelScreenRow: null
render: ->
- {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state
+ {focused, fontSize, lineHeight, fontFamily, showIndentGuide, showInvisibles} = @state
{editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
maxLineNumberDigits = editor.getScreenLineCount().toString().length
+ invisibles = if showInvisibles then @state.invisibles else {}
if @isMounted()
renderedRowRange = @getRenderedRowRange()
@@ -62,7 +63,8 @@ EditorComponent = React.createClass
lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges,
scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically,
@cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkPeriod,
- cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred, @mouseWheelScreenRow
+ cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred, @mouseWheelScreenRow,
+ invisibles
}
ScrollbarComponent
@@ -101,7 +103,7 @@ EditorComponent = React.createClass
{editor, lineOverdrawMargin} = @props
[visibleStartRow, visibleEndRow] = editor.getVisibleRowRange()
renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin)
- renderedEndRow = Math.min(editor.getLineCount(), visibleEndRow + lineOverdrawMargin)
+ renderedEndRow = Math.min(editor.getScreenLineCount(), visibleEndRow + lineOverdrawMargin)
[renderedStartRow, renderedEndRow]
getInitialState: -> {}
@@ -269,6 +271,8 @@ EditorComponent = React.createClass
@subscribe atom.config.observe 'editor.fontFamily', @setFontFamily
@subscribe atom.config.observe 'editor.fontSize', @setFontSize
@subscribe atom.config.observe 'editor.showIndentGuide', @setShowIndentGuide
+ @subscribe atom.config.observe 'editor.invisibles', @setInvisibles
+ @subscribe atom.config.observe 'editor.showInvisibles', @setShowInvisibles
measureScrollbars: ->
@measuringScrollbars = false
@@ -292,6 +296,25 @@ EditorComponent = React.createClass
setShowIndentGuide: (showIndentGuide) ->
@setState({showIndentGuide})
+ # Public: Defines which characters are invisible.
+ #
+ # invisibles - An {Object} defining the invisible characters:
+ # :eol - The end of line invisible {String} (default: `\u00ac`).
+ # :space - The space invisible {String} (default: `\u00b7`).
+ # :tab - The tab invisible {String} (default: `\u00bb`).
+ # :cr - The carriage return invisible {String} (default: `\u00a4`).
+ setInvisibles: (invisibles={}) ->
+ defaults invisibles,
+ eol: '\u00ac'
+ space: '\u00b7'
+ tab: '\u00bb'
+ cr: '\u00a4'
+
+ @setState({invisibles})
+
+ setShowInvisibles: (showInvisibles) ->
+ @setState({showInvisibles})
+
onFocus: ->
@refs.scrollView.focus()
diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee
index 966d5146e..81b37ce8b 100644
--- a/src/editor-scroll-view-component.coffee
+++ b/src/editor-scroll-view-component.coffee
@@ -16,7 +16,7 @@ EditorScrollViewComponent = React.createClass
overflowChangedWhilePaused: false
render: ->
- {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props
+ {editor, fontSize, fontFamily, lineHeight, showIndentGuide, invisibles} = @props
{renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically, mouseWheelScreenRow} = @props
{selectionChanged, selectionAdded, cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props
@@ -37,7 +37,7 @@ EditorScrollViewComponent = React.createClass
LinesComponent {
ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide,
renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically,
- selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow
+ selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles
}
componentDidMount: ->
diff --git a/src/lines-component.coffee b/src/lines-component.coffee
index 7a51605ad..8b4dc29fa 100644
--- a/src/lines-component.coffee
+++ b/src/lines-component.coffee
@@ -35,7 +35,7 @@ LinesComponent = React.createClass
shouldComponentUpdate: (newProps) ->
return true if newProps.selectionChanged
- return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically')
+ return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', 'invisibles')
{renderedRowRange, pendingChanges} = newProps
for change in pendingChanges
@@ -46,7 +46,7 @@ LinesComponent = React.createClass
componentDidUpdate: (prevProps) ->
@measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight')
@clearScreenRowCaches() unless prevProps.lineHeight is @props.lineHeight
- @removeLineNodes() unless prevProps.showIndentGuide is @props.showIndentGuide
+ @removeLineNodes() unless isEqualForProperties(prevProps, @props, 'showIndentGuide', 'invisibles')
@updateLines()
@clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily')
@measureCharactersInNewLines() unless @props.scrollingVertically
@@ -132,7 +132,7 @@ LinesComponent = React.createClass
" "
buildLineInnerHTML: (line) ->
- {invisibles, mini, showIndentGuide} = @props
+ {invisibles, mini, showIndentGuide, invisibles} = @props
{tokens, text} = line
innerHTML = ""
@@ -143,9 +143,22 @@ LinesComponent = React.createClass
innerHTML += @updateScopeStack(scopeStack, token.scopes)
hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly)
innerHTML += token.getValueAsHtml({invisibles, hasIndentGuide})
+
innerHTML += @popScope(scopeStack) while scopeStack.length > 0
+ innerHTML += @buildEndOfLineHTML(line, invisibles)
innerHTML
+ buildEndOfLineHTML: (line, invisibles) ->
+ return '' if @props.mini or line.isSoftWrapped()
+
+ html = ''
+ if invisibles.cr? and line.lineEnding is '\r\n'
+ html += "#{invisibles.cr}"
+ if invisibles.eol?
+ html += "#{invisibles.eol}"
+
+ html
+
updateScopeStack: (scopeStack, desiredScopes) ->
html = ""