Merge pull request #6733 from atom/as-tiled-rendering

Render lines via tiles
This commit is contained in:
Nathan Sobo
2015-06-03 00:18:41 +02:00
8 changed files with 878 additions and 634 deletions

View File

@@ -7,10 +7,10 @@ nbsp = String.fromCharCode(160)
describe "TextEditorComponent", ->
[contentNode, editor, wrapperView, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = []
[lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, lineOverdrawMargin] = []
[lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, tileSize] = []
beforeEach ->
lineOverdrawMargin = 2
tileSize = 3
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
@@ -34,7 +34,7 @@ describe "TextEditorComponent", ->
contentNode = document.querySelector('#jasmine-content')
contentNode.style.width = '1000px'
wrapperView = new TextEditorView(editor, {lineOverdrawMargin})
wrapperView = new TextEditorView(editor, {tileSize})
wrapperView.attachToDom()
wrapperNode = wrapperView.element
wrapperNode.setUpdatedSynchronously(false)
@@ -68,48 +68,111 @@ describe "TextEditorComponent", ->
expect(nextAnimationFrame).not.toThrow()
describe "line rendering", ->
it "renders the currently-visible lines plus the overdraw margin", ->
wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
expectTileContainsRow = (tileNode, screenRow, {top}) ->
lineNode = tileNode.querySelector("[data-screen-row='#{screenRow}']")
tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
expect(lineNode.offsetTop).toBe(top)
if tokenizedLine.text is ""
expect(lineNode.innerHTML).toBe(" ")
else
expect(lineNode.textContent).toBe(tokenizedLine.text)
it "renders the currently-visible lines in a tiled fashion", ->
wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
tileHeight = tileSize * lineHeightInPixels
component.measureDimensions()
nextAnimationFrame()
linesNode = componentNode.querySelector('.lines')
expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)"
expect(componentNode.querySelectorAll('.line').length).toBe 6 + 2 # no margin above
expect(component.lineNodeForScreenRow(0).textContent).toBe editor.tokenizedLineForScreenRow(0).text
expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0
expect(component.lineNodeForScreenRow(5).textContent).toBe editor.tokenizedLineForScreenRow(5).text
expect(component.lineNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels
tilesNodes = componentNode.querySelectorAll(".tile")
verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels
expect(tilesNodes.length).toBe(3)
expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)"
expect(tilesNodes[0].children.length).toBe(tileSize)
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].children.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].children.length).toBe(tileSize)
expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels)
expect(component.lineNodeForScreenRow(9)).toBeUndefined()
verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
nextAnimationFrame()
expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, #{-4.5 * lineHeightInPixels}px, 0px)"
expect(componentNode.querySelectorAll('.line').length).toBe 6 + 4 # margin above and below
expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNodeForScreenRow(2).textContent).toBe editor.tokenizedLineForScreenRow(2).text
expect(component.lineNodeForScreenRow(9).offsetTop).toBe 9 * lineHeightInPixels
expect(component.lineNodeForScreenRow(9).textContent).toBe editor.tokenizedLineForScreenRow(9).text
tilesNodes = componentNode.querySelectorAll(".tile")
it "updates the top position of subsequent lines when lines are inserted or removed", ->
expect(component.lineNodeForScreenRow(2)).toBeUndefined()
expect(tilesNodes.length).toBe(3)
expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, -5px, 0px)"
expect(tilesNodes[0].children.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].children.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].children.length).toBe(tileSize)
expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels)
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()
lineNodes = componentNode.querySelectorAll('.line')
expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0
expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
tilesNodes = componentNode.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)"
expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels)
editor.getBuffer().insert([0, 0], '\n\n')
nextAnimationFrame()
lineNodes = componentNode.querySelectorAll('.line')
expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels
expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels
expect(component.lineNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels
tilesNodes = componentNode.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)"
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)"
expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels)
expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels)
it "updates the lines when lines are inserted or removed above the rendered row range", ->
wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
@@ -483,7 +546,7 @@ describe "TextEditorComponent", ->
component.measureDimensions()
nextAnimationFrame()
expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number
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"
@@ -491,7 +554,7 @@ describe "TextEditorComponent", ->
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
nextAnimationFrame()
expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 4 + 1 # line overdraw margin above/below + dummy line number
expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # visible line-numbers + dummy line number
expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}3"
expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
@@ -527,7 +590,7 @@ describe "TextEditorComponent", ->
component.measureDimensions()
nextAnimationFrame()
expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line componentNode
expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # 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"
@@ -725,13 +788,13 @@ describe "TextEditorComponent", ->
cursorNodes = componentNode.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 2
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{10 * charWidth}px, #{4 * lineHeightInPixels}px)"
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)"
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{10 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)"
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)"
editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener')
cursor3.setScreenPosition([4, 11], autoscroll: false)
nextAnimationFrame()
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{4 * lineHeightInPixels}px)"
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)"
expect(cursorMovedListener).toHaveBeenCalled()
cursor3.destroy()
@@ -739,7 +802,7 @@ describe "TextEditorComponent", ->
cursorNodes = componentNode.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 1
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)"
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)"
it "accounts for character widths when positioning cursors", ->
atom.config.set('editor.fontFamily', 'sans-serif')
@@ -1001,7 +1064,7 @@ describe "TextEditorComponent", ->
nextAnimationFrame()
# Scroll decorations into view
verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels
verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
nextAnimationFrame()
expect(lineAndLineNumberHaveClass(9, 'b')).toBe true
@@ -1147,15 +1210,16 @@ describe "TextEditorComponent", ->
# Nothing when outside the rendered row range
expect(regions.length).toBe 0
verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels
verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
nextAnimationFrame()
expect(component.presenter.endRow).toBeGreaterThan(8)
regions = componentNode.querySelectorAll('.some-highlight .region')
expect(regions.length).toBe 1
regionRect = regions[0].style
expect(regionRect.top).toBe 9 * lineHeightInPixels + 'px'
expect(regionRect.top).toBe (9 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px'
expect(regionRect.height).toBe 1 * lineHeightInPixels + 'px'
expect(regionRect.left).toBe 2 * charWidth + 'px'
expect(regionRect.width).toBe 2 * charWidth + 'px'
@@ -1920,13 +1984,23 @@ describe "TextEditorComponent", ->
component.measureDimensions()
nextAnimationFrame()
linesNode = componentNode.querySelector('.lines')
expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)"
tilesNodes = componentNode.querySelectorAll(".tile")
top = 0
for tileNode in tilesNodes
expect(tileNode.style['-webkit-transform']).toBe "translate3d(0px, #{top}px, 0px)"
top += tileNode.offsetHeight
expect(horizontalScrollbarNode.scrollLeft).toBe 0
editor.setScrollLeft(100)
nextAnimationFrame()
expect(linesNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)"
top = 0
for tileNode in tilesNodes
expect(tileNode.style['-webkit-transform']).toBe "translate3d(-100px, #{top}px, 0px)"
top += tileNode.offsetHeight
expect(horizontalScrollbarNode.scrollLeft).toBe 100
it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", ->
@@ -2387,7 +2461,7 @@ describe "TextEditorComponent", ->
hiddenParent.style.display = 'none'
contentNode.appendChild(hiddenParent)
wrapperView = new TextEditorView(editor, {lineOverdrawMargin})
wrapperView = new TextEditorView(editor, {tileSize})
wrapperNode = wrapperView.element
wrapperView.appendTo(hiddenParent)
@@ -2508,7 +2582,7 @@ describe "TextEditorComponent", ->
advanceClock(atom.views.documentPollingInterval)
nextAnimationFrame()
expect(componentNode.querySelectorAll('.line')).toHaveLength(4 + lineOverdrawMargin + 1)
expect(componentNode.querySelectorAll('.line')).toHaveLength(6)
gutterWidth = componentNode.querySelector('.gutter').offsetWidth
componentNode.style.width = gutterWidth + 14 * charWidth + editor.getVerticalScrollbarWidth() + 'px'

View File

