diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 4e883cc78..c0b194a5a 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -91,7 +91,7 @@ describe "TokenizedBuffer.", -> { languageMode, buffer } = editSession benchmark "construction", 20, -> - new TokenizedBuffer(buffer, { languageMode, tabText: ' '}) + new TokenizedBuffer(buffer, { languageMode, tabLength: 2}) describe "OnigRegExp.", -> [regexes, line] = [] diff --git a/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee index 562501b2b..142fabaa6 100644 --- a/spec/app/display-buffer-spec.coffee +++ b/spec/app/display-buffer-spec.coffee @@ -2,10 +2,10 @@ DisplayBuffer = require 'display-buffer' Buffer = require 'buffer' describe "DisplayBuffer", -> - [editSession, displayBuffer, buffer, changeHandler, tabText] = [] + [editSession, displayBuffer, buffer, changeHandler, tabLength] = [] beforeEach -> - tabText = ' ' - editSession = fixturesProject.buildEditSessionForPath('sample.js', { tabText }) + tabLength = 2 + editSession = fixturesProject.buildEditSessionForPath('sample.js', { tabLength }) { buffer, displayBuffer } = editSession changeHandler = jasmine.createSpy 'changeHandler' displayBuffer.on 'change', changeHandler @@ -636,14 +636,14 @@ describe "DisplayBuffer", -> buffer.insert([0, 0], '\t') expect(displayBuffer.clipScreenPosition([0, 0])).toEqual [0, 0] expect(displayBuffer.clipScreenPosition([0, 1])).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, tabText.length])).toEqual [0, tabText.length] + expect(displayBuffer.clipScreenPosition([0, tabLength])).toEqual [0, tabLength] describe "when skipAtomicTokens is true", -> it "clips screen positions in the middle of atomic tab characters to the end of the character", -> buffer.insert([0, 0], '\t') expect(displayBuffer.clipScreenPosition([0, 0], skipAtomicTokens: true)).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, 1], skipAtomicTokens: true)).toEqual [0, tabText.length] - expect(displayBuffer.clipScreenPosition([0, tabText.length], skipAtomicTokens: true)).toEqual [0, tabText.length] + expect(displayBuffer.clipScreenPosition([0, 1], skipAtomicTokens: true)).toEqual [0, tabLength] + expect(displayBuffer.clipScreenPosition([0, tabLength], skipAtomicTokens: true)).toEqual [0, tabLength] describe ".maxLineLength()", -> it "returns the length of the longest screen line", -> diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 3347450e0..788859373 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1141,8 +1141,8 @@ describe "EditSession", -> describe ".indent()", -> describe "when nothing is selected", -> describe "if 'softTabs' is true (the default)", -> - it "inserts the value of 'tabText' into the buffer", -> - tabRegex = new RegExp("^#{editSession.tabText}") + it "inserts 'tabLength' spaces into the buffer", -> + tabRegex = new RegExp("^[ ]{#{editSession.tabLength}}") expect(buffer.lineForRow(0)).not.toMatch(tabRegex) editSession.indent() expect(buffer.lineForRow(0)).toMatch(tabRegex) @@ -1151,7 +1151,7 @@ describe "EditSession", -> describe "when the preceding line opens a new level of indentation", -> it "increases the level of indentation by one", -> buffer.insert([5, 0], " \n") - editSession.tabText = " " + editSession.tabLength = 2 editSession.setCursorBufferPosition [5, 2] editSession.setAutoIndent(true) editSession.indent() @@ -1162,7 +1162,7 @@ describe "EditSession", -> describe "when there are empty lines preceding the current line", -> it "bases indentation on the first non-blank preceding line", -> buffer.insert([5, 0], "\n\n\n \n") - editSession.tabText = " " + editSession.tabLength = 2 editSession.setCursorBufferPosition [8, 2] editSession.setAutoIndent(true) editSession.indent() @@ -1172,7 +1172,7 @@ describe "EditSession", -> it "properly indents the line", -> buffer.insert([7, 0], " \n") - editSession.tabText = " " + editSession.tabLength = 2 editSession.setCursorBufferPosition [7, 2] editSession.setAutoIndent(true) editSession.indent() @@ -1182,7 +1182,7 @@ describe "EditSession", -> it "allows for additional indentation if the cursor is beyond the proper indentation point", -> buffer.insert([7, 0], " \n") - editSession.tabText = " " + editSession.tabLength = 2 editSession.setCursorBufferPosition [7, 6] editSession.setAutoIndent(true) editSession.indent() @@ -1197,12 +1197,12 @@ describe "EditSession", -> editSession.indent() expect(buffer.lineForRow(0)).toMatch(/^\t/) expect(editSession.getCursorBufferPosition()).toEqual [0, 1] - expect(editSession.getCursorScreenPosition()).toEqual [0, editSession.tabText.length] + expect(editSession.getCursorScreenPosition()).toEqual [0, editSession.tabLength] editSession.indent() expect(buffer.lineForRow(0)).toMatch(/^\t\t/) expect(editSession.getCursorBufferPosition()).toEqual [0, 2] - expect(editSession.getCursorScreenPosition()).toEqual [0, editSession.tabText.length * 2] + expect(editSession.getCursorScreenPosition()).toEqual [0, editSession.tabLength * 2] describe "pasteboard operations", -> pasteboard = null @@ -1254,24 +1254,22 @@ describe "EditSession", -> expect(buffer.lineForRow(1)).toBe " var first = function(items) {" describe ".indentSelectedRows()", -> - tabLength = null - beforeEach -> - tabLength = editSession.tabText.length + editSession.tabLength = 2 describe "when nothing is selected", -> it "indents line and retains selection", -> editSession.setSelectedBufferRange([[0,3], [0,3]]) editSession.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "#{editSession.tabText}var quicksort = function () {" - expect(editSession.getSelectedBufferRange()).toEqual [[0, 3 + tabLength], [0, 3 + tabLength]] + expect(buffer.lineForRow(0)).toBe "#{editSession.getTabText()}var quicksort = function () {" + expect(editSession.getSelectedBufferRange()).toEqual [[0, 3 + editSession.tabLength], [0, 3 + editSession.tabLength]] describe "when one line is selected", -> it "indents line and retains selection", -> editSession.setSelectedBufferRange([[0,4], [0,14]]) editSession.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "#{editSession.tabText}var quicksort = function () {" - expect(editSession.getSelectedBufferRange()).toEqual [[0, 4 + tabLength], [0, 14 + tabLength]] + expect(buffer.lineForRow(0)).toBe "#{editSession.getTabText()}var quicksort = function () {" + expect(editSession.getSelectedBufferRange()).toEqual [[0, 4 + editSession.tabLength], [0, 14 + editSession.tabLength]] describe "when multiple lines are selected", -> it "indents selected lines (that are not empty) and retains selection", -> @@ -1280,28 +1278,25 @@ describe "EditSession", -> expect(buffer.lineForRow(9)).toBe " };" expect(buffer.lineForRow(10)).toBe "" expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editSession.getSelectedBufferRange()).toEqual [[9, 1 + tabLength], [11, 15 + tabLength]] + expect(editSession.getSelectedBufferRange()).toEqual [[9, 1 + editSession.tabLength], [11, 15 + editSession.tabLength]] describe ".outdentSelectedRows()", -> - tabLength = null - beforeEach -> - editSession.tabText = " " - tabLength = editSession.tabText.length + editSession.tabLength = 2 describe "when nothing is selected", -> it "outdents line and retains selection", -> editSession.setSelectedBufferRange([[1,3], [1,3]]) editSession.outdentSelectedRows() expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editSession.getSelectedBufferRange()).toEqual [[1, 3 - tabLength], [1, 3 - tabLength]] + expect(editSession.getSelectedBufferRange()).toEqual [[1, 3 - editSession.tabLength], [1, 3 - editSession.tabLength]] describe "when one line is selected", -> it "outdents line and retains editSession", -> editSession.setSelectedBufferRange([[1,4], [1,14]]) editSession.outdentSelectedRows() expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editSession.getSelectedBufferRange()).toEqual [[1, 4 - tabLength], [1, 14 - tabLength]] + expect(editSession.getSelectedBufferRange()).toEqual [[1, 4 - editSession.tabLength], [1, 14 - editSession.tabLength]] describe "when multiple lines are selected", -> it "outdents selected lines and retains editSession", -> @@ -1310,7 +1305,7 @@ describe "EditSession", -> expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [3, 15 - tabLength]] + expect(editSession.getSelectedBufferRange()).toEqual [[0, 1], [3, 15 - editSession.tabLength]] describe ".toggleLineCommentsInSelection()", -> it "toggles comments on the selected lines", -> diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 93fdc36a7..c544a5b01 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1472,6 +1472,17 @@ describe "Editor", -> expect(editor.renderedLines.find('.line:eq(14)').text()).toBe 'B' expect(editor.renderedLines.find('.line:eq(15)')).not.toExist() + describe "when editor.setShowInvisibles is called", -> + it "displays spaces as •, tabs as ▸ and newlines as ¬ when true", -> + editor.attachToDom() + editor.setText " a line with tabs\tand spaces " + expect(editor.showInvisibles).toBeFalsy() + expect(editor.find('.line').text()).toBe " a line with tabs and spaces " + editor.setShowInvisibles(true) + expect(editor.find('.line').text()).toBe "•a line with tabs▸ and spaces•¬" + editor.setShowInvisibles(false) + expect(editor.find('.line').text()).toBe " a line with tabs and spaces " + describe "gutter rendering", -> beforeEach -> editor.attachToDom(heightInLines: 5.5) @@ -1595,7 +1606,6 @@ describe "Editor", -> expect(miniEditor.getCursorBufferPosition().row).toBe 0 expect(miniEditor.find('.line.cursor-line').length).toBe 0 - describe "gutter line highlighting", -> beforeEach -> editor.attachToDom(heightInLines: 5.5) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 8e1c2ca81..31e612115 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -690,3 +690,27 @@ describe "RootView", -> expect(fs.read(buffer1.getPath())).toBe("edited1") expect(buffer2.isModified()).toBe(false) expect(fs.read(buffer2.getPath())).toBe("edited2") + + describe "window:toggle-invisibles event", -> + it "shows/hides invisibles in all open and future editors", -> + rootView.height(200) + rootView.attachToDom() + rightEditor = rootView.getActiveEditor() + rightEditor.setText(" \t ") + leftEditor = rightEditor.splitLeft() + expect(rightEditor.find(".line:first").text()).toBe " " + expect(leftEditor.find(".line:first").text()).toBe " " + + rootView.trigger "root-view:toggle-invisibles" + expect(rightEditor.find(".line:first").text()).toBe "•▸ •¬" + expect(leftEditor.find(".line:first").text()).toBe "•▸ •¬" + + lowerLeftEditor = leftEditor.splitDown() + expect(lowerLeftEditor.find(".line:first").text()).toBe "•▸ •¬" + + rootView.trigger "root-view:toggle-invisibles" + expect(rightEditor.find(".line:first").text()).toBe " " + expect(leftEditor.find(".line:first").text()).toBe " " + + lowerRightEditor = rightEditor.splitDown() + expect(lowerRightEditor.find(".line:first").text()).toBe " " diff --git a/spec/app/screen-line-spec.coffee b/spec/app/screen-line-spec.coffee index 4d0d9d181..9ea847856 100644 --- a/spec/app/screen-line-spec.coffee +++ b/spec/app/screen-line-spec.coffee @@ -3,11 +3,10 @@ Buffer = require 'buffer' TokenizedBuffer = require 'tokenized-buffer' describe "ScreenLine", -> - [editSession, buffer, tabText, screenLine, tokenizedBuffer] = [] + [editSession, buffer, screenLine, tokenizedBuffer] = [] beforeEach -> - tabText = '••' - editSession = fixturesProject.buildEditSessionForPath('sample.js', { tabText } ) + editSession = fixturesProject.buildEditSessionForPath('sample.js', { tabLength: 2 } ) { buffer, tokenizedBuffer } = editSession screenLine = tokenizedBuffer.lineForScreenRow(3) diff --git a/spec/app/selection-spec.coffee b/spec/app/selection-spec.coffee index a43ad7f1e..27427ae7a 100644 --- a/spec/app/selection-spec.coffee +++ b/spec/app/selection-spec.coffee @@ -7,7 +7,7 @@ describe "Selection", -> beforeEach -> buffer = new Buffer(require.resolve('fixtures/sample.js')) - editSession = new EditSession(buffer: buffer, tabText: ' ') + editSession = new EditSession(buffer: buffer, tabLength: 2) selection = editSession.getSelection() afterEach -> diff --git a/spec/app/token-spec.coffee b/spec/app/token-spec.coffee deleted file mode 100644 index 08758bf91..000000000 --- a/spec/app/token-spec.coffee +++ /dev/null @@ -1,16 +0,0 @@ -_ = require 'underscore' -Buffer = require 'buffer' -TokenizedBuffer = require 'tokenized-buffer' - -describe "Token", -> - [editSession, token] = [] - - beforeEach -> - tabText = ' ' - editSession = fixturesProject.buildEditSessionForPath('sample.js') - { tokenizedBuffer } = editSession - screenLine = tokenizedBuffer.lineForScreenRow(3) - token = _.last(screenLine.tokens) - - afterEach -> - editSession.destroy() diff --git a/spec/app/tokenized-buffer-spec.coffee b/spec/app/tokenized-buffer-spec.coffee index c3cd3dd1a..11a188e0a 100644 --- a/spec/app/tokenized-buffer-spec.coffee +++ b/spec/app/tokenized-buffer-spec.coffee @@ -151,25 +151,25 @@ describe "TokenizedBuffer", -> expect(event.newRange).toEqual new Range([2, 0], [7, buffer.lineForRow(7).length]) describe "when the buffer contains tab characters", -> - tabText = ' ' editSession2 = null beforeEach -> - editSession2 = fixturesProject.buildEditSessionForPath('sample-with-tabs.coffee', { tabText }) + tabLength = 2 + editSession2 = fixturesProject.buildEditSessionForPath('sample-with-tabs.coffee', { tabLength }) { buffer, tokenizedBuffer } = editSession2 afterEach -> editSession2.destroy() - it "always renders each tab as its own atomic token containing tabText", -> + it "always renders each tab as its own atomic token with a value of size tabLength", -> screenLine0 = tokenizedBuffer.lineForScreenRow(0) - expect(screenLine0.text).toBe "# Econ 101#{tabText}" + expect(screenLine0.text).toBe "# Econ 101#{editSession2.getTabText()}" { tokens } = screenLine0 expect(tokens.length).toBe 3 expect(tokens[0].value).toBe "#" expect(tokens[1].value).toBe " Econ 101" - expect(tokens[2].value).toBe tabText + expect(tokens[2].value).toBe editSession2.getTabText() expect(tokens[2].scopes).toEqual tokens[1].scopes expect(tokens[2].isAtomic).toBeTruthy() - expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "#{tabText} buy()#{tabText}while supply > demand" + expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "#{editSession2.getTabText()} buy()#{editSession2.getTabText()}while supply > demand" diff --git a/src/app/display-buffer.coffee b/src/app/display-buffer.coffee index edbdc2831..346b1183c 100644 --- a/src/app/display-buffer.coffee +++ b/src/app/display-buffer.coffee @@ -20,7 +20,6 @@ class DisplayBuffer constructor: (@buffer, options={}) -> @id = @constructor.idCounter++ - options.tabText ?= ' ' @languageMode = options.languageMode @tokenizedBuffer = new TokenizedBuffer(@buffer, options) @softWrapColumn = options.softWrapColumn ? Infinity diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index e861d00ac..10ba9e04c 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -35,14 +35,15 @@ class EditSession cursors: null selections: null autoIndent: false # TODO: re-enabled auto-indent after fixing the rest of tokenization + tabLength: null softTabs: true softWrap: false - constructor: ({@project, @buffer, @tabText, @autoIndent, @softTabs, @softWrap}) -> + constructor: ({@project, @buffer, @tabLength, @autoIndent, @softTabs, @softWrap }) -> @id = @constructor.idCounter++ @softTabs ?= true @languageMode = new LanguageMode(this, @buffer.getExtension()) - @displayBuffer = new DisplayBuffer(@buffer, { @languageMode, @tabText }) + @displayBuffer = new DisplayBuffer(@buffer, { @languageMode, @tabLength }) @tokenizedBuffer = @displayBuffer.tokenizedBuffer @anchors = [] @anchorRanges = [] @@ -106,6 +107,8 @@ class EditSession getSoftWrap: -> @softWrap setSoftWrap: (@softWrap) -> + getTabText: -> new Array(@tabLength + 1).join(" ") + clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) @@ -149,7 +152,7 @@ class EditSession currentRow = @getCursorBufferPosition().row if @getSelection().isEmpty() if @softTabs - @insertText(@tabText) + @insertText(@getTabText()) else @insertText('\t') else diff --git a/src/app/editor.coffee b/src/app/editor.coffee index ef6843aea..c1bef02d3 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -46,12 +46,12 @@ class Editor extends View @deserialize: (state, rootView) -> editSessions = state.editSessions.map (state) -> EditSession.deserialize(state, rootView.project) - editor = new Editor(editSession: editSessions[state.activeEditSessionIndex], mini: state.mini) + editor = new Editor(editSession: editSessions[state.activeEditSessionIndex], mini: state.mini, showInvisibles: rootView.showInvisibles) editor.editSessions = editSessions editor.isFocused = state.isFocused editor - initialize: ({editSession, @mini} = {}) -> + initialize: ({editSession, @mini, @showInvisibles} = {}) -> requireStylesheet 'editor.css' @id = Editor.idCounter++ @@ -69,7 +69,7 @@ class Editor extends View editSession = new EditSession buffer: new Buffer() softWrap: false - tabText: " " + tabLength: 2 autoIndent: false softTabs: true @@ -265,6 +265,11 @@ class Editor extends View getPageRows: -> Math.max(1, Math.ceil(@scrollView[0].clientHeight / @lineHeight)) + setShowInvisibles: (showInvisibles) -> + return if showInvisibles == @showInvisibles + @showInvisibles = showInvisibles + @renderLines() + setText: (text) -> @getBuffer().setText(text) getText: -> @getBuffer().getText() getPath: -> @getBuffer().getPath() @@ -569,7 +574,7 @@ class Editor extends View @updateRenderedLines() newSplitEditor: -> - new Editor { editSession: @activeEditSession.copy() } + new Editor { editSession: @activeEditSession.copy(), @showInvisibles } splitLeft: -> @pane()?.splitLeft(@newSplitEditor()).wrappedView @@ -874,20 +879,23 @@ class Editor extends View line.push("
")
if screenLine.text == ''
- line.push(' ')
+ line.push(" ") unless @showInvisibles
else
+ firstNonWhitespacePosition = screenLine.text.search(/\S/)
+ firstTrailingWhitespacePosition = screenLine.text.search(/\s*$/)
+ position = 0
for token in screenLine.tokens
updateScopeStack(token.scopes)
- line.push(
- token.value
- .replace(/&/g, '&')
- .replace(/"/g, '"')
- .replace(/'/g, ''')
- .replace(//g, '>')
- )
+ line.push(token.getValueAsHtml(
+ showInvisibles: @showInvisibles
+ hasLeadingWhitespace: position < firstNonWhitespacePosition
+ hasTrailingWhitespace: position + token.value.length > firstTrailingWhitespacePosition
+ ))
- line.push("")
+ position += token.value.length
+
+ line.push("¬") if @showInvisibles
+ line.push('')
line.join('')
insertLineElements: (row, lineElements) ->
diff --git a/src/app/language-mode.coffee b/src/app/language-mode.coffee
index 312a0f2f5..e2fac8b42 100644
--- a/src/app/language-mode.coffee
+++ b/src/app/language-mode.coffee
@@ -104,7 +104,7 @@ class LanguageMode
currentIndentation = @buffer.indentationForRow(bufferRow)
desiredIndentation = @buffer.indentationForRow(precedingRow)
- desiredIndentation += @editSession.tabText.length if increaseIndentPattern.test(precedingLine)
+ desiredIndentation += @editSession.tabLength if increaseIndentPattern.test(precedingLine)
if desiredIndentation > currentIndentation
@buffer.setIndentationForRow(bufferRow, desiredIndentation)
@@ -122,7 +122,7 @@ class LanguageMode
precedingLine = @buffer.lineForRow(precedingRow)
desiredIndentation = @buffer.indentationForRow(precedingRow)
- desiredIndentation -= @editSession.tabText.length unless increaseIndentPattern.test(precedingLine)
+ desiredIndentation -= @editSession.tabLength unless increaseIndentPattern.test(precedingLine)
if desiredIndentation < currentIndentation
@buffer.setIndentationForRow(bufferRow, desiredIndentation)
diff --git a/src/app/project.coffee b/src/app/project.coffee
index 1ce285833..dfe28a56d 100644
--- a/src/app/project.coffee
+++ b/src/app/project.coffee
@@ -10,7 +10,7 @@ ChildProcess = require 'child-process'
module.exports =
class Project
- tabText: ' '
+ tabLength: 2
autoIndent: true
softTabs: true
softWrap: false
@@ -101,9 +101,6 @@ class Project
relativize: (fullPath) ->
fullPath.replace(@getPath(), "").replace(/^\//, '')
- getTabText: -> @tabText
- setTabText: (@tabText) ->
-
getAutoIndent: -> @autoIndent
setAutoIndent: (@autoIndent) ->
@@ -126,7 +123,7 @@ class Project
editSession
defaultEditSessionOptions: ->
- tabText: @getTabText()
+ tabLength: @tabLength
autoIndent: @getAutoIndent()
softTabs: @getSoftTabs()
softWrap: @getSoftWrap()
diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee
index 38cb4f1e9..3f98d2d0f 100644
--- a/src/app/root-view.coffee
+++ b/src/app/root-view.coffee
@@ -31,6 +31,7 @@ class RootView extends View
extensions: null
extensionStates: null
fontSize: 20
+ showInvisibles: false
initialize: (pathToOpen, { @extensionStates, suppressOpen } = {}) ->
window.rootView = this
@@ -77,6 +78,7 @@ class RootView extends View
@on 'root-view:decrease-font-size', => @setFontSize(@getFontSize() - 1)
@on 'root-view:focus-next-pane', => @focusNextPane()
@on 'root-view:save-all', => @saveAll()
+ @on 'root-view:toggle-invisibles', => @setShowInvisibles(not @showInvisibles)
afterAttach: (onDom) ->
@focus() if onDom
@@ -117,7 +119,7 @@ class RootView extends View
unless editSession = @openInExistingEditor(path, allowActiveEditorChange, changeFocus)
editSession = @project.buildEditSessionForPath(path)
- editor = new Editor({editSession})
+ editor = new Editor({editSession, @showInvisibles})
pane = new Pane(editor)
@panes.append(pane)
if changeFocus
@@ -170,6 +172,11 @@ class RootView extends View
setTitle: (title='untitled') ->
document.title = title
+ setShowInvisibles: (showInvisibles) ->
+ return if @showInvisibles == showInvisibles
+ @showInvisibles = showInvisibles
+ editor.setShowInvisibles(@showInvisibles) for editor in @getEditors()
+
getEditors: ->
@panes.find('.pane > .editor').map(-> $(this).view()).toArray()
diff --git a/src/app/selection.coffee b/src/app/selection.coffee
index 0e0b711f7..4d050abba 100644
--- a/src/app/selection.coffee
+++ b/src/app/selection.coffee
@@ -208,15 +208,15 @@ class Selection
indentSelectedRows: ->
range = @getBufferRange()
for row in [range.start.row..range.end.row]
- @editSession.buffer.insert([row, 0], @editSession.tabText) unless @editSession.buffer.lineLengthForRow(row) == 0
+ @editSession.buffer.insert([row, 0], @editSession.getTabText()) unless @editSession.buffer.lineLengthForRow(row) == 0
outdentSelectedRows: ->
range = @getBufferRange()
buffer = @editSession.buffer
- leadingTabRegex = new RegExp("^#{@editSession.tabText}")
+ leadingTabRegex = new RegExp("^#{@editSession.getTabText()}")
for row in [range.start.row..range.end.row]
if leadingTabRegex.test buffer.lineForRow(row)
- buffer.delete [[row, 0], [row, @editSession.tabText.length]]
+ buffer.delete [[row, 0], [row, @editSession.tabLength]]
toggleLineComments: ->
@modifySelection =>
diff --git a/src/app/token.coffee b/src/app/token.coffee
index 80710ce9d..a413cd521 100644
--- a/src/app/token.coffee
+++ b/src/app/token.coffee
@@ -5,8 +5,9 @@ class Token
value: null
scopes: null
isAtomic: null
+ isTab: null
- constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @fold}) ->
+ constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @fold, @isTab}) ->
@screenDelta = @value.length
@bufferDelta ?= @screenDelta
@@ -21,14 +22,41 @@ class Token
value2 = @value.substring(splitIndex)
[new Token(value: value1, scopes: @scopes), new Token(value: value2, scopes: @scopes)]
- breakOutTabCharacters: (tabText) ->
+ breakOutTabCharacters: (tabLength, showInvisibles) ->
return [this] unless /\t/.test(@value)
- for substring in @value.match(/([^\t]+|\t)/g)
- if substring == '\t'
- @buildTabToken(tabText)
+ for substring in @value.match(/[^\t]+|\t/g)
+ if substring == "\t"
+ @buildTabToken(tabLength)
else
new Token(value: substring, scopes: @scopes)
- buildTabToken: (tabText) ->
- new Token(value: tabText, scopes: @scopes, bufferDelta: 1, isAtomic: true)
+ buildTabToken: (tabLength) ->
+ new Token(
+ value: new Array(tabLength + 1).join(" ")
+ scopes: @scopes
+ bufferDelta: 1
+ isAtomic: true
+ isTab: true
+ )
+
+ getValueAsHtml: ({showInvisibles, hasLeadingWhitespace, hasTrailingWhitespace})->
+ html = @value
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ .replace(//g, '>')
+
+ if showInvisibles
+ if @isTab
+ html = html.replace(/^./, "▸")
+ else
+ if hasLeadingWhitespace
+ html = html.replace /^[ ]+/, (match) ->
+ "#{match.replace(/./g, '•')}"
+ if hasTrailingWhitespace
+ html = html.replace /[ ]+$/, (match) ->
+ "#{match.replace(/./g, '•')}"
+
+ html
diff --git a/src/app/tokenized-buffer.coffee b/src/app/tokenized-buffer.coffee
index 02285a505..0e73574b8 100644
--- a/src/app/tokenized-buffer.coffee
+++ b/src/app/tokenized-buffer.coffee
@@ -10,11 +10,13 @@ class TokenizedBuffer
@idCounter: 1
languageMode: null
+ tabLength: null
buffer: null
aceAdaptor: null
screenLines: null
- constructor: (@buffer, { @languageMode, @tabText }) ->
+ constructor: (@buffer, { @languageMode, @tabLength }) ->
+ @tabLength ?= 2
@languageMode.tokenizedBuffer = this
@id = @constructor.idCounter++
@screenLines = @buildScreenLinesForRows(0, @buffer.getLastRow())
@@ -64,7 +66,7 @@ class TokenizedBuffer
tokenObjects = []
for tokenProperties in tokens
token = new Token(tokenProperties)
- tokenObjects.push(token.breakOutTabCharacters(@tabText)...)
+ tokenObjects.push(token.breakOutTabCharacters(@tabLength)...)
text = _.pluck(tokenObjects, 'value').join('')
new ScreenLine(tokenObjects, text, [1, 0], [1, 0], { stack })
diff --git a/static/editor.css b/static/editor.css
index ed4d1eb0f..c05e57a29 100644
--- a/static/editor.css
+++ b/static/editor.css
@@ -123,3 +123,7 @@
.editor .fold.selected {
background-color: #244;
}
+
+.editor .invisible {
+ opacity: 0.2;
+}