Merge branch 'master' into environment_fixes

This commit is contained in:
Bob Hyman 2020-09-23 04:40:26 -04:00
commit 88bba37c3a
29 changed files with 580 additions and 74 deletions

View file

@ -255,6 +255,21 @@ may be useful to share entries between shell sessions. In such a case, one can u
the ``flush`` action to immediately save the session history to disk and make it the ``flush`` action to immediately save the session history to disk and make it
accessible from other shell sessions. accessible from other shell sessions.
``clear`` action
================
Deletes the history from the current session up until this point. Later commands
will still be saved.
``off`` action
================
Deletes the history from the current session and turns off history saving for the
rest of the session. Only session metadata will be saved, not commands or output.
``on`` action
================
Turns history saving back on. Previous commands won't be saved, but future
commands will be.
``gc`` action ``gc`` action
=============== ===============
Last, but certainly not least, the ``gc`` action is a manual hook into executing Last, but certainly not least, the ``gc`` action is a manual hook into executing

23
news/ansi_osc.rst Normal file
View file

@ -0,0 +1,23 @@
**Added:**
* Support for ANSI OSC escape sequences in ``$PROMPT``, setting ``$TITLE`` for example. (#374, #1403)
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -0,0 +1,23 @@
**Added:**
* Use command_cache when finding available commands, to speedup command-not-found suggestions
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -0,0 +1,23 @@
**Added:**
* history clear, history off and history on actions, for managing whether history in the current session is saved.
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

23
news/name_in_error.rst Normal file
View file

@ -0,0 +1,23 @@
**Added:**
* ValueErrors from environ.register now report the name of the bad env var
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

23
news/prompt-speed.rst Normal file
View file

@ -0,0 +1,23 @@
**Added:**
* <news item>
**Changed:**
* Minor improvements to the get prompt speed. (Mostly in git.)
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -0,0 +1,25 @@
**Added:**
* <news item>
**Changed:**
* ptk key binding for TAB -- hitting TAB to start completion now automatically selects the first displayed completion (if any).
hitting TAB when in insert mode inserts TAB, as heretofore. This more exactly follows behavior of readline ``menu-complete``.
There is no configuration option for tailoring this behavior.
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

23
news/shift_del.rst Normal file
View file

@ -0,0 +1,23 @@
**Added:**
* Added to xontrib whole_word_jumping: Shift+Delete hotkey to delete whole word.
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -0,0 +1,23 @@
**Added:**
* <news item>
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* Fix crash when xonsh tries to run windows app execution aliases.
**Security:**
* <news item>

23
news/xontrib_descr.rst Normal file
View file

@ -0,0 +1,23 @@
**Added:**
* <news item>
**Changed:**
* xontrib-argcomplete and xontrib-pipeliner description improvement.
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -4,8 +4,12 @@ from rever.activities.ghrelease import git_archive_asset
$PROJECT = $GITHUB_ORG = $GITHUB_REPO = 'xonsh' $PROJECT = $GITHUB_ORG = $GITHUB_REPO = 'xonsh'
$WEBSITE_URL = 'http://xon.sh' $WEBSITE_URL = 'http://xon.sh'
$ACTIVITIES = ['authors', 'version_bump', 'changelog', 'pytest', 'appimage', $ACTIVITIES = ['authors', 'version_bump', 'changelog', 'pytest', 'appimage',
'tag', 'push_tag', 'ghrelease', 'sphinx', 'tag', 'push_tag',
'ghpages', 'pypi', 'conda_forge', 'ghrelease',
'sphinx',
'ghpages',
'pypi',
'conda_forge',
] ]
$PYPI_SIGN = False $PYPI_SIGN = False

View file

@ -483,3 +483,65 @@ def test__xhj_gc_xx_to_rmfiles(
assert minute_diff <= 60 assert minute_diff <= 60
else: else:
assert act_size == exp_size assert act_size == exp_size
def test_hist_clear_cmd(hist, xonsh_builtins, capsys, tmpdir):
"""Verify that the CLI history clear command works."""
xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)})
xonsh_builtins.__xonsh__.history = hist
xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set()
for ts, cmd in enumerate(CMDS): # populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 6
history_main(["clear"])
out, err = capsys.readouterr()
assert err.rstrip() == "History cleared"
assert len(xonsh_builtins.__xonsh__.history) == 0
def test_hist_off_cmd(hist, xonsh_builtins, capsys, tmpdir):
"""Verify that the CLI history off command works."""
xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)})
xonsh_builtins.__xonsh__.history = hist
xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set()
for ts, cmd in enumerate(CMDS): # populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 6
history_main(["off"])
out, err = capsys.readouterr()
assert err.rstrip() == "History off"
assert len(xonsh_builtins.__xonsh__.history) == 0
for ts, cmd in enumerate(CMDS): # attempt to populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 0
def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir):
"""Verify that the CLI history on command works."""
xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)})
xonsh_builtins.__xonsh__.history = hist
xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set()
for ts, cmd in enumerate(CMDS): # populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 6
history_main(["off"])
history_main(["on"])
out, err = capsys.readouterr()
assert err.rstrip().endswith("History on")
assert len(xonsh_builtins.__xonsh__.history) == 0
for ts, cmd in enumerate(CMDS): # populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 6