@@ -39,7 +39,6 @@ describe "TextEditorPresenter", ->
verticalScrollbarWidth: 10
scrollTop: 0
scrollLeft: 0
lineOverdrawMargin: 0
new TextEditorPresenter(params)
@@ -657,160 +656,125 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> editor.setPlaceholderText("new-placeholder-text")
expect(presenter.getState().content.placeholderText).toBe "new-placeholder-text"
describe ".lines", ->
lineStateForScreenRow = (presenter, screenRow) ->
presenter.getState().content.lines[presenter.model.tokenizedLineForScreenRow(screenRow).id]
describe ".tiles", ->
lineStateForScreenRow = (presenter, row) ->
lineId = presenter.model.tokenizedLineForScreenRow(row).id
tileRow = presenter.tileForRow(row)
presenter.getState().content.tiles[tileRow]?.lines[lineId]
it "contains states for lines that are visible on screen, plus and minus the overdraw margin", ->
presenter = buildPresenter(explicitHeight: 15, scrollTop: 50, lineHeight: 10, lineOverdrawMargin: 1)
it "contains states for tiles that are visible on screen", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expect(lineStateForScreenRow(presenter, 3)).toBeUndefined()
line4 = editor.tokenizedLineForScreenRow(4)
expectValues lineStateForScreenRow(presenter, 4), {
screenRow: 4
text: line4.text
tags: line4.tags
specialTokens: line4.specialTokens
firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex
invisibles: line4.invisibles
top: 10 * 4
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
}
line5 = editor.tokenizedLineForScreenRow(5)
expectValues lineStateForScreenRow(presenter, 5), {
screenRow: 5
text: line5.text
tags: line5.tags
specialTokens: line5.specialTokens
firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex
invisibles: line5.invisibles
top: 10 * 5
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
}
line6 = editor.tokenizedLineForScreenRow(6)
expectValues lineStateForScreenRow(presenter, 6), {
screenRow: 6
text: line6.text
tags: line6.tags
specialTokens: line6.specialTokens
firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex
invisibles: line6.invisibles
top: 10 * 6
}
expect(presenter.getState().content.tiles[12]).toBeUndefined()
line7 = editor.tokenizedLineForScreenRow(7)
expectValues lineStateForScreenRow(presenter, 7), {
screenRow: 7
text: line7.text
tags: line7.tags
specialTokens: line7.specialTokens
firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex
invisibles: line7.invisibles
top: 10 * 7
}
line8 = editor.tokenizedLineForScreenRow(8)
expectValues lineStateForScreenRow(presenter, 8), {
screenRow: 8
text: line8.text
tags: line8.tags
specialTokens: line8.specialTokens
firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex
invisibles: line8.invisibles
top: 10 * 8
}
expect(lineStateForScreenRow(presenter, 9)).toBeUndefined()
it "does not overdraw above the first row", ->
presenter = buildPresenter(explicitHeight: 15, scrollTop: 10, lineHeight: 10, lineOverdrawMargin: 2)
expect(lineStateForScreenRow(presenter, 0)).toBeDefined()
expect(lineStateForScreenRow(presenter, 1)).toBeDefined()
expect(lineStateForScreenRow(presenter, 2)).toBeDefined()
expect(lineStateForScreenRow(presenter, 3)).toBeDefined()
expect(lineStateForScreenRow(presenter, 4)).toBeDefined()
expect(lineStateForScreenRow(presenter, 5)).toBeDefined()
expect(lineStateForScreenRow(presenter, 6)).toBeUndefined()
it "does not overdraw below the last row", ->
presenter = buildPresenter(explicitHeight: 25, scrollTop: 105, lineHeight: 10, lineOverdrawMargin: 2)
expect(lineStateForScreenRow(presenter, 7)).toBeUndefined()
expect(lineStateForScreenRow(presenter, 8)).toBeDefined()
expect(lineStateForScreenRow(presenter, 9)).toBeDefined()
expect(lineStateForScreenRow(presenter, 10)).toBeDefined()
expect(lineStateForScreenRow(presenter, 11)).toBeDefined()
expect(lineStateForScreenRow(presenter, 12)).toBeDefined()
it "includes state for all lines if no external ::explicitHeight is assigned", ->
presenter = buildPresenter(explicitHeight: null)
expect(lineStateForScreenRow(presenter, 0)).toBeDefined()
expect(lineStateForScreenRow(presenter, 12)).toBeDefined()
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.lines).toEqual({})
expect(presenter.getState().content.tiles).toEqual({})
presenter.setExplicitHeight(25)
expect(presenter.getState().content.lines).toEqual({})
expect(presenter.getState().content.tiles).toEqual({})
presenter.setLineHeight(10)
expect(presenter.getState().content.lines).toEqual({})
expect(presenter.getState().content.tiles).toEqual({})
presenter.setScrollTop(0)
expect(presenter.getState().content.lines).not.toEqual({})
expect(presenter.getState().content.tiles).not.toEqual({})
it "updates when ::scrollTop changes", ->
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1)
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expect(lineStateForScreenRow(presenter, 0)).toBeDefined()
expect(lineStateForScreenRow(presenter, 4)).toBeDefined()
expect(lineStateForScreenRow(presenter, 5)).toBeUndefined()
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(25)
expectStateUpdate presenter, -> presenter.setScrollTop(2)
expect(lineStateForScreenRow(presenter, 0)).toBeUndefined()
expect(lineStateForScreenRow(presenter, 1)).toBeDefined()
expect(lineStateForScreenRow(presenter, 6)).toBeDefined()
expect(lineStateForScreenRow(presenter, 7)).toBeUndefined()
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: 15, scrollTop: 15, lineHeight: 10, lineOverdrawMargin: 1)
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
line5 = editor.tokenizedLineForScreenRow(5)
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()
expect(lineStateForScreenRow(presenter, 4)).toBeDefined()
expect(lineStateForScreenRow(presenter, 5)).toBeUndefined()
expectStateUpdate presenter, -> presenter.setExplicitHeight(8)
expectStateUpdate presenter, -> presenter.setExplicitHeight(35)
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()
expect(lineStateForScreenRow(presenter, 5)).toBeDefined()
expect(lineStateForScreenRow(presenter, 6)).toBeDefined()
expect(lineStateForScreenRow(presenter, 7)).toBeUndefined()
it "updates when ::lineHeight changes", ->
presenter = buildPresenter(explicitHeight: 15, scrollTop: 10, lineHeight: 10, lineOverdrawMargin: 0)
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
expect(lineStateForScreenRow(presenter, 0)).toBeUndefined()
expect(lineStateForScreenRow(presenter, 1)).toBeDefined()
expect(lineStateForScreenRow(presenter, 2)).toBeDefined()
expect(lineStateForScreenRow(presenter, 4)).toBeUndefined()
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(5)
expectStateUpdate presenter, -> presenter.setLineHeight(2)
expect(lineStateForScreenRow(presenter, 0)).toBeUndefined()
expect(lineStateForScreenRow(presenter, 1)).toBeUndefined()
expect(lineStateForScreenRow(presenter, 2)).toBeDefined()
expect(lineStateForScreenRow(presenter, 5)).toBeDefined()
expect(lineStateForScreenRow(presenter, 6)).toBeUndefined()
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)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10, tileSize: 2)
expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n")
@@ -832,53 +796,125 @@ describe "TextEditorPresenter", ->
tags: line3.tags
}
it "does not remove out-of-view lines corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", ->
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200)
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(lineStateForScreenRow(presenter, 0)).toBeDefined()
expect(lineStateForScreenRow(presenter, 4)).toBeDefined()
expect(lineStateForScreenRow(presenter, 5)).toBeUndefined()
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(35)
expectStateUpdate presenter, -> presenter.setScrollTop(4)
expect(lineStateForScreenRow(presenter, 0)).toBeDefined()
expect(lineStateForScreenRow(presenter, 1)).toBeUndefined()
expect(lineStateForScreenRow(presenter, 7)).toBeDefined()
expect(lineStateForScreenRow(presenter, 8)).toBeUndefined()
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(lineStateForScreenRow(presenter, 0)).toBeUndefined()
expect(lineStateForScreenRow(presenter, 1)).toBeUndefined()
expect(lineStateForScreenRow(presenter, 2)).toBeDefined()
expect(lineStateForScreenRow(presenter, 7)).toBeDefined()
expect(lineStateForScreenRow(presenter, 8)).toBeUndefined()
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(2)
presenter.setMouseWheelScreenRow(4)
advanceClock(200)
expectStateUpdate presenter, -> presenter.setScrollTop(45)
expect(lineStateForScreenRow(presenter, 2)).toBeUndefined()
expectStateUpdate presenter, -> presenter.setScrollTop(6)
expect(presenter.getState().content.tiles[4]).toBeUndefined()
it "does not preserve on-screen lines even if they correspond to ::mouseWheelScreenRow", ->
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200)
oldLine3 = editor.tokenizedLineForScreenRow(6)
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(3)
presenter.setMouseWheelScreenRow(2)
expectStateUpdate presenter, -> editor.getBuffer().insert([3, Infinity], 'xyz')
newLine3 = editor.tokenizedLineForScreenRow(3)
expectStateUpdate presenter, -> editor.setText("")
expect(presenter.getState().content.lines[oldLine3.id]).toBeUndefined()
expect(presenter.getState().content.lines[newLine3.id]).toBeDefined()
expect(presenter.getState().content.tiles[2]).toBeUndefined()
expect(presenter.getState().content.tiles[0]).toBeDefined()
it "does not attempt to preserve lines corresponding to ::mouseWheelScreenRow if they have been deleted", ->
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200)
presenter.setMouseWheelScreenRow(10)
editor.setText('')
describe "[tileId].lines[lineId]", -> # line state objects
it "includes the state for visible lines in a tile", ->
presenter = buildPresenter(explicitHeight: 3, scrollTop: 4, lineHeight: 1, tileSize: 3, stoppedScrollingDelay: 200)
expect(lineStateForScreenRow(presenter, 2)).toBeUndefined()
line3 = editor.tokenizedLineForScreenRow(3)
expectValues lineStateForScreenRow(presenter, 3), {
screenRow: 3
text: line3.text
tags: line3.tags
specialTokens: line3.specialTokens
firstNonWhitespaceIndex: line3.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line3.firstTrailingWhitespaceIndex
invisibles: line3.invisibles
top: 0
}
line4 = editor.tokenizedLineForScreenRow(4)
expectValues lineStateForScreenRow(presenter, 4), {
screenRow: 4
text: line4.text
tags: line4.tags
specialTokens: line4.specialTokens
firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex
invisibles: line4.invisibles
top: 1
}
line5 = editor.tokenizedLineForScreenRow(5)
expectValues lineStateForScreenRow(presenter, 5), {
screenRow: 5
text: line5.text
tags: line5.tags
specialTokens: line5.specialTokens
firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex
invisibles: line5.invisibles
top: 2
}
line6 = editor.tokenizedLineForScreenRow(6)
expectValues lineStateForScreenRow(presenter, 6), {
screenRow: 6
text: line6.text
tags: line6.tags
specialTokens: line6.specialTokens
firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex
invisibles: line6.invisibles
top: 0
}
line7 = editor.tokenizedLineForScreenRow(7)
expectValues lineStateForScreenRow(presenter, 7), {
screenRow: 7
text: line7.text
tags: line7.tags
specialTokens: line7.specialTokens
firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex
invisibles: line7.invisibles
top: 1
}
line8 = editor.tokenizedLineForScreenRow(8)
expectValues lineStateForScreenRow(presenter, 8), {
screenRow: 8
text: line8.text
tags: line8.tags
specialTokens: line8.specialTokens
firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex
invisibles: line8.invisibles
top: 2
}
expect(lineStateForScreenRow(presenter, 9)).toBeUndefined()
describe "[lineId]", -> # line state objects
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)
@@ -1031,9 +1067,9 @@ describe "TextEditorPresenter", ->
presenter = buildPresenter(explicitHeight: 30, scrollTop: 20)
expect(stateForCursor(presenter, 0)).toBeUndefined()
expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 2)).toBeUndefined()
expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10 - 20, left: 12 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 4)).toBeUndefined()
it "is empty until all of the required measurements are assigned", ->
@@ -1069,8 +1105,21 @@ describe "TextEditorPresenter", ->
expect(stateForCursor(presenter, 0)).toBeUndefined()
expect(stateForCursor(presenter, 1)).toBeUndefined()
expect(stateForCursor(presenter, 2)).toBeUndefined()
expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10, left: 4 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 3)).toEqual {top: 0, left: 12 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10 - 50, left: 4 * 10, width: 10, height: 10}
it "updates when ::scrollTop changes after the model was changed", ->
editor.setCursorBufferPosition([8, 22])
presenter = buildPresenter(explicitHeight: 50, scrollTop: 10 * 8)
expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 10 * 22, width: 10, height: 10}
expectStateUpdate presenter, ->
editor.getBuffer().deleteRow(12)
editor.getBuffer().deleteRow(11)
editor.getBuffer().deleteRow(10)
expect(stateForCursor(presenter, 0)).toEqual {top: 20, left: 10 * 22, width: 10, height: 10}
it "updates when ::explicitHeight changes", ->
editor.setSelectedBufferRanges([
@@ -1084,9 +1133,9 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setExplicitHeight(30)
expect(stateForCursor(presenter, 0)).toBeUndefined()
expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 2)).toBeUndefined()
expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10 - 20, left: 12 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 4)).toBeUndefined()
it "updates when ::lineHeight changes", ->
@@ -1103,15 +1152,15 @@ describe "TextEditorPresenter", ->
expect(stateForCursor(presenter, 0)).toBeUndefined()
expect(stateForCursor(presenter, 1)).toBeUndefined()
expect(stateForCursor(presenter, 2)).toBeUndefined()
expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 5, left: 12 * 10, width: 10, height: 5}
expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5, left: 4 * 10, width: 10, height: 5}
expect(stateForCursor(presenter, 3)).toEqual {top: 5, left: 12 * 10, width: 10, height: 5}
expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5 - 20, left: 4 * 10, width: 10, height: 5}
it "updates when ::baseCharacterWidth changes", ->
editor.setCursorBufferPosition([2, 4])
presenter = buildPresenter(explicitHeight: 20, scrollTop: 20)
expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20)
expect(stateForCursor(presenter, 0)).toEqual {top: 2 * 10, left: 4 * 20, width: 20, height: 10}
expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 20, width: 20, height: 10}
it "updates when scoped character widths change", ->
waitsForPromise ->
@@ -1137,11 +1186,11 @@ describe "TextEditorPresenter", ->
# moving into view
expect(stateForCursor(presenter, 0)).toBeUndefined()
editor.getCursors()[0].setBufferPosition([2, 4])
expect(stateForCursor(presenter, 0)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10}
# showing
expectStateUpdate presenter, -> editor.getSelections()[1].clear()
expect(stateForCursor(presenter, 1)).toEqual {top: 3 * 10, left: 5 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 1)).toEqual {top: 5, left: 5 * 10, width: 10, height: 10}
# hiding
expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 5]])
@@ -1153,11 +1202,11 @@ describe "TextEditorPresenter", ->
# adding
expectStateUpdate presenter, -> editor.addCursorAtBufferPosition([4, 4])
expect(stateForCursor(presenter, 2)).toEqual {top: 4 * 10, left: 4 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 2)).toEqual {top: 5, left: 4 * 10, width: 10, height: 10}
# moving added cursor
expectStateUpdate presenter, -> editor.getCursors()[2].setBufferPosition([4, 6])
expect(stateForCursor(presenter, 2)).toEqual {top: 4 * 10, left: 6 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 2)).toEqual {top: 5, left: 6 * 10, width: 10, height: 10}
# destroying
destroyedCursor = editor.getCursors()[2]
@@ -1275,39 +1324,39 @@ describe "TextEditorPresenter", ->
expectValues stateForHighlight(presenter, highlight2), {
class: 'b'
regions: [
{top: 2 * 10, left: 0 * 10, width: 6 * 10, height: 1 * 10}
{top: 2 * 10 - 20, left: 0 * 10, width: 6 * 10, height: 1 * 10}
]
}
expectValues stateForHighlight(presenter, highlight3), {
class: 'c'
regions: [
{top: 2 * 10, left: 0 * 10, right: 0, height: 1 * 10}
{top: 3 * 10, left: 0 * 10, width: 6 * 10, height: 1 * 10}
{top: 2 * 10 - 20, left: 0 * 10, right: 0, height: 1 * 10}
{top: 3 * 10 - 20, left: 0 * 10, width: 6 * 10, height: 1 * 10}
]
}
expectValues stateForHighlight(presenter, highlight4), {
class: 'd'
regions: [
{top: 2 * 10, left: 6 * 10, right: 0, height: 1 * 10}
{top: 3 * 10, left: 0, right: 0, height: 1 * 10}
{top: 4 * 10, left: 0, width: 6 * 10, height: 1 * 10}
{top: 2 * 10 - 20, left: 6 * 10, right: 0, height: 1 * 10}
{top: 3 * 10 - 20, left: 0, right: 0, height: 1 * 10}
{top: 4 * 10 - 20, left: 0, width: 6 * 10, height: 1 * 10}
]
}
expectValues stateForHighlight(presenter, highlight5), {
class: 'e'
regions: [
{top: 3 * 10, left: 6 * 10, right: 0, height: 1 * 10}
{top: 4 * 10, left: 0 * 10, right: 0, height: 2 * 10}
{top: 3 * 10 - 20, left: 6 * 10, right: 0, height: 1 * 10}
{top: 4 * 10 - 20, left: 0 * 10, right: 0, height: 2 * 10}
]
}
expectValues stateForHighlight(presenter, highlight6), {
class: 'f'
regions: [
{top: 5 * 10, left: 6 * 10, right: 0, height: 1 * 10}
{top: 5 * 10 - 20, left: 6 * 10, right: 0, height: 1 * 10}
]
}
@@ -1913,20 +1962,18 @@ describe "TextEditorPresenter", ->
getLineNumberGutterState(presenter).content.lineNumbers[key]
it "contains states for line numbers that are visible on screen, plus and minus the overdraw margin", ->
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, lineOverdrawMargin: 1)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10)
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false, top: 2 * 10}
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}
expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false, top: 7 * 10}
expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined()
it "includes states for all line numbers if no ::explicitHeight is assigned", ->
presenter = buildPresenter(explicitHeight: null)
@@ -1937,43 +1984,43 @@ describe "TextEditorPresenter", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineOverdrawMargin: 1)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30)
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2}
expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8}
expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
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, 0)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 1), {bufferRow: 1}
expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7}
expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined()
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, lineOverdrawMargin: 1)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30)
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2}
expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8}
expect(lineNumberStateForScreenRow(presenter, 8)).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, 2), {bufferRow: 2}
expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8}
expect(lineNumberStateForScreenRow(presenter, 9)).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, lineOverdrawMargin: 0)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0)
expectValues lineNumberStateForScreenRow(presenter, 0), {bufferRow: 0}
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
@@ -1989,7 +2036,7 @@ describe "TextEditorPresenter", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 35, scrollTop: 30, lineOverdrawMargin: 0)
presenter = buildPresenter(explicitHeight: 35, scrollTop: 30)
expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3}
@@ -2011,26 +2058,26 @@ describe "TextEditorPresenter", ->
expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
it "does not remove out-of-view line numbers corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", ->
presenter = buildPresenter(explicitHeight: 25, lineOverdrawMargin: 1, stoppedScrollingDelay: 200)
presenter = buildPresenter(explicitHeight: 25, stoppedScrollingDelay: 200)
expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 4)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 5)).toBeUndefined()
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, 7)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 8)).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, 7)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
expect(lineNumberStateForScreenRow(presenter, 6)).toBeDefined()
expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined()
it "correctly handles the first screen line being soft-wrapped", ->
editor.setSoftWrapped(true)
@@ -2213,12 +2260,11 @@ describe "TextEditorPresenter", ->
scrollTop = 0
lineHeight = 10
explicitHeight = lineHeight * 10
lineOverdrawMargin = 1
beforeEach ->
# At the beginning of each test, decoration1 and decoration2 are in visible range,
# but not decoration3.
presenter = buildPresenter({explicitHeight, scrollTop, lineHeight, lineOverdrawMargin})
presenter = buildPresenter({explicitHeight, scrollTop, lineHeight})
gutter = editor.addGutter({name: 'test-gutter', visible: true})
decorationItem = document.createElement('div')
decorationItem.class = 'decoration-item'
@@ -2589,7 +2635,7 @@ describe "TextEditorPresenter", ->
editor.setEditorWidthInChars(80)
presenterParams =
model: editor
lineOverdrawMargin: 1
presenter = new TextEditorPresenter(presenterParams)
statements = []

