Merge pull request #7101 from atom/as-tiled-gutter

Tiled Line Numbers
This commit is contained in:
Nathan Sobo
2015-06-30 11:54:13 -05:00
10 changed files with 761 additions and 683 deletions

View File

@@ -7,7 +7,7 @@ nbsp = String.fromCharCode(160)
describe "TextEditorComponent", ->
[contentNode, editor, wrapperView, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = []
[lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, tileSize] = []
[lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, tileSize, tileHeightInPixels] = []
beforeEach ->
tileSize = 3
@@ -45,6 +45,7 @@ describe "TextEditorComponent", ->
component.setFontSize(20)
lineHeightInPixels = editor.getLineHeightInPixels()
tileHeightInPixels = tileSize * lineHeightInPixels
charWidth = editor.getDefaultCharWidth()
componentNode = component.getDomNode()
verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar')
@@ -89,11 +90,10 @@ describe "TextEditorComponent", ->
it "renders the currently-visible lines in a tiled fashion", ->
wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
tileHeight = tileSize * lineHeightInPixels
component.measureDimensions()
nextAnimationFrame()
tilesNodes = componentNode.querySelectorAll(".tile")
tilesNodes = componentNode.querySelector(".lines").querySelectorAll(".tile")
expect(tilesNodes.length).toBe(3)
@@ -103,13 +103,13 @@ describe "TextEditorComponent", ->
expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels)
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)"
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)"
expect(tilesNodes[1].querySelectorAll(".line").length).toBe(tileSize)
expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels)
expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight}px, 0px)"
expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)"
expect(tilesNodes[2].querySelectorAll(".line").length).toBe(tileSize)
expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels)
@@ -121,24 +121,24 @@ describe "TextEditorComponent", ->
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
nextAnimationFrame()
tilesNodes = componentNode.querySelectorAll(".tile")
tilesNodes = componentNode.querySelector(".lines").querySelectorAll(".tile")
expect(component.lineNodeForScreenRow(2)).toBeUndefined()
expect(tilesNodes.length).toBe(3)
expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, -5px, 0px)"
expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{0 * tileHeightInPixels - 5}px, 0px)"
expect(tilesNodes[0].querySelectorAll(".line").length).toBe(tileSize)
expectTileContainsRow(tilesNodes[0], 3, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[0], 4, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[0], 5, top: 2 * lineHeightInPixels)
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight - 5}px, 0px)"
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels - 5}px, 0px)"
expect(tilesNodes[1].querySelectorAll(".line").length).toBe(tileSize)
expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels)
expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight - 5}px, 0px)"
expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels - 5}px, 0px)"
expect(tilesNodes[2].querySelectorAll(".line").length).toBe(tileSize)
expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels)
@@ -146,19 +146,18 @@ describe "TextEditorComponent", ->
it "updates the top position of subsequent tiles when lines are inserted or removed", ->
wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
tileHeight = tileSize * lineHeightInPixels
component.measureDimensions()
editor.getBuffer().deleteRows(0, 1)
nextAnimationFrame()
tilesNodes = componentNode.querySelectorAll(".tile")
tilesNodes = componentNode.querySelector(".lines").querySelectorAll(".tile")
expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)"
expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels)
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)"
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)"
expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels)
@@ -166,19 +165,19 @@ describe "TextEditorComponent", ->
editor.getBuffer().insert([0, 0], '\n\n')
nextAnimationFrame()
tilesNodes = componentNode.querySelectorAll(".tile")
tilesNodes = componentNode.querySelector(".lines").querySelectorAll(".tile")
expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)"
expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels)
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)"
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)"
expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels)
expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight}px, 0px)"
expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)"
expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels)
@@ -272,16 +271,22 @@ describe "TextEditorComponent", ->
atom.config.set("editor.showInvisibles", false)
expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp
it "gives the lines div the same background color as the editor to improve GPU performance", ->
it "gives the lines and tiles divs the same background color as the editor to improve GPU performance", ->
linesNode = componentNode.querySelector('.lines')
backgroundColor = getComputedStyle(wrapperNode).backgroundColor
expect(linesNode.style.backgroundColor).toBe backgroundColor
for tileNode in linesNode.querySelectorAll(".tile")
expect(tileNode.style.backgroundColor).toBe(backgroundColor)
wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)'
advanceClock(atom.views.documentPollingInterval)
nextAnimationFrame()
expect(linesNode.style.backgroundColor).toBe 'rgb(255, 0, 0)'
for tileNode in linesNode.querySelectorAll(".tile")
expect(tileNode.style.backgroundColor).toBe("rgb(255, 0, 0)")
it "applies .leading-whitespace for lines with leading spaces and/or tabs", ->
editor.setText(' a')
@@ -552,47 +557,90 @@ describe "TextEditorComponent", ->
expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy()
describe "gutter rendering", ->
it "renders the currently-visible line numbers", ->
expectTileContainsRow = (tileNode, screenRow, {top, text}) ->
lineNode = tileNode.querySelector("[data-screen-row='#{screenRow}']")
expect(lineNode.offsetTop).toBe(top)
expect(lineNode.textContent).toBe(text)
it "renders the currently-visible line numbers in a tiled fashion", ->
wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # visible line-numbers + dummy line number
expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1"
expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}6"
tilesNodes = componentNode.querySelector(".line-numbers").querySelectorAll(".tile")
verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels
expect(tilesNodes.length).toBe(3)
expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)"
expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe 3
expectTileContainsRow(tilesNodes[0], 0, top: lineHeightInPixels * 0, text: "#{nbsp}1")
expectTileContainsRow(tilesNodes[0], 1, top: lineHeightInPixels * 1, text: "#{nbsp}2")
expectTileContainsRow(tilesNodes[0], 2, top: lineHeightInPixels * 2, text: "#{nbsp}3")
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)"
expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe 3
expectTileContainsRow(tilesNodes[1], 3, top: lineHeightInPixels * 0, text: "#{nbsp}4")
expectTileContainsRow(tilesNodes[1], 4, top: lineHeightInPixels * 1, text: "#{nbsp}5")
expectTileContainsRow(tilesNodes[1], 5, top: lineHeightInPixels * 2, text: "#{nbsp}6")
expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)"
expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe 3
expectTileContainsRow(tilesNodes[2], 6, top: lineHeightInPixels * 0, text: "#{nbsp}7")
expectTileContainsRow(tilesNodes[2], 7, top: lineHeightInPixels * 1, text: "#{nbsp}8")
expectTileContainsRow(tilesNodes[2], 8, top: lineHeightInPixels * 2, text: "#{nbsp}9")
verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
nextAnimationFrame()
expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # visible line-numbers + dummy line number
tilesNodes = componentNode.querySelector(".line-numbers").querySelectorAll(".tile")
expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}3"
expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(7).textContent).toBe "#{nbsp}8"
expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe 7 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(2)).toBeUndefined()
expect(tilesNodes.length).toBe(3)
expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{0 * tileHeightInPixels - 5}px, 0px)"
expect(tilesNodes[0].querySelectorAll(".line-number").length).toBe(tileSize)
expectTileContainsRow(tilesNodes[0], 3, top: lineHeightInPixels * 0, text: "#{nbsp}4")
expectTileContainsRow(tilesNodes[0], 4, top: lineHeightInPixels * 1, text: "#{nbsp}5")
expectTileContainsRow(tilesNodes[0], 5, top: lineHeightInPixels * 2, text: "#{nbsp}6")
expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels - 5}px, 0px)"
expect(tilesNodes[1].querySelectorAll(".line-number").length).toBe(tileSize)
expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels, text: "#{nbsp}7")
expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels, text: "#{nbsp}8")
expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels, text: "#{nbsp}9")
expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels - 5}px, 0px)"
expect(tilesNodes[2].querySelectorAll(".line-number").length).toBe(tileSize)
expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels, text: "10")
expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels, text: "11")
expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels, text: "12")
it "updates the translation of subsequent line numbers when lines are inserted or removed", ->
editor.getBuffer().insert([0, 0], '\n\n')
nextAnimationFrame()
lineNumberNodes = componentNode.querySelectorAll('.line-number')
expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0
expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 0 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 2 * lineHeightInPixels
editor.getBuffer().insert([0, 0], '\n\n')
nextAnimationFrame()
expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0
expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe 6 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 0 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe 0 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(8).offsetTop).toBe 2 * lineHeightInPixels
it "renders • characters for soft-wrapped lines", ->
editor.setSoftWrapped(true)
@@ -601,13 +649,16 @@ describe "TextEditorComponent", ->
component.measureDimensions()
nextAnimationFrame()
expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # 1 dummy line
expect(componentNode.querySelectorAll('.line-number').length).toBe 9 + 1 # 3 line-numbers tiles + 1 dummy line
expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1"
expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}"
expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2"
expect(component.lineNumberNodeForScreenRow(3).textContent).toBe "#{nbsp}"
expect(component.lineNumberNodeForScreenRow(4).textContent).toBe "#{nbsp}3"
expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}"
expect(component.lineNumberNodeForScreenRow(6).textContent).toBe "#{nbsp}4"
expect(component.lineNumberNodeForScreenRow(7).textContent).toBe "#{nbsp}"
expect(component.lineNumberNodeForScreenRow(8).textContent).toBe "#{nbsp}"
it "pads line numbers to be right-justified based on the maximum number of line number digits", ->
editor.getBuffer().setText([1..10].join('\n'))
@@ -645,12 +696,16 @@ describe "TextEditorComponent", ->
lineNumbersNode = gutterNode.querySelector('.line-numbers')
{backgroundColor} = getComputedStyle(wrapperNode)
expect(lineNumbersNode.style.backgroundColor).toBe backgroundColor
for tileNode in lineNumbersNode.querySelectorAll(".tile")
expect(tileNode.style.backgroundColor).toBe(backgroundColor)
# favor gutter color if it's assigned
gutterNode.style.backgroundColor = 'rgb(255, 0, 0)'
advanceClock(atom.views.documentPollingInterval)
nextAnimationFrame()
expect(lineNumbersNode.style.backgroundColor).toBe 'rgb(255, 0, 0)'
for tileNode in lineNumbersNode.querySelectorAll(".tile")
expect(tileNode.style.backgroundColor).toBe("rgb(255, 0, 0)")
it "hides or shows the gutter based on the '::isLineNumberGutterVisible' property on the model and the global 'editor.showLineNumbers' config setting", ->
expect(component.gutterContainerComponent.getLineNumberGutterComponent()?).toBe true
@@ -978,7 +1033,7 @@ describe "TextEditorComponent", ->
it "renders 2 regions for 2-line selections", ->
editor.setSelectedScreenRange([[1, 6], [2, 10]])
nextAnimationFrame()
tileNode = componentNode.querySelectorAll(".tile")[0]
tileNode = componentNode.querySelector(".lines").querySelectorAll(".tile")[0]
regions = tileNode.querySelectorAll('.selection .region')
expect(regions.length).toBe 2
@@ -999,7 +1054,7 @@ describe "TextEditorComponent", ->
nextAnimationFrame()
# Tile 0
tileNode = componentNode.querySelectorAll(".tile")[0]
tileNode = componentNode.querySelector(".lines").querySelectorAll(".tile")[0]
regions = tileNode.querySelectorAll('.selection .region')
expect(regions.length).toBe(3)
@@ -1022,7 +1077,7 @@ describe "TextEditorComponent", ->
expect(region3Rect.right).toBe tileNode.getBoundingClientRect().right
# Tile 3
tileNode = componentNode.querySelectorAll(".tile")[1]
tileNode = componentNode.querySelector(".lines").querySelectorAll(".tile")[1]
regions = tileNode.querySelectorAll('.selection .region')
expect(regions.length).toBe(3)
@@ -2059,7 +2114,7 @@ describe "TextEditorComponent", ->
component.measureDimensions()
nextAnimationFrame()
tilesNodes = componentNode.querySelectorAll(".tile")
tilesNodes = componentNode.querySelector(".lines").querySelectorAll(".tile")
top = 0
for tileNode in tilesNodes

