mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Merge pull request #5293 from atom/ns-editor-presenters
Move all text editor view state into a presenter object
This commit is contained in:
@@ -7,12 +7,8 @@ CursorComponent = React.createClass
|
||||
displayName: 'CursorComponent'
|
||||
|
||||
render: ->
|
||||
{pixelRect, defaultCharWidth} = @props
|
||||
{pixelRect} = @props
|
||||
{top, left, height, width} = pixelRect
|
||||
width = defaultCharWidth if width is 0
|
||||
WebkitTransform = "translate(#{left}px, #{top}px)"
|
||||
|
||||
div className: 'cursor', style: {height, width, WebkitTransform}
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
not isEqualForProperties(newProps, @props, 'pixelRect', 'defaultCharWidth')
|
||||
|
||||
@@ -7,55 +7,14 @@ CursorComponent = require './cursor-component'
|
||||
module.exports =
|
||||
CursorsComponent = React.createClass
|
||||
displayName: 'CursorsComponent'
|
||||
mixins: [SubscriberMixin]
|
||||
|
||||
cursorBlinkIntervalHandle: null
|
||||
|
||||
render: ->
|
||||
{performedInitialMeasurement, cursorPixelRects, defaultCharWidth} = @props
|
||||
{blinkOff} = @state
|
||||
{presenter} = @props
|
||||
|
||||
className = 'cursors'
|
||||
className += ' blink-off' if blinkOff
|
||||
className += ' blink-off' if presenter.state.content.blinkCursorsOff
|
||||
|
||||
div {className},
|
||||
if performedInitialMeasurement
|
||||
for key, pixelRect of cursorPixelRects
|
||||
CursorComponent({key, pixelRect, defaultCharWidth})
|
||||
|
||||
getInitialState: ->
|
||||
blinkOff: false
|
||||
|
||||
componentDidMount: ->
|
||||
@startBlinkingCursors()
|
||||
|
||||
componentWillUnmount: ->
|
||||
@stopBlinkingCursors()
|
||||
|
||||
shouldComponentUpdate: (newProps, newState) ->
|
||||
not newState.blinkOff is @state.blinkOff or
|
||||
not isEqualForProperties(newProps, @props, 'cursorPixelRects', 'scrollTop', 'scrollLeft', 'defaultCharWidth', 'useHardwareAcceleration')
|
||||
|
||||
componentWillUpdate: (newProps) ->
|
||||
cursorsMoved = @props.cursorPixelRects? and
|
||||
isEqualForProperties(newProps, @props, 'defaultCharWidth', 'scopedCharacterWidthsChangeCount') and
|
||||
not isEqual(newProps.cursorPixelRects, @props.cursorPixelRects)
|
||||
|
||||
@pauseCursorBlinking() if cursorsMoved
|
||||
|
||||
startBlinkingCursors: ->
|
||||
@toggleCursorBlinkHandle = setInterval(@toggleCursorBlink, @props.cursorBlinkPeriod / 2) if @isMounted()
|
||||
|
||||
startBlinkingCursorsAfterDelay: null # Created lazily
|
||||
|
||||
stopBlinkingCursors: ->
|
||||
clearInterval(@toggleCursorBlinkHandle)
|
||||
|
||||
toggleCursorBlink: ->
|
||||
@setState(blinkOff: not @state.blinkOff)
|
||||
|
||||
pauseCursorBlinking: ->
|
||||
@state.blinkOff = false
|
||||
@stopBlinkingCursors()
|
||||
@startBlinkingCursorsAfterDelay ?= debounce(@startBlinkingCursors, @props.cursorBlinkResumeDelay)
|
||||
@startBlinkingCursorsAfterDelay()
|
||||
if presenter.hasRequiredMeasurements()
|
||||
for key, pixelRect of presenter.state.content.cursors
|
||||
CursorComponent({key, pixelRect})
|
||||
|
||||
@@ -72,6 +72,8 @@ class Decoration
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
|
||||
isDestroyed: -> @destroyed
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
@@ -617,10 +617,10 @@ class DisplayBuffer extends Model
|
||||
# bufferRange - The {Range} to convert
|
||||
#
|
||||
# Returns a {Range}.
|
||||
screenRangeForBufferRange: (bufferRange) ->
|
||||
screenRangeForBufferRange: (bufferRange, options) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
start = @screenPositionForBufferPosition(bufferRange.start)
|
||||
end = @screenPositionForBufferPosition(bufferRange.end)
|
||||
start = @screenPositionForBufferPosition(bufferRange.start, options)
|
||||
end = @screenPositionForBufferPosition(bufferRange.end, options)
|
||||
new Range(start, end)
|
||||
|
||||
# Given a screen range, this converts it into a buffer position.
|
||||
@@ -897,7 +897,7 @@ class DisplayBuffer extends Model
|
||||
getLineDecorations: (propertyFilter) ->
|
||||
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line')
|
||||
|
||||
getGutterDecorations: (propertyFilter) ->
|
||||
getLineNumberDecorations: (propertyFilter) ->
|
||||
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number')
|
||||
|
||||
getHighlightDecorations: (propertyFilter) ->
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
_ = require 'underscore-plus'
|
||||
React = require 'react-atom-fork'
|
||||
{div} = require 'reactionary-atom-fork'
|
||||
{isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
|
||||
_ = require 'underscore-plus'
|
||||
{isEqual, isEqualForProperties, multiplyString, toArray} = _
|
||||
Decoration = require './decoration'
|
||||
SubscriberMixin = require './subscriber-mixin'
|
||||
|
||||
@@ -12,23 +13,26 @@ GutterComponent = React.createClass
|
||||
displayName: 'GutterComponent'
|
||||
mixins: [SubscriberMixin]
|
||||
|
||||
maxLineNumberDigits: null
|
||||
dummyLineNumberNode: null
|
||||
measuredWidth: null
|
||||
|
||||
render: ->
|
||||
{scrollHeight, scrollViewHeight, backgroundColor, gutterBackgroundColor} = @props
|
||||
{presenter} = @props
|
||||
@newState = presenter.state.gutter
|
||||
@oldState ?= {lineNumbers: {}}
|
||||
|
||||
if gutterBackgroundColor isnt 'rbga(0, 0, 0, 0)'
|
||||
backgroundColor = gutterBackgroundColor
|
||||
{scrollHeight, backgroundColor} = @newState
|
||||
|
||||
div className: 'gutter',
|
||||
div className: 'line-numbers', ref: 'lineNumbers', style:
|
||||
height: Math.max(scrollHeight, scrollViewHeight)
|
||||
WebkitTransform: @getTransform()
|
||||
height: scrollHeight
|
||||
WebkitTransform: @getTransform() if presenter.hasRequiredMeasurements()
|
||||
backgroundColor: backgroundColor
|
||||
|
||||
getTransform: ->
|
||||
{scrollTop, useHardwareAcceleration} = @props
|
||||
{useHardwareAcceleration} = @props
|
||||
{scrollTop} = @newState
|
||||
|
||||
if useHardwareAcceleration
|
||||
"translate3d(0px, #{-scrollTop}px, 0px)"
|
||||
@@ -37,141 +41,81 @@ GutterComponent = React.createClass
|
||||
|
||||
componentWillMount: ->
|
||||
@lineNumberNodesById = {}
|
||||
@lineNumberIdsByScreenRow = {}
|
||||
@screenRowsByLineNumberId = {}
|
||||
@renderedDecorationsByLineNumberId = {}
|
||||
|
||||
componentDidMount: ->
|
||||
{@maxLineNumberDigits} = @newState
|
||||
@appendDummyLineNumber()
|
||||
@updateLineNumbers() if @props.performedInitialMeasurement
|
||||
@updateLineNumbers()
|
||||
|
||||
node = @getDOMNode()
|
||||
node.addEventListener 'click', @onClick
|
||||
node.addEventListener 'mousedown', @onMouseDown
|
||||
|
||||
# Only update the gutter if the visible row range has changed or if a
|
||||
# non-zero-delta change to the screen lines has occurred within the current
|
||||
# visible row range.
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
return true unless isEqualForProperties(newProps, @props,
|
||||
'renderedRowRange', 'scrollTop', 'lineHeightInPixels', 'mouseWheelScreenRow', 'lineDecorations',
|
||||
'scrollViewHeight', 'useHardwareAcceleration', 'backgroundColor', 'gutterBackgroundColor'
|
||||
)
|
||||
|
||||
{renderedRowRange, pendingChanges, lineDecorations} = newProps
|
||||
return false unless renderedRowRange?
|
||||
|
||||
for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0
|
||||
return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start
|
||||
|
||||
false
|
||||
|
||||
componentDidUpdate: (oldProps) ->
|
||||
return unless @props.performedInitialMeasurement
|
||||
|
||||
unless isEqualForProperties(oldProps, @props, 'maxLineNumberDigits')
|
||||
{maxLineNumberDigits} = @newState
|
||||
unless maxLineNumberDigits is @maxLineNumberDigits
|
||||
@maxLineNumberDigits = maxLineNumberDigits
|
||||
@updateDummyLineNumber()
|
||||
@removeLineNumberNodes()
|
||||
node.remove() for id, node of @lineNumberNodesById
|
||||
@oldState = {lineNumbers: {}}
|
||||
@lineNumberNodesById = {}
|
||||
|
||||
@clearScreenRowCaches() unless oldProps.lineHeightInPixels is @props.lineHeightInPixels
|
||||
@updateLineNumbers()
|
||||
|
||||
clearScreenRowCaches: ->
|
||||
@lineNumberIdsByScreenRow = {}
|
||||
@screenRowsByLineNumberId = {}
|
||||
|
||||
# This dummy line number element holds the gutter to the appropriate width,
|
||||
# since the real line numbers are absolutely positioned for performance reasons.
|
||||
appendDummyLineNumber: ->
|
||||
{maxLineNumberDigits} = @props
|
||||
WrapperDiv.innerHTML = @buildLineNumberHTML(-1, false, maxLineNumberDigits)
|
||||
WrapperDiv.innerHTML = @buildLineNumberHTML({bufferRow: -1})
|
||||
@dummyLineNumberNode = WrapperDiv.children[0]
|
||||
@refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode)
|
||||
|
||||
updateDummyLineNumber: ->
|
||||
@dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits)
|
||||
@dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false)
|
||||
|
||||
updateLineNumbers: ->
|
||||
lineNumberIdsToPreserve = @appendOrUpdateVisibleLineNumberNodes()
|
||||
@removeLineNumberNodes(lineNumberIdsToPreserve)
|
||||
|
||||
appendOrUpdateVisibleLineNumberNodes: ->
|
||||
{editor, renderedRowRange, scrollTop, maxLineNumberDigits, lineDecorations} = @props
|
||||
[startRow, endRow] = renderedRowRange
|
||||
|
||||
newLineNumberIds = null
|
||||
newLineNumbersHTML = null
|
||||
visibleLineNumberIds = new Set
|
||||
|
||||
wrapCount = 0
|
||||
for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1)
|
||||
screenRow = startRow + index
|
||||
|
||||
if bufferRow is lastBufferRow
|
||||
id = "#{bufferRow}-#{wrapCount++}"
|
||||
else
|
||||
id = bufferRow.toString()
|
||||
lastBufferRow = bufferRow
|
||||
wrapCount = 0
|
||||
|
||||
visibleLineNumberIds.add(id)
|
||||
|
||||
if @hasLineNumberNode(id)
|
||||
@updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0)
|
||||
for id, lineNumberState of @newState.lineNumbers
|
||||
if @oldState.lineNumbers.hasOwnProperty(id)
|
||||
@updateLineNumberNode(id, lineNumberState)
|
||||
else
|
||||
newLineNumberIds ?= []
|
||||
newLineNumbersHTML ?= ""
|
||||
newLineNumberIds.push(id)
|
||||
newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow)
|
||||
@screenRowsByLineNumberId[id] = screenRow
|
||||
@lineNumberIdsByScreenRow[screenRow] = id
|
||||
|
||||
@renderedDecorationsByLineNumberId[id] = lineDecorations[screenRow]
|
||||
newLineNumbersHTML += @buildLineNumberHTML(lineNumberState)
|
||||
@oldState.lineNumbers[id] = _.clone(lineNumberState)
|
||||
|
||||
if newLineNumberIds?
|
||||
WrapperDiv.innerHTML = newLineNumbersHTML
|
||||
newLineNumberNodes = toArray(WrapperDiv.children)
|
||||
|
||||
node = @refs.lineNumbers.getDOMNode()
|
||||
for lineNumberId, i in newLineNumberIds
|
||||
for id, i in newLineNumberIds
|
||||
lineNumberNode = newLineNumberNodes[i]
|
||||
@lineNumberNodesById[lineNumberId] = lineNumberNode
|
||||
@lineNumberNodesById[id] = lineNumberNode
|
||||
node.appendChild(lineNumberNode)
|
||||
|
||||
visibleLineNumberIds
|
||||
for id, lineNumberState of @oldState.lineNumbers
|
||||
unless @newState.lineNumbers.hasOwnProperty(id)
|
||||
@lineNumberNodesById[id].remove()
|
||||
delete @lineNumberNodesById[id]
|
||||
delete @oldState.lineNumbers[id]
|
||||
|
||||
removeLineNumberNodes: (lineNumberIdsToPreserve) ->
|
||||
{mouseWheelScreenRow} = @props
|
||||
node = @refs.lineNumbers.getDOMNode()
|
||||
for lineNumberId, lineNumberNode of @lineNumberNodesById when not lineNumberIdsToPreserve?.has(lineNumberId)
|
||||
screenRow = @screenRowsByLineNumberId[lineNumberId]
|
||||
if not screenRow? or screenRow isnt mouseWheelScreenRow
|
||||
delete @lineNumberNodesById[lineNumberId]
|
||||
delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId
|
||||
delete @screenRowsByLineNumberId[lineNumberId]
|
||||
delete @renderedDecorationsByLineNumberId[lineNumberId]
|
||||
node.removeChild(lineNumberNode)
|
||||
|
||||
buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) ->
|
||||
{editor, lineHeightInPixels, lineDecorations} = @props
|
||||
buildLineNumberHTML: (lineNumberState) ->
|
||||
{screenRow, bufferRow, softWrapped, top, decorationClasses} = lineNumberState
|
||||
if screenRow?
|
||||
style = "position: absolute; top: #{screenRow * lineHeightInPixels}px;"
|
||||
style = "position: absolute; top: #{top}px;"
|
||||
else
|
||||
style = "visibility: hidden;"
|
||||
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits)
|
||||
className = @buildLineNumberClassName(lineNumberState)
|
||||
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped)
|
||||
|
||||
classes = ''
|
||||
if lineDecorations? and decorations = lineDecorations[screenRow]
|
||||
for id, decoration of decorations
|
||||
if Decoration.isType(decoration, 'line-number')
|
||||
classes += decoration.class + ' '
|
||||
"<div class=\"#{className}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
|
||||
|
||||
classes += "foldable " if bufferRow >= 0 and editor.isFoldableAtBufferRow(bufferRow)
|
||||
classes += "line-number line-number-#{bufferRow}"
|
||||
buildLineNumberInnerHTML: (bufferRow, softWrapped) ->
|
||||
{maxLineNumberDigits} = @newState
|
||||
|
||||
"<div class=\"#{classes}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
|
||||
|
||||
buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) ->
|
||||
if softWrapped
|
||||
lineNumber = "•"
|
||||
else
|
||||
@@ -181,46 +125,34 @@ GutterComponent = React.createClass
|
||||
iconHTML = '<div class="icon-right"></div>'
|
||||
padding + lineNumber + iconHTML
|
||||
|
||||
updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped) ->
|
||||
{editor, lineDecorations} = @props
|
||||
updateLineNumberNode: (lineNumberId, newLineNumberState) ->
|
||||
oldLineNumberState = @oldState.lineNumbers[lineNumberId]
|
||||
node = @lineNumberNodesById[lineNumberId]
|
||||
|
||||
if editor.isFoldableAtBufferRow(bufferRow)
|
||||
node.classList.add('foldable')
|
||||
else
|
||||
node.classList.remove('foldable')
|
||||
unless oldLineNumberState.foldable is newLineNumberState.foldable and _.isEqual(oldLineNumberState.decorationClasses, newLineNumberState.decorationClasses)
|
||||
node.className = @buildLineNumberClassName(newLineNumberState)
|
||||
oldLineNumberState.foldable = newLineNumberState.foldable
|
||||
oldLineNumberState.decorationClasses = _.clone(newLineNumberState.decorationClasses)
|
||||
|
||||
decorations = lineDecorations[screenRow]
|
||||
previousDecorations = @renderedDecorationsByLineNumberId[lineNumberId]
|
||||
unless oldLineNumberState.top is newLineNumberState.top
|
||||
node.style.top = newLineNumberState.top + 'px'
|
||||
node.dataset.screenRow = newLineNumberState.screenRow
|
||||
oldLineNumberState.top = newLineNumberState.top
|
||||
oldLineNumberState.screenRow = newLineNumberState.screenRow
|
||||
|
||||
if previousDecorations?
|
||||
for id, decoration of previousDecorations
|
||||
if Decoration.isType(decoration, 'line-number') and not @hasDecoration(decorations, decoration)
|
||||
node.classList.remove(decoration.class)
|
||||
|
||||
if decorations?
|
||||
for id, decoration of decorations
|
||||
if Decoration.isType(decoration, 'line-number') and not @hasDecoration(previousDecorations, decoration)
|
||||
node.classList.add(decoration.class)
|
||||
|
||||
unless @screenRowsByLineNumberId[lineNumberId] is screenRow
|
||||
{lineHeightInPixels} = @props
|
||||
node.style.top = screenRow * lineHeightInPixels + 'px'
|
||||
node.dataset.screenRow = screenRow
|
||||
@screenRowsByLineNumberId[lineNumberId] = screenRow
|
||||
@lineNumberIdsByScreenRow[screenRow] = lineNumberId
|
||||
|
||||
hasDecoration: (decorations, decoration) ->
|
||||
decorations? and decorations[decoration.id] is decoration
|
||||
|
||||
hasLineNumberNode: (lineNumberId) ->
|
||||
@lineNumberNodesById.hasOwnProperty(lineNumberId)
|
||||
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses}) ->
|
||||
className = "line-number line-number-#{bufferRow}"
|
||||
className += " " + decorationClasses.join(' ') if decorationClasses?
|
||||
className += " foldable" if foldable
|
||||
className
|
||||
|
||||
lineNumberNodeForScreenRow: (screenRow) ->
|
||||
@lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]]
|
||||
for id, lineNumberState of @oldState.lineNumbers
|
||||
if lineNumberState.screenRow is screenRow
|
||||
return @lineNumberNodesById[id]
|
||||
null
|
||||
|
||||
onMouseDown: (event) ->
|
||||
{editor} = @props
|
||||
{target} = event
|
||||
lineNumber = target.parentNode
|
||||
|
||||
|
||||
@@ -5,94 +5,46 @@ React = require 'react-atom-fork'
|
||||
module.exports =
|
||||
HighlightComponent = React.createClass
|
||||
displayName: 'HighlightComponent'
|
||||
currentFlashCount: 0
|
||||
currentFlashClass: null
|
||||
|
||||
render: ->
|
||||
{startPixelPosition, endPixelPosition, decoration} = @props
|
||||
{state} = @props
|
||||
|
||||
className = 'highlight'
|
||||
className += " #{decoration.class}" if decoration.class?
|
||||
className += " #{state.class}" if state.class?
|
||||
|
||||
div {className},
|
||||
if endPixelPosition.top is startPixelPosition.top
|
||||
@renderSingleLineRegions(decoration.deprecatedRegionClass)
|
||||
else
|
||||
@renderMultiLineRegions(decoration.deprecatedRegionClass)
|
||||
for region, i in state.regions
|
||||
regionClassName = 'region'
|
||||
regionClassName += " #{state.deprecatedRegionClass}" if state.deprecatedRegionClass?
|
||||
div className: regionClassName, key: i, style: region
|
||||
|
||||
componentDidMount: ->
|
||||
{editor, decoration} = @props
|
||||
if decoration.id?
|
||||
@decoration = editor.decorationForId(decoration.id)
|
||||
@decorationDisposable = @decoration.onDidFlash @startFlashAnimation
|
||||
@startFlashAnimation()
|
||||
@flashIfRequested()
|
||||
|
||||
componentWillUnmount: ->
|
||||
@decorationDisposable?.dispose()
|
||||
@decorationDisposable = null
|
||||
componentDidUpdate: ->
|
||||
@flashIfRequested()
|
||||
|
||||
startFlashAnimation: ->
|
||||
return unless flash = @decoration.consumeNextFlash()
|
||||
flashIfRequested: ->
|
||||
if @props.state.flashCount > @currentFlashCount
|
||||
@currentFlashCount = @props.state.flashCount
|
||||
|
||||
node = @getDOMNode()
|
||||
node.classList.remove(flash.class)
|
||||
node = @getDOMNode()
|
||||
{flashClass, flashDuration} = @props.state
|
||||
|
||||
requestAnimationFrame =>
|
||||
node.classList.add(flash.class)
|
||||
clearTimeout(@flashTimeoutId)
|
||||
removeFlashClass = -> node.classList.remove(flash.class)
|
||||
@flashTimeoutId = setTimeout(removeFlashClass, flash.duration)
|
||||
addFlashClass = =>
|
||||
node.classList.add(flashClass)
|
||||
@currentFlashClass = flashClass
|
||||
@flashTimeoutId = setTimeout(removeFlashClass, flashDuration)
|
||||
|
||||
renderSingleLineRegions: (regionClass) ->
|
||||
{startPixelPosition, endPixelPosition, lineHeightInPixels} = @props
|
||||
removeFlashClass = =>
|
||||
node.classList.remove(@currentFlashClass)
|
||||
@currentFlashClass = null
|
||||
clearTimeout(@flashTimeoutId)
|
||||
|
||||
className = 'region'
|
||||
className += " #{regionClass}" if regionClass?
|
||||
|
||||
[
|
||||
div className: className, key: 0, style:
|
||||
top: startPixelPosition.top
|
||||
height: lineHeightInPixels
|
||||
left: startPixelPosition.left
|
||||
width: endPixelPosition.left - startPixelPosition.left
|
||||
]
|
||||
|
||||
renderMultiLineRegions: (regionClass) ->
|
||||
{startPixelPosition, endPixelPosition, lineHeightInPixels} = @props
|
||||
|
||||
className = 'region'
|
||||
className += " #{regionClass}" if regionClass?
|
||||
|
||||
regions = []
|
||||
index = 0
|
||||
|
||||
# First row, extending from selection start to the right side of screen
|
||||
regions.push(
|
||||
div className: className, key: index++, style:
|
||||
top: startPixelPosition.top
|
||||
left: startPixelPosition.left
|
||||
height: lineHeightInPixels
|
||||
right: 0
|
||||
)
|
||||
|
||||
# Middle rows, extending from left side to right side of screen
|
||||
if endPixelPosition.top - startPixelPosition.top > lineHeightInPixels
|
||||
regions.push(
|
||||
div className: className, key: index++, style:
|
||||
top: startPixelPosition.top + lineHeightInPixels
|
||||
height: endPixelPosition.top - startPixelPosition.top - lineHeightInPixels
|
||||
left: 0
|
||||
right: 0
|
||||
)
|
||||
|
||||
# Last row, extending from left side of screen to selection end
|
||||
regions.push(
|
||||
div className: className, key: index, style:
|
||||
top: endPixelPosition.top
|
||||
height: lineHeightInPixels
|
||||
left: 0
|
||||
width: endPixelPosition.left
|
||||
)
|
||||
|
||||
regions
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
not isEqualForProperties(newProps, @props, 'startPixelPosition', 'endPixelPosition', 'lineHeightInPixels', 'decoration')
|
||||
if @currentFlashClass?
|
||||
removeFlashClass()
|
||||
requestAnimationFrame(addFlashClass)
|
||||
else
|
||||
addFlashClass()
|
||||
|
||||
@@ -9,16 +9,13 @@ HighlightsComponent = React.createClass
|
||||
|
||||
render: ->
|
||||
div className: 'highlights',
|
||||
@renderHighlights() if @props.performedInitialMeasurement
|
||||
@renderHighlights()
|
||||
|
||||
renderHighlights: ->
|
||||
{editor, highlightDecorations, lineHeightInPixels} = @props
|
||||
|
||||
{presenter} = @props
|
||||
highlightComponents = []
|
||||
for markerId, {startPixelPosition, endPixelPosition, decorations} of highlightDecorations
|
||||
for decoration in decorations
|
||||
highlightComponents.push(HighlightComponent({editor, key: "#{markerId}-#{decoration.id}", startPixelPosition, endPixelPosition, decoration, lineHeightInPixels}))
|
||||
|
||||
for key, state of presenter.state.content.highlights
|
||||
highlightComponents.push(HighlightComponent({key, state}))
|
||||
highlightComponents
|
||||
|
||||
componentDidMount: ->
|
||||
@@ -26,6 +23,3 @@ HighlightsComponent = React.createClass
|
||||
insertionPoint = document.createElement('content')
|
||||
insertionPoint.setAttribute('select', '.underlayer')
|
||||
@getDOMNode().appendChild(insertionPoint)
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
not isEqualForProperties(newProps, @props, 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth', 'scopedCharacterWidthsChangeCount')
|
||||
|
||||
@@ -182,26 +182,8 @@ class LanguageMode
|
||||
|
||||
[bufferRow, foldEndRow]
|
||||
|
||||
# Public: Returns a {Boolean} indicating whether the given buffer row starts a
|
||||
# foldable row range. Rows that are "foldable" have a fold icon next to their
|
||||
# icon in the gutter in the default configuration.
|
||||
isFoldableAtBufferRow: (bufferRow) ->
|
||||
@isFoldableCodeAtBufferRow(bufferRow) or @isFoldableCommentAtBufferRow(bufferRow)
|
||||
|
||||
# Returns a {Boolean} indicating whether the given buffer row starts
|
||||
# a a foldable row range due to the code's indentation patterns.
|
||||
isFoldableCodeAtBufferRow: (bufferRow) ->
|
||||
return false if @editor.isBufferRowBlank(bufferRow) or @isLineCommentedAtBufferRow(bufferRow)
|
||||
nextNonEmptyRow = @editor.nextNonBlankBufferRow(bufferRow)
|
||||
return false unless nextNonEmptyRow?
|
||||
@editor.indentationForBufferRow(nextNonEmptyRow) > @editor.indentationForBufferRow(bufferRow)
|
||||
|
||||
# Returns a {Boolean} indicating whether the given buffer row starts
|
||||
# a foldable row range due to being the start of a multi-line comment.
|
||||
isFoldableCommentAtBufferRow: (bufferRow) ->
|
||||
@isLineCommentedAtBufferRow(bufferRow) and
|
||||
@isLineCommentedAtBufferRow(bufferRow + 1) and
|
||||
not @isLineCommentedAtBufferRow(bufferRow - 1)
|
||||
@editor.displayBuffer.tokenizedBuffer.isFoldableAtRow(bufferRow)
|
||||
|
||||
# Returns a {Boolean} indicating whether the line at the given buffer
|
||||
# row is a comment.
|
||||
|
||||
@@ -4,7 +4,6 @@ React = require 'react-atom-fork'
|
||||
{debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
|
||||
{$$} = require 'space-pen'
|
||||
|
||||
Decoration = require './decoration'
|
||||
CursorsComponent = require './cursors-component'
|
||||
HighlightsComponent = require './highlights-component'
|
||||
OverlayManager = require './overlay-manager'
|
||||
@@ -18,33 +17,26 @@ LinesComponent = React.createClass
|
||||
displayName: 'LinesComponent'
|
||||
|
||||
render: ->
|
||||
{performedInitialMeasurement, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
|
||||
{editor, presenter} = @props
|
||||
@oldState ?= {lines: {}}
|
||||
@newState = presenter.state.content
|
||||
|
||||
if performedInitialMeasurement
|
||||
{editor, overlayDecorations, highlightDecorations, scrollHeight, scrollWidth, placeholderText, backgroundColor} = @props
|
||||
{lineHeightInPixels, defaultCharWidth, scrollViewHeight, scopedCharacterWidthsChangeCount} = @props
|
||||
{scrollTop, scrollLeft, cursorPixelRects} = @props
|
||||
style =
|
||||
height: Math.max(scrollHeight, scrollViewHeight)
|
||||
width: scrollWidth
|
||||
WebkitTransform: @getTransform()
|
||||
backgroundColor: if editor.isMini() then null else backgroundColor
|
||||
{scrollHeight, scrollWidth, backgroundColor, placeholderText} = @newState
|
||||
|
||||
style =
|
||||
height: scrollHeight
|
||||
width: scrollWidth
|
||||
WebkitTransform: @getTransform()
|
||||
backgroundColor: backgroundColor
|
||||
|
||||
div {className: 'lines', style},
|
||||
div className: 'placeholder-text', placeholderText if placeholderText?
|
||||
|
||||
CursorsComponent {
|
||||
cursorPixelRects, cursorBlinkPeriod, cursorBlinkResumeDelay, lineHeightInPixels,
|
||||
defaultCharWidth, scopedCharacterWidthsChangeCount, performedInitialMeasurement
|
||||
}
|
||||
|
||||
HighlightsComponent {
|
||||
editor, highlightDecorations, lineHeightInPixels, defaultCharWidth,
|
||||
scopedCharacterWidthsChangeCount, performedInitialMeasurement
|
||||
}
|
||||
CursorsComponent {presenter}
|
||||
HighlightsComponent {presenter}
|
||||
|
||||
getTransform: ->
|
||||
{scrollTop, scrollLeft, useHardwareAcceleration} = @props
|
||||
{scrollTop, scrollLeft} = @newState
|
||||
{useHardwareAcceleration} = @props
|
||||
|
||||
if useHardwareAcceleration
|
||||
"translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)"
|
||||
@@ -52,7 +44,7 @@ LinesComponent = React.createClass
|
||||
"translate(#{-scrollLeft}px, #{-scrollTop}px)"
|
||||
|
||||
componentWillMount: ->
|
||||
@measuredLines = new WeakSet
|
||||
@measuredLines = new Set
|
||||
@lineNodesByLineId = {}
|
||||
@screenRowsByLineId = {}
|
||||
@lineIdsByScreenRow = {}
|
||||
@@ -71,124 +63,91 @@ LinesComponent = React.createClass
|
||||
else
|
||||
@overlayManager = new OverlayManager(@getDOMNode())
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
return true unless isEqualForProperties(newProps, @props,
|
||||
'renderedRowRange', 'lineDecorations', 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth',
|
||||
'overlayDecorations', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', 'visible',
|
||||
'scrollViewHeight', 'mouseWheelScreenRow', 'scopedCharacterWidthsChangeCount', 'lineWidth', 'useHardwareAcceleration',
|
||||
'placeholderText', 'performedInitialMeasurement', 'backgroundColor', 'cursorPixelRects'
|
||||
)
|
||||
componentDidUpdate: ->
|
||||
{visible, presenter} = @props
|
||||
|
||||
{renderedRowRange, pendingChanges} = newProps
|
||||
return false unless renderedRowRange?
|
||||
|
||||
[renderedStartRow, renderedEndRow] = renderedRowRange
|
||||
for change in pendingChanges
|
||||
if change.screenDelta is 0
|
||||
return true unless change.end < renderedStartRow or renderedEndRow <= change.start
|
||||
else
|
||||
return true unless renderedEndRow <= change.start
|
||||
|
||||
false
|
||||
|
||||
componentDidUpdate: (prevProps) ->
|
||||
{visible, scrollingVertically, performedInitialMeasurement} = @props
|
||||
return unless performedInitialMeasurement
|
||||
|
||||
@clearScreenRowCaches() unless prevProps.lineHeightInPixels is @props.lineHeightInPixels
|
||||
@removeLineNodes() unless isEqualForProperties(prevProps, @props, 'showIndentGuide')
|
||||
@updateLines(@props.lineWidth isnt prevProps.lineWidth)
|
||||
@measureCharactersInNewLines() if visible and not scrollingVertically
|
||||
@removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
|
||||
@updateLineNodes()
|
||||
@measureCharactersInNewLines() if visible and not @newState.scrollingVertically
|
||||
|
||||
@overlayManager?.render(@props)
|
||||
|
||||
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
|
||||
@oldState.scrollWidth = @newState.scrollWidth
|
||||
|
||||
clearScreenRowCaches: ->
|
||||
@screenRowsByLineId = {}
|
||||
@lineIdsByScreenRow = {}
|
||||
|
||||
updateLines: (updateWidth) ->
|
||||
{tokenizedLines, renderedRowRange, showIndentGuide, selectionChanged, lineDecorations} = @props
|
||||
[startRow] = renderedRowRange
|
||||
removeLineNodes: ->
|
||||
@removeLineNode(id) for id of @oldState.lines
|
||||
|
||||
@removeLineNodes(tokenizedLines)
|
||||
@appendOrUpdateVisibleLineNodes(tokenizedLines, startRow, updateWidth)
|
||||
removeLineNode: (id) ->
|
||||
@lineNodesByLineId[id].remove()
|
||||
delete @lineNodesByLineId[id]
|
||||
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
|
||||
delete @screenRowsByLineId[id]
|
||||
delete @oldState.lines[id]
|
||||
|
||||
removeLineNodes: (visibleLines=[]) ->
|
||||
{mouseWheelScreenRow} = @props
|
||||
visibleLineIds = new Set
|
||||
visibleLineIds.add(line.id.toString()) for line in visibleLines
|
||||
node = @getDOMNode()
|
||||
for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId)
|
||||
screenRow = @screenRowsByLineId[lineId]
|
||||
if not screenRow? or screenRow isnt mouseWheelScreenRow
|
||||
delete @lineNodesByLineId[lineId]
|
||||
delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId
|
||||
delete @screenRowsByLineId[lineId]
|
||||
delete @renderedDecorationsByLineId[lineId]
|
||||
node.removeChild(lineNode)
|
||||
updateLineNodes: ->
|
||||
{presenter} = @props
|
||||
|
||||
appendOrUpdateVisibleLineNodes: (visibleLines, startRow, updateWidth) ->
|
||||
{lineDecorations} = @props
|
||||
for id of @oldState.lines
|
||||
unless @newState.lines.hasOwnProperty(id)
|
||||
@removeLineNode(id)
|
||||
|
||||
newLines = null
|
||||
newLineIds = null
|
||||
newLinesHTML = null
|
||||
|
||||
for line, index in visibleLines
|
||||
screenRow = startRow + index
|
||||
|
||||
if @hasLineNode(line.id)
|
||||
@updateLineNode(line, screenRow, updateWidth)
|
||||
for id, lineState of @newState.lines
|
||||
if @oldState.lines.hasOwnProperty(id)
|
||||
@updateLineNode(id)
|
||||
else
|
||||
newLines ?= []
|
||||
newLineIds ?= []
|
||||
newLinesHTML ?= ""
|
||||
newLines.push(line)
|
||||
newLinesHTML += @buildLineHTML(line, screenRow)
|
||||
@screenRowsByLineId[line.id] = screenRow
|
||||
@lineIdsByScreenRow[screenRow] = line.id
|
||||
newLineIds.push(id)
|
||||
newLinesHTML += @buildLineHTML(id)
|
||||
@screenRowsByLineId[id] = lineState.screenRow
|
||||
@lineIdsByScreenRow[lineState.screenRow] = id
|
||||
@oldState.lines[id] = _.clone(lineState)
|
||||
|
||||
@renderedDecorationsByLineId[line.id] = lineDecorations[screenRow]
|
||||
|
||||
return unless newLines?
|
||||
return unless newLineIds?
|
||||
|
||||
WrapperDiv.innerHTML = newLinesHTML
|
||||
newLineNodes = toArray(WrapperDiv.children)
|
||||
node = @getDOMNode()
|
||||
for line, i in newLines
|
||||
for id, i in newLineIds
|
||||
lineNode = newLineNodes[i]
|
||||
@lineNodesByLineId[line.id] = lineNode
|
||||
@lineNodesByLineId[id] = lineNode
|
||||
node.appendChild(lineNode)
|
||||
|
||||
hasLineNode: (lineId) ->
|
||||
@lineNodesByLineId.hasOwnProperty(lineId)
|
||||
|
||||
buildLineHTML: (line, screenRow) ->
|
||||
{showIndentGuide, lineHeightInPixels, lineDecorations, lineWidth} = @props
|
||||
{tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line
|
||||
buildLineHTML: (id) ->
|
||||
{presenter} = @props
|
||||
{scrollWidth} = @newState
|
||||
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id]
|
||||
|
||||
classes = ''
|
||||
if decorations = lineDecorations[screenRow]
|
||||
for id, decoration of decorations
|
||||
if Decoration.isType(decoration, 'line')
|
||||
classes += decoration.class + ' '
|
||||
if decorationClasses?
|
||||
for decorationClass in decorationClasses
|
||||
classes += decorationClass + ' '
|
||||
classes += 'line'
|
||||
|
||||
top = screenRow * lineHeightInPixels
|
||||
lineHTML = "<div class=\"#{classes}\" style=\"position: absolute; top: #{top}px; width: #{lineWidth}px;\" data-screen-row=\"#{screenRow}\">"
|
||||
lineHTML = "<div class=\"#{classes}\" style=\"position: absolute; top: #{top}px; width: #{scrollWidth}px;\" data-screen-row=\"#{screenRow}\">"
|
||||
|
||||
if text is ""
|
||||
lineHTML += @buildEmptyLineInnerHTML(line)
|
||||
lineHTML += @buildEmptyLineInnerHTML(id)
|
||||
else
|
||||
lineHTML += @buildLineInnerHTML(line)
|
||||
lineHTML += @buildLineInnerHTML(id)
|
||||
|
||||
lineHTML += '<span class="fold-marker"></span>' if fold
|
||||
lineHTML += "</div>"
|
||||
lineHTML
|
||||
|
||||
buildEmptyLineInnerHTML: (line) ->
|
||||
{showIndentGuide} = @props
|
||||
{indentLevel, tabLength, endOfLineInvisibles} = line
|
||||
buildEmptyLineInnerHTML: (id) ->
|
||||
{indentGuidesVisible} = @newState
|
||||
{indentLevel, tabLength, endOfLineInvisibles} = @newState.lines[id]
|
||||
|
||||
if showIndentGuide and indentLevel > 0
|
||||
if indentGuidesVisible and indentLevel > 0
|
||||
invisibleIndex = 0
|
||||
lineHTML = ''
|
||||
for i in [0...indentLevel]
|
||||
@@ -201,30 +160,30 @@ LinesComponent = React.createClass
|
||||
lineHTML += "</span>"
|
||||
|
||||
while invisibleIndex < endOfLineInvisibles?.length
|
||||
lineHTML += "<span class='invisible-character'>#{line.endOfLineInvisibles[invisibleIndex++]}</span>"
|
||||
lineHTML += "<span class='invisible-character'>#{endOfLineInvisibles[invisibleIndex++]}</span>"
|
||||
|
||||
lineHTML
|
||||
else
|
||||
@buildEndOfLineHTML(line) or ' '
|
||||
@buildEndOfLineHTML(id) or ' '
|
||||
|
||||
buildLineInnerHTML: (line) ->
|
||||
{editor, showIndentGuide} = @props
|
||||
{tokens, text} = line
|
||||
buildLineInnerHTML: (id) ->
|
||||
{editor} = @props
|
||||
{indentGuidesVisible} = @newState
|
||||
{tokens, text, isOnlyWhitespace} = @newState.lines[id]
|
||||
innerHTML = ""
|
||||
|
||||
scopeStack = []
|
||||
lineIsWhitespaceOnly = line.isOnlyWhitespace()
|
||||
for token in tokens
|
||||
innerHTML += @updateScopeStack(scopeStack, token.scopes)
|
||||
hasIndentGuide = not editor.isMini() and showIndentGuide and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and lineIsWhitespaceOnly))
|
||||
hasIndentGuide = indentGuidesVisible and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and isOnlyWhitespace))
|
||||
innerHTML += token.getValueAsHtml({hasIndentGuide})
|
||||
|
||||
innerHTML += @popScope(scopeStack) while scopeStack.length > 0
|
||||
innerHTML += @buildEndOfLineHTML(line)
|
||||
innerHTML += @buildEndOfLineHTML(id)
|
||||
innerHTML
|
||||
|
||||
buildEndOfLineHTML: (line) ->
|
||||
{endOfLineInvisibles} = line
|
||||
buildEndOfLineHTML: (id) ->
|
||||
{endOfLineInvisibles} = @newState.lines[id]
|
||||
|
||||
html = ''
|
||||
if endOfLineInvisibles?
|
||||
@@ -257,33 +216,30 @@ LinesComponent = React.createClass
|
||||
scopeStack.push(scope)
|
||||
"<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
|
||||
|
||||
updateLineNode: (line, screenRow, updateWidth) ->
|
||||
{lineHeightInPixels, lineDecorations, lineWidth} = @props
|
||||
lineNode = @lineNodesByLineId[line.id]
|
||||
updateLineNode: (id) ->
|
||||
{scrollWidth} = @newState
|
||||
{screenRow, top} = @newState.lines[id]
|
||||
|
||||
decorations = lineDecorations[screenRow]
|
||||
previousDecorations = @renderedDecorationsByLineId[line.id]
|
||||
lineNode = @lineNodesByLineId[id]
|
||||
|
||||
if previousDecorations?
|
||||
for id, decoration of previousDecorations
|
||||
if Decoration.isType(decoration, 'line') and not @hasDecoration(decorations, decoration)
|
||||
lineNode.classList.remove(decoration.class)
|
||||
newDecorationClasses = @newState.lines[id].decorationClasses
|
||||
oldDecorationClasses = @oldState.lines[id].decorationClasses
|
||||
|
||||
if decorations?
|
||||
for id, decoration of decorations
|
||||
if Decoration.isType(decoration, 'line') and not @hasDecoration(previousDecorations, decoration)
|
||||
lineNode.classList.add(decoration.class)
|
||||
if oldDecorationClasses?
|
||||
for decorationClass in oldDecorationClasses
|
||||
unless newDecorationClasses? and decorationClass in newDecorationClasses
|
||||
lineNode.classList.remove(decorationClass)
|
||||
|
||||
lineNode.style.width = lineWidth + 'px' if updateWidth
|
||||
if newDecorationClasses?
|
||||
for decorationClass in newDecorationClasses
|
||||
unless oldDecorationClasses? and decorationClass in oldDecorationClasses
|
||||
lineNode.classList.add(decorationClass)
|
||||
|
||||
unless @screenRowsByLineId[line.id] is screenRow
|
||||
lineNode.style.top = screenRow * lineHeightInPixels + 'px'
|
||||
lineNode.dataset.screenRow = screenRow
|
||||
@screenRowsByLineId[line.id] = screenRow
|
||||
@lineIdsByScreenRow[screenRow] = line.id
|
||||
|
||||
hasDecoration: (decorations, decoration) ->
|
||||
decorations? and decorations[decoration.id] is decoration
|
||||
lineNode.style.width = scrollWidth + 'px'
|
||||
lineNode.style.top = top + 'px'
|
||||
lineNode.dataset.screenRow = screenRow
|
||||
@screenRowsByLineId[id] = screenRow
|
||||
@lineIdsByScreenRow[screenRow] = id
|
||||
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
|
||||
@@ -295,26 +251,24 @@ LinesComponent = React.createClass
|
||||
charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
|
||||
node.removeChild(DummyLineNode)
|
||||
|
||||
{editor} = @props
|
||||
editor.setLineHeightInPixels(lineHeightInPixels)
|
||||
editor.setDefaultCharWidth(charWidth)
|
||||
{editor, presenter} = @props
|
||||
presenter.setLineHeight(lineHeightInPixels)
|
||||
presenter.setBaseCharacterWidth(charWidth)
|
||||
|
||||
remeasureCharacterWidths: ->
|
||||
return unless @props.performedInitialMeasurement
|
||||
return unless @props.presenter.hasRequiredMeasurements()
|
||||
|
||||
@clearScopedCharWidths()
|
||||
@measureCharactersInNewLines()
|
||||
|
||||
measureCharactersInNewLines: ->
|
||||
{editor, tokenizedLines, renderedRowRange} = @props
|
||||
[visibleStartRow] = renderedRowRange
|
||||
node = @getDOMNode()
|
||||
{presenter} = @props
|
||||
|
||||
editor.batchCharacterMeasurement =>
|
||||
for tokenizedLine in tokenizedLines
|
||||
unless @measuredLines.has(tokenizedLine)
|
||||
lineNode = @lineNodesByLineId[tokenizedLine.id]
|
||||
@measureCharactersInLine(tokenizedLine, lineNode)
|
||||
presenter.batchCharacterMeasurement =>
|
||||
for id, lineState of @oldState.lines
|
||||
unless @measuredLines.has(id)
|
||||
lineNode = @lineNodesByLineId[id]
|
||||
@measureCharactersInLine(lineState, lineNode)
|
||||
return
|
||||
|
||||
measureCharactersInLine: (tokenizedLine, lineNode) ->
|
||||
@@ -356,12 +310,13 @@ LinesComponent = React.createClass
|
||||
rangeForMeasurement.setStart(textNode, i)
|
||||
rangeForMeasurement.setEnd(textNode, i + charLength)
|
||||
charWidth = rangeForMeasurement.getBoundingClientRect().width
|
||||
editor.setScopedCharWidth(scopes, char, charWidth)
|
||||
@props.presenter.setScopedCharacterWidth(scopes, char, charWidth)
|
||||
|
||||
charIndex += charLength
|
||||
|
||||
@measuredLines.add(tokenizedLine)
|
||||
@measuredLines.add(tokenizedLine.id)
|
||||
|
||||
clearScopedCharWidths: ->
|
||||
@measuredLines.clear()
|
||||
@props.editor.clearScopedCharWidths()
|
||||
@props.presenter.clearScopedCharacterWidths()
|
||||
|
||||
@@ -1,46 +1,41 @@
|
||||
module.exports =
|
||||
class OverlayManager
|
||||
constructor: (@container) ->
|
||||
@overlays = {}
|
||||
@overlayNodesById = {}
|
||||
|
||||
render: (props) ->
|
||||
{editor, overlayDecorations, lineHeightInPixels} = props
|
||||
{presenter} = props
|
||||
|
||||
existingDecorations = null
|
||||
for markerId, {headPixelPosition, tailPixelPosition, decorations} of overlayDecorations
|
||||
for decoration in decorations
|
||||
pixelPosition =
|
||||
if decoration.position is 'tail' then tailPixelPosition else headPixelPosition
|
||||
for decorationId, {pixelPosition, item} of presenter.state.content.overlays
|
||||
@renderOverlay(presenter, decorationId, item, pixelPosition)
|
||||
|
||||
@renderOverlay(editor, decoration, pixelPosition, lineHeightInPixels)
|
||||
|
||||
existingDecorations ?= {}
|
||||
existingDecorations[decoration.id] = true
|
||||
|
||||
for id, overlay of @overlays
|
||||
unless existingDecorations? and id of existingDecorations
|
||||
@container.removeChild(overlay)
|
||||
delete @overlays[id]
|
||||
for id, overlayNode of @overlayNodesById
|
||||
unless presenter.state.content.overlays.hasOwnProperty(id)
|
||||
overlayNode.remove()
|
||||
delete @overlayNodesById[id]
|
||||
|
||||
return
|
||||
|
||||
renderOverlay: (editor, decoration, pixelPosition, lineHeightInPixels) ->
|
||||
item = atom.views.getView(decoration.item)
|
||||
unless overlay = @overlays[decoration.id]
|
||||
overlay = @overlays[decoration.id] = document.createElement('atom-overlay')
|
||||
overlay.appendChild(item)
|
||||
@container.appendChild(overlay)
|
||||
renderOverlay: (presenter, decorationId, item, pixelPosition) ->
|
||||
item = atom.views.getView(item)
|
||||
unless overlayNode = @overlayNodesById[decorationId]
|
||||
overlayNode = @overlayNodesById[decorationId] = document.createElement('atom-overlay')
|
||||
overlayNode.appendChild(item)
|
||||
@container.appendChild(overlayNode)
|
||||
|
||||
itemWidth = item.offsetWidth
|
||||
itemHeight = item.offsetHeight
|
||||
|
||||
|
||||
{scrollTop, scrollLeft} = presenter.state.content
|
||||
|
||||
left = pixelPosition.left
|
||||
if left + itemWidth - editor.getScrollLeft() > editor.getWidth() and left - itemWidth >= editor.getScrollLeft()
|
||||
if left + itemWidth - scrollLeft > presenter.contentFrameWidth and left - itemWidth >= scrollLeft
|
||||
left -= itemWidth
|
||||
|
||||
top = pixelPosition.top + lineHeightInPixels
|
||||
if top + itemHeight - editor.getScrollTop() > editor.getHeight() and top - itemHeight - lineHeightInPixels >= editor.getScrollTop()
|
||||
top -= itemHeight + lineHeightInPixels
|
||||
top = pixelPosition.top + presenter.lineHeight
|
||||
if top + itemHeight - scrollTop > presenter.computeHeight() and top - itemHeight - presenter.lineHeight >= scrollTop
|
||||
top -= itemHeight + presenter.lineHeight
|
||||
|
||||
overlay.style.top = top + 'px'
|
||||
overlay.style.left = left + 'px'
|
||||
overlayNode.style.top = top + 'px'
|
||||
overlayNode.style.left = left + 'px'
|
||||
|
||||
@@ -7,28 +7,33 @@ ScrollbarComponent = React.createClass
|
||||
displayName: 'ScrollbarComponent'
|
||||
|
||||
render: ->
|
||||
{orientation, className, scrollHeight, scrollWidth, visible} = @props
|
||||
{scrollableInOppositeDirection, horizontalScrollbarHeight, verticalScrollbarWidth} = @props
|
||||
{useHardwareAcceleration} = @props
|
||||
{presenter, orientation, className, useHardwareAcceleration} = @props
|
||||
|
||||
switch orientation
|
||||
when 'vertical'
|
||||
@newState = presenter.state.verticalScrollbar
|
||||
when 'horizontal'
|
||||
@newState = presenter.state.horizontalScrollbar
|
||||
|
||||
style = {}
|
||||
style.display = 'none' unless visible
|
||||
|
||||
style.display = 'none' unless @newState.visible
|
||||
style.transform = 'translateZ(0)' if useHardwareAcceleration # See atom/atom#3559
|
||||
switch orientation
|
||||
when 'vertical'
|
||||
style.width = verticalScrollbarWidth
|
||||
style.bottom = horizontalScrollbarHeight if scrollableInOppositeDirection
|
||||
style.width = @newState.width
|
||||
style.bottom = @newState.bottom
|
||||
when 'horizontal'
|
||||
style.left = 0
|
||||
style.right = verticalScrollbarWidth if scrollableInOppositeDirection
|
||||
style.height = horizontalScrollbarHeight
|
||||
style.right = @newState.right
|
||||
style.height = @newState.height
|
||||
|
||||
div {className, style},
|
||||
switch orientation
|
||||
when 'vertical'
|
||||
div className: 'scrollbar-content', style: {height: scrollHeight}
|
||||
div className: 'scrollbar-content', style: {height: @newState.scrollHeight}
|
||||
when 'horizontal'
|
||||
div className: 'scrollbar-content', style: {width: scrollWidth}
|
||||
div className: 'scrollbar-content', style: {width: @newState.scrollWidth}
|
||||
|
||||
componentDidMount: ->
|
||||
{orientation} = @props
|
||||
@@ -41,26 +46,15 @@ ScrollbarComponent = React.createClass
|
||||
componentWillUnmount: ->
|
||||
@getDOMNode().removeEventListener 'scroll', @onScroll
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
return true if newProps.visible isnt @props.visible
|
||||
|
||||
switch @props.orientation
|
||||
when 'vertical'
|
||||
not isEqualForProperties(newProps, @props, 'scrollHeight', 'scrollTop', 'scrollableInOppositeDirection', 'verticalScrollbarWidth')
|
||||
when 'horizontal'
|
||||
not isEqualForProperties(newProps, @props, 'scrollWidth', 'scrollLeft', 'scrollableInOppositeDirection', 'horizontalScrollbarHeight')
|
||||
|
||||
componentDidUpdate: ->
|
||||
{orientation, scrollTop, scrollLeft} = @props
|
||||
{orientation} = @props
|
||||
node = @getDOMNode()
|
||||
|
||||
switch orientation
|
||||
when 'vertical'
|
||||
node.scrollTop = scrollTop
|
||||
@props.scrollTop = node.scrollTop # Ensure scrollTop reflects actual DOM without triggering another update
|
||||
node.scrollTop = @newState.scrollTop
|
||||
when 'horizontal'
|
||||
node.scrollLeft = scrollLeft
|
||||
@props.scrollLeft = node.scrollLeft # Ensure scrollLeft reflects actual DOM without triggering another update
|
||||
node.scrollLeft = @newState.scrollLeft
|
||||
|
||||
onScroll: ->
|
||||
{orientation, onScroll} = @props
|
||||
@@ -69,9 +63,7 @@ ScrollbarComponent = React.createClass
|
||||
switch orientation
|
||||
when 'vertical'
|
||||
scrollTop = node.scrollTop
|
||||
@props.scrollTop = scrollTop # Ensure scrollTop reflects actual DOM without triggering another update
|
||||
onScroll(scrollTop)
|
||||
when 'horizontal'
|
||||
scrollLeft = node.scrollLeft
|
||||
@props.scrollLeft = scrollLeft # Ensure scrollLeft reflects actual DOM without triggering another update
|
||||
onScroll(scrollLeft)
|
||||
|
||||
@@ -7,7 +7,11 @@ ScrollbarCornerComponent = React.createClass
|
||||
displayName: 'ScrollbarCornerComponent'
|
||||
|
||||
render: ->
|
||||
{visible, measuringScrollbars, width, height} = @props
|
||||
{presenter, measuringScrollbars} = @props
|
||||
|
||||
visible = presenter.state.horizontalScrollbar.visible and presenter.state.verticalScrollbar.visible
|
||||
width = presenter.state.verticalScrollbar.width
|
||||
height = presenter.state.horizontalScrollbar.height
|
||||
|
||||
if measuringScrollbars
|
||||
height = 25
|
||||
@@ -19,6 +23,3 @@ ScrollbarCornerComponent = React.createClass
|
||||
div style:
|
||||
height: height + 1
|
||||
width: width + 1
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
not isEqualForProperties(newProps, @props, 'measuringScrollbars', 'visible', 'width', 'height')
|
||||
|
||||
@@ -8,6 +8,7 @@ grim = require 'grim'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
ipc = require 'ipc'
|
||||
|
||||
TextEditorPresenter = require './text-editor-presenter'
|
||||
GutterComponent = require './gutter-component'
|
||||
InputComponent = require './input-component'
|
||||
LinesComponent = require './lines-component'
|
||||
@@ -21,9 +22,6 @@ TextEditorComponent = React.createClass
|
||||
mixins: [SubscriberMixin]
|
||||
|
||||
visible: false
|
||||
autoHeight: false
|
||||
backgroundColor: null
|
||||
gutterBackgroundColor: null
|
||||
pendingScrollTop: null
|
||||
pendingScrollLeft: null
|
||||
selectOnMouseMove: false
|
||||
@@ -32,61 +30,31 @@ TextEditorComponent = React.createClass
|
||||
updateRequestedWhilePaused: false
|
||||
cursorMoved: false
|
||||
selectionChanged: false
|
||||
scrollingVertically: false
|
||||
mouseWheelScreenRow: null
|
||||
mouseWheelScreenRowClearDelay: 150
|
||||
scrollSensitivity: 0.4
|
||||
heightAndWidthMeasurementRequested: false
|
||||
inputEnabled: true
|
||||
scopedCharacterWidthsChangeCount: null
|
||||
domPollingInterval: 100
|
||||
domPollingIntervalId: null
|
||||
domPollingPaused: false
|
||||
measureScrollbarsWhenShown: true
|
||||
measureLineHeightAndDefaultCharWidthWhenShown: true
|
||||
remeasureCharacterWidthsWhenShown: false
|
||||
stylingChangeAnimationFrameRequested: false
|
||||
|
||||
render: ->
|
||||
{focused, showIndentGuide, showLineNumbers, visible} = @state
|
||||
{focused, showLineNumbers} = @state
|
||||
{editor, cursorBlinkPeriod, cursorBlinkResumeDelay, hostElement, useShadowDOM} = @props
|
||||
maxLineNumberDigits = editor.getLineCount().toString().length
|
||||
hasSelection = editor.getLastSelection()? and !editor.getLastSelection().isEmpty()
|
||||
style = {}
|
||||
|
||||
@performedInitialMeasurement = false if editor.isDestroyed()
|
||||
|
||||
if @performedInitialMeasurement
|
||||
renderedRowRange = @getRenderedRowRange()
|
||||
[renderedStartRow, renderedEndRow] = renderedRowRange
|
||||
cursorPixelRects = @getCursorPixelRects(renderedRowRange)
|
||||
|
||||
tokenizedLines = editor.tokenizedLinesForScreenRows(renderedStartRow, renderedEndRow - 1)
|
||||
|
||||
decorations = editor.decorationsForScreenRowRange(renderedStartRow, renderedEndRow)
|
||||
highlightDecorations = @getHighlightDecorations(decorations)
|
||||
overlayDecorations = @getOverlayDecorations(decorations)
|
||||
lineDecorations = @getLineDecorations(decorations)
|
||||
placeholderText = editor.getPlaceholderText() if editor.isEmpty()
|
||||
visible = @isVisible()
|
||||
|
||||
scrollHeight = editor.getScrollHeight()
|
||||
scrollWidth = editor.getScrollWidth()
|
||||
scrollTop = editor.getScrollTop()
|
||||
scrollLeft = editor.getScrollLeft()
|
||||
lineHeightInPixels = editor.getLineHeightInPixels()
|
||||
defaultCharWidth = editor.getDefaultCharWidth()
|
||||
scrollViewHeight = editor.getHeight()
|
||||
lineWidth = Math.max(scrollWidth, editor.getWidth())
|
||||
horizontalScrollbarHeight = editor.getHorizontalScrollbarHeight()
|
||||
verticalScrollbarWidth = editor.getVerticalScrollbarWidth()
|
||||
verticallyScrollable = editor.verticallyScrollable()
|
||||
horizontallyScrollable = editor.horizontallyScrollable()
|
||||
hiddenInputStyle = @getHiddenInputPosition()
|
||||
hiddenInputStyle.WebkitTransform = 'translateZ(0)' if @useHardwareAcceleration
|
||||
if @mouseWheelScreenRow? and not (renderedStartRow <= @mouseWheelScreenRow < renderedEndRow)
|
||||
mouseWheelScreenRow = @mouseWheelScreenRow
|
||||
|
||||
style.height = scrollViewHeight if @autoHeight
|
||||
style.height = @presenter.state.height if @presenter.state.height?
|
||||
|
||||
if useShadowDOM
|
||||
className = 'editor-contents--private'
|
||||
@@ -98,10 +66,8 @@ TextEditorComponent = React.createClass
|
||||
div {className, style},
|
||||
if @gutterVisible
|
||||
GutterComponent {
|
||||
ref: 'gutter', onMouseDown: @onGutterMouseDown, lineDecorations,
|
||||
defaultCharWidth, editor, renderedRowRange, maxLineNumberDigits, scrollViewHeight,
|
||||
scrollTop, scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow,
|
||||
@useHardwareAcceleration, @performedInitialMeasurement, @backgroundColor, @gutterBackgroundColor
|
||||
ref: 'gutter', onMouseDown: @onGutterMouseDown,
|
||||
@presenter, editor, @useHardwareAcceleration
|
||||
}
|
||||
|
||||
div ref: 'scrollView', className: 'scroll-view',
|
||||
@@ -111,53 +77,30 @@ TextEditorComponent = React.createClass
|
||||
style: hiddenInputStyle
|
||||
|
||||
LinesComponent {
|
||||
ref: 'lines',
|
||||
editor, lineHeightInPixels, defaultCharWidth, tokenizedLines,
|
||||
lineDecorations, highlightDecorations, overlayDecorations, hostElement,
|
||||
showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft,
|
||||
@scrollingVertically, scrollHeight, scrollWidth, mouseWheelScreenRow,
|
||||
visible, scrollViewHeight, @scopedCharacterWidthsChangeCount, lineWidth, @useHardwareAcceleration,
|
||||
placeholderText, @performedInitialMeasurement, @backgroundColor, cursorPixelRects,
|
||||
cursorBlinkPeriod, cursorBlinkResumeDelay, useShadowDOM
|
||||
ref: 'lines', @presenter, editor, hostElement, @useHardwareAcceleration, useShadowDOM, visible
|
||||
}
|
||||
|
||||
ScrollbarComponent
|
||||
ref: 'horizontalScrollbar'
|
||||
className: 'horizontal-scrollbar'
|
||||
orientation: 'horizontal'
|
||||
presenter: @presenter
|
||||
onScroll: @onHorizontalScroll
|
||||
scrollLeft: scrollLeft
|
||||
scrollWidth: scrollWidth
|
||||
visible: horizontallyScrollable
|
||||
scrollableInOppositeDirection: verticallyScrollable
|
||||
verticalScrollbarWidth: verticalScrollbarWidth
|
||||
horizontalScrollbarHeight: horizontalScrollbarHeight
|
||||
useHardwareAcceleration: @useHardwareAcceleration
|
||||
|
||||
ScrollbarComponent
|
||||
ref: 'verticalScrollbar'
|
||||
className: 'vertical-scrollbar'
|
||||
orientation: 'vertical'
|
||||
presenter: @presenter
|
||||
onScroll: @onVerticalScroll
|
||||
scrollTop: scrollTop
|
||||
scrollHeight: scrollHeight
|
||||
visible: verticallyScrollable
|
||||
scrollableInOppositeDirection: horizontallyScrollable
|
||||
verticalScrollbarWidth: verticalScrollbarWidth
|
||||
horizontalScrollbarHeight: horizontalScrollbarHeight
|
||||
useHardwareAcceleration: @useHardwareAcceleration
|
||||
|
||||
# Also used to measure the height/width of scrollbars after the initial render
|
||||
ScrollbarCornerComponent
|
||||
ref: 'scrollbarCorner'
|
||||
visible: horizontallyScrollable and verticallyScrollable
|
||||
presenter: @presenter
|
||||
measuringScrollbars: @measuringScrollbars
|
||||
height: horizontalScrollbarHeight
|
||||
width: verticalScrollbarWidth
|
||||
|
||||
getPageRows: ->
|
||||
{editor} = @props
|
||||
Math.max(1, Math.ceil(editor.getHeight() / editor.getLineHeightInPixels()))
|
||||
|
||||
getInitialState: -> {}
|
||||
|
||||
@@ -167,11 +110,23 @@ TextEditorComponent = React.createClass
|
||||
lineOverdrawMargin: 15
|
||||
|
||||
componentWillMount: ->
|
||||
@pendingChanges = []
|
||||
@props.editor.manageScrollPosition = true
|
||||
@observeConfig()
|
||||
@setScrollSensitivity(atom.config.get('editor.scrollSensitivity'))
|
||||
|
||||
{editor, lineOverdrawMargin, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
|
||||
|
||||
@presenter = new TextEditorPresenter
|
||||
model: editor
|
||||
scrollTop: editor.getScrollTop()
|
||||
scrollLeft: editor.getScrollLeft()
|
||||
lineOverdrawMargin: lineOverdrawMargin
|
||||
cursorBlinkPeriod: cursorBlinkPeriod
|
||||
cursorBlinkResumeDelay: cursorBlinkResumeDelay
|
||||
stoppedScrollingDelay: 200
|
||||
@presenter.onDidUpdateState(@requestUpdate)
|
||||
|
||||
|
||||
componentDidMount: ->
|
||||
{editor, stylesElement} = @props
|
||||
|
||||
@@ -202,7 +157,6 @@ TextEditorComponent = React.createClass
|
||||
componentDidUpdate: (prevProps, prevState) ->
|
||||
cursorMoved = @cursorMoved
|
||||
selectionChanged = @selectionChanged
|
||||
@pendingChanges.length = 0
|
||||
@cursorMoved = false
|
||||
@selectionChanged = false
|
||||
|
||||
@@ -215,10 +169,10 @@ TextEditorComponent = React.createClass
|
||||
|
||||
becameVisible: ->
|
||||
@updatesPaused = true
|
||||
@measureScrollbars() if @measureScrollbarsWhenShown
|
||||
@sampleFontStyling()
|
||||
@sampleBackgroundColors()
|
||||
@measureHeightAndWidth()
|
||||
@measureScrollbars() if @measureScrollbarsWhenShown
|
||||
@measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
|
||||
@remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown
|
||||
@props.editor.setVisible(true)
|
||||
@@ -257,13 +211,6 @@ TextEditorComponent = React.createClass
|
||||
getTopmostDOMNode: ->
|
||||
@props.hostElement
|
||||
|
||||
getRenderedRowRange: ->
|
||||
{editor, lineOverdrawMargin} = @props
|
||||
[visibleStartRow, visibleEndRow] = editor.getVisibleRowRange()
|
||||
renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin)
|
||||
renderedEndRow = Math.min(editor.getScreenLineCount(), visibleEndRow + lineOverdrawMargin)
|
||||
[renderedStartRow, renderedEndRow]
|
||||
|
||||
getHiddenInputPosition: ->
|
||||
{editor} = @props
|
||||
{focused} = @state
|
||||
@@ -277,117 +224,13 @@ TextEditorComponent = React.createClass
|
||||
left = Math.max(0, Math.min(editor.getWidth() - width, left))
|
||||
{top, left}
|
||||
|
||||
getCursorScreenRanges: (renderedRowRange) ->
|
||||
{editor} = @props
|
||||
[renderedStartRow, renderedEndRow] = renderedRowRange
|
||||
|
||||
cursorScreenRanges = {}
|
||||
for selection in editor.getSelections() when selection.isEmpty()
|
||||
{cursor} = selection
|
||||
screenRange = cursor.getScreenRange()
|
||||
if renderedStartRow <= screenRange.start.row < renderedEndRow
|
||||
cursorScreenRanges[cursor.id] = screenRange
|
||||
cursorScreenRanges
|
||||
|
||||
getCursorPixelRects: (renderedRowRange) ->
|
||||
{editor} = @props
|
||||
[renderedStartRow, renderedEndRow] = renderedRowRange
|
||||
|
||||
cursorPixelRects = {}
|
||||
for selection in editor.getSelections() when selection.isEmpty()
|
||||
{cursor} = selection
|
||||
screenRange = cursor.getScreenRange()
|
||||
if renderedStartRow <= screenRange.start.row < renderedEndRow
|
||||
cursorPixelRects[cursor.id] = editor.pixelRectForScreenRange(screenRange)
|
||||
cursorPixelRects
|
||||
|
||||
getLineDecorations: (decorationsByMarkerId) ->
|
||||
{editor} = @props
|
||||
return {} if editor.isMini()
|
||||
|
||||
decorationsByScreenRow = {}
|
||||
for markerId, decorations of decorationsByMarkerId
|
||||
marker = editor.getMarker(markerId)
|
||||
screenRange = null
|
||||
headScreenRow = null
|
||||
if marker.isValid()
|
||||
for decoration in decorations
|
||||
if decoration.isType('line-number') or decoration.isType('line')
|
||||
decorationParams = decoration.getProperties()
|
||||
screenRange ?= marker.getScreenRange()
|
||||
headScreenRow ?= marker.getHeadScreenPosition().row
|
||||
startRow = screenRange.start.row
|
||||
endRow = screenRange.end.row
|
||||
endRow-- if not screenRange.isEmpty() and screenRange.end.column == 0
|
||||
for screenRow in [startRow..endRow]
|
||||
continue if decorationParams.onlyHead and screenRow isnt headScreenRow
|
||||
if screenRange.isEmpty()
|
||||
continue if decorationParams.onlyNonEmpty
|
||||
else
|
||||
continue if decorationParams.onlyEmpty
|
||||
|
||||
decorationsByScreenRow[screenRow] ?= {}
|
||||
decorationsByScreenRow[screenRow][decoration.id] = decorationParams
|
||||
|
||||
decorationsByScreenRow
|
||||
|
||||
getHighlightDecorations: (decorationsByMarkerId) ->
|
||||
{editor} = @props
|
||||
filteredDecorations = {}
|
||||
for markerId, decorations of decorationsByMarkerId
|
||||
marker = editor.getMarker(markerId)
|
||||
screenRange = marker.getScreenRange()
|
||||
if marker.isValid() and not screenRange.isEmpty()
|
||||
for decoration in decorations
|
||||
if decoration.isType('highlight')
|
||||
decorationParams = decoration.getProperties()
|
||||
filteredDecorations[markerId] ?=
|
||||
id: markerId
|
||||
startPixelPosition: editor.pixelPositionForScreenPosition(screenRange.start, true)
|
||||
endPixelPosition: editor.pixelPositionForScreenPosition(screenRange.end, true)
|
||||
decorations: []
|
||||
filteredDecorations[markerId].decorations.push decorationParams
|
||||
filteredDecorations
|
||||
|
||||
getOverlayDecorations: (decorationsByMarkerId) ->
|
||||
{editor} = @props
|
||||
filteredDecorations = {}
|
||||
for markerId, decorations of decorationsByMarkerId
|
||||
marker = editor.getMarker(markerId)
|
||||
headScreenPosition = marker.getHeadScreenPosition()
|
||||
tailScreenPosition = marker.getTailScreenPosition()
|
||||
if marker.isValid()
|
||||
for decoration in decorations
|
||||
if decoration.isType('overlay')
|
||||
decorationParams = decoration.getProperties()
|
||||
filteredDecorations[markerId] ?=
|
||||
id: markerId
|
||||
headPixelPosition: editor.pixelPositionForScreenPosition(headScreenPosition, true)
|
||||
tailPixelPosition: editor.pixelPositionForScreenPosition(tailScreenPosition, true)
|
||||
decorations: []
|
||||
filteredDecorations[markerId].decorations.push decorationParams
|
||||
filteredDecorations
|
||||
|
||||
observeEditor: ->
|
||||
{editor} = @props
|
||||
@subscribe editor.onDidChange(@onScreenLinesChanged)
|
||||
@subscribe editor.onDidChangeGutterVisible(@updateGutterVisible)
|
||||
@subscribe editor.onDidChangeMini(@setMini)
|
||||
@subscribe editor.observeGrammar(@onGrammarChanged)
|
||||
@subscribe editor.observeCursors(@onCursorAdded)
|
||||
@subscribe editor.observeSelections(@onSelectionAdded)
|
||||
@subscribe editor.observeDecorations(@onDecorationAdded)
|
||||
@subscribe editor.onDidRemoveDecoration(@onDecorationRemoved)
|
||||
@subscribe editor.onDidChangeCharacterWidths(@onCharacterWidthsChanged)
|
||||
@subscribe editor.onDidChangePlaceholderText(@onPlaceholderTextChanged)
|
||||
@subscribe editor.$scrollTop.changes, @onScrollTopChanged
|
||||
@subscribe editor.$scrollLeft.changes, @requestUpdate
|
||||
@subscribe editor.$verticalScrollbarWidth.changes, @requestUpdate
|
||||
@subscribe editor.$horizontalScrollbarHeight.changes, @requestUpdate
|
||||
@subscribe editor.$height.changes, @requestUpdate
|
||||
@subscribe editor.$width.changes, @requestUpdate
|
||||
@subscribe editor.$defaultCharWidth.changes, @requestUpdate
|
||||
@subscribe editor.$lineHeightInPixels.changes, @requestUpdate
|
||||
|
||||
listenForDOMEvents: ->
|
||||
node = @getDOMNode()
|
||||
@@ -458,7 +301,7 @@ TextEditorComponent = React.createClass
|
||||
|
||||
scopeDescriptor = editor.getRootScopeDescriptor()
|
||||
|
||||
subscriptions.add atom.config.observe 'editor.showIndentGuide', scope: scopeDescriptor, @setShowIndentGuide
|
||||
subscriptions.add atom.config.observe 'editor.showIndentGuide', scope: scopeDescriptor, @requestUpdate
|
||||
subscriptions.add atom.config.observe 'editor.showLineNumbers', scope: scopeDescriptor, @updateGutterVisible
|
||||
subscriptions.add atom.config.observe 'editor.scrollSensitivity', scope: scopeDescriptor, @setScrollSensitivity
|
||||
|
||||
@@ -505,7 +348,7 @@ TextEditorComponent = React.createClass
|
||||
@requestAnimationFrame =>
|
||||
pendingScrollTop = @pendingScrollTop
|
||||
@pendingScrollTop = null
|
||||
@props.editor.setScrollTop(pendingScrollTop)
|
||||
@presenter.setScrollTop(pendingScrollTop)
|
||||
|
||||
onHorizontalScroll: (scrollLeft) ->
|
||||
{editor} = @props
|
||||
@@ -516,7 +359,7 @@ TextEditorComponent = React.createClass
|
||||
@pendingScrollLeft = scrollLeft
|
||||
unless animationFramePending
|
||||
@requestAnimationFrame =>
|
||||
@props.editor.setScrollLeft(@pendingScrollLeft)
|
||||
@presenter.setScrollLeft(@pendingScrollLeft)
|
||||
@pendingScrollLeft = null
|
||||
|
||||
onMouseWheel: (event) ->
|
||||
@@ -537,15 +380,13 @@ TextEditorComponent = React.createClass
|
||||
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
|
||||
# Scrolling horizontally
|
||||
previousScrollLeft = editor.getScrollLeft()
|
||||
editor.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity))
|
||||
@presenter.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity))
|
||||
event.preventDefault() unless previousScrollLeft is editor.getScrollLeft()
|
||||
else
|
||||
# Scrolling vertically
|
||||
@mouseWheelScreenRow = @screenRowForNode(event.target)
|
||||
@clearMouseWheelScreenRowAfterDelay ?= debounce(@clearMouseWheelScreenRow, @mouseWheelScreenRowClearDelay)
|
||||
@clearMouseWheelScreenRowAfterDelay()
|
||||
previousScrollTop = editor.getScrollTop()
|
||||
editor.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity))
|
||||
@presenter.setMouseWheelScreenRow(@screenRowForNode(event.target))
|
||||
previousScrollTop = @presenter.scrollTop
|
||||
@presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity))
|
||||
event.preventDefault() unless previousScrollTop is editor.getScrollTop()
|
||||
|
||||
onScrollViewScroll: ->
|
||||
@@ -668,10 +509,14 @@ TextEditorComponent = React.createClass
|
||||
# This delay prevents the styling from going haywire when stylesheets are
|
||||
# reloaded in dev mode. It seems like a workaround for a browser bug, but
|
||||
# not totally sure.
|
||||
requestAnimationFrame =>
|
||||
if @isMounted()
|
||||
@refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet)
|
||||
@handleStylingChange()
|
||||
|
||||
unless @stylingChangeAnimationFrameRequested
|
||||
@stylingChangeAnimationFrameRequested = true
|
||||
requestAnimationFrame =>
|
||||
@stylingChangeAnimationFrameRequested = false
|
||||
if @isMounted()
|
||||
@refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet)
|
||||
@handleStylingChange()
|
||||
|
||||
onAllThemesLoaded: ->
|
||||
@refreshScrollbars()
|
||||
@@ -682,11 +527,6 @@ TextEditorComponent = React.createClass
|
||||
@sampleBackgroundColors()
|
||||
@remeasureCharacterWidths()
|
||||
|
||||
onScreenLinesChanged: (change) ->
|
||||
{editor} = @props
|
||||
@pendingChanges.push(change)
|
||||
@requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events
|
||||
|
||||
onSelectionAdded: (selection) ->
|
||||
{editor} = @props
|
||||
|
||||
@@ -705,21 +545,6 @@ TextEditorComponent = React.createClass
|
||||
@selectionChanged = true
|
||||
@requestUpdate()
|
||||
|
||||
onScrollTopChanged: ->
|
||||
@scrollingVertically = true
|
||||
@requestUpdate()
|
||||
@onStoppedScrollingAfterDelay ?= debounce(@onStoppedScrolling, 200)
|
||||
@onStoppedScrollingAfterDelay()
|
||||
|
||||
onStoppedScrolling: ->
|
||||
return unless @isMounted()
|
||||
|
||||
@scrollingVertically = false
|
||||
@mouseWheelScreenRow = null
|
||||
@requestUpdate()
|
||||
|
||||
onStoppedScrollingAfterDelay: null # created lazily
|
||||
|
||||
onCursorAdded: (cursor) ->
|
||||
@subscribe cursor.onDidChangePosition @onCursorMoved
|
||||
|
||||
@@ -727,23 +552,6 @@ TextEditorComponent = React.createClass
|
||||
@cursorMoved = true
|
||||
@requestUpdate()
|
||||
|
||||
onDecorationAdded: (decoration) ->
|
||||
@subscribe decoration.onDidChangeProperties(@onDecorationChanged)
|
||||
@subscribe decoration.getMarker().onDidChange(@onDecorationChanged)
|
||||
@requestUpdate()
|
||||
|
||||
onDecorationChanged: ->
|
||||
@requestUpdate()
|
||||
|
||||
onDecorationRemoved: ->
|
||||
@requestUpdate()
|
||||
|
||||
onCharacterWidthsChanged: (@scopedCharacterWidthsChangeCount) ->
|
||||
@requestUpdate()
|
||||
|
||||
onPlaceholderTextChanged: ->
|
||||
@requestUpdate()
|
||||
|
||||
handleDragUntilMouseUp: (event, dragHandler) ->
|
||||
{editor} = @props
|
||||
dragging = false
|
||||
@@ -840,20 +648,19 @@ TextEditorComponent = React.createClass
|
||||
{height} = hostElement.style
|
||||
|
||||
if position is 'absolute' or height
|
||||
if @autoHeight
|
||||
@autoHeight = false
|
||||
@forceUpdate() if not @updatesPaused and @canUpdate()
|
||||
|
||||
clientHeight = scrollViewNode.clientHeight
|
||||
editor.setHeight(clientHeight) if clientHeight > 0
|
||||
@presenter.setAutoHeight(false)
|
||||
height = hostElement.offsetHeight
|
||||
if height > 0
|
||||
@presenter.setExplicitHeight(height)
|
||||
else
|
||||
editor.setHeight(null)
|
||||
@autoHeight = true
|
||||
@presenter.setAutoHeight(true)
|
||||
@presenter.setExplicitHeight(null)
|
||||
|
||||
clientWidth = scrollViewNode.clientWidth
|
||||
paddingLeft = parseInt(getComputedStyle(scrollViewNode).paddingLeft)
|
||||
clientWidth -= paddingLeft
|
||||
editor.setWidth(clientWidth) if clientWidth > 0
|
||||
if clientWidth > 0
|
||||
@presenter.setContentFrameWidth(clientWidth)
|
||||
|
||||
sampleFontStyling: ->
|
||||
oldFontSize = @fontSize
|
||||
@@ -870,18 +677,13 @@ TextEditorComponent = React.createClass
|
||||
|
||||
sampleBackgroundColors: (suppressUpdate) ->
|
||||
{hostElement} = @props
|
||||
{showLineNumbers} = @state
|
||||
{backgroundColor} = getComputedStyle(hostElement)
|
||||
|
||||
if backgroundColor isnt @backgroundColor
|
||||
@backgroundColor = backgroundColor
|
||||
@requestUpdate() unless suppressUpdate
|
||||
@presenter.setBackgroundColor(backgroundColor)
|
||||
|
||||
if @refs.gutter?
|
||||
gutterBackgroundColor = getComputedStyle(@refs.gutter.getDOMNode()).backgroundColor
|
||||
if gutterBackgroundColor isnt @gutterBackgroundColor
|
||||
@gutterBackgroundColor = gutterBackgroundColor
|
||||
@requestUpdate() unless suppressUpdate
|
||||
@presenter.setGutterBackgroundColor(gutterBackgroundColor)
|
||||
|
||||
measureLineHeightAndDefaultCharWidth: ->
|
||||
if @isVisible()
|
||||
@@ -909,8 +711,8 @@ TextEditorComponent = React.createClass
|
||||
width = (cornerNode.offsetWidth - cornerNode.clientWidth) or 15
|
||||
height = (cornerNode.offsetHeight - cornerNode.clientHeight) or 15
|
||||
|
||||
editor.setVerticalScrollbarWidth(width)
|
||||
editor.setHorizontalScrollbarHeight(height)
|
||||
@presenter.setVerticalScrollbarWidth(width)
|
||||
@presenter.setHorizontalScrollbarHeight(height)
|
||||
|
||||
cornerNode.style.display = originalDisplayValue
|
||||
|
||||
@@ -955,13 +757,6 @@ TextEditorComponent = React.createClass
|
||||
horizontalNode.style.display = originalHorizontalDisplayValue
|
||||
cornerNode.style.display = originalCornerDisplayValue
|
||||
|
||||
clearMouseWheelScreenRow: ->
|
||||
if @mouseWheelScreenRow?
|
||||
@mouseWheelScreenRow = null
|
||||
@requestUpdate()
|
||||
|
||||
clearMouseWheelScreenRowAfterDelay: null # created lazily
|
||||
|
||||
consolidateSelections: (e) ->
|
||||
e.abortKeyBinding() unless @props.editor.consolidateSelections()
|
||||
|
||||
@@ -995,7 +790,7 @@ TextEditorComponent = React.createClass
|
||||
@sampleFontStyling()
|
||||
|
||||
setShowIndentGuide: (showIndentGuide) ->
|
||||
@setState({showIndentGuide})
|
||||
atom.config.set("editor.showIndentGuide", showIndentGuide)
|
||||
|
||||
setMini: ->
|
||||
@updateGutterVisible()
|
||||
|
||||
835
src/text-editor-presenter.coffee
Normal file
835
src/text-editor-presenter.coffee
Normal file
@@ -0,0 +1,835 @@
|
||||
{CompositeDisposable, Emitter} = require 'event-kit'
|
||||
{Point, Range} = require 'text-buffer'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
class TextEditorPresenter
|
||||
toggleCursorBlinkHandle: null
|
||||
startBlinkingCursorsAfterDelay: null
|
||||
stoppedScrollingTimeoutId: null
|
||||
mouseWheelScreenRow: null
|
||||
scopedCharacterWidthsChangeCount: 0
|
||||
|
||||
constructor: (params) ->
|
||||
{@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft} = params
|
||||
{@horizontalScrollbarHeight, @verticalScrollbarWidth} = params
|
||||
{@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params
|
||||
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay} = params
|
||||
|
||||
@disposables = new CompositeDisposable
|
||||
@emitter = new Emitter
|
||||
@characterWidthsByScope = {}
|
||||
@transferMeasurementsToModel()
|
||||
@observeModel()
|
||||
@observeConfig()
|
||||
@buildState()
|
||||
@startBlinkingCursors()
|
||||
|
||||
destroy: ->
|
||||
@disposables.dispose()
|
||||
|
||||
onDidUpdateState: (callback) ->
|
||||
@emitter.on 'did-update-state', callback
|
||||
|
||||
transferMeasurementsToModel: ->
|
||||
@model.setHeight(@explicitHeight) if @explicitHeight?
|
||||
@model.setWidth(@contentFrameWidth) if @contentFrameWidth?
|
||||
@model.setLineHeightInPixels(@lineHeight) if @lineHeight?
|
||||
@model.setDefaultCharWidth(@baseCharacterWidth) if @baseCharacterWidth?
|
||||
@model.setScrollTop(@scrollTop) if @scrollTop?
|
||||
@model.setScrollLeft(@scrollLeft) if @scrollLeft?
|
||||
@model.setVerticalScrollbarWidth(@verticalScrollbarWidth) if @verticalScrollbarWidth?
|
||||
@model.setHorizontalScrollbarHeight(@horizontalScrollbarHeight) if @horizontalScrollbarHeight?
|
||||
|
||||
observeModel: ->
|
||||
@disposables.add @model.onDidChange =>
|
||||
@updateHeightState()
|
||||
@updateVerticalScrollState()
|
||||
@updateHorizontalScrollState()
|
||||
@updateScrollbarsState()
|
||||
@updateContentState()
|
||||
@updateDecorations()
|
||||
@updateLinesState()
|
||||
@updateGutterState()
|
||||
@updateLineNumbersState()
|
||||
@disposables.add @model.onDidChangeGrammar(@updateContentState.bind(this))
|
||||
@disposables.add @model.onDidChangePlaceholderText(@updateContentState.bind(this))
|
||||
@disposables.add @model.onDidChangeMini =>
|
||||
@updateContentState()
|
||||
@updateDecorations()
|
||||
@updateLinesState()
|
||||
@updateLineNumbersState()
|
||||
@disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this))
|
||||
@disposables.add @model.onDidAddCursor(@didAddCursor.bind(this))
|
||||
@disposables.add @model.onDidChangeScrollTop(@setScrollTop.bind(this))
|
||||
@disposables.add @model.onDidChangeScrollLeft(@setScrollLeft.bind(this))
|
||||
@observeDecoration(decoration) for decoration in @model.getDecorations()
|
||||
@observeCursor(cursor) for cursor in @model.getCursors()
|
||||
|
||||
observeConfig: ->
|
||||
@disposables.add atom.config.onDidChange 'editor.showIndentGuide', scope: @model.getRootScopeDescriptor(), @updateContentState.bind(this)
|
||||
|
||||
buildState: ->
|
||||
@state =
|
||||
horizontalScrollbar: {}
|
||||
verticalScrollbar: {}
|
||||
content:
|
||||
scrollingVertically: false
|
||||
blinkCursorsOff: false
|
||||
lines: {}
|
||||
highlights: {}
|
||||
overlays: {}
|
||||
gutter:
|
||||
lineNumbers: {}
|
||||
@updateState()
|
||||
|
||||
updateState: ->
|
||||
@updateHeightState()
|
||||
@updateVerticalScrollState()
|
||||
@updateHorizontalScrollState()
|
||||
@updateScrollbarsState()
|
||||
@updateContentState()
|
||||
@updateDecorations()
|
||||
@updateLinesState()
|
||||
@updateCursorsState()
|
||||
@updateOverlaysState()
|
||||
@updateGutterState()
|
||||
@updateLineNumbersState()
|
||||
|
||||
updateHeightState: ->
|
||||
if @autoHeight
|
||||
@state.height = @computeContentHeight()
|
||||
else
|
||||
@state.height = null
|
||||
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
updateVerticalScrollState: ->
|
||||
scrollHeight = @computeScrollHeight()
|
||||
@state.content.scrollHeight = scrollHeight
|
||||
@state.gutter.scrollHeight = scrollHeight
|
||||
@state.verticalScrollbar.scrollHeight = scrollHeight
|
||||
|
||||
scrollTop = @computeScrollTop()
|
||||
@state.content.scrollTop = scrollTop
|
||||
@state.gutter.scrollTop = scrollTop
|
||||
@state.verticalScrollbar.scrollTop = scrollTop
|
||||
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
updateHorizontalScrollState: ->
|
||||
scrollWidth = @computeScrollWidth()
|
||||
@state.content.scrollWidth = scrollWidth
|
||||
@state.horizontalScrollbar.scrollWidth = scrollWidth
|
||||
|
||||
scrollLeft = @computeScrollLeft()
|
||||
@state.content.scrollLeft = scrollLeft
|
||||
@state.horizontalScrollbar.scrollLeft = scrollLeft
|
||||
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
updateScrollbarsState: ->
|
||||
horizontalScrollbarHeight = @computeHorizontalScrollbarHeight()
|
||||
verticalScrollbarWidth = @computeVerticalScrollbarWidth()
|
||||
|
||||
@state.horizontalScrollbar.visible = horizontalScrollbarHeight > 0
|
||||
@state.horizontalScrollbar.height = @horizontalScrollbarHeight
|
||||
@state.horizontalScrollbar.right = verticalScrollbarWidth
|
||||
|
||||
@state.verticalScrollbar.visible = verticalScrollbarWidth > 0
|
||||
@state.verticalScrollbar.width = @verticalScrollbarWidth
|
||||
@state.verticalScrollbar.bottom = horizontalScrollbarHeight
|
||||
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
updateContentState: ->
|
||||
@state.content.scrollWidth = @computeScrollWidth()
|
||||
@state.content.scrollLeft = @scrollLeft
|
||||
@state.content.indentGuidesVisible = not @model.isMini() and atom.config.get('editor.showIndentGuide', scope: @model.getRootScopeDescriptor())
|
||||
@state.content.backgroundColor = if @model.isMini() then null else @backgroundColor
|
||||
@state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
updateLinesState: ->
|
||||
return unless @hasRequiredMeasurements()
|
||||
|
||||
visibleLineIds = {}
|
||||
startRow = @computeStartRow()
|
||||
endRow = @computeEndRow()
|
||||
row = startRow
|
||||
while row < endRow
|
||||
line = @model.tokenizedLineForScreenRow(row)
|
||||
visibleLineIds[line.id] = true
|
||||
if @state.content.lines.hasOwnProperty(line.id)
|
||||
@updateLineState(row, line)
|
||||
else
|
||||
@buildLineState(row, line)
|
||||
row++
|
||||
|
||||
if @mouseWheelScreenRow?
|
||||
preservedLine = @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)
|
||||
visibleLineIds[preservedLine.id] = true
|
||||
|
||||
for id, line of @state.content.lines
|
||||
unless visibleLineIds.hasOwnProperty(id)
|
||||
delete @state.content.lines[id]
|
||||
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
updateLineState: (row, line) ->
|
||||
lineState = @state.content.lines[line.id]
|
||||
lineState.screenRow = row
|
||||
lineState.top = row * @lineHeight
|
||||
lineState.decorationClasses = @lineDecorationClassesForRow(row)
|
||||
|
||||
buildLineState: (row, line) ->
|
||||
@state.content.lines[line.id] =
|
||||
screenRow: row
|
||||
text: line.text
|
||||
tokens: line.tokens
|
||||
isOnlyWhitespace: line.isOnlyWhitespace()
|
||||
endOfLineInvisibles: line.endOfLineInvisibles
|
||||
indentLevel: line.indentLevel
|
||||
tabLength: line.tabLength
|
||||
fold: line.fold
|
||||
top: row * @lineHeight
|
||||
decorationClasses: @lineDecorationClassesForRow(row)
|
||||
|
||||
updateCursorsState: ->
|
||||
@state.content.cursors = {}
|
||||
return unless @hasRequiredMeasurements()
|
||||
|
||||
startRow = @computeStartRow()
|
||||
endRow = @computeEndRow()
|
||||
|
||||
for cursor in @model.getCursors()
|
||||
if cursor.isVisible() and startRow <= cursor.getScreenRow() < endRow
|
||||
pixelRect = @pixelRectForScreenRange(cursor.getScreenRange())
|
||||
pixelRect.width = @baseCharacterWidth if pixelRect.width is 0
|
||||
@state.content.cursors[cursor.id] = pixelRect
|
||||
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
updateOverlaysState: ->
|
||||
return unless @hasRequiredMeasurements()
|
||||
|
||||
visibleDecorationIds = {}
|
||||
|
||||
for decoration in @model.getOverlayDecorations()
|
||||
continue unless decoration.getMarker().isValid()
|
||||
|
||||
{item, position} = decoration.getProperties()
|
||||
if position is 'tail'
|
||||
screenPosition = decoration.getMarker().getTailScreenPosition()
|
||||
else
|
||||
screenPosition = decoration.getMarker().getHeadScreenPosition()
|
||||
|
||||
@state.content.overlays[decoration.id] ?= {item}
|
||||
@state.content.overlays[decoration.id].pixelPosition = @pixelPositionForScreenPosition(screenPosition)
|
||||
visibleDecorationIds[decoration.id] = true
|
||||
|
||||
for id of @state.content.overlays
|
||||
delete @state.content.overlays[id] unless visibleDecorationIds[id]
|
||||
|
||||
@emitter.emit "did-update-state"
|
||||
|
||||
updateGutterState: ->
|
||||
@state.gutter.maxLineNumberDigits = @model.getLineCount().toString().length
|
||||
@state.gutter.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)"
|
||||
@gutterBackgroundColor
|
||||
else
|
||||
@backgroundColor
|
||||
@emitter.emit "did-update-state"
|
||||
|
||||
updateLineNumbersState: ->
|
||||
startRow = @computeStartRow()
|
||||
endRow = @computeEndRow()
|
||||
visibleLineNumberIds = {}
|
||||
|
||||
if startRow > 0
|
||||
rowBeforeStartRow = startRow - 1
|
||||
lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow)
|
||||
wrapCount = rowBeforeStartRow - @model.screenRowForBufferRow(lastBufferRow)
|
||||
else
|
||||
lastBufferRow = null
|
||||
wrapCount = 0
|
||||
|
||||
for bufferRow, i in @model.bufferRowsForScreenRows(startRow, endRow - 1)
|
||||
if bufferRow is lastBufferRow
|
||||
wrapCount++
|
||||
id = bufferRow + '-' + wrapCount
|
||||
softWrapped = true
|
||||
else
|
||||
id = bufferRow
|
||||
wrapCount = 0
|
||||
lastBufferRow = bufferRow
|
||||
softWrapped = false
|
||||
|
||||
screenRow = startRow + i
|
||||
top = screenRow * @lineHeight
|
||||
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
|
||||
foldable = @model.isFoldableAtScreenRow(screenRow)
|
||||
|
||||
@state.gutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
|
||||
visibleLineNumberIds[id] = true
|
||||
|
||||
if @mouseWheelScreenRow?
|
||||
bufferRow = @model.bufferRowForScreenRow(@mouseWheelScreenRow)
|
||||
wrapCount = @mouseWheelScreenRow - @model.screenRowForBufferRow(bufferRow)
|
||||
id = bufferRow
|
||||
id += '-' + wrapCount if wrapCount > 0
|
||||
visibleLineNumberIds[id] = true
|
||||
|
||||
for id of @state.gutter.lineNumbers
|
||||
delete @state.gutter.lineNumbers[id] unless visibleLineNumberIds[id]
|
||||
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
buildHighlightRegions: (screenRange) ->
|
||||
lineHeightInPixels = @lineHeight
|
||||
startPixelPosition = @pixelPositionForScreenPosition(screenRange.start, true)
|
||||
endPixelPosition = @pixelPositionForScreenPosition(screenRange.end, true)
|
||||
spannedRows = screenRange.end.row - screenRange.start.row + 1
|
||||
|
||||
if spannedRows is 1
|
||||
[
|
||||
top: startPixelPosition.top
|
||||
height: lineHeightInPixels
|
||||
left: startPixelPosition.left
|
||||
width: endPixelPosition.left - startPixelPosition.left
|
||||
]
|
||||
else
|
||||
regions = []
|
||||
|
||||
# First row, extending from selection start to the right side of screen
|
||||
regions.push(
|
||||
top: startPixelPosition.top
|
||||
left: startPixelPosition.left
|
||||
height: lineHeightInPixels
|
||||
right: 0
|
||||
)
|
||||
|
||||
# Middle rows, extending from left side to right side of screen
|
||||
if spannedRows > 2
|
||||
regions.push(
|
||||
top: startPixelPosition.top + lineHeightInPixels
|
||||
height: endPixelPosition.top - startPixelPosition.top - lineHeightInPixels
|
||||
left: 0
|
||||
right: 0
|
||||
)
|
||||
|
||||
# Last row, extending from left side of screen to selection end
|
||||
if screenRange.end.column > 0
|
||||
regions.push(
|
||||
top: endPixelPosition.top
|
||||
height: lineHeightInPixels
|
||||
left: 0
|
||||
width: endPixelPosition.left
|
||||
)
|
||||
|
||||
regions
|
||||
|
||||
computeStartRow: ->
|
||||
startRow = Math.floor(@computeScrollTop() / @lineHeight) - @lineOverdrawMargin
|
||||
Math.max(0, startRow)
|
||||
|
||||
computeEndRow: ->
|
||||
startRow = Math.floor(@computeScrollTop() / @lineHeight)
|
||||
visibleLinesCount = Math.ceil(@computeHeight() / @lineHeight) + 1
|
||||
endRow = startRow + visibleLinesCount + @lineOverdrawMargin
|
||||
Math.min(@model.getScreenLineCount(), endRow)
|
||||
|
||||
computeScrollWidth: ->
|
||||
Math.max(@computeContentWidth(), @contentFrameWidth)
|
||||
|
||||
computeScrollHeight: ->
|
||||
Math.max(@computeContentHeight(), @computeHeight())
|
||||
|
||||
computeContentWidth: ->
|
||||
contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), Infinity]).left
|
||||
contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
|
||||
contentWidth
|
||||
|
||||
computeContentHeight: ->
|
||||
@lineHeight * @model.getScreenLineCount()
|
||||
|
||||
computeClientHeight: ->
|
||||
@computeHeight() - @computeHorizontalScrollbarHeight()
|
||||
|
||||
computeClientWidth: ->
|
||||
@contentFrameWidth - @computeVerticalScrollbarWidth()
|
||||
|
||||
computeScrollTop: ->
|
||||
@scrollTop = @constrainScrollTop(@scrollTop)
|
||||
|
||||
constrainScrollTop: (scrollTop) ->
|
||||
if @hasRequiredMeasurements()
|
||||
Math.max(0, Math.min(scrollTop, @computeScrollHeight() - @computeClientHeight()))
|
||||
else
|
||||
Math.max(0, scrollTop) if scrollTop?
|
||||
|
||||
computeScrollLeft: ->
|
||||
@scrollLeft = @constrainScrollLeft(@scrollLeft)
|
||||
|
||||
constrainScrollLeft: (scrollLeft) ->
|
||||
if @hasRequiredMeasurements()
|
||||
Math.max(0, Math.min(scrollLeft, @computeScrollWidth() - @computeClientWidth()))
|
||||
else
|
||||
Math.max(0, scrollLeft) if scrollLeft?
|
||||
|
||||
computeHorizontalScrollbarHeight: ->
|
||||
contentWidth = @computeContentWidth()
|
||||
contentHeight = @computeContentHeight()
|
||||
clientWidthWithoutVerticalScrollbar = @contentFrameWidth
|
||||
clientWidthWithVerticalScrollbar = clientWidthWithoutVerticalScrollbar - @verticalScrollbarWidth
|
||||
clientHeightWithoutHorizontalScrollbar = @computeHeight()
|
||||
clientHeightWithHorizontalScrollbar = clientHeightWithoutHorizontalScrollbar - @horizontalScrollbarHeight
|
||||
|
||||
horizontalScrollbarVisible =
|
||||
contentWidth > clientWidthWithoutVerticalScrollbar or
|
||||
contentWidth > clientWidthWithVerticalScrollbar and contentHeight > clientHeightWithoutHorizontalScrollbar
|
||||
|
||||
if horizontalScrollbarVisible
|
||||
@horizontalScrollbarHeight
|
||||
else
|
||||
0
|
||||
|
||||
computeVerticalScrollbarWidth: ->
|
||||
contentWidth = @computeContentWidth()
|
||||
contentHeight = @computeContentHeight()
|
||||
clientWidthWithoutVerticalScrollbar = @contentFrameWidth
|
||||
clientWidthWithVerticalScrollbar = clientWidthWithoutVerticalScrollbar - @verticalScrollbarWidth
|
||||
clientHeightWithoutHorizontalScrollbar = @computeHeight()
|
||||
clientHeightWithHorizontalScrollbar = clientHeightWithoutHorizontalScrollbar - @horizontalScrollbarHeight
|
||||
|
||||
verticalScrollbarVisible =
|
||||
contentHeight > clientHeightWithoutHorizontalScrollbar or
|
||||
contentHeight > clientHeightWithHorizontalScrollbar and contentWidth > clientWidthWithoutVerticalScrollbar
|
||||
|
||||
if verticalScrollbarVisible
|
||||
@verticalScrollbarWidth
|
||||
else
|
||||
0
|
||||
|
||||
lineDecorationClassesForRow: (row) ->
|
||||
return null if @model.isMini()
|
||||
|
||||
decorationClasses = null
|
||||
for id, decoration of @lineDecorationsByScreenRow[row]
|
||||
decorationClasses ?= []
|
||||
decorationClasses.push(decoration.getProperties().class)
|
||||
decorationClasses
|
||||
|
||||
lineNumberDecorationClassesForRow: (row) ->
|
||||
return null if @model.isMini()
|
||||
|
||||
decorationClasses = null
|
||||
for id, decoration of @lineNumberDecorationsByScreenRow[row]
|
||||
decorationClasses ?= []
|
||||
decorationClasses.push(decoration.getProperties().class)
|
||||
decorationClasses
|
||||
|
||||
getCursorBlinkPeriod: -> @cursorBlinkPeriod
|
||||
|
||||
getCursorBlinkResumeDelay: -> @cursorBlinkResumeDelay
|
||||
|
||||
hasRequiredMeasurements: ->
|
||||
@lineHeight? and
|
||||
@baseCharacterWidth? and
|
||||
@scrollTop? and
|
||||
@contentFrameWidth? and
|
||||
@scrollLeft? and
|
||||
@verticalScrollbarWidth? and
|
||||
@horizontalScrollbarHeight?
|
||||
|
||||
setScrollTop: (scrollTop) ->
|
||||
scrollTop = @constrainScrollTop(scrollTop)
|
||||
|
||||
unless @scrollTop is scrollTop
|
||||
@scrollTop = scrollTop
|
||||
@model.setScrollTop(scrollTop)
|
||||
@didStartScrolling()
|
||||
@updateVerticalScrollState()
|
||||
@updateDecorations()
|
||||
@updateLinesState()
|
||||
@updateCursorsState()
|
||||
@updateLineNumbersState()
|
||||
|
||||
didStartScrolling: ->
|
||||
if @stoppedScrollingTimeoutId?
|
||||
clearTimeout(@stoppedScrollingTimeoutId)
|
||||
@stoppedScrollingTimeoutId = null
|
||||
@stoppedScrollingTimeoutId = setTimeout(@didStopScrolling.bind(this), @stoppedScrollingDelay)
|
||||
@state.content.scrollingVertically = true
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
didStopScrolling: ->
|
||||
@state.content.scrollingVertically = false
|
||||
if @mouseWheelScreenRow?
|
||||
@mouseWheelScreenRow = null
|
||||
@updateLinesState()
|
||||
@updateLineNumbersState()
|
||||
else
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
setScrollLeft: (scrollLeft) ->
|
||||
scrollLeft = @constrainScrollLeft(scrollLeft)
|
||||
unless @scrollLeft is scrollLeft
|
||||
@scrollLeft = scrollLeft
|
||||
@model.setScrollLeft(scrollLeft)
|
||||
@updateHorizontalScrollState()
|
||||
|
||||
setHorizontalScrollbarHeight: (horizontalScrollbarHeight) ->
|
||||
unless @horizontalScrollbarHeight is horizontalScrollbarHeight
|
||||
@horizontalScrollbarHeight = horizontalScrollbarHeight
|
||||
@model.setHorizontalScrollbarHeight(horizontalScrollbarHeight)
|
||||
@updateScrollbarsState()
|
||||
@updateVerticalScrollState()
|
||||
|
||||
setVerticalScrollbarWidth: (verticalScrollbarWidth) ->
|
||||
unless @verticalScrollbarWidth is verticalScrollbarWidth
|
||||
@verticalScrollbarWidth = verticalScrollbarWidth
|
||||
@model.setVerticalScrollbarWidth(verticalScrollbarWidth)
|
||||
@updateScrollbarsState()
|
||||
@updateHorizontalScrollState()
|
||||
|
||||
setAutoHeight: (autoHeight) ->
|
||||
unless @autoHeight is autoHeight
|
||||
@autoHeight = autoHeight
|
||||
@updateHeightState()
|
||||
|
||||
setExplicitHeight: (explicitHeight) ->
|
||||
unless @explicitHeight is explicitHeight
|
||||
@explicitHeight = explicitHeight
|
||||
@model.setHeight(explicitHeight)
|
||||
@updateVerticalScrollState()
|
||||
@updateScrollbarsState()
|
||||
@updateDecorations()
|
||||
@updateLinesState()
|
||||
@updateCursorsState()
|
||||
@updateLineNumbersState()
|
||||
|
||||
computeHeight: ->
|
||||
@explicitHeight ? @computeContentHeight()
|
||||
|
||||
setContentFrameWidth: (contentFrameWidth) ->
|
||||
unless @contentFrameWidth is contentFrameWidth
|
||||
@contentFrameWidth = contentFrameWidth
|
||||
@model.setWidth(contentFrameWidth)
|
||||
@updateVerticalScrollState()
|
||||
@updateHorizontalScrollState()
|
||||
@updateScrollbarsState()
|
||||
@updateContentState()
|
||||
@updateDecorations()
|
||||
@updateLinesState()
|
||||
|
||||
setBackgroundColor: (backgroundColor) ->
|
||||
unless @backgroundColor is backgroundColor
|
||||
@backgroundColor = backgroundColor
|
||||
@updateContentState()
|
||||
@updateGutterState()
|
||||
|
||||
setGutterBackgroundColor: (gutterBackgroundColor) ->
|
||||
unless @gutterBackgroundColor is gutterBackgroundColor
|
||||
@gutterBackgroundColor = gutterBackgroundColor
|
||||
@updateGutterState()
|
||||
|
||||
setLineHeight: (lineHeight) ->
|
||||
unless @lineHeight is lineHeight
|
||||
@lineHeight = lineHeight
|
||||
@model.setLineHeightInPixels(lineHeight)
|
||||
@updateHeightState()
|
||||
@updateVerticalScrollState()
|
||||
@updateDecorations()
|
||||
@updateLinesState()
|
||||
@updateCursorsState()
|
||||
@updateLineNumbersState()
|
||||
@updateOverlaysState()
|
||||
|
||||
setMouseWheelScreenRow: (mouseWheelScreenRow) ->
|
||||
unless @mouseWheelScreenRow is mouseWheelScreenRow
|
||||
@mouseWheelScreenRow = mouseWheelScreenRow
|
||||
@didStartScrolling()
|
||||
|
||||
setBaseCharacterWidth: (baseCharacterWidth) ->
|
||||
unless @baseCharacterWidth is baseCharacterWidth
|
||||
@baseCharacterWidth = baseCharacterWidth
|
||||
@model.setDefaultCharWidth(baseCharacterWidth)
|
||||
@characterWidthsChanged()
|
||||
|
||||
getScopedCharacterWidth: (scopeNames, char) ->
|
||||
@getScopedCharacterWidths(scopeNames)[char]
|
||||
|
||||
getScopedCharacterWidths: (scopeNames) ->
|
||||
scope = @characterWidthsByScope
|
||||
for scopeName in scopeNames
|
||||
scope[scopeName] ?= {}
|
||||
scope = scope[scopeName]
|
||||
scope.characterWidths ?= {}
|
||||
scope.characterWidths
|
||||
|
||||
batchCharacterMeasurement: (fn) ->
|
||||
oldChangeCount = @scopedCharacterWidthsChangeCount
|
||||
@batchingCharacterMeasurement = true
|
||||
@model.batchCharacterMeasurement(fn)
|
||||
@batchingCharacterMeasurement = false
|
||||
@characterWidthsChanged() if oldChangeCount isnt @scopedCharacterWidthsChangeCount
|
||||
|
||||
setScopedCharacterWidth: (scopeNames, character, width) ->
|
||||
@getScopedCharacterWidths(scopeNames)[character] = width
|
||||
@model.setScopedCharWidth(scopeNames, character, width)
|
||||
@scopedCharacterWidthsChangeCount++
|
||||
@characterWidthsChanged() unless @batchingCharacterMeasurement
|
||||
|
||||
characterWidthsChanged: ->
|
||||
@updateHorizontalScrollState()
|
||||
@updateContentState()
|
||||
@updateDecorations()
|
||||
@updateLinesState()
|
||||
@updateCursorsState()
|
||||
@updateOverlaysState()
|
||||
|
||||
clearScopedCharacterWidths: ->
|
||||
@characterWidthsByScope = {}
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition, clip=true) ->
|
||||
screenPosition = Point.fromObject(screenPosition)
|
||||
screenPosition = @model.clipScreenPosition(screenPosition) if clip
|
||||
|
||||
targetRow = screenPosition.row
|
||||
targetColumn = screenPosition.column
|
||||
baseCharacterWidth = @baseCharacterWidth
|
||||
|
||||
top = targetRow * @lineHeight
|
||||
left = 0
|
||||
column = 0
|
||||
for token in @model.tokenizedLineForScreenRow(targetRow).tokens
|
||||
characterWidths = @getScopedCharacterWidths(token.scopes)
|
||||
|
||||
valueIndex = 0
|
||||
while valueIndex < token.value.length
|
||||
if token.hasPairedCharacter
|
||||
char = token.value.substr(valueIndex, 2)
|
||||
charLength = 2
|
||||
valueIndex += 2
|
||||
else
|
||||
char = token.value[valueIndex]
|
||||
charLength = 1
|
||||
valueIndex++
|
||||
|
||||
return {top, left} if column is targetColumn
|
||||
|
||||
left += characterWidths[char] ? baseCharacterWidth unless char is '\0'
|
||||
column += charLength
|
||||
{top, left}
|
||||
|
||||
pixelRectForScreenRange: (screenRange) ->
|
||||
if screenRange.end.row > screenRange.start.row
|
||||
top = @pixelPositionForScreenPosition(screenRange.start).top
|
||||
left = 0
|
||||
height = (screenRange.end.row - screenRange.start.row + 1) * @lineHeight
|
||||
width = @computeScrollWidth()
|
||||
else
|
||||
{top, left} = @pixelPositionForScreenPosition(screenRange.start, false)
|
||||
height = @lineHeight
|
||||
width = @pixelPositionForScreenPosition(screenRange.end, false).left - left
|
||||
|
||||
{top, left, width, height}
|
||||
|
||||
observeDecoration: (decoration) ->
|
||||
decorationDisposables = new CompositeDisposable
|
||||
decorationDisposables.add decoration.getMarker().onDidChange(@decorationMarkerDidChange.bind(this, decoration))
|
||||
if decoration.isType('highlight')
|
||||
decorationDisposables.add decoration.onDidChangeProperties(@updateHighlightState.bind(this, decoration))
|
||||
decorationDisposables.add decoration.onDidFlash(@highlightDidFlash.bind(this, decoration))
|
||||
decorationDisposables.add decoration.onDidDestroy =>
|
||||
@disposables.remove(decorationDisposables)
|
||||
decorationDisposables.dispose()
|
||||
@didDestroyDecoration(decoration)
|
||||
@disposables.add(decorationDisposables)
|
||||
|
||||
decorationMarkerDidChange: (decoration, change) ->
|
||||
if decoration.isType('line') or decoration.isType('line-number')
|
||||
intersectsVisibleRowRange = false
|
||||
startRow = @computeStartRow()
|
||||
endRow = @computeEndRow()
|
||||
oldRange = new Range(change.oldTailScreenPosition, change.oldHeadScreenPosition)
|
||||
newRange = new Range(change.newTailScreenPosition, change.newHeadScreenPosition)
|
||||
|
||||
if oldRange.intersectsRowRange(startRow, endRow - 1)
|
||||
@removeFromLineDecorationCaches(decoration, oldRange)
|
||||
intersectsVisibleRowRange = true
|
||||
|
||||
if newRange.intersectsRowRange(startRow, endRow - 1)
|
||||
@addToLineDecorationCaches(decoration, newRange)
|
||||
intersectsVisibleRowRange = true
|
||||
|
||||
if intersectsVisibleRowRange
|
||||
@updateLinesState() if decoration.isType('line')
|
||||
@updateLineNumbersState() if decoration.isType('line-number')
|
||||
|
||||
if decoration.isType('highlight')
|
||||
@updateHighlightState(decoration)
|
||||
|
||||
if decoration.isType('overlay')
|
||||
@updateOverlaysState()
|
||||
|
||||
didDestroyDecoration: (decoration) ->
|
||||
if decoration.isType('line') or decoration.isType('line-number')
|
||||
@removeFromLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
|
||||
@updateLinesState() if decoration.isType('line')
|
||||
@updateLineNumbersState() if decoration.isType('line-number')
|
||||
if decoration.isType('highlight')
|
||||
@updateHighlightState(decoration)
|
||||
if decoration.isType('overlay')
|
||||
@updateOverlaysState()
|
||||
|
||||
highlightDidFlash: (decoration) ->
|
||||
flash = decoration.consumeNextFlash()
|
||||
if decorationState = @state.content.highlights[decoration.id]
|
||||
decorationState.flashCount++
|
||||
decorationState.flashClass = flash.class
|
||||
decorationState.flashDuration = flash.duration
|
||||
@emitter.emit "did-update-state"
|
||||
|
||||
didAddDecoration: (decoration) ->
|
||||
@observeDecoration(decoration)
|
||||
|
||||
if decoration.isType('line') or decoration.isType('line-number')
|
||||
@addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
|
||||
@updateLinesState() if decoration.isType('line')
|
||||
@updateLineNumbersState() if decoration.isType('line-number')
|
||||
else if decoration.isType('highlight')
|
||||
@updateHighlightState(decoration)
|
||||
else if decoration.isType('overlay')
|
||||
@updateOverlaysState()
|
||||
|
||||
updateDecorations: ->
|
||||
@lineDecorationsByScreenRow = {}
|
||||
@lineNumberDecorationsByScreenRow = {}
|
||||
@highlightDecorationsById = {}
|
||||
|
||||
visibleHighlights = {}
|
||||
startRow = @computeStartRow()
|
||||
endRow = @computeEndRow()
|
||||
return unless 0 <= startRow <= endRow <= Infinity
|
||||
|
||||
for markerId, decorations of @model.decorationsForScreenRowRange(startRow, endRow - 1)
|
||||
range = @model.getMarker(markerId).getScreenRange()
|
||||
for decoration in decorations
|
||||
if decoration.isType('line') or decoration.isType('line-number')
|
||||
@addToLineDecorationCaches(decoration, range)
|
||||
else if decoration.isType('highlight')
|
||||
visibleHighlights[decoration.id] = @updateHighlightState(decoration)
|
||||
|
||||
for id of @state.content.highlights
|
||||
unless visibleHighlights[id]
|
||||
delete @state.content.highlights[id]
|
||||
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
removeFromLineDecorationCaches: (decoration, range) ->
|
||||
for row in [range.start.row..range.end.row] by 1
|
||||
delete @lineDecorationsByScreenRow[row]?[decoration.id]
|
||||
delete @lineNumberDecorationsByScreenRow[row]?[decoration.id]
|
||||
|
||||
addToLineDecorationCaches: (decoration, range) ->
|
||||
marker = decoration.getMarker()
|
||||
properties = decoration.getProperties()
|
||||
|
||||
return unless marker.isValid()
|
||||
|
||||
if range.isEmpty()
|
||||
return if properties.onlyNonEmpty
|
||||
else
|
||||
return if properties.onlyEmpty
|
||||
omitLastRow = range.end.column is 0
|
||||
|
||||
for row in [range.start.row..range.end.row] by 1
|
||||
continue if properties.onlyHead and row isnt marker.getHeadScreenPosition().row
|
||||
continue if omitLastRow and row is range.end.row
|
||||
|
||||
if decoration.isType('line')
|
||||
@lineDecorationsByScreenRow[row] ?= {}
|
||||
@lineDecorationsByScreenRow[row][decoration.id] = decoration
|
||||
|
||||
if decoration.isType('line-number')
|
||||
@lineNumberDecorationsByScreenRow[row] ?= {}
|
||||
@lineNumberDecorationsByScreenRow[row][decoration.id] = decoration
|
||||
|
||||
updateHighlightState: (decoration) ->
|
||||
return unless @hasRequiredMeasurements()
|
||||
|
||||
startRow = @computeStartRow()
|
||||
endRow = @computeEndRow()
|
||||
properties = decoration.getProperties()
|
||||
marker = decoration.getMarker()
|
||||
range = marker.getScreenRange()
|
||||
|
||||
if decoration.isDestroyed() or not marker.isValid() or range.isEmpty() or not range.intersectsRowRange(startRow, endRow - 1)
|
||||
delete @state.content.highlights[decoration.id]
|
||||
@emitter.emit 'did-update-state'
|
||||
return
|
||||
|
||||
if range.start.row < startRow
|
||||
range.start.row = startRow
|
||||
range.start.column = 0
|
||||
if range.end.row >= endRow
|
||||
range.end.row = endRow
|
||||
range.end.column = 0
|
||||
|
||||
if range.isEmpty()
|
||||
delete @state.content.highlights[decoration.id]
|
||||
@emitter.emit 'did-update-state'
|
||||
return
|
||||
|
||||
highlightState = @state.content.highlights[decoration.id] ?= {
|
||||
flashCount: 0
|
||||
flashDuration: null
|
||||
flashClass: null
|
||||
}
|
||||
highlightState.class = properties.class
|
||||
highlightState.deprecatedRegionClass = properties.deprecatedRegionClass
|
||||
highlightState.regions = @buildHighlightRegions(range)
|
||||
|
||||
@emitter.emit 'did-update-state'
|
||||
true
|
||||
|
||||
observeCursor: (cursor) ->
|
||||
didChangePositionDisposable = cursor.onDidChangePosition =>
|
||||
@pauseCursorBlinking()
|
||||
@updateCursorsState()
|
||||
|
||||
didChangeVisibilityDisposable = cursor.onDidChangeVisibility(@updateCursorsState.bind(this))
|
||||
|
||||
didDestroyDisposable = cursor.onDidDestroy =>
|
||||
@disposables.remove(didChangePositionDisposable)
|
||||
@disposables.remove(didChangeVisibilityDisposable)
|
||||
@disposables.remove(didDestroyDisposable)
|
||||
@updateCursorsState()
|
||||
|
||||
@disposables.add(didChangePositionDisposable)
|
||||
@disposables.add(didChangeVisibilityDisposable)
|
||||
@disposables.add(didDestroyDisposable)
|
||||
|
||||
didAddCursor: (cursor) ->
|
||||
@observeCursor(cursor)
|
||||
@pauseCursorBlinking()
|
||||
@updateCursorsState()
|
||||
|
||||
startBlinkingCursors: ->
|
||||
@toggleCursorBlinkHandle = setInterval(@toggleCursorBlink.bind(this), @getCursorBlinkPeriod() / 2)
|
||||
|
||||
stopBlinkingCursors: ->
|
||||
clearInterval(@toggleCursorBlinkHandle)
|
||||
|
||||
toggleCursorBlink: ->
|
||||
@state.content.blinkCursorsOff = not @state.content.blinkCursorsOff
|
||||
@emitter.emit 'did-update-state'
|
||||
|
||||
pauseCursorBlinking: ->
|
||||
@state.content.blinkCursorsOff = false
|
||||
@stopBlinkingCursors()
|
||||
@startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay())
|
||||
@startBlinkingCursorsAfterDelay()
|
||||
@emitter.emit 'did-update-state'
|
||||
@@ -283,7 +283,7 @@ class TextEditorView extends View
|
||||
|
||||
setShowIndentGuide: (showIndentGuide) ->
|
||||
deprecate 'This is going away. Use atom.config.set("editor.showIndentGuide", true|false) instead'
|
||||
@component.setShowIndentGuide(showIndentGuide)
|
||||
atom.config.set("editor.showIndentGuide", showIndentGuide)
|
||||
|
||||
setSoftWrap: (softWrapped) ->
|
||||
deprecate 'Use TextEditor::setSoftWrapped instead. You can get the editor via editorView.getModel()'
|
||||
|
||||
@@ -721,9 +721,13 @@ class TextEditor extends Model
|
||||
# {Delegates to: DisplayBuffer.bufferRowsForScreenRows}
|
||||
bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow)
|
||||
|
||||
screenRowForBufferRow: (row) -> @displayBuffer.screenRowForBufferRow(row)
|
||||
|
||||
# {Delegates to: DisplayBuffer.getMaxLineLength}
|
||||
getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength()
|
||||
|
||||
getLongestScreenRow: -> @displayBuffer.getLongestScreenRow()
|
||||
|
||||
# Returns the range for the given buffer row.
|
||||
#
|
||||
# * `row` A row {Number}.
|
||||
@@ -1353,14 +1357,19 @@ class TextEditor extends Model
|
||||
getLineDecorations: (propertyFilter) ->
|
||||
@displayBuffer.getLineDecorations(propertyFilter)
|
||||
|
||||
# Soft-deprecated (forgot to deprecated this pre 1.0)
|
||||
getGutterDecorations: (propertyFilter) ->
|
||||
deprecate("Use ::getLineNumberDecorations instead")
|
||||
@getLineNumberDecorations(propertyFilter)
|
||||
|
||||
# Extended: Get all decorations of type 'line-number'.
|
||||
#
|
||||
# * `propertyFilter` (optional) An {Object} containing key value pairs that
|
||||
# the returned decorations' properties must match.
|
||||
#
|
||||
# Returns an {Array} of {Decoration}s.
|
||||
getGutterDecorations: (propertyFilter) ->
|
||||
@displayBuffer.getGutterDecorations(propertyFilter)
|
||||
getLineNumberDecorations: (propertyFilter) ->
|
||||
@displayBuffer.getLineNumberDecorations(propertyFilter)
|
||||
|
||||
# Extended: Get all decorations of type 'highlight'.
|
||||
#
|
||||
@@ -2700,7 +2709,8 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isFoldableAtBufferRow: (bufferRow) ->
|
||||
@languageMode.isFoldableAtBufferRow(bufferRow)
|
||||
# @languageMode.isFoldableAtBufferRow(bufferRow)
|
||||
@displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)?.foldable ? false
|
||||
|
||||
# Extended: Determine whether the given row in screen coordinates is foldable.
|
||||
#
|
||||
|
||||
@@ -147,25 +147,30 @@ class TokenizedBuffer extends Model
|
||||
rowsRemaining = @chunkSize
|
||||
|
||||
while @firstInvalidRow()? and rowsRemaining > 0
|
||||
invalidRow = @invalidRows.shift()
|
||||
startRow = @invalidRows.shift()
|
||||
lastRow = @getLastRow()
|
||||
continue if invalidRow > lastRow
|
||||
continue if startRow > lastRow
|
||||
|
||||
row = invalidRow
|
||||
row = startRow
|
||||
loop
|
||||
previousStack = @stackForRow(row)
|
||||
@tokenizedLines[row] = @buildTokenizedTokenizedLineForRow(row, @stackForRow(row - 1))
|
||||
@tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1))
|
||||
if --rowsRemaining == 0
|
||||
filledRegion = false
|
||||
endRow = row
|
||||
break
|
||||
if row == lastRow or _.isEqual(@stackForRow(row), previousStack)
|
||||
filledRegion = true
|
||||
endRow = row
|
||||
break
|
||||
row++
|
||||
|
||||
@validateRow(row)
|
||||
@invalidateRow(row + 1) unless filledRegion
|
||||
event = { start: invalidRow, end: row, delta: 0 }
|
||||
@validateRow(endRow)
|
||||
@invalidateRow(endRow + 1) unless filledRegion
|
||||
|
||||
[startRow, endRow] = @updateFoldableStatus(startRow, endRow)
|
||||
|
||||
event = {start: startRow, end: endRow, delta: 0}
|
||||
@emit 'changed', event
|
||||
@emitter.emit 'did-change', event
|
||||
|
||||
@@ -215,6 +220,9 @@ class TokenizedBuffer extends Model
|
||||
if newEndStack and not _.isEqual(newEndStack, previousEndStack)
|
||||
@invalidateRow(end + delta + 1)
|
||||
|
||||
[start, end] = @updateFoldableStatus(start, end + delta)
|
||||
end -= delta
|
||||
|
||||
event = { start, end, delta, bufferChange: e }
|
||||
@emit 'changed', event
|
||||
@emitter.emit 'did-change', event
|
||||
@@ -223,18 +231,54 @@ class TokenizedBuffer extends Model
|
||||
line = @tokenizedLines[row]
|
||||
if line?.isOnlyWhitespace() and @indentLevelForRow(row) isnt line.indentLevel
|
||||
while line?.isOnlyWhitespace()
|
||||
@tokenizedLines[row] = @buildTokenizedTokenizedLineForRow(row, @stackForRow(row - 1))
|
||||
@tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1))
|
||||
row += increment
|
||||
line = @tokenizedLines[row]
|
||||
|
||||
row - increment
|
||||
|
||||
updateFoldableStatus: (startRow, endRow) ->
|
||||
scanStartRow = @buffer.previousNonBlankRow(startRow) ? startRow
|
||||
scanStartRow-- while scanStartRow > 0 and @tokenizedLineForRow(scanStartRow).isComment()
|
||||
scanEndRow = @buffer.nextNonBlankRow(endRow) ? endRow
|
||||
|
||||
for row in [scanStartRow..scanEndRow] by 1
|
||||
foldable = @isFoldableAtRow(row)
|
||||
line = @tokenizedLineForRow(row)
|
||||
unless line.foldable is foldable
|
||||
line.foldable = foldable
|
||||
startRow = Math.min(startRow, row)
|
||||
endRow = Math.max(endRow, row)
|
||||
|
||||
[startRow, endRow]
|
||||
|
||||
isFoldableAtRow: (row) ->
|
||||
@isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row)
|
||||
|
||||
# Returns a {Boolean} indicating whether the given buffer row starts
|
||||
# a a foldable row range due to the code's indentation patterns.
|
||||
isFoldableCodeAtRow: (row) ->
|
||||
return false if @buffer.isRowBlank(row) or @tokenizedLineForRow(row).isComment()
|
||||
nextRow = @buffer.nextNonBlankRow(row)
|
||||
return false unless nextRow?
|
||||
|
||||
@indentLevelForRow(nextRow) > @indentLevelForRow(row)
|
||||
|
||||
isFoldableCommentAtRow: (row) ->
|
||||
previousRow = row - 1
|
||||
nextRow = row + 1
|
||||
return false if nextRow > @buffer.getLastRow()
|
||||
|
||||
(row is 0 or not @tokenizedLineForRow(previousRow).isComment()) and
|
||||
@tokenizedLineForRow(row).isComment() and
|
||||
@tokenizedLineForRow(nextRow).isComment()
|
||||
|
||||
buildTokenizedLinesForRows: (startRow, endRow, startingStack) ->
|
||||
ruleStack = startingStack
|
||||
stopTokenizingAt = startRow + @chunkSize
|
||||
tokenizedLines = for row in [startRow..endRow]
|
||||
if (ruleStack or row == 0) and row < stopTokenizingAt
|
||||
screenLine = @buildTokenizedTokenizedLineForRow(row, ruleStack)
|
||||
screenLine = @buildTokenizedLineForRow(row, ruleStack)
|
||||
ruleStack = screenLine.ruleStack
|
||||
else
|
||||
screenLine = @buildPlaceholderTokenizedLineForRow(row)
|
||||
@@ -257,7 +301,7 @@ class TokenizedBuffer extends Model
|
||||
lineEnding = @buffer.lineEndingForRow(row)
|
||||
new TokenizedLine({tokens, tabLength, indentLevel, @invisibles, lineEnding})
|
||||
|
||||
buildTokenizedTokenizedLineForRow: (row, ruleStack) ->
|
||||
buildTokenizedLineForRow: (row, ruleStack) ->
|
||||
line = @buffer.lineForRow(row)
|
||||
lineEnding = @buffer.lineEndingForRow(row)
|
||||
tabLength = @getTabLength()
|
||||
|
||||
@@ -10,6 +10,7 @@ module.exports =
|
||||
class TokenizedLine
|
||||
endOfLineInvisibles: null
|
||||
lineIsWhitespaceOnly: false
|
||||
foldable: false
|
||||
|
||||
constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, @tabLength, @indentLevel, @invisibles}) ->
|
||||
@startBufferColumn ?= 0
|
||||
|
||||
Reference in New Issue
Block a user