Merge branch 'batch-updates'

This commit is contained in:
Nathan Sobo
2015-02-28 10:57:49 -07:00
12 changed files with 391 additions and 355 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,13 @@ module.exports =
class CursorsComponent
oldState: null
constructor: (@presenter) ->
constructor: ->
@cursorNodesById = {}
@domNode = document.createElement('div')
@domNode.classList.add('cursors')
@updateSync()
updateSync: ->
newState = @presenter.state.content
updateSync: (state) ->
newState = state.content
@oldState ?= {cursors: {}}
# update blink class

View File

@@ -18,10 +18,8 @@ class GutterComponent
@domNode.addEventListener 'click', @onClick
@domNode.addEventListener 'mousedown', @onMouseDown
@updateSync()
updateSync: ->
@newState = @presenter.state.gutter
updateSync: (state) ->
@newState = state.gutter
@oldState ?= {lineNumbers: {}}
@appendDummyLineNumber() unless @dummyLineNumberNode?

View File

@@ -5,7 +5,7 @@ module.exports =
class HighlightsComponent
oldState: null
constructor: (@presenter) ->
constructor: ->
@highlightNodesById = {}
@regionNodesByHighlightId = {}
@@ -17,8 +17,8 @@ class HighlightsComponent
insertionPoint.setAttribute('select', '.underlayer')
@domNode.appendChild(insertionPoint)
updateSync: ->
newState = @presenter.state.content.highlights
updateSync: (state) ->
newState = state.content.highlights
@oldState ?= {}
# remove highlights

View File

@@ -1,16 +1,15 @@
module.exports =
class InputComponent
constructor: (@presenter) ->
constructor: ->
@domNode = document.createElement('input')
@domNode.classList.add('hidden-input')
@domNode.setAttribute('data-react-skip-selection-restoration', true)
@domNode.style['-webkit-transform'] = 'translateZ(0)'
@domNode.addEventListener 'paste', (event) -> event.preventDefault()
@updateSync()
updateSync: ->
updateSync: (state) ->
@oldState ?= {}
newState = @presenter.state.hiddenInput
newState = state.hiddenInput
if newState.top isnt @oldState.top
@domNode.style.top = newState.top + 'px'

View File

@@ -42,15 +42,13 @@ class LinesComponent
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', 'atom-overlay')
@overlayManager = new OverlayManager(@hostElement)
@overlayManager = new OverlayManager(@presenter, @hostElement)
@domNode.appendChild(insertionPoint)
else
@overlayManager = new OverlayManager(@domNode)
@overlayManager = new OverlayManager(@presenter, @domNode)
@updateSync(visible)
updateSync: ->
@newState = @presenter.state.content
updateSync: (state) ->
@newState = state.content
@oldState ?= {lines: {}}
if @newState.scrollHeight isnt @oldState.scrollHeight
@@ -81,10 +79,10 @@ class LinesComponent
@domNode.style.width = @newState.scrollWidth + 'px'
@oldState.scrollWidth = @newState.scrollWidth
@cursorsComponent.updateSync()
@highlightsComponent.updateSync()
@cursorsComponent.updateSync(state)
@highlightsComponent.updateSync(state)
@overlayManager?.render(@presenter)
@overlayManager?.render(state)
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
@oldState.scrollWidth = @newState.scrollWidth

View File

@@ -1,20 +1,20 @@
module.exports =
class OverlayManager
constructor: (@container) ->
constructor: (@presenter, @container) ->
@overlayNodesById = {}
render: (presenter) ->
for decorationId, {pixelPosition, item} of presenter.state.content.overlays
@renderOverlay(presenter, decorationId, item, pixelPosition)
render: (state) ->
for decorationId, {pixelPosition, item} of state.content.overlays
@renderOverlay(state, decorationId, item, pixelPosition)
for id, overlayNode of @overlayNodesById
unless presenter.state.content.overlays.hasOwnProperty(id)
unless state.content.overlays.hasOwnProperty(id)
delete @overlayNodesById[id]
overlayNode.remove()
return
renderOverlay: (presenter, decorationId, item, pixelPosition) ->
renderOverlay: (state, decorationId, item, pixelPosition) ->
item = atom.views.getView(item)
unless overlayNode = @overlayNodesById[decorationId]
overlayNode = @overlayNodesById[decorationId] = document.createElement('atom-overlay')
@@ -25,15 +25,15 @@ class OverlayManager
itemHeight = item.offsetHeight
{scrollTop, scrollLeft} = presenter.state.content
{scrollTop, scrollLeft} = state.content
left = pixelPosition.left
if left + itemWidth - scrollLeft > presenter.contentFrameWidth and left - itemWidth >= scrollLeft
if left + itemWidth - scrollLeft > @presenter.contentFrameWidth and left - itemWidth >= scrollLeft
left -= itemWidth
top = pixelPosition.top + presenter.lineHeight
if top + itemHeight - scrollTop > presenter.height and top - itemHeight - presenter.lineHeight >= scrollTop
top -= itemHeight + presenter.lineHeight
top = pixelPosition.top + @presenter.lineHeight
if top + itemHeight - scrollTop > @presenter.height and top - itemHeight - @presenter.lineHeight >= scrollTop
top -= itemHeight + @presenter.lineHeight
overlayNode.style.top = top + 'px'
overlayNode.style.left = left + 'px'

