Repository: qutebrowser/qutebrowser

Issue:
Incremental search
While searching with `/`, the results only appear after hitting enter. Better to highlight as you type, as in luakit.



Discussion:
Not a feature of luakit.

Incremental searching is useful.  It's also a standard feature for web browsers.  Regardless of what luakit has, I consider this feature request important.

Ok, reopened.

Thank you!  Here's hoping others feel the same way.

+1

On Sat, Mar 28, 2015 at 03:30:42AM -0700, NoSuck scribed:

> Thank you! Here's hoping others feel the same way.
> 
> —
> Reply to this email directly or view it on GitHub.*

## 

```
 "Sometimes experts use their experience not to explore the future, 
                  but merely to protect the past."
                          (Michael Hewitt-Gleeson)
```

Geir Isene                 |    /"\    ASCII Ribbon Campaign  
http://isene.com           |    \ /    Respect for open standards
http://isene.me            |     X     No HTML/RTF in email  
+47-4550 2470              |    / \    No MS Word docs in email  

I was about to say this isn't possible because QtWebKit handles the searching - but actually it should be possible by invoking [QWebView::findText](http://doc.qt.io/qt-5/qwebview.html#findText) multiple times, i.e. once with each typed character.

However, the command input needs some way (e.g. a signal) to inform the `TabbedBrowser` a new partial search term was typed, so that can start a search.

+1

+1. Definetly a must-have.
@robcsi https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments :wink:
any progress of this issue?
No. Nobody picked this up so far as far as I'm aware.

Resolving patch:
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -1749,7 +1749,8 @@ def _search_cb(self, found, *, tab, old_scroll_pos, options, text, prev):
             elif going_up and tab.scroller.pos_px().y() > old_scroll_pos.y():
                 message.info("Search hit TOP, continuing at BOTTOM")
         else:
-            message.warning("Text '{}' not found on page!".format(text))
+            message.warning("Text '{}' not found on page!".format(text),
+                replace=True)
 
     @cmdutils.register(instance='command-dispatcher', scope='window',
                        maxsplit=0)
@@ -1769,7 +1770,7 @@ def search(self, text="", reverse=False):
             return
 
         options = {
-            'ignore_case': config.val.ignore_case,
+            'ignore_case': config.val.search.ignore_case,
             'reverse': reverse,
         }
 
diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py
--- a/qutebrowser/mainwindow/statusbar/command.py
+++ b/qutebrowser/mainwindow/statusbar/command.py
@@ -27,6 +27,7 @@
 from qutebrowser.misc import cmdhistory, editor
 from qutebrowser.misc import miscwidgets as misc
 from qutebrowser.utils import usertypes, log, objreg, message
+from qutebrowser.config import config
 
 
 class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@@ -66,6 +67,7 @@ def __init__(self, *, win_id, private, parent=None):
         self.cursorPositionChanged.connect(self.update_completion)
         self.textChanged.connect(self.update_completion)
         self.textChanged.connect(self.updateGeometry)
+        self.textChanged.connect(self._incremental_search)
 
     def prefix(self):
         """Get the currently entered command prefix."""
@@ -238,3 +240,16 @@ def sizeHint(self):
             text = 'x'
         width = self.fontMetrics().width(text)
         return QSize(width, height)
+
+    @pyqtSlot(str)
+    def _incremental_search(self, text):
+        if not config.val.search.incremental:
+            return
+
+        search_prefixes = {
+            '/': 'search -- ',
+            '?': 'search -r -- ',
+        }
+
+        if self.prefix() in ['/', '?']:
+            self.got_cmd[str].emit(search_prefixes[text[0]] + text[1:])


Test patch:
diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature
--- a/tests/end2end/features/qutescheme.feature
+++ b/tests/end2end/features/qutescheme.feature
@@ -99,19 +99,19 @@ Feature: Special qute:// pages
     # qute://settings
 
     Scenario: Focusing input fields in qute://settings and entering valid value