View file

@ -215,3 +215,65 @@ def test_history_getitem(index, exp, hist, xonsh_builtins):
assert [(e.cmd, e.out, e.rtn, e.ts) for e in entry] == exp assert [(e.cmd, e.out, e.rtn, e.ts) for e in entry] == exp
else: else:
assert (entry.cmd, entry.out, entry.rtn, entry.ts) == exp assert (entry.cmd, entry.out, entry.rtn, entry.ts) == exp
def test_hist_clear_cmd(hist, xonsh_builtins, capsys, tmpdir):
"""Verify that the CLI history clear command works."""
xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)})
xonsh_builtins.__xonsh__.history = hist
xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set()
for ts, cmd in enumerate(CMDS): # populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 6
history_main(["clear"])
out, err = capsys.readouterr()
assert err.rstrip() == "History cleared"
assert len(xonsh_builtins.__xonsh__.history) == 0
def test_hist_off_cmd(hist, xonsh_builtins, capsys, tmpdir):
"""Verify that the CLI history off command works."""
xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)})
xonsh_builtins.__xonsh__.history = hist
xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set()
for ts, cmd in enumerate(CMDS): # populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 6
history_main(["off"])
out, err = capsys.readouterr()
assert err.rstrip() == "History off"
assert len(xonsh_builtins.__xonsh__.history) == 0
for ts, cmd in enumerate(CMDS): # attempt to populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 0
def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir):
"""Verify that the CLI history on command works."""
xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)})
xonsh_builtins.__xonsh__.history = hist
xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set()
for ts, cmd in enumerate(CMDS): # populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 6
history_main(["off"])
history_main(["on"])
out, err = capsys.readouterr()
assert err.rstrip().endswith("History on")
assert len(xonsh_builtins.__xonsh__.history) == 0
for ts, cmd in enumerate(CMDS): # populate the shell history
hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)})
assert len(xonsh_builtins.__xonsh__.history) == 6

View file

@ -8,7 +8,7 @@ from xonsh.platform import minimum_required_ptk_version
# verify error if ptk not installed or below min # verify error if ptk not installed or below min
from xonsh.ptk_shell.shell import tokenize_ansi from xonsh.ptk_shell.shell import tokenize_ansi, remove_ansi_osc
from xonsh.shell import Shell from xonsh.shell import Shell
@ -105,4 +105,31 @@ def test_tokenize_ansi(prompt_tokens, ansi_string_parts):
assert token[1] == text assert token[1] == text
@pytest.mark.parametrize(
"raw_prompt, prompt, osc_tokens",
[
# no title
("test prompt", "test prompt", []),
# starts w/ title
("\033]0;TITLE THIS\007test prompt", "test prompt", ["\033]0;TITLE THIS\007"]),
# ends w/ title
("test prompt\033]0;TITLE THIS\007", "test prompt", ["\033]0;TITLE THIS\007"]),
# title in the middle
("test \033]0;TITLE THIS\007prompt", "test prompt", ["\033]0;TITLE THIS\007"]),
# title + iTerm2 OSC exapmle
(
"test \033]0;TITLE THIS\007prompt \033]133;A\007here",
"test prompt here",
["\033]0;TITLE THIS\007", "\033]133;A\007"],
),
],
)
def test_remove_ansi_osc(raw_prompt, prompt, osc_tokens):
checked_prompt, removed_osc = remove_ansi_osc(raw_prompt)
assert prompt == checked_prompt
assert len(removed_osc) == len(osc_tokens)
for removed, ref in zip(removed_osc, osc_tokens):
assert removed == ref
# someday: initialize PromptToolkitShell and have it actually do something. # someday: initialize PromptToolkitShell and have it actually do something.