View File

@@ -1,6 +1,6 @@
module.exports =
class ScrollbarComponent
constructor: ({@presenter, @orientation, @onScroll}) ->
constructor: ({@orientation, @onScroll}) ->
@domNode = document.createElement('div')
@domNode.classList.add "#{@orientation}-scrollbar"
@domNode.style['-webkit-transform'] = 'translateZ(0)' # See atom/atom#3559
@@ -12,16 +12,14 @@ class ScrollbarComponent
@domNode.addEventListener 'scroll', @onScrollCallback
@updateSync()
updateSync: ->
updateSync: (state) ->
@oldState ?= {}
switch @orientation
when 'vertical'
@newState = @presenter.state.verticalScrollbar
@newState = state.verticalScrollbar
@updateVertical()
when 'horizontal'
@newState = @presenter.state.horizontalScrollbar
@newState = state.horizontalScrollbar
@updateHorizontal()
if @newState.visible isnt @oldState.visible

View File

@@ -1,20 +1,18 @@
module.exports =
class ScrollbarCornerComponent
constructor: (@presenter) ->
constructor: () ->
@domNode = document.createElement('div')
@domNode.classList.add('scrollbar-corner')
@contentNode = document.createElement('div')
@domNode.appendChild(@contentNode)
@updateSync()
updateSync: ->
updateSync: (state) ->
@oldState ?= {}
@newState ?= {}
newHorizontalState = @presenter.state.horizontalScrollbar
newVerticalState = @presenter.state.verticalScrollbar
newHorizontalState = state.horizontalScrollbar
newVerticalState = state.verticalScrollbar
@newState.visible = newHorizontalState.visible and newVerticalState.visible
@newState.height = newHorizontalState.height
@newState.width = newVerticalState.width

View File

@@ -64,21 +64,21 @@ class TextEditorComponent
@scrollViewNode.classList.add('scroll-view')
@domNode.appendChild(@scrollViewNode)
@mountGutterComponent() if @presenter.state.gutter.visible
@mountGutterComponent() if @presenter.getState().gutter.visible
@hiddenInputComponent = new InputComponent(@presenter)
@hiddenInputComponent = new InputComponent
@scrollViewNode.appendChild(@hiddenInputComponent.domNode)
@linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM})
@scrollViewNode.appendChild(@linesComponent.domNode)
@horizontalScrollbarComponent = new ScrollbarComponent({@presenter, orientation: 'horizontal', onScroll: @onHorizontalScroll})
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
@scrollViewNode.appendChild(@horizontalScrollbarComponent.domNode)
@verticalScrollbarComponent = new ScrollbarComponent({@presenter, orientation: 'vertical', onScroll: @onVerticalScroll})
@verticalScrollbarComponent = new ScrollbarComponent({orientation: 'vertical', onScroll: @onVerticalScroll})
@domNode.appendChild(@verticalScrollbarComponent.domNode)
@scrollbarCornerComponent = new ScrollbarCornerComponent(@presenter)
@scrollbarCornerComponent = new ScrollbarCornerComponent
@domNode.appendChild(@scrollbarCornerComponent.domNode)
@observeEditor()
@@ -104,7 +104,7 @@ class TextEditorComponent
updateSync: ->
@oldState ?= {}
@newState = @presenter.state
@newState = @presenter.getState()
cursorMoved = @cursorMoved
selectionChanged = @selectionChanged
@@ -128,18 +128,18 @@ class TextEditorComponent
else
@domNode.style.height = ''
if @presenter.state.gutter.visible
if @newState.gutter.visible
@mountGutterComponent() unless @gutterComponent?
@gutterComponent.updateSync()
@gutterComponent.updateSync(@newState)
else
@gutterComponent?.domNode?.remove()
@gutterComponent = null
@hiddenInputComponent.updateSync()
@linesComponent.updateSync()
@horizontalScrollbarComponent.updateSync()
@verticalScrollbarComponent.updateSync()
@scrollbarCornerComponent.updateSync()
@hiddenInputComponent.updateSync(@newState)
@linesComponent.updateSync(@newState)
@horizontalScrollbarComponent.updateSync(@newState)
@verticalScrollbarComponent.updateSync(@newState)
@scrollbarCornerComponent.updateSync(@newState)
if @editor.isAlive()
@updateParentViewFocusedClassIfNeeded()
@@ -152,7 +152,7 @@ class TextEditorComponent
@linesComponent.measureCharactersInNewLines() if @isVisible() and not @newState.content.scrollingVertically
mountGutterComponent: ->
@gutterComponent = new GutterComponent({@presenter, @editor, onMouseDown: @onGutterMouseDown})
@gutterComponent = new GutterComponent({@editor, onMouseDown: @onGutterMouseDown})
@domNode.insertBefore(@gutterComponent.domNode, @domNode.firstChild)
becameVisible: ->

