Merge pull request #6776 from jssln/gutterPolish

Refactor `state.gutters` in TextEditorPresenter; pass minimal state to gutter components
This commit is contained in:
jssln 2015-05-14 15:43:39 -07:00
commit f6b15ee678
8 changed files with 779 additions and 689 deletions

View File

@ -30,15 +30,12 @@ describe "CustomGutterComponent", ->
buildTestState = (customDecorations) ->
mockTestState =
gutters:
content: if customDecorations then customDecorations else {}
styles:
scrollHeight: 100
scrollTop: 10
backgroundColor: 'black'
sortedDescriptions: [{gutter, visible: true}]
customDecorations: customDecorations
lineNumberGutter:
maxLineNumberDigits: 10
lineNumbers: {}
mockTestState
it "sets the custom-decoration wrapper's scrollHeight, scrollTop, and background color", ->
@ -53,7 +50,7 @@ describe "CustomGutterComponent", ->
expect(decorationsWrapperNode.style.backgroundColor).not.toBe ''
it "creates a new DOM node for a new decoration and adds it to the gutter at the right place", ->
customDecorations = 'test-gutter':
customDecorations =
'decoration-id-1':
top: 0
height: 10
@ -75,7 +72,7 @@ describe "CustomGutterComponent", ->
expect(decorationItem).toBe decorationItem1
it "updates the existing DOM node for a decoration that existed but has new properties", ->
initialCustomDecorations = 'test-gutter':
initialCustomDecorations =
'decoration-id-1':
top: 0
height: 10
@ -86,7 +83,7 @@ describe "CustomGutterComponent", ->
# Change the dimensions and item, remove the class.
decorationItem2 = document.createElement('div')
changedCustomDecorations = 'test-gutter':
changedCustomDecorations =
'decoration-id-1':
top: 10
height: 20
@ -103,7 +100,7 @@ describe "CustomGutterComponent", ->
expect(decorationItem).toBe decorationItem2
# Remove the item, add a class.
changedCustomDecorations = 'test-gutter':
changedCustomDecorations =
'decoration-id-1':
top: 10
height: 20
@ -118,7 +115,7 @@ describe "CustomGutterComponent", ->
expect(changedDecorationNode.children.length).toBe 0
it "removes any decorations that existed previously but aren't in the latest update", ->
customDecorations = 'test-gutter':
customDecorations =
'decoration-id-1':
top: 0
height: 10
@ -127,6 +124,6 @@ describe "CustomGutterComponent", ->
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
expect(decorationsWrapperNode.children.length).toBe 1
emptyCustomDecorations = 'test-gutter': {}
emptyCustomDecorations = {}
gutterComponent.updateSync(buildTestState(emptyCustomDecorations))
expect(decorationsWrapperNode.children.length).toBe 0

View File

