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 = ""