View file

@ -179,7 +179,16 @@ def pathsearch(func, s, pymode=False, pathobj=False):
RE_SHEBANG = LazyObject(lambda: re.compile(r"#![ \t]*(.+?)$"), globals(), "RE_SHEBANG") RE_SHEBANG = LazyObject(lambda: re.compile(r"#![ \t]*(.+?)$"), globals(), "RE_SHEBANG")
def is_app_execution_alias(fname):
""" App execution aliases behave strangly on windows and python.
Here try to detect if a file is an app execution alias
"""
fname = pathlib.Path(fname)
return not os.path.exists(fname) and fname.name in os.listdir(fname.parent)
def _is_binary(fname, limit=80): def _is_binary(fname, limit=80):
try:
with open(fname, "rb") as f: with open(fname, "rb") as f:
for i in range(limit): for i in range(limit):
char = f.read(1) char = f.read(1)
@ -188,7 +197,12 @@ def _is_binary(fname, limit=80):
if char == b"\n": if char == b"\n":
return False return False
if char == b"": if char == b"":
return False return
except OSError as e:
if ON_WINDOWS and is_app_execution_alias(fname):
return True
raise e
return False return False

View file

@ -61,10 +61,7 @@ class RichCompletion(str):
def __repr__(self): def __repr__(self):
return "RichCompletion({}, prefix_len={}, display={}, description={})".format( return "RichCompletion({}, prefix_len={}, display={}, description={})".format(
repr(str(self)), repr(str(self)), self.prefix_len, repr(self.display), repr(self.description)
self.prefix_len,
repr(self.display),
repr(self.description),
) )

View file

