Make editor push decorator updates to the gutter

This commit is contained in:
Ben Ogle
2014-06-05 17:56:39 -07:00
parent 1b8be75a76
commit 5cd8f5952f
3 changed files with 64 additions and 59 deletions

View File

@@ -359,7 +359,7 @@ describe "EditorComponent", ->
editor.addDecorationToBufferRow(2, type: 'gutter', class: 'fancy-class')
editor.addDecorationToBufferRow(2, type: 'someother-type', class: 'nope-class')
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(lineNumberHasClass(2, 'fancy-class')).toBe true
expect(lineNumberHasClass(2, 'nope-class')).toBe false
@@ -367,7 +367,7 @@ describe "EditorComponent", ->
editor.removeDecorationFromBufferRow(2, type: 'gutter', class: 'fancy-class')
editor.removeDecorationFromBufferRow(2, type: 'someother-type', class: 'nope-class')
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(lineNumberHasClass(2, 'fancy-class')).toBe false
expect(lineNumberHasClass(2, 'nope-class')).toBe false
@@ -390,7 +390,7 @@ describe "EditorComponent", ->
editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'no-wrap')
editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'wrap-me')
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(lineNumberHasClass(2, 'no-wrap')).toBe false
expect(lineNumberHasClass(2, 'wrap-me')).toBe false
@@ -401,7 +401,7 @@ describe "EditorComponent", ->
editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap')
editor.addDecorationToBufferRow(1, type: 'gutter', class: 'wrap-me', softWrap: true)
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(lineNumberHasClass(2, 'no-wrap')).toBe true
expect(lineNumberHasClass(2, 'wrap-me')).toBe true
@@ -414,7 +414,7 @@ describe "EditorComponent", ->
marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside')
decoration = {type: 'gutter', class: 'someclass'}
editor.addDecorationForMarker(marker, decoration)
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
it "updates line number classes when the marker moves", ->
expect(lineNumberHasClass(1, 'someclass')).toBe false
@@ -424,7 +424,7 @@ describe "EditorComponent", ->
editor.getBuffer().insert([0, 0], '\n')
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(lineNumberHasClass(2, 'someclass')).toBe false
expect(lineNumberHasClass(3, 'someclass')).toBe true
@@ -433,7 +433,7 @@ describe "EditorComponent", ->
editor.getBuffer().deleteRows(0, 1)
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(lineNumberHasClass(0, 'someclass')).toBe false
expect(lineNumberHasClass(1, 'someclass')).toBe true
@@ -443,7 +443,7 @@ describe "EditorComponent", ->
it "removes line number classes when a decoration's marker is invalidated", ->
editor.getBuffer().insert([3, 2], 'n')
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(marker.isValid()).toBe false
@@ -454,7 +454,7 @@ describe "EditorComponent", ->
editor.getBuffer().undo()
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(marker.isValid()).toBe true
expect(lineNumberHasClass(1, 'someclass')).toBe false
@@ -465,7 +465,7 @@ describe "EditorComponent", ->
it "removes the classes and unsubscribes from the marker when decoration is removed", ->
editor.removeDecorationForMarker(marker, decoration)
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(lineNumberHasClass(1, 'someclass')).toBe false
expect(lineNumberHasClass(2, 'someclass')).toBe false
@@ -474,7 +474,7 @@ describe "EditorComponent", ->
editor.getBuffer().insert([0, 0], '\n')
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(lineNumberHasClass(2, 'someclass')).toBe false
expect(lineNumberHasClass(3, 'someclass')).toBe false
@@ -482,7 +482,7 @@ describe "EditorComponent", ->
it "removes the line number classes when the decoration's marker is destroyed", ->
marker.destroy()
waitsFor -> not gutter.decorationRenderImmediate?
waitsFor -> not component.decorationChangedImmediate?
runs ->
expect(lineNumberHasClass(1, 'someclass')).toBe false
expect(lineNumberHasClass(2, 'someclass')).toBe false

