/ src / decoration-manager.js
decoration-manager.js
  1  const { Emitter } = require('event-kit');
  2  const Decoration = require('./decoration');
  3  const LayerDecoration = require('./layer-decoration');
  4  
  5  module.exports = class DecorationManager {
  6    constructor(editor) {
  7      this.editor = editor;
  8      this.displayLayer = this.editor.displayLayer;
  9  
 10      this.emitter = new Emitter();
 11      this.decorationCountsByLayer = new Map();
 12      this.markerDecorationCountsByLayer = new Map();
 13      this.decorationsByMarker = new Map();
 14      this.layerDecorationsByMarkerLayer = new Map();
 15      this.overlayDecorations = new Set();
 16      this.layerUpdateDisposablesByLayer = new WeakMap();
 17    }
 18  
 19    observeDecorations(callback) {
 20      const decorations = this.getDecorations();
 21      for (let i = 0; i < decorations.length; i++) {
 22        callback(decorations[i]);
 23      }
 24      return this.onDidAddDecoration(callback);
 25    }
 26  
 27    onDidAddDecoration(callback) {
 28      return this.emitter.on('did-add-decoration', callback);
 29    }
 30  
 31    onDidRemoveDecoration(callback) {
 32      return this.emitter.on('did-remove-decoration', callback);
 33    }
 34  
 35    onDidUpdateDecorations(callback) {
 36      return this.emitter.on('did-update-decorations', callback);
 37    }
 38  
 39    getDecorations(propertyFilter) {
 40      let allDecorations = [];
 41  
 42      this.decorationsByMarker.forEach(decorations => {
 43        decorations.forEach(decoration => allDecorations.push(decoration));
 44      });
 45      if (propertyFilter != null) {
 46        allDecorations = allDecorations.filter(function(decoration) {
 47          for (let key in propertyFilter) {
 48            const value = propertyFilter[key];
 49            if (decoration.properties[key] !== value) return false;
 50          }
 51          return true;
 52        });
 53      }
 54      return allDecorations;
 55    }
 56  
 57    getLineDecorations(propertyFilter) {
 58      return this.getDecorations(propertyFilter).filter(decoration =>
 59        decoration.isType('line')
 60      );
 61    }
 62  
 63    getLineNumberDecorations(propertyFilter) {
 64      return this.getDecorations(propertyFilter).filter(decoration =>
 65        decoration.isType('line-number')
 66      );
 67    }
 68  
 69    getHighlightDecorations(propertyFilter) {
 70      return this.getDecorations(propertyFilter).filter(decoration =>
 71        decoration.isType('highlight')
 72      );
 73    }
 74  
 75    getOverlayDecorations(propertyFilter) {
 76      const result = [];
 77      result.push(...Array.from(this.overlayDecorations));
 78      if (propertyFilter != null) {
 79        return result.filter(function(decoration) {
 80          for (let key in propertyFilter) {
 81            const value = propertyFilter[key];
 82            if (decoration.properties[key] !== value) {
 83              return false;
 84            }
 85          }
 86          return true;
 87        });
 88      } else {
 89        return result;
 90      }
 91    }
 92  
 93    decorationPropertiesByMarkerForScreenRowRange(startScreenRow, endScreenRow) {
 94      const decorationPropertiesByMarker = new Map();
 95  
 96      this.decorationCountsByLayer.forEach((count, markerLayer) => {
 97        const markers = markerLayer.findMarkers({
 98          intersectsScreenRowRange: [startScreenRow, endScreenRow - 1]
 99        });
100        const layerDecorations = this.layerDecorationsByMarkerLayer.get(
101          markerLayer
102        );
103        const hasMarkerDecorations =
104          this.markerDecorationCountsByLayer.get(markerLayer) > 0;
105  
106        for (let i = 0; i < markers.length; i++) {
107          const marker = markers[i];
108          if (!marker.isValid()) continue;
109  
110          let decorationPropertiesForMarker = decorationPropertiesByMarker.get(
111            marker
112          );
113          if (decorationPropertiesForMarker == null) {
114            decorationPropertiesForMarker = [];
115            decorationPropertiesByMarker.set(
116              marker,
117              decorationPropertiesForMarker
118            );
119          }
120  
121          if (layerDecorations) {
122            layerDecorations.forEach(layerDecoration => {
123              const properties =
124                layerDecoration.getPropertiesForMarker(marker) ||
125                layerDecoration.getProperties();
126              decorationPropertiesForMarker.push(properties);
127            });
128          }
129  
130          if (hasMarkerDecorations) {
131            const decorationsForMarker = this.decorationsByMarker.get(marker);
132            if (decorationsForMarker) {
133              decorationsForMarker.forEach(decoration => {
134                decorationPropertiesForMarker.push(decoration.getProperties());
135              });
136            }
137          }
138        }
139      });
140  
141      return decorationPropertiesByMarker;
142    }
143  
144    decorationsForScreenRowRange(startScreenRow, endScreenRow) {
145      const decorationsByMarkerId = {};
146      for (const layer of this.decorationCountsByLayer.keys()) {
147        for (const marker of layer.findMarkers({
148          intersectsScreenRowRange: [startScreenRow, endScreenRow]
149        })) {
150          const decorations = this.decorationsByMarker.get(marker);
151          if (decorations) {
152            decorationsByMarkerId[marker.id] = Array.from(decorations);
153          }
154        }
155      }
156      return decorationsByMarkerId;
157    }
158  
159    decorationsStateForScreenRowRange(startScreenRow, endScreenRow) {
160      const decorationsState = {};
161  
162      for (const layer of this.decorationCountsByLayer.keys()) {
163        for (const marker of layer.findMarkers({
164          intersectsScreenRowRange: [startScreenRow, endScreenRow]
165        })) {
166          if (marker.isValid()) {
167            const screenRange = marker.getScreenRange();
168            const bufferRange = marker.getBufferRange();
169            const rangeIsReversed = marker.isReversed();
170  
171            const decorations = this.decorationsByMarker.get(marker);
172            if (decorations) {
173              decorations.forEach(decoration => {
174                decorationsState[decoration.id] = {
175                  properties: decoration.properties,
176                  screenRange,
177                  bufferRange,
178                  rangeIsReversed
179                };
180              });
181            }
182  
183            const layerDecorations = this.layerDecorationsByMarkerLayer.get(
184              layer
185            );
186            if (layerDecorations) {
187              layerDecorations.forEach(layerDecoration => {
188                const properties =
189                  layerDecoration.getPropertiesForMarker(marker) ||
190                  layerDecoration.getProperties();
191                decorationsState[`${layerDecoration.id}-${marker.id}`] = {
192                  properties,
193                  screenRange,
194                  bufferRange,
195                  rangeIsReversed
196                };
197              });
198            }
199          }
200        }
201      }
202  
203      return decorationsState;
204    }
205  
206    decorateMarker(marker, decorationParams) {
207      if (marker.isDestroyed()) {
208        const error = new Error('Cannot decorate a destroyed marker');
209        error.metadata = { markerLayerIsDestroyed: marker.layer.isDestroyed() };
210        if (marker.destroyStackTrace != null) {
211          error.metadata.destroyStackTrace = marker.destroyStackTrace;
212        }
213        if (
214          marker.bufferMarker != null &&
215          marker.bufferMarker.destroyStackTrace != null
216        ) {
217          error.metadata.destroyStackTrace =
218            marker.bufferMarker.destroyStackTrace;
219        }
220        throw error;
221      }
222      marker = this.displayLayer
223        .getMarkerLayer(marker.layer.id)
224        .getMarker(marker.id);
225      const decoration = new Decoration(marker, this, decorationParams);
226      let decorationsForMarker = this.decorationsByMarker.get(marker);
227      if (!decorationsForMarker) {
228        decorationsForMarker = new Set();
229        this.decorationsByMarker.set(marker, decorationsForMarker);
230      }
231      decorationsForMarker.add(decoration);
232      if (decoration.isType('overlay')) this.overlayDecorations.add(decoration);
233      this.observeDecoratedLayer(marker.layer, true);
234      this.editor.didAddDecoration(decoration);
235      this.emitDidUpdateDecorations();
236      this.emitter.emit('did-add-decoration', decoration);
237      return decoration;
238    }
239  
240    decorateMarkerLayer(markerLayer, decorationParams) {
241      if (markerLayer.isDestroyed()) {
242        throw new Error('Cannot decorate a destroyed marker layer');
243      }
244      markerLayer = this.displayLayer.getMarkerLayer(markerLayer.id);
245      const decoration = new LayerDecoration(markerLayer, this, decorationParams);
246      let layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer);
247      if (layerDecorations == null) {
248        layerDecorations = new Set();
249        this.layerDecorationsByMarkerLayer.set(markerLayer, layerDecorations);
250      }
251      layerDecorations.add(decoration);
252      this.observeDecoratedLayer(markerLayer, false);
253      this.emitDidUpdateDecorations();
254      return decoration;
255    }
256  
257    emitDidUpdateDecorations() {
258      this.editor.scheduleComponentUpdate();
259      this.emitter.emit('did-update-decorations');
260    }
261  
262    decorationDidChangeType(decoration) {
263      if (decoration.isType('overlay')) {
264        this.overlayDecorations.add(decoration);
265      } else {
266        this.overlayDecorations.delete(decoration);
267      }
268    }
269  
270    didDestroyMarkerDecoration(decoration) {
271      const { marker } = decoration;
272      const decorations = this.decorationsByMarker.get(marker);
273      if (decorations && decorations.has(decoration)) {
274        decorations.delete(decoration);
275        if (decorations.size === 0) this.decorationsByMarker.delete(marker);
276        this.overlayDecorations.delete(decoration);
277        this.unobserveDecoratedLayer(marker.layer, true);
278        this.emitter.emit('did-remove-decoration', decoration);
279        this.emitDidUpdateDecorations();
280      }
281    }
282  
283    didDestroyLayerDecoration(decoration) {
284      const { markerLayer } = decoration;
285      const decorations = this.layerDecorationsByMarkerLayer.get(markerLayer);
286  
287      if (decorations && decorations.has(decoration)) {
288        decorations.delete(decoration);
289        if (decorations.size === 0) {
290          this.layerDecorationsByMarkerLayer.delete(markerLayer);
291        }
292        this.unobserveDecoratedLayer(markerLayer, true);
293        this.emitDidUpdateDecorations();
294      }
295    }
296  
297    observeDecoratedLayer(layer, isMarkerDecoration) {
298      const newCount = (this.decorationCountsByLayer.get(layer) || 0) + 1;
299      this.decorationCountsByLayer.set(layer, newCount);
300      if (newCount === 1) {
301        this.layerUpdateDisposablesByLayer.set(
302          layer,
303          layer.onDidUpdate(this.emitDidUpdateDecorations.bind(this))
304        );
305      }
306      if (isMarkerDecoration) {
307        this.markerDecorationCountsByLayer.set(
308          layer,
309          (this.markerDecorationCountsByLayer.get(layer) || 0) + 1
310        );
311      }
312    }
313  
314    unobserveDecoratedLayer(layer, isMarkerDecoration) {
315      const newCount = this.decorationCountsByLayer.get(layer) - 1;
316      if (newCount === 0) {
317        this.layerUpdateDisposablesByLayer.get(layer).dispose();
318        this.decorationCountsByLayer.delete(layer);
319      } else {
320        this.decorationCountsByLayer.set(layer, newCount);
321      }
322      if (isMarkerDecoration) {
323        this.markerDecorationCountsByLayer.set(
324          this.markerDecorationCountsByLayer.get(layer) - 1
325        );
326      }
327    }
328  };