Repository: qutebrowser/qutebrowser

Issue:
Completion: highlight only the matched terms
1. start `qutebrowser --temp-basedir`
2. `:open https://github.com/The-Compiler/qutebrowser/`
3. `:open git vim` (not followed by Enter)
4. then only the terms "Git" and "vim" in the title should be highlighted
   
   (currently everything between the terms is also highlighted: "GitHub - The-Compiler/qutebrowser: A keyboard-driven, vim")



Discussion:
The problem with that is that it's confusing with overlapping terms since the highlight doesn't match the actual matching - see https://github.com/The-Compiler/qutebrowser/pull/1195#discussion_r48901865

(edit: That link doesn't do the right thing here - it's the first outdated/hidden comment thread there)

(at least it unhides the comments...)

So I take it that with proper detection of overlaps it would be preferred this way? Sounds like a challenge for me... :wink:

Yes, though I'm also worried about the performance impact when we start with more complicated logic (the matching is run over thousands of items with the first few letters typed).

Well, it's not about matching itself, only highlighting the results. It should be fine to run it just over the items that are actually going to be displayed...

Oh, right! I need more coffee! :wink:

As far as I understand, the highlighting already happens only for the to-be-displayed items, which is probably why there was no performance difference in https://github.com/The-Compiler/qutebrowser/pull/1195#issuecomment-169145881.

And since overlapping matches are already handled by [re.sub](https://docs.python.org/3/library/re.html#re.sub), I don't see how you managed to get the highlighting described in https://github.com/The-Compiler/qutebrowser/pull/1195#discussion_r48901865 (your link from above). With this change on top of current master:

``` diff
--- a/qutebrowser/completion/completiondelegate.py
+++ b/qutebrowser/completion/completiondelegate.py
@@ -200,7 +200,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
             columns_to_filter = index.model().srcmodel.columns_to_filter
             if index.column() in columns_to_filter and pattern:
                 repl = r'<span class="highlight">\g<0></span>'
-                text = re.sub(re.escape(pattern).replace(r'\ ', r'.*'),
+                text = re.sub(re.escape(pattern).replace(r'\ ', r'|'),
                               repl, self._opt.text, flags=re.IGNORECASE)
                 self._doc.setHtml(text)
             else:
```

and the same URL (`http://qutebrowser.org:8010/waterfall?reload=5`) and pattern (`qute t`) I get this highlighting: h**tt**p://**qute**browser.org:8010/wa**t**erfall?reload=5
I think that is much better than http://**qutebrowser.org:8010/wat**erfall?reload=5

If you want to get fancy, you could build to pattern to highlight with groups (like `pattern = '(' + re.escape(pattern).replace(r'\ ', r').*(') + ')'`) and then manually iterate over the groups and insert the HTML tags only where you really want them (i.e. the matched words), like

``` python
match = re.search(pattern, text)
result = text
for index in range(match.lastindex, 0, -1):
    start, end = match.span(index)
    result = result[:start] + "<b>" + result[start:end] + "</b>" + result[end:]
```

This should produce a highlight like "bar hi **foo** baz **bar** bye" if you've searched for "foo bar" (that is, the first occurence of "bar" is not highlighted).

I think that the order of the terms supplied by the user is not important (see #1651), so if we don't know in which occurrence the user is interested in, we'd better highlight all of them.


Resolving patch:
diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py
--- a/qutebrowser/completion/completiondelegate.py
+++ b/qutebrowser/completion/completiondelegate.py
@@ -200,7 +200,7 @@ def _get_textdoc(self, index):
             columns_to_filter = index.model().srcmodel.columns_to_filter
             if index.column() in columns_to_filter and pattern:
                 repl = r'<span class="highlight">\g<0></span>'
-                text = re.sub(re.escape(pattern).replace(r'\ ', r'.*'),
+                text = re.sub(re.escape(pattern).replace(r'\ ', r'|'),
                               repl, self._opt.text, flags=re.IGNORECASE)
                 self._doc.setHtml(text)
             else:
diff --git a/qutebrowser/completion/models/base.py b/qutebrowser/completion/models/base.py
--- a/qutebrowser/completion/models/base.py
+++ b/qutebrowser/completion/models/base.py
@@ -126,20 +126,3 @@ def flags(self, index):
         else:
             # category
             return Qt.NoItemFlags
-
-    def sort(self, column, order=Qt.AscendingOrder):
-        """Sort the data in column according to order.
-
-        Override QAbstractItemModel::sort.
-        """
-        raise NotImplementedError
-
-    def custom_filter(self, pattern, row, parent):
-        """Custom filter.
-
-        Args:
-            pattern: The current filter pattern.
-            row: The row to accept or reject in the filter.
-            parent: The parent item QModelIndex.
-        """
-        raise NotImplementedError
diff --git a/qutebrowser/completion/models/sortfilter.py b/qutebrowser/completion/models/sortfilter.py
--- a/qutebrowser/completion/models/sortfilter.py
+++ b/qutebrowser/completion/models/sortfilter.py
@@ -63,9 +63,6 @@ def set_pattern(self, val):
 
         Invalidates the filter and re-sorts the model.
 
-        If the current completion model overrides sort(), it is used.
-        If not, the default implementation in QCompletionFilterModel is called.
-
         Args:
             val: The value to set.
         """
@@ -74,13 +71,9 @@ def set_pattern(self, val):
             val = re.escape(val)
             val = val.replace(r'\ ', r'.*')
             self.pattern_re = re.compile(val, re.IGNORECASE)
-            self.invalidateFilter()
-            sortcol = 0
-            try:
-                self.srcmodel.sort(sortcol)
-            except NotImplementedError:
-                self.sort(sortcol)
             self.invalidate()
+            sortcol = 0
+            self.sort(sortcol)
 
     def count(self):
         """Get the count of non-toplevel items currently visible.
@@ -140,20 +133,17 @@ def filterAcceptsRow(self, row, parent):
         if parent == QModelIndex() or not self.pattern:
             return True
 
-        try:
-            return self.srcmodel.custom_filter(self.pattern, row, parent)
-        except NotImplementedError:
-            for col in self.srcmodel.columns_to_filter:
-                idx = self.srcmodel.index(row, col, parent)
-                if not idx.isValid():
-                    # No entries in parent model
-                    continue
-                data = self.srcmodel.data(idx)
-                if not data:
-                    continue
-                elif self.pattern_re.search(data):
-                    return True
-            return False
+        for col in self.srcmodel.columns_to_filter:
+            idx = self.srcmodel.index(row, col, parent)
+            if not idx.isValid():
+                # No entries in parent model
+                continue
+            data = self.srcmodel.data(idx)
+            if not data:
+                continue
+            elif self.pattern_re.search(data):
+                return True
+        return False
 
     def intelligentLessThan(self, lindex, rindex):
         """Custom sorting implementation.


Test patch: