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:
Noorhteen Raja NJ 2021-01-29 22:48:22 +05:30 committed by GitHub
parent 71fe9014d2
commit d50cf32f84
Failed to generate hash of commit
4 changed files with 153 additions and 43 deletions

View 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>

View 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

View file

@ -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(

View file

@ -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(