Merge pull request #9930 from atom/as-block-decorations

Block Decorations
This commit is contained in:
Antonio Scandurra
2016-01-14 13:20:00 -07:00
14 changed files with 1352 additions and 58 deletions

View File

@@ -0,0 +1,72 @@
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
clone
module.exports =
class BlockDecorationsComponent
constructor: (@container, @views, @presenter, @domElementPool) ->
@newState = null
@oldState = null
@blockDecorationNodesById = {}
@domNode = @domElementPool.buildElement("content")
@domNode.setAttribute("select", ".atom--invisible-block-decoration")
@domNode.style.visibility = "hidden"
getDomNode: ->
@domNode
updateSync: (state) ->
@newState = state.content
@oldState ?= {blockDecorations: {}, width: 0}
if @newState.width isnt @oldState.width
@domNode.style.width = @newState.width + "px"
@oldState.width = @newState.width
for id, blockDecorationState of @oldState.blockDecorations
unless @newState.blockDecorations.hasOwnProperty(id)
@blockDecorationNodesById[id].remove()
delete @blockDecorationNodesById[id]
delete @oldState.blockDecorations[id]
for id, blockDecorationState of @newState.blockDecorations
if @oldState.blockDecorations.hasOwnProperty(id)
@updateBlockDecorationNode(id)
else
@oldState.blockDecorations[id] = {}
@createAndAppendBlockDecorationNode(id)
measureBlockDecorations: ->
for decorationId, blockDecorationNode of @blockDecorationNodesById
style = getComputedStyle(blockDecorationNode)
decoration = @newState.blockDecorations[decorationId].decoration
marginBottom = parseInt(style.marginBottom) ? 0
marginTop = parseInt(style.marginTop) ? 0
@presenter.setBlockDecorationDimensions(
decoration,
blockDecorationNode.offsetWidth,
blockDecorationNode.offsetHeight + marginTop + marginBottom
)
createAndAppendBlockDecorationNode: (id) ->
blockDecorationState = @newState.blockDecorations[id]
blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item)
blockDecorationNode.id = "atom--block-decoration-#{id}"
@container.appendChild(blockDecorationNode)
@blockDecorationNodesById[id] = blockDecorationNode
@updateBlockDecorationNode(id)
updateBlockDecorationNode: (id) ->
newBlockDecorationState = @newState.blockDecorations[id]
oldBlockDecorationState = @oldState.blockDecorations[id]
blockDecorationNode = @blockDecorationNodesById[id]
if newBlockDecorationState.isVisible
blockDecorationNode.classList.remove("atom--invisible-block-decoration")
else
blockDecorationNode.classList.add("atom--invisible-block-decoration")
if oldBlockDecorationState.screenRow isnt newBlockDecorationState.screenRow
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
oldBlockDecorationState.screenRow = newBlockDecorationState.screenRow

View File

@@ -35,7 +35,6 @@ translateDecorationParamsOldToNew = (decorationParams) ->
# the marker.
module.exports =
class Decoration
# Private: Check if the `decorationProperties.type` matches `type`
#
# * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
@@ -154,6 +153,13 @@ class Decoration
@displayBuffer.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
###
Section: Utility
###
inspect: ->
"<Decoration #{@id}>"
###
Section: Private methods
###

View File

@@ -96,12 +96,13 @@ class LineNumbersTileComponent
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
buildLineNumberNode: (lineNumberState) ->
{screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex} = lineNumberState
{screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex, blockDecorationsHeight} = lineNumberState
className = @buildLineNumberClassName(lineNumberState)
lineNumberNode = @domElementPool.buildElement("div", className)
lineNumberNode.dataset.screenRow = screenRow
lineNumberNode.dataset.bufferRow = bufferRow
lineNumberNode.style.marginTop = blockDecorationsHeight + "px"
@setLineNumberInnerNodes(bufferRow, softWrapped, lineNumberNode)
lineNumberNode
@@ -139,6 +140,10 @@ class LineNumbersTileComponent
oldLineNumberState.screenRow = newLineNumberState.screenRow
oldLineNumberState.bufferRow = newLineNumberState.bufferRow
unless oldLineNumberState.blockDecorationsHeight is newLineNumberState.blockDecorationsHeight
node.style.marginTop = newLineNumberState.blockDecorationsHeight + "px"
oldLineNumberState.blockDecorationsHeight = newLineNumberState.blockDecorationsHeight
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) ->
className = "line-number"
className += " " + decorationClasses.join(' ') if decorationClasses?

View File

@@ -20,6 +20,8 @@ class LinesTileComponent
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@textNodesByLineId = {}
@insertionPointsBeforeLineById = {}
@insertionPointsAfterLineById = {}
@domNode = @domElementPool.buildElement("div")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@@ -80,6 +82,9 @@ class LinesTileComponent
removeLineNode: (id) ->
@domElementPool.freeElementAndDescendants(@lineNodesByLineId[id])
@removeBlockDecorationInsertionPointBeforeLine(id)
@removeBlockDecorationInsertionPointAfterLine(id)
delete @lineNodesByLineId[id]
delete @textNodesByLineId[id]
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
@@ -116,6 +121,71 @@ class LinesTileComponent
else
@domNode.appendChild(lineNode)
@insertBlockDecorationInsertionPointBeforeLine(id)
@insertBlockDecorationInsertionPointAfterLine(id)
removeBlockDecorationInsertionPointBeforeLine: (id) ->
if insertionPoint = @insertionPointsBeforeLineById[id]
@domElementPool.freeElementAndDescendants(insertionPoint)
delete @insertionPointsBeforeLineById[id]
insertBlockDecorationInsertionPointBeforeLine: (id) ->
{hasPrecedingBlockDecorations, screenRow} = @newTileState.lines[id]
if hasPrecedingBlockDecorations
lineNode = @lineNodesByLineId[id]
insertionPoint = @domElementPool.buildElement("content")
@domNode.insertBefore(insertionPoint, lineNode)
@insertionPointsBeforeLineById[id] = insertionPoint
insertionPoint.dataset.screenRow = screenRow
@updateBlockDecorationInsertionPointBeforeLine(id)
updateBlockDecorationInsertionPointBeforeLine: (id) ->
oldLineState = @oldTileState.lines[id]
newLineState = @newTileState.lines[id]
insertionPoint = @insertionPointsBeforeLineById[id]
return unless insertionPoint?
if newLineState.screenRow isnt oldLineState.screenRow
insertionPoint.dataset.screenRow = newLineState.screenRow
precedingBlockDecorationsSelector = newLineState.precedingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',')
if precedingBlockDecorationsSelector isnt oldLineState.precedingBlockDecorationsSelector
insertionPoint.setAttribute("select", precedingBlockDecorationsSelector)
oldLineState.precedingBlockDecorationsSelector = precedingBlockDecorationsSelector
removeBlockDecorationInsertionPointAfterLine: (id) ->
if insertionPoint = @insertionPointsAfterLineById[id]
@domElementPool.freeElementAndDescendants(insertionPoint)
delete @insertionPointsAfterLineById[id]
insertBlockDecorationInsertionPointAfterLine: (id) ->
{hasFollowingBlockDecorations, screenRow} = @newTileState.lines[id]
if hasFollowingBlockDecorations
lineNode = @lineNodesByLineId[id]
insertionPoint = @domElementPool.buildElement("content")
@domNode.insertBefore(insertionPoint, lineNode.nextSibling)
@insertionPointsAfterLineById[id] = insertionPoint
insertionPoint.dataset.screenRow = screenRow
@updateBlockDecorationInsertionPointAfterLine(id)
updateBlockDecorationInsertionPointAfterLine: (id) ->
oldLineState = @oldTileState.lines[id]
newLineState = @newTileState.lines[id]
insertionPoint = @insertionPointsAfterLineById[id]
return unless insertionPoint?
if newLineState.screenRow isnt oldLineState.screenRow
insertionPoint.dataset.screenRow = newLineState.screenRow
followingBlockDecorationsSelector = newLineState.followingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',')
if followingBlockDecorationsSelector isnt oldLineState.followingBlockDecorationsSelector
insertionPoint.setAttribute("select", followingBlockDecorationsSelector)
oldLineState.followingBlockDecorationsSelector = followingBlockDecorationsSelector
findNodeNextTo: (node) ->
for nextNode, index in @domNode.children
continue if index is 0 # skips highlights node
@@ -336,12 +406,28 @@ class LinesTileComponent
oldLineState.decorationClasses = newLineState.decorationClasses
if not oldLineState.hasPrecedingBlockDecorations and newLineState.hasPrecedingBlockDecorations
@insertBlockDecorationInsertionPointBeforeLine(id)
else if oldLineState.hasPrecedingBlockDecorations and not newLineState.hasPrecedingBlockDecorations
@removeBlockDecorationInsertionPointBeforeLine(id)
if not oldLineState.hasFollowingBlockDecorations and newLineState.hasFollowingBlockDecorations
@insertBlockDecorationInsertionPointAfterLine(id)
else if oldLineState.hasFollowingBlockDecorations and not newLineState.hasFollowingBlockDecorations
@removeBlockDecorationInsertionPointAfterLine(id)
if newLineState.screenRow isnt oldLineState.screenRow
lineNode.dataset.screenRow = newLineState.screenRow
oldLineState.screenRow = newLineState.screenRow
@lineIdsByScreenRow[newLineState.screenRow] = id
@screenRowsByLineId[id] = newLineState.screenRow
@updateBlockDecorationInsertionPointBeforeLine(id)
@updateBlockDecorationInsertionPointAfterLine(id)
oldLineState.screenRow = newLineState.screenRow
oldLineState.hasPrecedingBlockDecorations = newLineState.hasPrecedingBlockDecorations
oldLineState.hasFollowingBlockDecorations = newLineState.hasFollowingBlockDecorations
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]