-        When I set ignore_case to never
+        When I set search.ignore_case to never
         And I open qute://settings
         # scroll to the right - the table does not fit in the default screen
         And I run :scroll-to-perc -x 100
-        And I run :jseval document.getElementById('input-ignore_case').value = ''
-        And I run :click-element id input-ignore_case
+        And I run :jseval document.getElementById('input-search.ignore_case').value = ''
+        And I run :click-element id input-search.ignore_case
         And I wait for "Entering mode KeyMode.insert *" in the log
         And I press the keys "always"
         And I press the key "<Escape>"
         # an explicit Tab to unfocus the input field seems to stabilize the tests
         And I press the key "<Tab>"
-        And I wait for "Config option changed: ignore_case *" in the log
-        Then the option ignore_case should be set to always
+        And I wait for "Config option changed: search.ignore_case *" in the log
+        Then the option search.ignore_case should be set to always
 
     # Sometimes, an unrelated value gets set
     @flaky
@@ -119,8 +119,8 @@ Feature: Special qute:// pages
         When I open qute://settings
         # scroll to the right - the table does not fit in the default screen
         And I run :scroll-to-perc -x 100
-        And I run :jseval document.getElementById('input-ignore_case').value = ''
-        And I run :click-element id input-ignore_case
+        And I run :jseval document.getElementById('input-search.ignore_case').value = ''
+        And I run :click-element id input-search.ignore_case
         And I wait for "Entering mode KeyMode.insert *" in the log
         And I press the keys "foo"
         And I press the key "<Escape>"
diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature
--- a/tests/end2end/features/search.feature
+++ b/tests/end2end/features/search.feature
@@ -22,7 +22,7 @@ Feature: Searching on a page
         Then "Bar" should be found
 
     Scenario: Searching with --reverse
-        When I set ignore_case to always
+        When I set search.ignore_case to always
         And I run :search -r foo
         And I wait for "search found foo with flags FindBackward" in the log
         Then "Foo" should be found
@@ -52,28 +52,28 @@ Feature: Searching on a page
         And I wait for "search didn't find blub" in the log
         Then the warning "Text 'blub' not found on page!" should be shown
 
-    ## ignore_case
+    ## search.ignore_case
 
-    Scenario: Searching text with ignore_case = always
-        When I set ignore_case to always
+    Scenario: Searching text with search.ignore_case = always
+        When I set search.ignore_case to always
         And I run :search bar
         And I wait for "search found bar" in the log
         Then "Bar" should be found
 
-    Scenario: Searching text with ignore_case = never
-        When I set ignore_case to never
+    Scenario: Searching text with search.ignore_case = never
+        When I set search.ignore_case to never
         And I run :search bar
         And I wait for "search found bar with flags FindCaseSensitively" in the log
         Then "bar" should be found
 
-    Scenario: Searching text with ignore_case = smart (lower-case)
-        When I set ignore_case to smart
+    Scenario: Searching text with search.ignore_case = smart (lower-case)
+        When I set search.ignore_case to smart
         And I run :search bar
         And I wait for "search found bar" in the log
         Then "Bar" should be found
 
-    Scenario: Searching text with ignore_case = smart (upper-case)
-        When I set ignore_case to smart
+    Scenario: Searching text with search.ignore_case = smart (upper-case)
+        When I set search.ignore_case to smart
         And I run :search Foo
         And I wait for "search found Foo with flags FindCaseSensitively" in the log
         Then "Foo" should be found  # even though foo was first
@@ -81,7 +81,7 @@ Feature: Searching on a page
     ## :search-next
 
     Scenario: Jumping to next match
-        When I set ignore_case to always
+        When I set search.ignore_case to always
         And I run :search foo
         And I wait for "search found foo" in the log
         And I run :search-next
@@ -89,7 +89,7 @@ Feature: Searching on a page
         Then "Foo" should be found
 
     Scenario: Jumping to next match with count
-        When I set ignore_case to always
+        When I set search.ignore_case to always
         And I run :search baz
         And I wait for "search found baz" in the log
         And I run :search-next with count 2
