Repository: xonsh/xonsh

Issue:
abbrevs should only be expanded at the start of the command
<!--- Provide a general summary of the issue in the Title above -->
<!--- If you have a question along the lines of "How do I do this Bash command in xonsh"
please first look over the Bash to Xonsh translation guide: https://xon.sh/bash_to_xsh.html
If you don't find an answer there, please do open an issue! -->

## xonfig

<details>

```
+------------------+---------------------+
| xonsh            | 0.9.18              |
| Git SHA          | 26475146            |
| Commit Date      | May 5 06:42:28 2020 |
| Python           | 3.8.3               |
| PLY              | 3.11                |
| have readline    | True                |
| prompt toolkit   | 3.0.5               |
| shell type       | prompt_toolkit      |
| pygments         | 2.6.1               |
| on posix         | True                |
| on linux         | True                |
| distro           | manjaro             |
| 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     |
+------------------+---------------------+
```

</details>

<!--- Tell us what should happen -->
as of abbrevs expands in all places. So it may clash will subcommands. For example, if we have 

~~`aliases['ps'] = 'procs'`~~
```
abbrevs['ps'] = 'procs'
``` 

and do type `docker ps` then it will get expanded to `docker procs`. 

So the default behaviour should be to expand only at the start of the command. special handling for sudo can be added. 
Also we could have special symbols in aliases to mark it as expandable in all places like `aliases['*ps*']` like the glob pattern. 






Discussion:
I think that it does only expand at the beginning.  In this example, docker has a `ps` subcommand.