View File

@@ -3,7 +3,7 @@ TokenIterator = require './token-iterator'
module.exports =
class LinesYardstick
constructor: (@model, @lineNodesProvider, grammarRegistry) ->
constructor: (@model, @lineNodesProvider, @lineTopIndex, grammarRegistry) ->
@tokenIterator = new TokenIterator({grammarRegistry})
@rangeForMeasurement = document.createRange()
@invalidateCache()
@@ -20,8 +20,8 @@ class LinesYardstick
targetTop = pixelPosition.top
targetLeft = pixelPosition.left
defaultCharWidth = @model.getDefaultCharWidth()
row = Math.floor(targetTop / @model.getLineHeightInPixels())
targetLeft = 0 if row < 0
row = @lineTopIndex.rowForPixelPosition(targetTop)
targetLeft = 0 if targetTop < 0
targetLeft = Infinity if row > @model.getLastScreenRow()
row = Math.min(row, @model.getLastScreenRow())
row = Math.max(0, row)
@@ -81,7 +81,7 @@ class LinesYardstick
targetRow = screenPosition.row
targetColumn = screenPosition.column
top = targetRow * @model.getLineHeightInPixels()
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
left = @leftPixelPositionForScreenPosition(targetRow, targetColumn)
{top, left}

View File

@@ -13,6 +13,8 @@ ScrollbarCornerComponent = require './scrollbar-corner-component'
OverlayManager = require './overlay-manager'
DOMElementPool = require './dom-element-pool'
LinesYardstick = require './lines-yardstick'
BlockDecorationsComponent = require './block-decorations-component'
LineTopIndex = require 'line-top-index'
module.exports =
class TextEditorComponent
@@ -48,6 +50,9 @@ class TextEditorComponent
@observeConfig()
@setScrollSensitivity(@config.get('editor.scrollSensitivity'))
lineTopIndex = new LineTopIndex({
defaultLineHeight: @editor.getLineHeightInPixels()
})
@presenter = new TextEditorPresenter
model: @editor
tileSize: tileSize
@@ -55,11 +60,11 @@ class TextEditorComponent
cursorBlinkResumeDelay: @cursorBlinkResumeDelay
stoppedScrollingDelay: 200
config: @config
lineTopIndex: lineTopIndex
@presenter.onDidUpdateState(@requestUpdate)
@domElementPool = new DOMElementPool
@domNode = document.createElement('div')
if @useShadowDOM
@domNode.classList.add('editor-contents--private')
@@ -68,6 +73,7 @@ class TextEditorComponent
insertionPoint.setAttribute('select', 'atom-overlay')
@domNode.appendChild(insertionPoint)
@overlayManager = new OverlayManager(@presenter, @hostElement, @views)
@blockDecorationsComponent = new BlockDecorationsComponent(@hostElement, @views, @presenter, @domElementPool)
else
@domNode.classList.add('editor-contents')
@overlayManager = new OverlayManager(@presenter, @domNode, @views)
@@ -82,7 +88,10 @@ class TextEditorComponent
@linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars})
@scrollViewNode.appendChild(@linesComponent.getDomNode())
@linesYardstick = new LinesYardstick(@editor, @linesComponent, @grammars)
if @blockDecorationsComponent?
@linesComponent.getDomNode().appendChild(@blockDecorationsComponent.getDomNode())
@linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex, @grammars)
@presenter.setLinesYardstick(@linesYardstick)
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
@@ -158,6 +167,7 @@ class TextEditorComponent
@hiddenInputComponent.updateSync(@newState)
@linesComponent.updateSync(@newState)
@blockDecorationsComponent?.updateSync(@newState)
@horizontalScrollbarComponent.updateSync(@newState)
@verticalScrollbarComponent.updateSync(@newState)
@scrollbarCornerComponent.updateSync(@newState)
@@ -177,6 +187,7 @@ class TextEditorComponent
readAfterUpdateSync: =>
@overlayManager?.measureOverlays()
@blockDecorationsComponent?.measureBlockDecorations() if @isVisible()
mountGutterContainerComponent: ->
@gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views})
@@ -279,13 +290,13 @@ class TextEditorComponent
observeConfig: ->
@disposables.add @config.onDidChange 'editor.fontSize', =>
@sampleFontStyling()
@invalidateCharacterWidths()
@invalidateMeasurements()
@disposables.add @config.onDidChange 'editor.fontFamily', =>
@sampleFontStyling()
@invalidateCharacterWidths()
@invalidateMeasurements()
@disposables.add @config.onDidChange 'editor.lineHeight', =>
@sampleFontStyling()
@invalidateCharacterWidths()
@invalidateMeasurements()
onGrammarChanged: =>
if @scopedConfigDisposables?
@@ -485,6 +496,9 @@ class TextEditorComponent
@editor.screenPositionForBufferPosition(bufferPosition)
)
invalidateBlockDecorationDimensions: ->
@presenter.invalidateBlockDecorationDimensions(arguments...)
onMouseDown: (event) =>
unless event.button is 0 or (event.button is 1 and process.platform is 'linux')
# Only handle mouse down events for left mouse button on all platforms
@@ -602,7 +616,7 @@ class TextEditorComponent
handleStylingChange: =>
@sampleFontStyling()
@sampleBackgroundColors()
@invalidateCharacterWidths()
@invalidateMeasurements()
handleDragUntilMouseUp: (dragHandler) ->
dragging = false
@@ -756,7 +770,7 @@ class TextEditorComponent
if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
@clearPoolAfterUpdate = true
@measureLineHeightAndDefaultCharWidth()
@invalidateCharacterWidths()
@invalidateMeasurements()
sampleBackgroundColors: (suppressUpdate) ->
{backgroundColor} = getComputedStyle(@hostElement)
@@ -866,7 +880,7 @@ class TextEditorComponent
setFontSize: (fontSize) ->
@getTopmostDOMNode().style.fontSize = fontSize + 'px'
@sampleFontStyling()
@invalidateCharacterWidths()
@invalidateMeasurements()
getFontFamily: ->
getComputedStyle(@getTopmostDOMNode()).fontFamily
@@ -874,16 +888,16 @@ class TextEditorComponent
setFontFamily: (fontFamily) ->
@getTopmostDOMNode().style.fontFamily = fontFamily
@sampleFontStyling()
@invalidateCharacterWidths()
@invalidateMeasurements()
setLineHeight: (lineHeight) ->
@getTopmostDOMNode().style.lineHeight = lineHeight
@sampleFontStyling()
@invalidateCharacterWidths()
@invalidateMeasurements()
invalidateCharacterWidths: ->
invalidateMeasurements: ->
@linesYardstick.invalidateCache()
@presenter.characterWidthsChanged()
@presenter.measurementsChanged()
setShowIndentGuide: (showIndentGuide) ->
@config.set("editor.showIndentGuide", showIndentGuide)