View File

@@ -58,6 +58,157 @@ describe "TextEditorPresenter", ->
expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn)
tiledContentContract = (stateFn) ->
it "contains states for tiles that are visible on screen", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expectValues stateFn(presenter).tiles[0], {
top: 0
}
expectValues stateFn(presenter).tiles[2], {
top: 2
}
expectValues stateFn(presenter).tiles[4], {
top: 4
}
expectValues stateFn(presenter).tiles[6], {
top: 6
}
expect(stateFn(presenter).tiles[8]).toBeUndefined()
expectStateUpdate presenter, -> presenter.setScrollTop(3)
expect(stateFn(presenter).tiles[0]).toBeUndefined()
expectValues stateFn(presenter).tiles[2], {
top: -1
}
expectValues stateFn(presenter).tiles[4], {
top: 1
}
expectValues stateFn(presenter).tiles[6], {
top: 3
}
expectValues stateFn(presenter).tiles[8], {
top: 5
}
expectValues stateFn(presenter).tiles[10], {
top: 7
}
expect(stateFn(presenter).tiles[12]).toBeUndefined()
it "includes state for all tiles if no external ::explicitHeight is assigned", ->
presenter = buildPresenter(explicitHeight: null, tileSize: 2)
expect(stateFn(presenter).tiles[0]).toBeDefined()
expect(stateFn(presenter).tiles[12]).toBeDefined()
it "is empty until all of the required measurements are assigned", ->
presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null)
expect(stateFn(presenter).tiles).toEqual({})
presenter.setExplicitHeight(25)
expect(stateFn(presenter).tiles).toEqual({})
presenter.setLineHeight(10)
expect(stateFn(presenter).tiles).toEqual({})
presenter.setScrollTop(0)
expect(stateFn(presenter).tiles).not.toEqual({})
it "updates when ::scrollTop changes", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expect(stateFn(presenter).tiles[0]).toBeDefined()
expect(stateFn(presenter).tiles[2]).toBeDefined()
expect(stateFn(presenter).tiles[4]).toBeDefined()
expect(stateFn(presenter).tiles[6]).toBeDefined()
expect(stateFn(presenter).tiles[8]).toBeUndefined()
expectStateUpdate presenter, -> presenter.setScrollTop(2)
expect(stateFn(presenter).tiles[0]).toBeUndefined()
expect(stateFn(presenter).tiles[2]).toBeDefined()
expect(stateFn(presenter).tiles[4]).toBeDefined()
expect(stateFn(presenter).tiles[6]).toBeDefined()
expect(stateFn(presenter).tiles[8]).toBeDefined()
expect(stateFn(presenter).tiles[10]).toBeUndefined()
it "updates when ::explicitHeight changes", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expect(stateFn(presenter).tiles[0]).toBeDefined()
expect(stateFn(presenter).tiles[2]).toBeDefined()
expect(stateFn(presenter).tiles[4]).toBeDefined()
expect(stateFn(presenter).tiles[6]).toBeDefined()
expect(stateFn(presenter).tiles[8]).toBeUndefined()
expectStateUpdate presenter, -> presenter.setExplicitHeight(8)
expect(stateFn(presenter).tiles[0]).toBeDefined()
expect(stateFn(presenter).tiles[2]).toBeDefined()
expect(stateFn(presenter).tiles[4]).toBeDefined()
expect(stateFn(presenter).tiles[6]).toBeDefined()
expect(stateFn(presenter).tiles[8]).toBeDefined()
expect(stateFn(presenter).tiles[10]).toBeUndefined()
it "updates when ::lineHeight changes", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expect(stateFn(presenter).tiles[0]).toBeDefined()
expect(stateFn(presenter).tiles[2]).toBeDefined()
expect(stateFn(presenter).tiles[4]).toBeDefined()
expect(stateFn(presenter).tiles[6]).toBeDefined()
expect(stateFn(presenter).tiles[8]).toBeUndefined()
expectStateUpdate presenter, -> presenter.setLineHeight(2)
expect(stateFn(presenter).tiles[0]).toBeDefined()
expect(stateFn(presenter).tiles[2]).toBeDefined()
expect(stateFn(presenter).tiles[4]).toBeDefined()
expect(stateFn(presenter).tiles[6]).toBeUndefined()
it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200)
expect(stateFn(presenter).tiles[0]).toBeDefined()
expect(stateFn(presenter).tiles[6]).toBeDefined()
expect(stateFn(presenter).tiles[8]).toBeUndefined()
presenter.setMouseWheelScreenRow(0)
expectStateUpdate presenter, -> presenter.setScrollTop(4)
expect(stateFn(presenter).tiles[0]).toBeDefined()
expect(stateFn(presenter).tiles[2]).toBeUndefined()
expect(stateFn(presenter).tiles[4]).toBeDefined()
expect(stateFn(presenter).tiles[12]).toBeUndefined()
expectStateUpdate presenter, -> advanceClock(200)
expect(stateFn(presenter).tiles[0]).toBeUndefined()
expect(stateFn(presenter).tiles[2]).toBeUndefined()
expect(stateFn(presenter).tiles[4]).toBeDefined()
expect(stateFn(presenter).tiles[12]).toBeUndefined()
# should clear ::mouseWheelScreenRow after stoppedScrollingDelay elapses even if we don't scroll first
presenter.setMouseWheelScreenRow(4)
advanceClock(200)
expectStateUpdate presenter, -> presenter.setScrollTop(6)
expect(stateFn(presenter).tiles[4]).toBeUndefined()
it "does not preserve deleted on-screen tiles even if they correspond to ::mouseWheelScreenRow", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200)
presenter.setMouseWheelScreenRow(2)
expectStateUpdate presenter, -> editor.setText("")
expect(stateFn(presenter).tiles[2]).toBeUndefined()
expect(stateFn(presenter).tiles[0]).toBeDefined()
describe "during state retrieval", ->
it "does not trigger onDidUpdateState events", ->
presenter = buildPresenter()
@@ -662,178 +813,7 @@ describe "TextEditorPresenter", ->
tileRow = presenter.tileForRow(row)
presenter.getState().content.tiles[tileRow]?.lines[lineId]
it "contains states for tiles that are visible on screen", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expectValues presenter.getState().content.tiles[0], {
top: 0
}
expectValues presenter.getState().content.tiles[2], {
top: 2
}
expectValues presenter.getState().content.tiles[4], {
top: 4
}
expectValues presenter.getState().content.tiles[6], {
top: 6
}
expect(presenter.getState().content.tiles[8]).toBeUndefined()
expectStateUpdate presenter, -> presenter.setScrollTop(3)
expect(presenter.getState().content.tiles[0]).toBeUndefined()
expectValues presenter.getState().content.tiles[2], {
top: -1
}
expectValues presenter.getState().content.tiles[4], {
top: 1
}
expectValues presenter.getState().content.tiles[6], {
top: 3
}
expectValues presenter.getState().content.tiles[8], {
top: 5
}
expectValues presenter.getState().content.tiles[10], {
top: 7
}
expect(presenter.getState().content.tiles[12]).toBeUndefined()
it "includes state for all tiles if no external ::explicitHeight is assigned", ->
presenter = buildPresenter(explicitHeight: null, tileSize: 2)
expect(presenter.getState().content.tiles[0]).toBeDefined()
expect(presenter.getState().content.tiles[12]).toBeDefined()
it "is empty until all of the required measurements are assigned", ->
presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null)
expect(presenter.getState().content.tiles).toEqual({})
presenter.setExplicitHeight(25)
expect(presenter.getState().content.tiles).toEqual({})
presenter.setLineHeight(10)
expect(presenter.getState().content.tiles).toEqual({})
presenter.setScrollTop(0)
expect(presenter.getState().content.tiles).not.toEqual({})
it "updates when ::scrollTop changes", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expect(presenter.getState().content.tiles[0]).toBeDefined()
expect(presenter.getState().content.tiles[2]).toBeDefined()
expect(presenter.getState().content.tiles[4]).toBeDefined()
expect(presenter.getState().content.tiles[6]).toBeDefined()
expect(presenter.getState().content.tiles[8]).toBeUndefined()
expectStateUpdate presenter, -> presenter.setScrollTop(2)
expect(presenter.getState().content.tiles[0]).toBeUndefined()
expect(presenter.getState().content.tiles[2]).toBeDefined()
expect(presenter.getState().content.tiles[4]).toBeDefined()
expect(presenter.getState().content.tiles[6]).toBeDefined()
expect(presenter.getState().content.tiles[8]).toBeDefined()
expect(presenter.getState().content.tiles[10]).toBeUndefined()
it "updates when ::explicitHeight changes", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expect(presenter.getState().content.tiles[0]).toBeDefined()
expect(presenter.getState().content.tiles[2]).toBeDefined()
expect(presenter.getState().content.tiles[4]).toBeDefined()
expect(presenter.getState().content.tiles[6]).toBeDefined()
expect(presenter.getState().content.tiles[8]).toBeUndefined()
expectStateUpdate presenter, -> presenter.setExplicitHeight(8)
expect(presenter.getState().content.tiles[0]).toBeDefined()
expect(presenter.getState().content.tiles[2]).toBeDefined()
expect(presenter.getState().content.tiles[4]).toBeDefined()
expect(presenter.getState().content.tiles[6]).toBeDefined()
expect(presenter.getState().content.tiles[8]).toBeDefined()
expect(presenter.getState().content.tiles[10]).toBeUndefined()
it "updates when ::lineHeight changes", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expect(presenter.getState().content.tiles[0]).toBeDefined()
expect(presenter.getState().content.tiles[2]).toBeDefined()
expect(presenter.getState().content.tiles[4]).toBeDefined()
expect(presenter.getState().content.tiles[6]).toBeDefined()
expect(presenter.getState().content.tiles[8]).toBeUndefined()
expectStateUpdate presenter, -> presenter.setLineHeight(2)
expect(presenter.getState().content.tiles[0]).toBeDefined()
expect(presenter.getState().content.tiles[2]).toBeDefined()
expect(presenter.getState().content.tiles[4]).toBeDefined()
expect(presenter.getState().content.tiles[6]).toBeUndefined()
it "updates when the editor's content changes", ->
presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10, tileSize: 2)
expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n")
line1 = editor.tokenizedLineForScreenRow(1)
expectValues lineStateForScreenRow(presenter, 1), {
text: line1.text
tags: line1.tags
}
line2 = editor.tokenizedLineForScreenRow(2)
expectValues lineStateForScreenRow(presenter, 2), {
text: line2.text
tags: line2.tags
}
line3 = editor.tokenizedLineForScreenRow(3)
expectValues lineStateForScreenRow(presenter, 3), {
text: line3.text
tags: line3.tags
}
it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200)
expect(presenter.getState().content.tiles[0]).toBeDefined()
expect(presenter.getState().content.tiles[6]).toBeDefined()
expect(presenter.getState().content.tiles[8]).toBeUndefined()
presenter.setMouseWheelScreenRow(0)
expectStateUpdate presenter, -> presenter.setScrollTop(4)
expect(presenter.getState().content.tiles[0]).toBeDefined()
expect(presenter.getState().content.tiles[2]).toBeUndefined()
expect(presenter.getState().content.tiles[4]).toBeDefined()
expect(presenter.getState().content.tiles[12]).toBeUndefined()
expectStateUpdate presenter, -> advanceClock(200)
expect(presenter.getState().content.tiles[0]).toBeUndefined()
expect(presenter.getState().content.tiles[2]).toBeUndefined()
expect(presenter.getState().content.tiles[4]).toBeDefined()
expect(presenter.getState().content.tiles[12]).toBeUndefined()
# should clear ::mouseWheelScreenRow after stoppedScrollingDelay elapses even if we don't scroll first
presenter.setMouseWheelScreenRow(4)
advanceClock(200)
expectStateUpdate presenter, -> presenter.setScrollTop(6)
expect(presenter.getState().content.tiles[4]).toBeUndefined()
it "does not preserve deleted on-screen tiles even if they correspond to ::mouseWheelScreenRow", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200)
presenter.setMouseWheelScreenRow(2)
expectStateUpdate presenter, -> editor.setText("")
expect(presenter.getState().content.tiles[2]).toBeUndefined()
expect(presenter.getState().content.tiles[0]).toBeDefined()
tiledContentContract (presenter) -> presenter.getState().content
describe "[tileId].lines[lineId]", -> # line state objects
it "includes the state for visible lines in a tile", ->
@@ -915,6 +895,29 @@ describe "TextEditorPresenter", ->
expect(lineStateForScreenRow(presenter, 9)).toBeUndefined()
it "updates when the editor's content changes", ->
presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10, tileSize: 2)
expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n")
line1 = editor.tokenizedLineForScreenRow(1)
expectValues lineStateForScreenRow(presenter, 1), {
text: line1.text
tags: line1.tags
}
line2 = editor.tokenizedLineForScreenRow(2)
expectValues lineStateForScreenRow(presenter, 2), {
text: line2.text
tags: line2.tags
}
line3 = editor.tokenizedLineForScreenRow(3)
expectValues lineStateForScreenRow(presenter, 3), {
text: line3.text
tags: line3.tags
}
it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", ->
editor.setText("hello\nworld\r\n")
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10)
@@ -2007,9 +2010,10 @@ describe "TextEditorPresenter", ->
editor.setText("1\n2\n3")
expect(getLineNumberGutterState(presenter).content.maxLineNumberDigits).toBe 1
describe ".content.lineNumbers", ->
describe ".content.tiles", ->
lineNumberStateForScreenRow = (presenter, screenRow) ->
editor = presenter.model
tileRow = presenter.tileForRow(screenRow)
bufferRow = editor.bufferRowForScreenRow(screenRow)
wrapCount = screenRow - editor.screenRowForBufferRow(bufferRow)
if wrapCount > 0
@@ -2017,291 +2021,224 @@ describe "TextEditorPresenter", ->
else
key = bufferRow
getLineNumberGutterState(presenter).content.lineNumbers[key]
gutterState = getLineNumberGutterState(presenter)
gutterState.content.tiles[tileRow]?.lineNumbers[key]
it "contains states for line numbers that are visible on screen", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10)
tiledContentContract (presenter) -> getLineNumberGutterState(presenter).content
expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false, top: 3 * 10}
expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true, top: 4 * 10}
expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false, top: 5 * 10}
expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false, top: 6 * 10}
expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined()
it "includes states for all line numbers if no ::explicitHeight is assigned", ->
presenter = buildPresenter(explicitHeight: null)
expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 12)).toBeDefined()
it "updates when ::scrollTop changes", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30)
expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7}
expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined()
expectStateUpdate presenter, -> presenter.setScrollTop(20)
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2}
expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4}
expect(lineNumberStateForScreenRow(presenter, 6)).toBeUndefined()
it "updates when ::explicitHeight changes", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30)
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7}
expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined()
expectStateUpdate presenter, -> presenter.setExplicitHeight(35)
expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8}
expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
it "updates when ::lineHeight changes", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0)
expectValues lineNumberStateForScreenRow(presenter, 0), {bufferRow: 0}
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
expect(lineNumberStateForScreenRow(presenter, 4)).toBeUndefined()
expectStateUpdate presenter, -> presenter.setLineHeight(5)
expectValues lineNumberStateForScreenRow(presenter, 0), {bufferRow: 0}
expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4}
expect(lineNumberStateForScreenRow(presenter, 6)).toBeUndefined()
it "updates when the editor's content changes", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 35, scrollTop: 30)
expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4}
expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7}
expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8}
expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
expectStateUpdate presenter, ->
editor.getBuffer().insert([3, Infinity], new Array(25).join("x "))
expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 4}
expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 7}
expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
it "does not remove out-of-view line numbers corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", ->
presenter = buildPresenter(explicitHeight: 25, stoppedScrollingDelay: 200)
expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 3)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 4)).toBeUndefined()
presenter.setMouseWheelScreenRow(0)
expectStateUpdate presenter, -> presenter.setScrollTop(35)
expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expect(lineNumberStateForScreenRow(presenter, 6)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined()
expectStateUpdate presenter, -> advanceClock(200)
expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined()
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expect(lineNumberStateForScreenRow(presenter, 6)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined()
it "correctly handles the first screen line being soft-wrapped", ->
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(30)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 50)
expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 3, softWrapped: true}
expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 3, softWrapped: true}
expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 4, softWrapped: false}
describe ".decorationClasses", ->
it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", ->
marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration1 = editor.decorateMarker(marker1, type: 'line-number', class: 'a')
presenter = buildPresenter()
marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration2 = editor.decorateMarker(marker2, type: 'line-number', class: 'b')
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x')
expect(marker1.isValid()).toBe false
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.undo()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]])
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> decoration1.destroy()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker2.destroy()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
it "honors the 'onlyEmpty' option on line-number decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 1]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyEmpty: true)
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker.clearTail()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
it "honors the 'onlyNonEmpty' option on line-number decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 2]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyNonEmpty: true)
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
expectStateUpdate presenter, -> marker.clearTail()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
it "honors the 'onlyHead' option on line-number decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 2]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyHead: true)
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
it "does not decorate the last line of a non-empty line-number decoration range if it ends at column 0", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 0]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a')
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
it "does not apply line-number decorations to mini editors", ->
editor.setMini(true)
presenter = buildPresenter()
marker = editor.markBufferRange([[0, 0], [0, 0]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a')
# A mini editor will have no gutters.
expect(getLineNumberGutterState(presenter)).toBeUndefined()
expectStateUpdate presenter, -> editor.setMini(false)
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'cursor-line-no-selection', 'a']
expectStateUpdate presenter, -> editor.setMini(true)
expect(getLineNumberGutterState(presenter)).toBeUndefined()
it "only applies line-number decorations to screen rows that are spanned by their marker when lines are soft-wrapped", ->
editor.setText("a line that wraps, ok")
describe ".lineNumbers[id]", ->
it "contains states for line numbers that are visible on screen", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(16)
marker = editor.markBufferRange([[0, 0], [0, 2]])
editor.decorateMarker(marker, type: 'line-number', class: 'a')
presenter = buildPresenter(explicitHeight: 10)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, tileSize: 2)
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false, top: 0 * 10}
expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false, top: 1 * 10}
expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true, top: 0 * 10}
expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false, top: 1 * 10}
expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false, top: 0 * 10}
expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false, top: 1 * 10}
expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
marker.setBufferRange([[0, 0], [0, Infinity]])
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
it "updates when the editor's content changes", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 35, scrollTop: 30, tileSize: 2)
describe ".foldable", ->
it "marks line numbers at the start of a foldable region as foldable", ->
presenter = buildPresenter()
expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2}
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4}
expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7}
expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8}
expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8}
expectValues lineNumberStateForScreenRow(presenter, 9), {bufferRow: 9}
expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined()
it "updates the foldable class on the correct line numbers when the foldable positions change", ->
presenter = buildPresenter()
editor.getBuffer().insert([0, 0], '\n')
expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 6).foldable).toBe false
expectStateUpdate presenter, ->
editor.getBuffer().insert([3, Infinity], new Array(25).join("x "))
it "updates the foldable class on a line number that becomes foldable", ->
presenter = buildPresenter()
expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2}
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 3}
expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 4}
expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 7}
expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8}
expectValues lineNumberStateForScreenRow(presenter, 9), {bufferRow: 8}
expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined()
editor.getBuffer().insert([11, 44], '\n fold me')
expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe true
it "correctly handles the first screen line being soft-wrapped", ->
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(30)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 50, tileSize: 2)
editor.undo()
expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false
expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 3, softWrapped: true}
expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 3, softWrapped: true}
expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 4, softWrapped: false}
describe ".decorationClasses", ->
it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", ->
marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration1 = editor.decorateMarker(marker1, type: 'line-number', class: 'a')
presenter = buildPresenter()
marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration2 = editor.decorateMarker(marker2, type: 'line-number', class: 'b')
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x')
expect(marker1.isValid()).toBe false
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.undo()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]])
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> decoration1.destroy()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker2.destroy()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
it "honors the 'onlyEmpty' option on line-number decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 1]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyEmpty: true)
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker.clearTail()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
it "honors the 'onlyNonEmpty' option on line-number decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 2]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyNonEmpty: true)
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
expectStateUpdate presenter, -> marker.clearTail()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
it "honors the 'onlyHead' option on line-number decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 2]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyHead: true)
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
it "does not decorate the last line of a non-empty line-number decoration range if it ends at column 0", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 0]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a')
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
it "does not apply line-number decorations to mini editors", ->
editor.setMini(true)
presenter = buildPresenter()
marker = editor.markBufferRange([[0, 0], [0, 0]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a')
# A mini editor will have no gutters.
expect(getLineNumberGutterState(presenter)).toBeUndefined()
expectStateUpdate presenter, -> editor.setMini(false)
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'cursor-line-no-selection', 'a']
expectStateUpdate presenter, -> editor.setMini(true)
expect(getLineNumberGutterState(presenter)).toBeUndefined()
it "only applies line-number decorations to screen rows that are spanned by their marker when lines are soft-wrapped", ->
editor.setText("a line that wraps, ok")
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(16)
marker = editor.markBufferRange([[0, 0], [0, 2]])
editor.decorateMarker(marker, type: 'line-number', class: 'a')
presenter = buildPresenter(explicitHeight: 10)
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
marker.setBufferRange([[0, 0], [0, Infinity]])
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
describe ".foldable", ->
it "marks line numbers at the start of a foldable region as foldable", ->
presenter = buildPresenter()
expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe false
it "updates the foldable class on the correct line numbers when the foldable positions change", ->
presenter = buildPresenter()
editor.getBuffer().insert([0, 0], '\n')
expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe false
expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe true
expect(lineNumberStateForScreenRow(presenter, 6).foldable).toBe false
it "updates the foldable class on a line number that becomes foldable", ->
presenter = buildPresenter()
expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false
editor.getBuffer().insert([11, 44], '\n fold me')
expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe true
editor.undo()
expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false
describe "for a gutter description that corresponds to a custom gutter", ->
describe ".content", ->

View File

@@ -7,7 +7,6 @@ LineNumberGutterComponent = require './line-number-gutter-component'
module.exports =
class GutterContainerComponent
constructor: ({@onLineNumberGutterMouseDown, @editor}) ->
# An array of objects of the form: {name: {String}, component: {Object}}
@gutterComponents = []

View File

@@ -1,14 +1,13 @@
_ = require 'underscore-plus'
{setDimensionsAndBackground} = require './gutter-component-helpers'
TiledComponent = require './tiled-component'
LineNumbersTileComponent = require './line-numbers-tile-component'
WrapperDiv = document.createElement('div')
DummyLineNumberComponent = LineNumbersTileComponent.createDummy()
module.exports =
class LineNumberGutterComponent
class LineNumberGutterComponent extends TiledComponent
dummyLineNumberNode: null
constructor: ({@onMouseDown, @editor, @gutter}) ->
@lineNumberNodesById = {}
@visible = true
@domNode = atom.views.getView(@gutter)
@@ -35,28 +34,33 @@ 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
@oldState ?=
lineNumbers: {}
buildEmptyState: ->
{
tiles: {}
styles: {}
}
getNewState: (state) -> state
getTilesNode: -> @lineNumbersNode
beforeUpdateSync: (state) ->
@appendDummyLineNumber() unless @dummyLineNumberNode?
setDimensionsAndBackground(@oldState.styles, @newState.styles, @lineNumbersNode)
if @newState.styles.scrollHeight isnt @oldState.styles.scrollHeight
@lineNumbersNode.style.height = @newState.styles.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
@lineNumbersNode.style.backgroundColor = @newState.styles.backgroundColor
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
@updateDummyLineNumber()
node.remove() for id, node of @lineNumberNodesById
@oldState =
maxLineNumberDigits: @newState.maxLineNumberDigits
lineNumbers: {}
styles: {}
@lineNumberNodesById = {}
@oldState.styles = {}
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
@updateLineNumbers()
buildComponentForTile: (id) -> new LineNumbersTileComponent({id})
###
Section: Private Methods
@@ -65,94 +69,14 @@ class LineNumberGutterComponent
# This dummy line number element holds the gutter to the appropriate width,
# since the real line numbers are absolutely positioned for performance reasons.
appendDummyLineNumber: ->
WrapperDiv.innerHTML = @buildLineNumberHTML({bufferRow: -1})
DummyLineNumberComponent.newState = @newState
WrapperDiv.innerHTML = DummyLineNumberComponent.buildLineNumberHTML({bufferRow: -1})
@dummyLineNumberNode = WrapperDiv.children[0]
@lineNumbersNode.appendChild(@dummyLineNumberNode)
updateDummyLineNumber: ->
@dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false)
updateLineNumbers: ->
newLineNumberIds = null
newLineNumbersHTML = null
for id, lineNumberState of @newState.lineNumbers
if @oldState.lineNumbers.hasOwnProperty(id)
@updateLineNumberNode(id, lineNumberState)
else
newLineNumberIds ?= []
newLineNumbersHTML ?= ""
newLineNumberIds.push(id)
newLineNumbersHTML += @buildLineNumberHTML(lineNumberState)
@oldState.lineNumbers[id] = _.clone(lineNumberState)
if newLineNumberIds?
WrapperDiv.innerHTML = newLineNumbersHTML
newLineNumberNodes = _.toArray(WrapperDiv.children)
node = @lineNumbersNode
for id, i in newLineNumberIds
lineNumberNode = newLineNumberNodes[i]
@lineNumberNodesById[id] = lineNumberNode
node.appendChild(lineNumberNode)
for id, lineNumberState of @oldState.lineNumbers
unless @newState.lineNumbers.hasOwnProperty(id)
@lineNumberNodesById[id].remove()
delete @lineNumberNodesById[id]
delete @oldState.lineNumbers[id]
return
buildLineNumberHTML: (lineNumberState) ->
{screenRow, bufferRow, softWrapped, top, decorationClasses} = lineNumberState
if screenRow?
style = "position: absolute; top: #{top}px;"
else
style = "visibility: hidden;"
className = @buildLineNumberClassName(lineNumberState)
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped)
"<div class=\"#{className}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
buildLineNumberInnerHTML: (bufferRow, softWrapped) ->
{maxLineNumberDigits} = @newState
if softWrapped
lineNumber = ""
else
lineNumber = (bufferRow + 1).toString()
padding = _.multiplyString('&nbsp;', maxLineNumberDigits - lineNumber.length)
iconHTML = '<div class="icon-right"></div>'
padding + lineNumber + iconHTML
updateLineNumberNode: (lineNumberId, newLineNumberState) ->
oldLineNumberState = @oldState.lineNumbers[lineNumberId]
node = @lineNumberNodesById[lineNumberId]
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)
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
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) ->
className = "line-number line-number-#{bufferRow}"
className += " " + decorationClasses.join(' ') if decorationClasses?
className += " foldable" if foldable and not softWrapped
className
lineNumberNodeForScreenRow: (screenRow) ->
for id, lineNumberState of @oldState.lineNumbers
if lineNumberState.screenRow is screenRow
return @lineNumberNodesById[id]
null
DummyLineNumberComponent.newState = @newState
@dummyLineNumberNode.innerHTML = DummyLineNumberComponent.buildLineNumberInnerHTML(0, false)
onMouseDown: (event) =>
{target} = event

