diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee
index d55d54d37..55d5c6937 100644
--- a/spec/app/editor-spec.coffee
+++ b/spec/app/editor-spec.coffee
@@ -1472,6 +1472,16 @@ describe "Editor", ->
expect(editor.renderedLines.find('.line:eq(14)').text()).toBe 'B'
expect(editor.renderedLines.find('.line:eq(15)')).not.toExist()
+ describe "when editSession.showInvisibles is true", ->
+ beforeEach ->
+ project.setShowInvisibles(true)
+ rootView.open()
+ editor.attachToDom(5)
+
+ it "displays spaces as •, tabs as ▸ and newlines as ¬", ->
+ editor.setText " a line with tabs\tand spaces "
+ expect(editor.find('.line').text()).toBe "•a line with tabs▸ and spaces•¬"
+
describe "gutter rendering", ->
beforeEach ->
editor.attachToDom(heightInLines: 5.5)
@@ -1827,13 +1837,3 @@ describe "Editor", ->
editor.pageUp()
expect(editor.getCursor().getScreenPosition().row).toBe(0)
expect(editor.getFirstVisibleScreenRow()).toBe(0)
-
- describe "when editSession.showInvisibles is true", ->
- beforeEach ->
- project.setShowInvisibles(true)
- rootView.open()
- editor.attachToDom(5)
-
- it "displays spaces as •, tabs as ▸ and newlines as ¬", ->
- editor.setText " a line with tabs\tand spaces "
- expect(editor.find('.line').text()).toBe "•a•line•with•tabs▸ and•spaces•¬"
diff --git a/spec/app/token-spec.coffee b/spec/app/token-spec.coffee
deleted file mode 100644
index 57004fd93..000000000
--- a/spec/app/token-spec.coffee
+++ /dev/null
@@ -1,61 +0,0 @@
-_ = require 'underscore'
-Token = require 'token'
-Buffer = require 'buffer'
-TokenizedBuffer = require 'tokenized-buffer'
-
-describe "Token", ->
- [editSession, token] = []
-
- beforeEach ->
- editSession = fixturesProject.buildEditSessionForPath('sample.js', { tabLength: 2 })
- { tokenizedBuffer } = editSession
- screenLine = tokenizedBuffer.lineForScreenRow(3)
- token = _.last(screenLine.tokens)
-
- afterEach ->
- editSession.destroy()
-
- describe ".breakOutWhitespaceCharacters(tabLength, showInvisbles)", ->
- describe "when showInvisbles is false", ->
- it "replaces spaces and tabs with their own tokens", ->
- value = " spaces tabs\t\t\t "
- scopes = ["whatever"]
- isAtomic = false
- bufferDelta = value.length
- token = new Token({value, scopes, isAtomic, bufferDelta})
- tokens = token.breakOutWhitespaceCharacters(4, false)
-
- expect(tokens.length).toBe(8)
- expect(tokens[0].value).toBe(" ")
- expect(tokens[0].scopes).not.toContain("invisible")
- expect(tokens[1].value).toBe("spaces")
- expect(tokens[1].scopes).not.toContain("invisible")
- expect(tokens[2].value).toBe(" ")
- expect(tokens[3].value).toBe("tabs")
- expect(tokens[4].value).toBe(" ")
- expect(tokens[4].scopes).not.toContain("invisible")
- expect(tokens[5].value).toBe(" ")
- expect(tokens[6].value).toBe(" ")
- expect(tokens[7].value).toBe(" ")
-
- describe "when showInvisbles is true", ->
- it "replaces spaces and tabs with their own tokens", ->
- value = " spaces tabs\t\t\t "
- scopes = ["whatever"]
- isAtomic = false
- bufferDelta = value.length
- token = new Token({value, scopes, isAtomic, bufferDelta})
- tokens = token.breakOutWhitespaceCharacters(4, true)
-
- expect(tokens.length).toBe(8)
- expect(tokens[0].value).toBe("•")
- expect(tokens[0].scopes).toContain("invisible")
- expect(tokens[1].value).toBe("spaces")
- expect(tokens[1].scopes).not.toContain("invisible")
- expect(tokens[2].value).toBe("•••")
- expect(tokens[3].value).toBe("tabs")
- expect(tokens[4].value).toBe("▸ ")
- expect(tokens[4].scopes).toContain("invisible")
- expect(tokens[5].value).toBe("▸ ")
- expect(tokens[6].value).toBe("▸ ")
- expect(tokens[7].value).toBe("••")
diff --git a/spec/app/tokenized-buffer-spec.coffee b/spec/app/tokenized-buffer-spec.coffee
index 5cb716d85..11a188e0a 100644
--- a/spec/app/tokenized-buffer-spec.coffee
+++ b/spec/app/tokenized-buffer-spec.coffee
@@ -81,12 +81,12 @@ describe "TokenizedBuffer", ->
# previous line 3 should be combined with input to form line 1
expect(tokenizedBuffer.lineForScreenRow(1).tokens[0]).toEqual(value: 'foo', scopes: ['source.js'])
- expect(tokenizedBuffer.lineForScreenRow(1).tokens[8]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
+ expect(tokenizedBuffer.lineForScreenRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
# lines below deleted regions should be shifted upward
expect(tokenizedBuffer.lineForScreenRow(2).tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
- expect(tokenizedBuffer.lineForScreenRow(3).tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
- expect(tokenizedBuffer.lineForScreenRow(4).tokens[3]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.js'])
+ expect(tokenizedBuffer.lineForScreenRow(3).tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
+ expect(tokenizedBuffer.lineForScreenRow(4).tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -125,7 +125,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.lineForScreenRow(4).tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
# previous line 3 is pushed down to become line 5
- expect(tokenizedBuffer.lineForScreenRow(5).tokens[5]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
+ expect(tokenizedBuffer.lineForScreenRow(5).tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -165,12 +165,11 @@ describe "TokenizedBuffer", ->
screenLine0 = tokenizedBuffer.lineForScreenRow(0)
expect(screenLine0.text).toBe "# Econ 101#{editSession2.getTabText()}"
{ tokens } = screenLine0
- expect(tokens.length).toBe 6
+ expect(tokens.length).toBe 3
expect(tokens[0].value).toBe "#"
- expect(tokens[2].value).toBe "Econ"
- expect(tokens[4].value).toBe "101"
- expect(tokens[5].value).toBe editSession2.getTabText()
- expect(tokens[5].scopes).toEqual tokens[1].scopes
- expect(tokens[5].isAtomic).toBeTruthy()
+ expect(tokens[1].value).toBe " Econ 101"
+ 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 "#{editSession2.getTabText()} buy()#{editSession2.getTabText()}while supply > demand"
diff --git a/src/app/editor.coffee b/src/app/editor.coffee
index 5ecb3dbfd..c4fc11c91 100644
--- a/src/app/editor.coffee
+++ b/src/app/editor.coffee
@@ -876,9 +876,18 @@ class Editor extends View
if screenLine.text == ''
line.push(" ") unless @activeEditSession.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.escapeValue(@activeEditSession.showInvisibles))
+ line.push(token.getValueAsHtml(
+ showInvisibles: @activeEditSession.showInvisibles
+ hasLeadingWhitespace: position < firstNonWhitespacePosition
+ hasTrailingWhitespace: position + token.value.length > firstTrailingWhitespacePosition
+ ))
+
+ position += token.value.length
line.push("¬") if @activeEditSession.showInvisibles
line.push('')
diff --git a/src/app/token.coffee b/src/app/token.coffee
index 636d94c93..a413cd521 100644
--- a/src/app/token.coffee
+++ b/src/app/token.coffee
@@ -5,6 +5,7 @@ class Token
value: null
scopes: null
isAtomic: null
+ isTab: null
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @fold, @isTab}) ->
@screenDelta = @value.length
@@ -21,29 +22,41 @@ class Token
value2 = @value.substring(splitIndex)
[new Token(value: value1, scopes: @scopes), new Token(value: value2, scopes: @scopes)]
- breakOutWhitespaceCharacters: (tabLength, showInvisibles) ->
- return [this] unless /\t| /.test(@value)
-
- for substring in @value.match(/([^\t ]+|(\t| +))/g)
- scopesForInvisibles = if showInvisibles then @scopes.concat("invisible") else @scopes
+ breakOutTabCharacters: (tabLength, showInvisibles) ->
+ return [this] unless /\t/.test(@value)
+ for substring in @value.match(/[^\t]+|\t/g)
if substring == "\t"
- value = new Array(tabLength + 1).join(" ")
- value = "▸" + value[1..] if showInvisibles
- new Token(value: value, scopes: scopesForInvisibles, bufferDelta: 1, isAtomic: true)
- else if /^ +$/.test(substring)
- value = if showInvisibles then substring.replace(/[ ]/g, "•") else substring
- new Token(value: value, scopes: scopesForInvisibles)
+ @buildTabToken(tabLength)
else
new Token(value: substring, scopes: @scopes)
buildTabToken: (tabLength) ->
- tabText = new Array(tabLength + 1).join(" ")
+ new Token(
+ value: new Array(tabLength + 1).join(" ")
+ scopes: @scopes
+ bufferDelta: 1
+ isAtomic: true
+ isTab: true
+ )
- escapeValue: (showInvisibles)->
- @value
+ 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 b2776454e..19faf3ab7 100644
--- a/src/app/tokenized-buffer.coffee
+++ b/src/app/tokenized-buffer.coffee
@@ -67,7 +67,7 @@ class TokenizedBuffer
tokenObjects = []
for tokenProperties in tokens
token = new Token(tokenProperties)
- tokenObjects.push(token.breakOutWhitespaceCharacters(@tabLength, @showInvisibles)...)
+ tokenObjects.push(token.breakOutTabCharacters(@tabLength, @showInvisibles)...)
text = _.pluck(tokenObjects, 'value').join('')
new ScreenLine(tokenObjects, text, [1, 0], [1, 0], { stack })