View File

@@ -49,6 +49,7 @@ EditorComponent = React.createClass
[renderedStartRow, renderedEndRow] = renderedRowRange
cursorScreenRanges = @getCursorScreenRanges(renderedRowRange)
selectionScreenRanges = @getSelectionScreenRanges(renderedRowRange)
decorations = @getGutterDecorations(renderedRowRange)
scrollHeight = editor.getScrollHeight()
scrollWidth = editor.getScrollWidth()
scrollTop = editor.getScrollTop()
@@ -71,7 +72,8 @@ EditorComponent = React.createClass
div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1,
GutterComponent {
ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop,
scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow
scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow,
decorations
}
div ref: 'scrollView', className: 'scroll-view', onMouseDown: @onMouseDown,
@@ -225,6 +227,19 @@ EditorComponent = React.createClass
selectionScreenRanges
getGutterDecorations: (renderedRowRange) ->
{editor} = @props
[renderedStartRow, renderedEndRow] = renderedRowRange
bufferRows = editor.bufferRowsForScreenRows(renderedStartRow, renderedEndRow - 1)
decorations = {}
for bufferRow in bufferRows
decorations[bufferRow] = editor.decorationsForBufferRow(bufferRow, 'gutter')
decorations[bufferRow].push {class: 'foldable'} if editor.isFoldableAtBufferRow(bufferRow)
decorations[bufferRow].push {class: 'folded'} if editor.isFoldedAtBufferRow(bufferRow)
decorations
observeEditor: ->
{editor} = @props
@subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted
@@ -233,6 +248,7 @@ EditorComponent = React.createClass
@subscribe editor, 'cursors-moved', @onCursorsMoved
@subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged
@subscribe editor, 'selection-added', @onSelectionAdded
@subscribe editor, 'decoration-changed', @onDecorationChanged
@subscribe editor.$scrollTop.changes, @onScrollTopChanged
@subscribe editor.$scrollLeft.changes, @requestUpdate
@subscribe editor.$height.changes, @requestUpdate
@@ -510,6 +526,11 @@ EditorComponent = React.createClass
onCursorsMoved: ->
@cursorsMoved = true
onDecorationChanged: ->
@decorationChangedImmediate = setImmediate =>
@requestUpdate()
@decorationChangedImmediate = null
selectToMousePositionUntilMouseUp: (event) ->
{editor} = @props
dragging = false

View File