View File

@@ -0,0 +1,134 @@
_ = require 'underscore-plus'
WrapperDiv = document.createElement('div')
module.exports =
class LineNumbersTileComponent
@createDummy: ->
new LineNumbersTileComponent({id: -1})
constructor: ({@id}) ->
@lineNumberNodesById = {}
@domNode = document.createElement("div")
@domNode.classList.add("tile")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber
getDomNode: ->
@domNode
updateSync: (state) ->
@newState = state
unless @oldState
@oldState = {tiles: {}, styles: {}}
@oldState.tiles[@id] = {lineNumbers: {}}
@newTileState = @newState.tiles[@id]
@oldTileState = @oldState.tiles[@id]
if @newTileState.display isnt @oldTileState.display
@domNode.style.display = @newTileState.display
@oldTileState.display = @newTileState.display
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
@domNode.style.backgroundColor = @newState.styles.backgroundColor
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
if @newTileState.height isnt @oldTileState.height
@domNode.style.height = @newTileState.height + 'px'
@oldTileState.height = @newTileState.height
if @newTileState.top isnt @oldTileState.top
@domNode.style['-webkit-transform'] = "translate3d(0, #{@newTileState.top}px, 0px)"
@oldTileState.top = @newTileState.top
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
node.remove() for id, node of @lineNumberNodesById
@oldState.tiles[@id] = {lineNumbers: {}}
@oldTileState = @oldState.tiles[@id]
@lineNumberNodesById = {}
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
@updateLineNumbers()
updateLineNumbers: ->
newLineNumberIds = null
newLineNumbersHTML = null
for id, lineNumberState of @oldTileState.lineNumbers
unless @newTileState.lineNumbers.hasOwnProperty(id)
@lineNumberNodesById[id].remove()
delete @lineNumberNodesById[id]
delete @oldTileState.lineNumbers[id]
for id, lineNumberState of @newTileState.lineNumbers
if @oldTileState.lineNumbers.hasOwnProperty(id)
@updateLineNumberNode(id, lineNumberState)
else
newLineNumberIds ?= []
newLineNumbersHTML ?= ""
newLineNumberIds.push(id)
newLineNumbersHTML += @buildLineNumberHTML(lineNumberState)
@oldTileState.lineNumbers[id] = _.clone(lineNumberState)
if newLineNumberIds?
WrapperDiv.innerHTML = newLineNumbersHTML
newLineNumberNodes = _.toArray(WrapperDiv.children)
node = @domNode
for id, i in newLineNumberIds
lineNumberNode = newLineNumberNodes[i]
@lineNumberNodesById[id] = lineNumberNode
node.appendChild(lineNumberNode)
return
buildLineNumberHTML: (lineNumberState) ->
{screenRow, bufferRow, softWrapped, top, decorationClasses} = lineNumberState
if screenRow?
style = "position: absolute; top: #{top}px;"
else
style = "visibility: hidden;"
className = @buildLineNumberClassName(lineNumberState)
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped)
"<div class=\"#{className}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
buildLineNumberInnerHTML: (bufferRow, softWrapped) ->
{maxLineNumberDigits} = @newState
if softWrapped
lineNumber = ""
else
lineNumber = (bufferRow + 1).toString()
padding = _.multiplyString('&nbsp;', maxLineNumberDigits - lineNumber.length)
iconHTML = '<div class="icon-right"></div>'
padding + lineNumber + iconHTML
updateLineNumberNode: (lineNumberId, newLineNumberState) ->
oldLineNumberState = @oldTileState.lineNumbers[lineNumberId]
node = @lineNumberNodesById[lineNumberId]
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)
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
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) ->
className = "line-number line-number-#{bufferRow}"
className += " " + decorationClasses.join(' ') if decorationClasses?
className += " foldable" if foldable and not softWrapped
className
lineNumberNodeForScreenRow: (screenRow) ->
for id, lineNumberState of @oldTileState.lineNumbers
if lineNumberState.screenRow is screenRow
return @lineNumberNodesById[id]
null