View File

@@ -347,4 +347,13 @@ class TextEditorElement extends HTMLElement
getHeight: ->
@offsetHeight
# Experimental: Invalidate the passed block {Decoration} dimensions, forcing
# them to be recalculated and the surrounding content to be adjusted on the
# next animation frame.
#
# * {blockDecoration} A {Decoration} representing the block decoration you
# want to update the dimensions of.
invalidateBlockDecorationDimensions: ->
@component.invalidateBlockDecorationDimensions(arguments...)
module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype

View File

@@ -13,7 +13,7 @@ class TextEditorPresenter
minimumReflowInterval: 200
constructor: (params) ->
{@model, @config} = params
{@model, @config, @lineTopIndex} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params
{@contentFrameWidth} = params
@@ -28,6 +28,9 @@ class TextEditorPresenter
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@customGutterDecorationsByGutterName = {}
@observedBlockDecorations = new Set()
@invalidatedDimensionsByBlockDecoration = new Set()
@invalidateAllBlockDecorationsDimensions = false
@screenRowsToMeasure = []
@transferMeasurementsToModel()
@transferMeasurementsFromModel()
@@ -85,6 +88,7 @@ class TextEditorPresenter
if @shouldUpdateDecorations
@fetchDecorations()
@updateLineDecorations()
@updateBlockDecorations()
@updateTilesState()
@@ -126,7 +130,8 @@ class TextEditorPresenter
@shouldUpdateDecorations = true
observeModel: ->
@disposables.add @model.onDidChange =>
@disposables.add @model.onDidChange ({start, end, screenDelta}) =>
@spliceBlockDecorationsInRange(start, end, screenDelta)
@shouldUpdateDecorations = true
@emitDidUpdateState()
@@ -134,6 +139,11 @@ class TextEditorPresenter
@shouldUpdateDecorations = true
@emitDidUpdateState()
@disposables.add @model.onDidAddDecoration(@didAddBlockDecoration.bind(this))
for decoration in @model.getDecorations({type: 'block'})
this.didAddBlockDecoration(decoration)
@disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this))
@disposables.add @model.onDidChangePlaceholderText(@emitDidUpdateState.bind(this))
@disposables.add @model.onDidChangeMini =>
@@ -192,6 +202,7 @@ class TextEditorPresenter
highlights: {}
overlays: {}
cursors: {}
blockDecorations: {}
gutters: []
# Shared state that is copied into ``@state.gutters`.
@sharedGutterStyles = {}
@@ -327,6 +338,7 @@ class TextEditorPresenter
zIndex = 0
for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize
tileEndRow = @constrainRow(tileStartRow + @tileSize)
rowsWithinTile = []
while screenRowIndex >= 0
@@ -337,17 +349,21 @@ class TextEditorPresenter
continue if rowsWithinTile.length is 0
top = Math.round(@lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow))
bottom = Math.round(@lineTopIndex.pixelPositionBeforeBlocksForRow(tileEndRow))
height = bottom - top
tile = @state.content.tiles[tileStartRow] ?= {}
tile.top = tileStartRow * @lineHeight - @scrollTop
tile.top = top - @scrollTop
tile.left = -@scrollLeft
tile.height = @tileSize * @lineHeight
tile.height = height
tile.display = "block"
tile.zIndex = zIndex
tile.highlights ?= {}
gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {}
gutterTile.top = tileStartRow * @lineHeight - @scrollTop
gutterTile.height = @tileSize * @lineHeight
gutterTile.top = top - @scrollTop
gutterTile.height = height
gutterTile.display = "block"
gutterTile.zIndex = zIndex
@@ -380,10 +396,16 @@ class TextEditorPresenter
throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}")
visibleLineIds[line.id] = true
precedingBlockDecorations = @precedingBlockDecorationsByScreenRow[screenRow] ? []
followingBlockDecorations = @followingBlockDecorationsByScreenRow[screenRow] ? []
if tileState.lines.hasOwnProperty(line.id)
lineState = tileState.lines[line.id]
lineState.screenRow = screenRow
lineState.decorationClasses = @lineDecorationClassesForRow(screenRow)
lineState.precedingBlockDecorations = precedingBlockDecorations
lineState.followingBlockDecorations = followingBlockDecorations
lineState.hasPrecedingBlockDecorations = precedingBlockDecorations.length > 0
lineState.hasFollowingBlockDecorations = followingBlockDecorations.length > 0
else
tileState.lines[line.id] =
screenRow: screenRow
@@ -400,6 +422,10 @@ class TextEditorPresenter
tabLength: line.tabLength
fold: line.fold
decorationClasses: @lineDecorationClassesForRow(screenRow)
precedingBlockDecorations: precedingBlockDecorations
followingBlockDecorations: followingBlockDecorations
hasPrecedingBlockDecorations: precedingBlockDecorations.length > 0
hasFollowingBlockDecorations: followingBlockDecorations.length > 0
for id, line of tileState.lines
delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id)
@@ -536,9 +562,11 @@ class TextEditorPresenter
continue unless @gutterIsVisible(gutter)
for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName]
top = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRange.start.row)
bottom = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRange.end.row + 1)
@customGutterDecorations[gutterName][decorationId] =
top: @lineHeight * screenRange.start.row
height: @lineHeight * screenRange.getRowCount()
top: top
height: bottom - top
item: properties.item
class: properties.class
@@ -586,8 +614,13 @@ class TextEditorPresenter
line = @model.tokenizedLineForScreenRow(screenRow)
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
foldable = @model.isFoldableAtScreenRow(screenRow)
blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow)
blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight
if screenRow % @tileSize isnt 0
blockDecorationsAfterPreviousScreenRowHeight = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) - @lineHeight - @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow - 1)
blockDecorationsHeight += blockDecorationsAfterPreviousScreenRowHeight
tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable}
tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight}
visibleLineNumberIds[line.id] = true
for id of tileState.lineNumbers
@@ -598,16 +631,15 @@ class TextEditorPresenter
updateStartRow: ->
return unless @scrollTop? and @lineHeight?
startRow = Math.floor(@scrollTop / @lineHeight)
@startRow = Math.max(0, startRow)
@startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop))
updateEndRow: ->
return unless @scrollTop? and @lineHeight? and @height?
startRow = Math.max(0, Math.floor(@scrollTop / @lineHeight))
visibleLinesCount = Math.ceil(@height / @lineHeight) + 1
endRow = startRow + visibleLinesCount
@endRow = Math.min(@model.getScreenLineCount(), endRow)
@endRow = Math.min(
@model.getScreenLineCount(),
@lineTopIndex.rowForPixelPosition(@scrollTop + @height + @lineHeight - 1) + 1
)
updateRowsPerPage: ->
rowsPerPage = Math.floor(@getClientHeight() / @lineHeight)
@@ -639,7 +671,7 @@ class TextEditorPresenter
updateVerticalDimensions: ->
if @lineHeight?
oldContentHeight = @contentHeight
@contentHeight = @lineHeight * @model.getScreenLineCount()
@contentHeight = Math.round(@lineTopIndex.pixelPositionAfterBlocksForRow(@model.getScreenLineCount()))
if @contentHeight isnt oldContentHeight
@updateHeight()
@@ -806,6 +838,7 @@ class TextEditorPresenter
didStopScrolling: ->
if @mouseWheelScreenRow?
@mouseWheelScreenRow = null
@shouldUpdateDecorations = true
@emitDidUpdateState()
@@ -898,12 +931,15 @@ class TextEditorPresenter
@editorWidthInChars = null
@updateScrollbarDimensions()
@updateClientWidth()
@invalidateAllBlockDecorationsDimensions = true
@shouldUpdateDecorations = true
@emitDidUpdateState()
setBoundingClientRect: (boundingClientRect) ->
unless @clientRectsEqual(@boundingClientRect, boundingClientRect)
@boundingClientRect = boundingClientRect
@invalidateAllBlockDecorationsDimensions = true
@shouldUpdateDecorations = true
@emitDidUpdateState()
clientRectsEqual: (clientRectA, clientRectB) ->
@@ -917,6 +953,8 @@ class TextEditorPresenter
if @windowWidth isnt width or @windowHeight isnt height
@windowWidth = width
@windowHeight = height
@invalidateAllBlockDecorationsDimensions = true
@shouldUpdateDecorations = true
@emitDidUpdateState()
@@ -941,6 +979,8 @@ class TextEditorPresenter
setLineHeight: (lineHeight) ->
unless @lineHeight is lineHeight
@lineHeight = lineHeight
@model.setLineHeightInPixels(@lineHeight)
@lineTopIndex.setDefaultLineHeight(@lineHeight)
@restoreScrollTopIfNeeded()
@model.setLineHeightInPixels(lineHeight)
@shouldUpdateDecorations = true
@@ -959,9 +999,10 @@ class TextEditorPresenter
@koreanCharWidth = koreanCharWidth
@model.setDefaultCharWidth(baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
@restoreScrollLeftIfNeeded()
@characterWidthsChanged()
@measurementsChanged()
characterWidthsChanged: ->
measurementsChanged: ->
@invalidateAllBlockDecorationsDimensions = true
@shouldUpdateDecorations = true
@emitDidUpdateState()
@@ -1014,6 +1055,43 @@ class TextEditorPresenter
return unless 0 <= @startRow <= @endRow <= Infinity
@decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1)
updateBlockDecorations: ->
@blockDecorationsToRenderById = {}
@precedingBlockDecorationsByScreenRow = {}
@followingBlockDecorationsByScreenRow = {}
visibleDecorationsByMarkerId = @model.decorationsForScreenRowRange(@getStartTileRow(), @getEndTileRow() + @tileSize - 1)
if @invalidateAllBlockDecorationsDimensions
for decoration in @model.getDecorations(type: 'block')
@invalidatedDimensionsByBlockDecoration.add(decoration)
@invalidateAllBlockDecorationsDimensions = false
for markerId, decorations of visibleDecorationsByMarkerId
for decoration in decorations when decoration.isType('block')
@updateBlockDecorationState(decoration, true)
@invalidatedDimensionsByBlockDecoration.forEach (decoration) =>
@updateBlockDecorationState(decoration, false)
for decorationId, decorationState of @state.content.blockDecorations
continue if @blockDecorationsToRenderById[decorationId]
continue if decorationState.screenRow is @mouseWheelScreenRow
delete @state.content.blockDecorations[decorationId]
updateBlockDecorationState: (decoration, isVisible) ->
return if @blockDecorationsToRenderById[decoration.getId()]
screenRow = decoration.getMarker().getHeadScreenPosition().row
if decoration.getProperties().position is "before"
@precedingBlockDecorationsByScreenRow[screenRow] ?= []
@precedingBlockDecorationsByScreenRow[screenRow].push(decoration)
else
@followingBlockDecorationsByScreenRow[screenRow] ?= []
@followingBlockDecorationsByScreenRow[screenRow].push(decoration)
@state.content.blockDecorations[decoration.getId()] = {decoration, screenRow, isVisible}
@blockDecorationsToRenderById[decoration.getId()] = true
updateLineDecorations: ->
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@@ -1136,7 +1214,7 @@ class TextEditorPresenter
screenRange.end.column = 0
repositionRegionWithinTile: (region, tileStartRow) ->
region.top += @scrollTop - tileStartRow * @lineHeight
region.top += @scrollTop - @lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow)
region.left += @scrollLeft
buildHighlightRegions: (screenRange) ->
@@ -1206,6 +1284,73 @@ class TextEditorPresenter
@emitDidUpdateState()
setBlockDecorationDimensions: (decoration, width, height) ->
return unless @observedBlockDecorations.has(decoration)
@lineTopIndex.resizeBlock(decoration.getId(), height)
@invalidatedDimensionsByBlockDecoration.delete(decoration)
@shouldUpdateDecorations = true
@emitDidUpdateState()
invalidateBlockDecorationDimensions: (decoration) ->
@invalidatedDimensionsByBlockDecoration.add(decoration)
@shouldUpdateDecorations = true
@emitDidUpdateState()
spliceBlockDecorationsInRange: (start, end, screenDelta) ->
return if screenDelta is 0
oldExtent = end - start
newExtent = end - start + screenDelta
invalidatedBlockDecorationIds = @lineTopIndex.splice(start, oldExtent, newExtent)
invalidatedBlockDecorationIds.forEach (id) =>
decoration = @model.decorationForId(id)
newScreenPosition = decoration.getMarker().getHeadScreenPosition()
@lineTopIndex.moveBlock(id, newScreenPosition.row)
@invalidatedDimensionsByBlockDecoration.add(decoration)
didAddBlockDecoration: (decoration) ->
return if not decoration.isType('block') or @observedBlockDecorations.has(decoration)
didMoveDisposable = decoration.getMarker().bufferMarker.onDidChange (markerEvent) =>
@didMoveBlockDecoration(decoration, markerEvent)
didDestroyDisposable = decoration.onDidDestroy =>
@disposables.remove(didMoveDisposable)
@disposables.remove(didDestroyDisposable)
didMoveDisposable.dispose()
didDestroyDisposable.dispose()
@didDestroyBlockDecoration(decoration)
isAfter = decoration.getProperties().position is "after"
@lineTopIndex.insertBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition().row, 0, isAfter)
@observedBlockDecorations.add(decoration)
@invalidateBlockDecorationDimensions(decoration)
@disposables.add(didMoveDisposable)
@disposables.add(didDestroyDisposable)
@shouldUpdateDecorations = true
@emitDidUpdateState()
didMoveBlockDecoration: (decoration, markerEvent) ->
# Don't move blocks after a text change, because we already splice on buffer
# change.
return if markerEvent.textChanged
@lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition().row)
@shouldUpdateDecorations = true
@emitDidUpdateState()
didDestroyBlockDecoration: (decoration) ->
return unless @observedBlockDecorations.has(decoration)
@lineTopIndex.removeBlock(decoration.getId())
@observedBlockDecorations.delete(decoration)
@invalidatedDimensionsByBlockDecoration.delete(decoration)
@shouldUpdateDecorations = true
@emitDidUpdateState()
observeCursor: (cursor) ->
didChangePositionDisposable = cursor.onDidChangePosition =>
@pauseCursorBlinking()
@@ -1266,7 +1411,7 @@ class TextEditorPresenter
@emitDidUpdateState()
didChangeFirstVisibleScreenRow: (screenRow) ->
@setScrollTop(screenRow * @lineHeight)
@setScrollTop(@lineTopIndex.pixelPositionAfterBlocksForRow(screenRow))
getVerticalScrollMarginInPixels: ->
Math.round(@model.getVerticalScrollMargin() * @lineHeight)
@@ -1287,8 +1432,8 @@ class TextEditorPresenter
verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels()
top = screenRange.start.row * @lineHeight
bottom = (screenRange.end.row + 1) * @lineHeight
top = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRange.start.row)
bottom = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRange.end.row) + @lineHeight
if options?.center
desiredScrollCenter = (top + bottom) / 2
@@ -1360,7 +1505,7 @@ class TextEditorPresenter
restoreScrollTopIfNeeded: ->
unless @scrollTop?
@updateScrollTop(@model.getFirstVisibleScreenRow() * @lineHeight)
@updateScrollTop(@lineTopIndex.pixelPositionAfterBlocksForRow(@model.getFirstVisibleScreenRow()))
restoreScrollLeftIfNeeded: ->
unless @scrollLeft?