View File

@@ -1,16 +1,10 @@
_ = require 'underscore-plus'
{toArray} = require 'underscore-plus'
{$$} = require 'space-pen'
CursorsComponent = require './cursors-component'
HighlightsComponent = require './highlights-component'
TokenIterator = require './token-iterator'
TileComponent = require './tile-component'
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
WrapperDiv = document.createElement('div')
TokenTextEscapeRegex = /[&"'<>]/g
MaxTokenLength = 20000
cloneObject = (object) ->
clone = {}
@@ -22,12 +16,7 @@ class LinesComponent
placeholderTextDiv: null
constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) ->
@tokenIterator = new TokenIterator
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@renderedDecorationsByLineId = {}
@tileComponentsByTileId = {}
@domNode = document.createElement('div')
@domNode.classList.add('lines')
@@ -48,17 +37,12 @@ class LinesComponent
updateSync: (state) ->
@newState = state.content
@oldState ?= {lines: {}}
@oldState ?= {tiles: {}}
if @newState.scrollHeight isnt @oldState.scrollHeight
@domNode.style.height = @newState.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newState.scrollTop isnt @oldState.scrollTop or @newState.scrollLeft isnt @oldState.scrollLeft
@domNode.style['-webkit-transform'] = "translate3d(#{-@newState.scrollLeft}px, #{-@newState.scrollTop}px, 0px)"
@oldState.scrollTop = @newState.scrollTop
@oldState.scrollLeft = @newState.scrollLeft
if @newState.backgroundColor isnt @oldState.backgroundColor
@domNode.style.backgroundColor = @newState.backgroundColor
@oldState.backgroundColor = @newState.backgroundColor
@@ -71,8 +55,8 @@ class LinesComponent
@placeholderTextDiv.textContent = @newState.placeholderText
@domNode.appendChild(@placeholderTextDiv)
@removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateLineNodes()
@removeTileNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateTileNodes()
if @newState.scrollWidth isnt @oldState.scrollWidth
@domNode.style.width = @newState.scrollWidth + 'px'
@@ -84,248 +68,35 @@ class LinesComponent
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
@oldState.scrollWidth = @newState.scrollWidth
removeLineNodes: ->
@removeLineNode(id) for id of @oldState.lines
removeTileNodes: ->
@removeTileNode(id) for id of @oldState.tiles
return
removeLineNode: (id) ->
@lineNodesByLineId[id].remove()
delete @lineNodesByLineId[id]
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
delete @screenRowsByLineId[id]
delete @oldState.lines[id]
removeTileNode: (id) ->
node = @tileComponentsByTileId[id].getDomNode()
updateLineNodes: ->
for id of @oldState.lines
unless @newState.lines.hasOwnProperty(id)
@removeLineNode(id)
node.remove()
delete @tileComponentsByTileId[id]
delete @oldState.tiles[id]
newLineIds = null
newLinesHTML = null
updateTileNodes: ->
for id of @oldState.tiles
unless @newState.tiles.hasOwnProperty(id)
@removeTileNode(id)
for id, lineState of @newState.lines
if @oldState.lines.hasOwnProperty(id)
@updateLineNode(id)
for id, tileState of @newState.tiles
if @oldState.tiles.hasOwnProperty(id)
tileComponent = @tileComponentsByTileId[id]
else
newLineIds ?= []
newLinesHTML ?= ""
newLineIds.push(id)
newLinesHTML += @buildLineHTML(id)
@screenRowsByLineId[id] = lineState.screenRow
@lineIdsByScreenRow[lineState.screenRow] = id
@oldState.lines[id] = cloneObject(lineState)
tileComponent = @tileComponentsByTileId[id] = new TileComponent({id, @presenter})
return unless newLineIds?
@domNode.appendChild(tileComponent.getDomNode())
@oldState.tiles[id] = cloneObject(tileState)
WrapperDiv.innerHTML = newLinesHTML
newLineNodes = _.toArray(WrapperDiv.children)
for id, i in newLineIds
lineNode = newLineNodes[i]
@lineNodesByLineId[id] = lineNode
@domNode.appendChild(lineNode)
tileComponent.updateSync(@newState)
return
buildLineHTML: (id) ->
{scrollWidth} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id]
classes = ''
if decorationClasses?
for decorationClass in decorationClasses
classes += decorationClass + ' '
classes += 'line'
lineHTML = "<div class=\"#{classes}\" style=\"position: absolute; top: #{top}px; width: #{scrollWidth}px;\" data-screen-row=\"#{screenRow}\">"
if text is ""
lineHTML += @buildEmptyLineInnerHTML(id)
else
lineHTML += @buildLineInnerHTML(id)
lineHTML += '<span class="fold-marker"></span>' if fold
lineHTML += "</div>"
lineHTML
buildEmptyLineInnerHTML: (id) ->
{indentGuidesVisible} = @newState
{indentLevel, tabLength, endOfLineInvisibles} = @newState.lines[id]
if indentGuidesVisible and indentLevel > 0
invisibleIndex = 0
lineHTML = ''
for i in [0...indentLevel]
lineHTML += "<span class='indent-guide'>"
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
lineHTML += "<span class='invisible-character'>#{invisible}</span>"
else
lineHTML += ' '
lineHTML += "</span>"
while invisibleIndex < endOfLineInvisibles?.length
lineHTML += "<span class='invisible-character'>#{endOfLineInvisibles[invisibleIndex++]}</span>"
lineHTML
else
@buildEndOfLineHTML(id) or '&nbsp;'
buildLineInnerHTML: (id) ->
lineState = @newState.lines[id]
{firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
innerHTML = ""
@tokenIterator.reset(lineState)
while @tokenIterator.next()
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
for scope in @tokenIterator.getScopeStarts()
innerHTML += "<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
tokenStart = @tokenIterator.getScreenStart()
tokenEnd = @tokenIterator.getScreenEnd()
tokenText = @tokenIterator.getText()
isHardTab = @tokenIterator.isHardTab()
if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
else
tokenFirstNonWhitespaceIndex = null
if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
else
tokenFirstTrailingWhitespaceIndex = null
hasIndentGuide =
@newState.indentGuidesVisible and
(hasLeadingWhitespace or lineIsWhitespaceOnly)
hasInvisibleCharacters =
(invisibles?.tab and isHardTab) or
(invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters)
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
for scope in @tokenIterator.getScopes()
innerHTML += "</span>"
innerHTML += @buildEndOfLineHTML(id)
innerHTML
buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) ->
if isHardTab
classes = 'hard-tab'
classes += ' leading-whitespace' if firstNonWhitespaceIndex?
classes += ' trailing-whitespace' if firstTrailingWhitespaceIndex?
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if hasInvisibleCharacters
return "<span class='#{classes}'>#{@escapeTokenText(tokenText)}</span>"
else
startIndex = 0
endIndex = tokenText.length
leadingHtml = ''
trailingHtml = ''
if firstNonWhitespaceIndex?
leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex)
classes = 'leading-whitespace'
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if hasInvisibleCharacters
leadingHtml = "<span class='#{classes}'>#{leadingWhitespace}</span>"
startIndex = firstNonWhitespaceIndex
if firstTrailingWhitespaceIndex?
tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
trailingWhitespace = tokenText.substring(firstTrailingWhitespaceIndex)
classes = 'trailing-whitespace'
classes += ' indent-guide' if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
classes += ' invisible-character' if hasInvisibleCharacters
trailingHtml = "<span class='#{classes}'>#{trailingWhitespace}</span>"
endIndex = firstTrailingWhitespaceIndex
html = leadingHtml
if tokenText.length > MaxTokenLength
while startIndex < endIndex
html += "<span>" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + "</span>"
startIndex += MaxTokenLength
else
html += @escapeTokenText(tokenText, startIndex, endIndex)
html += trailingHtml
html
escapeTokenText: (tokenText, startIndex, endIndex) ->
if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
tokenText = tokenText.slice(startIndex, endIndex)
tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace)
escapeTokenTextReplace: (match) ->
switch match
when '&' then '&amp;'
when '"' then '&quot;'
when "'" then '&#39;'
when '<' then '&lt;'
when '>' then '&gt;'
else match
buildEndOfLineHTML: (id) ->
{endOfLineInvisibles} = @newState.lines[id]
html = ''
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
html += "<span class='invisible-character'>#{invisible}</span>"
html
updateLineNode: (id) ->
oldLineState = @oldState.lines[id]
newLineState = @newState.lines[id]
lineNode = @lineNodesByLineId[id]
if @newState.scrollWidth isnt @oldState.scrollWidth
lineNode.style.width = @newState.scrollWidth + 'px'
newDecorationClasses = newLineState.decorationClasses
oldDecorationClasses = oldLineState.decorationClasses
if oldDecorationClasses?
for decorationClass in oldDecorationClasses
unless newDecorationClasses? and decorationClass in newDecorationClasses
lineNode.classList.remove(decorationClass)
if newDecorationClasses?
for decorationClass in newDecorationClasses
unless oldDecorationClasses? and decorationClass in oldDecorationClasses
lineNode.classList.add(decorationClass)
oldLineState.decorationClasses = newLineState.decorationClasses
if newLineState.top isnt oldLineState.top
lineNode.style.top = newLineState.top + 'px'
oldLineState.top = newLineState.cop
if newLineState.screenRow isnt oldLineState.screenRow
lineNode.dataset.screenRow = newLineState.screenRow
oldLineState.screenRow = newLineState.screenRow
@lineIdsByScreenRow[newLineState.screenRow] = id
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
measureLineHeightAndDefaultCharWidth: ->
@domNode.appendChild(DummyLineNode)
lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
@@ -343,59 +114,18 @@ class LinesComponent
measureCharactersInNewLines: ->
@presenter.batchCharacterMeasurement =>
for id, lineState of @oldState.lines
unless @measuredLines.has(id)
lineNode = @lineNodesByLineId[id]
@measureCharactersInLine(id, lineState, lineNode)
for id, component of @tileComponentsByTileId
component.measureCharactersInNewLines()
return
measureCharactersInLine: (lineId, tokenizedLine, lineNode) ->
rangeForMeasurement = null
iterator = null
charIndex = 0
@tokenIterator.reset(tokenizedLine)
while @tokenIterator.next()
scopes = @tokenIterator.getScopes()
text = @tokenIterator.getText()
charWidths = @presenter.getScopedCharacterWidths(scopes)
textIndex = 0
while textIndex < text.length
if @tokenIterator.isPairedCharacter()
char = text
charLength = 2
textIndex += 2
else
char = text[textIndex]
charLength = 1
textIndex++
continue if char is '\0'
unless charWidths[char]?
unless textNode?
rangeForMeasurement ?= document.createRange()
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
textNode = iterator.nextNode()
textNodeIndex = 0
nextTextNodeIndex = textNode.textContent.length
while nextTextNodeIndex <= charIndex
textNode = iterator.nextNode()
textNodeIndex = nextTextNodeIndex
nextTextNodeIndex = textNodeIndex + textNode.textContent.length
i = charIndex - textNodeIndex
rangeForMeasurement.setStart(textNode, i)
rangeForMeasurement.setEnd(textNode, i + charLength)
charWidth = rangeForMeasurement.getBoundingClientRect().width
@presenter.setScopedCharacterWidth(scopes, char, charWidth)
charIndex += charLength
@measuredLines.add(lineId)
clearScopedCharWidths: ->
@measuredLines.clear()
for id, component of @tileComponentsByTileId
component.clearMeasurements()
@presenter.clearScopedCharacterWidths()
lineNodeForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
@tileComponentsByTileId[tile]?.lineNodeForScreenRow(screenRow)