View File

@@ -1,26 +1,20 @@
{$$} = require 'space-pen'
CursorsComponent = require './cursors-component'
TileComponent = require './tile-component'
LinesTileComponent = require './lines-tile-component'
TiledComponent = require './tiled-component'
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
clone
module.exports =
class LinesComponent
class LinesComponent extends TiledComponent
placeholderTextDiv: null
constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) ->
@tileComponentsByTileId = {}
@domNode = document.createElement('div')
@domNode.classList.add('lines')
@cursorsComponent = new CursorsComponent(@presenter)
@cursorsComponent = new CursorsComponent
@domNode.appendChild(@cursorsComponent.getDomNode())
if @useShadowDOM
@@ -31,10 +25,10 @@ class LinesComponent
getDomNode: ->
@domNode
updateSync: (state) ->
@newState = state.content
@oldState ?= {tiles: {}}
shouldRecreateAllTilesOnUpdate: ->
@oldState.indentGuidesVisible isnt @newState.indentGuidesVisible
beforeUpdateSync: (state) ->
if @newState.scrollHeight isnt @oldState.scrollHeight
@domNode.style.height = @newState.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
@@ -43,6 +37,7 @@ class LinesComponent
@domNode.style.backgroundColor = @newState.backgroundColor
@oldState.backgroundColor = @newState.backgroundColor
afterUpdateSync: (state) ->
if @newState.placeholderText isnt @oldState.placeholderText
@placeholderTextDiv?.remove()
if @newState.placeholderText?
@@ -51,46 +46,23 @@ class LinesComponent
@placeholderTextDiv.textContent = @newState.placeholderText
@domNode.appendChild(@placeholderTextDiv)
@removeTileNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateTileNodes()
if @newState.width isnt @oldState.width
@domNode.style.width = @newState.width + 'px'
@oldState.width = @newState.width
@cursorsComponent.updateSync(state)
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
@oldState.scrollWidth = @newState.scrollWidth
@oldState.width = @newState.width
removeTileNodes: ->
@removeTileNode(id) for id of @oldState.tiles
return
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter})
removeTileNode: (id) ->
node = @tileComponentsByTileId[id].getDomNode()
buildEmptyState: ->
{tiles: {}}
node.remove()
delete @tileComponentsByTileId[id]
delete @oldState.tiles[id]
getNewState: (state) ->
state.content
updateTileNodes: ->
for id of @oldState.tiles
unless @newState.tiles.hasOwnProperty(id)
@removeTileNode(id)
for id, tileState of @newState.tiles
if @oldState.tiles.hasOwnProperty(id)
tileComponent = @tileComponentsByTileId[id]
else
tileComponent = @tileComponentsByTileId[id] = new TileComponent({id, @presenter})
@domNode.appendChild(tileComponent.getDomNode())
@oldState.tiles[id] = cloneObject(tileState)
tileComponent.updateSync(@newState)
return
getTilesNode: -> @domNode
measureLineHeightAndDefaultCharWidth: ->
@domNode.appendChild(DummyLineNode)
@@ -109,18 +81,13 @@ class LinesComponent
measureCharactersInNewLines: ->
@presenter.batchCharacterMeasurement =>
for id, component of @tileComponentsByTileId
for id, component of @componentsByTileId
component.measureCharactersInNewLines()
return
clearScopedCharWidths: ->
for id, component of @tileComponentsByTileId
for id, component of @componentsByTileId
component.clearMeasurements()
@presenter.clearScopedCharacterWidths()
lineNodeForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
@tileComponentsByTileId[tile]?.lineNodeForScreenRow(screenRow)

