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