home *** CD-ROM | disk | FTP | other *** search
/ PC Advisor 2010 April / PCA177.iso / ESSENTIALS / Firefox Setup.exe / nonlocalized / components / nsTaggingService.js < prev    next >
Encoding:
Text File  |  2009-07-15  |  20.6 KB  |  664 lines

  1. /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  2.  * ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License
  6.  * Version 1.1 (the "License"); you may not use this file except in
  7.  * compliance with the License. You may obtain a copy of the License
  8.  * at http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS"
  11.  * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
  12.  * the License for the specific language governing rights and
  13.  * limitations under the License.
  14.  *
  15.  * The Original Code is the Places Tagging Service.
  16.  *
  17.  * The Initial Developer of the Original Code is
  18.  * Mozilla Corporation.
  19.  * Portions created by the Initial Developer are Copyright (C) 2007
  20.  * the Initial Developer. All Rights Reserved.
  21.  *
  22.  * Contributor(s):
  23.  *   Asaf Romano <mano@mozilla.com> (Original Author)
  24.  *   Dietrich Ayala <dietrich@mozilla.com>
  25.  *   Marco Bonardo <mak77@bonardo.net>
  26.  *   Drew Willcoxon <adw@mozilla.com>
  27.  *
  28.  * Alternatively, the contents of this file may be used under the terms of
  29.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  30.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  31.  * in which case the provisions of the GPL or the LGPL are applicable instead
  32.  * of those above. If you wish to allow use of your version of this file only
  33.  * under the terms of either the GPL or the LGPL, and not to allow others to
  34.  * use your version of this file under the terms of the MPL, indicate your
  35.  * decision by deleting the provisions above and replace them with the notice
  36.  * and other provisions required by the GPL or the LGPL. If you do not delete
  37.  * the provisions above, a recipient may use your version of this file under
  38.  * the terms of any one of the MPL, the GPL or the LGPL.
  39.  *
  40.  * ***** END LICENSE BLOCK ***** */
  41.  
  42. const Cc = Components.classes;
  43. const Ci = Components.interfaces;
  44. const Cr = Components.results;
  45. const Cu = Components.utils;
  46.  
  47. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  48.  
  49. const NH_CONTRACTID = "@mozilla.org/browser/nav-history-service;1";
  50. const BMS_CONTRACTID = "@mozilla.org/browser/nav-bookmarks-service;1";
  51. const IO_CONTRACTID = "@mozilla.org/network/io-service;1";
  52. const ANNO_CONTRACTID = "@mozilla.org/browser/annotation-service;1";
  53. const FAV_CONTRACTID = "@mozilla.org/browser/favicon-service;1";
  54. const OBSS_CONTRACTID = "@mozilla.org/observer-service;1";
  55.  
  56. var gIoService = Cc[IO_CONTRACTID].getService(Ci.nsIIOService);
  57.  
  58. /**
  59.  * The Places Tagging Service
  60.  */
  61. function TaggingService() {
  62.   this._bms = Cc[BMS_CONTRACTID].getService(Ci.nsINavBookmarksService);
  63.   this._bms.addObserver(this, false);
  64.  
  65.   this._obss = Cc[OBSS_CONTRACTID].getService(Ci.nsIObserverService);
  66.   this._obss.addObserver(this, "xpcom-shutdown", false);
  67. }
  68.  
  69. TaggingService.prototype = {
  70.   get _history() {
  71.     if (!this.__history)
  72.       this.__history = Cc[NH_CONTRACTID].getService(Ci.nsINavHistoryService);
  73.     return this.__history;
  74.   },
  75.  
  76.   get _annos() {
  77.     if (!this.__annos)
  78.       this.__annos =  Cc[ANNO_CONTRACTID].getService(Ci.nsIAnnotationService);
  79.     return this.__annos;
  80.   },
  81.  
  82.   // Feed XPCOMUtils
  83.   classDescription: "Places Tagging Service",
  84.   contractID: "@mozilla.org/browser/tagging-service;1",
  85.   classID: Components.ID("{bbc23860-2553-479d-8b78-94d9038334f7}"),
  86.  
  87.   // nsISupports
  88.   QueryInterface: XPCOMUtils.generateQI([Ci.nsITaggingService,
  89.                                          Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS,
  90.                                          Ci.nsINavBookmarkObserver,
  91.                                          Ci.nsIObserver]),
  92.  
  93.   /**
  94.    * If there's no tag with the given name or id, null is returned;
  95.    */
  96.   _getTagResult: function TS__getTagResult(aTagNameOrId) {
  97.     if (!aTagNameOrId)
  98.       throw Cr.NS_ERROR_INVALID_ARG;
  99.  
  100.     var tagId = null;
  101.     if (typeof(aTagNameOrId) == "string")
  102.       tagId = this._getItemIdForTag(aTagNameOrId);
  103.     else
  104.       tagId = aTagNameOrId;
  105.  
  106.     if (tagId == -1)
  107.       return null;
  108.  
  109.     var options = this._history.getNewQueryOptions();
  110.     var query = this._history.getNewQuery();
  111.     query.setFolders([tagId], 1);
  112.     var result = this._history.executeQuery(query, options);
  113.     return result;
  114.   },
  115.  
  116.   /**
  117.    * Creates a tag container under the tags-root with the given name.
  118.    *
  119.    * @param aName
  120.    *        the name for the new container.
  121.    * @returns the id of the new container.
  122.    */
  123.   _createTag: function TS__createTag(aName) {
  124.     return this._bms.createFolder(this._bms.tagsFolder, aName,
  125.                                   this._bms.DEFAULT_INDEX);
  126.   },
  127.  
  128.   /**
  129.    * Checks whether the given uri is tagged with the given tag.
  130.    *
  131.    * @param [in] aURI
  132.    *        url to check for
  133.    * @param [in] aTagName
  134.    *        the tag to check for
  135.    * @returns the item id if the URI is tagged with the given tag, -1
  136.    *          otherwise.
  137.    */
  138.   _getItemIdForTaggedURI: function TS__getItemIdForTaggedURI(aURI, aTagName) {
  139.     var tagId = this._getItemIdForTag(aTagName);
  140.     if (tagId == -1)
  141.       return -1;
  142.     var bookmarkIds = this._bms.getBookmarkIdsForURI(aURI, {});
  143.     for (var i=0; i < bookmarkIds.length; i++) {
  144.       var parent = this._bms.getFolderIdForItem(bookmarkIds[i]);
  145.       if (parent == tagId)
  146.         return bookmarkIds[i];
  147.     }
  148.     return -1;
  149.   },
  150.  
  151.   /**
  152.    * Returns the folder id for a tag, or -1 if not found.
  153.    * @param [in] aTag
  154.    *        string tag to search for
  155.    * @returns integer id for the bookmark folder for the tag
  156.    */
  157.   _getItemIdForTag: function TS_getItemIdForTag(aTagName) {
  158.     for (var i in this._tagFolders) {
  159.       if (aTagName.toLowerCase() == this._tagFolders[i].toLowerCase())
  160.         return parseInt(i);
  161.     }
  162.     return -1;
  163.   },
  164.  
  165.   // nsITaggingService
  166.   tagURI: function TS_tagURI(aURI, aTags) {
  167.     if (!aURI || !aTags)
  168.       throw Cr.NS_ERROR_INVALID_ARG;
  169.  
  170.     this._bms.runInBatchMode({
  171.       _self: this,
  172.       runBatched: function(aUserData) {
  173.         for (var i = 0; i < aTags.length; i++) {
  174.           var tag = aTags[i];
  175.           var tagId = null;
  176.           if (typeof(tag) == "number") {
  177.             // is it a tag folder id?
  178.             if (this._self._tagFolders[tag]) {
  179.               tagId = tag;
  180.               tag = this._self._tagFolders[tagId];
  181.             }
  182.             else
  183.               throw Cr.NS_ERROR_INVALID_ARG;
  184.           }
  185.           else {
  186.             tagId = this._self._getItemIdForTag(tag);
  187.             if (tagId == -1)
  188.               tagId = this._self._createTag(tag);
  189.           }
  190.  
  191.           var itemId = this._self._getItemIdForTaggedURI(aURI, tag);
  192.           if (itemId == -1)
  193.             this._self._bms.insertBookmark(tagId, aURI,
  194.                                            this._self._bms.DEFAULT_INDEX, null);
  195.  
  196.           // Rename the tag container so the Places view would match the
  197.           // most-recent user-typed values.
  198.           var currentTagTitle = this._self._bms.getItemTitle(tagId);
  199.           if (currentTagTitle != tag) {
  200.             this._self._bms.setItemTitle(tagId, tag);
  201.             this._self._tagFolders[tagId] = tag;
  202.           }
  203.         }
  204.       }
  205.     }, null);
  206.   },
  207.  
  208.   /**
  209.    * Removes the tag container from the tags-root if the given tag is empty.
  210.    *
  211.    * @param aTagId
  212.    *        the item-id of the tag element under the tags root
  213.    */
  214.   _removeTagIfEmpty: function TS__removeTagIfEmpty(aTagId) {
  215.     var result = this._getTagResult(aTagId);
  216.     if (!result)
  217.       return;
  218.     var node = result.root;
  219.     node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
  220.     node.containerOpen = true;
  221.     var cc = node.childCount;
  222.     node.containerOpen = false;
  223.     if (cc == 0)
  224.       this._bms.removeFolder(node.itemId);
  225.   },
  226.  
  227.   // nsITaggingService
  228.   untagURI: function TS_untagURI(aURI, aTags) {
  229.     if (!aURI)
  230.       throw Cr.NS_ERROR_INVALID_ARG;
  231.  
  232.     if (!aTags) {
  233.       // see IDL.
  234.       // XXXmano: write a perf-sensitive version of this code path...
  235.       aTags = this.getTagsForURI(aURI, { });
  236.     }
  237.  
  238.     this._bms.runInBatchMode({
  239.       _self: this,
  240.       runBatched: function(aUserData) {
  241.         for (var i = 0; i < aTags.length; i++) {
  242.           var tag = aTags[i];
  243.           var tagId = null;
  244.           if (typeof(tag) == "number") {
  245.             // is it a tag folder id?
  246.             if (this._self._tagFolders[tag]) {
  247.               tagId = tag;
  248.               tag = this._self._tagFolders[tagId];
  249.             }
  250.             else
  251.               throw Cr.NS_ERROR_INVALID_ARG;
  252.           }
  253.           else
  254.             tagId = this._self._getItemIdForTag(tag);
  255.  
  256.           if (tagId != -1) {
  257.             var itemId = this._self._getItemIdForTaggedURI(aURI, tag);
  258.             if (itemId != -1) {
  259.               this._self._bms.removeItem(itemId);
  260.               this._self._removeTagIfEmpty(tagId);
  261.             }
  262.           }
  263.         }
  264.       }
  265.     }, null);
  266.   },
  267.  
  268.   // nsITaggingService
  269.   getURIsForTag: function TS_getURIsForTag(aTag) {
  270.     if (!aTag || aTag.length == 0)
  271.       throw Cr.NS_ERROR_INVALID_ARG;
  272.  
  273.     var uris = [];
  274.     var tagResult = this._getTagResult(aTag);
  275.     if (tagResult) {
  276.       var tagNode = tagResult.root;
  277.       tagNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
  278.       tagNode.containerOpen = true;
  279.       var cc = tagNode.childCount;
  280.       for (var i = 0; i < cc; i++) {
  281.         try {
  282.           uris.push(gIoService.newURI(tagNode.getChild(i).uri, null, null));
  283.         } catch (ex) {
  284.           // This is an invalid node, tags should only contain valid uri nodes.
  285.           // continue to next node.
  286.         }
  287.       }
  288.       tagNode.containerOpen = false;
  289.     }
  290.     return uris;
  291.   },
  292.  
  293.   // nsITaggingService
  294.   getTagsForURI: function TS_getTagsForURI(aURI, aCount) {
  295.     if (!aURI)
  296.       throw Cr.NS_ERROR_INVALID_ARG;
  297.  
  298.     var tags = [];
  299.     var bookmarkIds = this._bms.getBookmarkIdsForURI(aURI, {});
  300.     for (var i=0; i < bookmarkIds.length; i++) {
  301.       var folderId = this._bms.getFolderIdForItem(bookmarkIds[i]);
  302.       if (this._tagFolders[folderId])
  303.         tags.push(this._tagFolders[folderId]);
  304.     }
  305.  
  306.     // sort the tag list
  307.     tags.sort(function(a, b) {
  308.         return a.toLowerCase().localeCompare(b.toLowerCase());
  309.       });
  310.     aCount.value = tags.length;
  311.     return tags;
  312.   },
  313.  
  314.   __tagFolders: null, 
  315.   get _tagFolders() {
  316.     if (!this.__tagFolders) {
  317.       this.__tagFolders = [];
  318.       var options = this._history.getNewQueryOptions();
  319.       var query = this._history.getNewQuery();
  320.       query.setFolders([this._bms.tagsFolder], 1);
  321.       var tagsResult = this._history.executeQuery(query, options);
  322.       var root = tagsResult.root;
  323.       root.containerOpen = true;
  324.       var cc = root.childCount;
  325.       for (var i=0; i < cc; i++) {
  326.         var child = root.getChild(i);
  327.         this.__tagFolders[child.itemId] = child.title;
  328.       }
  329.       root.containerOpen = false;
  330.     }
  331.  
  332.     return this.__tagFolders;
  333.   },
  334.  
  335.   // nsITaggingService
  336.   get allTags() {
  337.     var allTags = [];
  338.     for (var i in this._tagFolders)
  339.       allTags.push(this._tagFolders[i]);
  340.     // sort the tag list
  341.     allTags.sort(function(a, b) {
  342.         return a.toLowerCase().localeCompare(b.toLowerCase());
  343.       });
  344.     return allTags;
  345.   },
  346.  
  347.   // nsIObserver
  348.   observe: function TS_observe(aSubject, aTopic, aData) {
  349.     if (aTopic == "xpcom-shutdown") {
  350.       this._bms.removeObserver(this);
  351.       this._obss.removeObserver(this, "xpcom-shutdown");
  352.     }
  353.   },
  354.  
  355.   /**
  356.    * If the only bookmark items associated with aURI are contained in tag
  357.    * folders, returns the IDs of those tag folders.  This can be the case if
  358.    * the URI was bookmarked and tagged at some point, but the bookmark was
  359.    * removed, leaving only the bookmark items in tag folders.  Returns null
  360.    * if the URI is either properly bookmarked or not tagged.
  361.    *
  362.    * @param   aURI
  363.    *          A URI (string) that may or may not be bookmarked
  364.    * @returns null or an array of tag IDs
  365.    */
  366.   _getTagsIfUnbookmarkedURI: function TS__getTagsIfUnbookmarkedURI(aURI) {
  367.     var tagIds = [];
  368.     var isBookmarked = false;
  369.     var itemIds = this._bms.getBookmarkIdsForURI(aURI, {});
  370.  
  371.     for (let i = 0; !isBookmarked && i < itemIds.length; i++) {
  372.       var parentId = this._bms.getFolderIdForItem(itemIds[i]);
  373.       if (this._tagFolders[parentId])
  374.         tagIds.push(parentId);
  375.       else
  376.         isBookmarked = true;
  377.     }
  378.  
  379.     return !isBookmarked && tagIds.length > 0 ? tagIds : null;
  380.   },
  381.  
  382.   // boolean to indicate if we're in a batch
  383.   _inBatch: false,
  384.  
  385.   // maps the IDs of bookmarks in the process of being removed to their URIs
  386.   _itemsInRemoval: {},
  387.  
  388.   // nsINavBookmarkObserver
  389.   onBeginUpdateBatch: function() {
  390.     this._inBatch = true;
  391.   },
  392.   onEndUpdateBatch: function() {
  393.     this._inBatch = false;
  394.   },
  395.   onItemAdded: function(aItemId, aFolderId, aIndex) {
  396.     if (aFolderId == this._bms.tagsFolder &&
  397.         this._bms.getItemType(aItemId) == this._bms.TYPE_FOLDER)
  398.       this._tagFolders[aItemId] = this._bms.getItemTitle(aItemId);
  399.   },
  400.   onBeforeItemRemoved: function(aItemId) {
  401.     // Remember the bookmark's URI, because it will be gone by the time
  402.     // onItemRemoved() is called.  getBookmarkURI() will throw if the item is
  403.     // not a bookmark, which is fine.
  404.     try {
  405.       this._itemsInRemoval[aItemId] = this._bms.getBookmarkURI(aItemId);
  406.     }
  407.     catch (e) {}
  408.   },
  409.   onItemRemoved: function(aItemId, aFolderId, aIndex) {
  410.     var itemURI = this._itemsInRemoval[aItemId];
  411.     delete this._itemsInRemoval[aItemId];
  412.  
  413.     // Item is a tag folder.
  414.     if (aFolderId == this._bms.tagsFolder && this._tagFolders[aItemId])
  415.       delete this._tagFolders[aItemId];
  416.  
  417.     // Item is a bookmark that was removed from a non-tag folder.
  418.     else if (itemURI && !this._tagFolders[aFolderId]) {
  419.  
  420.       // If the only bookmark items now associated with the bookmark's URI are
  421.       // contained in tag folders, the URI is no longer properly bookmarked, so
  422.       // untag it.
  423.       var tagIds = this._getTagsIfUnbookmarkedURI(itemURI);
  424.       if (tagIds)
  425.         this.untagURI(itemURI, tagIds);
  426.     }
  427.   },
  428.   onItemChanged: function(aItemId, aProperty, aIsAnnotationProperty, aValue) {
  429.     if (this._tagFolders[aItemId])
  430.       this._tagFolders[aItemId] = this._bms.getItemTitle(aItemId);
  431.   },
  432.   onItemVisited: function(aItemId, aVisitID, time) {},
  433.   onItemMoved: function(aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex) {
  434.     if (this._tagFolders[aItemId] && this._bms.tagFolder == aOldParent &&
  435.         this._bms.tagFolder != aNewParent)
  436.       delete this._tagFolders[aItemId];
  437.   }
  438. };
  439.  
  440. // Implements nsIAutoCompleteResult
  441. function TagAutoCompleteResult(searchString, searchResult,
  442.                                defaultIndex, errorDescription,
  443.                                results, comments) {
  444.   this._searchString = searchString;
  445.   this._searchResult = searchResult;
  446.   this._defaultIndex = defaultIndex;
  447.   this._errorDescription = errorDescription;
  448.   this._results = results;
  449.   this._comments = comments;
  450. }
  451.  
  452. TagAutoCompleteResult.prototype = {
  453.   
  454.   /**
  455.    * The original search string
  456.    */
  457.   get searchString() {
  458.     return this._searchString;
  459.   },
  460.  
  461.   /**
  462.    * The result code of this result object, either:
  463.    *         RESULT_IGNORED   (invalid searchString)
  464.    *         RESULT_FAILURE   (failure)
  465.    *         RESULT_NOMATCH   (no matches found)
  466.    *         RESULT_SUCCESS   (matches found)
  467.    */
  468.   get searchResult() {
  469.     return this._searchResult;
  470.   },
  471.  
  472.   /**
  473.    * Index of the default item that should be entered if none is selected
  474.    */
  475.   get defaultIndex() {
  476.     return this._defaultIndex;
  477.   },
  478.  
  479.   /**
  480.    * A string describing the cause of a search failure
  481.    */
  482.   get errorDescription() {
  483.     return this._errorDescription;
  484.   },
  485.  
  486.   /**
  487.    * The number of matches
  488.    */
  489.   get matchCount() {
  490.     return this._results.length;
  491.   },
  492.  
  493.   /**
  494.    * Get the value of the result at the given index
  495.    */
  496.   getValueAt: function PTACR_getValueAt(index) {
  497.     return this._results[index];
  498.   },
  499.  
  500.   /**
  501.    * Get the comment of the result at the given index
  502.    */
  503.   getCommentAt: function PTACR_getCommentAt(index) {
  504.     return this._comments[index];
  505.   },
  506.  
  507.   /**
  508.    * Get the style hint for the result at the given index
  509.    */
  510.   getStyleAt: function PTACR_getStyleAt(index) {
  511.     if (!this._comments[index])
  512.       return null;  // not a category label, so no special styling
  513.  
  514.     if (index == 0)
  515.       return "suggestfirst";  // category label on first line of results
  516.  
  517.     return "suggesthint";   // category label on any other line of results
  518.   },
  519.  
  520.   /**
  521.    * Get the image for the result at the given index
  522.    */
  523.   getImageAt: function PTACR_getImageAt(index) {
  524.     return null;
  525.   },
  526.  
  527.   /**
  528.    * Remove the value at the given index from the autocomplete results.
  529.    * If removeFromDb is set to true, the value should be removed from
  530.    * persistent storage as well.
  531.    */
  532.   removeValueAt: function PTACR_removeValueAt(index, removeFromDb) {
  533.     this._results.splice(index, 1);
  534.     this._comments.splice(index, 1);
  535.   },
  536.  
  537.   QueryInterface: function(aIID) {
  538.     if (!aIID.equals(Ci.nsIAutoCompleteResult) && !aIID.equals(Ci.nsISupports))
  539.         throw Components.results.NS_ERROR_NO_INTERFACE;
  540.     return this;
  541.   }
  542. };
  543.  
  544. // Implements nsIAutoCompleteSearch
  545. function TagAutoCompleteSearch() {
  546. }
  547.  
  548. TagAutoCompleteSearch.prototype = {
  549.   _stopped : false, 
  550.  
  551.   get tagging() {
  552.     let svc = Cc["@mozilla.org/browser/tagging-service;1"].
  553.               getService(Ci.nsITaggingService);
  554.     this.__defineGetter__("tagging", function() svc);
  555.     return this.tagging;
  556.   },
  557.  
  558.   /*
  559.    * Search for a given string and notify a listener (either synchronously
  560.    * or asynchronously) of the result
  561.    *
  562.    * @param searchString - The string to search for
  563.    * @param searchParam - An extra parameter
  564.    * @param previousResult - A previous result to use for faster searching
  565.    * @param listener - A listener to notify when the search is complete
  566.    */
  567.   startSearch: function PTACS_startSearch(searchString, searchParam, result, listener) {
  568.     var searchResults = this.tagging.allTags;
  569.     var results = [];
  570.     var comments = [];
  571.     this._stopped = false;
  572.  
  573.     // only search on characters for the last tag
  574.     var index = Math.max(searchString.lastIndexOf(","), 
  575.       searchString.lastIndexOf(";"));
  576.     var before = ''; 
  577.     if (index != -1) {  
  578.       before = searchString.slice(0, index+1);
  579.       searchString = searchString.slice(index+1);
  580.       // skip past whitespace
  581.       var m = searchString.match(/\s+/);
  582.       if (m) {
  583.          before += m[0];
  584.          searchString = searchString.slice(m[0].length);
  585.       }
  586.     }
  587.  
  588.     if (!searchString.length) {
  589.       var newResult = new TagAutoCompleteResult(searchString,
  590.         Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 0, "", results, comments);
  591.       listener.onSearchResult(self, newResult);
  592.       return;
  593.     }
  594.     
  595.     var self = this;
  596.     // generator: if yields true, not done
  597.     function doSearch() {
  598.       var i = 0;
  599.       while (i < searchResults.length) {
  600.         if (self._stopped)
  601.           yield false;
  602.         // for each match, prepend what the user has typed so far
  603.         if (searchResults[i].toLowerCase()
  604.                             .indexOf(searchString.toLowerCase()) == 0 &&
  605.             comments.indexOf(searchResults[i]) == -1) {
  606.           results.push(before + searchResults[i]);
  607.           comments.push(searchResults[i]);
  608.         }
  609.     
  610.         ++i;
  611.  
  612.         /* TODO: bug 481451
  613.          * For each yield we pass a new result to the autocomplete
  614.          * listener. The listener appends instead of replacing previous results,
  615.          * causing invalid matchCount values.
  616.          *
  617.          * As a workaround, all tags are searched through in a single batch,
  618.          * making this synchronous until the above issue is fixed.
  619.          */
  620.  
  621.         /*
  622.         // 100 loops per yield
  623.         if ((i % 100) == 0) {
  624.           var newResult = new TagAutoCompleteResult(searchString,
  625.             Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING, 0, "", results, comments);
  626.           listener.onSearchResult(self, newResult);
  627.           yield true;
  628.         }
  629.         */
  630.       }
  631.  
  632.       var newResult = new TagAutoCompleteResult(searchString,
  633.         Ci.nsIAutoCompleteResult.RESULT_SUCCESS, 0, "", results, comments);
  634.       listener.onSearchResult(self, newResult);
  635.       yield false;
  636.     }
  637.     
  638.     // chunk the search results via the generator
  639.     var gen = doSearch();
  640.     while (gen.next());
  641.     gen.close();
  642.   },
  643.  
  644.   /**
  645.    * Stop an asynchronous search that is in progress
  646.    */
  647.   stopSearch: function PTACS_stopSearch() {
  648.     this._stopped = true;
  649.   },
  650.  
  651.   // nsISupports
  652.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteSearch,
  653.                                          Ci.nsITimerCallback]), 
  654.  
  655.   classDescription: "Places Tag AutoComplete",
  656.   contractID: "@mozilla.org/autocomplete/search;1?name=places-tag-autocomplete",
  657.   classID: Components.ID("{1dcc23b0-d4cb-11dc-9ad6-479d56d89593}")
  658. };
  659.  
  660. var component = [TaggingService, TagAutoCompleteSearch];
  661. function NSGetModule(compMgr, fileSpec) {
  662.   return XPCOMUtils.generateModule(component);
  663. }
  664.