View File

@@ -13,7 +13,7 @@ cloneObject = (object) ->
clone
module.exports =
class TileComponent
class LinesTileComponent
constructor: ({@presenter, @id}) ->
@tokenIterator = new TokenIterator
@measuredLines = new Set
@@ -25,7 +25,7 @@ class TileComponent
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@highlightsComponent = new HighlightsComponent(@presenter)
@highlightsComponent = new HighlightsComponent
@domNode.appendChild(@highlightsComponent.getDomNode())
getDomNode: ->
@@ -54,6 +54,7 @@ class TileComponent
if @newState.width isnt @oldState.width
@domNode.style.width = @newState.width + 'px'
@oldTileState.width = @newTileState.width
if @newTileState.top isnt @oldTileState.top or @newTileState.left isnt @oldTileState.left
@domNode.style['-webkit-transform'] = "translate3d(#{@newTileState.left}px, #{@newTileState.top}px, 0px)"

View File

@@ -729,9 +729,18 @@ class TextEditorComponent
consolidateSelections: (e) ->
e.abortKeyBinding() unless @editor.consolidateSelections()
lineNodeForScreenRow: (screenRow) -> @linesComponent.lineNodeForScreenRow(screenRow)
lineNodeForScreenRow: (screenRow) ->
tileRow = @presenter.tileForRow(screenRow)
tileComponent = @linesComponent.getComponentForTile(tileRow)
lineNumberNodeForScreenRow: (screenRow) -> @gutterContainerComponent.getLineNumberGutterComponent().lineNumberNodeForScreenRow(screenRow)
tileComponent?.lineNodeForScreenRow(screenRow)
lineNumberNodeForScreenRow: (screenRow) ->
tileRow = @presenter.tileForRow(screenRow)
gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
tileComponent = gutterComponent.getComponentForTile(tileRow)
tileComponent?.lineNumberNodeForScreenRow(screenRow)
screenRowForNode: (node) ->
while node?

