toolkit/components/places/nsTaggingService.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/places/nsTaggingService.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,687 @@
     1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
     1.5 + * This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +const Cc = Components.classes;
    1.10 +const Ci = Components.interfaces;
    1.11 +const Cr = Components.results;
    1.12 +
    1.13 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    1.14 +Components.utils.import("resource://gre/modules/Services.jsm");
    1.15 +Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
    1.16 +
    1.17 +const TOPIC_SHUTDOWN = "places-shutdown";
    1.18 +
    1.19 +/**
    1.20 + * The Places Tagging Service
    1.21 + */
    1.22 +function TaggingService() {
    1.23 +  // Observe bookmarks changes.
    1.24 +  PlacesUtils.bookmarks.addObserver(this, false);
    1.25 +
    1.26 +  // Cleanup on shutdown.
    1.27 +  Services.obs.addObserver(this, TOPIC_SHUTDOWN, false);
    1.28 +}
    1.29 +
    1.30 +TaggingService.prototype = {
    1.31 +  /**
    1.32 +   * Creates a tag container under the tags-root with the given name.
    1.33 +   *
    1.34 +   * @param aTagName
    1.35 +   *        the name for the new tag.
    1.36 +   * @returns the id of the new tag container.
    1.37 +   */
    1.38 +  _createTag: function TS__createTag(aTagName) {
    1.39 +    var newFolderId = PlacesUtils.bookmarks.createFolder(
    1.40 +      PlacesUtils.tagsFolderId, aTagName, PlacesUtils.bookmarks.DEFAULT_INDEX
    1.41 +    );
    1.42 +    // Add the folder to our local cache, so we can avoid doing this in the
    1.43 +    // observer that would have to check itemType.
    1.44 +    this._tagFolders[newFolderId] = aTagName;
    1.45 +
    1.46 +    return newFolderId;
    1.47 +  },
    1.48 +
    1.49 +  /**
    1.50 +   * Checks whether the given uri is tagged with the given tag.
    1.51 +   *
    1.52 +   * @param [in] aURI
    1.53 +   *        url to check for
    1.54 +   * @param [in] aTagName
    1.55 +   *        the tag to check for
    1.56 +   * @returns the item id if the URI is tagged with the given tag, -1
    1.57 +   *          otherwise.
    1.58 +   */
    1.59 +  _getItemIdForTaggedURI: function TS__getItemIdForTaggedURI(aURI, aTagName) {
    1.60 +    var tagId = this._getItemIdForTag(aTagName);
    1.61 +    if (tagId == -1)
    1.62 +      return -1;
    1.63 +    // Using bookmarks service API for this would be a pain.
    1.64 +    // Until tags implementation becomes sane, go the query way.
    1.65 +    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
    1.66 +                                .DBConnection;
    1.67 +    let stmt = db.createStatement(
    1.68 +      "SELECT id FROM moz_bookmarks "
    1.69 +    + "WHERE parent = :tag_id "
    1.70 +    + "AND fk = (SELECT id FROM moz_places WHERE url = :page_url)"
    1.71 +    );
    1.72 +    stmt.params.tag_id = tagId;
    1.73 +    stmt.params.page_url = aURI.spec;
    1.74 +    try {
    1.75 +      if (stmt.executeStep()) {
    1.76 +        return stmt.row.id;
    1.77 +      }
    1.78 +    }
    1.79 +    finally {
    1.80 +      stmt.finalize();
    1.81 +    }
    1.82 +    return -1;
    1.83 +  },
    1.84 +
    1.85 +  /**
    1.86 +   * Returns the folder id for a tag, or -1 if not found.
    1.87 +   * @param [in] aTag
    1.88 +   *        string tag to search for
    1.89 +   * @returns integer id for the bookmark folder for the tag
    1.90 +   */
    1.91 +  _getItemIdForTag: function TS_getItemIdForTag(aTagName) {
    1.92 +    for (var i in this._tagFolders) {
    1.93 +      if (aTagName.toLowerCase() == this._tagFolders[i].toLowerCase())
    1.94 +        return parseInt(i);
    1.95 +    }
    1.96 +    return -1;
    1.97 +  },
    1.98 +
    1.99 +  /**
   1.100 +   * Makes a proper array of tag objects like  { id: number, name: string }.
   1.101 +   *
   1.102 +   * @param aTags
   1.103 +   *        Array of tags.  Entries can be tag names or concrete item id.
   1.104 +   * @return Array of tag objects like { id: number, name: string }.
   1.105 +   *
   1.106 +   * @throws Cr.NS_ERROR_INVALID_ARG if any element of the input array is not
   1.107 +   *         a valid tag.
   1.108 +   */
   1.109 +  _convertInputMixedTagsArray: function TS__convertInputMixedTagsArray(aTags)
   1.110 +  {
   1.111 +    return aTags.map(function (val)
   1.112 +    {
   1.113 +      let tag = { _self: this };
   1.114 +      if (typeof(val) == "number" && this._tagFolders[val]) {
   1.115 +        // This is a tag folder id.
   1.116 +        tag.id = val;
   1.117 +        // We can't know the name at this point, since a previous tag could
   1.118 +        // want to change it.
   1.119 +        tag.__defineGetter__("name", function () this._self._tagFolders[this.id]);
   1.120 +      }
   1.121 +      else if (typeof(val) == "string" && val.length > 0) {
   1.122 +        // This is a tag name.
   1.123 +        tag.name = val;
   1.124 +        // We can't know the id at this point, since a previous tag could
   1.125 +        // have created it.
   1.126 +        tag.__defineGetter__("id", function () this._self._getItemIdForTag(this.name));
   1.127 +      }
   1.128 +      else {
   1.129 +        throw Cr.NS_ERROR_INVALID_ARG;
   1.130 +      }
   1.131 +      return tag;
   1.132 +    }, this);
   1.133 +  },
   1.134 +
   1.135 +  // nsITaggingService
   1.136 +  tagURI: function TS_tagURI(aURI, aTags)
   1.137 +  {
   1.138 +    if (!aURI || !aTags || !Array.isArray(aTags)) {
   1.139 +      throw Cr.NS_ERROR_INVALID_ARG;
   1.140 +    }
   1.141 +
   1.142 +    // This also does some input validation.
   1.143 +    let tags = this._convertInputMixedTagsArray(aTags);
   1.144 +
   1.145 +    let taggingService = this;
   1.146 +    PlacesUtils.bookmarks.runInBatchMode({
   1.147 +      runBatched: function (aUserData)
   1.148 +      {
   1.149 +        tags.forEach(function (tag)
   1.150 +        {
   1.151 +          if (tag.id == -1) {
   1.152 +            // Tag does not exist yet, create it.
   1.153 +            this._createTag(tag.name);
   1.154 +          }
   1.155 +
   1.156 +          if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) {
   1.157 +            // The provided URI is not yet tagged, add a tag for it.
   1.158 +            // Note that bookmarks under tag containers must have null titles.
   1.159 +            PlacesUtils.bookmarks.insertBookmark(
   1.160 +              tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX, null
   1.161 +            );
   1.162 +          }
   1.163 +
   1.164 +          // Try to preserve user's tag name casing.
   1.165 +          // Rename the tag container so the Places view matches the most-recent
   1.166 +          // user-typed value.
   1.167 +          if (PlacesUtils.bookmarks.getItemTitle(tag.id) != tag.name) {
   1.168 +            // this._tagFolders is updated by the bookmarks observer.
   1.169 +            PlacesUtils.bookmarks.setItemTitle(tag.id, tag.name);
   1.170 +          }
   1.171 +        }, taggingService);
   1.172 +      }
   1.173 +    }, null);
   1.174 +  },
   1.175 +
   1.176 +  /**
   1.177 +   * Removes the tag container from the tags root if the given tag is empty.
   1.178 +   *
   1.179 +   * @param aTagId
   1.180 +   *        the itemId of the tag element under the tags root
   1.181 +   */
   1.182 +  _removeTagIfEmpty: function TS__removeTagIfEmpty(aTagId) {
   1.183 +    let count = 0;
   1.184 +    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   1.185 +                                .DBConnection;
   1.186 +    let stmt = db.createStatement(
   1.187 +      "SELECT count(*) AS count FROM moz_bookmarks "
   1.188 +    + "WHERE parent = :tag_id"
   1.189 +    );
   1.190 +    stmt.params.tag_id = aTagId;
   1.191 +    try {
   1.192 +      if (stmt.executeStep()) {
   1.193 +        count = stmt.row.count;
   1.194 +      }
   1.195 +    }
   1.196 +    finally {
   1.197 +      stmt.finalize();
   1.198 +    }
   1.199 +
   1.200 +    if (count == 0) {
   1.201 +      PlacesUtils.bookmarks.removeItem(aTagId);
   1.202 +    }
   1.203 +  },
   1.204 +
   1.205 +  // nsITaggingService
   1.206 +  untagURI: function TS_untagURI(aURI, aTags)
   1.207 +  {
   1.208 +    if (!aURI || (aTags && !Array.isArray(aTags))) {
   1.209 +      throw Cr.NS_ERROR_INVALID_ARG;
   1.210 +    }
   1.211 +
   1.212 +    if (!aTags) {
   1.213 +      // Passing null should clear all tags for aURI, see the IDL.
   1.214 +      // XXXmano: write a perf-sensitive version of this code path...
   1.215 +      aTags = this.getTagsForURI(aURI);
   1.216 +    }
   1.217 +
   1.218 +    // This also does some input validation.
   1.219 +    let tags = this._convertInputMixedTagsArray(aTags);
   1.220 +
   1.221 +    let taggingService = this;
   1.222 +    PlacesUtils.bookmarks.runInBatchMode({
   1.223 +      runBatched: function (aUserData)
   1.224 +      {
   1.225 +        tags.forEach(function (tag)
   1.226 +        {
   1.227 +          if (tag.id != -1) {
   1.228 +            // A tag could exist.
   1.229 +            let itemId = this._getItemIdForTaggedURI(aURI, tag.name);
   1.230 +            if (itemId != -1) {
   1.231 +              // There is a tagged item.
   1.232 +              PlacesUtils.bookmarks.removeItem(itemId);
   1.233 +            }
   1.234 +          }
   1.235 +        }, taggingService);
   1.236 +      }
   1.237 +    }, null);
   1.238 +  },
   1.239 +
   1.240 +  // nsITaggingService
   1.241 +  getURIsForTag: function TS_getURIsForTag(aTagName) {
   1.242 +    if (!aTagName || aTagName.length == 0)
   1.243 +      throw Cr.NS_ERROR_INVALID_ARG;
   1.244 +
   1.245 +    let uris = [];
   1.246 +    let tagId = this._getItemIdForTag(aTagName);
   1.247 +    if (tagId == -1)
   1.248 +      return uris;
   1.249 +
   1.250 +    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   1.251 +                                .DBConnection;
   1.252 +    let stmt = db.createStatement(
   1.253 +      "SELECT h.url FROM moz_places h "
   1.254 +    + "JOIN moz_bookmarks b ON b.fk = h.id "
   1.255 +    + "WHERE b.parent = :tag_id "
   1.256 +    );
   1.257 +    stmt.params.tag_id = tagId;
   1.258 +    try {
   1.259 +      while (stmt.executeStep()) {
   1.260 +        try {
   1.261 +          uris.push(Services.io.newURI(stmt.row.url, null, null));
   1.262 +        } catch (ex) {}
   1.263 +      }
   1.264 +    }
   1.265 +    finally {
   1.266 +      stmt.finalize();
   1.267 +    }
   1.268 +
   1.269 +    return uris;
   1.270 +  },
   1.271 +
   1.272 +  // nsITaggingService
   1.273 +  getTagsForURI: function TS_getTagsForURI(aURI, aCount) {
   1.274 +    if (!aURI)
   1.275 +      throw Cr.NS_ERROR_INVALID_ARG;
   1.276 +
   1.277 +    var tags = [];
   1.278 +    var bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(aURI);
   1.279 +    for (var i=0; i < bookmarkIds.length; i++) {
   1.280 +      var folderId = PlacesUtils.bookmarks.getFolderIdForItem(bookmarkIds[i]);
   1.281 +      if (this._tagFolders[folderId])
   1.282 +        tags.push(this._tagFolders[folderId]);
   1.283 +    }
   1.284 +
   1.285 +    // sort the tag list
   1.286 +    tags.sort(function(a, b) {
   1.287 +        return a.toLowerCase().localeCompare(b.toLowerCase());
   1.288 +      });
   1.289 +    if (aCount)
   1.290 +      aCount.value = tags.length;
   1.291 +    return tags;
   1.292 +  },
   1.293 +
   1.294 +  __tagFolders: null, 
   1.295 +  get _tagFolders() {
   1.296 +    if (!this.__tagFolders) {
   1.297 +      this.__tagFolders = [];
   1.298 +
   1.299 +      let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   1.300 +                                  .DBConnection;
   1.301 +      let stmt = db.createStatement(
   1.302 +        "SELECT id, title FROM moz_bookmarks WHERE parent = :tags_root "
   1.303 +      );
   1.304 +      stmt.params.tags_root = PlacesUtils.tagsFolderId;
   1.305 +      try {
   1.306 +        while (stmt.executeStep()) {
   1.307 +          this.__tagFolders[stmt.row.id] = stmt.row.title;
   1.308 +        }
   1.309 +      }
   1.310 +      finally {
   1.311 +        stmt.finalize();
   1.312 +      }
   1.313 +    }
   1.314 +
   1.315 +    return this.__tagFolders;
   1.316 +  },
   1.317 +
   1.318 +  // nsITaggingService
   1.319 +  get allTags() {
   1.320 +    var allTags = [];
   1.321 +    for (var i in this._tagFolders)
   1.322 +      allTags.push(this._tagFolders[i]);
   1.323 +    // sort the tag list
   1.324 +    allTags.sort(function(a, b) {
   1.325 +        return a.toLowerCase().localeCompare(b.toLowerCase());
   1.326 +      });
   1.327 +    return allTags;
   1.328 +  },
   1.329 +
   1.330 +  // nsITaggingService
   1.331 +  get hasTags() {
   1.332 +    return this._tagFolders.length > 0;
   1.333 +  },
   1.334 +
   1.335 +  // nsIObserver
   1.336 +  observe: function TS_observe(aSubject, aTopic, aData) {
   1.337 +    if (aTopic == TOPIC_SHUTDOWN) {
   1.338 +      PlacesUtils.bookmarks.removeObserver(this);
   1.339 +      Services.obs.removeObserver(this, TOPIC_SHUTDOWN);
   1.340 +    }
   1.341 +  },
   1.342 +
   1.343 +  /**
   1.344 +   * If the only bookmark items associated with aURI are contained in tag
   1.345 +   * folders, returns the IDs of those items.  This can be the case if
   1.346 +   * the URI was bookmarked and tagged at some point, but the bookmark was
   1.347 +   * removed, leaving only the bookmark items in tag folders.  If the URI is
   1.348 +   * either properly bookmarked or not tagged just returns and empty array.
   1.349 +   *
   1.350 +   * @param   aURI
   1.351 +   *          A URI (string) that may or may not be bookmarked
   1.352 +   * @returns an array of item ids
   1.353 +   */
   1.354 +  _getTaggedItemIdsIfUnbookmarkedURI:
   1.355 +  function TS__getTaggedItemIdsIfUnbookmarkedURI(aURI) {
   1.356 +    var itemIds = [];
   1.357 +    var isBookmarked = false;
   1.358 +
   1.359 +    // Using bookmarks service API for this would be a pain.
   1.360 +    // Until tags implementation becomes sane, go the query way.
   1.361 +    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   1.362 +                                .DBConnection;
   1.363 +    let stmt = db.createStatement(
   1.364 +      "SELECT id, parent "
   1.365 +    + "FROM moz_bookmarks "
   1.366 +    + "WHERE fk = (SELECT id FROM moz_places WHERE url = :page_url)"
   1.367 +    );
   1.368 +    stmt.params.page_url = aURI.spec;
   1.369 +    try {
   1.370 +      while (stmt.executeStep() && !isBookmarked) {
   1.371 +        if (this._tagFolders[stmt.row.parent]) {
   1.372 +          // This is a tag entry.
   1.373 +          itemIds.push(stmt.row.id);
   1.374 +        }
   1.375 +        else {
   1.376 +          // This is a real bookmark, so the bookmarked URI is not an orphan.
   1.377 +          isBookmarked = true;
   1.378 +        }
   1.379 +      }
   1.380 +    }
   1.381 +    finally {
   1.382 +      stmt.finalize();
   1.383 +    }
   1.384 +
   1.385 +    return isBookmarked ? [] : itemIds;
   1.386 +  },
   1.387 +
   1.388 +  // nsINavBookmarkObserver
   1.389 +  onItemAdded: function TS_onItemAdded(aItemId, aFolderId, aIndex, aItemType,
   1.390 +                                       aURI, aTitle) {
   1.391 +    // Nothing to do if this is not a tag.
   1.392 +    if (aFolderId != PlacesUtils.tagsFolderId ||
   1.393 +        aItemType != PlacesUtils.bookmarks.TYPE_FOLDER)
   1.394 +      return;
   1.395 +
   1.396 +    this._tagFolders[aItemId] = aTitle;
   1.397 +  },
   1.398 +
   1.399 +  onItemRemoved: function TS_onItemRemoved(aItemId, aFolderId, aIndex,
   1.400 +                                           aItemType, aURI) {
   1.401 +    // Item is a tag folder.
   1.402 +    if (aFolderId == PlacesUtils.tagsFolderId && this._tagFolders[aItemId]) {
   1.403 +      delete this._tagFolders[aItemId];
   1.404 +    }
   1.405 +    // Item is a bookmark that was removed from a non-tag folder.
   1.406 +    else if (aURI && !this._tagFolders[aFolderId]) {
   1.407 +      // If the only bookmark items now associated with the bookmark's URI are
   1.408 +      // contained in tag folders, the URI is no longer properly bookmarked, so
   1.409 +      // untag it.
   1.410 +      let itemIds = this._getTaggedItemIdsIfUnbookmarkedURI(aURI);
   1.411 +      for (let i = 0; i < itemIds.length; i++) {
   1.412 +        try {
   1.413 +          PlacesUtils.bookmarks.removeItem(itemIds[i]);
   1.414 +        } catch (ex) {}
   1.415 +      }
   1.416 +    }
   1.417 +    // Item is a tag entry.  If this was the last entry for this tag, remove it.
   1.418 +    else if (aURI && this._tagFolders[aFolderId]) {
   1.419 +      this._removeTagIfEmpty(aFolderId);
   1.420 +    }
   1.421 +  },
   1.422 +
   1.423 +  onItemChanged: function TS_onItemChanged(aItemId, aProperty,
   1.424 +                                           aIsAnnotationProperty, aNewValue,
   1.425 +                                           aLastModified, aItemType) {
   1.426 +    if (aProperty == "title" && this._tagFolders[aItemId])
   1.427 +      this._tagFolders[aItemId] = aNewValue;
   1.428 +  },
   1.429 +
   1.430 +  onItemMoved: function TS_onItemMoved(aItemId, aOldParent, aOldIndex,
   1.431 +                                      aNewParent, aNewIndex, aItemType) {
   1.432 +    if (this._tagFolders[aItemId] && PlacesUtils.tagsFolderId == aOldParent &&
   1.433 +        PlacesUtils.tagsFolderId != aNewParent)
   1.434 +      delete this._tagFolders[aItemId];
   1.435 +  },
   1.436 +
   1.437 +  onItemVisited: function () {},
   1.438 +  onBeginUpdateBatch: function () {},
   1.439 +  onEndUpdateBatch: function () {},
   1.440 +
   1.441 +  //////////////////////////////////////////////////////////////////////////////
   1.442 +  //// nsISupports
   1.443 +
   1.444 +  classID: Components.ID("{bbc23860-2553-479d-8b78-94d9038334f7}"),
   1.445 +
   1.446 +  _xpcom_factory: XPCOMUtils.generateSingletonFactory(TaggingService),
   1.447 +
   1.448 +  QueryInterface: XPCOMUtils.generateQI([
   1.449 +    Ci.nsITaggingService
   1.450 +  , Ci.nsINavBookmarkObserver
   1.451 +  , Ci.nsIObserver
   1.452 +  ])
   1.453 +};
   1.454 +
   1.455 +
   1.456 +function TagAutoCompleteResult(searchString, searchResult,
   1.457 +                               defaultIndex, errorDescription,
   1.458 +                               results, comments) {
   1.459 +  this._searchString = searchString;
   1.460 +  this._searchResult = searchResult;
   1.461 +  this._defaultIndex = defaultIndex;
   1.462 +  this._errorDescription = errorDescription;
   1.463 +  this._results = results;
   1.464 +  this._comments = comments;
   1.465 +}
   1.466 +
   1.467 +TagAutoCompleteResult.prototype = {
   1.468 +  
   1.469 +  /**
   1.470 +   * The original search string
   1.471 +   */
   1.472 +  get searchString() {
   1.473 +    return this._searchString;
   1.474 +  },
   1.475 +
   1.476 +  /**
   1.477 +   * The result code of this result object, either:
   1.478 +   *         RESULT_IGNORED   (invalid searchString)
   1.479 +   *         RESULT_FAILURE   (failure)
   1.480 +   *         RESULT_NOMATCH   (no matches found)
   1.481 +   *         RESULT_SUCCESS   (matches found)
   1.482 +   */
   1.483 +  get searchResult() {
   1.484 +    return this._searchResult;
   1.485 +  },
   1.486 +
   1.487 +  /**
   1.488 +   * Index of the default item that should be entered if none is selected
   1.489 +   */
   1.490 +  get defaultIndex() {
   1.491 +    return this._defaultIndex;
   1.492 +  },
   1.493 +
   1.494 +  /**
   1.495 +   * A string describing the cause of a search failure
   1.496 +   */
   1.497 +  get errorDescription() {
   1.498 +    return this._errorDescription;
   1.499 +  },
   1.500 +
   1.501 +  /**
   1.502 +   * The number of matches
   1.503 +   */
   1.504 +  get matchCount() {
   1.505 +    return this._results.length;
   1.506 +  },
   1.507 +
   1.508 +  get typeAheadResult() false,
   1.509 +
   1.510 +  /**
   1.511 +   * Get the value of the result at the given index
   1.512 +   */
   1.513 +  getValueAt: function PTACR_getValueAt(index) {
   1.514 +    return this._results[index];
   1.515 +  },
   1.516 +
   1.517 +  getLabelAt: function PTACR_getLabelAt(index) {
   1.518 +    return this.getValueAt(index);
   1.519 +  },
   1.520 +
   1.521 +  /**
   1.522 +   * Get the comment of the result at the given index
   1.523 +   */
   1.524 +  getCommentAt: function PTACR_getCommentAt(index) {
   1.525 +    return this._comments[index];
   1.526 +  },
   1.527 +
   1.528 +  /**
   1.529 +   * Get the style hint for the result at the given index
   1.530 +   */
   1.531 +  getStyleAt: function PTACR_getStyleAt(index) {
   1.532 +    if (!this._comments[index])
   1.533 +      return null;  // not a category label, so no special styling
   1.534 +
   1.535 +    if (index == 0)
   1.536 +      return "suggestfirst";  // category label on first line of results
   1.537 +
   1.538 +    return "suggesthint";   // category label on any other line of results
   1.539 +  },
   1.540 +
   1.541 +  /**
   1.542 +   * Get the image for the result at the given index
   1.543 +   */
   1.544 +  getImageAt: function PTACR_getImageAt(index) {
   1.545 +    return null;
   1.546 +  },
   1.547 +
   1.548 +  /**
   1.549 +   * Get the image for the result at the given index
   1.550 +   */
   1.551 +  getFinalCompleteValueAt: function PTACR_getFinalCompleteValueAt(index) {
   1.552 +    return this.getValueAt(index);
   1.553 +  },
   1.554 +
   1.555 +  /**
   1.556 +   * Remove the value at the given index from the autocomplete results.
   1.557 +   * If removeFromDb is set to true, the value should be removed from
   1.558 +   * persistent storage as well.
   1.559 +   */
   1.560 +  removeValueAt: function PTACR_removeValueAt(index, removeFromDb) {
   1.561 +    this._results.splice(index, 1);
   1.562 +    this._comments.splice(index, 1);
   1.563 +  },
   1.564 +
   1.565 +  // nsISupports
   1.566 +  QueryInterface: XPCOMUtils.generateQI([
   1.567 +    Ci.nsIAutoCompleteResult
   1.568 +  ])
   1.569 +};
   1.570 +
   1.571 +// Implements nsIAutoCompleteSearch
   1.572 +function TagAutoCompleteSearch() {
   1.573 +  XPCOMUtils.defineLazyServiceGetter(this, "tagging",
   1.574 +                                     "@mozilla.org/browser/tagging-service;1",
   1.575 +                                     "nsITaggingService");
   1.576 +}
   1.577 +
   1.578 +TagAutoCompleteSearch.prototype = {
   1.579 +  _stopped : false, 
   1.580 +
   1.581 +  /*
   1.582 +   * Search for a given string and notify a listener (either synchronously
   1.583 +   * or asynchronously) of the result
   1.584 +   *
   1.585 +   * @param searchString - The string to search for
   1.586 +   * @param searchParam - An extra parameter
   1.587 +   * @param previousResult - A previous result to use for faster searching
   1.588 +   * @param listener - A listener to notify when the search is complete
   1.589 +   */
   1.590 +  startSearch: function PTACS_startSearch(searchString, searchParam, result, listener) {
   1.591 +    var searchResults = this.tagging.allTags;
   1.592 +    var results = [];
   1.593 +    var comments = [];
   1.594 +    this._stopped = false;
   1.595 +
   1.596 +    // only search on characters for the last tag
   1.597 +    var index = Math.max(searchString.lastIndexOf(","), 
   1.598 +      searchString.lastIndexOf(";"));
   1.599 +    var before = ''; 
   1.600 +    if (index != -1) {  
   1.601 +      before = searchString.slice(0, index+1);
   1.602 +      searchString = searchString.slice(index+1);
   1.603 +      // skip past whitespace
   1.604 +      var m = searchString.match(/\s+/);
   1.605 +      if (m) {
   1.606 +         before += m[0];
   1.607 +         searchString = searchString.slice(m[0].length);
   1.608 +      }
   1.609 +    }
   1.610 +
   1.611 +    if (!searchString.length) {
   1.612 +      var newResult = new TagAutoCompleteResult(searchString,
   1.613 +        Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 0, "", results, comments);
   1.614 +      listener.onSearchResult(self, newResult);
   1.615 +      return;
   1.616 +    }
   1.617 +    
   1.618 +    var self = this;
   1.619 +    // generator: if yields true, not done
   1.620 +    function doSearch() {
   1.621 +      var i = 0;
   1.622 +      while (i < searchResults.length) {
   1.623 +        if (self._stopped)
   1.624 +          yield false;
   1.625 +        // for each match, prepend what the user has typed so far
   1.626 +        if (searchResults[i].toLowerCase()
   1.627 +                            .indexOf(searchString.toLowerCase()) == 0 &&
   1.628 +            comments.indexOf(searchResults[i]) == -1) {
   1.629 +          results.push(before + searchResults[i]);
   1.630 +          comments.push(searchResults[i]);
   1.631 +        }
   1.632 +    
   1.633 +        ++i;
   1.634 +
   1.635 +        /* TODO: bug 481451
   1.636 +         * For each yield we pass a new result to the autocomplete
   1.637 +         * listener. The listener appends instead of replacing previous results,
   1.638 +         * causing invalid matchCount values.
   1.639 +         *
   1.640 +         * As a workaround, all tags are searched through in a single batch,
   1.641 +         * making this synchronous until the above issue is fixed.
   1.642 +         */
   1.643 +
   1.644 +        /*
   1.645 +        // 100 loops per yield
   1.646 +        if ((i % 100) == 0) {
   1.647 +          var newResult = new TagAutoCompleteResult(searchString,
   1.648 +            Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING, 0, "", results, comments);
   1.649 +          listener.onSearchResult(self, newResult);
   1.650 +          yield true;
   1.651 +        }
   1.652 +        */
   1.653 +      }
   1.654 +
   1.655 +      let searchResult = results.length > 0 ?
   1.656 +                           Ci.nsIAutoCompleteResult.RESULT_SUCCESS :
   1.657 +                           Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
   1.658 +      var newResult = new TagAutoCompleteResult(searchString, searchResult, 0,
   1.659 +                                                "", results, comments);
   1.660 +      listener.onSearchResult(self, newResult);
   1.661 +      yield false;
   1.662 +    }
   1.663 +    
   1.664 +    // chunk the search results via the generator
   1.665 +    var gen = doSearch();
   1.666 +    while (gen.next());
   1.667 +    gen.close();
   1.668 +  },
   1.669 +
   1.670 +  /**
   1.671 +   * Stop an asynchronous search that is in progress
   1.672 +   */
   1.673 +  stopSearch: function PTACS_stopSearch() {
   1.674 +    this._stopped = true;
   1.675 +  },
   1.676 +
   1.677 +  //////////////////////////////////////////////////////////////////////////////
   1.678 +  //// nsISupports
   1.679 +
   1.680 +  classID: Components.ID("{1dcc23b0-d4cb-11dc-9ad6-479d56d89593}"),
   1.681 +
   1.682 +  _xpcom_factory: XPCOMUtils.generateSingletonFactory(TagAutoCompleteSearch),
   1.683 +
   1.684 +  QueryInterface: XPCOMUtils.generateQI([
   1.685 +    Ci.nsIAutoCompleteSearch
   1.686 +  ])
   1.687 +};
   1.688 +
   1.689 +let component = [TaggingService, TagAutoCompleteSearch];
   1.690 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial