mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-04 08:24:40 +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.
|
This modules is the place where one would define the xontribs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
import functools
|
import functools
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
import typing as tp
|
import typing as tp
|
||||||
|
|
||||||
|
from xonsh.lazyasd import LazyObject, lazyobject
|
||||||
|
|
||||||
|
|
||||||
class _XontribPkg(tp.NamedTuple):
|
class _XontribPkg(tp.NamedTuple):
|
||||||
"""Class to define package information of a xontrib.
|
"""Class to define package information of a xontrib.
|
||||||
|
@ -43,11 +48,20 @@ class Xontrib(tp.NamedTuple):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url: str = ""
|
url: str = ""
|
||||||
description: str = ""
|
description: tp.Union[str, LazyObject] = ""
|
||||||
package: tp.Optional[_XontribPkg] = None
|
package: tp.Optional[_XontribPkg] = None
|
||||||
tags: tp.Tuple[str, ...] = ()
|
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()
|
@functools.lru_cache()
|
||||||
def get_xontribs() -> tp.Dict[str, Xontrib]:
|
def get_xontribs() -> tp.Dict[str, Xontrib]:
|
||||||
"""Return xontrib definitions lazily."""
|
"""Return xontrib definitions lazily."""
|
||||||
|
@ -70,20 +84,7 @@ def define_xontribs():
|
||||||
return {
|
return {
|
||||||
"abbrevs": Xontrib(
|
"abbrevs": Xontrib(
|
||||||
url="http://xon.sh",
|
url="http://xon.sh",
|
||||||
description="Adds ``abbrevs`` dictionary to hold user-defined "
|
description=lazyobject(lambda: get_module_docstring("xontrib.abbrevs")),
|
||||||
"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",
|
|
||||||
package=core_pkg,
|
package=core_pkg,
|
||||||
),
|
),
|
||||||
"apt_tabcomplete": Xontrib(
|
"apt_tabcomplete": Xontrib(
|
||||||
|
|
|
@ -2,12 +2,36 @@
|
||||||
Command abbreviations.
|
Command abbreviations.
|
||||||
|
|
||||||
This expands input words from `abbrevs` disctionary as you type.
|
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 builtins
|
||||||
|
import typing as tp
|
||||||
|
|
||||||
from prompt_toolkit.filters import completion_is_selected, IsMultiline
|
from prompt_toolkit.filters import completion_is_selected, IsMultiline
|
||||||
from prompt_toolkit.keys import Keys
|
from prompt_toolkit.keys import Keys
|
||||||
from xonsh.built_ins import DynamicAccessProxy
|
from xonsh.built_ins import DynamicAccessProxy
|
||||||
|
from xonsh.events import events
|
||||||
from xonsh.tools import check_for_partial_string
|
from xonsh.tools import check_for_partial_string
|
||||||
|
|
||||||
__all__ = ()
|
__all__ = ()
|
||||||
|
@ -16,59 +40,70 @@ builtins.__xonsh__.abbrevs = dict()
|
||||||
proxy = DynamicAccessProxy("abbrevs", "__xonsh__.abbrevs")
|
proxy = DynamicAccessProxy("abbrevs", "__xonsh__.abbrevs")
|
||||||
setattr(builtins, "abbrevs", proxy)
|
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
|
global last_expanded
|
||||||
last_expanded = None
|
last_expanded = None
|
||||||
abbrevs = getattr(builtins, "abbrevs", None)
|
abbrevs = getattr(builtins, "abbrevs", None)
|
||||||
if abbrevs is None:
|
if abbrevs is None:
|
||||||
return
|
return False
|
||||||
document = buffer.document
|
document = buffer.document
|
||||||
word = document.get_word_before_cursor(WORD=True)
|
word = document.get_word_before_cursor(WORD=True)
|
||||||
if word in abbrevs.keys():
|
if word in abbrevs.keys():
|
||||||
partial = document.text[: document.cursor_position]
|
partial = document.text[: document.cursor_position]
|
||||||
startix, endix, quote = check_for_partial_string(partial)
|
startix, endix, quote = check_for_partial_string(partial)
|
||||||
if startix is not None and endix is None:
|
if startix is not None and endix is None:
|
||||||
return
|
return False
|
||||||
buffer.delete_before_cursor(count=len(word))
|
buffer.delete_before_cursor(count=len(word))
|
||||||
buffer.insert_text(abbrevs[word])
|
text = get_abbreviated(word, buffer)
|
||||||
last_expanded = word
|
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
|
global last_expanded
|
||||||
if last_expanded is None:
|
if last_expanded is None:
|
||||||
return False
|
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
|
document = buffer.document
|
||||||
expansion = abbrevs[last_expanded] + " "
|
expansion = last_expanded.expanded + " "
|
||||||
if not document.text_before_cursor.endswith(expansion):
|
if not document.text_before_cursor.endswith(expansion):
|
||||||
return False
|
return False
|
||||||
buffer.delete_before_cursor(count=len(expansion))
|
buffer.delete_before_cursor(count=len(expansion))
|
||||||
buffer.insert_text(last_expanded)
|
buffer.insert_text(last_expanded.word)
|
||||||
last_expanded = None
|
last_expanded = None
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def set_cursor_position(buffer):
|
def set_cursor_position(buffer, expanded: str) -> None:
|
||||||
abbrevs = getattr(builtins, "abbrevs", None)
|
pos = expanded.rfind(EDIT_SYMBOL)
|
||||||
if abbrevs is None:
|
|
||||||
return False
|
|
||||||
global last_expanded
|
|
||||||
abbr = abbrevs[last_expanded]
|
|
||||||
pos = abbr.rfind("<edit>")
|
|
||||||
if pos == -1:
|
if pos == -1:
|
||||||
return False
|
return
|
||||||
buffer.cursor_position = buffer.cursor_position - (len(abbr) - pos)
|
buffer.cursor_position = buffer.cursor_position - (len(expanded) - pos)
|
||||||
buffer.delete(6)
|
buffer.delete(len(EDIT_SYMBOL))
|
||||||
last_expanded = None
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@events.on_ptk_create
|
@events.on_ptk_create
|
||||||
|
@ -83,9 +118,13 @@ def custom_keybindings(bindings, **kw):
|
||||||
@handler(" ", filter=IsMultiline() & insert_mode)
|
@handler(" ", filter=IsMultiline() & insert_mode)
|
||||||
def handle_space(event):
|
def handle_space(event):
|
||||||
buffer = event.app.current_buffer
|
buffer = event.app.current_buffer
|
||||||
|
|
||||||
|
add_space = True
|
||||||
if not revert_abbrev(buffer):
|
if not revert_abbrev(buffer):
|
||||||
expand_abbrev(buffer)
|
position_changed = expand_abbrev(buffer)
|
||||||
if last_expanded is None or not set_cursor_position(buffer):
|
if position_changed:
|
||||||
|
add_space = False
|
||||||
|
if add_space:
|
||||||
buffer.insert_text(" ")
|
buffer.insert_text(" ")
|
||||||
|
|
||||||
@handler(
|
@handler(
|
||||||
|
|
Loading…
Add table
Reference in a new issue