View File

@@ -80,11 +80,10 @@ class TextEditorPresenter
@updateHiddenInputState() if @shouldUpdateHiddenInputState
@updateContentState() if @shouldUpdateContentState
@updateDecorations() if @shouldUpdateDecorations
@updateTilesState() if @shouldUpdateTilesState
@updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState
@updateCursorsState() if @shouldUpdateCursorsState
@updateOverlaysState() if @shouldUpdateOverlaysState
@updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState
@updateLineNumbersState() if @shouldUpdateLineNumbersState
@updateGutterOrderState() if @shouldUpdateGutterOrderState
@updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState
@updating = false
@@ -102,7 +101,7 @@ class TextEditorPresenter
@shouldUpdateHiddenInputState = false
@shouldUpdateContentState = false
@shouldUpdateDecorations = false
@shouldUpdateTilesState = false
@shouldUpdateLinesState = false
@shouldUpdateCursorsState = false
@shouldUpdateOverlaysState = false
@shouldUpdateLineNumberGutterState = false
@@ -121,7 +120,7 @@ class TextEditorPresenter
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateCursorsState = true
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@@ -129,7 +128,7 @@ class TextEditorPresenter
@emitDidUpdateState()
@model.onDidUpdateMarkers =>
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateDecorations = true
@shouldUpdateOverlaysState = true
@@ -145,7 +144,7 @@ class TextEditorPresenter
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@@ -228,11 +227,14 @@ class TextEditorPresenter
@sharedGutterStyles = {}
@customGutterDecorations = {}
@lineNumberGutter =
lineNumbers: {}
tiles: {}
@updateState()
updateState: ->
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@updateContentDimensions()
@updateScrollbarDimensions()
@updateStartRow()
@@ -250,7 +252,6 @@ class TextEditorPresenter
@updateCursorsState()
@updateOverlaysState()
@updateLineNumberGutterState()
@updateLineNumbersState()
@updateCommonGutterState()
@updateGutterOrderState()
@updateCustomGutterDecorationState()
@@ -339,7 +340,13 @@ class TextEditorPresenter
tile.display = "block"
tile.highlights ?= {}
@updateLinesState(tile, startRow, endRow)
gutterTile = @lineNumberGutter.tiles[startRow] ?= {}
gutterTile.top = startRow * @lineHeight - @scrollTop
gutterTile.height = @tileSize * @lineHeight
gutterTile.display = "block"
@updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState
@updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState
visibleTiles[startRow] = true
@@ -347,6 +354,7 @@ class TextEditorPresenter
mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
unless visibleTiles[mouseWheelTile]?
@lineNumberGutter.tiles[mouseWheelTile].display = "none"
@state.content.tiles[mouseWheelTile].display = "none"
visibleTiles[mouseWheelTile] = true
@@ -354,6 +362,7 @@ class TextEditorPresenter
continue if visibleTiles.hasOwnProperty(id)
delete @state.content.tiles[id]
delete @lineNumberGutter.tiles[id]
updateLinesState: (tileState, startRow, endRow) ->
tileState.lines ?= {}
@@ -562,21 +571,20 @@ class TextEditorPresenter
isVisible = isVisible and @showLineNumbers
isVisible
updateLineNumbersState: ->
return unless @startRow? and @endRow? and @lineHeight?
updateLineNumbersState: (tileState, startRow, endRow) ->
tileState.lineNumbers ?= {}
visibleLineNumberIds = {}
if @startRow > 0
rowBeforeStartRow = @startRow - 1
if startRow > 0
rowBeforeStartRow = startRow - 1
lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow)
wrapCount = rowBeforeStartRow - @model.screenRowForBufferRow(lastBufferRow)
else
lastBufferRow = null
wrapCount = 0
if @endRow > @startRow
for bufferRow, i in @model.bufferRowsForScreenRows(@startRow, @endRow - 1)
if endRow > startRow
for bufferRow, i in @model.bufferRowsForScreenRows(startRow, endRow - 1)
if bufferRow is lastBufferRow
wrapCount++
id = bufferRow + '-' + wrapCount
@@ -587,23 +595,16 @@ class TextEditorPresenter
lastBufferRow = bufferRow
softWrapped = false
screenRow = @startRow + i
top = screenRow * @lineHeight
screenRow = startRow + i
top = (screenRow - startRow) * @lineHeight
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
foldable = @model.isFoldableAtScreenRow(screenRow)
@lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
tileState.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 @lineNumberGutter.lineNumbers
delete @lineNumberGutter.lineNumbers[id] unless visibleLineNumberIds[id]
for id of tileState.lineNumbers
delete tileState.lineNumbers[id] unless visibleLineNumberIds[id]
return
@@ -797,7 +798,7 @@ class TextEditorPresenter
@shouldUpdateVerticalScrollState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateDecorations = true
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@@ -820,7 +821,7 @@ class TextEditorPresenter
@state.content.scrollingVertically = false
if @mouseWheelScreenRow?
@mouseWheelScreenRow = null
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@@ -837,7 +838,7 @@ class TextEditorPresenter
@shouldUpdateCursorsState = true
@shouldUpdateOverlaysState = true
@shouldUpdateDecorations = true
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@emitDidUpdateState()
@@ -885,7 +886,7 @@ class TextEditorPresenter
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateDecorations = true
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@@ -913,7 +914,7 @@ class TextEditorPresenter
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true unless oldContentFrameWidth?
@emitDidUpdateState()
@@ -979,7 +980,7 @@ class TextEditorPresenter
@shouldUpdateScrollbarsState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateDecorations = true
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@@ -1031,7 +1032,7 @@ class TextEditorPresenter
@shouldUpdateHiddenInputState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateOverlaysState = true
@@ -1119,7 +1120,7 @@ class TextEditorPresenter
@shouldUpdateDecorations = true
if decoration.isType('line') or decoration.isType('gutter')
if decoration.isType('line') or Decoration.isType(oldProperties, 'line')
@shouldUpdateTilesState = true
@shouldUpdateLinesState = true
if decoration.isType('line-number') or Decoration.isType(oldProperties, 'line-number')
@shouldUpdateLineNumbersState = true
if (decoration.isType('gutter') and not decoration.isType('line-number')) or
@@ -1132,7 +1133,7 @@ class TextEditorPresenter
didDestroyDecoration: (decoration) ->
@shouldUpdateDecorations = true
if decoration.isType('line') or decoration.isType('gutter')
@shouldUpdateTilesState = true if decoration.isType('line')
@shouldUpdateLinesState = true if decoration.isType('line')
if decoration.isType('line-number')
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')
@@ -1147,7 +1148,7 @@ class TextEditorPresenter
if decoration.isType('line') or decoration.isType('gutter')
@shouldUpdateDecorations = true
@shouldUpdateTilesState = true if decoration.isType('line')
@shouldUpdateLinesState = true if decoration.isType('line')
if decoration.isType('line-number')
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')