@ -2099,7 +2099,7 @@ class Env(cabc.MutableMapping):
pass pass
else: else:
raise ValueError( raise ValueError(
"Default value does not match type specified by validate" f"Default value for {name} does not match type specified by validate"
) )
self._vars[name] = Var( self._vars[name] = Var(

View file

@ -71,6 +71,7 @@ class History:
self.last_cmd_out = None self.last_cmd_out = None
self.hist_size = None self.hist_size = None
self.hist_units = None self.hist_units = None
self.remember_history = True
def __len__(self): def __len__(self):
"""Return the number of items in current session.""" """Return the number of items in current session."""
@ -153,3 +154,9 @@ class History:
If set blocking, then wait until gc action finished. If set blocking, then wait until gc action finished.
""" """
pass pass
def clear(self):
"""Clears the history of the current session from both the disk and
memory.
"""
pass

View file

@ -202,7 +202,7 @@ class JsonHistoryGC(threading.Thread):
# info: file size, closing timestamp, number of commands, filename # info: file size, closing timestamp, number of commands, filename
ts = lj.get("ts", (0.0, None)) ts = lj.get("ts", (0.0, None))
files.append( files.append(
(ts[1] or ts[0], len(lj.sizes["cmds"]) - 1, f, cur_file_size,), (ts[1] or ts[0], len(lj.sizes["cmds"]) - 1, f, cur_file_size)
) )
lj.close() lj.close()
if xonsh_debug: if xonsh_debug:
@ -303,6 +303,9 @@ class JsonCommandField(cabc.Sequence):
return len(self.hist) return len(self.hist)
def __getitem__(self, key): def __getitem__(self, key):
if not self.hist.remember_history:
return ""
size = len(self) size = len(self)
if isinstance(key, slice): if isinstance(key, slice):
return [self[i] for i in range(*key.indices(size))] return [self[i] for i in range(*key.indices(size))]
@ -407,6 +410,8 @@ class JsonHistory(History):
hf : JsonHistoryFlusher or None hf : JsonHistoryFlusher or None
The thread that was spawned to flush history The thread that was spawned to flush history
""" """
if not self.remember_history:
return
self.buffer.append(cmd) self.buffer.append(cmd)
self._len += 1 # must come before flushing self._len += 1 # must come before flushing
if len(self.buffer) >= self.buffersize: if len(self.buffer) >= self.buffersize:
@ -429,6 +434,7 @@ class JsonHistory(History):
hf : JsonHistoryFlusher or None hf : JsonHistoryFlusher or None
The thread that was spawned to flush history The thread that was spawned to flush history
""" """
# Implicitly covers case of self.remember_history being False.
if len(self.buffer) == 0: if len(self.buffer) == 0:
return return
@ -502,3 +508,18 @@ class JsonHistory(History):
if blocking: if blocking:
while self.gc.is_alive(): # while waiting for gc. while self.gc.is_alive(): # while waiting for gc.
time.sleep(0.1) # don't monopolize the thread (or Python GIL?) time.sleep(0.1) # don't monopolize the thread (or Python GIL?)
def clear(self):
"""Clears the current session's history from both memory and disk."""
# Wipe history from memory. Keep sessionid and other metadata.
self.buffer = []
self.tss = JsonCommandField("ts", self)
self.inps = JsonCommandField("inp", self)
self.outs = JsonCommandField("out", self)
self.rtns = JsonCommandField("rtn", self)
self._len = 0
self._skipped = 0
# Flush empty history object to disk, overwriting previous data.
self.flush()

View file

@ -224,7 +224,18 @@ def _XH_HISTORY_SESSIONS():
} }
_XH_MAIN_ACTIONS = {"show", "id", "file", "info", "diff", "gc", "flush"} _XH_MAIN_ACTIONS = {
"show",
"id",
"file",
"info",
"diff",
"gc",
"flush",
"off",
"on",
"clear",
}
@functools.lru_cache() @functools.lru_cache()
@ -354,6 +365,17 @@ def _xh_create_parser():
# 'flush' subcommand # 'flush' subcommand
subp.add_parser("flush", help="flush the current history to disk") subp.add_parser("flush", help="flush the current history to disk")
# 'off' subcommand
subp.add_parser("off", help="history will not be saved for this session")
# 'on' subcommand
subp.add_parser(
"on", help="history will be saved for the rest of the session (default)"
)
# 'clear' subcommand
subp.add_parser("clear", help="one-time wipe of session history")
return p return p
@ -417,5 +439,17 @@ def history_main(
hf = hist.flush() hf = hist.flush()
if isinstance(hf, threading.Thread): if isinstance(hf, threading.Thread):
hf.join() hf.join()
elif ns.action == "off":
if hist.remember_history:
hist.clear()
hist.remember_history = False
print("History off", file=sys.stderr)
elif ns.action == "on":
if not hist.remember_history:
hist.remember_history = True
print("History on", file=sys.stderr)
elif ns.action == "clear":
hist.clear()
print("History cleared", file=sys.stderr)
else: else:
print("Unknown history action {}".format(ns.action), file=sys.stderr) print("Unknown history action {}".format(ns.action), file=sys.stderr)

View file

@ -240,6 +240,8 @@ class SqliteHistory(History):
setattr(XH_SQLITE_CACHE, XH_SQLITE_CREATED_SQL_TBL, False) setattr(XH_SQLITE_CACHE, XH_SQLITE_CREATED_SQL_TBL, False)
def append(self, cmd): def append(self, cmd):
if not self.remember_history:
return
envs = builtins.__xonsh__.env envs = builtins.__xonsh__.env
inp = cmd["inp"].rstrip() inp = cmd["inp"].rstrip()
self.inps.append(inp) self.inps.append(inp)
@ -296,3 +298,18 @@ class SqliteHistory(History):
if blocking: if blocking:
while self.gc.is_alive(): while self.gc.is_alive():
continue continue
def clear(self):
"""Clears the current session's history from both memory and disk."""
# Wipe memory
self.inps = []
self.rtns = []
self.outs = []
self.tss = []
# Wipe the current session's entries from the database.
sql = "DELETE FROM xonsh_history WHERE sessionid = ?"
with _xh_sqlite_get_conn(filename=self.filename) as conn:
c = conn.cursor()
_xh_sqlite_create_history_table(c)
c.execute(sql, (str(self.sessionid),))

View file

@ -181,7 +181,7 @@ class FStringAdaptor:
) )
) )
field_node = ast.Tuple( field_node = ast.Tuple(
elts=elts, ctx=ast.Load(), lineno=lineno, col_offset=col_offset, elts=elts, ctx=ast.Load(), lineno=lineno, col_offset=col_offset
) )
node.args[0] = field_node node.args[0] = field_node

View file

@ -24,26 +24,15 @@ RE_REMOVE_ANSI = LazyObject(
def _get_git_branch(q): def _get_git_branch(q):
denv = builtins.__xonsh__.env.detype() denv = builtins.__xonsh__.env.detype()
try: try:
branches = xt.decode_bytes( cmd = ["git", "rev-parse", "--abbrev-ref", "HEAD"]
subprocess.check_output( branch = xt.decode_bytes(
["git", "branch"], env=denv, stderr=subprocess.DEVNULL subprocess.check_output(cmd, env=denv, stderr=subprocess.DEVNULL)
) )
).splitlines() branch = branch.splitlines()[0] or None
except (subprocess.CalledProcessError, OSError, FileNotFoundError): except (subprocess.CalledProcessError, OSError, FileNotFoundError):
q.put(None) q.put(None)
else: else:
for branch in branches:
if not branch.startswith("* "):
continue
elif branch.endswith(")"):
branch = branch.split()[-1][:-1]
else:
branch = branch.split()[-1]
q.put(branch) q.put(branch)
break
else:
q.put(None)
def get_git_branch(): def get_git_branch():
@ -59,7 +48,6 @@ def get_git_branch():
t.join(timeout=timeout) t.join(timeout=timeout)
try: try:
branch = q.get_nowait() branch = q.get_nowait()
# branch = RE_REMOVE_ANSI.sub("", branch or "")
if branch: if branch:
branch = RE_REMOVE_ANSI.sub("", branch) branch = RE_REMOVE_ANSI.sub("", branch)
except queue.Empty: except queue.Empty:
@ -149,6 +137,15 @@ def _first_branch_timeout_message():
) )
def _vc_has(binary):
""" This allows us to locate binaries after git only if necessary """
cmds = builtins.__xonsh__.commands_cache
if cmds.is_empty():
return bool(cmds.locate_binary(binary, ignore_alias=True))
else:
return bool(cmds.lazy_locate_binary(binary, ignore_alias=True))
def current_branch(): def current_branch():
"""Gets the branch for a current working directory. Returns an empty string """Gets the branch for a current working directory. Returns an empty string
if the cwd is not a repository. This currently only works for git and hg if the cwd is not a repository. This currently only works for git and hg
@ -156,17 +153,9 @@ def current_branch():
'<branch-timeout>' is returned. '<branch-timeout>' is returned.
""" """
branch = None branch = None
cmds = builtins.__xonsh__.commands_cache if _vc_has("git"):
# check for binary only once
if cmds.is_empty():
has_git = bool(cmds.locate_binary("git", ignore_alias=True))
has_hg = bool(cmds.locate_binary("hg", ignore_alias=True))
else:
has_git = bool(cmds.lazy_locate_binary("git", ignore_alias=True))
has_hg = bool(cmds.lazy_locate_binary("hg", ignore_alias=True))
if has_git:
branch = get_git_branch() branch = get_git_branch()
if not branch and has_hg: if not branch and _vc_has("hg"):
branch = get_hg_branch() branch = get_hg_branch()
if isinstance(branch, subprocess.TimeoutExpired): if isinstance(branch, subprocess.TimeoutExpired):
branch = "<branch-timeout>" branch = "<branch-timeout>"
@ -175,19 +164,31 @@ def current_branch():
def _git_dirty_working_directory(q, include_untracked): def _git_dirty_working_directory(q, include_untracked):
status = None
denv = builtins.__xonsh__.env.detype() denv = builtins.__xonsh__.env.detype()
try: try:
cmd = ["git", "status", "--porcelain"] # Borrowed from this conversation
# https://github.com/sindresorhus/pure/issues/115
if include_untracked: if include_untracked:
cmd.append("--untracked-files=normal") cmd = [
"git",
"status",
"--porcelain",
"--quiet",
"--untracked-files=normal",
]
else: else:
cmd.append("--untracked-files=no") unindexed = ["git", "diff", "--no-ext-diff", "--quiet"]
status = subprocess.check_output(cmd, stderr=subprocess.DEVNULL, env=denv) indexed = unindexed + ["--cached", "HEAD"]
cmd = unindexed + ["||"] + indexed
child = subprocess.run(cmd, stderr=subprocess.DEVNULL, env=denv)
# "--quiet" git commands imply "--exit-code", which returns:
# 1 if there are differences
# 0 if there are no differences
dwd = bool(child.returncode)
except (subprocess.CalledProcessError, OSError, FileNotFoundError): except (subprocess.CalledProcessError, OSError, FileNotFoundError):
q.put(None) q.put(None)
if status is not None: else:
return q.put(bool(status)) q.put(dwd)
def git_dirty_working_directory(include_untracked=False): def git_dirty_working_directory(include_untracked=False):
@ -241,10 +242,9 @@ def dirty_working_directory():
None. Currently supports git and hg. None. Currently supports git and hg.
""" """
dwd = None dwd = None
cmds = builtins.__xonsh__.commands_cache if _vc_has("git"):
if cmds.lazy_locate_binary("git", ignore_alias=True):
dwd = git_dirty_working_directory() dwd = git_dirty_working_directory()
if cmds.lazy_locate_binary("hg", ignore_alias=True) and dwd is None: if dwd is None and _vc_has("hg"):
dwd = hg_dirty_working_directory() dwd = hg_dirty_working_directory()
return dwd return dwd

