Files
atom/src/gutter.coffee
Nathan Sobo 2831b43042 Update foldability indicators on changes that don't insert/remove lines
On changes that insert/remove lines, we redraw the entire gutter, which
ensures the foldability markers are up to date. But other kinds of
changes could change the foldability of lines… if a line becomes
commented or its indentation changes, the foldability of that line and
the lines above and below could be affected. For now, the gutter
understands this fact and updates the foldability for the region
extending from one line before to one line after each change. Someday
I'd like the view logic to not bake in knowledge of the folding rules,
but this works for now.
2014-01-20 11:17:58 -07:00

283 lines
9.7 KiB
CoffeeScript

{View, $, $$, $$$} = require './space-pen-extensions'
{Range} = require 'text-buffer'
_ = require 'underscore-plus'
# Private: Represents the portion of the {EditorView} containing row numbers.
#
# The gutter also indicates if rows are folded.
module.exports =
class Gutter extends View
### Internal ###
@content: ->
@div class: 'gutter', =>
@div outlet: 'lineNumbers', class: 'line-numbers'
firstScreenRow: null
lastScreenRow: null
initialize: ->
@elementBuilder = document.createElement('div')
afterAttach: (onDom) ->
return if @attached or not onDom
@attached = true
highlightLines = => @highlightLines()
@getEditorView().on 'cursor:moved', highlightLines
@getEditorView().on 'selection:changed', highlightLines
@on 'mousedown', (e) => @handleMouseEvents(e)
beforeRemove: ->
$(document).off(".gutter-#{@getEditorView().id}")
handleMouseEvents: (e) ->
editorView = @getEditorView()
editor = @getEditor()
startRow = editorView.screenPositionFromMouseEvent(e).row
if e.shiftKey
editor.selectToScreenPosition([startRow + 1, 0])
return
else
editor.getSelection().setScreenRange([[startRow, 0], [startRow, 0]])
moveHandler = (e) =>
start = startRow
end = editorView.screenPositionFromMouseEvent(e).row
if end > start then end++ else start++
editor.getSelection().setScreenRange([[start, 0], [end, 0]])
$(document).on "mousemove.gutter-#{editorView.id}", moveHandler
$(document).one "mouseup.gutter-#{editorView.id}", => $(document).off 'mousemove', moveHandler
### Public ###
# Retrieves the containing {EditorView}.
#
# Returns an {EditorView}.
getEditorView: ->
@parentView
getEditor: ->
@getEditorView().getEditor()
# Defines whether to show the gutter or not.
#
# showLineNumbers - A {Boolean} which, if `false`, hides the gutter
setShowLineNumbers: (showLineNumbers) ->
if showLineNumbers then @lineNumbers.show() else @lineNumbers.hide()
# Get all the line-number divs.
#
# Returns a list of {HTMLElement}s.
getLineNumberElements: ->
@lineNumbers[0].children
# Get all the line-number divs.
#
# Returns a list of {HTMLElement}s.
getLineNumberElementsForClass: (klass) ->
@lineNumbers[0].getElementsByClassName(klass)
# Get a single line-number div.
#
# * bufferRow: 0 based line number
#
# Returns a list of {HTMLElement}s that correspond to the bufferRow. More than
# one in the list indicates a wrapped line.
getLineNumberElement: (bufferRow) ->
@getLineNumberElementsForClass("line-number-#{bufferRow}")
# Add a class to all line-number divs.
#
# * klass: string class name
#
# Returns true if the class was added to any lines
addClassToAllLines: (klass)->
elements = @getLineNumberElements()
el.classList.add(klass) for el in elements
!!elements.length
# Remove a class from all line-number divs.
#
# * klass: string class name. Can only be one class name. i.e. 'my-class'
#
# Returns true if the class was removed from any lines
removeClassFromAllLines: (klass)->
# This is faster than calling $.removeClass on all lines, and faster than
# making a new array and iterating through it.
elements = @getLineNumberElementsForClass(klass)
willRemoveClasses = !!elements.length
elements[0].classList.remove(klass) while elements.length > 0
willRemoveClasses
# Add a class to a single line-number div
#
# * bufferRow: 0 based line number
# * klass: string class name
#
# Returns true if there were lines the class was added to
addClassToLine: (bufferRow, klass)->
elements = @getLineNumberElement(bufferRow)
el.classList.add(klass) for el in elements
!!elements.length
# Remove a class from a single line-number div
#
# * bufferRow: 0 based line number
# * klass: string class name
#
# Returns true if there were lines the class was removed from
removeClassFromLine: (bufferRow, klass)->
classesRemoved = false
elements = @getLineNumberElement(bufferRow)
for el in elements
hasClass = el.classList.contains(klass)
classesRemoved |= hasClass
el.classList.remove(klass) if hasClass
classesRemoved
### Internal ###
updateLineNumbers: (changes, startScreenRow, endScreenRow) ->
# Check if we have something already rendered that overlaps the requested range
updateAllLines = not (startScreenRow? and endScreenRow?)
updateAllLines |= endScreenRow <= @firstScreenRow or startScreenRow >= @lastScreenRow
unless updateAllLines
for change in changes
if change.screenDelta or change.bufferDelta
updateAllLines = true
break
# If a change added or removed lines, just rebuild the entire gutter
if updateAllLines
@lineNumbers[0].innerHTML = @buildLineElementsHtml(startScreenRow, endScreenRow)
# Handle changes that didn't add/remove lines more optimally
else
if startScreenRow < @firstScreenRow
@prependLineElements(@buildLineElements(startScreenRow, @firstScreenRow-1))
else if startScreenRow != @firstScreenRow
@removeLineElements(startScreenRow - @firstScreenRow)
if endScreenRow > @lastScreenRow
@appendLineElements(@buildLineElements(@lastScreenRow+1, endScreenRow))
else if endScreenRow != @lastScreenRow
@removeLineElements(endScreenRow - @lastScreenRow)
@updateFoldableClasses(changes)
@firstScreenRow = startScreenRow
@lastScreenRow = endScreenRow
@highlightedRows = null
@highlightLines()
prependLineElements: (lineElements) ->
anchor = @lineNumbers[0].children[0]
return appendLineElements(lineElements) unless anchor?
@lineNumbers[0].insertBefore(lineElements[0], anchor) while lineElements.length > 0
null # defeat coffeescript array return
appendLineElements: (lineElements) ->
@lineNumbers[0].appendChild(lineElements[0]) while lineElements.length > 0
null # defeat coffeescript array return
removeLineElements: (numberOfElements) ->
children = @getLineNumberElements()
# children is a live NodeList, so remove from the desired end {numberOfElements} times
if numberOfElements < 0
@lineNumbers[0].removeChild(children[children.length-1]) while numberOfElements++
else if numberOfElements > 0
@lineNumbers[0].removeChild(children[0]) while numberOfElements--
null # defeat coffeescript array return
buildLineElements: (startScreenRow, endScreenRow) ->
@elementBuilder.innerHTML = @buildLineElementsHtml(startScreenRow, endScreenRow)
@elementBuilder.children
buildLineElementsHtml: (startScreenRow, endScreenRow) =>
editor = @getEditor()
maxDigits = editor.getLineCount().toString().length
rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow)
html = ''
for row in rows
if row is lastRow
rowValue = ''
else
rowValue = (row + 1).toString()
classes = "line-number line-number-#{row}"
classes += ' foldable' if row isnt lastRow and editor.isFoldableAtBufferRow(row)
classes += ' folded' if editor.isFoldedAtBufferRow(row)
rowValuePadding = _.multiplyString('&nbsp;', maxDigits - rowValue.length)
html += """<div class="#{classes}" data-buffer-row=#{row}>#{rowValuePadding}#{rowValue}<div class="icon-right"></div></div>"""
lastRow = row
html
# Private: Called to update the 'foldable' class of line numbers when there's
# a change to the display buffer that doesn't regenerate all the line numbers
# anyway.
updateFoldableClasses: (changes) ->
editor = @getEditor()
for {start, end} in changes when start <= @lastScreenRow
startScreenRow = Math.max(start - 1, @firstScreenRow)
endScreenRow = Math.min(end + 1, @lastScreenRow)
lastBufferRow = null
for bufferRow in editor.bufferRowsForScreenRows(startScreenRow, endScreenRow) when bufferRow isnt lastBufferRow
lastBufferRow = bufferRow
lineNumberElement = @getLineNumberElement(bufferRow)[0]
if editor.isFoldableAtBufferRow(bufferRow)
lineNumberElement.classList.add('foldable')
else
lineNumberElement.classList.remove('foldable')
removeLineHighlights: ->
return unless @highlightedLineNumbers
for line in @highlightedLineNumbers
line.classList.remove('cursor-line')
line.classList.remove('cursor-line-no-selection')
@highlightedLineNumbers = null
addLineHighlight: (row, emptySelection) ->
return if row < @firstScreenRow or row > @lastScreenRow
@highlightedLineNumbers ?= []
if highlightedLineNumber = @lineNumbers[0].children[row - @firstScreenRow]
highlightedLineNumber.classList.add('cursor-line')
highlightedLineNumber.classList.add('cursor-line-no-selection') if emptySelection
@highlightedLineNumbers.push(highlightedLineNumber)
highlightLines: ->
editor = @getEditor()
return unless editor?.isAlive()
if editor.getSelection().isEmpty()
row = editor.getCursorScreenPosition().row
rowRange = new Range([row, 0], [row, 0])
return if @selectionEmpty and @highlightedRows?.isEqual(rowRange)
@removeLineHighlights()
@addLineHighlight(row, true)
@highlightedRows = rowRange
@selectionEmpty = true
else
selectedRows = editor.getSelection().getScreenRange()
endRow = selectedRows.end.row
endRow-- if selectedRows.end.column is 0
selectedRows = new Range([selectedRows.start.row, 0], [endRow, 0])
return if not @selectionEmpty and @highlightedRows?.isEqual(selectedRows)
@removeLineHighlights()
for row in [selectedRows.start.row..selectedRows.end.row]
@addLineHighlight(row, false)
@highlightedRows = selectedRows
@selectionEmpty = false