|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 const Cc = Components.classes; |
|
7 const Ci = Components.interfaces; |
|
8 const Cr = Components.results; |
|
9 |
|
10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
11 Components.utils.import("resource://gre/modules/Services.jsm"); |
|
12 Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); |
|
13 |
|
14 const TOPIC_SHUTDOWN = "places-shutdown"; |
|
15 |
|
16 /** |
|
17 * The Places Tagging Service |
|
18 */ |
|
19 function TaggingService() { |
|
20 // Observe bookmarks changes. |
|
21 PlacesUtils.bookmarks.addObserver(this, false); |
|
22 |
|
23 // Cleanup on shutdown. |
|
24 Services.obs.addObserver(this, TOPIC_SHUTDOWN, false); |
|
25 } |
|
26 |
|
27 TaggingService.prototype = { |
|
28 /** |
|
29 * Creates a tag container under the tags-root with the given name. |
|
30 * |
|
31 * @param aTagName |
|
32 * the name for the new tag. |
|
33 * @returns the id of the new tag container. |
|
34 */ |
|
35 _createTag: function TS__createTag(aTagName) { |
|
36 var newFolderId = PlacesUtils.bookmarks.createFolder( |
|
37 PlacesUtils.tagsFolderId, aTagName, PlacesUtils.bookmarks.DEFAULT_INDEX |
|
38 ); |
|
39 // Add the folder to our local cache, so we can avoid doing this in the |
|
40 // observer that would have to check itemType. |
|
41 this._tagFolders[newFolderId] = aTagName; |
|
42 |
|
43 return newFolderId; |
|
44 }, |
|
45 |
|
46 /** |
|
47 * Checks whether the given uri is tagged with the given tag. |
|
48 * |
|
49 * @param [in] aURI |
|
50 * url to check for |
|
51 * @param [in] aTagName |
|
52 * the tag to check for |
|
53 * @returns the item id if the URI is tagged with the given tag, -1 |
|
54 * otherwise. |
|
55 */ |
|
56 _getItemIdForTaggedURI: function TS__getItemIdForTaggedURI(aURI, aTagName) { |
|
57 var tagId = this._getItemIdForTag(aTagName); |
|
58 if (tagId == -1) |
|
59 return -1; |
|
60 // Using bookmarks service API for this would be a pain. |
|
61 // Until tags implementation becomes sane, go the query way. |
|
62 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) |
|
63 .DBConnection; |
|
64 let stmt = db.createStatement( |
|
65 "SELECT id FROM moz_bookmarks " |
|
66 + "WHERE parent = :tag_id " |
|
67 + "AND fk = (SELECT id FROM moz_places WHERE url = :page_url)" |
|
68 ); |
|
69 stmt.params.tag_id = tagId; |
|
70 stmt.params.page_url = aURI.spec; |
|
71 try { |
|
72 if (stmt.executeStep()) { |
|
73 return stmt.row.id; |
|
74 } |
|
75 } |
|
76 finally { |
|
77 stmt.finalize(); |
|
78 } |
|
79 return -1; |
|
80 }, |
|
81 |
|
82 /** |
|
83 * Returns the folder id for a tag, or -1 if not found. |
|
84 * @param [in] aTag |
|
85 * string tag to search for |
|
86 * @returns integer id for the bookmark folder for the tag |
|
87 */ |
|
88 _getItemIdForTag: function TS_getItemIdForTag(aTagName) { |
|
89 for (var i in this._tagFolders) { |
|
90 if (aTagName.toLowerCase() == this._tagFolders[i].toLowerCase()) |
|
91 return parseInt(i); |
|
92 } |
|
93 return -1; |
|
94 }, |
|
95 |
|
96 /** |
|
97 * Makes a proper array of tag objects like { id: number, name: string }. |
|
98 * |
|
99 * @param aTags |
|
100 * Array of tags. Entries can be tag names or concrete item id. |
|
101 * @return Array of tag objects like { id: number, name: string }. |
|
102 * |
|
103 * @throws Cr.NS_ERROR_INVALID_ARG if any element of the input array is not |
|
104 * a valid tag. |
|
105 */ |
|
106 _convertInputMixedTagsArray: function TS__convertInputMixedTagsArray(aTags) |
|
107 { |
|
108 return aTags.map(function (val) |
|
109 { |
|
110 let tag = { _self: this }; |
|
111 if (typeof(val) == "number" && this._tagFolders[val]) { |
|
112 // This is a tag folder id. |
|
113 tag.id = val; |
|
114 // We can't know the name at this point, since a previous tag could |
|
115 // want to change it. |
|
116 tag.__defineGetter__("name", function () this._self._tagFolders[this.id]); |
|
117 } |
|
118 else if (typeof(val) == "string" && val.length > 0) { |
|
119 // This is a tag name. |
|
120 tag.name = val; |
|
121 // We can't know the id at this point, since a previous tag could |
|
122 // have created it. |
|
123 tag.__defineGetter__("id", function () this._self._getItemIdForTag(this.name)); |
|
124 } |
|
125 else { |
|
126 throw Cr.NS_ERROR_INVALID_ARG; |
|
127 } |
|
128 return tag; |
|
129 }, this); |
|
130 }, |
|
131 |
|
132 // nsITaggingService |
|
133 tagURI: function TS_tagURI(aURI, aTags) |
|
134 { |
|
135 if (!aURI || !aTags || !Array.isArray(aTags)) { |
|
136 throw Cr.NS_ERROR_INVALID_ARG; |
|
137 } |
|
138 |
|
139 // This also does some input validation. |
|
140 let tags = this._convertInputMixedTagsArray(aTags); |
|
141 |
|
142 let taggingService = this; |
|
143 PlacesUtils.bookmarks.runInBatchMode({ |
|
144 runBatched: function (aUserData) |
|
145 { |
|
146 tags.forEach(function (tag) |
|
147 { |
|
148 if (tag.id == -1) { |
|
149 // Tag does not exist yet, create it. |
|
150 this._createTag(tag.name); |
|
151 } |
|
152 |
|
153 if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) { |
|
154 // The provided URI is not yet tagged, add a tag for it. |
|
155 // Note that bookmarks under tag containers must have null titles. |
|
156 PlacesUtils.bookmarks.insertBookmark( |
|
157 tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX, null |
|
158 ); |
|
159 } |
|
160 |
|
161 // Try to preserve user's tag name casing. |
|
162 // Rename the tag container so the Places view matches the most-recent |
|
163 // user-typed value. |
|
164 if (PlacesUtils.bookmarks.getItemTitle(tag.id) != tag.name) { |
|
165 // this._tagFolders is updated by the bookmarks observer. |
|
166 PlacesUtils.bookmarks.setItemTitle(tag.id, tag.name); |
|
167 } |
|
168 }, taggingService); |
|
169 } |
|
170 }, null); |
|
171 }, |
|
172 |
|
173 /** |
|
174 * Removes the tag container from the tags root if the given tag is empty. |
|
175 * |
|
176 * @param aTagId |
|
177 * the itemId of the tag element under the tags root |
|
178 */ |
|
179 _removeTagIfEmpty: function TS__removeTagIfEmpty(aTagId) { |
|
180 let count = 0; |
|
181 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) |
|
182 .DBConnection; |
|
183 let stmt = db.createStatement( |
|
184 "SELECT count(*) AS count FROM moz_bookmarks " |
|
185 + "WHERE parent = :tag_id" |
|
186 ); |
|
187 stmt.params.tag_id = aTagId; |
|
188 try { |
|
189 if (stmt.executeStep()) { |
|
190 count = stmt.row.count; |
|
191 } |
|
192 } |
|
193 finally { |
|
194 stmt.finalize(); |
|
195 } |
|
196 |
|
197 if (count == 0) { |
|
198 PlacesUtils.bookmarks.removeItem(aTagId); |
|
199 } |
|
200 }, |
|
201 |
|
202 // nsITaggingService |
|
203 untagURI: function TS_untagURI(aURI, aTags) |
|
204 { |
|
205 if (!aURI || (aTags && !Array.isArray(aTags))) { |
|
206 throw Cr.NS_ERROR_INVALID_ARG; |
|
207 } |
|
208 |
|
209 if (!aTags) { |
|
210 // Passing null should clear all tags for aURI, see the IDL. |
|
211 // XXXmano: write a perf-sensitive version of this code path... |
|
212 aTags = this.getTagsForURI(aURI); |
|
213 } |
|
214 |
|
215 // This also does some input validation. |
|
216 let tags = this._convertInputMixedTagsArray(aTags); |
|
217 |
|
218 let taggingService = this; |
|
219 PlacesUtils.bookmarks.runInBatchMode({ |
|
220 runBatched: function (aUserData) |
|
221 { |
|
222 tags.forEach(function (tag) |
|
223 { |
|
224 if (tag.id != -1) { |
|
225 // A tag could exist. |
|
226 let itemId = this._getItemIdForTaggedURI(aURI, tag.name); |
|
227 if (itemId != -1) { |
|
228 // There is a tagged item. |
|
229 PlacesUtils.bookmarks.removeItem(itemId); |
|
230 } |
|
231 } |
|
232 }, taggingService); |
|
233 } |
|
234 }, null); |
|
235 }, |
|
236 |
|
237 // nsITaggingService |
|
238 getURIsForTag: function TS_getURIsForTag(aTagName) { |
|
239 if (!aTagName || aTagName.length == 0) |
|
240 throw Cr.NS_ERROR_INVALID_ARG; |
|
241 |
|
242 let uris = []; |
|
243 let tagId = this._getItemIdForTag(aTagName); |
|
244 if (tagId == -1) |
|
245 return uris; |
|
246 |
|
247 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) |
|
248 .DBConnection; |
|
249 let stmt = db.createStatement( |
|
250 "SELECT h.url FROM moz_places h " |
|
251 + "JOIN moz_bookmarks b ON b.fk = h.id " |
|
252 + "WHERE b.parent = :tag_id " |
|
253 ); |
|
254 stmt.params.tag_id = tagId; |
|
255 try { |
|
256 while (stmt.executeStep()) { |
|
257 try { |
|
258 uris.push(Services.io.newURI(stmt.row.url, null, null)); |
|
259 } catch (ex) {} |
|
260 } |
|
261 } |
|
262 finally { |
|
263 stmt.finalize(); |
|
264 } |
|
265 |
|
266 return uris; |
|
267 }, |
|
268 |
|
269 // nsITaggingService |
|
270 getTagsForURI: function TS_getTagsForURI(aURI, aCount) { |
|
271 if (!aURI) |
|
272 throw Cr.NS_ERROR_INVALID_ARG; |
|
273 |
|
274 var tags = []; |
|
275 var bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(aURI); |
|
276 for (var i=0; i < bookmarkIds.length; i++) { |
|
277 var folderId = PlacesUtils.bookmarks.getFolderIdForItem(bookmarkIds[i]); |
|
278 if (this._tagFolders[folderId]) |
|
279 tags.push(this._tagFolders[folderId]); |
|
280 } |
|
281 |
|
282 // sort the tag list |
|
283 tags.sort(function(a, b) { |
|
284 return a.toLowerCase().localeCompare(b.toLowerCase()); |
|
285 }); |
|
286 if (aCount) |
|
287 aCount.value = tags.length; |
|
288 return tags; |
|
289 }, |
|
290 |
|
291 __tagFolders: null, |
|
292 get _tagFolders() { |
|
293 if (!this.__tagFolders) { |
|
294 this.__tagFolders = []; |
|
295 |
|
296 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) |
|
297 .DBConnection; |
|
298 let stmt = db.createStatement( |
|
299 "SELECT id, title FROM moz_bookmarks WHERE parent = :tags_root " |
|
300 ); |
|
301 stmt.params.tags_root = PlacesUtils.tagsFolderId; |
|
302 try { |
|
303 while (stmt.executeStep()) { |
|
304 this.__tagFolders[stmt.row.id] = stmt.row.title; |
|
305 } |
|
306 } |
|
307 finally { |
|
308 stmt.finalize(); |
|
309 } |
|
310 } |
|
311 |
|
312 return this.__tagFolders; |
|
313 }, |
|
314 |
|
315 // nsITaggingService |
|
316 get allTags() { |
|
317 var allTags = []; |
|
318 for (var i in this._tagFolders) |
|
319 allTags.push(this._tagFolders[i]); |
|
320 // sort the tag list |
|
321 allTags.sort(function(a, b) { |
|
322 return a.toLowerCase().localeCompare(b.toLowerCase()); |
|
323 }); |
|
324 return allTags; |
|
325 }, |
|
326 |
|
327 // nsITaggingService |
|
328 get hasTags() { |
|
329 return this._tagFolders.length > 0; |
|
330 }, |
|
331 |
|
332 // nsIObserver |
|
333 observe: function TS_observe(aSubject, aTopic, aData) { |
|
334 if (aTopic == TOPIC_SHUTDOWN) { |
|
335 PlacesUtils.bookmarks.removeObserver(this); |
|
336 Services.obs.removeObserver(this, TOPIC_SHUTDOWN); |
|
337 } |
|
338 }, |
|
339 |
|
340 /** |
|
341 * If the only bookmark items associated with aURI are contained in tag |
|
342 * folders, returns the IDs of those items. This can be the case if |
|
343 * the URI was bookmarked and tagged at some point, but the bookmark was |
|
344 * removed, leaving only the bookmark items in tag folders. If the URI is |
|
345 * either properly bookmarked or not tagged just returns and empty array. |
|
346 * |
|
347 * @param aURI |
|
348 * A URI (string) that may or may not be bookmarked |
|
349 * @returns an array of item ids |
|
350 */ |
|
351 _getTaggedItemIdsIfUnbookmarkedURI: |
|
352 function TS__getTaggedItemIdsIfUnbookmarkedURI(aURI) { |
|
353 var itemIds = []; |
|
354 var isBookmarked = false; |
|
355 |
|
356 // Using bookmarks service API for this would be a pain. |
|
357 // Until tags implementation becomes sane, go the query way. |
|
358 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) |
|
359 .DBConnection; |
|
360 let stmt = db.createStatement( |
|
361 "SELECT id, parent " |
|
362 + "FROM moz_bookmarks " |
|
363 + "WHERE fk = (SELECT id FROM moz_places WHERE url = :page_url)" |
|
364 ); |
|
365 stmt.params.page_url = aURI.spec; |
|
366 try { |
|
367 while (stmt.executeStep() && !isBookmarked) { |
|
368 if (this._tagFolders[stmt.row.parent]) { |
|
369 // This is a tag entry. |
|
370 itemIds.push(stmt.row.id); |
|
371 } |
|
372 else { |
|
373 // This is a real bookmark, so the bookmarked URI is not an orphan. |
|
374 isBookmarked = true; |
|
375 } |
|
376 } |
|
377 } |
|
378 finally { |
|
379 stmt.finalize(); |
|
380 } |
|
381 |
|
382 return isBookmarked ? [] : itemIds; |
|
383 }, |
|
384 |
|
385 // nsINavBookmarkObserver |
|
386 onItemAdded: function TS_onItemAdded(aItemId, aFolderId, aIndex, aItemType, |
|
387 aURI, aTitle) { |
|
388 // Nothing to do if this is not a tag. |
|
389 if (aFolderId != PlacesUtils.tagsFolderId || |
|
390 aItemType != PlacesUtils.bookmarks.TYPE_FOLDER) |
|
391 return; |
|
392 |
|
393 this._tagFolders[aItemId] = aTitle; |
|
394 }, |
|
395 |
|
396 onItemRemoved: function TS_onItemRemoved(aItemId, aFolderId, aIndex, |
|
397 aItemType, aURI) { |
|
398 // Item is a tag folder. |
|
399 if (aFolderId == PlacesUtils.tagsFolderId && this._tagFolders[aItemId]) { |
|
400 delete this._tagFolders[aItemId]; |
|
401 } |
|
402 // Item is a bookmark that was removed from a non-tag folder. |
|
403 else if (aURI && !this._tagFolders[aFolderId]) { |
|
404 // If the only bookmark items now associated with the bookmark's URI are |
|
405 // contained in tag folders, the URI is no longer properly bookmarked, so |
|
406 // untag it. |
|
407 let itemIds = this._getTaggedItemIdsIfUnbookmarkedURI(aURI); |
|
408 for (let i = 0; i < itemIds.length; i++) { |
|
409 try { |
|
410 PlacesUtils.bookmarks.removeItem(itemIds[i]); |
|
411 } catch (ex) {} |
|
412 } |
|
413 } |
|
414 // Item is a tag entry. If this was the last entry for this tag, remove it. |
|
415 else if (aURI && this._tagFolders[aFolderId]) { |
|
416 this._removeTagIfEmpty(aFolderId); |
|
417 } |
|
418 }, |
|
419 |
|
420 onItemChanged: function TS_onItemChanged(aItemId, aProperty, |
|
421 aIsAnnotationProperty, aNewValue, |
|
422 aLastModified, aItemType) { |
|
423 if (aProperty == "title" && this._tagFolders[aItemId]) |
|
424 this._tagFolders[aItemId] = aNewValue; |
|
425 }, |
|
426 |
|
427 onItemMoved: function TS_onItemMoved(aItemId, aOldParent, aOldIndex, |
|
428 aNewParent, aNewIndex, aItemType) { |
|
429 if (this._tagFolders[aItemId] && PlacesUtils.tagsFolderId == aOldParent && |
|
430 PlacesUtils.tagsFolderId != aNewParent) |
|
431 delete this._tagFolders[aItemId]; |
|
432 }, |
|
433 |
|
434 onItemVisited: function () {}, |
|
435 onBeginUpdateBatch: function () {}, |
|
436 onEndUpdateBatch: function () {}, |
|
437 |
|
438 ////////////////////////////////////////////////////////////////////////////// |
|
439 //// nsISupports |
|
440 |
|
441 classID: Components.ID("{bbc23860-2553-479d-8b78-94d9038334f7}"), |
|
442 |
|
443 _xpcom_factory: XPCOMUtils.generateSingletonFactory(TaggingService), |
|
444 |
|
445 QueryInterface: XPCOMUtils.generateQI([ |
|
446 Ci.nsITaggingService |
|
447 , Ci.nsINavBookmarkObserver |
|
448 , Ci.nsIObserver |
|
449 ]) |
|
450 }; |
|
451 |
|
452 |
|
453 function TagAutoCompleteResult(searchString, searchResult, |
|
454 defaultIndex, errorDescription, |
|
455 results, comments) { |
|
456 this._searchString = searchString; |
|
457 this._searchResult = searchResult; |
|
458 this._defaultIndex = defaultIndex; |
|
459 this._errorDescription = errorDescription; |
|
460 this._results = results; |
|
461 this._comments = comments; |
|
462 } |
|
463 |
|
464 TagAutoCompleteResult.prototype = { |
|
465 |
|
466 /** |
|
467 * The original search string |
|
468 */ |
|
469 get searchString() { |
|
470 return this._searchString; |
|
471 }, |
|
472 |
|
473 /** |
|
474 * The result code of this result object, either: |
|
475 * RESULT_IGNORED (invalid searchString) |
|
476 * RESULT_FAILURE (failure) |
|
477 * RESULT_NOMATCH (no matches found) |
|
478 * RESULT_SUCCESS (matches found) |
|
479 */ |
|
480 get searchResult() { |
|
481 return this._searchResult; |
|
482 }, |
|
483 |
|
484 /** |
|
485 * Index of the default item that should be entered if none is selected |
|
486 */ |
|
487 get defaultIndex() { |
|
488 return this._defaultIndex; |
|
489 }, |
|
490 |
|
491 /** |
|
492 * A string describing the cause of a search failure |
|
493 */ |
|
494 get errorDescription() { |
|
495 return this._errorDescription; |
|
496 }, |
|
497 |
|
498 /** |
|
499 * The number of matches |
|
500 */ |
|
501 get matchCount() { |
|
502 return this._results.length; |
|
503 }, |
|
504 |
|
505 get typeAheadResult() false, |
|
506 |
|
507 /** |
|
508 * Get the value of the result at the given index |
|
509 */ |
|
510 getValueAt: function PTACR_getValueAt(index) { |
|
511 return this._results[index]; |
|
512 }, |
|
513 |
|
514 getLabelAt: function PTACR_getLabelAt(index) { |
|
515 return this.getValueAt(index); |
|
516 }, |
|
517 |
|
518 /** |
|
519 * Get the comment of the result at the given index |
|
520 */ |
|
521 getCommentAt: function PTACR_getCommentAt(index) { |
|
522 return this._comments[index]; |
|
523 }, |
|
524 |
|
525 /** |
|
526 * Get the style hint for the result at the given index |
|
527 */ |
|
528 getStyleAt: function PTACR_getStyleAt(index) { |
|
529 if (!this._comments[index]) |
|
530 return null; // not a category label, so no special styling |
|
531 |
|
532 if (index == 0) |
|
533 return "suggestfirst"; // category label on first line of results |
|
534 |
|
535 return "suggesthint"; // category label on any other line of results |
|
536 }, |
|
537 |
|
538 /** |
|
539 * Get the image for the result at the given index |
|
540 */ |
|
541 getImageAt: function PTACR_getImageAt(index) { |
|
542 return null; |
|
543 }, |
|
544 |
|
545 /** |
|
546 * Get the image for the result at the given index |
|
547 */ |
|
548 getFinalCompleteValueAt: function PTACR_getFinalCompleteValueAt(index) { |
|
549 return this.getValueAt(index); |
|
550 }, |
|
551 |
|
552 /** |
|
553 * Remove the value at the given index from the autocomplete results. |
|
554 * If removeFromDb is set to true, the value should be removed from |
|
555 * persistent storage as well. |
|
556 */ |
|
557 removeValueAt: function PTACR_removeValueAt(index, removeFromDb) { |
|
558 this._results.splice(index, 1); |
|
559 this._comments.splice(index, 1); |
|
560 }, |
|
561 |
|
562 // nsISupports |
|
563 QueryInterface: XPCOMUtils.generateQI([ |
|
564 Ci.nsIAutoCompleteResult |
|
565 ]) |
|
566 }; |
|
567 |
|
568 // Implements nsIAutoCompleteSearch |
|
569 function TagAutoCompleteSearch() { |
|
570 XPCOMUtils.defineLazyServiceGetter(this, "tagging", |
|
571 "@mozilla.org/browser/tagging-service;1", |
|
572 "nsITaggingService"); |
|
573 } |
|
574 |
|
575 TagAutoCompleteSearch.prototype = { |
|
576 _stopped : false, |
|
577 |
|
578 /* |
|
579 * Search for a given string and notify a listener (either synchronously |
|
580 * or asynchronously) of the result |
|
581 * |
|
582 * @param searchString - The string to search for |
|
583 * @param searchParam - An extra parameter |
|
584 * @param previousResult - A previous result to use for faster searching |
|
585 * @param listener - A listener to notify when the search is complete |
|
586 */ |
|
587 startSearch: function PTACS_startSearch(searchString, searchParam, result, listener) { |
|
588 var searchResults = this.tagging.allTags; |
|
589 var results = []; |
|
590 var comments = []; |
|
591 this._stopped = false; |
|
592 |
|
593 // only search on characters for the last tag |
|
594 var index = Math.max(searchString.lastIndexOf(","), |
|
595 searchString.lastIndexOf(";")); |
|
596 var before = ''; |
|
597 if (index != -1) { |
|
598 before = searchString.slice(0, index+1); |
|
599 searchString = searchString.slice(index+1); |
|
600 // skip past whitespace |
|
601 var m = searchString.match(/\s+/); |
|
602 if (m) { |
|
603 before += m[0]; |
|
604 searchString = searchString.slice(m[0].length); |
|
605 } |
|
606 } |
|
607 |
|
608 if (!searchString.length) { |
|
609 var newResult = new TagAutoCompleteResult(searchString, |
|
610 Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 0, "", results, comments); |
|
611 listener.onSearchResult(self, newResult); |
|
612 return; |
|
613 } |
|
614 |
|
615 var self = this; |
|
616 // generator: if yields true, not done |
|
617 function doSearch() { |
|
618 var i = 0; |
|
619 while (i < searchResults.length) { |
|
620 if (self._stopped) |
|
621 yield false; |
|
622 // for each match, prepend what the user has typed so far |
|
623 if (searchResults[i].toLowerCase() |
|
624 .indexOf(searchString.toLowerCase()) == 0 && |
|
625 comments.indexOf(searchResults[i]) == -1) { |
|
626 results.push(before + searchResults[i]); |
|
627 comments.push(searchResults[i]); |
|
628 } |
|
629 |
|
630 ++i; |
|
631 |
|
632 /* TODO: bug 481451 |
|
633 * For each yield we pass a new result to the autocomplete |
|
634 * listener. The listener appends instead of replacing previous results, |
|
635 * causing invalid matchCount values. |
|
636 * |
|
637 * As a workaround, all tags are searched through in a single batch, |
|
638 * making this synchronous until the above issue is fixed. |
|
639 */ |
|
640 |
|
641 /* |
|
642 // 100 loops per yield |
|
643 if ((i % 100) == 0) { |
|
644 var newResult = new TagAutoCompleteResult(searchString, |
|
645 Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING, 0, "", results, comments); |
|
646 listener.onSearchResult(self, newResult); |
|
647 yield true; |
|
648 } |
|
649 */ |
|
650 } |
|
651 |
|
652 let searchResult = results.length > 0 ? |
|
653 Ci.nsIAutoCompleteResult.RESULT_SUCCESS : |
|
654 Ci.nsIAutoCompleteResult.RESULT_NOMATCH; |
|
655 var newResult = new TagAutoCompleteResult(searchString, searchResult, 0, |
|
656 "", results, comments); |
|
657 listener.onSearchResult(self, newResult); |
|
658 yield false; |
|
659 } |
|
660 |
|
661 // chunk the search results via the generator |
|
662 var gen = doSearch(); |
|
663 while (gen.next()); |
|
664 gen.close(); |
|
665 }, |
|
666 |
|
667 /** |
|
668 * Stop an asynchronous search that is in progress |
|
669 */ |
|
670 stopSearch: function PTACS_stopSearch() { |
|
671 this._stopped = true; |
|
672 }, |
|
673 |
|
674 ////////////////////////////////////////////////////////////////////////////// |
|
675 //// nsISupports |
|
676 |
|
677 classID: Components.ID("{1dcc23b0-d4cb-11dc-9ad6-479d56d89593}"), |
|
678 |
|
679 _xpcom_factory: XPCOMUtils.generateSingletonFactory(TagAutoCompleteSearch), |
|
680 |
|
681 QueryInterface: XPCOMUtils.generateQI([ |
|
682 Ci.nsIAutoCompleteSearch |
|
683 ]) |
|
684 }; |
|
685 |
|
686 let component = [TaggingService, TagAutoCompleteSearch]; |
|
687 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); |