View File

@@ -18,7 +18,7 @@ class TextEditorComponent
scrollSensitivity: 0.4
cursorBlinkPeriod: 800
cursorBlinkResumeDelay: 100
lineOverdrawMargin: 15
tileSize: 12
pendingScrollTop: null
pendingScrollLeft: null
@@ -36,8 +36,8 @@ class TextEditorComponent
gutterComponent: null
mounted: true
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, lineOverdrawMargin}) ->
@lineOverdrawMargin = lineOverdrawMargin if lineOverdrawMargin?
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@observeConfig()
@@ -47,7 +47,7 @@ class TextEditorComponent
model: @editor
scrollTop: @editor.getScrollTop()
scrollLeft: @editor.getScrollLeft()
lineOverdrawMargin: lineOverdrawMargin
tileSize: tileSize
cursorBlinkPeriod: @cursorBlinkPeriod
cursorBlinkResumeDelay: @cursorBlinkResumeDelay
stoppedScrollingDelay: 200
@@ -768,8 +768,8 @@ class TextEditorComponent
{clientX, clientY} = event
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
top = clientY - linesClientRect.top
left = clientX - linesClientRect.left
top = clientY - linesClientRect.top + @presenter.scrollTop
left = clientX - linesClientRect.left + @presenter.scrollLeft
{top, left}
getModel: ->

