Add indent guide to editor

The guide displays a continuous vertical line across lines with
the same indent levels.

Closes #50
This commit is contained in:
Kevin Sawicki
2013-02-19 14:59:08 -08:00
parent 2f797bfc7a
commit 101b1aba12
4 changed files with 112 additions and 26 deletions

View File

@@ -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)

View File

@@ -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 = "<span class='indent-guide'>#{_.multiplyString(' ', @activeEditSession.getTabLength())}</span>"
return _.multiplyString(indentationHtml, indentation)
return '&nbsp;' unless @showInvisibles
buildLineHtml: (screenLine, screenRow) ->
scopeStack = []
line = []
@@ -1153,7 +1182,8 @@ class Editor extends View
invisibles = @invisibles if @showInvisibles
if screenLine.text == ''
line.push("&nbsp;") 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

View File

@@ -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, '&amp;')
.replace(/"/g, '&quot;')
@@ -70,26 +71,29 @@ class Token
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
if invisibles
if @isHardTab and invisibles.tab
html = html.replace(/^./, "<span class='invisible hard-tab'>#{invisibles.tab}</span>")
else if invisibles.space
if hasLeadingWhitespace
html = html.replace /^[ ]+/, (match) ->
"<span class='invisible leading-whitespace'>#{match.replace(/./g, invisibles.space)}</span>"
if hasTrailingWhitespace
html = html.replace /[ ]+$/, (match) ->
"<span class='invisible trailing-whitespace'>#{match.replace(/./g, invisibles.space)}</span>"
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
"<span class='#{classes.join(' ')}'>#{match}</span>"
else
if @isHardTab
html = html.replace /^./, (match) ->
"<span class='hard-tab'>#{match}</span>"
else
if hasLeadingWhitespace
html = html.replace /^[ ]+/, (match) ->
"<span class='leading-whitespace'>#{match}</span>"
if hasTrailingWhitespace
html = html.replace /[ ]+$/, (match) ->
"<span class='trailing-whitespace'>#{match}</span>"
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
"<span class='#{classes.join(' ')}'>#{match}</span>"
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
"<span class='#{classes.join(' ')}'>#{match}</span>"
html

View File

@@ -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;