Repository: qutebrowser/qutebrowser

Issue:
Add :rl-backward-kill-word
Previously, you could use ^W in the download prompt to delete the previous word, which was useful for backing up a single directory. Now it deletes everything up to the /.

---

qutebrowser v0.8.0
Git commit: 

CPython: 3.5.2
Qt: 5.7.0 (compiled 5.6.0)
PyQt: 5.6

sip: 4.18
colorama: no
pypeg2: 2.15
jinja2: 2.8
pygments: 2.1.3
yaml: 3.11
cssutils: no
typing: yes
PyQt5.QtWebEngineWidgets: yes
pdf.js: 1.2.109 (/usr/share/pdf.js/build/pdf.js)
Webkit: 538.1
Harfbuzz: old
SSL: OpenSSL 1.0.2h  3 May 2016

Platform: Linux-4.6.4-1-ARCH-x86_64-with-arch-Arch-Linux, 64bit
Desktop: None
Frozen: False
Imported from /usr/lib/python3.5/site-packages/qutebrowser
OS Version: 

--- /etc/os-release ---
NAME="Arch Linux"
ID=arch
PRETTY_NAME="Arch Linux"

--- /etc/arch-release ---
Arch Linux release

--- /etc/lsb-release ---
LSB_VERSION=1.4
DISTRIB_ID=Arch
DISTRIB_RELEASE=rolling
DISTRIB_DESCRIPTION="Arch Linux"



Discussion:
Yeah, the behaviour changed to match what readline does. You can use `Ctrl-Backspace` FWIW, but it might make sense to override this for (download?) prompts.

cc @edi9999 who did that in #1592.

Well, readline is customizable via `inputrc`, so the right way to do this is via [readline](https://docs.python.org/3.5/library/readline.html) or similar interface which does not block and provides the completion callbacks.

Edit: see also
- http://bugs.python.org/issue1713877
- http://pythonhosted.org/pyreadline/introduction.html

Hey, yes indeed, I wanted to respect readline a bit more by having C-W delete one full argument (that's what readline does by default with C-W). 

I think in readline, to delete a word, the shortcut is `Alt-backspace`, but in qutebrowser it is `Ctrl-Backspace`.

> but it might make sense to override this for (download?) prompts.

I would find it very confusing if the shortcuts were behaving differently depending on the type of prompt. I think `C-w` might also make sense in the download section, since you maybe want to save your file to /tmp/foo.txt for example, in which case it is fastest to start from an empty string.

> the right way to do this is via readline

I would also like to have a "real" readline implementation, but I'm not sure readline is "input" agnostic, eg how can you make it work from inside Qt (Usually readline is used with console AFAICT) ?

Using a real readline implementation is an interesting idea, but I think it has some drawbacks - I think the implementation might get quite complicated, and it's not available on Windows.

pyreadline seemed interesting, but it seems to just expose the same interface `readline` does? Also, it seems pretty much unmaintained, and not commonly packaged.

But back to this issue - I think `Ctrl-Backspace` just works because it's a default Qt shortcut for input fields, not because qutebrowser does anything specific. I think we could simply add a `:rl-backward-kill-word` command (what `Alt-Backspace` is in readline) with the old behaviour, and then @SirCmpwn could simply rebind `C-w` to that?


Resolving patch:
diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py
--- a/qutebrowser/config/configdata.py
+++ b/qutebrowser/config/configdata.py
@@ -1587,7 +1587,8 @@ def data(readonly=False):
         ('rl-unix-line-discard', ['<Ctrl-U>']),
         ('rl-kill-line', ['<Ctrl-K>']),
         ('rl-kill-word', ['<Alt-D>']),
-        ('rl-unix-word-rubout', ['<Ctrl-W>', '<Alt-Backspace>']),
+        ('rl-unix-word-rubout', ['<Ctrl-W>']),
+        ('rl-backward-kill-word', ['<Alt-Backspace>']),
         ('rl-yank', ['<Ctrl-Y>']),
         ('rl-delete-char', ['<Ctrl-?>']),
         ('rl-backward-delete-char', ['<Ctrl-H>']),
diff --git a/qutebrowser/misc/readline.py b/qutebrowser/misc/readline.py
--- a/qutebrowser/misc/readline.py
+++ b/qutebrowser/misc/readline.py
@@ -153,7 +153,8 @@ def rl_kill_line(self):
     def rl_unix_word_rubout(self):
         """Remove chars from the cursor to the beginning of the word.
 
-        This acts like readline's unix-word-rubout.
+        This acts like readline's unix-word-rubout. Whitespace is used as a
+        word delimiter.
         """
         widget = self._widget()
         if widget is None:
@@ -178,6 +179,21 @@ def rl_unix_word_rubout(self):
         self._deleted[widget] = widget.selectedText()
         widget.del_()
 
+    @cmdutils.register(instance='readline-bridge', hide=True,
+                       modes=[typ.KeyMode.command, typ.KeyMode.prompt])
+    def rl_backward_kill_word(self):
+        """Remove chars from the cursor to the beginning of the word.
+
+        This acts like readline's backward-kill-word. Any non-alphanumeric
+        character is considered a word delimiter.
+        """
+        widget = self._widget()
+        if widget is None:
+            return
+        widget.cursorWordBackward(True)
+        self._deleted[widget] = widget.selectedText()
+        widget.del_()
+
     @cmdutils.register(instance='readline-bridge', hide=True,
                        modes=[typ.KeyMode.command, typ.KeyMode.prompt])
     def rl_kill_word(self):


Test patch:
diff --git a/tests/unit/misc/test_readline.py b/tests/unit/misc/test_readline.py
--- a/tests/unit/misc/test_readline.py
+++ b/tests/unit/misc/test_readline.py
@@ -269,6 +269,25 @@ def test_rl_kill_word(lineedit, bridge, text, deleted, rest):
     assert lineedit.aug_text() == deleted + '|'
 
 
+@pytest.mark.parametrize('text, deleted, rest', [
+    ('test delete|foobar', 'delete', 'test |foobar'),
+    ('test delete |foobar', 'delete ', 'test |foobar'),
+    ('open -t github.com/foo/bar  |', 'bar  ', 'open -t github.com/foo/|'),
+    ('open -t |github.com/foo/bar', 't ', 'open -|github.com/foo/bar'),
+    fixme(('test del<ete>foobar', 'delete', 'test |foobar')),
+    ('test del<ete >foobar', 'del', 'test |ete foobar'),  # wrong
+])
+def test_rl_backward_kill_word(lineedit, bridge, text, deleted, rest):
+    """Delete to word beginning and see if it comes back with yank."""
+    lineedit.set_aug_text(text)
+    bridge.rl_backward_kill_word()
+    assert bridge._deleted[lineedit] == deleted
+    assert lineedit.aug_text() == rest
+    lineedit.clear()
+    bridge.rl_yank()
+    assert lineedit.aug_text() == deleted + '|'
+
+
 def test_rl_yank_no_text(lineedit, bridge):
     """Test yank without having deleted anything."""
     lineedit.clear()