diff --git a/spec/decoration-manager-spec.coffee b/spec/decoration-manager-spec.coffee index e57660a57..ba5de0cf2 100644 --- a/spec/decoration-manager-spec.coffee +++ b/spec/decoration-manager-spec.coffee @@ -14,8 +14,7 @@ describe "DecorationManager", -> atom.packages.activatePackage('language-javascript') afterEach -> - decorationManager.destroy() - buffer.release() + buffer.destroy() describe "decorations", -> [layer1Marker, layer2Marker, layer1MarkerDecoration, layer2MarkerDecoration, decorationProperties] = [] diff --git a/src/decoration-manager.coffee b/src/decoration-manager.coffee deleted file mode 100644 index 05935f018..000000000 --- a/src/decoration-manager.coffee +++ /dev/null @@ -1,191 +0,0 @@ -{Emitter} = require 'event-kit' -Model = require './model' -Decoration = require './decoration' -LayerDecoration = require './layer-decoration' - -module.exports = -class DecorationManager extends Model - didUpdateDecorationsEventScheduled: false - updatedSynchronously: false - - constructor: (@displayLayer) -> - super - - @emitter = new Emitter - @decorationsById = {} - @decorationsByMarkerId = {} - @overlayDecorationsById = {} - @layerDecorationsByMarkerLayerId = {} - @decorationCountsByLayerId = {} - @layerUpdateDisposablesByLayerId = {} - - observeDecorations: (callback) -> - callback(decoration) for decoration in @getDecorations() - @onDidAddDecoration(callback) - - onDidAddDecoration: (callback) -> - @emitter.on 'did-add-decoration', callback - - onDidRemoveDecoration: (callback) -> - @emitter.on 'did-remove-decoration', callback - - onDidUpdateDecorations: (callback) -> - @emitter.on 'did-update-decorations', callback - - setUpdatedSynchronously: (@updatedSynchronously) -> - - decorationForId: (id) -> - @decorationsById[id] - - getDecorations: (propertyFilter) -> - allDecorations = [] - for markerId, decorations of @decorationsByMarkerId - allDecorations.push(decorations...) if decorations? - if propertyFilter? - allDecorations = allDecorations.filter (decoration) -> - for key, value of propertyFilter - return false unless decoration.properties[key] is value - true - allDecorations - - getLineDecorations: (propertyFilter) -> - @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line') - - getLineNumberDecorations: (propertyFilter) -> - @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number') - - getHighlightDecorations: (propertyFilter) -> - @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight') - - getOverlayDecorations: (propertyFilter) -> - result = [] - for id, decoration of @overlayDecorationsById - result.push(decoration) - if propertyFilter? - result.filter (decoration) -> - for key, value of propertyFilter - return false unless decoration.properties[key] is value - true - else - result - - decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> - decorationsByMarkerId = {} - for layerId of @decorationCountsByLayerId - layer = @displayLayer.getMarkerLayer(layerId) - for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) - if decorations = @decorationsByMarkerId[marker.id] - decorationsByMarkerId[marker.id] = decorations - decorationsByMarkerId - - decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> - decorationsState = {} - - for layerId of @decorationCountsByLayerId - layer = @displayLayer.getMarkerLayer(layerId) - - for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid() - screenRange = marker.getScreenRange() - bufferRange = marker.getBufferRange() - rangeIsReversed = marker.isReversed() - - if decorations = @decorationsByMarkerId[marker.id] - for decoration in decorations - decorationsState[decoration.id] = { - properties: decoration.properties - screenRange, bufferRange, rangeIsReversed - } - - if layerDecorations = @layerDecorationsByMarkerLayerId[layerId] - for layerDecoration in layerDecorations - decorationsState["#{layerDecoration.id}-#{marker.id}"] = { - properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties - screenRange, bufferRange, rangeIsReversed - } - - decorationsState - - decorateMarker: (marker, decorationParams) -> - if marker.isDestroyed() - error = new Error("Cannot decorate a destroyed marker") - error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()} - if marker.destroyStackTrace? - error.metadata.destroyStackTrace = marker.destroyStackTrace - if marker.bufferMarker?.destroyStackTrace? - error.metadata.destroyStackTrace = marker.bufferMarker?.destroyStackTrace - throw error - marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id) - decoration = new Decoration(marker, this, decorationParams) - @decorationsByMarkerId[marker.id] ?= [] - @decorationsByMarkerId[marker.id].push(decoration) - @overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay') - @decorationsById[decoration.id] = decoration - @observeDecoratedLayer(marker.layer) - @scheduleUpdateDecorationsEvent() - @emitter.emit 'did-add-decoration', decoration - decoration - - decorateMarkerLayer: (markerLayer, decorationParams) -> - throw new Error("Cannot decorate a destroyed marker layer") if markerLayer.isDestroyed() - decoration = new LayerDecoration(markerLayer, this, decorationParams) - @layerDecorationsByMarkerLayerId[markerLayer.id] ?= [] - @layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration) - @observeDecoratedLayer(markerLayer) - @scheduleUpdateDecorationsEvent() - decoration - - decorationsForMarkerId: (markerId) -> - @decorationsByMarkerId[markerId] - - scheduleUpdateDecorationsEvent: -> - if @updatedSynchronously - @emitter.emit 'did-update-decorations' - return - - unless @didUpdateDecorationsEventScheduled - @didUpdateDecorationsEventScheduled = true - process.nextTick => - @didUpdateDecorationsEventScheduled = false - @emitter.emit 'did-update-decorations' - - decorationDidChangeType: (decoration) -> - if decoration.isType('overlay') - @overlayDecorationsById[decoration.id] = decoration - else - delete @overlayDecorationsById[decoration.id] - - didDestroyMarkerDecoration: (decoration) -> - {marker} = decoration - return unless decorations = @decorationsByMarkerId[marker.id] - index = decorations.indexOf(decoration) - - if index > -1 - decorations.splice(index, 1) - delete @decorationsById[decoration.id] - @emitter.emit 'did-remove-decoration', decoration - delete @decorationsByMarkerId[marker.id] if decorations.length is 0 - delete @overlayDecorationsById[decoration.id] - @unobserveDecoratedLayer(marker.layer) - @scheduleUpdateDecorationsEvent() - - didDestroyLayerDecoration: (decoration) -> - {markerLayer} = decoration - return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id] - index = decorations.indexOf(decoration) - - if index > -1 - decorations.splice(index, 1) - delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0 - @unobserveDecoratedLayer(markerLayer) - @scheduleUpdateDecorationsEvent() - - observeDecoratedLayer: (layer) -> - @decorationCountsByLayerId[layer.id] ?= 0 - if ++@decorationCountsByLayerId[layer.id] is 1 - @layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this)) - - unobserveDecoratedLayer: (layer) -> - if --@decorationCountsByLayerId[layer.id] is 0 - @layerUpdateDisposablesByLayerId[layer.id].dispose() - delete @decorationCountsByLayerId[layer.id] - delete @layerUpdateDisposablesByLayerId[layer.id] diff --git a/src/decoration-manager.js b/src/decoration-manager.js new file mode 100644 index 000000000..f9e31e60d --- /dev/null +++ b/src/decoration-manager.js @@ -0,0 +1,272 @@ +const {Emitter} = require('event-kit') +const Decoration = require('./decoration') +const LayerDecoration = require('./layer-decoration') + +module.exports = +class DecorationManager { + constructor(displayLayer) { + this.displayLayer = displayLayer + + this.emitter = new Emitter + this.didUpdateDecorationsEventScheduled = false + this.updatedSynchronously = false + this.decorationsById = {} + this.decorationsByMarkerId = {} + this.overlayDecorationsById = {} + this.layerDecorationsByMarkerLayerId = {} + this.decorationCountsByLayerId = {} + this.layerUpdateDisposablesByLayerId = {} + } + + observeDecorations(callback) { + for (let decoration of this.getDecorations()) { callback(decoration); } + return this.onDidAddDecoration(callback) + } + + onDidAddDecoration(callback) { + return this.emitter.on('did-add-decoration', callback) + } + + onDidRemoveDecoration(callback) { + return this.emitter.on('did-remove-decoration', callback) + } + + onDidUpdateDecorations(callback) { + return this.emitter.on('did-update-decorations', callback) + } + + setUpdatedSynchronously(updatedSynchronously) { + this.updatedSynchronously = updatedSynchronously + } + + decorationForId(id) { + return this.decorationsById[id] + } + + getDecorations(propertyFilter) { + let allDecorations = [] + for (let markerId in this.decorationsByMarkerId) { + const decorations = this.decorationsByMarkerId[markerId] + if (decorations != null) { + allDecorations.push(...decorations) + } + } + if (propertyFilter != null) { + allDecorations = allDecorations.filter(function(decoration) { + for (let key in propertyFilter) { + const value = propertyFilter[key] + if (decoration.properties[key] !== value) return false + } + return true + }) + } + return allDecorations + } + + getLineDecorations(propertyFilter) { + return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line')) + } + + getLineNumberDecorations(propertyFilter) { + return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line-number')) + } + + getHighlightDecorations(propertyFilter) { + return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('highlight')) + } + + getOverlayDecorations(propertyFilter) { + const result = [] + for (let id in this.overlayDecorationsById) { + const decoration = this.overlayDecorationsById[id] + result.push(decoration) + } + if (propertyFilter != null) { + return result.filter(function(decoration) { + for (let key in propertyFilter) { + const value = propertyFilter[key] + if (decoration.properties[key] !== value) { + return false + } + } + return true + }) + } else { + return result + } + } + + decorationsForScreenRowRange(startScreenRow, endScreenRow) { + const decorationsByMarkerId = {} + for (let layerId in this.decorationCountsByLayerId) { + const layer = this.displayLayer.getMarkerLayer(layerId) + for (let marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) { + const decorations = this.decorationsByMarkerId[marker.id] + if (decorations) { + decorationsByMarkerId[marker.id] = decorations + } + } + } + return decorationsByMarkerId + } + + decorationsStateForScreenRowRange(startScreenRow, endScreenRow) { + const decorationsState = {} + + for (let layerId in this.decorationCountsByLayerId) { + const layer = this.displayLayer.getMarkerLayer(layerId) + + for (let marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) { + if (marker.isValid()) { + const screenRange = marker.getScreenRange() + const bufferRange = marker.getBufferRange() + const rangeIsReversed = marker.isReversed() + + const decorations = this.decorationsByMarkerId[marker.id] + if (decorations) { + for (let decoration of decorations) { + decorationsState[decoration.id] = { + properties: decoration.properties, + screenRange, bufferRange, rangeIsReversed + } + } + } + + const layerDecorations = this.layerDecorationsByMarkerLayerId[layerId] + if (layerDecorations) { + for (let layerDecoration of layerDecorations) { + decorationsState[`${layerDecoration.id}-${marker.id}`] = { + properties: layerDecoration.overridePropertiesByMarkerId[marker.id] != null ? layerDecoration.overridePropertiesByMarkerId[marker.id] : layerDecoration.properties, + screenRange, bufferRange, rangeIsReversed + } + } + } + } + } + } + + return decorationsState + } + + decorateMarker(marker, decorationParams) { + if (marker.isDestroyed()) { + const error = new Error("Cannot decorate a destroyed marker") + error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()} + if (marker.destroyStackTrace != null) { + error.metadata.destroyStackTrace = marker.destroyStackTrace + } + if (marker.bufferMarker != null && marker.bufferMarker.destroyStackTrace != null) { + error.metadata.destroyStackTrace = marker.bufferMarker.destroyStackTrace + } + throw error + } + marker = this.displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id) + const decoration = new Decoration(marker, this, decorationParams) + if (this.decorationsByMarkerId[marker.id] == null) { + this.decorationsByMarkerId[marker.id] = [] + } + this.decorationsByMarkerId[marker.id].push(decoration) + if (decoration.isType('overlay')) { + this.overlayDecorationsById[decoration.id] = decoration + } + this.decorationsById[decoration.id] = decoration + this.observeDecoratedLayer(marker.layer) + this.scheduleUpdateDecorationsEvent() + this.emitter.emit('did-add-decoration', decoration) + return decoration + } + + decorateMarkerLayer(markerLayer, decorationParams) { + if (markerLayer.isDestroyed()) { + throw new Error("Cannot decorate a destroyed marker layer") + } + const decoration = new LayerDecoration(markerLayer, this, decorationParams) + if (this.layerDecorationsByMarkerLayerId[markerLayer.id] == null) { + this.layerDecorationsByMarkerLayerId[markerLayer.id] = [] + } + this.layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration) + this.observeDecoratedLayer(markerLayer) + this.scheduleUpdateDecorationsEvent() + return decoration + } + + decorationsForMarkerId(markerId) { + return this.decorationsByMarkerId[markerId] + } + + scheduleUpdateDecorationsEvent() { + if (this.updatedSynchronously) { + this.emitter.emit('did-update-decorations') + return + } + + if (!this.didUpdateDecorationsEventScheduled) { + this.didUpdateDecorationsEventScheduled = true + return process.nextTick(() => { + this.didUpdateDecorationsEventScheduled = false + return this.emitter.emit('did-update-decorations') + } + ) + } + } + + decorationDidChangeType(decoration) { + if (decoration.isType('overlay')) { + return this.overlayDecorationsById[decoration.id] = decoration + } else { + return delete this.overlayDecorationsById[decoration.id] + } + } + + didDestroyMarkerDecoration(decoration) { + let decorations + const {marker} = decoration + if (!(decorations = this.decorationsByMarkerId[marker.id])) return + const index = decorations.indexOf(decoration) + + if (index > -1) { + decorations.splice(index, 1) + delete this.decorationsById[decoration.id] + this.emitter.emit('did-remove-decoration', decoration) + if (decorations.length === 0) { + delete this.decorationsByMarkerId[marker.id] + } + delete this.overlayDecorationsById[decoration.id] + this.unobserveDecoratedLayer(marker.layer) + } + return this.scheduleUpdateDecorationsEvent() + } + + didDestroyLayerDecoration(decoration) { + let decorations + const {markerLayer} = decoration + if (!(decorations = this.layerDecorationsByMarkerLayerId[markerLayer.id])) return + const index = decorations.indexOf(decoration) + + if (index > -1) { + decorations.splice(index, 1) + if (decorations.length === 0) { + delete this.layerDecorationsByMarkerLayerId[markerLayer.id] + } + this.unobserveDecoratedLayer(markerLayer) + } + return this.scheduleUpdateDecorationsEvent() + } + + observeDecoratedLayer(layer) { + if (this.decorationCountsByLayerId[layer.id] == null) { + this.decorationCountsByLayerId[layer.id] = 0 + } + if (++this.decorationCountsByLayerId[layer.id] === 1) { + this.layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(this.scheduleUpdateDecorationsEvent.bind(this)) + } + } + + unobserveDecoratedLayer(layer) { + if (--this.decorationCountsByLayerId[layer.id] === 0) { + this.layerUpdateDisposablesByLayerId[layer.id].dispose() + delete this.decorationCountsByLayerId[layer.id] + delete this.layerUpdateDisposablesByLayerId[layer.id] + } + } +}