michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); michael@0: michael@0: const TOPIC_SHUTDOWN = "places-shutdown"; michael@0: michael@0: /** michael@0: * The Places Tagging Service michael@0: */ michael@0: function TaggingService() { michael@0: // Observe bookmarks changes. michael@0: PlacesUtils.bookmarks.addObserver(this, false); michael@0: michael@0: // Cleanup on shutdown. michael@0: Services.obs.addObserver(this, TOPIC_SHUTDOWN, false); michael@0: } michael@0: michael@0: TaggingService.prototype = { michael@0: /** michael@0: * Creates a tag container under the tags-root with the given name. michael@0: * michael@0: * @param aTagName michael@0: * the name for the new tag. michael@0: * @returns the id of the new tag container. michael@0: */ michael@0: _createTag: function TS__createTag(aTagName) { michael@0: var newFolderId = PlacesUtils.bookmarks.createFolder( michael@0: PlacesUtils.tagsFolderId, aTagName, PlacesUtils.bookmarks.DEFAULT_INDEX michael@0: ); michael@0: // Add the folder to our local cache, so we can avoid doing this in the michael@0: // observer that would have to check itemType. michael@0: this._tagFolders[newFolderId] = aTagName; michael@0: michael@0: return newFolderId; michael@0: }, michael@0: michael@0: /** michael@0: * Checks whether the given uri is tagged with the given tag. michael@0: * michael@0: * @param [in] aURI michael@0: * url to check for michael@0: * @param [in] aTagName michael@0: * the tag to check for michael@0: * @returns the item id if the URI is tagged with the given tag, -1 michael@0: * otherwise. michael@0: */ michael@0: _getItemIdForTaggedURI: function TS__getItemIdForTaggedURI(aURI, aTagName) { michael@0: var tagId = this._getItemIdForTag(aTagName); michael@0: if (tagId == -1) michael@0: return -1; michael@0: // Using bookmarks service API for this would be a pain. michael@0: // Until tags implementation becomes sane, go the query way. michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: let stmt = db.createStatement( michael@0: "SELECT id FROM moz_bookmarks " michael@0: + "WHERE parent = :tag_id " michael@0: + "AND fk = (SELECT id FROM moz_places WHERE url = :page_url)" michael@0: ); michael@0: stmt.params.tag_id = tagId; michael@0: stmt.params.page_url = aURI.spec; michael@0: try { michael@0: if (stmt.executeStep()) { michael@0: return stmt.row.id; michael@0: } michael@0: } michael@0: finally { michael@0: stmt.finalize(); michael@0: } michael@0: return -1; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the folder id for a tag, or -1 if not found. michael@0: * @param [in] aTag michael@0: * string tag to search for michael@0: * @returns integer id for the bookmark folder for the tag michael@0: */ michael@0: _getItemIdForTag: function TS_getItemIdForTag(aTagName) { michael@0: for (var i in this._tagFolders) { michael@0: if (aTagName.toLowerCase() == this._tagFolders[i].toLowerCase()) michael@0: return parseInt(i); michael@0: } michael@0: return -1; michael@0: }, michael@0: michael@0: /** michael@0: * Makes a proper array of tag objects like { id: number, name: string }. michael@0: * michael@0: * @param aTags michael@0: * Array of tags. Entries can be tag names or concrete item id. michael@0: * @return Array of tag objects like { id: number, name: string }. michael@0: * michael@0: * @throws Cr.NS_ERROR_INVALID_ARG if any element of the input array is not michael@0: * a valid tag. michael@0: */ michael@0: _convertInputMixedTagsArray: function TS__convertInputMixedTagsArray(aTags) michael@0: { michael@0: return aTags.map(function (val) michael@0: { michael@0: let tag = { _self: this }; michael@0: if (typeof(val) == "number" && this._tagFolders[val]) { michael@0: // This is a tag folder id. michael@0: tag.id = val; michael@0: // We can't know the name at this point, since a previous tag could michael@0: // want to change it. michael@0: tag.__defineGetter__("name", function () this._self._tagFolders[this.id]); michael@0: } michael@0: else if (typeof(val) == "string" && val.length > 0) { michael@0: // This is a tag name. michael@0: tag.name = val; michael@0: // We can't know the id at this point, since a previous tag could michael@0: // have created it. michael@0: tag.__defineGetter__("id", function () this._self._getItemIdForTag(this.name)); michael@0: } michael@0: else { michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: return tag; michael@0: }, this); michael@0: }, michael@0: michael@0: // nsITaggingService michael@0: tagURI: function TS_tagURI(aURI, aTags) michael@0: { michael@0: if (!aURI || !aTags || !Array.isArray(aTags)) { michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: michael@0: // This also does some input validation. michael@0: let tags = this._convertInputMixedTagsArray(aTags); michael@0: michael@0: let taggingService = this; michael@0: PlacesUtils.bookmarks.runInBatchMode({ michael@0: runBatched: function (aUserData) michael@0: { michael@0: tags.forEach(function (tag) michael@0: { michael@0: if (tag.id == -1) { michael@0: // Tag does not exist yet, create it. michael@0: this._createTag(tag.name); michael@0: } michael@0: michael@0: if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) { michael@0: // The provided URI is not yet tagged, add a tag for it. michael@0: // Note that bookmarks under tag containers must have null titles. michael@0: PlacesUtils.bookmarks.insertBookmark( michael@0: tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX, null michael@0: ); michael@0: } michael@0: michael@0: // Try to preserve user's tag name casing. michael@0: // Rename the tag container so the Places view matches the most-recent michael@0: // user-typed value. michael@0: if (PlacesUtils.bookmarks.getItemTitle(tag.id) != tag.name) { michael@0: // this._tagFolders is updated by the bookmarks observer. michael@0: PlacesUtils.bookmarks.setItemTitle(tag.id, tag.name); michael@0: } michael@0: }, taggingService); michael@0: } michael@0: }, null); michael@0: }, michael@0: michael@0: /** michael@0: * Removes the tag container from the tags root if the given tag is empty. michael@0: * michael@0: * @param aTagId michael@0: * the itemId of the tag element under the tags root michael@0: */ michael@0: _removeTagIfEmpty: function TS__removeTagIfEmpty(aTagId) { michael@0: let count = 0; michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: let stmt = db.createStatement( michael@0: "SELECT count(*) AS count FROM moz_bookmarks " michael@0: + "WHERE parent = :tag_id" michael@0: ); michael@0: stmt.params.tag_id = aTagId; michael@0: try { michael@0: if (stmt.executeStep()) { michael@0: count = stmt.row.count; michael@0: } michael@0: } michael@0: finally { michael@0: stmt.finalize(); michael@0: } michael@0: michael@0: if (count == 0) { michael@0: PlacesUtils.bookmarks.removeItem(aTagId); michael@0: } michael@0: }, michael@0: michael@0: // nsITaggingService michael@0: untagURI: function TS_untagURI(aURI, aTags) michael@0: { michael@0: if (!aURI || (aTags && !Array.isArray(aTags))) { michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: michael@0: if (!aTags) { michael@0: // Passing null should clear all tags for aURI, see the IDL. michael@0: // XXXmano: write a perf-sensitive version of this code path... michael@0: aTags = this.getTagsForURI(aURI); michael@0: } michael@0: michael@0: // This also does some input validation. michael@0: let tags = this._convertInputMixedTagsArray(aTags); michael@0: michael@0: let taggingService = this; michael@0: PlacesUtils.bookmarks.runInBatchMode({ michael@0: runBatched: function (aUserData) michael@0: { michael@0: tags.forEach(function (tag) michael@0: { michael@0: if (tag.id != -1) { michael@0: // A tag could exist. michael@0: let itemId = this._getItemIdForTaggedURI(aURI, tag.name); michael@0: if (itemId != -1) { michael@0: // There is a tagged item. michael@0: PlacesUtils.bookmarks.removeItem(itemId); michael@0: } michael@0: } michael@0: }, taggingService); michael@0: } michael@0: }, null); michael@0: }, michael@0: michael@0: // nsITaggingService michael@0: getURIsForTag: function TS_getURIsForTag(aTagName) { michael@0: if (!aTagName || aTagName.length == 0) michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: let uris = []; michael@0: let tagId = this._getItemIdForTag(aTagName); michael@0: if (tagId == -1) michael@0: return uris; michael@0: michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: let stmt = db.createStatement( michael@0: "SELECT h.url FROM moz_places h " michael@0: + "JOIN moz_bookmarks b ON b.fk = h.id " michael@0: + "WHERE b.parent = :tag_id " michael@0: ); michael@0: stmt.params.tag_id = tagId; michael@0: try { michael@0: while (stmt.executeStep()) { michael@0: try { michael@0: uris.push(Services.io.newURI(stmt.row.url, null, null)); michael@0: } catch (ex) {} michael@0: } michael@0: } michael@0: finally { michael@0: stmt.finalize(); michael@0: } michael@0: michael@0: return uris; michael@0: }, michael@0: michael@0: // nsITaggingService michael@0: getTagsForURI: function TS_getTagsForURI(aURI, aCount) { michael@0: if (!aURI) michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: var tags = []; michael@0: var bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(aURI); michael@0: for (var i=0; i < bookmarkIds.length; i++) { michael@0: var folderId = PlacesUtils.bookmarks.getFolderIdForItem(bookmarkIds[i]); michael@0: if (this._tagFolders[folderId]) michael@0: tags.push(this._tagFolders[folderId]); michael@0: } michael@0: michael@0: // sort the tag list michael@0: tags.sort(function(a, b) { michael@0: return a.toLowerCase().localeCompare(b.toLowerCase()); michael@0: }); michael@0: if (aCount) michael@0: aCount.value = tags.length; michael@0: return tags; michael@0: }, michael@0: michael@0: __tagFolders: null, michael@0: get _tagFolders() { michael@0: if (!this.__tagFolders) { michael@0: this.__tagFolders = []; michael@0: michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: let stmt = db.createStatement( michael@0: "SELECT id, title FROM moz_bookmarks WHERE parent = :tags_root " michael@0: ); michael@0: stmt.params.tags_root = PlacesUtils.tagsFolderId; michael@0: try { michael@0: while (stmt.executeStep()) { michael@0: this.__tagFolders[stmt.row.id] = stmt.row.title; michael@0: } michael@0: } michael@0: finally { michael@0: stmt.finalize(); michael@0: } michael@0: } michael@0: michael@0: return this.__tagFolders; michael@0: }, michael@0: michael@0: // nsITaggingService michael@0: get allTags() { michael@0: var allTags = []; michael@0: for (var i in this._tagFolders) michael@0: allTags.push(this._tagFolders[i]); michael@0: // sort the tag list michael@0: allTags.sort(function(a, b) { michael@0: return a.toLowerCase().localeCompare(b.toLowerCase()); michael@0: }); michael@0: return allTags; michael@0: }, michael@0: michael@0: // nsITaggingService michael@0: get hasTags() { michael@0: return this._tagFolders.length > 0; michael@0: }, michael@0: michael@0: // nsIObserver michael@0: observe: function TS_observe(aSubject, aTopic, aData) { michael@0: if (aTopic == TOPIC_SHUTDOWN) { michael@0: PlacesUtils.bookmarks.removeObserver(this); michael@0: Services.obs.removeObserver(this, TOPIC_SHUTDOWN); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * If the only bookmark items associated with aURI are contained in tag michael@0: * folders, returns the IDs of those items. This can be the case if michael@0: * the URI was bookmarked and tagged at some point, but the bookmark was michael@0: * removed, leaving only the bookmark items in tag folders. If the URI is michael@0: * either properly bookmarked or not tagged just returns and empty array. michael@0: * michael@0: * @param aURI michael@0: * A URI (string) that may or may not be bookmarked michael@0: * @returns an array of item ids michael@0: */ michael@0: _getTaggedItemIdsIfUnbookmarkedURI: michael@0: function TS__getTaggedItemIdsIfUnbookmarkedURI(aURI) { michael@0: var itemIds = []; michael@0: var isBookmarked = false; michael@0: michael@0: // Using bookmarks service API for this would be a pain. michael@0: // Until tags implementation becomes sane, go the query way. michael@0: let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: let stmt = db.createStatement( michael@0: "SELECT id, parent " michael@0: + "FROM moz_bookmarks " michael@0: + "WHERE fk = (SELECT id FROM moz_places WHERE url = :page_url)" michael@0: ); michael@0: stmt.params.page_url = aURI.spec; michael@0: try { michael@0: while (stmt.executeStep() && !isBookmarked) { michael@0: if (this._tagFolders[stmt.row.parent]) { michael@0: // This is a tag entry. michael@0: itemIds.push(stmt.row.id); michael@0: } michael@0: else { michael@0: // This is a real bookmark, so the bookmarked URI is not an orphan. michael@0: isBookmarked = true; michael@0: } michael@0: } michael@0: } michael@0: finally { michael@0: stmt.finalize(); michael@0: } michael@0: michael@0: return isBookmarked ? [] : itemIds; michael@0: }, michael@0: michael@0: // nsINavBookmarkObserver michael@0: onItemAdded: function TS_onItemAdded(aItemId, aFolderId, aIndex, aItemType, michael@0: aURI, aTitle) { michael@0: // Nothing to do if this is not a tag. michael@0: if (aFolderId != PlacesUtils.tagsFolderId || michael@0: aItemType != PlacesUtils.bookmarks.TYPE_FOLDER) michael@0: return; michael@0: michael@0: this._tagFolders[aItemId] = aTitle; michael@0: }, michael@0: michael@0: onItemRemoved: function TS_onItemRemoved(aItemId, aFolderId, aIndex, michael@0: aItemType, aURI) { michael@0: // Item is a tag folder. michael@0: if (aFolderId == PlacesUtils.tagsFolderId && this._tagFolders[aItemId]) { michael@0: delete this._tagFolders[aItemId]; michael@0: } michael@0: // Item is a bookmark that was removed from a non-tag folder. michael@0: else if (aURI && !this._tagFolders[aFolderId]) { michael@0: // If the only bookmark items now associated with the bookmark's URI are michael@0: // contained in tag folders, the URI is no longer properly bookmarked, so michael@0: // untag it. michael@0: let itemIds = this._getTaggedItemIdsIfUnbookmarkedURI(aURI); michael@0: for (let i = 0; i < itemIds.length; i++) { michael@0: try { michael@0: PlacesUtils.bookmarks.removeItem(itemIds[i]); michael@0: } catch (ex) {} michael@0: } michael@0: } michael@0: // Item is a tag entry. If this was the last entry for this tag, remove it. michael@0: else if (aURI && this._tagFolders[aFolderId]) { michael@0: this._removeTagIfEmpty(aFolderId); michael@0: } michael@0: }, michael@0: michael@0: onItemChanged: function TS_onItemChanged(aItemId, aProperty, michael@0: aIsAnnotationProperty, aNewValue, michael@0: aLastModified, aItemType) { michael@0: if (aProperty == "title" && this._tagFolders[aItemId]) michael@0: this._tagFolders[aItemId] = aNewValue; michael@0: }, michael@0: michael@0: onItemMoved: function TS_onItemMoved(aItemId, aOldParent, aOldIndex, michael@0: aNewParent, aNewIndex, aItemType) { michael@0: if (this._tagFolders[aItemId] && PlacesUtils.tagsFolderId == aOldParent && michael@0: PlacesUtils.tagsFolderId != aNewParent) michael@0: delete this._tagFolders[aItemId]; michael@0: }, michael@0: michael@0: onItemVisited: function () {}, michael@0: onBeginUpdateBatch: function () {}, michael@0: onEndUpdateBatch: function () {}, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: classID: Components.ID("{bbc23860-2553-479d-8b78-94d9038334f7}"), michael@0: michael@0: _xpcom_factory: XPCOMUtils.generateSingletonFactory(TaggingService), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsITaggingService michael@0: , Ci.nsINavBookmarkObserver michael@0: , Ci.nsIObserver michael@0: ]) michael@0: }; michael@0: michael@0: michael@0: function TagAutoCompleteResult(searchString, searchResult, michael@0: defaultIndex, errorDescription, michael@0: results, comments) { michael@0: this._searchString = searchString; michael@0: this._searchResult = searchResult; michael@0: this._defaultIndex = defaultIndex; michael@0: this._errorDescription = errorDescription; michael@0: this._results = results; michael@0: this._comments = comments; michael@0: } michael@0: michael@0: TagAutoCompleteResult.prototype = { michael@0: michael@0: /** michael@0: * The original search string michael@0: */ michael@0: get searchString() { michael@0: return this._searchString; michael@0: }, michael@0: michael@0: /** michael@0: * The result code of this result object, either: michael@0: * RESULT_IGNORED (invalid searchString) michael@0: * RESULT_FAILURE (failure) michael@0: * RESULT_NOMATCH (no matches found) michael@0: * RESULT_SUCCESS (matches found) michael@0: */ michael@0: get searchResult() { michael@0: return this._searchResult; michael@0: }, michael@0: michael@0: /** michael@0: * Index of the default item that should be entered if none is selected michael@0: */ michael@0: get defaultIndex() { michael@0: return this._defaultIndex; michael@0: }, michael@0: michael@0: /** michael@0: * A string describing the cause of a search failure michael@0: */ michael@0: get errorDescription() { michael@0: return this._errorDescription; michael@0: }, michael@0: michael@0: /** michael@0: * The number of matches michael@0: */ michael@0: get matchCount() { michael@0: return this._results.length; michael@0: }, michael@0: michael@0: get typeAheadResult() false, michael@0: michael@0: /** michael@0: * Get the value of the result at the given index michael@0: */ michael@0: getValueAt: function PTACR_getValueAt(index) { michael@0: return this._results[index]; michael@0: }, michael@0: michael@0: getLabelAt: function PTACR_getLabelAt(index) { michael@0: return this.getValueAt(index); michael@0: }, michael@0: michael@0: /** michael@0: * Get the comment of the result at the given index michael@0: */ michael@0: getCommentAt: function PTACR_getCommentAt(index) { michael@0: return this._comments[index]; michael@0: }, michael@0: michael@0: /** michael@0: * Get the style hint for the result at the given index michael@0: */ michael@0: getStyleAt: function PTACR_getStyleAt(index) { michael@0: if (!this._comments[index]) michael@0: return null; // not a category label, so no special styling michael@0: michael@0: if (index == 0) michael@0: return "suggestfirst"; // category label on first line of results michael@0: michael@0: return "suggesthint"; // category label on any other line of results michael@0: }, michael@0: michael@0: /** michael@0: * Get the image for the result at the given index michael@0: */ michael@0: getImageAt: function PTACR_getImageAt(index) { michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Get the image for the result at the given index michael@0: */ michael@0: getFinalCompleteValueAt: function PTACR_getFinalCompleteValueAt(index) { michael@0: return this.getValueAt(index); michael@0: }, michael@0: michael@0: /** michael@0: * Remove the value at the given index from the autocomplete results. michael@0: * If removeFromDb is set to true, the value should be removed from michael@0: * persistent storage as well. michael@0: */ michael@0: removeValueAt: function PTACR_removeValueAt(index, removeFromDb) { michael@0: this._results.splice(index, 1); michael@0: this._comments.splice(index, 1); michael@0: }, michael@0: michael@0: // nsISupports michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIAutoCompleteResult michael@0: ]) michael@0: }; michael@0: michael@0: // Implements nsIAutoCompleteSearch michael@0: function TagAutoCompleteSearch() { michael@0: XPCOMUtils.defineLazyServiceGetter(this, "tagging", michael@0: "@mozilla.org/browser/tagging-service;1", michael@0: "nsITaggingService"); michael@0: } michael@0: michael@0: TagAutoCompleteSearch.prototype = { michael@0: _stopped : false, michael@0: michael@0: /* michael@0: * Search for a given string and notify a listener (either synchronously michael@0: * or asynchronously) of the result michael@0: * michael@0: * @param searchString - The string to search for michael@0: * @param searchParam - An extra parameter michael@0: * @param previousResult - A previous result to use for faster searching michael@0: * @param listener - A listener to notify when the search is complete michael@0: */ michael@0: startSearch: function PTACS_startSearch(searchString, searchParam, result, listener) { michael@0: var searchResults = this.tagging.allTags; michael@0: var results = []; michael@0: var comments = []; michael@0: this._stopped = false; michael@0: michael@0: // only search on characters for the last tag michael@0: var index = Math.max(searchString.lastIndexOf(","), michael@0: searchString.lastIndexOf(";")); michael@0: var before = ''; michael@0: if (index != -1) { michael@0: before = searchString.slice(0, index+1); michael@0: searchString = searchString.slice(index+1); michael@0: // skip past whitespace michael@0: var m = searchString.match(/\s+/); michael@0: if (m) { michael@0: before += m[0]; michael@0: searchString = searchString.slice(m[0].length); michael@0: } michael@0: } michael@0: michael@0: if (!searchString.length) { michael@0: var newResult = new TagAutoCompleteResult(searchString, michael@0: Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 0, "", results, comments); michael@0: listener.onSearchResult(self, newResult); michael@0: return; michael@0: } michael@0: michael@0: var self = this; michael@0: // generator: if yields true, not done michael@0: function doSearch() { michael@0: var i = 0; michael@0: while (i < searchResults.length) { michael@0: if (self._stopped) michael@0: yield false; michael@0: // for each match, prepend what the user has typed so far michael@0: if (searchResults[i].toLowerCase() michael@0: .indexOf(searchString.toLowerCase()) == 0 && michael@0: comments.indexOf(searchResults[i]) == -1) { michael@0: results.push(before + searchResults[i]); michael@0: comments.push(searchResults[i]); michael@0: } michael@0: michael@0: ++i; michael@0: michael@0: /* TODO: bug 481451 michael@0: * For each yield we pass a new result to the autocomplete michael@0: * listener. The listener appends instead of replacing previous results, michael@0: * causing invalid matchCount values. michael@0: * michael@0: * As a workaround, all tags are searched through in a single batch, michael@0: * making this synchronous until the above issue is fixed. michael@0: */ michael@0: michael@0: /* michael@0: // 100 loops per yield michael@0: if ((i % 100) == 0) { michael@0: var newResult = new TagAutoCompleteResult(searchString, michael@0: Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING, 0, "", results, comments); michael@0: listener.onSearchResult(self, newResult); michael@0: yield true; michael@0: } michael@0: */ michael@0: } michael@0: michael@0: let searchResult = results.length > 0 ? michael@0: Ci.nsIAutoCompleteResult.RESULT_SUCCESS : michael@0: Ci.nsIAutoCompleteResult.RESULT_NOMATCH; michael@0: var newResult = new TagAutoCompleteResult(searchString, searchResult, 0, michael@0: "", results, comments); michael@0: listener.onSearchResult(self, newResult); michael@0: yield false; michael@0: } michael@0: michael@0: // chunk the search results via the generator michael@0: var gen = doSearch(); michael@0: while (gen.next()); michael@0: gen.close(); michael@0: }, michael@0: michael@0: /** michael@0: * Stop an asynchronous search that is in progress michael@0: */ michael@0: stopSearch: function PTACS_stopSearch() { michael@0: this._stopped = true; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: classID: Components.ID("{1dcc23b0-d4cb-11dc-9ad6-479d56d89593}"), michael@0: michael@0: _xpcom_factory: XPCOMUtils.generateSingletonFactory(TagAutoCompleteSearch), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIAutoCompleteSearch michael@0: ]) michael@0: }; michael@0: michael@0: let component = [TaggingService, TagAutoCompleteSearch]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);