View File

@@ -15,8 +15,9 @@ class TextEditorElement extends HTMLElement
componentDescriptor: null
component: null
attached: false
lineOverdrawMargin: null
tileSize: null
focusOnAttach: false
hasTiledRendering: true
createdCallback: ->
@emitter = new Emitter
@@ -110,7 +111,7 @@ class TextEditorElement extends HTMLElement
rootElement: @rootElement
stylesElement: @stylesElement
editor: @model
lineOverdrawMargin: @lineOverdrawMargin
tileSize: @tileSize
useShadowDOM: @useShadowDOM
)
@rootElement.appendChild(@component.getDomNode())

View File

@@ -15,11 +15,12 @@ class TextEditorPresenter
constructor: (params) ->
{@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params
{horizontalScrollbarHeight, verticalScrollbarWidth} = params
{@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params
{@lineHeight, @baseCharacterWidth, @backgroundColor, @gutterBackgroundColor, @tileSize} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params
@measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
@measuredVerticalScrollbarWidth = verticalScrollbarWidth
@gutterWidth ?= 0
@tileSize ?= 12
@disposables = new CompositeDisposable
@emitter = new Emitter
@@ -74,7 +75,7 @@ class TextEditorPresenter
@updateHiddenInputState() if @shouldUpdateHiddenInputState
@updateContentState() if @shouldUpdateContentState
@updateDecorations() if @shouldUpdateDecorations
@updateLinesState() if @shouldUpdateLinesState
@updateTilesState() if @shouldUpdateTilesState
@updateCursorsState() if @shouldUpdateCursorsState
@updateOverlaysState() if @shouldUpdateOverlaysState
@updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState
@@ -96,7 +97,7 @@ class TextEditorPresenter
@shouldUpdateHiddenInputState = false
@shouldUpdateContentState = false
@shouldUpdateDecorations = false
@shouldUpdateLinesState = false
@shouldUpdateTilesState = false
@shouldUpdateCursorsState = false
@shouldUpdateOverlaysState = false
@shouldUpdateLineNumberGutterState = false
@@ -114,7 +115,8 @@ class TextEditorPresenter
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateTilesState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@@ -130,7 +132,7 @@ class TextEditorPresenter
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateTilesState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@@ -205,7 +207,7 @@ class TextEditorPresenter
content:
scrollingVertically: false
cursorsVisible: false
lines: {}
tiles: {}
highlights: {}
overlays: {}
gutters: []
@@ -231,7 +233,7 @@ class TextEditorPresenter
@updateHiddenInputState()
@updateContentState()
@updateDecorations()
@updateLinesState()
@updateTilesState()
@updateCursorsState()
@updateOverlaysState()
@updateLineNumberGutterState()
@@ -282,8 +284,6 @@ class TextEditorPresenter
{top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange())
if @focused
top -= @scrollTop
left -= @scrollLeft
@state.hiddenInput.top = Math.max(Math.min(top, @clientHeight - height), 0)
@state.hiddenInput.left = Math.max(Math.min(left, @clientWidth - width), 0)
else
@@ -300,56 +300,84 @@ class TextEditorPresenter
@state.content.backgroundColor = if @model.isMini() then null else @backgroundColor
@state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null
updateLinesState: ->
tileForRow: (row) ->
row - (row % @tileSize)
getStartTileRow: ->
Math.max(0, @tileForRow(@startRow))
getEndTileRow: ->
Math.min(
@tileForRow(@model.getScreenLineCount()), @tileForRow(@endRow)
)
updateTilesState: ->
return unless @startRow? and @endRow? and @lineHeight?
visibleTiles = {}
for startRow in [@getStartTileRow()..@getEndTileRow()] by @tileSize
endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize)
tile = @state.content.tiles[startRow] ?= {}
tile.top = startRow * @lineHeight - @scrollTop
tile.left = -@scrollLeft
tile.height = @tileSize * @lineHeight
tile.display = "block"
@updateLinesState(tile, startRow, endRow)
visibleTiles[startRow] = true
if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)?
mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
unless visibleTiles[mouseWheelTile]?
@state.content.tiles[mouseWheelTile].display = "none"
visibleTiles[mouseWheelTile] = true
for id, tile of @state.content.tiles
continue if visibleTiles.hasOwnProperty(id)
delete @state.content.tiles[id]
updateLinesState: (tileState, startRow, endRow) ->
tileState.lines ?= {}
visibleLineIds = {}
row = @startRow
while row < @endRow
row = startRow
while row < endRow
line = @model.tokenizedLineForScreenRow(row)
unless line?
throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}")
visibleLineIds[line.id] = true
if @state.content.lines.hasOwnProperty(line.id)
@updateLineState(row, line)
if tileState.lines.hasOwnProperty(line.id)
lineState = tileState.lines[line.id]
lineState.screenRow = row
lineState.top = (row - startRow) * @lineHeight
lineState.decorationClasses = @lineDecorationClassesForRow(row)
else
@buildLineState(row, line)
tileState.lines[line.id] =
screenRow: row
text: line.text
openScopes: line.openScopes
tags: line.tags
specialTokens: line.specialTokens
firstNonWhitespaceIndex: line.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex
invisibles: line.invisibles
endOfLineInvisibles: line.endOfLineInvisibles
isOnlyWhitespace: line.isOnlyWhitespace()
indentLevel: line.indentLevel
tabLength: line.tabLength
fold: line.fold
top: (row - startRow) * @lineHeight
decorationClasses: @lineDecorationClassesForRow(row)
row++
if @mouseWheelScreenRow?
if preservedLine = @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)
visibleLineIds[preservedLine.id] = true
for id, line of @state.content.lines
unless visibleLineIds.hasOwnProperty(id)
delete @state.content.lines[id]
for id, line of tileState.lines
delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id)
return
updateLineState: (row, line) ->
lineState = @state.content.lines[line.id]
lineState.screenRow = row
lineState.top = row * @lineHeight
lineState.decorationClasses = @lineDecorationClassesForRow(row)
buildLineState: (row, line) ->
@state.content.lines[line.id] =
screenRow: row
text: line.text
openScopes: line.openScopes
tags: line.tags
specialTokens: line.specialTokens
firstNonWhitespaceIndex: line.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex
invisibles: line.invisibles
endOfLineInvisibles: line.endOfLineInvisibles
isOnlyWhitespace: line.isOnlyWhitespace()
indentLevel: line.indentLevel
tabLength: line.tabLength
fold: line.fold
top: row * @lineHeight
decorationClasses: @lineDecorationClassesForRow(row)
updateCursorsState: ->
@state.content.cursors = {}
@updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
@@ -382,12 +410,10 @@ class TextEditorPresenter
else
screenPosition = decoration.getMarker().getHeadScreenPosition()
pixelPosition = @pixelPositionForScreenPosition(screenPosition)
pixelPosition = @pixelPositionForScreenPosition(screenPosition, true)
{scrollTop, scrollLeft} = @state.content
top = pixelPosition.top + @lineHeight - scrollTop
left = pixelPosition.left + @gutterWidth - scrollLeft
top = pixelPosition.top + @lineHeight
left = pixelPosition.left + @gutterWidth
if overlayDimensions = @overlayDimensions[decoration.id]
{itemWidth, itemHeight, contentMargin} = overlayDimensions
@@ -569,7 +595,7 @@ class TextEditorPresenter
updateStartRow: ->
return unless @scrollTop? and @lineHeight?
startRow = Math.floor(@scrollTop / @lineHeight) - @lineOverdrawMargin
startRow = Math.floor(@scrollTop / @lineHeight)
@startRow = Math.max(0, startRow)
updateEndRow: ->
@@ -577,7 +603,7 @@ class TextEditorPresenter
startRow = Math.max(0, Math.floor(@scrollTop / @lineHeight))
visibleLinesCount = Math.ceil(@height / @lineHeight) + 1
endRow = startRow + visibleLinesCount + @lineOverdrawMargin
endRow = startRow + visibleLinesCount
@endRow = Math.min(@model.getScreenLineCount(), endRow)
updateScrollWidth: ->
@@ -610,6 +636,7 @@ class TextEditorPresenter
oldContentWidth = @contentWidth
clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped()
@contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), @model.getMaxScreenLineLength()], clip).left
@contentWidth += @scrollLeft
@contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
if @contentHeight isnt oldContentHeight
@@ -755,7 +782,7 @@ class TextEditorPresenter
@shouldUpdateVerticalScrollState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateTilesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@@ -778,7 +805,7 @@ class TextEditorPresenter
@state.content.scrollingVertically = false
if @mouseWheelScreenRow?
@mouseWheelScreenRow = null
@shouldUpdateLinesState = true
@shouldUpdateTilesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@@ -792,8 +819,10 @@ class TextEditorPresenter
@model.setScrollLeft(scrollLeft)
@shouldUpdateHorizontalScrollState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateCursorsState = true unless oldScrollLeft?
@shouldUpdateCursorsState = true
@shouldUpdateOverlaysState = true
@shouldUpdateDecorations = true
@shouldUpdateTilesState = true
@emitDidUpdateState()
@@ -841,7 +870,7 @@ class TextEditorPresenter
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateTilesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@@ -869,7 +898,7 @@ class TextEditorPresenter
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateTilesState = true
@shouldUpdateCursorsState = true unless oldContentFrameWidth?
@emitDidUpdateState()
@@ -935,7 +964,7 @@ class TextEditorPresenter
@shouldUpdateScrollbarsState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateTilesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@@ -943,9 +972,9 @@ class TextEditorPresenter
@emitDidUpdateState()
setMouseWheelScreenRow: (mouseWheelScreenRow) ->
unless @mouseWheelScreenRow is mouseWheelScreenRow
@mouseWheelScreenRow = mouseWheelScreenRow
setMouseWheelScreenRow: (screenRow) ->
if @mouseWheelScreenRow isnt screenRow
@mouseWheelScreenRow = screenRow
@didStartScrolling()
setBaseCharacterWidth: (baseCharacterWidth) ->
@@ -987,7 +1016,7 @@ class TextEditorPresenter
@shouldUpdateHiddenInputState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateTilesState = true
@shouldUpdateCursorsState = true
@shouldUpdateOverlaysState = true
@@ -1028,10 +1057,13 @@ class TextEditorPresenter
charLength = 1
valueIndex++
return {top, left} if column is targetColumn
break if column is targetColumn
left += characterWidths[char] ? baseCharacterWidth unless char is '\0'
column += charLength
top -= @scrollTop
left -= @scrollLeft
{top, left}
hasPixelRectRequirements: ->
@@ -1082,7 +1114,7 @@ class TextEditorPresenter
intersectsVisibleRowRange = true
if intersectsVisibleRowRange
@shouldUpdateLinesState = true if decoration.isType('line')
@shouldUpdateTilesState = true if decoration.isType('line')
if decoration.isType('line-number')
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')
@@ -1107,7 +1139,7 @@ class TextEditorPresenter
decoration.getMarker().getScreenRange())
@addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
if decoration.isType('line') or Decoration.isType(oldProperties, 'line')
@shouldUpdateLinesState = true
@shouldUpdateTilesState = 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
@@ -1123,7 +1155,7 @@ class TextEditorPresenter
didDestroyDecoration: (decoration) ->
if decoration.isType('line') or decoration.isType('gutter')
@removeFromLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
@shouldUpdateLinesState = true if decoration.isType('line')
@shouldUpdateTilesState = true if decoration.isType('line')
if decoration.isType('line-number')
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')
@@ -1148,7 +1180,7 @@ class TextEditorPresenter
if decoration.isType('line') or decoration.isType('gutter')
@addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
@shouldUpdateLinesState = true if decoration.isType('line')
@shouldUpdateTilesState = true if decoration.isType('line')
if decoration.isType('line-number')
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')