```
$ docker ps --help                                                                                                                                                                

Usage:	docker ps [OPTIONS]

List containers

Options:
  -a, --all             Show all containers (default shows just running)
  -f, --filter filter   Filter output based on conditions provided
      --format string   Pretty-print containers using a Go template
  -n, --last int        Show n last created containers (includes all states) (default -1)
  -l, --latest          Show the latest created container (includes all states)
      --no-trunc        Don't truncate output
  -q, --quiet           Only display numeric IDs
  -s, --size            Display total file sizes
```
This should show the issue
![image](https://user-images.githubusercontent.com/6702219/88026340-5bbc1b80-cb53-11ea-81da-bb77b9984a97.png)

In the description, it was a typo. I will change. The definition is in `abbrevs`
What is abbrevs?
https://xon.sh/xontribs.html#abbrevs
Oh OK! I am not fmailiar with that part of the code
This is a feature rather than a bug. I specifically wanted the abbrevs to be able to expand one or more subprocess arguments and Python expressions. You would also lose the ability to expand abbreviated commands in logical subprocess expressions (`foo and bar`) and pipelines (`foo | bar`) by limiting the expansion to the start of the command line. The expansion can be avoided by quoting the argument string (e.g. `docker "ps"`). So, I'm against adding such a limitation.
What about adding a possibility to "revert" the expansion for instance by pressing the space key once again after the expansion takes place?
I think the default workflow should not interfere with how things work usually. There are two things that will make it more correct. 

1. make it context aware. if it founds within subprocess/sudo then it could expand. we could add more cases as we find them and with full correctness.
2. have the shortcut that forces a word expansion even if it fails. as of now simple `space/enter` triggers it. we can make it to be `shift+space`

> 1. make it context aware. if it founds within subprocess/sudo then it could expand. we could add more cases as we find them and with full correctness.

Such a dictionary based rule is a no-go to me, simply because the number of command prefixes is infinite (`sudo`, `super`, `su -c`, `sh -c`, `env`, `ssh`, ...). We would also lose the Python mode expansion. Now we even can expand Xonsh/Python code in multiline...

> 2. have the shortcut that forces a word expansion even if it fails. as of now simple `space/enter` triggers it. we can make it to be `shift+space`

We are limited by what VT100/VT220 terminals and their emulators can do. Combinations of space key and modifiers are either non-existent (<kbd>Shift</kbd>+<kbd>Space</kbd>) or ambiguous (<kbd>Ctrl</kbd>+<kbd>Space</kbd>).
@laloch so you don't want to handle such clashes without disrupting how things usually work? please check the fish shell's way of doing. https://fishshell.com/docs/current/cmds/abbr.html

Atleast we need a way to restrict abbrevs that only expand at the start. Can we have something like `^abbrev` definitions that expand only at the start? 

also there must be some way to detect the context other than having a list of dictionary right?
How do I make this nice feature work?
This guide's suggestion of using `$ abbrevs['ps'] = lambda buffer, word: "procs" if buffer.text.startswith(word) else word` doesn't work, I get 'ps' replaced with 'ps' even at the start of a command line
As far as I understand from some rudimentary testing the `buffer` that is passed only contains the buffer **before** the abbreviation
`$ abbrevs['aa'] = lambda buffer, word: '|' + str(buffer.text) + '|'`
`$ aa`'   → '`|| `' (buffer is empty before '_aa_', so nothing gets passed)
`$ b aa`' → '`b |b | `' (buffer before '_aa_' is '_b_ ', so '_b_ ' gets passed)

This is with the latest xonsh with an empty config with only `xontrib load abbrevs` loaded both in a Windows and a Ubuntu shell
What am I doing wrong?
> How do I make this nice feature work?
> This guide's suggestion of using `$ abbrevs['ps'] = lambda buffer, word: "procs" if buffer.text.startswith(word) else word` doesn't work, I get 'ps' replaced with 'ps' even at the start of a command line
> As far as I understand from some rudimentary testing the `buffer` that is passed only contains the buffer **before** the abbreviation
> `$ abbrevs['aa'] = lambda buffer, word: '|' + str(buffer.text) + '|'`
> `$ aa`' → '`|| `' (buffer is empty before '_aa_', so nothing gets passed)
> `$ b aa`' → '`b |b | `' (buffer before '_aa_' is '_b_ ', so '_b_ ' gets passed)
> 
> This is with the latest xonsh with an empty config with only `xontrib load abbrevs` loaded both in a Windows and a Ubuntu shell
> What am I doing wrong?

Sorry it turns out to be a bug. The code clears out the word before passing to the callback. I am preparing a PR with the fix. 

Resolving patch:
diff --git a/xontrib/abbrevs.py b/xontrib/abbrevs.py
--- a/xontrib/abbrevs.py
+++ b/xontrib/abbrevs.py
@@ -28,6 +28,7 @@
 import builtins
 import typing as tp
 
+from prompt_toolkit.buffer import Buffer
 from prompt_toolkit.filters import completion_is_selected, IsMultiline
 from prompt_toolkit.keys import Keys
 from xonsh.built_ins import DynamicAccessProxy
@@ -60,7 +61,7 @@ def get_abbreviated(key: str, buffer) -> str:
     return text
 
 
-def expand_abbrev(buffer) -> bool:
+def expand_abbrev(buffer: Buffer) -> bool:
     """expand the given abbr text. Return true if cursor position changed."""
     global last_expanded
     last_expanded = None
@@ -74,9 +75,11 @@ def expand_abbrev(buffer) -> bool:
         startix, endix, quote = check_for_partial_string(partial)
         if startix is not None and endix is None:
             return False
-        buffer.delete_before_cursor(count=len(word))
         text = get_abbreviated(word, buffer)
+
+        buffer.delete_before_cursor(count=len(word))
         buffer.insert_text(text)
+
         last_expanded = _LastExpanded(word, text)
         if EDIT_SYMBOL in text:
             set_cursor_position(buffer, text)


Test patch:
diff --git a/tests/xontribs/test_abbrevs.py b/tests/xontribs/test_abbrevs.py
--- a/tests/xontribs/test_abbrevs.py
+++ b/tests/xontribs/test_abbrevs.py
@@ -27,11 +27,17 @@ def abbrevs_xontrib(monkeypatch, source_path):
     del sys.modules[spec.name]
 
 
+ps_special_expand = (
+    lambda buffer, word: "procs" if buffer.text.startswith(word) else word
+)
+
+
 @mark.parametrize(
     "abbr,val,expanded,cur",
     [
         ("ps", "procs", "procs", None),
-        ("ps", lambda **kw: "callback", "callback", None),
+        ("ps", ps_special_expand, "procs", None),
+        ("docker ps", ps_special_expand, "docker ps", None),
         ("kill", "kill <edit> -9", "kill  -9", 5),
         ("pt", "poe<edit>try", "poetry", 3),
     ],