View File

@@ -26,13 +26,18 @@ class TextEditorPresenter
@observeConfig()
@buildState()
@startBlinkingCursors() if @focused
@updating = false
destroy: ->
@disposables.dispose()
# Calls your `callback` when some changes in the model occurred and the current state has been updated.
onDidUpdateState: (callback) ->
@emitter.on 'did-update-state', callback
emitDidUpdateState: ->
@emitter.emit "did-update-state" if @isBatching()
transferMeasurementsToModel: ->
@model.setHeight(@explicitHeight) if @explicitHeight?
@model.setWidth(@contentFrameWidth) if @contentFrameWidth?
@@ -43,6 +48,59 @@ class TextEditorPresenter
@model.setVerticalScrollbarWidth(@measuredVerticalScrollbarWidth) if @measuredVerticalScrollbarWidth?
@model.setHorizontalScrollbarHeight(@measuredHorizontalScrollbarHeight) if @measuredHorizontalScrollbarHeight?
# Private: Determines whether {TextEditorPresenter} is currently batching changes.
# Returns a {Boolean}, `true` if is collecting changes, `false` if is applying them.
isBatching: ->
@updating is false
# Private: Executes `fn` if `isBatching()` is false, otherwise sets `@[flagName]` to `true` for later processing. In either cases, it calls `emitDidUpdateState`.
# * `flagName` {String} name of a property of this presenter
# * `fn` {Function} to call when not batching.
batch: (flagName, fn) ->
if @isBatching()
@[flagName] = true
else
fn.apply(this)
@emitDidUpdateState()
# Public: Gets this presenter's state, updating it just in time before returning from this function.
# Returns a state {Object}, useful for rendering to screen.
getState: ->
@updating = true
@updateFocusedState() if @shouldUpdateFocusedState
@updateHeightState() if @shouldUpdateHeightState
@updateVerticalScrollState() if @shouldUpdateVerticalScrollState
@updateHorizontalScrollState() if @shouldUpdateHorizontalScrollState
@updateScrollbarsState() if @shouldUpdateScrollbarsState
@updateHiddenInputState() if @shouldUpdateHiddenInputState
@updateContentState() if @shouldUpdateContentState
@updateDecorations() if @shouldUpdateDecorations
@updateLinesState() if @shouldUpdateLinesState
@updateCursorsState() if @shouldUpdateCursorsState
@updateOverlaysState() if @shouldUpdateOverlaysState
@updateGutterState() if @shouldUpdateGutterState
@updateLineNumbersState() if @shouldUpdateLineNumbersState
@shouldUpdateFocusedState = false
@shouldUpdateHeightState = false
@shouldUpdateVerticalScrollState = false
@shouldUpdateHorizontalScrollState = false
@shouldUpdateScrollbarsState = false
@shouldUpdateHiddenInputState = false
@shouldUpdateContentState = false
@shouldUpdateDecorations = false
@shouldUpdateLinesState = false
@shouldUpdateCursorsState = false
@shouldUpdateOverlaysState = false
@shouldUpdateGutterState = false
@shouldUpdateLineNumbersState = false
@updating = false
@state
observeModel: ->
@disposables.add @model.onDidChange =>
@updateContentDimensions()
@@ -141,18 +199,16 @@ class TextEditorPresenter
@updateGutterState()
@updateLineNumbersState()
updateFocusedState: ->
updateFocusedState: -> @batch "shouldUpdateFocusedState", ->
@state.focused = @focused
updateHeightState: ->
updateHeightState: -> @batch "shouldUpdateHeightState", ->
if @autoHeight
@state.height = @contentHeight
else
@state.height = null
@emitter.emit 'did-update-state'
updateVerticalScrollState: ->
updateVerticalScrollState: -> @batch "shouldUpdateVerticalScrollState", ->
@state.content.scrollHeight = @scrollHeight
@state.gutter.scrollHeight = @scrollHeight
@state.verticalScrollbar.scrollHeight = @scrollHeight
@@ -161,18 +217,14 @@ class TextEditorPresenter
@state.gutter.scrollTop = @scrollTop
@state.verticalScrollbar.scrollTop = @scrollTop
@emitter.emit 'did-update-state'
updateHorizontalScrollState: ->
updateHorizontalScrollState: -> @batch "shouldUpdateHorizontalScrollState", ->
@state.content.scrollWidth = @scrollWidth
@state.horizontalScrollbar.scrollWidth = @scrollWidth
@state.content.scrollLeft = @scrollLeft
@state.horizontalScrollbar.scrollLeft = @scrollLeft
@emitter.emit 'did-update-state'
updateScrollbarsState: ->
updateScrollbarsState: -> @batch "shouldUpdateScrollbarsState", ->
@state.horizontalScrollbar.visible = @horizontalScrollbarHeight > 0
@state.horizontalScrollbar.height = @measuredHorizontalScrollbarHeight
@state.horizontalScrollbar.right = @verticalScrollbarWidth
@@ -181,9 +233,7 @@ class TextEditorPresenter
@state.verticalScrollbar.width = @measuredVerticalScrollbarWidth
@state.verticalScrollbar.bottom = @horizontalScrollbarHeight
@emitter.emit 'did-update-state'
updateHiddenInputState: ->
updateHiddenInputState: -> @batch "shouldUpdateHiddenInputState", ->
return unless lastCursor = @model.getLastCursor()
{top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange())
@@ -200,17 +250,14 @@ class TextEditorPresenter
@state.hiddenInput.height = height
@state.hiddenInput.width = Math.max(width, 2)
@emitter.emit 'did-update-state'
updateContentState: ->
updateContentState: -> @batch "shouldUpdateContentState", ->
@state.content.scrollWidth = @scrollWidth
@state.content.scrollLeft = @scrollLeft
@state.content.indentGuidesVisible = not @model.isMini() and @showIndentGuide
@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: ->
updateLinesState: -> @batch "shouldUpdateLinesState", ->
return unless @startRow? and @endRow? and @lineHeight?
visibleLineIds = {}
@@ -235,8 +282,6 @@ class TextEditorPresenter
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
@@ -256,14 +301,10 @@ class TextEditorPresenter
top: row * @lineHeight
decorationClasses: @lineDecorationClassesForRow(row)
updateCursorsState: ->
updateCursorsState: -> @batch "shouldUpdateCursorsState", ->
@state.content.cursors = {}
@updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
@emitter.emit 'did-update-state'
updateCursorState: (cursor, destroyOnly = false) ->
delete @state.content.cursors[cursor.id]
@@ -275,7 +316,9 @@ class TextEditorPresenter
pixelRect.width = @baseCharacterWidth if pixelRect.width is 0
@state.content.cursors[cursor.id] = pixelRect
updateOverlaysState: ->
@emitDidUpdateState()
updateOverlaysState: -> @batch "shouldUpdateOverlaysState", ->
return unless @hasPixelRectRequirements()
visibleDecorationIds = {}
@@ -296,18 +339,15 @@ class TextEditorPresenter
for id of @state.content.overlays
delete @state.content.overlays[id] unless visibleDecorationIds[id]
@emitter.emit "did-update-state"
updateGutterState: ->
updateGutterState: -> @batch "shouldUpdateGutterState", ->
@state.gutter.visible = not @model.isMini() and (@model.isGutterVisible() ? true) and @showLineNumbers
@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: ->
updateLineNumbersState: -> @batch "shouldUpdateLineNumbersState", ->
return unless @startRow? and @endRow? and @lineHeight?
visibleLineNumberIds = {}
@@ -350,8 +390,6 @@ class TextEditorPresenter
for id of @state.gutter.lineNumbers
delete @state.gutter.lineNumbers[id] unless visibleLineNumberIds[id]
@emitter.emit 'did-update-state'
updateStartRow: ->
return unless @scrollTop? and @lineHeight?
@@ -536,7 +574,7 @@ class TextEditorPresenter
@stoppedScrollingTimeoutId = null
@stoppedScrollingTimeoutId = setTimeout(@didStopScrolling.bind(this), @stoppedScrollingDelay)
@state.content.scrollingVertically = true
@emitter.emit 'did-update-state'
@emitDidUpdateState()
didStopScrolling: ->
@state.content.scrollingVertically = false
@@ -545,7 +583,7 @@ class TextEditorPresenter
@updateLinesState()
@updateLineNumbersState()
else
@emitter.emit 'did-update-state'
@emitDidUpdateState()
setScrollLeft: (scrollLeft) ->
scrollLeft = @constrainScrollLeft(scrollLeft)
@@ -809,7 +847,7 @@ class TextEditorPresenter
decorationState.flashCount++
decorationState.flashClass = flash.class
decorationState.flashDuration = flash.duration
@emitter.emit "did-update-state"
@emitDidUpdateState()
didAddDecoration: (decoration) ->
@observeDecoration(decoration)
@@ -823,7 +861,7 @@ class TextEditorPresenter
else if decoration.isType('overlay')
@updateOverlaysState()
updateDecorations: ->
updateDecorations: -> @batch "shouldUpdateDecorations", ->
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@highlightDecorationsById = {}
@@ -843,7 +881,6 @@ class TextEditorPresenter
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
@@ -883,7 +920,7 @@ class TextEditorPresenter
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'
@emitDidUpdateState()
return
if range.start.row < @startRow
@@ -895,7 +932,7 @@ class TextEditorPresenter
if range.isEmpty()
delete @state.content.highlights[decoration.id]
@emitter.emit 'did-update-state'
@emitDidUpdateState()
return
highlightState = @state.content.highlights[decoration.id] ?= {
@@ -906,8 +943,8 @@ class TextEditorPresenter
highlightState.class = properties.class
highlightState.deprecatedRegionClass = properties.deprecatedRegionClass
highlightState.regions = @buildHighlightRegions(range)
@emitDidUpdateState()
@emitter.emit 'did-update-state'
true
buildHighlightRegions: (screenRange) ->
@@ -993,10 +1030,10 @@ class TextEditorPresenter
toggleCursorBlink: ->
@state.content.cursorsVisible = not @state.content.cursorsVisible
@emitter.emit 'did-update-state'
@emitDidUpdateState()
pauseCursorBlinking: ->
@stopBlinkingCursors(true)
@startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay())
@startBlinkingCursorsAfterDelay()
@emitter.emit 'did-update-state'
@emitDidUpdateState()

