From 101b1aba124c34fd218bbc452ee9a5a889b8928c Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 19 Feb 2013 14:59:08 -0800 Subject: [PATCH] Add indent guide to editor The guide displays a continuous vertical line across lines with the same indent levels. Closes #50 --- spec/app/editor-spec.coffee | 45 ++++++++++++++++++++++++++++++++++++ src/app/editor.coffee | 41 +++++++++++++++++++++++++++++---- src/app/token.coffee | 46 ++++++++++++++++++++----------------- static/editor.css | 6 +++++ 4 files changed, 112 insertions(+), 26 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 6b8719add..a41bbd7f1 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1804,6 +1804,51 @@ describe "Editor", -> expect(editor.renderedLines.find('.line:eq(1)').text()).toBe "that#{cr}#{eol}" expect(editor.renderedLines.find('.line:last').text()).toBe "#{eol}" + describe "when config.editor.showIndentGuide is set to true", -> + it "adds an indent-guide class to each leading whitespace span", -> + editor.attachToDom() + + expect(config.get("editor.showIndentGuide")).toBeFalsy() + config.set("editor.showIndentGuide", true) + expect(editor.showIndentGuide).toBeTruthy() + + expect(editor.renderedLines.find('.line:eq(0) .indent-guide').length).toBe 0 + + expect(editor.renderedLines.find('.line:eq(1) .indent-guide').length).toBe 1 + expect(editor.renderedLines.find('.line:eq(1) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(2) .indent-guide').length).toBe 2 + expect(editor.renderedLines.find('.line:eq(2) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(3) .indent-guide').length).toBe 2 + expect(editor.renderedLines.find('.line:eq(3) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(4) .indent-guide').length).toBe 2 + expect(editor.renderedLines.find('.line:eq(4) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(5) .indent-guide').length).toBe 3 + expect(editor.renderedLines.find('.line:eq(5) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(6) .indent-guide').length).toBe 3 + expect(editor.renderedLines.find('.line:eq(6) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(7) .indent-guide').length).toBe 2 + expect(editor.renderedLines.find('.line:eq(7) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(8) .indent-guide').length).toBe 2 + expect(editor.renderedLines.find('.line:eq(8) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(9) .indent-guide').length).toBe 1 + expect(editor.renderedLines.find('.line:eq(9) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 + expect(editor.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(11) .indent-guide').length).toBe 1 + expect(editor.renderedLines.find('.line:eq(11) .indent-guide').text()).toBe ' ' + + expect(editor.renderedLines.find('.line:eq(12) .indent-guide').length).toBe 0 + describe "gutter rendering", -> beforeEach -> editor.attachToDom(heightInLines: 5.5) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 842539291..47f676d89 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -15,6 +15,7 @@ class Editor extends View @configDefaults: fontSize: 20 showInvisibles: false + showIndentGuide: false autosave: false autoIndent: true autoIndentOnPaste: false @@ -191,6 +192,7 @@ class Editor extends View 'editor:move-line-down': @moveLineDown 'editor:duplicate-line': @duplicateLine 'editor:undo-close-session': @undoDestroySession + 'editor:toggle-indent-guide': => config.set('editor.showIndentGuide', !config.get('editor.showIndentGuide')) documentation = {} for name, method of editorBindings @@ -330,6 +332,11 @@ class Editor extends View cr: '\u00a4' @resetDisplay() + setShowIndentGuide: (showIndentGuide) -> + return if showIndentGuide == @showIndentGuide + @showIndentGuide = showIndentGuide + @resetDisplay() + checkoutHead: -> @getBuffer().checkoutHead() setText: (text) -> @getBuffer().setText(text) getText: -> @getBuffer().getText() @@ -346,6 +353,7 @@ class Editor extends View configure: -> @observeConfig 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles) + @observeConfig 'editor.showIndentGuide', (showIndentGuide) => @setShowIndentGuide(showIndentGuide) @observeConfig 'editor.invisibles', (invisibles) => @setInvisibles(invisibles) @observeConfig 'editor.fontSize', (fontSize) => @setFontSize(fontSize) @observeConfig 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily) @@ -1110,13 +1118,34 @@ class Editor extends View buildLineElementsForScreenRows: (startRow, endRow) -> div = document.createElement('div') - div.innerHTML = @buildLinesHtml(@activeEditSession.linesForScreenRows(startRow, endRow)) + div.innerHTML = @buildLinesHtml(startRow, endRow) new Array(div.children...) - buildLinesHtml: (screenLines) -> - screenLines.map((line) => @buildLineHtml(line)).join('\n\n') + buildLinesHtml: (startRow, endRow) -> + lines = @activeEditSession.linesForScreenRows(startRow, endRow) + htmlLines = [] + screenRow = startRow + for line in @activeEditSession.linesForScreenRows(startRow, endRow) + htmlLines.push(@buildLineHtml(line, screenRow++)) + htmlLines.join('\n\n') - buildLineHtml: (screenLine) -> + buildEmptyLineHtml: (screenRow) -> + if not @mini and @showIndentGuide + indentation = 0 + while --screenRow >= 0 + bufferRow = @activeEditSession.bufferPositionForScreenPosition([screenRow]).row + bufferLine = @activeEditSession.lineForBufferRow(bufferRow) + unless bufferLine is '' + indentation = Math.ceil(@activeEditSession.indentLevelForLine(bufferLine)) + break + + if indentation > 0 + indentationHtml = "#{_.multiplyString(' ', @activeEditSession.getTabLength())}" + return _.multiplyString(indentationHtml, indentation) + + return ' ' unless @showInvisibles + + buildLineHtml: (screenLine, screenRow) -> scopeStack = [] line = [] @@ -1153,7 +1182,8 @@ class Editor extends View invisibles = @invisibles if @showInvisibles if screenLine.text == '' - line.push(" ") unless @showInvisibles + html = @buildEmptyLineHtml(screenRow) + line.push(html) if html else firstNonWhitespacePosition = screenLine.text.search(/\S/) firstTrailingWhitespacePosition = screenLine.text.search(/\s*$/) @@ -1164,6 +1194,7 @@ class Editor extends View invisibles: invisibles hasLeadingWhitespace: position < firstNonWhitespacePosition hasTrailingWhitespace: position + token.value.length > firstTrailingWhitespacePosition + hasIndentGuide: @showIndentGuide )) position += token.value.length diff --git a/src/app/token.coffee b/src/app/token.coffee index 5a7c677ed..fa3f0dc6b 100644 --- a/src/app/token.coffee +++ b/src/app/token.coffee @@ -62,7 +62,8 @@ class Token isOnlyWhitespace: -> not /\S/.test(@value) - getValueAsHtml: ({invisibles, hasLeadingWhitespace, hasTrailingWhitespace})-> + getValueAsHtml: ({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})-> + invisibles ?= {} html = @value .replace(/&/g, '&') .replace(/"/g, '"') @@ -70,26 +71,29 @@ class Token .replace(//g, '>') - if invisibles - if @isHardTab and invisibles.tab - html = html.replace(/^./, "") - else if invisibles.space - if hasLeadingWhitespace - html = html.replace /^[ ]+/, (match) -> - "" - if hasTrailingWhitespace - html = html.replace /[ ]+$/, (match) -> - "" + if @isHardTab + html = html.replace /^./, (match) -> + classes = [] + classes.push('invisible') if invisibles.tab + classes.push('indent-guide') if hasIndentGuide + classes.push('hard-tab') + match = invisibles.tab ? match + "#{match}" else - if @isHardTab - html = html.replace /^./, (match) -> - "#{match}" - else - if hasLeadingWhitespace - html = html.replace /^[ ]+/, (match) -> - "#{match}" - if hasTrailingWhitespace - html = html.replace /[ ]+$/, (match) -> - "#{match}" + if hasLeadingWhitespace + html = html.replace /^[ ]+/, (match) -> + classes = [] + classes.push('invisible') if invisibles.space + classes.push('indent-guide') if hasIndentGuide + classes.push('leading-whitespace') + match = match.replace(/./g, invisibles.space) if invisibles.space + "#{match}" + if hasTrailingWhitespace + html = html.replace /[ ]+$/, (match) -> + classes = [] + classes.push('invisible') if invisibles.space + classes.push('trailing-whitespace') + match = match.replace(/./g, invisibles.space) if invisibles.space + "#{match}" html diff --git a/static/editor.css b/static/editor.css index 13ed33b71..b75743fb0 100644 --- a/static/editor.css +++ b/static/editor.css @@ -90,6 +90,12 @@ font-style: normal !important; } +.editor .indent-guide { + opacity: 0.2; + display: inline-block; + box-shadow: inset 1px 0px; +} + .editor .vertical-scrollbar { position: absolute; right: 0;