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) -> + "" + if hasTrailingWhitespace + html = html.replace /[ ]+$/, (match) -> + "" + + 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 })