mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-04 00:14:41 +01:00
Feat abbrevs callback (#4031)
* feat: support callbacks in abbrevs dict facilitate solving problems like #3642 fixes #3642 * test: add tests for abbrevs and news item * fix: mypy error * refactor: update imports for xontrib_meta module
This commit is contained in:
parent
71fe9014d2
commit
d50cf32f84
4 changed files with 153 additions and 43 deletions
23
news/callback-in-abbrevs.rst
Normal file
23
news/callback-in-abbrevs.rst
Normal file
|
@ -0,0 +1,23 @@
|
|||
**Added:**
|
||||
|
||||
* abbrevs now support callbacks
|
||||
|
||||
**Changed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Removed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Fixed:**
|
||||
|
||||
* <news item>
|
||||
|
||||
**Security:**
|
||||
|
||||
* <news item>
|
47
tests/xontribs/test_abbrevs.py
Normal file
47
tests/xontribs/test_abbrevs.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
"""test xontrib.abbrevs"""
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
from prompt_toolkit.buffer import Buffer
|
||||
from pytest import fixture, mark
|
||||
from xonsh.xontribs import find_xontrib
|
||||
import builtins
|
||||
|
||||
|
||||
@fixture
|
||||
def _buffer():
|
||||
def _wrapper(text):
|
||||
buf = Buffer()
|
||||
buf.insert_text(text)
|
||||
return buf
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
@fixture
|
||||
def abbrevs_xontrib(monkeypatch, source_path):
|
||||
monkeypatch.syspath_prepend(source_path)
|
||||
spec = find_xontrib("abbrevs")
|
||||
yield importlib.import_module(spec.name)
|
||||
del sys.modules[spec.name]
|
||||
|
||||
|
||||
@mark.parametrize(
|
||||
"abbr,val,expanded,cur",
|
||||
[
|
||||
("ps", "procs", "procs", None),
|
||||
("ps", lambda **kw: "callback", "callback", None),
|
||||
("kill", "kill <edit> -9", "kill -9", 5),
|
||||
("pt", "poe<edit>try", "poetry", 3),
|
||||
],
|
||||
)
|
||||
def test_gets_expanded(abbr, val, expanded, cur, abbrevs_xontrib, _buffer):
|
||||
builtins.abbrevs[abbr] = val
|
||||
from xontrib.abbrevs import expand_abbrev
|
||||
|
||||
buf = _buffer(abbr)
|
||||
expand_abbrev(buf)
|
||||
assert buf.text == expanded
|
||||
if cur is not None:
|
||||
assert buf.cursor_position == cur
|
|
@ -2,9 +2,14 @@
|
|||
This modules is the place where one would define the xontribs.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import functools
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import typing as tp
|
||||
|
||||
from xonsh.lazyasd import LazyObject, lazyobject
|
||||
|
||||
|
||||
class _XontribPkg(tp.NamedTuple):
|
||||
"""Class to define package information of a xontrib.
|
||||
|
@ -43,11 +48,20 @@ class Xontrib(tp.NamedTuple):
|
|||
"""
|
||||
|
||||
url: str = ""
|
||||
description: str = ""
|
||||
description: tp.Union[str, LazyObject] = ""
|
||||
package: tp.Optional[_XontribPkg] = None
|
||||
tags: tp.Tuple[str, ...] = ()
|
||||
|
||||
|
||||
def get_module_docstring(module: str) -> str:
|
||||
"""Find the module and return its docstring without actual import"""
|
||||
|
||||
spec = importlib.util.find_spec(module)
|
||||
if spec and spec.has_location and spec.origin:
|
||||
return ast.get_docstring(ast.parse(Path(spec.origin).read_text()))
|
||||
return ""
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_xontribs() -> tp.Dict[str, Xontrib]:
|
||||
"""Return xontrib definitions lazily."""
|
||||
|
@ -70,20 +84,7 @@ def define_xontribs():
|
|||
return {
|
||||
"abbrevs": Xontrib(
|
||||
url="http://xon.sh",
|
||||
description="Adds ``abbrevs`` dictionary to hold user-defined "
|
||||
"command abbreviations. The dictionary is searched "
|
||||
"as you type and the matching words are replaced "
|
||||
"at the command line by the corresponding "
|
||||
"dictionary contents once you hit 'Space' or "
|
||||
"'Return' key. For instance a frequently used "
|
||||
"command such as ``git status`` can be abbreviated "
|
||||
"to ``gst`` as follows::\n"
|
||||
"\n"
|
||||
" $ xontrib load abbrevs\n"
|
||||
" $ abbrevs['gst'] = 'git status'\n"
|
||||
" $ gst # Once you hit <space> or <return>, "
|
||||
"'gst' gets expanded to 'git status'.\n"
|
||||
"\n",
|
||||
description=lazyobject(lambda: get_module_docstring("xontrib.abbrevs")),
|
||||
package=core_pkg,
|
||||
),
|
||||
"apt_tabcomplete": Xontrib(
|
||||
|
|
|
@ -2,12 +2,36 @@
|
|||
Command abbreviations.
|
||||
|
||||
This expands input words from `abbrevs` disctionary as you type.
|
||||
Adds ``abbrevs`` dictionary to hold user-defined "command abbreviations.
|
||||
The dictionary is searched as you type the matching words are replaced
|
||||
at the command line by the corresponding dictionary contents once you hit
|
||||
'Space' or 'Return' key.
|
||||
|
||||
For instance a frequently used command such as ``git status`` can be abbreviated to ``gst`` as follows::
|
||||
|
||||
$ xontrib load abbrevs
|
||||
$ abbrevs['gst'] = 'git status'
|
||||
$ gst # Once you hit <space> or <return> 'gst' gets expanded to 'git status'.
|
||||
|
||||
one can set a callback function that receives current buffer and word to customize the expanded word based on context
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
$ abbrevs['ps'] = lambda buffer, word: "procs" if buffer.text.startswith(word) else word
|
||||
|
||||
|
||||
It is also possible to set the cursor position after expansion with,
|
||||
|
||||
$ abbrevs['gp'] = "git push <edit> --force"
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import typing as tp
|
||||
|
||||
from prompt_toolkit.filters import completion_is_selected, IsMultiline
|
||||
from prompt_toolkit.keys import Keys
|
||||
from xonsh.built_ins import DynamicAccessProxy
|
||||
from xonsh.events import events
|
||||
from xonsh.tools import check_for_partial_string
|
||||
|
||||
__all__ = ()
|
||||
|
@ -16,59 +40,70 @@ builtins.__xonsh__.abbrevs = dict()
|
|||
proxy = DynamicAccessProxy("abbrevs", "__xonsh__.abbrevs")
|
||||
setattr(builtins, "abbrevs", proxy)
|
||||
|
||||
last_expanded = None
|
||||
|
||||
class _LastExpanded(tp.NamedTuple):
|
||||
word: str
|
||||
expanded: str
|
||||
|
||||
|
||||
def expand_abbrev(buffer):
|
||||
last_expanded: tp.Optional[_LastExpanded] = None
|
||||
EDIT_SYMBOL = "<edit>"
|
||||
|
||||
|
||||
def get_abbreviated(key: str, buffer) -> str:
|
||||
abbrevs = getattr(builtins, "abbrevs", None)
|
||||
abbr = abbrevs[key]
|
||||
if callable(abbr):
|
||||
text = abbr(buffer=buffer, word=key)
|
||||
else:
|
||||
text = abbr
|
||||
return text
|
||||
|
||||
|
||||
def expand_abbrev(buffer) -> bool:
|
||||
"""expand the given abbr text. Return true if cursor position changed."""
|
||||
global last_expanded
|
||||
last_expanded = None
|
||||
abbrevs = getattr(builtins, "abbrevs", None)
|
||||
if abbrevs is None:
|
||||
return
|
||||
return False
|
||||
document = buffer.document
|
||||
word = document.get_word_before_cursor(WORD=True)
|
||||
if word in abbrevs.keys():
|
||||
partial = document.text[: document.cursor_position]
|
||||
startix, endix, quote = check_for_partial_string(partial)
|
||||
if startix is not None and endix is None:
|
||||
return
|
||||
return False
|
||||
buffer.delete_before_cursor(count=len(word))
|
||||
buffer.insert_text(abbrevs[word])
|
||||
last_expanded = word
|
||||
text = get_abbreviated(word, buffer)
|
||||
buffer.insert_text(text)
|
||||
last_expanded = _LastExpanded(word, text)
|
||||
if EDIT_SYMBOL in text:
|
||||
set_cursor_position(buffer, text)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def revert_abbrev(buffer):
|
||||
def revert_abbrev(buffer) -> bool:
|
||||
global last_expanded
|
||||
if last_expanded is None:
|
||||
return False
|
||||
abbrevs = getattr(builtins, "abbrevs", None)
|
||||
if abbrevs is None:
|
||||
return False
|
||||
if last_expanded not in abbrevs.keys():
|
||||
return False
|
||||
document = buffer.document
|
||||
expansion = abbrevs[last_expanded] + " "
|
||||
expansion = last_expanded.expanded + " "
|
||||
if not document.text_before_cursor.endswith(expansion):
|
||||
return False
|
||||
buffer.delete_before_cursor(count=len(expansion))
|
||||
buffer.insert_text(last_expanded)
|
||||
buffer.insert_text(last_expanded.word)
|
||||
last_expanded = None
|
||||
return True
|
||||
|
||||
|
||||
def set_cursor_position(buffer):
|
||||
abbrevs = getattr(builtins, "abbrevs", None)
|
||||
if abbrevs is None:
|
||||
return False
|
||||
global last_expanded
|
||||
abbr = abbrevs[last_expanded]
|
||||
pos = abbr.rfind("<edit>")
|
||||
def set_cursor_position(buffer, expanded: str) -> None:
|
||||
pos = expanded.rfind(EDIT_SYMBOL)
|
||||
if pos == -1:
|
||||
return False
|
||||
buffer.cursor_position = buffer.cursor_position - (len(abbr) - pos)
|
||||
buffer.delete(6)
|
||||
last_expanded = None
|
||||
return True
|
||||
return
|
||||
buffer.cursor_position = buffer.cursor_position - (len(expanded) - pos)
|
||||
buffer.delete(len(EDIT_SYMBOL))
|
||||
|
||||
|
||||
@events.on_ptk_create
|
||||
|
@ -83,9 +118,13 @@ def custom_keybindings(bindings, **kw):
|
|||
@handler(" ", filter=IsMultiline() & insert_mode)
|
||||
def handle_space(event):
|
||||
buffer = event.app.current_buffer
|
||||
|
||||
add_space = True
|
||||
if not revert_abbrev(buffer):
|
||||
expand_abbrev(buffer)
|
||||
if last_expanded is None or not set_cursor_position(buffer):
|
||||
position_changed = expand_abbrev(buffer)
|
||||
if position_changed:
|
||||
add_space = False
|
||||
if add_space:
|
||||
buffer.insert_text(" ")
|
||||
|
||||
@handler(
|
||||
|
|
Loading…
Add table
Reference in a new issue