Repository: xonsh/xonsh

Issue:
ptk: pressing Backspace deletes an entire word instead of a single character
## xonfig

```
+------------------+----------------------+
| xonsh            | 0.10.1               |
| Git SHA          | f5acbe30             |
| Commit Date      | Jul 31 17:38:31 2021 |
| Python           | 3.9.6                |
| PLY              | 3.11                 |
| have readline    | True                 |
| prompt toolkit   | 3.0.18               |
| shell type       | prompt_toolkit       |
| history backend  | sqlite               |
| pygments         | 2.9.0                |
| on posix         | True                 |
| on linux         | True                 |
| distro           | arch                 |
| on wsl           | False                |
| on darwin        | False                |
| on windows       | False                |
| on cygwin        | False                |
| on msys2         | False                |
| is superuser     | False                |
| default encoding | utf-8                |
| xonsh encoding   | utf-8                |
| encoding errors  | surrogateescape      |
| on jupyter       | False                |
| jupyter kernel   | None                 |
| xontrib          | []                   |
+------------------+----------------------+
```

## Expected Behavior
When Backspace is pressed, a single character to the left of the cursor should be deleted.

## Current Behavior
When Backspace is pressed, the entire word to the left of the cursor gets deleted.

## Steps to Reproduce
1. Open a prompt-toolkit xonsh shell (updated to the latest `main`)
2. Type a word (e.g. `hello`)
3. Press Backspace

I think this issue was introduced by PR https://github.com/xonsh/xonsh/pull/4402, which was intended to make <kbd>Ctrl+Backspace</kbd> delete a word, but it unintentionally also changed <kbd>Backspace</kbd> to delete a word.

I have been able to produce this behavior on multiple terminal emulators: Alacritty, Gnome Terminal, and Konsole.

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
Hi!
I see something wierd happened with that pr since it doesn't work for me locally at all right now (and it used to work for sure), so something got messed up 🤔
I'll look into it
In those terminal emulators, what do you see when running `cat` and pressing ctrl-backspace? 
I see a `^H` for each ctrl-backspace that I press. Pressing ctrl-backspace three times, this is what I get:
```
[yaxollum@archlinux ~]$ cat
^H^H^H
```

If I try running `cat | hexdump -C`, and then press the following keystrokes:
1. ctrl-backspace
2. ctrl-backspace
3. ctrl-backspace
4. Enter
5. Ctrl-D

This is the output of `hexdump`:
```
00000000  08 08 08 0a                                       |....|
00000004
```
The key combinations `<Control-Backspace>` and `<Backspace>` are not guaranteed to
produce different results. Sometimes <Backspace> actually produces `<Del>`, in
which case PR #4402 will have the intended behaviour, but it depends.  Some
terminals may produce `<C-H>` (code 8) when `<Backspace>` is pressed, in which case
there is no difference between `<Control-Backspace>` and `<Backspace>`.

When prompt-toolket is in use, `<Del>` is converted to `<C-H>` in order to make
things behave consistently regardless of terminal behaviour. So again, there is
no difference between `<Control-Backspace>` and `<Backspace>`.

The botton line is that there is no practicable and portable way to distinguish
the two key combinations differently.

So, unfortunately, PR #4402 really needs to be reversed.

Resolving patch:
diff --git a/xonsh/environ.py b/xonsh/environ.py
--- a/xonsh/environ.py
+++ b/xonsh/environ.py
@@ -1553,6 +1553,13 @@ class PTKSetting(PromptSetting):  # sub-classing -> sub-group
         "Whether to copy words/lines to clipboard on deletion (must be set in .xonshrc file)."
         "Only available under the prompt-toolkit shell.",
     )
+    XONSH_CTRL_BKSP_DELETION = Var.with_default(
+        False,
+        "Delete a word on CTRL-Backspace (like ALT-Backspace). "
+        r"This will only work when your terminal emulator sends ``\x7f`` on backspace and "
+        r"``\x08`` on CTRL-Backspace (which is configurable on most terminal emulators). "
+        r"On windows, the keys are reversed.",
+    )
 
 
 class AsyncPromptSetting(PTKSetting):
diff --git a/xonsh/ptk_shell/key_bindings.py b/xonsh/ptk_shell/key_bindings.py
--- a/xonsh/ptk_shell/key_bindings.py
+++ b/xonsh/ptk_shell/key_bindings.py
@@ -13,6 +13,7 @@
     IsSearching,
 )
 from prompt_toolkit.keys import Keys
+from prompt_toolkit.input import ansi_escape_sequences
 from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase
 from prompt_toolkit.key_binding.bindings.named_commands import get_by_name
 
@@ -23,6 +24,7 @@
     ends_with_colon_token,
 )
 from xonsh.built_ins import XSH
+from xonsh.platform import ON_WINDOWS
 from xonsh.shell import transform_command
 
 DEDENT_TOKENS = frozenset(["raise", "return", "pass", "break", "continue"])
@@ -222,11 +224,31 @@ def load_xonsh_bindings(ptk_bindings: KeyBindingsBase) -> KeyBindingsBase:
     has_selection = HasSelection()
     insert_mode = ViInsertMode() | EmacsInsertMode()
 
-    # Most terminals send ^H from ctrl-backspace
-    @handle(Keys.ControlH, filter=insert_mode)
-    def delete_word(event):
-        """Delete a single word (like ALT-backspace)"""
-        get_by_name("backward-kill-word").call(event)
+    if XSH.env["XONSH_CTRL_BKSP_DELETION"]:
+        # Not all terminal emulators emit the same keys for backspace, therefore
+        # ptk always maps backspace ("\x7f") to ^H ("\x08"), and all the backspace bindings are registered for ^H.
+        # This means we can't re-map backspace and instead we register a new "real-ctrl-bksp" key.
+        # See https://github.com/xonsh/xonsh/issues/4407
+
+        if ON_WINDOWS:
+            # On windows BKSP is "\x08" and CTRL-BKSP is "\x7f"
+            REAL_CTRL_BKSP = "\x7f"
+
+            # PTK uses a second mapping
+            from prompt_toolkit.input import win32 as ptk_win32
+
+            ptk_win32.ConsoleInputReader.mappings[b"\x7f"] = REAL_CTRL_BKSP  # type: ignore
+        else:
+            REAL_CTRL_BKSP = "\x08"
+
+        # Prompt-toolkit allows using single-character keys that aren't in the `Keys` enum.
+        ansi_escape_sequences.ANSI_SEQUENCES[REAL_CTRL_BKSP] = REAL_CTRL_BKSP  # type: ignore
+        ansi_escape_sequences.REVERSE_ANSI_SEQUENCES[REAL_CTRL_BKSP] = REAL_CTRL_BKSP  # type: ignore
+
+        @handle(REAL_CTRL_BKSP, filter=insert_mode)
+        def delete_word(event):
+            """Delete a single word (like ALT-backspace)"""
+            get_by_name("backward-kill-word").call(event)
 
     @handle(Keys.Tab, filter=tab_insert_indent)
     def insert_indent(event):


Test patch: