diff --git a/README.rst b/README.rst index aaae7f3db..84f182ba4 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,12 @@ And visit https://xon.sh for more information: Extensions ********** -- `Core extensions (xontribs) `_ -- `External extensions on Github `_ -- `List of awesome xontribs `_ + +Xonsh has the certain term for extensions and additional materials - xontrib - the short version of "contribution" word. + +- `Xontribs on Github `_ +- `Awesome xontribs `_ +- `Core xontribs `_ - `Create a xontrib step by step from template `_ Projects that use xonsh or compatible diff --git a/docs/bash_to_xsh.rst b/docs/bash_to_xsh.rst index 36e54a931..919ef266e 100644 --- a/docs/bash_to_xsh.rst +++ b/docs/bash_to_xsh.rst @@ -148,5 +148,5 @@ to set :ref:`$XONSH_TRACE_SUBPROC ` to ``True``: TRACE SUBPROC: (['echo', 'hello\n', 'world'], '|', ['grep', 'hello']) If after time you still try to type ``export``, ``unset`` or ``!!`` commands -there are the `bashisms `_ -and `sh `_ xontribs. +there are the `bashisms `_ +and `sh `_ xontribs. diff --git a/docs/platform-issues.rst b/docs/platform-issues.rst index 16f873796..7d66c6d0d 100644 --- a/docs/platform-issues.rst +++ b/docs/platform-issues.rst @@ -197,6 +197,10 @@ works by hooking the prompt to reset the current working directory to the root drive folder whenever the shell is idle. It only works with the prompt-toolkit back-end. To enable that behaviour run the following: +.. code-block:: xonshcon + + >>> xpip install xontrib-free-cwd + Add this line to your ``~/.xonshrc`` file to have it always enabled. .. code-block:: xonshcon diff --git a/docs/python_virtual_environments.rst b/docs/python_virtual_environments.rst index d1b5eb26c..e205aac65 100644 --- a/docs/python_virtual_environments.rst +++ b/docs/python_virtual_environments.rst @@ -8,7 +8,9 @@ Python Virtual Environments The usual tools for creating Python virtual environments—``venv``, ``virtualenv``, ``pew``—don't play well with xonsh. We won't dig deeper into why it is so, but the general gist is that these tools are hacky and hard-coded for bash, zsh, and other mainstream shells. -Luckily, xonsh ships with its own virtual environments manager called **Vox**. +Luckily, xonsh has its own virtual environments manager called **Vox**. Run to install Vox:: + + $ xpip install xontrib-vox Vox === diff --git a/news/xontribs_transfer.rst b/news/xontribs_transfer.rst new file mode 100644 index 000000000..5acf7b47c --- /dev/null +++ b/news/xontribs_transfer.rst @@ -0,0 +1,29 @@ +**Added:** + +* + +**Changed:** + +* ``abbrevs`` xontrib transferred to `xontrib-abbrevs `_. +* ``bashisms`` xontrib transferred to `xontrib-bashisms `_. +* ``free_cwd`` xontrib transferred to `xontrib-free-cwd `_. +* ``whole_word_jumping`` xontrib transferred to `xontrib-whole-word-jumping `_. +* ``fish_completer`` xontrib transferred to `xontrib-fish-completer `_. +* ``vox``, ``autovox``, ``voxapi`` xontribs transferred to `xontrib-vox `_. +* ``pdb``, ``xog`` xontribs transferred to `xontrib-debug-tools `_. + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Fixed xontrib-jupyter to work in JupyterLab and terminal-based `Euporie `_ environment. + +**Security:** + +* diff --git a/setup.cfg b/setup.cfg index 2af412ca8..61d92f5ba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -96,7 +96,6 @@ per-file-ignores = xonsh/tokenize.py:F821 F841, xonsh/tools.py:E731 E305, xonsh/xonfig.py:E731, - xontrib/vox.py:F821, # remove these later xonsh/color_tools.py:E305 xonsh/completers/_aliases.py:E305, diff --git a/tests/completers/test_xompletions.py b/tests/completers/test_xompletions.py index 364ef5ffd..db7196140 100644 --- a/tests/completers/test_xompletions.py +++ b/tests/completers/test_xompletions.py @@ -38,9 +38,6 @@ def test_xonfig(args, prefix, exp, xsh_with_aliases, monkeypatch, check_complete None, { # the list may vary wrt the env. so testing only part of the coreutils. - "abbrevs", - "pdb", - "bashisms", "coreutils", }, ), diff --git a/tests/test_bashisms.py b/tests/test_bashisms.py deleted file mode 100644 index 2f488f5a8..000000000 --- a/tests/test_bashisms.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Tests bashisms xontrib.""" -import sys - -import pytest - - -@pytest.fixture(name="bash_preproc", scope="module") -def _bash_preproc(): - from xontrib.bashisms import bash_preproc - - yield bash_preproc - del sys.modules["xontrib.bashisms"] - - -@pytest.mark.parametrize( - "history, inp, exp", - [ - # No history: - ([], "!!", ""), - ([], "!$", ""), - ([], "!^", ""), - ([], "!*", ""), - ([], "!echo", ""), - # No substitution: - (["aa 1 2", "ab 3 4"], "ls", "ls"), - (["aa 1 2", "ab 3 4"], "x = 42", "x = 42"), - (["aa 1 2", "ab 3 4"], "!", "!"), - # Bang command only: - (["aa 1 2", "ab 3 4"], "!!", "ab 3 4"), - (["aa 1 2", "ab 3 4"], "!$", "4"), - (["aa 1 2", "ab 3 4"], "!^", "ab"), - (["aa 1 2", "ab 3 4"], "!*", "3 4"), - (["aa 1 2", "ab 3 4"], "!a", "ab 3 4"), - (["aa 1 2", "ab 3 4"], "!aa", "aa 1 2"), - (["aa 1 2", "ab 3 4"], "!ab", "ab 3 4"), - # Bang command with others: - (["aa 1 2", "ab 3 4"], "echo !! >log", "echo ab 3 4 >log"), - (["aa 1 2", "ab 3 4"], "echo !$ >log", "echo 4 >log"), - (["aa 1 2", "ab 3 4"], "echo !^ >log", "echo ab >log"), - (["aa 1 2", "ab 3 4"], "echo !* >log", "echo 3 4 >log"), - (["aa 1 2", "ab 3 4"], "echo !a >log", "echo ab 3 4 >log"), - (["aa 1 2", "ab 3 4"], "echo !aa >log", "echo aa 1 2 >log"), - (["aa 1 2", "ab 3 4"], "echo !ab >log", "echo ab 3 4 >log"), - ], -) -def test_preproc(history, inp, exp, xession, bash_preproc): - """Test the bash preprocessor.""" - - xession.history.inps = history - obs = bash_preproc(inp) - assert exp == obs diff --git a/tests/test_vox.py b/tests/test_vox.py deleted file mode 100644 index cf9942bf1..000000000 --- a/tests/test_vox.py +++ /dev/null @@ -1,470 +0,0 @@ -"""Vox tests""" -import io -import os -import pathlib -import stat -import subprocess as sp -import sys -import types -from typing import TYPE_CHECKING - -import pytest -from py.path import local - -from xonsh.platform import ON_WINDOWS -from xonsh.pytest.tools import skip_if_on_conda, skip_if_on_msys -from xontrib.voxapi import Vox, _get_vox_default_interpreter - -if TYPE_CHECKING: - from pytest_subprocess import FakeProcess - - from xontrib.vox import VoxHandler - - -@pytest.fixture -def venv_home(tmpdir, xession): - """Path where VENVs are created""" - home = tmpdir / "venvs" - home.ensure_dir() - # Set up an isolated venv home - xession.env["VIRTUALENV_HOME"] = str(home) - return home - - -@pytest.fixture -def venv_proc(fake_process: "FakeProcess", venv_home): - def version_handle(process): - ver = str(sys.version).split()[0] - process.stdout.write(f"Python {ver}") - - def venv_handle(process): - env_path = local(process.args[3]) - (env_path / "lib").ensure_dir() - bin_path = env_path / ("Scripts" if ON_WINDOWS else "bin") - - (bin_path / "python").write("", ensure=True) - (bin_path / "python.exe").write("", ensure=True) - for file in bin_path.listdir(): - st = os.stat(str(file)) - os.chmod(str(file), st.st_mode | stat.S_IEXEC) - - for pip_name in ["pip", "pip.exe"]: - fake_process.register( - [str(bin_path / pip_name), "freeze", fake_process.any()], stdout="" - ) - - # will be used by `vox runin` - fake_process.register( - [pip_name, "--version"], - stdout=f"pip 22.0.4 from {env_path}/lib/python3.10/site-packages/pip (python 3.10)", - ) - fake_process.keep_last_process(True) - return env_path - - def get_interpreters(): - interpreter = _get_vox_default_interpreter() - yield interpreter - if sys.executable != interpreter: - yield sys.executable - - for cmd in get_interpreters(): - fake_process.register([cmd, "--version"], callback=version_handle) - venv = (cmd, "-m", "venv") - fake_process.register([*venv, fake_process.any(min=1)], callback=venv_handle) - fake_process.keep_last_process(True) - return fake_process - - -@pytest.fixture -def vox(xession, load_xontrib, venv_proc) -> "VoxHandler": - """vox Alias function""" - - # Set up enough environment for xonsh to function - xession.env["PWD"] = os.getcwd() - xession.env["DIRSTACK_SIZE"] = 10 - xession.env["PATH"] = [] - xession.env["XONSH_SHOW_TRACEBACK"] = True - - load_xontrib("vox") - vox = xession.aliases["vox"] - return vox - - -@pytest.fixture -def record_events(xession): - class Listener: - def __init__(self): - self.last = None - - def listener(self, name): - def _wrapper(**kwargs): - self.last = (name,) + tuple(kwargs.values()) - - return _wrapper - - def __call__(self, *events: str): - for name in events: - event = getattr(xession.builtins.events, name) - event(self.listener(name)) - - yield Listener() - - -def test_vox_flow(xession, vox, record_events, venv_home): - """ - Creates a virtual environment, gets it, enumerates it, and then deletes it. - """ - - record_events( - "vox_on_create", "vox_on_delete", "vox_on_activate", "vox_on_deactivate" - ) - - vox(["create", "spam"]) - assert stat.S_ISDIR(venv_home.join("spam").stat().mode) - assert record_events.last == ("vox_on_create", "spam") - - ve = vox.vox["spam"] - assert ve.env == str(venv_home.join("spam")) - assert os.path.isdir(ve.bin) - - assert "spam" in vox.vox - assert "spam" in list(vox.vox) - - # activate - vox(["activate", "spam"]) - assert xession.env["VIRTUAL_ENV"] == vox.vox["spam"].env - assert record_events.last == ("vox_on_activate", "spam", str(ve.env)) - - out = io.StringIO() - # info - vox(["info"], stdout=out) - assert "spam" in out.getvalue() - out.seek(0) - - # list - vox(["list"], stdout=out) - print(out.getvalue()) - assert "spam" in out.getvalue() - out.seek(0) - - # wipe - vox(["wipe"], stdout=out) - print(out.getvalue()) - assert "Nothing to remove" in out.getvalue() - out.seek(0) - - # deactivate - vox(["deactivate"]) - assert "VIRTUAL_ENV" not in xession.env - assert record_events.last == ("vox_on_deactivate", "spam", str(ve.env)) - - # runin - vox(["runin", "spam", "pip", "--version"], stdout=out) - print(out.getvalue()) - assert "spam" in out.getvalue() - out.seek(0) - - # removal - vox(["rm", "spam", "--force"]) - assert not venv_home.join("spam").check() - assert record_events.last == ("vox_on_delete", "spam") - - -def test_activate_non_vox_venv(xession, vox, record_events, venv_proc, venv_home): - """ - Create a virtual environment using Python's built-in venv module - (not in VIRTUALENV_HOME) and verify that vox can activate it correctly. - """ - xession.env["PATH"] = [] - - record_events("vox_on_activate", "vox_on_deactivate") - - with venv_home.as_cwd(): - venv_dirname = "venv" - sp.run([sys.executable, "-m", "venv", venv_dirname]) - vox(["activate", venv_dirname]) - vxv = vox.vox[venv_dirname] - - env = xession.env - assert os.path.isabs(vxv.bin) - assert env["PATH"][0] == vxv.bin - assert os.path.isabs(vxv.env) - assert env["VIRTUAL_ENV"] == vxv.env - assert record_events.last == ( - "vox_on_activate", - venv_dirname, - str(pathlib.Path(str(venv_home)) / venv_dirname), - ) - - vox(["deactivate"]) - assert not env["PATH"] - assert "VIRTUAL_ENV" not in env - assert record_events.last == ( - "vox_on_deactivate", - venv_dirname, - str(pathlib.Path(str(venv_home)) / venv_dirname), - ) - - -@skip_if_on_msys -@skip_if_on_conda -def test_path(xession, vox, a_venv): - """ - Test to make sure Vox properly activates and deactivates by examining $PATH - """ - oldpath = list(xession.env["PATH"]) - vox(["activate", a_venv.basename]) - - assert oldpath != xession.env["PATH"] - - vox.deactivate() - - assert oldpath == xession.env["PATH"] - - -def test_crud_subdir(xession, venv_home, venv_proc): - """ - Creates a virtual environment, gets it, enumerates it, and then deletes it. - """ - - vox = Vox(force_removals=True) - vox.create("spam/eggs") - assert stat.S_ISDIR(venv_home.join("spam", "eggs").stat().mode) - - ve = vox["spam/eggs"] - assert ve.env == str(venv_home.join("spam", "eggs")) - assert os.path.isdir(ve.bin) - - assert "spam/eggs" in vox - assert "spam" not in vox - - # assert 'spam/eggs' in list(vox) # This is NOT true on Windows - assert "spam" not in list(vox) - - del vox["spam/eggs"] - - assert not venv_home.join("spam", "eggs").check() - - -def test_crud_path(xession, tmpdir, venv_proc): - """ - Creates a virtual environment, gets it, enumerates it, and then deletes it. - """ - tmp = str(tmpdir) - - vox = Vox(force_removals=True) - vox.create(tmp) - assert stat.S_ISDIR(tmpdir.join("lib").stat().mode) - - ve = vox[tmp] - assert ve.env == str(tmp) - assert os.path.isdir(ve.bin) - - del vox[tmp] - - assert not tmpdir.check() - - -@skip_if_on_msys -@skip_if_on_conda -def test_reserved_names(xession, tmpdir): - """ - Tests that reserved words are disallowed. - """ - xession.env["VIRTUALENV_HOME"] = str(tmpdir) - - vox = Vox() - with pytest.raises(ValueError): - if ON_WINDOWS: - vox.create("Scripts") - else: - vox.create("bin") - - with pytest.raises(ValueError): - if ON_WINDOWS: - vox.create("spameggs/Scripts") - else: - vox.create("spameggs/bin") - - -@pytest.mark.parametrize("registered", [False, True]) -def test_autovox(xession, vox, a_venv, load_xontrib, registered): - """ - Tests that autovox works - """ - from xonsh.dirstack import popd, pushd - - # Makes sure that event handlers are registered - load_xontrib("autovox") - - env_name = a_venv.basename - env_path = str(a_venv) - - # init properly - assert vox.parser - - def policy(path, **_): - if str(path) == env_path: - return env_name - - if registered: - xession.builtins.events.autovox_policy(policy) - - pushd([env_path]) - value = env_name if registered else None - assert vox.vox.active() == value - popd([]) - - -@pytest.fixture -def create_venv(venv_proc): - vox = Vox(force_removals=True) - - def wrapped(name): - vox.create(name) - return local(vox[name].env) - - return wrapped - - -@pytest.fixture -def venvs(venv_home, create_venv): - """Create virtualenv with names venv0, venv1""" - from xonsh.dirstack import popd, pushd - - pushd([str(venv_home)]) - yield [create_venv(f"venv{idx}") for idx in range(2)] - popd([]) - - -@pytest.fixture -def a_venv(create_venv): - return create_venv("venv0") - - -@pytest.fixture -def patched_cmd_cache(xession, vox, monkeypatch): - cc = xession.commands_cache - - def no_change(self, *_): - return False, False - - monkeypatch.setattr(cc, "_check_changes", types.MethodType(no_change, cc)) - bins = {path: (path, False) for path in _PY_BINS} - monkeypatch.setattr(cc, "_cmds_cache", bins) - yield cc - - -_VENV_NAMES = {"venv1", "venv1/", "venv0/", "venv0"} -if ON_WINDOWS: - _VENV_NAMES = {"venv1\\", "venv0\\", "venv0", "venv1"} - -_HELP_OPTS = { - "-h", - "--help", -} -_PY_BINS = {"/bin/python2", "/bin/python3"} - -_VOX_NEW_OPTS = { - "--ssp", - "--system-site-packages", - "--without-pip", -}.union(_HELP_OPTS) - -if ON_WINDOWS: - _VOX_NEW_OPTS.add("--symlinks") -else: - _VOX_NEW_OPTS.add("--copies") - -_VOX_RM_OPTS = {"-f", "--force"}.union(_HELP_OPTS) - - -class TestVoxCompletions: - @pytest.fixture - def check(self, check_completer, xession, vox): - def wrapped(cmd, positionals, options=None): - for k in list(xession.completers): - if k != "alias": - xession.completers.pop(k) - assert check_completer(cmd) == positionals - xession.env["ALIAS_COMPLETIONS_OPTIONS_BY_DEFAULT"] = True - if options: - assert check_completer(cmd) == positionals.union(options) - - return wrapped - - @pytest.mark.parametrize( - "args, positionals, opts", - [ - ( - "vox", - { - "delete", - "new", - "remove", - "del", - "workon", - "list", - "exit", - "info", - "ls", - "rm", - "deactivate", - "activate", - "enter", - "create", - "project-get", - "project-set", - "runin", - "runin-all", - "toggle-ssp", - "wipe", - "upgrade", - }, - _HELP_OPTS, - ), - ( - "vox create", - set(), - _VOX_NEW_OPTS.union( - { - "-a", - "--activate", - "--wp", - "--without-pip", - "-p", - "--interpreter", - "-i", - "--install", - "-l", - "--link", - "--link-project", - "-r", - "--requirements", - "-t", - "--temp", - "--prompt", - } - ), - ), - ("vox activate", _VENV_NAMES, _HELP_OPTS.union({"-n", "--no-cd"})), - ("vox rm", _VENV_NAMES, _VOX_RM_OPTS), - ("vox rm venv1", _VENV_NAMES, _VOX_RM_OPTS), # pos nargs: one or more - ("vox rm venv1 venv2", _VENV_NAMES, _VOX_RM_OPTS), # pos nargs: two or more - ], - ) - def test_vox_commands(self, args, positionals, opts, check, venvs): - check(args, positionals, opts) - - @pytest.mark.parametrize( - "args", - [ - "vox new --activate --interpreter", # option after option - "vox new --interpreter", # "option: first - "vox new --activate env1 --interpreter", # option after pos - "vox new env1 --interpreter", # "option: at end" - "vox new env1 --interpreter=", # "option: at end with - ], - ) - def test_interpreter(self, check, args, patched_cmd_cache): - check(args, _PY_BINS) diff --git a/tests/test_xontribs.py b/tests/test_xontribs.py index 8291af82b..3e3197812 100644 --- a/tests/test_xontribs.py +++ b/tests/test_xontribs.py @@ -161,4 +161,4 @@ hello = 'world' def test_xontrib_list(xession, capsys): xontribs_main(["list"]) out, err = capsys.readouterr() - assert "abbrevs" in out + assert "coreutils" in out diff --git a/tests/xontribs/test_abbrevs.py b/tests/xontribs/test_abbrevs.py deleted file mode 100644 index 4e405da2a..000000000 --- a/tests/xontribs/test_abbrevs.py +++ /dev/null @@ -1,55 +0,0 @@ -"""test xontrib.abbrevs""" - -import importlib -import sys - -from prompt_toolkit.buffer import Buffer -from pytest import fixture, mark - -from xonsh.xontribs import find_xontrib - - -@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] - - -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", ps_special_expand, "procs", None), - ("docker ps", ps_special_expand, "docker ps", None), - ("kill", "kill -9", "kill -9", 5), - ("pt", "poetry", "poetry", 3), - ], -) -def test_gets_expanded(abbr, val, expanded, cur, abbrevs_xontrib, _buffer): - from xontrib import abbrevs - - abbrevs.abbrevs[abbr] = val - - abbrev = abbrevs.Abbreviation() - buf = _buffer(abbr) - abbrev.expand(buf) - assert buf.text == expanded - if cur is not None: - assert buf.cursor_position == cur diff --git a/tests/xontribs/test_fish_completer.py b/tests/xontribs/test_fish_completer.py deleted file mode 100644 index 773764009..000000000 --- a/tests/xontribs/test_fish_completer.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest - - -@pytest.fixture -def fish_completer(tmpdir, xession, load_xontrib, fake_process): - """vox Alias function""" - load_xontrib("fish_completer") - xession.env.update( - dict( - XONSH_DATA_DIR=str(tmpdir), - XONSH_SHOW_TRACEBACK=True, - ) - ) - - fake_process.register_subprocess( - command=["fish", fake_process.any()], - # completion for "git chec" - stdout=b"""\ -cherry-pick Apply the change introduced by an existing commit -checkout Checkout and switch to a branch""", - ) - - return fake_process - - -def test_fish_completer(fish_completer, check_completer): - assert check_completer("git", prefix="chec") == {"checkout"} diff --git a/xonsh/webconfig/routes.py b/xonsh/webconfig/routes.py index e75394e56..2c29dc3ab 100644 --- a/xonsh/webconfig/routes.py +++ b/xonsh/webconfig/routes.py @@ -1,5 +1,4 @@ import cgi -import inspect import sys from typing import TYPE_CHECKING @@ -356,51 +355,3 @@ class AliasesPage(Routes): def get(self): yield t.div("table-responsive")[self.get_table()] - - -class AbbrevsPage(Routes): - path = "/abbrevs" - mod_name = XontribsPage.mod_name("abbrevs") - - def __init__(self, **kwargs): - super().__init__(**kwargs) - # lazy import as to not load by accident - from xontrib.abbrevs import abbrevs # type: ignore - - self.abbrevs: "dict[str, str]" = abbrevs - - @classmethod - def nav_title(cls): - if cls.mod_name in sys.modules: - return "Abbrevs" - - def get_header(self): - yield t.tr()[ - t.th("text-right")["Name"], - t.th()["Value"], - ] - - def get_rows(self): - for name in sorted(self.abbrevs.keys()): - alias = self.abbrevs[name] - if callable(alias): - display = inspect.getsource(alias) - else: - display = str(alias) - # todo: - # 2. way to update - - yield t.tr()[ - t.td("text-right")[str(name)], - t.td()[t.p()[repr(display)],], - ] - - def get_table(self): - rows = list(self.get_rows()) - yield t.tbl("table-sm", "table-striped")[ - self.get_header(), - rows, - ] - - def get(self): - yield t.div("table-responsive")[self.get_table()] diff --git a/xontrib/README b/xontrib/README deleted file mode 100644 index 92d9a7551..000000000 --- a/xontrib/README +++ /dev/null @@ -1,5 +0,0 @@ -xontrib is an implicit namespace package. DO NOT add an __init__.py file -to this directory. - -Feel free to add both *.xsh and *.py files to the directory, they will be -installed and available. diff --git a/xontrib/README.rst b/xontrib/README.rst new file mode 100644 index 000000000..0197a6abf --- /dev/null +++ b/xontrib/README.rst @@ -0,0 +1,7 @@ +Xontributions, or xontribs, are a set of tools and conventions for extending +the functionality of xonsh beyond what is provided by default. This allows +3rd party developers and users to improve their xonsh experience without +having to go through the xonsh development and release cycle. + +This xontrib directory represents an implicit namespace package. +DO NOT add an __init__.py file to this directory. diff --git a/xontrib/abbrevs.py b/xontrib/abbrevs.py deleted file mode 100644 index a71e7d00f..000000000 --- a/xontrib/abbrevs.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -Command abbreviations. - -This expands input words from `abbrevs` dictionary 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 or '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 --force" -""" - -import builtins -import typing as tp - -from prompt_toolkit.buffer import Buffer -from prompt_toolkit.filters import IsMultiline - -from xonsh.built_ins import DynamicAccessProxy, XonshSession -from xonsh.tools import check_for_partial_string - -__all__ = () - - -if tp.TYPE_CHECKING: - - class AbbrCallType(tp.Protocol): - def __call__(self, word: str, buffer: Buffer) -> str: - ... - - AbbrValType = tp.Union[str, AbbrCallType] - -abbrevs: "dict[str, AbbrValType]" = dict() - - -class _LastExpanded(tp.NamedTuple): - word: str - expanded: str - - -class Abbreviation: - """A container class to handle state related to abbreviating keywords""" - - last_expanded: tp.Optional[_LastExpanded] = None - - def expand(self, buffer: Buffer) -> bool: - """expand the given abbr text. Return true if cursor position changed.""" - if not abbrevs: - 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 False - text = get_abbreviated(word, buffer) - - buffer.delete_before_cursor(count=len(word)) - buffer.insert_text(text) - - self.last_expanded = _LastExpanded(word, text) - if EDIT_SYMBOL in text: - set_cursor_position(buffer, text) - return True - return False - - def revert(self, buffer) -> bool: - if self.last_expanded is None: - return False - document = buffer.document - expansion = self.last_expanded.expanded + " " - if not document.text_before_cursor.endswith(expansion): - return False - buffer.delete_before_cursor(count=len(expansion)) - buffer.insert_text(self.last_expanded.word) - self.last_expanded = None - return True - - -EDIT_SYMBOL = "" - - -def get_abbreviated(key: str, buffer) -> str: - abbr = abbrevs[key] - if callable(abbr): - text = abbr(buffer=buffer, word=key) - else: - text = abbr - return text - - -def set_cursor_position(buffer, expanded: str) -> None: - pos = expanded.rfind(EDIT_SYMBOL) - if pos == -1: - return - buffer.cursor_position = buffer.cursor_position - (len(expanded) - pos) - buffer.delete(len(EDIT_SYMBOL)) - - -def _load_xontrib_(xsh: XonshSession, **_): - @xsh.builtins.events.on_ptk_create # type:ignore - def custom_keybindings(bindings, **kw): - from prompt_toolkit.filters import EmacsInsertMode, ViInsertMode - - handler = bindings.add - insert_mode = ViInsertMode() | EmacsInsertMode() - abbrev = Abbreviation() - - @handler(" ", filter=IsMultiline() & insert_mode) - def handle_space(event): - buffer = event.app.current_buffer - - add_space = True - if not abbrev.revert(buffer): - position_changed = abbrev.expand(buffer) - if position_changed: - add_space = False - if add_space: - buffer.insert_text(" ") - - # XSH.builtins is a namespace and extendable - xsh.builtins.abbrevs = abbrevs - proxy = DynamicAccessProxy("abbrevs", "__xonsh__.builtins.abbrevs") - builtins.abbrevs = proxy # type: ignore - - return {"abbrevs": abbrevs} diff --git a/xontrib/autovox.py b/xontrib/autovox.py deleted file mode 100644 index 52f73fa45..000000000 --- a/xontrib/autovox.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -Manages automatic activation of virtual environments. - -This coordinates multiple automatic vox policies and deals with some of the -mechanics of venv searching and chdir handling. - -This provides no interface for end users. - -Developers should look at XSH.builtins.events.autovox_policy -""" -import itertools -import warnings -from pathlib import Path - -import xontrib.voxapi as voxapi -from xonsh.built_ins import XSH, XonshSession - -__all__ = () - - -def autovox_policy(path: "Path") -> "str|Path|None": - """ - Register a policy with autovox. - - A policy is a function that takes a Path and returns the venv associated with it, - if any. - - NOTE: The policy should only return a venv for this path exactly, not for - parent paths. Parent walking is handled by autovox so that all policies can - be queried at each level. - """ - - -class MultipleVenvsWarning(RuntimeWarning): - pass - - -def get_venv(vox, dirpath): - # Search up the directory tree until a venv is found, or none - for path in itertools.chain((dirpath,), dirpath.parents): - venvs = [ - vox[p] - for p in XSH.builtins.events.autovox_policy.fire(path=path) - if p is not None and p in vox # Filter out venvs that don't exist - ] - if len(venvs) == 0: - continue - else: - if len(venvs) > 1: - warnings.warn( - MultipleVenvsWarning( - "Found {numvenvs} venvs for {path}; using the first".format( - numvenvs=len(venvs), path=path - ) - ) - ) - return venvs[0] - - -def check_for_new_venv(curdir, olddir): - vox = voxapi.Vox() - if olddir is ... or olddir is None: - try: - oldve = vox[...] - except KeyError: - oldve = None - else: - oldve = get_venv(vox, olddir) - newve = get_venv(vox, curdir) - - if oldve != newve: - if newve is None: - vox.deactivate() - else: - vox.activate(newve.env) - - -# Core mechanism: Check for venv when the current directory changes - - -def cd_handler(newdir, olddir, **_): - check_for_new_venv(Path(newdir), Path(olddir)) - - -# Recalculate when venvs are created or destroyed - - -def create_handler(**_): - check_for_new_venv(Path.cwd(), ...) - - -def destroy_handler(**_): - check_for_new_venv(Path.cwd(), ...) - - -# Initial activation before first prompt - - -def load_handler(**_): - check_for_new_venv(Path.cwd(), None) - - -def _load_xontrib_(xsh: XonshSession, **_): - xsh.builtins.events.register(autovox_policy) - xsh.builtins.events.on_chdir(cd_handler) - xsh.builtins.events.vox_on_create(create_handler) - xsh.builtins.events.vox_on_destroy(destroy_handler) - xsh.builtins.events.on_post_init(load_handler) diff --git a/xontrib/bashisms.py b/xontrib/bashisms.py deleted file mode 100644 index cddd932f8..000000000 --- a/xontrib/bashisms.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Bash-like interface extensions for xonsh. - -Enables additional Bash-like syntax while at the command prompt. -For example, the ``!!`` syntax for running the previous command is now usable. - -Note that these features are implemented as precommand events and -these additions do not affect the xonsh language when run as script. -That said, you might find them useful if you have strong muscle memory. - -**Warning:** This xontrib may modify user command line input to implement its behavior. -To see the modifications as they are applied (in unified diffformat), please set ``$XONSH_DEBUG`` to ``2`` or higher. -The xontrib also adds commands: ``alias``, ``export``, ``unset``, ``set``, ``shopt``, ``complete``. -""" -import re -import shlex -import sys - -from xonsh.built_ins import XSH, XonshSession - -__all__ = () - - -def _warn_not_supported(msg: str): - print( - f"""Not supported ``{msg}`` in xontrib bashisms. -PRs are welcome - https://github.com/xonsh/xonsh/blob/main/xontrib/bashisms.py""", - file=sys.stderr, - ) - - -def bash_preproc(cmd, **kw): - bang_previous = { - "!": lambda x: x, - "$": lambda x: shlex.split(x)[-1], - "^": lambda x: shlex.split(x)[0], - "*": lambda x: " ".join(shlex.split(x)[1:]), - } - - def replace_bang(m): - arg = m.group(1) - inputs = XSH.history.inps - - # Dissect the previous command. - if arg in bang_previous: - try: - return bang_previous[arg](inputs[-1]) - except IndexError: - print(f"xonsh: no history for '!{arg}'") - return "" - - # Look back in history for a matching command. - else: - try: - return next(x for x in reversed(inputs) if x.startswith(arg)) - except StopIteration: - print(f"xonsh: no previous commands match '!{arg}'") - return "" - - return re.sub(r"!([!$^*]|[\w]+)", replace_bang, cmd) - - -def alias(args, stdin=None): - ret = 0 - - if args: - for arg in args: - if "=" in arg: - # shlex.split to remove quotes, e.g. "foo='echo hey'" into - # "foo=echo hey" - name, cmd = shlex.split(arg)[0].split("=", 1) - XSH.aliases[name] = shlex.split(cmd) - elif arg in XSH.aliases: - print(f"{arg}={XSH.aliases[arg]}") - else: - print(f"alias: {arg}: not found", file=sys.stderr) - ret = 1 - else: - for alias, cmd in XSH.aliases.items(): - print(f"{alias}={cmd}") - - return ret - - -def _unset(args): - if not args: - print("Usage: unset ENV_VARIABLE", file=sys.stderr) - - for v in args: - try: - XSH.env.pop(v) - except KeyError: - print(f"{v} not found", file=sys.stderr) - - -def _export(args): - if not args: - print("Usage: export ENV_VARIABLE=VALUE", file=sys.stderr) - - for eq in args: - if "=" in eq: - name, val = shlex.split(eq)[0].split("=", 1) - XSH.env[name] = val - else: - print(f"{eq} equal sign not found", file=sys.stderr) - - -def _set(args): - arg = args[0] - if arg == "-e": - XSH.env["RAISE_SUBPROC_ERROR"] = True - elif arg == "+e": - XSH.env["RAISE_SUBPROC_ERROR"] = False - elif arg == "-x": - XSH.env["XONSH_TRACE_SUBPROC"] = True - elif arg == "+x": - XSH.env["XONSH_TRACE_SUBPROC"] = False - else: - _warn_not_supported(f"set {arg}") - - -def _shopt(args): - supported_shopt = ["DOTGLOB"] - - args_len = len(args) - if args_len == 0: - for so in supported_shopt: - onoff = "on" if so in XSH.env and XSH.env[so] else "off" - print(f"dotglob\t{onoff}") - return - elif args_len < 2 or args[0] in ["-h", "--help"]: - print(f'Usage: shopt <-s|-u> <{"|".join(supported_shopt).lower()}>') - return - - opt = args[0] - optname = args[1] - - if opt == "-s" and optname == "dotglob": - XSH.env["DOTGLOB"] = True - elif opt == "-u" and optname == "dotglob": - XSH.env["DOTGLOB"] = False - else: - _warn_not_supported(f"shopt {args}") - - -def _load_xontrib_(xsh: XonshSession, **_): - xsh.builtins.events.on_transform_command(bash_preproc) - xsh.aliases.register(_unset) - xsh.aliases.register(_export) - xsh.aliases.register(_shopt) - xsh.aliases.register(_set) - xsh.aliases["complete"] = "completer list".split() - xsh.aliases["alias"] = alias - xsh.env["THREAD_SUBPROCS"] = False diff --git a/xontrib/fish_completer.py b/xontrib/fish_completer.py deleted file mode 100644 index 7782b3de5..000000000 --- a/xontrib/fish_completer.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Populate rich completions using fish and remove the default bash based completer""" - -from xonsh.completers import completer -from xonsh.completers.tools import complete_from_sub_proc, contextual_command_completer -from xonsh.parsers.completion_context import CommandContext - - -@contextual_command_completer -def fish_proc_completer(ctx: CommandContext): - if not ctx.args: - return - line = ctx.text_before_cursor - - script_lines = [ - f"complete --no-files {ctx.command}", # switch off basic file completions for the executable - f"complete -C '{line}'", - ] - - return ( - complete_from_sub_proc( - "fish", - "-c", - "; ".join(script_lines), - ), - False, - ) - - -def _load_xontrib_(**_): - completer.add_one_completer("fish", fish_proc_completer, " None`` -* ``vox_on_activate(env: str, path: pathlib.Path) -> None`` -* ``vox_on_deactivate(env: str, path: pathlib.Path) -> None`` -* ``vox_on_delete(env: str) -> None`` -""" -import collections.abc -import logging -import os -import shutil -import subprocess as sp -import sys -import typing - -from xonsh.built_ins import XSH - -# This is because builtins aren't globally created during testing. -# FIXME: Is there a better way? -from xonsh.events import events -from xonsh.platform import ON_POSIX, ON_WINDOWS - -events.doc( - "vox_on_create", - """ -vox_on_create(env: str) -> None - -Fired after an environment is created. -""", -) - -events.doc( - "vox_on_activate", - """ -vox_on_activate(env: str, path: pathlib.Path) -> None - -Fired after an environment is activated. -""", -) - -events.doc( - "vox_on_deactivate", - """ -vox_on_deactivate(env: str, path: pathlib.Path) -> None - -Fired after an environment is deactivated. -""", -) - -events.doc( - "vox_on_delete", - """ -vox_on_delete(env: str) -> None - -Fired after an environment is deleted (through vox). -""", -) - - -class VirtualEnvironment(typing.NamedTuple): - env: str - bin: str - lib: str - inc: str - - -def _subdir_names(): - """ - Gets the names of the special dirs in a venv. - - This is not necessarily exhaustive of all the directories that could be in a venv, and there - may additional logic to get to useful places. - """ - if ON_WINDOWS: - return "Scripts", "Lib", "Include" - elif ON_POSIX: - return "bin", "lib", "include" - else: - raise OSError("This OS is not supported.") - - -def _mkvenv(env_dir): - """ - Constructs a VirtualEnvironment based on the given base path. - - This only cares about the platform. No filesystem calls are made. - """ - env_dir = os.path.abspath(env_dir) - if ON_WINDOWS: - binname = os.path.join(env_dir, "Scripts") - incpath = os.path.join(env_dir, "Include") - libpath = os.path.join(env_dir, "Lib", "site-packages") - elif ON_POSIX: - binname = os.path.join(env_dir, "bin") - incpath = os.path.join(env_dir, "include") - libpath = os.path.join( - env_dir, "lib", "python%d.%d" % sys.version_info[:2], "site-packages" - ) - else: - raise OSError("This OS is not supported.") - - return VirtualEnvironment(env_dir, binname, libpath, incpath) - - -class EnvironmentInUse(Exception): - """The given environment is currently activated, and the operation cannot be performed.""" - - -class NoEnvironmentActive(Exception): - """No environment is currently activated, and the operation cannot be performed.""" - - -class Vox(collections.abc.Mapping): - """API access to Vox and virtual environments, in a dict-like format. - - Makes use of the VirtualEnvironment namedtuple: - - 1. ``env``: The full path to the environment - 2. ``bin``: The full path to the bin/Scripts directory of the environment - """ - - def __init__(self, force_removals=False): - if not XSH.env.get("VIRTUALENV_HOME"): - home_path = os.path.expanduser("~") - self.venvdir = os.path.join(home_path, ".virtualenvs") - XSH.env["VIRTUALENV_HOME"] = self.venvdir - else: - self.venvdir = XSH.env["VIRTUALENV_HOME"] - self.force_removals = force_removals - self.sub_dirs = _subdir_names() - - def create( - self, - name, - interpreter=None, - system_site_packages=False, - symlinks=False, - with_pip=True, - prompt=None, - ): - """Create a virtual environment in $VIRTUALENV_HOME with python3's ``venv``. - - Parameters - ---------- - name : str - Virtual environment name - interpreter: str - Python interpreter used to create the virtual environment. - Can be configured via the $VOX_DEFAULT_INTERPRETER environment variable. - system_site_packages : bool - If True, the system (global) site-packages dir is available to - created environments. - symlinks : bool - If True, attempt to symlink rather than copy files into virtual - environment. - with_pip : bool - If True, ensure pip is installed in the virtual environment. (Default is True) - prompt: str - Provides an alternative prompt prefix for this environment. - """ - if interpreter is None: - interpreter = _get_vox_default_interpreter() - print(f"Using Interpreter: {interpreter}") - - # NOTE: clear=True is the same as delete then create. - # NOTE: upgrade=True is its own method - if isinstance(name, os.PathLike): - env_path = os.fspath(name) - else: - env_path = os.path.join(self.venvdir, name) - if not self._check_reserved(env_path): - raise ValueError( - "venv can't contain reserved names ({})".format( - ", ".join(self.sub_dirs) - ) - ) - - self._create( - env_path, - interpreter, - system_site_packages, - symlinks, - with_pip, - prompt=prompt, - ) - events.vox_on_create.fire(name=name) - - def upgrade(self, name, symlinks=False, with_pip=True, interpreter=None): - """Create a virtual environment in $VIRTUALENV_HOME with python3's ``venv``. - - WARNING: If a virtual environment was created with symlinks or without PIP, you must - specify these options again on upgrade. - - Parameters - ---------- - name : str - Virtual environment name - interpreter: str - The Python interpreter used to create the virtualenv - symlinks : bool - If True, attempt to symlink rather than copy files into virtual - environment. - with_pip : bool - If True, ensure pip is installed in the virtual environment. - """ - - if interpreter is None: - interpreter = _get_vox_default_interpreter() - print(f"Using Interpreter: {interpreter}") - - # venv doesn't reload this, so we have to do it ourselves. - # Is there a bug for this in Python? There should be. - venv = self[name] - cfgfile = os.path.join(venv.env, "pyvenv.cfg") - cfgops = {} - with open(cfgfile) as cfgfile: - for line in cfgfile: - line = line.strip() - if "=" not in line: - continue - k, v = line.split("=", 1) - cfgops[k.strip()] = v.strip() - flags = { - "system_site_packages": cfgops["include-system-site-packages"] == "true", - "symlinks": symlinks, - "with_pip": with_pip, - } - prompt = cfgops.get("prompt") - if prompt: - flags["prompt"] = prompt.lstrip("'\"").rstrip("'\"") - # END things we shouldn't be doing. - - # Ok, do what we came here to do. - self._create(venv.env, interpreter, upgrade=True, **flags) - return venv - - @staticmethod - def _create( - env_path, - interpreter, - system_site_packages=False, - symlinks=False, - with_pip=True, - upgrade=False, - prompt=None, - ): - version_output = sp.check_output( - [interpreter, "--version"], stderr=sp.STDOUT, text=True - ) - - interpreter_major_version = int(version_output.split()[-1].split(".")[0]) - module = "venv" if interpreter_major_version >= 3 else "virtualenv" - system_site_packages = "--system-site-packages" if system_site_packages else "" - symlinks = "--symlinks" if symlinks and interpreter_major_version >= 3 else "" - with_pip = "" if with_pip else "--without-pip" - upgrade = "--upgrade" if upgrade else "" - - cmd = [ - interpreter, - "-m", - module, - env_path, - system_site_packages, - symlinks, - with_pip, - upgrade, - ] - if prompt and module == "venv": - cmd.extend(["--prompt", prompt]) - - cmd = [arg for arg in cmd if arg] # remove empty args - logging.debug(cmd) - - sp.check_call(cmd) - - def _check_reserved(self, name): - return ( - os.path.basename(name) not in self.sub_dirs - ) # FIXME: Check the middle components, too - - def __getitem__(self, name) -> "VirtualEnvironment": - """Get information about a virtual environment. - - Parameters - ---------- - name : str or Ellipsis - Virtual environment name or absolute path. If ... is given, return - the current one (throws a KeyError if there isn't one). - """ - if name is ...: - env = XSH.env - env_paths = [env["VIRTUAL_ENV"]] - elif isinstance(name, os.PathLike): - env_paths = [os.fspath(name)] - else: - if not self._check_reserved(name): - # Don't allow a venv that could be a venv special dir - raise KeyError() - - env_paths = [] - if os.path.isdir(name): - env_paths += [name] - env_paths += [os.path.join(self.venvdir, name)] - - for ep in env_paths: - ve = _mkvenv(ep) - - # Actually check if this is an actual venv or just a organizational directory - # eg, if 'spam/eggs' is a venv, reject 'spam' - if not os.path.exists(ve.bin): - continue - return ve - else: - raise KeyError() - - def __contains__(self, name): - # For some reason, MutableMapping seems to do this against iter, which is just silly. - try: - self[name] - except KeyError: - return False - else: - return True - - def get_binary_path(self, binary: str, *dirs: str): - bin_, _, _ = self.sub_dirs - python_exec = binary - if ON_WINDOWS and not python_exec.endswith(".exe"): - python_exec += ".exe" - return os.path.join(*dirs, bin_, python_exec) - - def __iter__(self): - """List available virtual environments found in $VIRTUALENV_HOME.""" - for dirpath, dirnames, _ in os.walk(self.venvdir): - python_exec = self.get_binary_path("python", dirpath) - if os.access(python_exec, os.X_OK): - yield dirpath[len(self.venvdir) + 1 :] # +1 is to remove the separator - dirnames.clear() - - def __len__(self): - """Counts known virtual environments, using the same rules as iter().""" - line = 0 - for _ in self: - line += 1 - return line - - def active(self): - """Get the name of the active virtual environment. - - You can use this as a key to get further information. - - Returns None if no environment is active. - """ - env = XSH.env - if "VIRTUAL_ENV" not in env: - return - env_path = env["VIRTUAL_ENV"] - if env_path.startswith(self.venvdir): - name = env_path[len(self.venvdir) :] - if name[0] in "/\\": - name = name[1:] - return name - else: - return env_path - - def activate(self, name): - """ - Activate a virtual environment. - - Parameters - ---------- - name : str - Virtual environment name or absolute path. - """ - env = XSH.env - ve = self[name] - if "VIRTUAL_ENV" in env: - self.deactivate() - - type(self).oldvars = {"PATH": list(env["PATH"])} - env["PATH"].insert(0, ve.bin) - env["VIRTUAL_ENV"] = ve.env - if "PYTHONHOME" in env: - type(self).oldvars["PYTHONHOME"] = env.pop("PYTHONHOME") - - events.vox_on_activate.fire(name=name, path=ve.env) - - def deactivate(self): - """ - Deactivate the active virtual environment. Returns its name. - """ - env = XSH.env - if "VIRTUAL_ENV" not in env: - raise NoEnvironmentActive("No environment currently active.") - - env_name = self.active() - - if hasattr(type(self), "oldvars"): - for k, v in type(self).oldvars.items(): - env[k] = v - del type(self).oldvars - - del env["VIRTUAL_ENV"] - - events.vox_on_deactivate.fire(name=env_name, path=self[env_name].env) - return env_name - - def __delitem__(self, name): - """ - Permanently deletes a virtual environment. - - Parameters - ---------- - name : str - Virtual environment name or absolute path. - """ - env_path = self[name].env - try: - if self[...].env == env_path: - raise EnvironmentInUse( - 'The "%s" environment is currently active.' % name - ) - except KeyError: - # No current venv, ... fails - pass - - env_path = os.path.abspath(env_path) - if not self.force_removals: - print(f"The directory {env_path}") - print("and all of its content will be deleted.") - answer = input("Do you want to continue? [Y/n]") - if "n" in answer: - return - - shutil.rmtree(env_path) - - events.vox_on_delete.fire(name=name) - - -def _get_vox_default_interpreter(): - """Return the interpreter set by the $VOX_DEFAULT_INTERPRETER if set else sys.executable""" - default = "python3" - if default in XSH.commands_cache: - default = XSH.commands_cache.locate_binary(default) - else: - default = sys.executable - return XSH.env.get("VOX_DEFAULT_INTERPRETER", default) diff --git a/xontrib/whole_word_jumping.py b/xontrib/whole_word_jumping.py deleted file mode 100644 index 1693fd290..000000000 --- a/xontrib/whole_word_jumping.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Jump/delete across whole (non-whitespace) words with Ctrl+Left/Right/Delete/Backspace. - -Control+left/right: Jump to previous/next whole word -Control+backspace: Delete to beginning of whole word -Control+delete: Delete to end of whole word -Shift+delete: Delete whole word - -Alt+Left/Right/Delete/Backspace remain unmodified: - -Alt+left/right: Jump to previous/next token -Alt+backspace: Delete to beginning of token -Alt+delete: Delete to end of token - -Some terminals cannot differentiate between Backspace and Control+Backspace. -In this case, users can set `$XONSH_WHOLE_WORD_CTRL_BKSP = False` to skip -configuration of the Control+Backspace key binding. -""" - -import prompt_toolkit.input.ansi_escape_sequences as ansiseq -from prompt_toolkit.filters import EmacsInsertMode, ViInsertMode -from prompt_toolkit.key_binding.bindings.named_commands import get_by_name -from prompt_toolkit.keys import Keys - -from xonsh.built_ins import XSH, XonshSession -from xonsh.platform import ON_WINDOWS - - -def custom_keybindings(bindings, **kw): - insert_mode = ViInsertMode() | EmacsInsertMode() - - # Key bindings for jumping over whole words (everything that's not - # white space) using Ctrl+Left and Ctrl+Right; - # Alt+Left and Alt+Right still jump over smaller word segments. - # See https://github.com/xonsh/xonsh/issues/2403 - - @bindings.add(Keys.ControlLeft) - def ctrl_left(event): - buff = event.current_buffer - pos = buff.document.find_previous_word_beginning(count=event.arg, WORD=True) - if pos: - buff.cursor_position += pos - - @bindings.add(Keys.ControlRight) - def ctrl_right(event): - buff = event.current_buffer - pos = buff.document.find_next_word_ending(count=event.arg, WORD=True) - if pos: - buff.cursor_position += pos - - @bindings.add(Keys.ShiftDelete, filter=insert_mode) - def delete_surrounding_big_word(event): - buff = event.current_buffer - startpos, endpos = buff.document.find_boundaries_of_current_word(WORD=True) - startpos = buff.cursor_position + startpos - 1 - startpos = 0 if startpos < 0 else startpos - endpos = buff.cursor_position + endpos - endpos = endpos + 1 if startpos == 0 else endpos - buff.text = buff.text[:startpos] + buff.text[endpos:] - buff.cursor_position = startpos - - @bindings.add(Keys.ControlDelete, filter=insert_mode) - def delete_big_word(event): - buff = event.current_buffer - pos = buff.document.find_next_word_ending(count=event.arg, WORD=True) - if pos: - buff.delete(count=pos) - - @bindings.add(Keys.Escape, Keys.Delete, filter=insert_mode) - def delete_small_word(event): - get_by_name("kill-word").call(event) - - # PTK sets both "\x7f" (^?) and "\x08" (^H) to the same behavior. Refs: - # https://github.com/prompt-toolkit/python-prompt-toolkit/blob/65c3d0607c69c19d80abb052a18569a2546280e5/src/prompt_toolkit/input/ansi_escape_sequences.py#L65 - # https://github.com/prompt-toolkit/python-prompt-toolkit/issues/257#issuecomment-190328366 - # We patch the ANSI sequences used by PTK. This requires a terminal - # that sends different codes for and . - # PTK sets Keys.Backspace = Keys.ControlH, so we hardcode the code. - # Windows has the codes reversed, see https://github.com/xonsh/xonsh/commit/406d20f78f18af39d9bbaf9580b0a763df78a0db - if XSH.env.get("XONSH_WHOLE_WORD_CTRL_BKSP", True): - CONTROL_BKSP = "\x08" - if ON_WINDOWS: - # issue #4845: Windows only (isort competes with black :-() - from prompt_toolkit.input import win32 as ptk_win32 # black:skip - - # On windows BKSP is "\x08" and CTRL-BKSP is "\x7f" - CONTROL_BKSP = "\x7f" - ptk_win32.ConsoleInputReader.mappings[b"\x7f"] = CONTROL_BKSP - ansiseq.ANSI_SEQUENCES[CONTROL_BKSP] = CONTROL_BKSP - ansiseq.REVERSE_ANSI_SEQUENCES[CONTROL_BKSP] = CONTROL_BKSP - - @bindings.add(CONTROL_BKSP, filter=insert_mode) - def backward_delete_big_word(event): - get_by_name("unix-word-rubout").call(event) - - # backward_delete_small_word works on Alt+Backspace by default - - -def _load_xontrib_(xsh: XonshSession, **_): - xsh.builtins.events.on_ptk_create(custom_keybindings) diff --git a/xontrib/xog.py b/xontrib/xog.py deleted file mode 100644 index 005c5bf25..000000000 --- a/xontrib/xog.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -This adds `xog` - a simple command to establish and print temporary traceback log file. -""" - -import os -import pathlib -import tempfile - -from xonsh.built_ins import XSH, XonshSession - - -def _get_log_file_name(): - return pathlib.Path(f"{tempfile.gettempdir()}/xonsh-{os.getpid()}.log") - - -def _clear_log(logfile, stderr): - try: - os.remove(logfile) - except OSError as e: - print(f"xog: {e}", file=stderr) - return False - return True - - -def _print_log(logfile, stdout, stderr): - try: - with open(logfile) as log: - for line in log: - print(line, end="", file=stdout) - except OSError as e: - print(f"xog: {e}", file=stderr) - return False - return True - - -def _print_help(stdout): - print( - """Usage: xog [OPTIONS] -Prints contents of the shell's traceback log file. - -Options: - -c, --clear\t\tclear the log file contents - -?, --help\t\tprint this help text -""" - ) - - -def _xog(args, stdout=None, stderr=None): - if "-?" in args or "--help" in args: - _print_help(stdout) - return 0 - - logfile = XSH.env.get("XONSH_TRACEBACK_LOGFILE", "") - if not (logfile and os.path.isfile(logfile)): - print("Traceback log file doesn't exist.", file=stderr) - return -1 - - if "-c" in args or "--clear" in args: - rc = _clear_log(logfile, stderr=stderr) - return 0 if rc else -1 - - rc = _print_log(logfile, stdout, stderr) - return 0 if rc else -1 - - -def _load_xontrib_(xsh: XonshSession, **_): - xsh.env["XONSH_TRACEBACK_LOGFILE"] = _get_log_file_name() - xsh.aliases["xog"] = _xog