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 };