View File

@@ -834,7 +834,8 @@ class TextEditor extends Model
# argument will be a {Selection} and the second argument will be the
# {Number} index of that selection.
mutateSelectedText: (fn) ->
@transact => fn(selection, index) for selection, index in @getSelections()
@mergeIntersectingSelections =>
@transact => fn(selection, index) for selection, index in @getSelections()
# Move lines intersection the most recent selection up by one row in screen
# coordinates.
@@ -993,17 +994,18 @@ class TextEditor extends Model
# selections to create multiple single-line selections that cumulatively cover
# the same original area.
splitSelectionsIntoLines: ->
for selection in @getSelections()
range = selection.getBufferRange()
continue if range.isSingleLine()
@mergeIntersectingSelections =>
for selection in @getSelections()
range = selection.getBufferRange()
continue if range.isSingleLine()
selection.destroy()
{start, end} = range
@addSelectionForBufferRange([start, [start.row, Infinity]])
{row} = start
while ++row < end.row
@addSelectionForBufferRange([[row, 0], [row, Infinity]])
@addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0
selection.destroy()
{start, end} = range
@addSelectionForBufferRange([start, [start.row, Infinity]])
{row} = start
while ++row < end.row
@addSelectionForBufferRange([[row, 0], [row, Infinity]])
@addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0
# Extended: For each selection, transpose the selected text.
#
@@ -1140,7 +1142,8 @@ class TextEditor extends Model
# with a positive `groupingInterval` is committed while the previous transaction is
# still 'groupable', the two transactions are merged with respect to undo and redo.
# * `fn` A {Function} to call inside the transaction.
transact: (groupingInterval, fn) -> @buffer.transact(groupingInterval, fn)
transact: (groupingInterval, fn) ->
@buffer.transact(groupingInterval, fn)
# Deprecated: Start an open-ended transaction.
beginTransaction: (groupingInterval) -> @buffer.beginTransaction(groupingInterval)
@@ -2228,6 +2231,7 @@ class TextEditor extends Model
[head, tail...] = @getSelectionsOrderedByBufferPosition()
_.reduce(tail, reducer, [head])
return result if fn?
# Add a {Selection} based on the given {Marker}.
#