View File

@@ -0,0 +1,51 @@
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
clone
module.exports =
class TiledComponent
updateSync: (state) ->
@newState = @getNewState(state)
@oldState ?= @buildEmptyState()
@beforeUpdateSync?(state)
@removeTileNodes() if @shouldRecreateAllTilesOnUpdate?()
@updateTileNodes()
@afterUpdateSync?(state)
removeTileNodes: ->
@removeTileNode(tileRow) for tileRow of @oldState.tiles
return
removeTileNode: (tileRow) ->
node = @componentsByTileId[tileRow].getDomNode()
node.remove()
delete @componentsByTileId[tileRow]
delete @oldState.tiles[tileRow]
updateTileNodes: ->
@componentsByTileId ?= {}
for tileRow of @oldState.tiles
unless @newState.tiles.hasOwnProperty(tileRow)
@removeTileNode(tileRow)
for tileRow, tileState of @newState.tiles
if @oldState.tiles.hasOwnProperty(tileRow)
component = @componentsByTileId[tileRow]
else
component = @componentsByTileId[tileRow] = @buildComponentForTile(tileRow)
@getTilesNode().appendChild(component.getDomNode())
@oldState.tiles[tileRow] = cloneObject(tileState)
component.updateSync(@newState)
return
getComponentForTile: (tileRow) ->
@componentsByTileId[tileRow]