lib/mixins/decoration-management.js
'use babel'
import Mixin from 'mixto'
import {Emitter} from 'atom'
import Decoration from '../decoration'
/**
* The mixin that provides the decorations API to the minimap editor
* view.
*
* This mixin is injected into the `Minimap` prototype, so every methods defined
* in this file will be available on any `Minimap` instance.
*/
export default class DecorationManagement extends Mixin {
/**
* Initializes the decorations related properties.
*/
initializeDecorations () {
if (this.emitter == null) {
/**
* The minimap emitter, lazily created if not created yet.
* @type {Emitter}
* @access private
*/
this.emitter = new Emitter()
}
/**
* A map with the decoration id as key and the decoration as value.
* @type {Object}
* @access private
*/
this.decorationsById = {}
/**
* The decorations stored in an array indexed with their marker id.
* @type {Object}
* @access private
*/
this.decorationsByMarkerId = {}
/**
* The subscriptions to the markers `did-change` event indexed using the
* marker id.
* @type {Object}
* @access private
*/
this.decorationMarkerChangedSubscriptions = {}
/**
* The subscriptions to the markers `did-destroy` event indexed using the
* marker id.
* @type {Object}
* @access private
*/
this.decorationMarkerDestroyedSubscriptions = {}
/**
* The subscriptions to the decorations `did-change-properties` event
* indexed using the decoration id.
* @type {Object}
* @access private
*/
this.decorationUpdatedSubscriptions = {}
/**
* The subscriptions to the decorations `did-destroy` event indexed using
* the decoration id.
* @type {Object}
* @access private
*/
this.decorationDestroyedSubscriptions = {}
}
/**
* Returns all the decorations registered in the current `Minimap`.
*
* @return {Array<Decoration>} all the decorations in this `Minimap`
*/
getDecorations () {
let decorations = this.decorationsById
let results = []
for (let id in decorations) { results.push(decorations[id]) }
return results
}
/**
* Registers an event listener to the `did-add-decoration` event.
*
* @param {function(event:Object):void} callback a function to call when the
* event is triggered.
* the callback will be called
* with an event object with
* the following properties:
* - marker: the marker object that was decorated
* - decoration: the decoration object that was created
* @return {Disposable} a disposable to stop listening to the event
*/
onDidAddDecoration (callback) {
return this.emitter.on('did-add-decoration', callback)
}
/**
* Registers an event listener to the `did-remove-decoration` event.
*
* @param {function(event:Object):void} callback a function to call when the
* event is triggered.
* the callback will be called
* with an event object with
* the following properties:
* - marker: the marker object that was decorated
* - decoration: the decoration object that was created
* @return {Disposable} a disposable to stop listening to the event
*/
onDidRemoveDecoration (callback) {
return this.emitter.on('did-remove-decoration', callback)
}
/**
* Registers an event listener to the `did-change-decoration` event.
*
* This event is triggered when the marker targeted by the decoration
* was changed.
*
* @param {function(event:Object):void} callback a function to call when the
* event is triggered.
* the callback will be called
* with an event object with
* the following properties:
* - marker: the marker object that was decorated
* - decoration: the decoration object that was created
* @return {Disposable} a disposable to stop listening to the event
*/
onDidChangeDecoration (callback) {
return this.emitter.on('did-change-decoration', callback)
}
/**
* Registers an event listener to the `did-change-decoration-range` event.
*
* This event is triggered when the marker range targeted by the decoration
* was changed.
*
* @param {function(event:Object):void} callback a function to call when the
* event is triggered.
* the callback will be called
* with an event object with
* the following properties:
* - marker: the marker object that was decorated
* - decoration: the decoration object that was created
* @return {Disposable} a disposable to stop listening to the event
*/
onDidChangeDecorationRange (callback) {
return this.emitter.on('did-change-decoration-range', callback)
}
/**
* Registers an event listener to the `did-update-decoration` event.
*
* This event is triggered when the decoration itself is modified.
*
* @param {function(decoration:Decoration):void} callback a function to call
* when the event is
* triggered
* @return {Disposable} a disposable to stop listening to the event
*/
onDidUpdateDecoration (callback) {
return this.emitter.on('did-update-decoration', callback)
}
/**
* Returns the decoration with the passed-in id.
*
* @param {number} id the decoration id
* @return {Decoration} the decoration with the given id
*/
decorationForId (id) {
return this.decorationsById[id]
}
/**
* Returns all the decorations that intersect the passed-in row range.
*
* @param {number} startScreenRow the first row of the range
* @param {number} endScreenRow the last row of the range
* @return {Array<Decoration>} the decorations that intersect the passed-in
* range
*/
decorationsForScreenRowRange (startScreenRow, endScreenRow) {
let decorationsByMarkerId = {}
let markers = this.findMarkers({
intersectsScreenRowRange: [startScreenRow, endScreenRow]
})
for (let i = 0, len = markers.length; i < len; i++) {
let marker = markers[i]
let decorations = this.decorationsByMarkerId[marker.id]
if (decorations != null) {
decorationsByMarkerId[marker.id] = decorations
}
}
return decorationsByMarkerId
}
/**
* Returns the decorations that intersects the passed-in row range
* in a structured way.
*
* At the first level, the keys are the available decoration types.
* At the second level, the keys are the row index for which there
* are decorations available. The value is an array containing the
* decorations that intersects with the corresponding row.
*
* @return {Object} the decorations grouped by type and then rows
* @property {Object} line all the line decorations by row
* @property {Array<Decoration>} line[row] all the line decorations
* at a given row
* @property {Object} highlight-under all the highlight-under decorations
* by row
* @property {Array<Decoration>} highlight-under[row] all the highlight-under
* decorations at a given row
* @property {Object} highlight-over all the highlight-over decorations
* by row
* @property {Array<Decoration>} highlight-over[row] all the highlight-over
* decorations at a given row
* @property {Object} highlight-outine all the highlight-outine decorations
* by row
* @property {Array<Decoration>} highlight-outine[row] all the
* highlight-outine decorations at a given
* row
*/
decorationsByTypeThenRows () {
if (this.decorationsByTypeThenRowsCache != null) {
return this.decorationsByTypeThenRowsCache
}
let cache = {}
for (let id in this.decorationsById) {
let decoration = this.decorationsById[id]
let range = decoration.marker.getScreenRange()
let type = decoration.getProperties().type
if (cache[type] == null) { cache[type] = {} }
for (let row = range.start.row, len = range.end.row; row <= len; row++) {
if (cache[type][row] == null) { cache[type][row] = [] }
cache[type][row].push(decoration)
}
}
/**
* The grouped decorations cache.
* @type {Object}
* @access private
*/
this.decorationsByTypeThenRowsCache = cache
return cache
}
/**
* Invalidates the decoration by screen rows cache.
*/
invalidateDecorationForScreenRowsCache () {
this.decorationsByTypeThenRowsCache = null
}
/**
* Adds a decoration that tracks a `Marker`. When the marker moves,
* is invalidated, or is destroyed, the decoration will be updated to reflect
* the marker's state.
*
* @param {Marker} marker the marker you want this decoration to follow
* @param {Object} decorationParams the decoration properties
* @param {string} decorationParams.type the decoration type in the following
* list:
* - __line__: Fills the line background with the decoration color.
* - __highlight__: Renders a colored rectangle on the minimap. The highlight
* is rendered above the line's text.
* - __highlight-over__: Same as __highlight__.
* - __highlight-under__: Renders a colored rectangle on the minimap. The
* highlight is rendered below the line's text.
* - __highlight-outline__: Renders a colored outline on the minimap. The
* highlight box is rendered above the line's text.
* @param {string} decorationParams.class the CSS class to use to retrieve
* the background color of the
* decoration by building a scop
* corresponding to
* `.minimap .editor <your-class>`
* @param {string} decorationParams.scope the scope to use to retrieve the
* decoration background. Note that if
* the `scope` property is set, the
* `class` won't be used.
* @param {string} decorationParams.color the CSS color to use to render the
* decoration. When set, neither
* `scope` nor `class` are used.
* @return {Decoration} the created decoration
* @emits {did-add-decoration} when the decoration is created successfully
* @emits {did-change} when the decoration is created successfully
*/
decorateMarker (marker, decorationParams) {
if (this.destroyed || marker == null) { return }
let {id} = marker
if (decorationParams.type === 'highlight') {
decorationParams.type = 'highlight-over'
}
if (decorationParams.scope == null && decorationParams['class'] != null) {
let cls = decorationParams['class'].split(' ').join('.')
decorationParams.scope = `.minimap .${cls}`
}
if (this.decorationMarkerDestroyedSubscriptions[id] == null) {
this.decorationMarkerDestroyedSubscriptions[id] =
marker.onDidDestroy(() => {
this.removeAllDecorationsForMarker(marker)
})
}
if (this.decorationMarkerChangedSubscriptions[id] == null) {
this.decorationMarkerChangedSubscriptions[id] =
marker.onDidChange((event) => {
let decorations = this.decorationsByMarkerId[id]
this.invalidateDecorationForScreenRowsCache()
if (decorations != null) {
for (let i = 0, len = decorations.length; i < len; i++) {
let decoration = decorations[i]
this.emitter.emit('did-change-decoration', {
marker: marker,
decoration: decoration,
event: event
})
}
}
let oldStart = event.oldTailScreenPosition
let oldEnd = event.oldHeadScreenPosition
let newStart = event.newTailScreenPosition
let newEnd = event.newHeadScreenPosition
if (oldStart.row > oldEnd.row) {
[oldStart, oldEnd] = [oldEnd, oldStart]
}
if (newStart.row > newEnd.row) {
[newStart, newEnd] = [newEnd, newStart]
}
let rangesDiffs = this.computeRangesDiffs(
oldStart, oldEnd,
newStart, newEnd
)
for (let i = 0, len = rangesDiffs.length; i < len; i++) {
let [start, end] = rangesDiffs[i]
this.emitRangeChanges({
start: start,
end: end
}, 0)
}
})
}
let decoration = new Decoration(marker, this, decorationParams)
if (this.decorationsByMarkerId[id] == null) {
this.decorationsByMarkerId[id] = []
}
this.decorationsByMarkerId[id].push(decoration)
this.decorationsById[decoration.id] = decoration
if (this.decorationUpdatedSubscriptions[decoration.id] == null) {
this.decorationUpdatedSubscriptions[decoration.id] =
decoration.onDidChangeProperties((event) => {
this.emitDecorationChanges(decoration)
})
}
this.decorationDestroyedSubscriptions[decoration.id] =
decoration.onDidDestroy(() => {
this.removeDecoration(decoration)
})
this.emitDecorationChanges(decoration)
this.emitter.emit('did-add-decoration', {
marker: marker,
decoration: decoration
})
return decoration
}
/**
* Given two ranges, it returns an array of ranges representing the
* differences between them.
*
* @param {number} oldStart the row index of the first range start
* @param {number} oldEnd the row index of the first range end
* @param {number} newStart the row index of the second range start
* @param {number} newEnd the row index of the second range end
* @return {Array<Object>} the array of diff ranges
* @access private
*/
computeRangesDiffs (oldStart, oldEnd, newStart, newEnd) {
let diffs = []
if (oldStart.isLessThan(newStart)) {
diffs.push([oldStart, newStart])
} else if (newStart.isLessThan(oldStart)) {
diffs.push([newStart, oldStart])
}
if (oldEnd.isLessThan(newEnd)) {
diffs.push([oldEnd, newEnd])
} else if (newEnd.isLessThan(oldEnd)) {
diffs.push([newEnd, oldEnd])
}
return diffs
}
/**
* Emits a change in the `Minimap` corresponding to the
* passed-in decoration.
*
* @param {Decoration} decoration the decoration for which emitting an event
* @access private
*/
emitDecorationChanges (decoration) {
if (decoration.marker.displayBuffer.isDestroyed()) { return }
this.invalidateDecorationForScreenRowsCache()
let range = decoration.marker.getScreenRange()
if (range == null) { return }
this.emitRangeChanges(range, 0)
}
/**
* Emits a change for the specified range.
*
* @param {Object} range the range where changes occured
* @param {number} [screenDelta] an optional screen delta for the
* change object
* @access private
*/
emitRangeChanges (range, screenDelta) {
let startScreenRow = range.start.row
let endScreenRow = range.end.row
let lastRenderedScreenRow = this.getLastVisibleScreenRow()
let firstRenderedScreenRow = this.getFirstVisibleScreenRow()
if (screenDelta == null) {
screenDelta = (lastRenderedScreenRow - firstRenderedScreenRow) -
(endScreenRow - startScreenRow)
}
let changeEvent = {
start: startScreenRow,
end: endScreenRow,
screenDelta: screenDelta
}
this.emitter.emit('did-change-decoration-range', changeEvent)
}
/**
* Removes a `Decoration` from this minimap.
*
* @param {Decoration} decoration the decoration to remove
* @emits {did-change} when the decoration is removed
* @emits {did-remove-decoration} when the decoration is removed
*/
removeDecoration (decoration) {
if (decoration == null) { return }
let marker = decoration.marker
let subscription
delete this.decorationsById[decoration.id]
subscription = this.decorationUpdatedSubscriptions[decoration.id]
if (subscription != null) { subscription.dispose() }
subscription = this.decorationDestroyedSubscriptions[decoration.id]
if (subscription != null) { subscription.dispose() }
delete this.decorationUpdatedSubscriptions[decoration.id]
delete this.decorationDestroyedSubscriptions[decoration.id]
let decorations = this.decorationsByMarkerId[marker.id]
if (!decorations) { return }
this.emitDecorationChanges(decoration)
let index = decorations.indexOf(decoration)
if (index > -1) {
decorations.splice(index, 1)
this.emitter.emit('did-remove-decoration', {
marker: marker,
decoration: decoration
})
if (decorations.length === 0) {
this.removedAllMarkerDecorations(marker)
}
}
}
/**
* Removes all the decorations registered for the passed-in marker.
*
* @param {Marker} marker the marker for which removing its decorations
* @emits {did-change} when a decoration have been removed
* @emits {did-remove-decoration} when a decoration have been removed
*/
removeAllDecorationsForMarker (marker) {
if (marker == null) { return }
let decorations = this.decorationsByMarkerId[marker.id]
if (!decorations) { return }
for (let i = 0, len = decorations.length; i < len; i++) {
let decoration = decorations[i]
this.emitDecorationChanges(decoration)
this.emitter.emit('did-remove-decoration', {
marker: marker,
decoration: decoration
})
}
this.removedAllMarkerDecorations(marker)
}
/**
* Performs the removal of a decoration for a given marker.
*
* @param {Marker} marker the marker for which removing decorations
* @access private
*/
removedAllMarkerDecorations (marker) {
if (marker == null) { return }
this.decorationMarkerChangedSubscriptions[marker.id].dispose()
this.decorationMarkerDestroyedSubscriptions[marker.id].dispose()
delete this.decorationsByMarkerId[marker.id]
delete this.decorationMarkerChangedSubscriptions[marker.id]
delete this.decorationMarkerDestroyedSubscriptions[marker.id]
}
/**
* Removes all the decorations that was created in the current `Minimap`.
*/
removeAllDecorations () {
for (let id in this.decorationMarkerChangedSubscriptions) {
this.decorationMarkerChangedSubscriptions[id].dispose()
}
for (let id in this.decorationMarkerDestroyedSubscriptions) {
this.decorationMarkerDestroyedSubscriptions[id].dispose()
}
for (let id in this.decorationUpdatedSubscriptions) {
this.decorationUpdatedSubscriptions[id].dispose()
}
for (let id in this.decorationDestroyedSubscriptions) {
this.decorationDestroyedSubscriptions[id].dispose()
}
for (let id in this.decorationsById) {
this.decorationsById[id].destroy()
}
this.decorationsById = {}
this.decorationsByMarkerId = {}
this.decorationMarkerChangedSubscriptions = {}
this.decorationMarkerDestroyedSubscriptions = {}
this.decorationUpdatedSubscriptions = {}
this.decorationDestroyedSubscriptions = {}
}
}