Render line and line number decorations

This commit is contained in:
Nathan Sobo
2017-03-06 13:35:25 -07:00
committed by Antonio Scandurra
parent e15e7e3c96
commit ff325c0151
6 changed files with 227 additions and 31 deletions

View File

@@ -9,6 +9,7 @@ class DecorationManager {
this.emitter = new Emitter()
this.decorationCountsByLayer = new Map()
this.markerDecorationCountsByLayer = new Map()
this.decorationsByMarker = new Map()
this.layerDecorationsByMarkerLayer = new Map()
this.overlayDecorations = new Set()
@@ -80,6 +81,40 @@ class DecorationManager {
}
}
decorationPropertiesByMarkerForScreenRowRange (startScreenRow, endScreenRow) {
const decorationPropertiesByMarker = new Map()
this.decorationCountsByLayer.forEach((count, markerLayer) => {
const markers = markerLayer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow - 1]})
const layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
const hasMarkerDecorations = this.markerDecorationCountsByLayer.get(markerLayer) > 0
for (let i = 0; i < markers.length; i++) {
const marker = markers[i]
let decorationPropertiesForMarker = decorationPropertiesByMarker.get(marker)
if (decorationPropertiesForMarker == null) {
decorationPropertiesForMarker = []
decorationPropertiesByMarker.set(marker, decorationPropertiesForMarker)
}
if (layerDecorations) {
layerDecorations.forEach((layerDecoration) => {
decorationPropertiesForMarker.push(layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties())
})
}
if (hasMarkerDecorations) {
this.decorationsByMarker.get(marker).forEach((decoration) => {
decorationPropertiesForMarker.push(decoration.getProperties())
})
}
}
})
return decorationPropertiesByMarker
}
decorationsForScreenRowRange (startScreenRow, endScreenRow) {
const decorationsByMarkerId = {}
for (const layer of this.decorationCountsByLayer.keys()) {
@@ -118,7 +153,7 @@ class DecorationManager {
const layerDecorations = this.layerDecorationsByMarkerLayer.get(layer)
if (layerDecorations) {
layerDecorations.forEach((layerDecoration) => {
const properties = layerDecoration.overridePropertiesByMarkerId[marker.id] != null ? layerDecoration.overridePropertiesByMarkerId[marker.id] : layerDecoration.properties
const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties()
decorationsState[`${layerDecoration.id}-${marker.id}`] = {
properties,
screenRange,
@@ -155,7 +190,7 @@ class DecorationManager {
}
decorationsForMarker.add(decoration)
if (decoration.isType('overlay')) this.overlayDecorations.add(decoration)
this.observeDecoratedLayer(marker.layer)
this.observeDecoratedLayer(marker.layer, true)
this.emitDidUpdateDecorations()
this.emitter.emit('did-add-decoration', decoration)
return decoration
@@ -172,7 +207,7 @@ class DecorationManager {
this.layerDecorationsByMarkerLayer.set(markerLayer, layerDecorations)
}
layerDecorations.add(decoration)
this.observeDecoratedLayer(markerLayer)
this.observeDecoratedLayer(markerLayer, false)
this.emitDidUpdateDecorations()
return decoration
}
@@ -196,7 +231,7 @@ class DecorationManager {
decorations.delete(decoration)
if (decorations.size === 0) this.decorationsByMarker.delete(marker)
this.overlayDecorations.delete(decoration)
this.unobserveDecoratedLayer(marker.layer)
this.unobserveDecoratedLayer(marker.layer, true)
this.emitter.emit('did-remove-decoration', decoration)
this.emitDidUpdateDecorations()
}
@@ -211,20 +246,23 @@ class DecorationManager {
if (decorations.size === 0) {
this.layerDecorationsByMarkerLayer.delete(markerLayer)
}
this.unobserveDecoratedLayer(markerLayer)
this.unobserveDecoratedLayer(markerLayer, true)
this.emitDidUpdateDecorations()
}
}
observeDecoratedLayer (layer) {
observeDecoratedLayer (layer, isMarkerDecoration) {
const newCount = (this.decorationCountsByLayer.get(layer) || 0) + 1
this.decorationCountsByLayer.set(layer, newCount)
if (newCount === 1) {
this.layerUpdateDisposablesByLayer.set(layer, layer.onDidUpdate(this.emitDidUpdateDecorations.bind(this)))
}
if (isMarkerDecoration) {
this.markerDecorationCountsByLayer.set(layer, (this.markerDecorationCountsByLayer.get(layer) || 0) + 1)
}
}
unobserveDecoratedLayer (layer) {
unobserveDecoratedLayer (layer, isMarkerDecoration) {
const newCount = this.decorationCountsByLayer.get(layer) - 1
if (newCount === 0) {
this.layerUpdateDisposablesByLayer.get(layer).dispose()
@@ -232,5 +270,8 @@ class DecorationManager {
} else {
this.decorationCountsByLayer.set(layer, newCount)
}
if (isMarkerDecoration) {
this.markerDecorationCountsByLayer.set(this.markerDecorationCountsByLayer.get(layer) - 1)
}
}
}

View File

@@ -9,7 +9,7 @@ class LayerDecoration
@id = nextId()
@destroyed = false
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
@overridePropertiesByMarkerId = {}
@overridePropertiesByMarker = null
# Essential: Destroys the decoration.
destroy: ->
@@ -42,7 +42,7 @@ class LayerDecoration
setProperties: (newProperties) ->
return if @destroyed
@properties = newProperties
@decorationManager.scheduleUpdateDecorationsEvent()
@decorationManager.emitDidUpdateDecorations()
# Essential: Override the decoration properties for a specific marker.
#
@@ -52,8 +52,12 @@ class LayerDecoration
# Pass `null` to clear the override.
setPropertiesForMarker: (marker, properties) ->
return if @destroyed
@overridePropertiesByMarker ?= new Map()
if properties?
@overridePropertiesByMarkerId[marker.id] = properties
@overridePropertiesByMarker.set(marker, properties)
else
delete @overridePropertiesByMarkerId[marker.id]
@decorationManager.scheduleUpdateDecorationsEvent()
@overridePropertiesByMarker.delete(marker.id)
@decorationManager.emitDidUpdateDecorations()
getPropertiesForMarker: (marker) ->
@overridePropertiesByMarker?.get(marker)

View File

@@ -38,6 +38,10 @@ class TextEditorComponent {
this.lastKeydownBeforeKeypress = null
this.openedAccentedCharacterMenu = false
this.cursorsToRender = []
this.decorationsToRender = {
lineNumbers: new Map(),
lines: new Map()
}
if (this.props.model) this.observeModel()
resizeDetector.listenTo(this.element, this.didResize.bind(this))
@@ -74,6 +78,7 @@ class TextEditorComponent {
if (this.pendingAutoscroll) this.initiateAutoscroll()
this.populateVisibleRowRange()
const longestLineToMeasure = this.checkForNewLongestLine()
this.queryDecorationsToRender()
this.queryCursorsToRender()
etch.updateSync(this)
@@ -166,9 +171,11 @@ class TextEditorComponent {
if (this.measurements) {
const startRow = this.getRenderedStartRow()
const endRow = Math.min(model.getApproximateScreenLineCount(), this.getRenderedEndRow())
const bufferRows = new Array(endRow - startRow)
const foldableFlags = new Array(endRow - startRow)
const softWrappedFlags = new Array(endRow - startRow)
const visibleRowCount = endRow - startRow
const bufferRows = new Array(visibleRowCount)
const foldableFlags = new Array(visibleRowCount)
const softWrappedFlags = new Array(visibleRowCount)
const lineNumberDecorations = new Array(visibleRowCount)
let previousBufferRow = (startRow > 0) ? model.bufferRowForScreenRow(startRow - 1) : -1
for (let row = startRow; row < endRow; row++) {
@@ -177,17 +184,20 @@ class TextEditorComponent {
bufferRows[i] = bufferRow
softWrappedFlags[i] = bufferRow === previousBufferRow
foldableFlags[i] = model.isFoldableAtBufferRow(bufferRow)
lineNumberDecorations[i] = this.decorationsToRender.lineNumbers.get(row)
previousBufferRow = bufferRow
}
const rowsPerTile = this.getRowsPerTile()
this.currentFrameLineNumberGutterProps = {
ref: 'lineNumberGutter',
height: this.getScrollHeight(),
width: this.measurements.lineNumberGutterWidth,
lineHeight: this.measurements.lineHeight,
startRow, endRow, rowsPerTile, maxLineNumberDigits,
bufferRows, softWrappedFlags, foldableFlags
bufferRows, lineNumberDecorations, softWrappedFlags,
foldableFlags
}
return $(LineNumberGutterComponent, this.currentFrameLineNumberGutterProps)
@@ -265,12 +275,18 @@ class TextEditorComponent {
const tileHeight = rowsPerTile * this.measurements.lineHeight
const tileIndex = (tileStartRow / rowsPerTile) % visibleTileCount
const lineDecorations = new Array(rowsPerTile)
for (let row = tileStartRow; row < tileEndRow; row++) {
lineDecorations[row - tileStartRow] = this.decorationsToRender.lines.get(row)
}
tileNodes[tileIndex] = $(LinesTileComponent, {
key: tileIndex,
height: tileHeight,
width: tileWidth,
top: this.topPixelPositionForRow(tileStartRow),
screenLines: screenLines.slice(tileStartRow - startRow, tileEndRow - startRow),
lineDecorations,
displayLayer,
lineNodesByScreenLineId,
textNodesByScreenLineId
@@ -393,6 +409,52 @@ class TextEditorComponent {
}
}
queryDecorationsToRender () {
this.decorationsToRender.lineNumbers.clear()
this.decorationsToRender.lines.clear()
const decorationsByMarker =
this.getModel().decorationManager.decorationPropertiesByMarkerForScreenRowRange(
this.getRenderedStartRow(),
this.getRenderedEndRow()
)
decorationsByMarker.forEach((decorations, marker) => {
const screenRange = marker.getScreenRange()
const reversed = marker.isReversed()
for (let i = 0, length = decorations.length; i < decorations.length; i++) {
const decoration = decorations[i]
this.addToDecorationsToRender(decoration.type, decoration, screenRange, reversed)
}
})
}
addToDecorationsToRender (type, decoration, screenRange, reversed) {
if (Array.isArray(type)) {
for (let i = 0, length = type.length; i < length; i++) {
this.addToDecorationsToRender(type[i], decoration, screenRange, reversed)
}
} else {
switch (type) {
case 'line-number':
for (let row = screenRange.start.row; row <= screenRange.end.row; row++) {
const currentClassName = this.decorationsToRender.lineNumbers.get(row)
const newClassName = currentClassName ? currentClassName + ' ' + decoration.class : decoration.class
this.decorationsToRender.lineNumbers.set(row, newClassName)
}
break
case 'line':
for (let row = screenRange.start.row; row <= screenRange.end.row; row++) {
const currentClassName = this.decorationsToRender.lines.get(row)
const newClassName = currentClassName ? currentClassName + ' ' + decoration.class : decoration.class
this.decorationsToRender.lines.set(row, newClassName)
}
break
}
}
}
positionCursorsToRender () {
const height = this.measurements.lineHeight + 'px'
for (let i = 0; i < this.cursorsToRender.length; i++) {
@@ -878,6 +940,7 @@ class TextEditorComponent {
const scheduleUpdate = this.scheduleUpdate.bind(this)
this.disposables.add(model.selectionsMarkerLayer.onDidUpdate(scheduleUpdate))
this.disposables.add(model.displayLayer.onDidChangeSync(scheduleUpdate))
this.disposables.add(model.onDidUpdateDecorations(scheduleUpdate))
this.disposables.add(model.onDidRequestAutoscroll(this.didRequestAutoscroll.bind(this)))
}
@@ -1017,7 +1080,8 @@ class LineNumberGutterComponent {
render () {
const {
height, width, lineHeight, startRow, endRow, rowsPerTile,
maxLineNumberDigits, bufferRows, softWrappedFlags, foldableFlags
maxLineNumberDigits, bufferRows, softWrappedFlags, foldableFlags,
lineNumberDecorations
} = this.props
const visibleTileCount = Math.ceil((endRow - startRow) / rowsPerTile)
@@ -1046,6 +1110,10 @@ class LineNumberGutterComponent {
lineNumber = (bufferRow + 1).toString()
if (foldable) className += ' foldable'
}
const lineNumberDecoration = lineNumberDecorations[i]
if (lineNumberDecoration != null) className += ' ' + lineNumberDecoration
lineNumber = NBSP_CHARACTER.repeat(maxLineNumberDigits - lineNumber.length) + lineNumber
tileChildren[row - tileStartRow] = $.div({key, className},
@@ -1100,6 +1168,7 @@ class LineNumberGutterComponent {
if (!arraysEqual(oldProps.bufferRows, newProps.bufferRows)) return true
if (!arraysEqual(oldProps.softWrappedFlags, newProps.softWrappedFlags)) return true
if (!arraysEqual(oldProps.foldableFlags, newProps.foldableFlags)) return true
if (!arraysEqual(oldProps.lineNumberDecorations, newProps.lineNumberDecorations)) return true
return false
}
}
@@ -1120,8 +1189,8 @@ class LinesTileComponent {
render () {
const {
height, width, top,
screenLines, displayLayer,
lineNodesByScreenLineId, textNodesByScreenLineId
screenLines, lineDecorations, displayLayer,
lineNodesByScreenLineId, textNodesByScreenLineId,
} = this.props
const children = new Array(screenLines.length)
@@ -1134,6 +1203,7 @@ class LinesTileComponent {
children[i] = $(LineComponent, {
key: screenLine.id,
screenLine,
lineDecoration: lineDecorations[i],
displayLayer,
lineNodesByScreenLineId,
textNodesByScreenLineId
@@ -1159,16 +1229,17 @@ class LinesTileComponent {
if (oldProps.height !== newProps.height) return true
if (oldProps.width !== newProps.width) return true
if (!arraysEqual(oldProps.screenLines, newProps.screenLines)) return true
if (!arraysEqual(oldProps.lineDecorations, newProps.lineDecorations)) return true
return false
}
}
class LineComponent {
constructor (props) {
const {displayLayer, screenLine, lineNodesByScreenLineId, textNodesByScreenLineId} = props
const {displayLayer, screenLine, lineDecoration, lineNodesByScreenLineId, textNodesByScreenLineId} = props
this.props = props
this.element = document.createElement('div')
this.element.classList.add('line')
this.element.className = this.buildClassName()
lineNodesByScreenLineId.set(screenLine.id, this.element)
const textNodes = []
@@ -1214,7 +1285,12 @@ class LineComponent {
}
}
update () {}
update (newProps) {
if (this.props.lineDecoration !== newProps.lineDecoration) {
this.props = newProps
this.element.className = this.buildClassName()
}
}
destroy () {
const {lineNodesByScreenLineId, textNodesByScreenLineId, screenLine} = this.props
@@ -1223,6 +1299,13 @@ class LineComponent {
textNodesByScreenLineId.delete(screenLine.id)
}
}
buildClassName () {
const {lineDecoration} = this.props
let className = 'line'
if (lineDecoration != null) className += ' ' + lineDecoration
return className
}
}
const classNamesByScopeName = new Map()

View File

@@ -1849,9 +1849,6 @@ class TextEditor extends Model
getOverlayDecorations: (propertyFilter) ->
@decorationManager.getOverlayDecorations(propertyFilter)
decorationForId: (id) ->
@decorationManager.decorationForId(id)
###
Section: Markers
###