@@ -1,3 +1,4 @@
_ = require 'underscore-plus'
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
@@ -24,17 +25,10 @@ GutterComponent = React.createClass
@lineNumberNodesById = {}
@lineNumberIdsByScreenRow = {}
@screenRowsByLineNumberId = {}
@decoratorUpdates = {}
@previousDecorations = {}
componentDidMount: ->
@appendDummyLineNumber()
@subscribeToEditor()
componentWillUnmount: ->
@unsubscribe()
subscribeToEditor: ->
@subscribe @props.editor, 'decoration-changed', @onDecorationChanged
# 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
@@ -44,10 +38,12 @@ GutterComponent = React.createClass
'renderedRowRange', 'scrollTop', 'lineHeightInPixels', 'mouseWheelScreenRow'
)
{renderedRowRange, pendingChanges} = newProps
{renderedRowRange, pendingChanges, decorations} = newProps
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
return true unless _.isEqual(@previousDecorations, decorations)
false
componentDidUpdate: (oldProps) ->
@@ -78,7 +74,7 @@ GutterComponent = React.createClass
@removeLineNumberNodes(lineNumberIdsToPreserve)
appendOrUpdateVisibleLineNumberNodes: ->
{editor, renderedRowRange, scrollTop, maxLineNumberDigits} = @props
{editor, renderedRowRange, scrollTop, maxLineNumberDigits, decorations} = @props
[startRow, endRow] = renderedRowRange
newLineNumberIds = null
@@ -99,12 +95,12 @@ GutterComponent = React.createClass
visibleLineNumberIds.add(id)
if @hasLineNumberNode(id)
@updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0)
@updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0, decorations[bufferRow])
else
newLineNumberIds ?= []
newLineNumbersHTML ?= ""
newLineNumberIds.push(id)
newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow)
newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow, decorations[bufferRow])
@screenRowsByLineNumberId[id] = screenRow
@lineNumberIdsByScreenRow[screenRow] = id
@@ -118,7 +114,7 @@ GutterComponent = React.createClass
@lineNumberNodesById[lineNumberId] = lineNumberNode
node.appendChild(lineNumberNode)
@decoratorUpdates = {}
@previousDecorations = decorations
visibleLineNumberIds
removeLineNumberNodes: (lineNumberIdsToPreserve) ->
@@ -132,7 +128,7 @@ GutterComponent = React.createClass
delete @screenRowsByLineNumberId[lineNumberId]
node.removeChild(lineNumberNode)
buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) ->
buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow, decorations) ->
if screenRow?
{lineHeightInPixels} = @props
style = "position: absolute; top: #{screenRow * lineHeightInPixels}px;"
@@ -140,15 +136,13 @@ GutterComponent = React.createClass
style = "visibility: hidden;"
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits)
classes = ['line-number']
classes.push 'foldable' if not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow)
classes.push 'folded' if @props.editor.isFoldedAtBufferRow(bufferRow)
classes = ''
if decorations?
for decoration in decorations
classes += decoration.class + ' ' if not softWrapped or softWrapped and decoration.softWrap
classes += 'line-number'
decorations = @props.editor.decorationsForBufferRow(bufferRow, 'gutter')
for decoration in decorations
classes.push(decoration.class) if not softWrapped or softWrapped and decoration.softWrap
"<div class=\"#{classes.join(' ')}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
"<div class=\"#{classes}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) ->
if softWrapped
@@ -160,18 +154,17 @@ GutterComponent = React.createClass
iconHTML = '<div class="icon-right"></div>'
padding + lineNumber + iconHTML
updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped) ->
updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped, decorations) ->
node = @lineNumberNodesById[lineNumberId]
previousDecorations = @previousDecorations[bufferRow]
@toggleClass node, 'foldable', not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow)
@toggleClass node, 'folded', @props.editor.isFoldedAtBufferRow(bufferRow)
if previousDecorations?
for decoration in previousDecorations
node.classList.remove(decoration.class) if not contains(decorations, decoration)
if @decoratorUpdates[bufferRow]?
for change in @decoratorUpdates[bufferRow]
if change.action == 'add' and (not softWrapped or softWrapped and change.decoration.softWrap)
node.classList.add(change.decoration.class)
else if change.action == 'remove'
node.classList.remove(change.decoration.class)
for decoration in decorations
if not contains(previousDecorations, decoration) and (not softWrapped or softWrapped and decoration.softWrap)
node.classList.add(decoration.class)
unless @screenRowsByLineNumberId[lineNumberId] is screenRow
{lineHeightInPixels} = @props
@@ -186,18 +179,9 @@ GutterComponent = React.createClass
lineNumberNodeForScreenRow: (screenRow) ->
@lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]]
toggleClass: (node, klass, condition) ->
if condition then node.classList.add(klass) else node.classList.remove(klass)
onDecorationChanged: (change) ->
if change.decoration.type is 'gutter'
@decoratorUpdates[change.bufferRow] ?= []
@decoratorUpdates[change.bufferRow].push change
@renderDecorations()
renderDecorations: ->
clearImmediate(@decorationRenderImmediate) if @decorationRenderImmediate
render = =>
@forceUpdate()
@decorationRenderImmediate = null
@decorationRenderImmediate = setImmediate(render)
# Created because underscore uses === not _.isEqual, which we need
contains = (array, target) ->
return false unless array?
for object in array
return true if _.isEqual(object, target)
false