View file

@ -206,6 +206,15 @@ def load_xonsh_bindings() -> KeyBindingsBase:
env = builtins.__xonsh__.env env = builtins.__xonsh__.env
event.cli.current_buffer.insert_text(env.get("INDENT")) event.cli.current_buffer.insert_text(env.get("INDENT"))
@handle(Keys.Tab, filter=~tab_insert_indent)
def start_complete(event):
"""If starting completions, automatically move to first option"""
buff = event.app.current_buffer
if buff.complete_state:
buff.complete_next()
else:
buff.start_completion(select_first=True)
@handle(Keys.ControlX, Keys.ControlE, filter=~has_selection) @handle(Keys.ControlX, Keys.ControlE, filter=~has_selection)
def open_editor(event): def open_editor(event):
""" Open current buffer in editor """ """ Open current buffer in editor """

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""The prompt_toolkit based xonsh shell.""" """The prompt_toolkit based xonsh shell."""
import os import os
import re
import sys import sys
import builtins import builtins
from types import MethodType from types import MethodType
@ -37,6 +38,7 @@ from prompt_toolkit.styles.pygments import (
) )
ANSI_OSC_PATTERN = re.compile("\x1b].*?\007")
Token = _TokenType() Token = _TokenType()
events.transmogrify("on_ptk_create", "LoadEvent") events.transmogrify("on_ptk_create", "LoadEvent")
@ -76,6 +78,19 @@ def tokenize_ansi(tokens):
return ansi_tokens return ansi_tokens
def remove_ansi_osc(prompt):
"""Removes the ANSI OSC escape codes - ``prompt_toolkit`` does not support them.
Some terminal emulators - like iTerm2 - uses them for various things.
See: https://www.iterm2.com/documentation-escape-codes.html
"""
osc_tokens = ANSI_OSC_PATTERN.findall(prompt)
prompt = ANSI_OSC_PATTERN.sub("", prompt)
return prompt, osc_tokens
class PromptToolkitShell(BaseShell): class PromptToolkitShell(BaseShell):
"""The xonsh shell for prompt_toolkit v2 and later.""" """The xonsh shell for prompt_toolkit v2 and later."""
@ -250,10 +265,21 @@ class PromptToolkitShell(BaseShell):
p = self.prompt_formatter(p) p = self.prompt_formatter(p)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
print_exception() print_exception()
p, osc_tokens = remove_ansi_osc(p)
toks = partial_color_tokenize(p) toks = partial_color_tokenize(p)
if self._first_prompt: if self._first_prompt:
carriage_return() carriage_return()
self._first_prompt = False self._first_prompt = False
# handle OSC tokens
for osc in osc_tokens:
if osc[2:4] == "0;":
builtins.__xonsh__.env["TITLE"] = osc[4:-1]
else:
print(osc, file=sys.__stdout__, flush=True)
self.settitle() self.settitle()
return tokenize_ansi(PygmentsTokens(toks)) return tokenize_ansi(PygmentsTokens(toks))

View file

@ -882,13 +882,10 @@ def suggest_commands(cmd, env, aliases):
if levenshtein(alias.lower(), cmd, thresh) < thresh: if levenshtein(alias.lower(), cmd, thresh) < thresh:
suggested[alias] = "Alias" suggested[alias] = "Alias"
for path in filter(os.path.isdir, env.get("PATH")): for _cmd in builtins.__xonsh__.commands_cache.all_commands:
for _file in executables_in(path): if _cmd not in suggested:
if ( if levenshtein(_cmd.lower(), cmd, thresh) < thresh:
_file not in suggested suggested[_cmd] = "Command ({0})".format(_cmd)
and levenshtein(_file.lower(), cmd, thresh) < thresh
):
suggested[_file] = "Command ({0})".format(os.path.join(path, _file))
suggested = collections.OrderedDict( suggested = collections.OrderedDict(
sorted( sorted(

View file

@ -20,7 +20,7 @@
{"name": "argcomplete", {"name": "argcomplete",
"package": "xontrib-argcomplete", "package": "xontrib-argcomplete",
"url": "https://github.com/anki-code/xontrib-argcomplete", "url": "https://github.com/anki-code/xontrib-argcomplete",
"description": ["Adding support of kislyuk/argcomplete to xonsh."] "description": ["Argcomplete support to tab completion of python and xonsh scripts in xonsh."]
}, },
{"name": "autojump", {"name": "autojump",
"package": "xontrib-autojump", "package": "xontrib-autojump",
@ -222,7 +222,7 @@
{"name": "pipeliner", {"name": "pipeliner",
"package": "xontrib-pipeliner", "package": "xontrib-pipeliner",
"url": "https://github.com/anki-code/xontrib-pipeliner", "url": "https://github.com/anki-code/xontrib-pipeliner",
"description": ["Easily process the lines using pipes."] "description": ["Let your pipe lines flow thru the Python code in xonsh."]
}, },
{"name": "vox", {"name": "vox",
"package": "xonsh", "package": "xonsh",

View file

@ -159,13 +159,7 @@ def _ul_add_action(actions, opt, res_type, stderr):
actions.append( actions.append(
[ [
_ul_show, _ul_show,
{ {"res": r[0], "res_type": res_type, "desc": r[3], "unit": r[4], "opt": opt},
"res": r[0],
"res_type": res_type,
"desc": r[3],
"unit": r[4],
"opt": opt,
},
] ]
) )
return True return True

View file

@ -28,3 +28,14 @@ def custom_keybindings(bindings, **kw):
pos = buff.document.find_next_word_ending(count=event.arg, WORD=True) pos = buff.document.find_next_word_ending(count=event.arg, WORD=True)
if pos: if pos:
buff.cursor_position += pos buff.cursor_position += pos
@bindings.add(Keys.ShiftDelete)
def shift_delete(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