@ -5,17 +5,20 @@ describe "GutterContainerComponent", ->
[gutterContainerComponent] = []
mockGutterContainer = {}
buildTestState = (sortedDescriptions) ->
mockTestState =
gutters:
scrollHeight: 100
scrollTop: 10
backgroundColor: 'black'
sortedDescriptions: sortedDescriptions
customDecorations: {}
lineNumberGutter:
maxLineNumberDigits: 10
lineNumbers: {}
buildTestState = (gutters) ->
styles =
scrollHeight: 100
scrollTop: 10
backgroundColor: 'black'
mockTestState = {gutters: []}
for gutter in gutters
if gutter.name is 'line-number'
content = {maxLineNumberDigits: 10, lineNumbers: {}}
else
content = {}
mockTestState.gutters.push({gutter, styles, content, visible: gutter.visible})
mockTestState
beforeEach ->
@ -30,7 +33,7 @@ describe "GutterContainerComponent", ->
describe "when updated with state that contains a new line-number gutter", ->
it "adds a LineNumberGutterComponent to its children", ->
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
testState = buildTestState([{gutter: lineNumberGutter, visible: true}])
testState = buildTestState([lineNumberGutter])
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
gutterContainerComponent.updateSync(testState)
@ -45,7 +48,7 @@ describe "GutterContainerComponent", ->
describe "when updated with state that contains a new custom gutter", ->
it "adds a CustomGutterComponent to its children", ->
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
testState = buildTestState([{gutter: customGutter, visible: true}])
testState = buildTestState([customGutter])
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
gutterContainerComponent.updateSync(testState)
@ -57,15 +60,16 @@ describe "GutterContainerComponent", ->
describe "when updated with state that contains a new gutter that is not visible", ->
it "creates the gutter view but hides it, and unhides it when it is later updated to be visible", ->
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
testState = buildTestState([{gutter: customGutter, visible: false}])
customGutter = new Gutter(mockGutterContainer, {name: 'custom', visible: false})
testState = buildTestState([customGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
expect(expectedCustomGutterNode.style.display).toBe 'none'
testState = buildTestState([{gutter: customGutter, visible: true}])
customGutter.show()
testState = buildTestState([customGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
@ -74,20 +78,20 @@ describe "GutterContainerComponent", ->
describe "when updated with a gutter that already exists", ->
it "reuses the existing gutter view, instead of recreating it", ->
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
testState = buildTestState([{gutter: customGutter, visible: true}])
testState = buildTestState([customGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
testState = buildTestState([{gutter: customGutter, visible: true}])
testState = buildTestState([customGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
expect(gutterContainerComponent.getDomNode().children.item(0)).toBe expectedCustomGutterNode
it "removes a gutter from the DOM if it does not appear in the latest state update", ->
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
testState = buildTestState([{gutter: lineNumberGutter, visible: true}])
testState = buildTestState([lineNumberGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
@ -99,7 +103,7 @@ describe "GutterContainerComponent", ->
it "positions (and repositions) the gutters to match the order they appear in each state update", ->
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
customGutter1 = new Gutter(mockGutterContainer, {name: 'custom', priority: -100})
testState = buildTestState([{gutter: customGutter1, visible: true}, {gutter: lineNumberGutter, visible: true}])
testState = buildTestState([customGutter1, lineNumberGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 2
@ -110,11 +114,7 @@ describe "GutterContainerComponent", ->
# Add a gutter.
customGutter2 = new Gutter(mockGutterContainer, {name: 'custom2', priority: -10})
testState = buildTestState([
{gutter: customGutter1, visible: true},
{gutter: customGutter2, visible: true},
{gutter: lineNumberGutter, visible: true}
])
testState = buildTestState([customGutter1, customGutter2, lineNumberGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0)
@ -125,12 +125,9 @@ describe "GutterContainerComponent", ->
expect(expectedLineNumbersNode).toBe atom.views.getView(lineNumberGutter)
# Hide one gutter, reposition one gutter, remove one gutter; and add a new gutter.
customGutter2.hide()
customGutter3 = new Gutter(mockGutterContainer, {name: 'custom3', priority: 100})
testState = buildTestState([
{gutter: customGutter2, visible: false},
{gutter: customGutter1, visible: true},
{gutter: customGutter3, visible: true}
])
testState = buildTestState([customGutter2, customGutter1, customGutter3])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(0)

File diff suppressed because it is too large Load Diff

View File

@ -29,13 +29,14 @@ class CustomGutterComponent
@domNode.style.removeProperty('display')
@visible = true
# `state` is a subset of the TextEditorPresenter state that is specific
# to this line number gutter.
updateSync: (state) ->
@oldDimensionsAndBackgroundState ?= {}
newDimensionsAndBackgroundState = state.gutters
setDimensionsAndBackground(@oldDimensionsAndBackgroundState, newDimensionsAndBackgroundState, @decorationsNode)
setDimensionsAndBackground(@oldDimensionsAndBackgroundState, state.styles, @decorationsNode)
@oldDecorationPositionState ?= {}
decorationState = state.gutters.customDecorations[@gutter.name]
decorationState = state.content
updatedDecorationIds = new Set
for decorationId, decorationInfo of decorationState

View File

@ -1,3 +1,4 @@
_ = require 'underscore-plus'
CustomGutterComponent = require './custom-gutter-component'
LineNumberGutterComponent = require './line-number-gutter-component'
@ -26,11 +27,11 @@ class GutterContainerComponent
updateSync: (state) ->
# The GutterContainerComponent expects the gutters to be sorted in the order
# they should appear.
newState = state.gutters.sortedDescriptions
newState = state.gutters
newGutterComponents = []
newGutterComponentsByGutterName = {}
for {gutter, visible} in newState
for {gutter, visible, styles, content} in newState
gutterComponent = @gutterComponentsByGutterName[gutter.name]
if not gutterComponent
if gutter.name is 'line-number'
@ -38,8 +39,20 @@ class GutterContainerComponent
@lineNumberGutterComponent = gutterComponent
else
gutterComponent = new CustomGutterComponent({gutter})
if visible then gutterComponent.showNode() else gutterComponent.hideNode()
gutterComponent.updateSync(state)
# Pass the gutter only the state that it needs.
if gutter.name is 'line-number'
# For ease of use in the line number gutter component, set the shared
# 'styles' as a field under the 'content'.
gutterSubstate = _.clone(content)
gutterSubstate.styles = styles
else
# Custom gutter 'content' is keyed on gutter name, so we cannot set
# 'styles' as a subfield directly under it.
gutterSubstate = {content, styles}
gutterComponent.updateSync(gutterSubstate)
newGutterComponents.push({
name: gutter.name,
component: gutterComponent,

View File

@ -31,19 +31,25 @@ class LineNumberGutterComponent
@domNode.style.removeProperty('display')
@visible = true
# `state` is a subset of the TextEditorPresenter state that is specific
# to this line number gutter.
updateSync: (state) ->
@newState = state.gutters.lineNumberGutter
@oldState ?= {lineNumbers: {}}
@newState = state
@oldState ?=
lineNumbers: {}
styles: {}
@appendDummyLineNumber() unless @dummyLineNumberNode?
newDimensionsAndBackgroundState = state.gutters
setDimensionsAndBackground(@oldState, newDimensionsAndBackgroundState, @lineNumbersNode)
setDimensionsAndBackground(@oldState.styles, @newState.styles, @lineNumbersNode)
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
@updateDummyLineNumber()
node.remove() for id, node of @lineNumberNodesById
@oldState = {maxLineNumberDigits: @newState.maxLineNumberDigits, lineNumbers: {}}
@oldState =
maxLineNumberDigits: @newState.maxLineNumberDigits
lineNumbers: {}
styles: {}
@lineNumberNodesById = {}
@updateLineNumbers()

View File

@ -70,7 +70,7 @@ class TextEditorComponent
@scrollViewNode.classList.add('scroll-view')
@domNode.appendChild(@scrollViewNode)
@mountGutterContainerComponent() if @presenter.getState().gutters.sortedDescriptions.length
@mountGutterContainerComponent() if @presenter.getState().gutters.length
@hiddenInputComponent = new InputComponent
@scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
@ -137,7 +137,7 @@ class TextEditorComponent
else
@domNode.style.height = ''
if @newState.gutters.sortedDescriptions.length
if @newState.gutters.length
@mountGutterContainerComponent() unless @gutterContainerComponent?
@gutterContainerComponent.updateSync(@newState)
else

View File

@ -208,11 +208,13 @@ class TextEditorPresenter
lines: {}
highlights: {}
overlays: {}
gutters:
sortedDescriptions: []
customDecorations: {}
lineNumberGutter:
lineNumbers: {}
gutters: []
# Shared state that is copied into ``@state.gutters`.
@sharedGutterStyles = {}
@customGutterDecorations = {}
@lineNumberGutter =
lineNumbers: {}
@updateState()
updateState: ->
@ -251,11 +253,11 @@ class TextEditorPresenter
updateVerticalScrollState: ->
@state.content.scrollHeight = @scrollHeight
@state.gutters.scrollHeight = @scrollHeight
@sharedGutterStyles.scrollHeight = @scrollHeight
@state.verticalScrollbar.scrollHeight = @scrollHeight
@state.content.scrollTop = @scrollTop
@state.gutters.scrollTop = @scrollTop
@sharedGutterStyles.scrollTop = @scrollTop
@state.verticalScrollbar.scrollTop = @scrollTop
updateHorizontalScrollState: ->
@ -410,10 +412,10 @@ class TextEditorPresenter
return
updateLineNumberGutterState: ->
@state.gutters.lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length
@lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length
updateCommonGutterState: ->
@state.gutters.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)"
@sharedGutterStyles.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)"
@gutterBackgroundColor
else
@backgroundColor
@ -441,15 +443,25 @@ class TextEditorPresenter
@emitDidUpdateState()
updateGutterOrderState: ->
@state.gutters.sortedDescriptions = []
@state.gutters = []
if @model.isMini()
return
for gutter in @model.getGutters()
isVisible = @gutterIsVisible(gutter)
@state.gutters.sortedDescriptions.push({gutter, visible: isVisible})
if gutter.name is 'line-number'
content = @lineNumberGutter
else
@customGutterDecorations[gutter.name] ?= {}
content = @customGutterDecorations[gutter.name]
@state.gutters.push({
gutter,
visible: isVisible,
styles: @sharedGutterStyles,
content,
})
# Updates the decoration state for the gutter with the given gutterName.
# @state.gutters.customDecorations is an {Object}, with the form:
# @customGutterDecorations is an {Object}, with the form:
# * gutterName : {
# decoration.id : {
# top: # of pixels from top
@ -461,23 +473,43 @@ class TextEditorPresenter
updateCustomGutterDecorationState: ->
return unless @startRow? and @endRow? and @lineHeight?
@state.gutters.customDecorations = {}
return if @model.isMini()
if @model.isMini()
# Mini editors have no gutter decorations.
# We clear instead of reassigning to preserve the reference.
@clearAllCustomGutterDecorations()
for gutter in @model.getGutters()
gutterName = gutter.name
@state.gutters.customDecorations[gutterName] = {}
gutterDecorations = @customGutterDecorations[gutterName]
if gutterDecorations
# Clear the gutter decorations; they are rebuilt.
# We clear instead of reassigning to preserve the reference.
@clearDecorationsForCustomGutterName(gutterName)
else
@customGutterDecorations[gutterName] = {}
return if not @gutterIsVisible(gutter)
relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1)
relevantDecorations.forEach (decoration) =>
decorationRange = decoration.getMarker().getScreenRange()
@state.gutters.customDecorations[gutterName][decoration.id] =
@customGutterDecorations[gutterName][decoration.id] =
top: @lineHeight * decorationRange.start.row
height: @lineHeight * decorationRange.getRowCount()
item: decoration.getProperties().item
class: decoration.getProperties().class
clearAllCustomGutterDecorations: ->
allGutterNames = Object.keys(@customGutterDecorations)
for gutterName in allGutterNames
@clearDecorationsForCustomGutterName(gutterName)
clearDecorationsForCustomGutterName: (gutterName) ->
gutterDecorations = @customGutterDecorations[gutterName]
if gutterDecorations
allDecorationIds = Object.keys(gutterDecorations)
for decorationId in allDecorationIds
delete gutterDecorations[decorationId]
gutterIsVisible: (gutterModel) ->
isVisible = gutterModel.isVisible()
if gutterModel.name is 'line-number'
@ -514,7 +546,7 @@ class TextEditorPresenter
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
foldable = @model.isFoldableAtScreenRow(screenRow)
@state.gutters.lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
@lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
visibleLineNumberIds[id] = true
if @mouseWheelScreenRow?
@ -524,8 +556,8 @@ class TextEditorPresenter
id += '-' + wrapCount if wrapCount > 0
visibleLineNumberIds[id] = true
for id of @state.gutters.lineNumberGutter.lineNumbers
delete @state.gutters.lineNumberGutter.lineNumbers[id] unless visibleLineNumberIds[id]
for id of @lineNumberGutter.lineNumbers
delete @lineNumberGutter.lineNumbers[id] unless visibleLineNumberIds[id]
return