View File

@@ -1437,6 +1437,8 @@ class TextEditor extends Model
# * __gutter__: A decoration that tracks a {TextEditorMarker} in a {Gutter}. Gutter
# decorations are created by calling {Gutter::decorateMarker} on the
# desired `Gutter` instance.
# * __block__: Positions the view associated with the given item before or
# after the row of the given `TextEditorMarker`.
#
# ## Arguments
#
@@ -1456,11 +1458,14 @@ class TextEditor extends Model
# property.
# * `gutter` Tracks a {TextEditorMarker} in a {Gutter}. Created by calling
# {Gutter::decorateMarker} on the desired `Gutter` instance.
# * `block` Positions the view associated with the given item before or
# after the row of the given `TextEditorMarker`, depending on the `position`
# property.
# * `class` This CSS class will be applied to the decorated line number,
# line, highlight, or overlay.
# * `item` (optional) An {HTMLElement} or a model {Object} with a
# corresponding view registered. Only applicable to the `gutter` and
# `overlay` types.
# corresponding view registered. Only applicable to the `gutter`,
# `overlay` and `block` types.
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
# the head of the `TextEditorMarker`. Only applicable to the `line` and
# `line-number` types.
@@ -1470,9 +1475,10 @@ class TextEditor extends Model
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
# if the associated `TextEditorMarker` is non-empty. Only applicable to the
# `gutter`, `line`, and `line-number` types.
# * `position` (optional) Only applicable to decorations of type `overlay`,
# controls where the overlay view is positioned relative to the `TextEditorMarker`.
# Values can be `'head'` (the default), or `'tail'`.
# * `position` (optional) Only applicable to decorations of type `overlay` and `block`,
# controls where the view is positioned relative to the `TextEditorMarker`.
# Values can be `'head'` (the default) or `'tail'` for overlay decorations, and
# `'before'` (the default) or `'after'` for block decorations.
#
# Returns a {Decoration} object
decorateMarker: (marker, decorationParams) ->