| Index: lib/subscriptionClasses.js | 
| =================================================================== | 
| --- a/lib/subscriptionClasses.js | 
| +++ b/lib/subscriptionClasses.js | 
| @@ -31,19 +31,23 @@ | 
| * | 
| * @param {string} url    download location of the subscription | 
| * @param {string} [title]  title of the filter subscription | 
| * @constructor | 
| */ | 
| function Subscription(url, title) | 
| { | 
| this.url = url; | 
| -  this.filters = []; | 
| + | 
| +  this._filterText = []; | 
| +  this._filters = []; | 
| + | 
| if (title) | 
| this._title = title; | 
| + | 
| Subscription.knownSubscriptions.set(url, this); | 
| } | 
| exports.Subscription = Subscription; | 
|  | 
| Subscription.prototype = | 
| { | 
| /** | 
| * Download location of the subscription | 
| @@ -53,20 +57,36 @@ | 
|  | 
| /** | 
| * Type of the subscription | 
| * @type {?string} | 
| */ | 
| type: null, | 
|  | 
| /** | 
| -   * Filters contained in the filter subscription | 
| -   * @type {Filter[]} | 
| +   * Filter text contained in the filter subscription. | 
| +   * @type {Array.<string>} | 
| +   * @private | 
| */ | 
| -  filters: null, | 
| +  _filterText: null, | 
| + | 
| +  /** | 
| +   * {@link Filter} objects corresponding to the subscription's filter text. | 
| +   * @type {Array.<Filter>} | 
| +   * @private | 
| +   */ | 
| +  _filters: null, | 
| + | 
| +  /** | 
| +   * Set of filter text contained in the filter subscription, used for faster | 
| +   * lookup. | 
| +   * @type {?Set.<string>} | 
| +   * @private | 
| +   */ | 
| +  _filterTextLookup: null, | 
|  | 
| _title: null, | 
| _fixedTitle: false, | 
| _disabled: false, | 
|  | 
| /** | 
| * Title of the filter subscription | 
| * @type {string} | 
| @@ -120,16 +140,139 @@ | 
| let oldValue = this._disabled; | 
| this._disabled = value; | 
| filterNotifier.emit("subscription.disabled", this, value, oldValue); | 
| } | 
| return this._disabled; | 
| }, | 
|  | 
| /** | 
| +   * The number of filters in the subscription. | 
| +   * @type {number} | 
| +   */ | 
| +  get filterCount() | 
| +  { | 
| +    return this._filters.length; | 
| +  }, | 
| + | 
| +  /** | 
| +   * Yields the text for each filter in the subscription. | 
| +   * @yields {string} | 
| +   */ | 
| +  *filterText() | 
| +  { | 
| +    yield* this._filterText; | 
| +  }, | 
| + | 
| +  /** | 
| +   * Yields the {@link Filter} object for each filter in the subscription. | 
| +   * @yields {Filter} | 
| +   */ | 
| +  *filters() | 
| +  { | 
| +    yield* this._filters; | 
| +  }, | 
| + | 
| +  /** | 
| +   * Returns the {@link Filter} object at the given 0-based index. | 
| +   * @param {number} index | 
| +   * @returns {?Filter} | 
| +   */ | 
| +  filterAt(index) | 
| +  { | 
| +    return this._filters[index] || null; | 
| +  }, | 
| + | 
| +  /** | 
| +   * Returns the 0-based index of the given filter. | 
| +   * @param {Filter} filter | 
| +   * @param {number} [fromIndex] The index from which to start the search. | 
| +   * @return {number} | 
| +   */ | 
| +  searchFilter(filter, fromIndex = 0) | 
| +  { | 
| +    return this._filterText.indexOf(filter.text, fromIndex); | 
| +  }, | 
| + | 
| +  /** | 
| +   * Checks whether the subscription contains the given filter. | 
| +   * @param {Filter} filter | 
| +   * @return {boolean} | 
| +   */ | 
| +  hasFilter(filter) | 
| +  { | 
| +    if (!this._filterTextLookup) | 
| +      this._filterTextLookup = new Set([...this._filterText]); | 
| + | 
| +    return this._filterTextLookup.has(filter.text); | 
| +  }, | 
| + | 
| +  /** | 
| +   * Removes all filters from the subscription. | 
| +   */ | 
| +  clearFilters() | 
| +  { | 
| +    this._filterText = []; | 
| +    this._filters = []; | 
| + | 
| +    this._filterTextLookup = null; | 
| +  }, | 
| + | 
| +  /** | 
| +   * Adds a filter to the subscription. | 
| +   * @param {Filter} filter | 
| +   */ | 
| +  addFilter(filter) | 
| +  { | 
| +    this._filterText.push(filter.text); | 
| +    this._filters.push(filter); | 
| + | 
| +    // Invalidate filter text lookup. | 
| +    this._filterTextLookup = null; | 
| +  }, | 
| + | 
| +  /** | 
| +   * Inserts a filter into the subscription. | 
| +   * @param {Filter} filter | 
| +   * @param {number} index The index at which to insert the filter. | 
| +   */ | 
| +  insertFilterAt(filter, index) | 
| +  { | 
| +    this._filterText.splice(index, 0, filter.text); | 
| +    this._filters.splice(index, 0, filter); | 
| + | 
| +    this._filterTextLookup = null; | 
| +  }, | 
| + | 
| +  /** | 
| +   * Deletes a filter from the subscription. | 
| +   * @param {number} index The index at which to delete the filter. | 
| +   */ | 
| +  deleteFilterAt(index) | 
| +  { | 
| +    // Ignore index if out of bounds on the negative side, for consistency. | 
| +    if (index < 0) | 
| +      return; | 
| + | 
| +    this._filterText.splice(index, 1); | 
| +    this._filters.splice(index, 1); | 
| + | 
| +    this._filterTextLookup = null; | 
| +  }, | 
| + | 
| +  /** | 
| +   * Clears any in-memory caches held by the object. | 
| +   * @package | 
| +   */ | 
| +  clearCaches() | 
| +  { | 
| +    this._filterTextLookup = null; | 
| +  }, | 
| + | 
| +  /** | 
| * Serializes the subscription for writing out on disk. | 
| * @yields {string} | 
| */ | 
| *serialize() | 
| { | 
| let {url, type, _title, _fixedTitle, _disabled} = this; | 
|  | 
| yield "[Subscription]"; | 
| @@ -142,22 +285,22 @@ | 
| if (_fixedTitle) | 
| yield "fixedTitle=true"; | 
| if (_disabled) | 
| yield "disabled=true"; | 
| }, | 
|  | 
| *serializeFilters() | 
| { | 
| -    let {filters} = this; | 
| +    let {_filterText} = this; | 
|  | 
| yield "[Subscription filters]"; | 
|  | 
| -    for (let filter of filters) | 
| -      yield filter.text.replace(/\[/g, "\\["); | 
| +    for (let text of _filterText) | 
| +      yield text.replace(/\[/g, "\\["); | 
| }, | 
|  | 
| toString() | 
| { | 
| return [...this.serialize()].join("\n"); | 
| } | 
| }; | 
|  | 
| @@ -328,17 +471,17 @@ | 
| * Creates a new user-defined filter group and adds the given filter to it. | 
| * This group will act as the default group for this filter type. | 
| * @param {Filter} filter | 
| * @return {SpecialSubscription} | 
| */ | 
| SpecialSubscription.createForFilter = function(filter) | 
| { | 
| let subscription = SpecialSubscription.create(); | 
| -  subscription.filters.push(filter); | 
| +  subscription.addFilter(filter); | 
| for (let [type, class_] of SpecialSubscription.defaultsMap) | 
| { | 
| if (filter instanceof class_) | 
| subscription.defaults = [type]; | 
| } | 
| if (!subscription.defaults) | 
| subscription.defaults = ["blocking"]; | 
| return subscription; | 
|  |