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

@@ -28,7 +28,6 @@ describe "DecorationManager", ->
it "can add decorations associated with markers and remove them", ->
expect(layer1MarkerDecoration).toBeDefined()
expect(layer1MarkerDecoration.getProperties()).toBe decorationProperties
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).toBe layer1MarkerDecoration
expect(decorationManager.decorationsForScreenRowRange(2, 3)).toEqual {
"#{layer1Marker.id}": [layer1MarkerDecoration],
"#{layer2Marker.id}": [layer2MarkerDecoration]
@@ -36,15 +35,12 @@ describe "DecorationManager", ->
layer1MarkerDecoration.destroy()
expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer1Marker.id]).not.toBeDefined()
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined()
layer2MarkerDecoration.destroy()
expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer2Marker.id]).not.toBeDefined()
expect(decorationManager.decorationForId(layer2MarkerDecoration.id)).not.toBeDefined()
it "will not fail if the decoration is removed twice", ->
layer1MarkerDecoration.destroy()
layer1MarkerDecoration.destroy()
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined()
it "does not allow destroyed markers to be decorated", ->
layer1Marker.destroy()

View File

@@ -175,8 +175,6 @@ describe('TextEditorComponent', () => {
jasmine.attachToDOM(element)
expect(getBaseCharacterWidth(component)).toBe(55)
console.log(element.offsetWidth);
expect(lineNodeForScreenRow(component, 3).textContent).toBe(
' var pivot = items.shift(), current, left = [], '
)
@@ -344,6 +342,74 @@ describe('TextEditorComponent', () => {
expect(scroller.scrollLeft).toBe(expectedScrollLeft)
})
})
describe('line and line number decorations', () => {
it('adds decoration classes on screen lines spanned by decorated markers', async () => {
const {component, element, editor} = buildComponent({width: 435, attach: false})
editor.setSoftWrapped(true)
jasmine.attachToDOM(element)
expect(lineNodeForScreenRow(component, 3).textContent).toBe(
' var pivot = items.shift(), current, left = [], '
)
expect(lineNodeForScreenRow(component, 4).textContent).toBe(
' right = [];'
)
const marker1 = editor.markScreenRange([[1, 10], [3, 10]])
const layer = editor.addMarkerLayer()
const marker2 = layer.markScreenPosition([5, 0])
const marker3 = layer.markScreenPosition([8, 0])
const marker4 = layer.markScreenPosition([10, 0])
const markerDecoration = editor.decorateMarker(marker1, {type: ['line', 'line-number'], class: 'a'})
const layerDecoration = editor.decorateMarkerLayer(layer, {type: ['line', 'line-number'], class: 'b'})
layerDecoration.setPropertiesForMarker(marker4, {type: 'line', class: 'c'})
await component.getNextUpdatePromise()
expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(true)
expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(true)
expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(true)
expect(lineNodeForScreenRow(component, 4).classList.contains('a')).toBe(false)
expect(lineNodeForScreenRow(component, 5).classList.contains('b')).toBe(true)
expect(lineNodeForScreenRow(component, 8).classList.contains('b')).toBe(true)
expect(lineNodeForScreenRow(component, 10).classList.contains('b')).toBe(false)
expect(lineNodeForScreenRow(component, 10).classList.contains('c')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 2).classList.contains('a')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 3).classList.contains('a')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 4).classList.contains('a')).toBe(false)
expect(lineNumberNodeForScreenRow(component, 5).classList.contains('b')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 8).classList.contains('b')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 10).classList.contains('b')).toBe(false)
expect(lineNumberNodeForScreenRow(component, 10).classList.contains('c')).toBe(false)
marker1.setScreenRange([[5, 0], [8, 0]])
await component.getNextUpdatePromise()
expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(false)
expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(false)
expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(false)
expect(lineNodeForScreenRow(component, 4).classList.contains('a')).toBe(false)
expect(lineNodeForScreenRow(component, 5).classList.contains('a')).toBe(true)
expect(lineNodeForScreenRow(component, 5).classList.contains('b')).toBe(true)
expect(lineNodeForScreenRow(component, 6).classList.contains('a')).toBe(true)
expect(lineNodeForScreenRow(component, 7).classList.contains('a')).toBe(true)
expect(lineNodeForScreenRow(component, 8).classList.contains('a')).toBe(true)
expect(lineNodeForScreenRow(component, 8).classList.contains('b')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(false)
expect(lineNumberNodeForScreenRow(component, 2).classList.contains('a')).toBe(false)
expect(lineNumberNodeForScreenRow(component, 3).classList.contains('a')).toBe(false)
expect(lineNumberNodeForScreenRow(component, 4).classList.contains('a')).toBe(false)
expect(lineNumberNodeForScreenRow(component, 5).classList.contains('a')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 5).classList.contains('b')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 6).classList.contains('a')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 7).classList.contains('a')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 8).classList.contains('a')).toBe(true)
expect(lineNumberNodeForScreenRow(component, 8).classList.contains('b')).toBe(true)
})
})
})
function buildComponent (params = {}) {
@@ -401,6 +467,15 @@ function clientLeftForCharacter (component, row, column) {
}
}
function lineNumberNodeForScreenRow (component, row) {
const gutterElement = component.refs.lineNumberGutter.element
const endRow = Math.min(component.getRenderedEndRow(), component.getModel().getApproximateScreenLineCount())
const visibleTileCount = Math.ceil((endRow - component.getRenderedStartRow()) / component.getRowsPerTile())
const tileStartRow = component.getTileStartRow(row)
const tileIndex = (tileStartRow / component.getRowsPerTile()) % visibleTileCount
return gutterElement.children[tileIndex].children[row - tileStartRow]
}
function lineNodeForScreenRow (component, row) {
const screenLine = component.getModel().screenLineForScreenRow(row)
return component.lineNodesByScreenLineId.get(screenLine.id)

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
###