View File

@@ -60,7 +60,7 @@ class TextEditorView extends View
placeholderText: placeholderText
element = new TextEditorElement
element.lineOverdrawMargin = props?.lineOverdrawMargin
element.tileSize = props?.tileSize
element.setAttribute(name, value) for name, value of attributes if attributes?
element.setModel(model)
return element.__spacePenView

361
src/tile-component.coffee Normal file
View File

@@ -0,0 +1,361 @@
_ = require 'underscore-plus'
TokenIterator = require './token-iterator'
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
WrapperDiv = document.createElement('div')
TokenTextEscapeRegex = /[&"'<>]/g
MaxTokenLength = 20000
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
clone
module.exports =
class TileComponent
placeholderTextDiv: null
constructor: ({@presenter, @id}) ->
@tokenIterator = new TokenIterator
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@domNode = document.createElement("div")
@domNode.classList.add("tile")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
getDomNode: ->
@domNode
updateSync: (state) ->
@newState = state
unless @oldState
@oldState = {tiles: {}}
@oldState.tiles[@id] = {lines: {}}
@newTileState = @newState.tiles[@id]
@oldTileState = @oldState.tiles[@id]
if @newTileState.display isnt @oldTileState.display
@domNode.style.display = @newTileState.display
@oldTileState.display = @newTileState.display
if @newTileState.height isnt @oldTileState.height
@domNode.style.height = @newTileState.height + 'px'
@oldTileState.height = @newTileState.height
if @newTileState.top isnt @oldTileState.top or @newTileState.left isnt @oldTileState.left
@domNode.style['-webkit-transform'] = "translate3d(#{@newTileState.left}px, #{@newTileState.top}px, 0px)"
@oldTileState.top = @newTileState.top
@oldTileState.left = @newTileState.left
@removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateLineNodes()
if @newState.scrollWidth isnt @oldState.scrollWidth
@domNode.style.width = @newState.scrollWidth + 'px'
@oldState.scrollWidth = @newState.scrollWidth
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
@oldState.scrollWidth = @newState.scrollWidth
removeLineNodes: ->
@removeLineNode(id) for id of @oldTileState.lines
return
removeLineNode: (id) ->
@lineNodesByLineId[id].remove()
delete @lineNodesByLineId[id]
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
delete @screenRowsByLineId[id]
delete @oldTileState.lines[id]
updateLineNodes: ->
for id of @oldTileState.lines
unless @newTileState.lines.hasOwnProperty(id)
@removeLineNode(id)
newLineIds = null
newLinesHTML = null
for id, lineState of @newTileState.lines
if @oldTileState.lines.hasOwnProperty(id)
@updateLineNode(id)
else
newLineIds ?= []
newLinesHTML ?= ""
newLineIds.push(id)
newLinesHTML += @buildLineHTML(id)
@screenRowsByLineId[id] = lineState.screenRow
@lineIdsByScreenRow[lineState.screenRow] = id
@oldTileState.lines[id] = cloneObject(lineState)
return unless newLineIds?
WrapperDiv.innerHTML = newLinesHTML
newLineNodes = _.toArray(WrapperDiv.children)
for id, i in newLineIds
lineNode = newLineNodes[i]
@lineNodesByLineId[id] = lineNode
@domNode.appendChild(lineNode)
return
buildLineHTML: (id) ->
{scrollWidth} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
classes = ''
if decorationClasses?
for decorationClass in decorationClasses
classes += decorationClass + ' '
classes += 'line'
lineHTML = "<div class=\"#{classes}\" style=\"position: absolute; top: #{top}px; width: #{scrollWidth}px;\" data-screen-row=\"#{screenRow}\">"
if text is ""
lineHTML += @buildEmptyLineInnerHTML(id)
else
lineHTML += @buildLineInnerHTML(id)
lineHTML += '<span class="fold-marker"></span>' if fold
lineHTML += "</div>"
lineHTML
buildEmptyLineInnerHTML: (id) ->
{indentGuidesVisible} = @newState
{indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id]
if indentGuidesVisible and indentLevel > 0
invisibleIndex = 0
lineHTML = ''
for i in [0...indentLevel]
lineHTML += "<span class='indent-guide'>"
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
lineHTML += "<span class='invisible-character'>#{invisible}</span>"
else
lineHTML += ' '
lineHTML += "</span>"
while invisibleIndex < endOfLineInvisibles?.length
lineHTML += "<span class='invisible-character'>#{endOfLineInvisibles[invisibleIndex++]}</span>"
lineHTML
else
@buildEndOfLineHTML(id) or '&nbsp;'
buildLineInnerHTML: (id) ->
lineState = @newTileState.lines[id]
{firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
innerHTML = ""
@tokenIterator.reset(lineState)
while @tokenIterator.next()
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
for scope in @tokenIterator.getScopeStarts()
innerHTML += "<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
tokenStart = @tokenIterator.getScreenStart()
tokenEnd = @tokenIterator.getScreenEnd()
tokenText = @tokenIterator.getText()
isHardTab = @tokenIterator.isHardTab()
if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
else
tokenFirstNonWhitespaceIndex = null
if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
else
tokenFirstTrailingWhitespaceIndex = null
hasIndentGuide =
@newState.indentGuidesVisible and
(hasLeadingWhitespace or lineIsWhitespaceOnly)
hasInvisibleCharacters =
(invisibles?.tab and isHardTab) or
(invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters)
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
for scope in @tokenIterator.getScopes()
innerHTML += "</span>"
innerHTML += @buildEndOfLineHTML(id)
innerHTML
buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) ->
if isHardTab
classes = 'hard-tab'
classes += ' leading-whitespace' if firstNonWhitespaceIndex?
classes += ' trailing-whitespace' if firstTrailingWhitespaceIndex?
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if hasInvisibleCharacters
return "<span class='#{classes}'>#{@escapeTokenText(tokenText)}</span>"
else
startIndex = 0
endIndex = tokenText.length
leadingHtml = ''
trailingHtml = ''
if firstNonWhitespaceIndex?
leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex)
classes = 'leading-whitespace'
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if hasInvisibleCharacters
leadingHtml = "<span class='#{classes}'>#{leadingWhitespace}</span>"
startIndex = firstNonWhitespaceIndex
if firstTrailingWhitespaceIndex?
tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
trailingWhitespace = tokenText.substring(firstTrailingWhitespaceIndex)
classes = 'trailing-whitespace'
classes += ' indent-guide' if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
classes += ' invisible-character' if hasInvisibleCharacters
trailingHtml = "<span class='#{classes}'>#{trailingWhitespace}</span>"
endIndex = firstTrailingWhitespaceIndex
html = leadingHtml
if tokenText.length > MaxTokenLength
while startIndex < endIndex
html += "<span>" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + "</span>"
startIndex += MaxTokenLength
else
html += @escapeTokenText(tokenText, startIndex, endIndex)
html += trailingHtml
html
escapeTokenText: (tokenText, startIndex, endIndex) ->
if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
tokenText = tokenText.slice(startIndex, endIndex)
tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace)
escapeTokenTextReplace: (match) ->
switch match
when '&' then '&amp;'
when '"' then '&quot;'
when "'" then '&#39;'
when '<' then '&lt;'
when '>' then '&gt;'
else match
buildEndOfLineHTML: (id) ->
{endOfLineInvisibles} = @newTileState.lines[id]
html = ''
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
html += "<span class='invisible-character'>#{invisible}</span>"
html
updateLineNode: (id) ->
oldLineState = @oldTileState.lines[id]
newLineState = @newTileState.lines[id]
lineNode = @lineNodesByLineId[id]
if @newState.scrollWidth isnt @oldState.scrollWidth
lineNode.style.width = @newState.scrollWidth + 'px'
newDecorationClasses = newLineState.decorationClasses
oldDecorationClasses = oldLineState.decorationClasses
if oldDecorationClasses?
for decorationClass in oldDecorationClasses
unless newDecorationClasses? and decorationClass in newDecorationClasses
lineNode.classList.remove(decorationClass)
if newDecorationClasses?
for decorationClass in newDecorationClasses
unless oldDecorationClasses? and decorationClass in oldDecorationClasses
lineNode.classList.add(decorationClass)
oldLineState.decorationClasses = newLineState.decorationClasses
if newLineState.top isnt oldLineState.top
lineNode.style.top = newLineState.top + 'px'
oldLineState.top = newLineState.top
if newLineState.screenRow isnt oldLineState.screenRow
lineNode.dataset.screenRow = newLineState.screenRow
oldLineState.screenRow = newLineState.screenRow
@lineIdsByScreenRow[newLineState.screenRow] = id
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
measureCharactersInNewLines: ->
for id, lineState of @oldTileState.lines
unless @measuredLines.has(id)
lineNode = @lineNodesByLineId[id]
@measureCharactersInLine(id, lineState, lineNode)
return
measureCharactersInLine: (lineId, tokenizedLine, lineNode) ->
rangeForMeasurement = null
iterator = null
charIndex = 0
@tokenIterator.reset(tokenizedLine)
while @tokenIterator.next()
scopes = @tokenIterator.getScopes()
text = @tokenIterator.getText()
charWidths = @presenter.getScopedCharacterWidths(scopes)
textIndex = 0
while textIndex < text.length
if @tokenIterator.isPairedCharacter()
char = text
charLength = 2
textIndex += 2
else
char = text[textIndex]
charLength = 1
textIndex++
continue if char is '\0'
unless charWidths[char]?
unless textNode?
rangeForMeasurement ?= document.createRange()
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
textNode = iterator.nextNode()
textNodeIndex = 0
nextTextNodeIndex = textNode.textContent.length
while nextTextNodeIndex <= charIndex
textNode = iterator.nextNode()
textNodeIndex = nextTextNodeIndex
nextTextNodeIndex = textNodeIndex + textNode.textContent.length
i = charIndex - textNodeIndex
rangeForMeasurement.setStart(textNode, i)
rangeForMeasurement.setEnd(textNode, i + charLength)
charWidth = rangeForMeasurement.getBoundingClientRect().width
@presenter.setScopedCharacterWidth(scopes, char, charWidth)
charIndex += charLength
@measuredLines.add(lineId)
clearMeasurements: ->
@measuredLines.clear()