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