DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (ba237def08d5)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/**
 * This module exports a provider, returning open tabs matches for the urlbar.
 * It is also used to register and unregister open tabs.
 */

var EXPORTED_SYMBOLS = ["UrlbarProviderOpenTabs"];

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
  Log: "resource://gre/modules/Log.jsm",
  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
  UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
  UrlbarResult: "resource:///modules/UrlbarResult.jsm",
  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
});

XPCOMUtils.defineLazyGetter(this, "logger", () =>
  Log.repository.getLogger("Urlbar.Provider.OpenTabs")
);

/**
 * Class used to create the provider.
 */
class ProviderOpenTabs extends UrlbarProvider {
  constructor() {
    super();
    // Maps the open tabs by userContextId.
    this.openTabs = new Map();
    // Maps the running queries by queryContext.
    this.queries = new Map();
  }

  /**
   * Database handle. For performance reasons the temp tables are created and
   * populated only when a query starts, rather than when a tab is added.
   * @returns {object} the Sqlite database handle.
   */
  async promiseDb() {
    if (this._db) {
      return this._db;
    }
    let conn = await PlacesUtils.promiseLargeCacheDBConnection();
    // Create the temp tables to store open pages.
    UrlbarProvidersManager.runInCriticalSection(async () => {
      // These should be kept up-to-date with the definition in nsPlacesTables.h.
      await conn.execute(`
        CREATE TEMP TABLE IF NOT EXISTS moz_openpages_temp (
          url TEXT,
          userContextId INTEGER,
          open_count INTEGER,
          PRIMARY KEY (url, userContextId)
        )
      `);
      await conn.execute(`
        CREATE TEMP TRIGGER IF NOT EXISTS moz_openpages_temp_afterupdate_trigger
        AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW
        WHEN NEW.open_count = 0
        BEGIN
          DELETE FROM moz_openpages_temp
          WHERE url = NEW.url
            AND userContextId = NEW.userContextId;
        END
      `);
    }).catch(Cu.reportError);

    // Populate the table with the current cache contents...
    for (let [userContextId, urls] of this.openTabs) {
      for (let url of urls) {
        await addToMemoryTable(conn, url, userContextId).catch(Cu.reportError);
      }
    }
    return (this._db = conn);
  }

  /**
   * Returns the name of this provider.
   * @returns {string} the name of this provider.
   */
  get name() {
    return "OpenTabs";
  }

  /**
   * Returns the type of this provider.
   * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
   */
  get type() {
    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
  }

  /**
   * Whether this provider should be invoked for the given context.
   * If this method returns false, the providers manager won't start a query
   * with this provider, to save on resources.
   * @param {UrlbarQueryContext} queryContext The query context object
   * @returns {boolean} Whether this provider should be invoked for the search.
   */
  isActive(queryContext) {
    // For now we don't actually use this provider to query open tabs, instead
    // we join the temp table in UnifiedComplete.
    return false;
  }

  /**
   * Whether this provider wants to restrict results to just itself.
   * Other providers won't be invoked, unless this provider doesn't
   * support the current query.
   * @param {UrlbarQueryContext} queryContext The query context object
   * @returns {boolean} Whether this provider wants to restrict results.
   */
  isRestricting(queryContext) {
    return false;
  }

  /**
   * Registers a tab as open.
   * @param {string} url Address of the tab
   * @param {integer} userContextId Containers user context id
   */
  registerOpenTab(url, userContextId = 0) {
    if (!this.openTabs.has(userContextId)) {
      this.openTabs.set(userContextId, []);
    }
    this.openTabs.get(userContextId).push(url);
    if (this._db) {
      addToMemoryTable(this._db, url, userContextId);
    }
  }

  /**
   * Unregisters a previously registered open tab.
   * @param {string} url Address of the tab
   * @param {integer} userContextId Containers user context id
   */
  unregisterOpenTab(url, userContextId = 0) {
    let openTabs = this.openTabs.get(userContextId);
    if (openTabs) {
      let index = openTabs.indexOf(url);
      if (index != -1) {
        openTabs.splice(index, 1);
        if (this._db) {
          removeFromMemoryTable(this._db, url, userContextId);
        }
      }
    }
  }

  /**
   * Starts querying.
   * @param {object} queryContext The query context object
   * @param {function} addCallback Callback invoked by the provider to add a new
   *        match.
   * @returns {Promise} resolved when the query stops.
   */
  async startQuery(queryContext, addCallback) {
    // Note: this is not actually expected to be used as an internal provider,
    // because normal history search will already coalesce with the open tabs
    // temp table to return proper frecency.
    // TODO:
    //  * properly search and handle tokens, this is just a mock for now.
    logger.info(`Starting query for ${queryContext.searchString}`);
    let instance = {};
    this.queries.set(queryContext, instance);
    let conn = await this.promiseDb();
    await conn.executeCached(
      `
      SELECT url, userContextId
      FROM moz_openpages_temp
    `,
      {},
      (row, cancel) => {
        if (!this.queries.has(queryContext)) {
          cancel();
          return;
        }
        addCallback(
          this,
          new UrlbarResult(
            UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
            UrlbarUtils.RESULT_SOURCE.TABS,
            {
              url: row.getResultByName("url"),
              userContextId: row.getResultByName("userContextId"),
            }
          )
        );
      }
    );
    // We are done.
    this.queries.delete(queryContext);
  }

  /**
   * Cancels a running query.
   * @param {object} queryContext The query context object
   */
  cancelQuery(queryContext) {
    logger.info(`Canceling query for ${queryContext.searchString}`);
    this.queries.delete(queryContext);
  }
}

var UrlbarProviderOpenTabs = new ProviderOpenTabs();

/**
 * Adds an open page to the memory table.
 * @param {object} conn A Sqlite.jsm database handle
 * @param {string} url Address of the page
 * @param {number} userContextId Containers user context id
 * @returns {Promise} resolved after the addition.
 */
async function addToMemoryTable(conn, url, userContextId) {
  return UrlbarProvidersManager.runInCriticalSection(async () => {
    await conn.executeCached(
      `
      INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count)
      VALUES ( :url,
                :userContextId,
                IFNULL( ( SELECT open_count + 1
                          FROM moz_openpages_temp
                          WHERE url = :url
                          AND userContextId = :userContextId ),
                        1
                      )
              )
    `,
      { url, userContextId }
    );
  });
}

/**
 * Removes an open page from the memory table.
 * @param {object} conn A Sqlite.jsm database handle
 * @param {string} url Address of the page
 * @param {number} userContextId Containers user context id
 * @returns {Promise} resolved after the removal.
 */
async function removeFromMemoryTable(conn, url, userContextId) {
  return UrlbarProvidersManager.runInCriticalSection(async () => {
    await conn.executeCached(
      `
      UPDATE moz_openpages_temp
      SET open_count = open_count - 1
      WHERE url = :url
        AND userContextId = :userContextId
    `,
      { url, userContextId }
    );
  });
}