mirror of
https://github.com/atom/atom.git
synced 2026-01-22 21:38:10 -05:00
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.
283 lines
9.7 KiB
CoffeeScript
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(' ', 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
|