/ src / decoration.js
decoration.js
  1  const { Emitter } = require('event-kit');
  2  
  3  let idCounter = 0;
  4  const nextId = () => idCounter++;
  5  
  6  const normalizeDecorationProperties = function(decoration, decorationParams) {
  7    decorationParams.id = decoration.id;
  8  
  9    if (
 10      decorationParams.type === 'line-number' &&
 11      decorationParams.gutterName == null
 12    ) {
 13      decorationParams.gutterName = 'line-number';
 14    }
 15  
 16    if (decorationParams.order == null) {
 17      decorationParams.order = Infinity;
 18    }
 19  
 20    return decorationParams;
 21  };
 22  
 23  // Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is
 24  // basically a visual representation of a marker. It allows you to add CSS
 25  // classes to line numbers in the gutter, lines, and add selection-line regions
 26  // around marked ranges of text.
 27  //
 28  // {Decoration} objects are not meant to be created directly, but created with
 29  // {TextEditor::decorateMarker}. eg.
 30  //
 31  // ```coffee
 32  // range = editor.getSelectedBufferRange() # any range you like
 33  // marker = editor.markBufferRange(range)
 34  // decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
 35  // ```
 36  //
 37  // Best practice for destroying the decoration is by destroying the {DisplayMarker}.
 38  //
 39  // ```coffee
 40  // marker.destroy()
 41  // ```
 42  //
 43  // You should only use {Decoration::destroy} when you still need or do not own
 44  // the marker.
 45  module.exports = class Decoration {
 46    // Private: Check if the `decorationProperties.type` matches `type`
 47    //
 48    // * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
 49    // * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
 50    //   be an {Array} of {String}s, where it will return true if the decoration's
 51    //   type matches any in the array.
 52    //
 53    // Returns {Boolean}
 54    // Note: 'line-number' is a special subtype of the 'gutter' type. I.e., a
 55    // 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'.
 56    static isType(decorationProperties, type) {
 57      // 'line-number' is a special case of 'gutter'.
 58      if (Array.isArray(decorationProperties.type)) {
 59        if (decorationProperties.type.includes(type)) {
 60          return true;
 61        }
 62  
 63        if (
 64          type === 'gutter' &&
 65          decorationProperties.type.includes('line-number')
 66        ) {
 67          return true;
 68        }
 69  
 70        return false;
 71      } else {
 72        if (type === 'gutter') {
 73          return ['gutter', 'line-number'].includes(decorationProperties.type);
 74        } else {
 75          return type === decorationProperties.type;
 76        }
 77      }
 78    }
 79  
 80    /*
 81    Section: Construction and Destruction
 82    */
 83  
 84    constructor(marker, decorationManager, properties) {
 85      this.marker = marker;
 86      this.decorationManager = decorationManager;
 87      this.emitter = new Emitter();
 88      this.id = nextId();
 89      this.setProperties(properties);
 90      this.destroyed = false;
 91      this.markerDestroyDisposable = this.marker.onDidDestroy(() =>
 92        this.destroy()
 93      );
 94    }
 95  
 96    // Essential: Destroy this marker decoration.
 97    //
 98    // You can also destroy the marker if you own it, which will destroy this
 99    // decoration.
100    destroy() {
101      if (this.destroyed) {
102        return;
103      }
104      this.markerDestroyDisposable.dispose();
105      this.markerDestroyDisposable = null;
106      this.destroyed = true;
107      this.decorationManager.didDestroyMarkerDecoration(this);
108      this.emitter.emit('did-destroy');
109      return this.emitter.dispose();
110    }
111  
112    isDestroyed() {
113      return this.destroyed;
114    }
115  
116    /*
117    Section: Event Subscription
118    */
119  
120    // Essential: When the {Decoration} is updated via {Decoration::update}.
121    //
122    // * `callback` {Function}
123    //   * `event` {Object}
124    //     * `oldProperties` {Object} the old parameters the decoration used to have
125    //     * `newProperties` {Object} the new parameters the decoration now has
126    //
127    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
128    onDidChangeProperties(callback) {
129      return this.emitter.on('did-change-properties', callback);
130    }
131  
132    // Essential: Invoke the given callback when the {Decoration} is destroyed
133    //
134    // * `callback` {Function}
135    //
136    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
137    onDidDestroy(callback) {
138      return this.emitter.once('did-destroy', callback);
139    }
140  
141    /*
142    Section: Decoration Details
143    */
144  
145    // Essential: An id unique across all {Decoration} objects
146    getId() {
147      return this.id;
148    }
149  
150    // Essential: Returns the marker associated with this {Decoration}
151    getMarker() {
152      return this.marker;
153    }
154  
155    // Public: Check if this decoration is of type `type`
156    //
157    // * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
158    //   be an {Array} of {String}s, where it will return true if the decoration's
159    //   type matches any in the array.
160    //
161    // Returns {Boolean}
162    isType(type) {
163      return Decoration.isType(this.properties, type);
164    }
165  
166    /*
167    Section: Properties
168    */
169  
170    // Essential: Returns the {Decoration}'s properties.
171    getProperties() {
172      return this.properties;
173    }
174  
175    // Essential: Update the marker with new Properties. Allows you to change the decoration's class.
176    //
177    // ## Examples
178    //
179    // ```coffee
180    // decoration.setProperties({type: 'line-number', class: 'my-new-class'})
181    // ```
182    //
183    // * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
184    setProperties(newProperties) {
185      if (this.destroyed) {
186        return;
187      }
188      const oldProperties = this.properties;
189      this.properties = normalizeDecorationProperties(this, newProperties);
190      if (newProperties.type != null) {
191        this.decorationManager.decorationDidChangeType(this);
192      }
193      this.decorationManager.emitDidUpdateDecorations();
194      return this.emitter.emit('did-change-properties', {
195        oldProperties,
196        newProperties
197      });
198    }
199  
200    /*
201    Section: Utility
202    */
203  
204    inspect() {
205      return `<Decoration ${this.id}>`;
206    }
207  
208    /*
209    Section: Private methods
210    */
211  
212    matchesPattern(decorationPattern) {
213      if (decorationPattern == null) {
214        return false;
215      }
216      for (let key in decorationPattern) {
217        const value = decorationPattern[key];
218        if (this.properties[key] !== value) {
219          return false;
220        }
221      }
222      return true;
223    }
224  
225    flash(klass, duration) {
226      if (duration == null) {
227        duration = 500;
228      }
229      this.properties.flashRequested = true;
230      this.properties.flashClass = klass;
231      this.properties.flashDuration = duration;
232      this.decorationManager.emitDidUpdateDecorations();
233      return this.emitter.emit('did-flash');
234    }
235  };