diff options
| author | Niklas Baumstark | 2012-01-25 22:03:26 +0100 | 
|---|---|---|
| committer | Niklas Baumstark | 2012-04-10 23:58:08 +0200 | 
| commit | 392232f5756f095dca0329dfd7ce3ae0752245ec (patch) | |
| tree | 6260270ee192b38c659da201cfa6e6e99576318c /lib | |
| parent | e8b402595e5e7ebeb5abc6e1d058692370369cad (diff) | |
| download | vimium-392232f5756f095dca0329dfd7ce3ae0752245ec.tar.bz2 | |
add lazy evaluation at several places
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/completion.js | 256 | 
1 files changed, 136 insertions, 120 deletions
diff --git a/lib/completion.js b/lib/completion.js index 68f8d4bf..1915a83a 100644 --- a/lib/completion.js +++ b/lib/completion.js @@ -195,62 +195,54 @@ var completion = (function() {        return [ open(false), open(true, true), open(true, false) ];    } -  /** Creates a completion that renders by marking fuzzy-matched parts. */ -  function createHighlightingCompletion(query, str, action, relevancy) { -    return { -      action: action, -      relevancy: relevancy, - -      render: function() { -        // we want to match the content in HTML tags, but not the HTML tags themselves, so we remove the -        // tags and reinsert them after the matching process -        var htmlTags = {}; -        str = stripHtmlTags(str, htmlTags); -        var groups = fuzzyMatcher.getMatchGroups(query, str); -        var html = ''; -        var htmlOffset = 0; - -        // this helper function adds the HTML generated _for one single character_ to the HTML output -        // and reinserts HTML tags stripped before, if they were at this position -        function addToHtml(str) { -          if (htmlOffset in htmlTags) -            html += htmlTags[htmlOffset]; -          html += str; -          ++htmlOffset; -        } - -        function addCharsWithDecoration(str, before, after) { -          before = before || ''; -          after = after || ''; -          for (var i = 0; i < str.length; ++i) -            addToHtml(before + str[i] + after); -        } - -        // iterate over the match groups. They are non-matched and matched string parts, in alternating order -        for (var i = 0; i < groups.length; ++i) { -          if (i % 2 == 0) -            // we have a non-matched part, it could have several characters. We need to insert them character -            // by character, so that addToHtml can keep track of the position in the original string -            addCharsWithDecoration(groups[i]); -          else -            // we have a matched part. In addition to the characters themselves, we add some decorating HTML. -            addCharsWithDecoration(groups[i], '<span class="fuzzyMatch">', '</span>'); -        }; - -        // call it another time so that a tag at the very last position is reinserted -        addToHtml(''); - -        return html; -      }, -    } -  } -    /** Creates an file-internal representation of a URL match with the given paramters */ -  function createCompletionHtml(type, url, title) { +  function createCompletionHtml(type, str, title) {      title = title || '';      // sanitize input, it could come from a malicious web site      title = title.length > 0 ? ' <span class="title">' + utils.escapeHtml(title) + '</span>' : ''; -    return '<em>' + type + '</em> ' + utils.escapeHtml(url) + title; +    return '<em>' + type + '</em> ' + utils.escapeHtml(str) + title; +  } + +  /** Renders a completion by marking fuzzy-matched parts. */ +  function renderFuzzy(query, html) { +    // we want to match the content in HTML tags, but not the HTML tags themselves, so we remove the +    // tags and reinsert them after the matching process +    var htmlTags = {}; +    var groups = fuzzyMatcher.getMatchGroups(query, stripHtmlTags(html, htmlTags)); + +    html = []; +    var htmlOffset = 0; + +    // this helper function adds the HTML generated _for one single character_ to the HTML output +    // and reinserts HTML tags stripped before, if they were at this position +    function addToHtml(str) { +      if (htmlOffset in htmlTags) +        html.push(htmlTags[htmlOffset]); +      html.push(str); +      ++htmlOffset; +    } + +    function addCharsWithDecoration(str, before, after) { +      before = before || ''; +      after = after || ''; +      for (var i = 0; i < str.length; ++i) +        addToHtml(before + str[i] + after); +    } + +    // iterate over the match groups. They are non-matched and matched string parts, in alternating order +    for (var i = 0; i < groups.length; ++i) { +      if (i % 2 == 0) +        // we have a non-matched part, it could have several characters. We need to insert them character +        // by character, so that addToHtml can keep track of the position in the original string +        addCharsWithDecoration(groups[i]); +      else +        // we have a matched part. In addition to the characters themselves, we add some decorating HTML. +        addCharsWithDecoration(groups[i], '<span class="fuzzyMatch">', '</span>'); +    }; + +    // call it another time so that a tag at the very last position is reinserted +    addToHtml(''); +    return html.join('');    }    /** Creates a function that returns a constant value */ @@ -258,27 +250,24 @@ var completion = (function() {      return function() { return x; }    } +  /** A completion class that only holds a relevancy value and a function to get HTML and action +   * properties */ +  var LazyCompletion = function(relevancy, builder) { +    this.relevancy = relevancy; +    this.build = builder; +  } +    /** Helper class to construct fuzzy completers for asynchronous data sources like history or bookmark     * matchers. */ -  var AsyncFuzzyUrlCompleter = function() { +  var AsyncCompleter = function() {      this.completions = null;      this.id = utils.createUniqueId();      this.readyCallback = this.fallbackReadyCallback = function(results) {        this.completions = results;      } -    this.extractStringFromMatch = function(match) { return stripHtmlTags(match.str); }    } -  AsyncFuzzyUrlCompleter.prototype = { -    calculateRelevancy: function(query, match) { -      return match.url.length / -        (fuzzyMatcher.calculateRelevancy(query, this.extractStringFromMatch(match)) + 1); -    }, - -    createAction: function(match) { -      return createActionOpenUrl(match.url); -    }, - -    /** Convenience function to remove shared code in the completers. Clear the completion cache, sends +  AsyncCompleter.prototype = { +    /** Convenience function to remove shared code in the completers. Clears the completion cache, sends       * a message to an extension port and pipes the returned message through a callback before storing it into       * the instance's completion cache.       */ @@ -298,17 +287,44 @@ var completion = (function() {        fuzzyMatcher.invalidateFilterCache(this.id);      }, +    /** Convenience function to remove shared code in the completers. Creates an internal representation of +     * a fuzzy completion item that is still independent of the query. The bind function will be called with +     * the actual query as an argument later. */ +    createInternalMatch: function(type, item, action) { +      var url = item.url; +      var parts = [type, url, item.title]; +      var str = parts.join(' '); +      action = action || createActionOpenUrl(url); + +      function createLazyCompletion(query) { +        return new LazyCompletion(url.length / fuzzyMatcher.calculateRelevancy(query, str), function() { +          return { +            html:   renderFuzzy(query, createCompletionHtml.apply(null, parts)), +            action: action, +          }}); +      } + +      // add one more layer of indirection: For filtering, we only need the string to match. +      // Only after we reduced the number of possible results, we call :bind on them to get +      // an actual completion object +      return { +        str: parts.join(' '), +        bind: createLazyCompletion, +      } +    }, + +    // Default to handle results using fuzzy matching. This can be overridden by subclasses. +    processResults: function(query, results) { +      results = fuzzyMatcher.filter(query, results, function(match) { return match.str }, this.id); +      // bind the query-agnostic, lazy results to a query +      return results.map(function(result) { return result.bind(query); }); +    }, +      filter: function(query, callback) {        var self = this;        var handler = function(results) { -        var filtered = fuzzyMatcher.filter(query, results, self.extractStringFromMatch, self.id); -        callback(filtered.map(function(match) { -          return createHighlightingCompletion( -                query, match.str, -                self.createAction(match), -                self.calculateRelevancy(query, match)); -        })); +        callback(self.processResults(query, results));        }        // are the results ready? @@ -319,8 +335,8 @@ var completion = (function() {          // no: register the handler as a callback          this.readyCallback = function(results) {            handler(results); -          this.readyCallback = this.fallbackReadyCallback; -          this.readyCallback(results); +          self.readyCallback = self.fallbackReadyCallback; +          self.readyCallback(results);          }        }      }, @@ -344,85 +360,86 @@ var completion = (function() {          var pattern = commands[cmd][1];          var url = typeof pattern == 'function' ? pattern(term) : pattern.replace(/%s/g, term); -        return { -          render: function() { return createCompletionHtml(desc, term) }, -          action: createActionOpenUrl(utils.createFullUrl(url)), -          relevancy: -2 // this will appear even before the URL/search suggestion -        }; +        // this will appear even before the URL/search suggestion +        return new LazyCompletion(-2, function() { +          return { +            html:   createCompletionHtml(desc, term), +            action: createActionOpenUrl(utils.createFullUrl(url)), +          }})        });      }      /** Checks if the input is a URL. If yes, returns a suggestion to visit it. If no, returns a suggestion       * to start a web search. */ -    this.getUrlOrSearchSuggestions = function(query, suggestions) { +    this.getUrlOrSearchSuggestion = function(query, suggestions) {        // trim query        query = query.replace(/^\s+|\s+$/g, '');        var isUrl = utils.isUrl(query); -      return [{ -        render: function() { return createCompletionHtml(isUrl ? 'goto' : 'search', query); }, -        action: createActionOpenUrl(isUrl ? utils.createFullUrl(query) -                                          : utils.createSearchUrl(query)), -        relevancy: -1, // low relevancy so this should appear at the top -      }]; +      return new LazyCompletion(-1, function() { +        return { +          html: createCompletionHtml(isUrl ? 'goto' : 'search', query), +          action: createActionOpenUrl(isUrl ? utils.createFullUrl(query) +                                            : utils.createSearchUrl(query)), +        }});      }      this.filter = function(query, callback) { -      callback(this.getCommandSuggestions(query).concat( -               this.getUrlOrSearchSuggestions(query))); +      suggestions = this.getCommandSuggestions(query); +      suggestions.push(this.getUrlOrSearchSuggestion(query)); +      callback(suggestions);      }    }    /** A fuzzy history completer */    var FuzzyHistoryCompleter = function(maxResults) { -    AsyncFuzzyUrlCompleter.call(this); +    AsyncCompleter.call(this);      this.maxResults = maxResults || 1000;    } -  utils.extend(AsyncFuzzyUrlCompleter, FuzzyHistoryCompleter); +  utils.extend(AsyncCompleter, FuzzyHistoryCompleter); +    FuzzyHistoryCompleter.prototype.refresh = function() { -    this.resetCache(); -    this.fetchFromPort('getHistory', { maxResults: this.maxResults }, function(msg) { -      return msg.history.map(function(historyItem) { -        return { str: createCompletionHtml('history', historyItem.url, historyItem.title), -                 url: historyItem.url }; +    var self = this; +    self.resetCache(); +    self.fetchFromPort('getHistory', { maxResults: self.maxResults }, function(msg) { +      return msg.history.map(function(item) { +        return self.createInternalMatch('history', item);        });      });    }    /** A fuzzy bookmark completer */    var FuzzyBookmarkCompleter = function() { -    AsyncFuzzyUrlCompleter.call(this); +    AsyncCompleter.call(this);    } -  utils.extend(AsyncFuzzyUrlCompleter, FuzzyBookmarkCompleter); +  utils.extend(AsyncCompleter, FuzzyBookmarkCompleter); +    FuzzyBookmarkCompleter.prototype.refresh = function() { -    this.resetCache(); -    this.fetchFromPort('getAllBookmarks', {}, function(msg) { +    var self = this; +    self.resetCache(); +    self.fetchFromPort('getAllBookmarks', {}, function(msg) {        return msg.bookmarks.filter(function(bookmark) { return bookmark.url !== undefined })                            .map(function(bookmark) { -        return { str: createCompletionHtml('bookmark', bookmark.url, bookmark.title), -                 url: bookmark.url }; -      }) +        return self.createInternalMatch('bookmark', bookmark); +      });      });    }    /** A fuzzy tab completer */    var FuzzyTabCompleter = function() { -    AsyncFuzzyUrlCompleter.call(this); -  } -  utils.extend(AsyncFuzzyUrlCompleter, FuzzyTabCompleter); -  FuzzyTabCompleter.prototype.createAction = function(match) { -    var open = function() { -      chrome.extension.sendRequest({ handler: 'selectSpecificTab', id: match.tab.id }); -    } -    return [ open, open ]; +    AsyncCompleter.call(this);    } +  utils.extend(AsyncCompleter, FuzzyTabCompleter); +    FuzzyTabCompleter.prototype.refresh = function() { -    this.resetCache(); -    this.fetchFromPort('getTabsInCurrentWindow', {}, function(msg) { +    var self = this; +    self.resetCache(); +    self.fetchFromPort('getTabsInCurrentWindow', {}, function(msg) {        return msg.tabs.map(function(tab) { -        return { str: createCompletionHtml('tab', tab.url, tab.title), -                 url: tab.url, -                 tab: tab }; +        var open = function() { +          chrome.extension.sendRequest({ handler: 'selectSpecificTab', id: tab.id }); +        } +        return self.createInternalMatch('tab', tab, [open, open, open]);        });      });    } @@ -441,7 +458,7 @@ var completion = (function() {        this.sources.forEach(function(x) { x.refresh(); });      }, -    filter: function(query, callback) { +    filter: function(query, maxResults, callback) {        if (query.length < this.queryThreshold) {          callback([]);          return; @@ -457,10 +474,9 @@ var completion = (function() {              return;            // all sources have provided results by now, so we can sort and return -          all.sort(function(a,b) { -            return a.relevancy - b.relevancy; -          }); -          callback(all); +          all.sort(function(a,b) { return a.relevancy - b.relevancy; }); +          // evalulate lazy completions for the top n results +          callback(all.slice(0, maxResults).map(function(result) { return result.build(); }));          });        });      }  | 
