src/decoration-manager.js
const { Emitter } = require('event-kit');
const Decoration = require('./decoration');
const LayerDecoration = require('./layer-decoration');
module.exports = class DecorationManager {
constructor(editor) {
this.editor = editor;
this.displayLayer = this.editor.displayLayer;
this.emitter = new Emitter();
this.decorationCountsByLayer = new Map();
this.markerDecorationCountsByLayer = new Map();
this.decorationsByMarker = new Map();
this.layerDecorationsByMarkerLayer = new Map();
this.overlayDecorations = new Set();
this.layerUpdateDisposablesByLayer = new WeakMap();
}
observeDecorations(callback) {
const decorations = this.getDecorations();
for (let i = 0; i < decorations.length; i++) {
callback(decorations[i]);
}
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);
}
getDecorations(propertyFilter) {
let allDecorations = [];
this.decorationsByMarker.forEach(decorations => {
decorations.forEach(decoration => allDecorations.push(decoration));
});
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 = [];
result.push(...Array.from(this.overlayDecorations));
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;
}
}
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];
if (!marker.isValid()) continue;
let decorationPropertiesForMarker = decorationPropertiesByMarker.get(
marker
);
if (decorationPropertiesForMarker == null) {
decorationPropertiesForMarker = [];
decorationPropertiesByMarker.set(
marker,
decorationPropertiesForMarker
);
}
if (layerDecorations) {
layerDecorations.forEach(layerDecoration => {
const properties =
layerDecoration.getPropertiesForMarker(marker) ||
layerDecoration.getProperties();
decorationPropertiesForMarker.push(properties);
});
}
if (hasMarkerDecorations) {
const decorationsForMarker = this.decorationsByMarker.get(marker);
if (decorationsForMarker) {
decorationsForMarker.forEach(decoration => {
decorationPropertiesForMarker.push(decoration.getProperties());
});
}
}
}
});
return decorationPropertiesByMarker;
}
decorationsForScreenRowRange(startScreenRow, endScreenRow) {
const decorationsByMarkerId = {};
for (const layer of this.decorationCountsByLayer.keys()) {
for (const marker of layer.findMarkers({
intersectsScreenRowRange: [startScreenRow, endScreenRow]
})) {
const decorations = this.decorationsByMarker.get(marker);
if (decorations) {
decorationsByMarkerId[marker.id] = Array.from(decorations);
}
}
}
return decorationsByMarkerId;
}
decorationsStateForScreenRowRange(startScreenRow, endScreenRow) {
const decorationsState = {};
for (const layer of this.decorationCountsByLayer.keys()) {
for (const 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.decorationsByMarker.get(marker);
if (decorations) {
decorations.forEach(decoration => {
decorationsState[decoration.id] = {
properties: decoration.properties,
screenRange,
bufferRange,
rangeIsReversed
};
});
}
const layerDecorations = this.layerDecorationsByMarkerLayer.get(
layer
);
if (layerDecorations) {
layerDecorations.forEach(layerDecoration => {
const properties =
layerDecoration.getPropertiesForMarker(marker) ||
layerDecoration.getProperties();
decorationsState[`${layerDecoration.id}-${marker.id}`] = {
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);
let decorationsForMarker = this.decorationsByMarker.get(marker);
if (!decorationsForMarker) {
decorationsForMarker = new Set();
this.decorationsByMarker.set(marker, decorationsForMarker);
}
decorationsForMarker.add(decoration);
if (decoration.isType('overlay')) this.overlayDecorations.add(decoration);
this.observeDecoratedLayer(marker.layer, true);
this.editor.didAddDecoration(decoration);
this.emitDidUpdateDecorations();
this.emitter.emit('did-add-decoration', decoration);
return decoration;
}
decorateMarkerLayer(markerLayer, decorationParams) {
if (markerLayer.isDestroyed()) {
throw new Error('Cannot decorate a destroyed marker layer');
}
markerLayer = this.displayLayer.getMarkerLayer(markerLayer.id);
const decoration = new LayerDecoration(markerLayer, this, decorationParams);
let layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer);
if (layerDecorations == null) {
layerDecorations = new Set();
this.layerDecorationsByMarkerLayer.set(markerLayer, layerDecorations);
}
layerDecorations.add(decoration);
this.observeDecoratedLayer(markerLayer, false);
this.emitDidUpdateDecorations();
return decoration;
}
emitDidUpdateDecorations() {
this.editor.scheduleComponentUpdate();
this.emitter.emit('did-update-decorations');
}
decorationDidChangeType(decoration) {
if (decoration.isType('overlay')) {
this.overlayDecorations.add(decoration);
} else {
this.overlayDecorations.delete(decoration);
}
}
didDestroyMarkerDecoration(decoration) {
const { marker } = decoration;
const decorations = this.decorationsByMarker.get(marker);
if (decorations && decorations.has(decoration)) {
decorations.delete(decoration);
if (decorations.size === 0) this.decorationsByMarker.delete(marker);
this.overlayDecorations.delete(decoration);
this.unobserveDecoratedLayer(marker.layer, true);
this.emitter.emit('did-remove-decoration', decoration);
this.emitDidUpdateDecorations();
}
}
didDestroyLayerDecoration(decoration) {
const { markerLayer } = decoration;
const decorations = this.layerDecorationsByMarkerLayer.get(markerLayer);
if (decorations && decorations.has(decoration)) {
decorations.delete(decoration);
if (decorations.size === 0) {
this.layerDecorationsByMarkerLayer.delete(markerLayer);
}
this.unobserveDecoratedLayer(markerLayer, true);
this.emitDidUpdateDecorations();
}
}
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, isMarkerDecoration) {
const newCount = this.decorationCountsByLayer.get(layer) - 1;
if (newCount === 0) {
this.layerUpdateDisposablesByLayer.get(layer).dispose();
this.decorationCountsByLayer.delete(layer);
} else {
this.decorationCountsByLayer.set(layer, newCount);
}
if (isMarkerDecoration) {
this.markerDecorationCountsByLayer.set(
this.markerDecorationCountsByLayer.get(layer) - 1
);
}
}
};