@@ -97,7 +97,7 @@ Feature: Searching on a page
         Then "BAZ" should be found
 
     Scenario: Jumping to next match with --reverse
-        When I set ignore_case to always
+        When I set search.ignore_case to always
         And I run :search --reverse foo
         And I wait for "search found foo with flags FindBackward" in the log
         And I run :search-next
@@ -121,7 +121,7 @@ Feature: Searching on a page
 
     # https://github.com/qutebrowser/qutebrowser/issues/2438
     Scenario: Jumping to next match after clearing
-        When I set ignore_case to always
+        When I set search.ignore_case to always
         And I run :search foo
         And I wait for "search found foo" in the log
         And I run :search
@@ -132,7 +132,7 @@ Feature: Searching on a page
     ## :search-prev
 
     Scenario: Jumping to previous match
-        When I set ignore_case to always
+        When I set search.ignore_case to always
         And I run :search foo
         And I wait for "search found foo" in the log
         And I run :search-next
@@ -142,7 +142,7 @@ Feature: Searching on a page
         Then "foo" should be found
 
     Scenario: Jumping to previous match with count
-        When I set ignore_case to always
+        When I set search.ignore_case to always
         And I run :search baz
         And I wait for "search found baz" in the log
         And I run :search-next
@@ -154,7 +154,7 @@ Feature: Searching on a page
         Then "baz" should be found
 
     Scenario: Jumping to previous match with --reverse
-        When I set ignore_case to always
+        When I set search.ignore_case to always
         And I run :search --reverse foo
         And I wait for "search found foo with flags FindBackward" in the log
         And I run :search-next
diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py
--- a/tests/end2end/test_invocations.py
+++ b/tests/end2end/test_invocations.py
@@ -359,14 +359,14 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
     args = _base_args(request.config) + ['--basedir', str(short_tmpdir)]
     quteproc_new.start(args)
     quteproc_new.open_path(
-        'qute://settings/set?option=ignore_case&value=always')
-    assert quteproc_new.get_setting('ignore_case') == 'always'
+        'qute://settings/set?option=search.ignore_case&value=always')
+    assert quteproc_new.get_setting('search.ignore_case') == 'always'
 
     quteproc_new.send_cmd(':quit')
     quteproc_new.wait_for_quit()
 
     quteproc_new.start(args)
-    assert quteproc_new.get_setting('ignore_case') == 'always'
+    assert quteproc_new.get_setting('search.ignore_case') == 'always'
 
 
 @pytest.mark.no_xvfb
diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py
--- a/tests/unit/config/test_configcommands.py
+++ b/tests/unit/config/test_configcommands.py
@@ -281,7 +281,7 @@ class TestSource:
     def test_config_source(self, tmpdir, commands, config_stub, config_tmpdir,
                            use_default_dir, clear):
         assert config_stub.val.content.javascript.enabled
-        config_stub.val.ignore_case = 'always'
+        config_stub.val.search.ignore_case = 'always'
 
         if use_default_dir:
             pyfile = config_tmpdir / 'config.py'
@@ -295,7 +295,8 @@ def test_config_source(self, tmpdir, commands, config_stub, config_tmpdir,
         commands.config_source(arg, clear=clear)
 
         assert not config_stub.val.content.javascript.enabled
-        assert config_stub.val.ignore_case == ('smart' if clear else 'always')
+        ignore_case = config_stub.val.search.ignore_case
+        assert ignore_case == ('smart' if clear else 'always')
 
     def test_errors(self, commands, config_tmpdir):
         pyfile = config_tmpdir / 'config.py'
diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py
--- a/tests/unit/config/test_configdata.py
+++ b/tests/unit/config/test_configdata.py
@@ -34,7 +34,7 @@ def test_init(config_stub):
     # configdata.init() is called by config_stub
     config_stub.val.aliases = {}
     assert isinstance(configdata.DATA, dict)
-    assert 'ignore_case' in configdata.DATA
+    assert 'search.ignore_case' in configdata.DATA
 
 
 def test_data(config_stub):