diff --git a/docs/tutorial_hist.rst b/docs/tutorial_hist.rst index add815d0c..4383f0742 100644 --- a/docs/tutorial_hist.rst +++ b/docs/tutorial_hist.rst @@ -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 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 =============== Last, but certainly not least, the ``gc`` action is a manual hook into executing diff --git a/news/ansi_osc.rst b/news/ansi_osc.rst new file mode 100644 index 000000000..0b42302cc --- /dev/null +++ b/news/ansi_osc.rst @@ -0,0 +1,23 @@ +**Added:** + +* Support for ANSI OSC escape sequences in ``$PROMPT``, setting ``$TITLE`` for example. (#374, #1403) + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/cache-executables-in.rst b/news/cache-executables-in.rst new file mode 100644 index 000000000..f0c347284 --- /dev/null +++ b/news/cache-executables-in.rst @@ -0,0 +1,23 @@ +**Added:** + +* Use command_cache when finding available commands, to speedup command-not-found suggestions + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/history_testing-ead.rst b/news/history_testing-ead.rst new file mode 100644 index 000000000..0d54b6392 --- /dev/null +++ b/news/history_testing-ead.rst @@ -0,0 +1,23 @@ +**Added:** + +* history clear, history off and history on actions, for managing whether history in the current session is saved. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/name_in_error.rst b/news/name_in_error.rst new file mode 100644 index 000000000..fa8a1d88e --- /dev/null +++ b/news/name_in_error.rst @@ -0,0 +1,23 @@ +**Added:** + +* ValueErrors from environ.register now report the name of the bad env var + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/prompt-speed.rst b/news/prompt-speed.rst new file mode 100644 index 000000000..5ac871e51 --- /dev/null +++ b/news/prompt-speed.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* Minor improvements to the get prompt speed. (Mostly in git.) + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/ptk-complete-select-first.rst b/news/ptk-complete-select-first.rst new file mode 100644 index 000000000..7a4b64bff --- /dev/null +++ b/news/ptk-complete-select-first.rst @@ -0,0 +1,25 @@ +**Added:** + +* + +**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:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/shift_del.rst b/news/shift_del.rst new file mode 100644 index 000000000..9f1d2e3f9 --- /dev/null +++ b/news/shift_del.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added to xontrib whole_word_jumping: Shift+Delete hotkey to delete whole word. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/windows_app_execution_aliases.rst b/news/windows_app_execution_aliases.rst new file mode 100644 index 000000000..a5689b00b --- /dev/null +++ b/news/windows_app_execution_aliases.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Fix crash when xonsh tries to run windows app execution aliases. + +**Security:** + +* diff --git a/news/xontrib_descr.rst b/news/xontrib_descr.rst new file mode 100644 index 000000000..9ff290e23 --- /dev/null +++ b/news/xontrib_descr.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* xontrib-argcomplete and xontrib-pipeliner description improvement. + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/rever.xsh b/rever.xsh index 59d8013d9..19a1fad89 100644 --- a/rever.xsh +++ b/rever.xsh @@ -4,8 +4,12 @@ from rever.activities.ghrelease import git_archive_asset $PROJECT = $GITHUB_ORG = $GITHUB_REPO = 'xonsh' $WEBSITE_URL = 'http://xon.sh' $ACTIVITIES = ['authors', 'version_bump', 'changelog', 'pytest', 'appimage', - 'tag', 'push_tag', 'ghrelease', 'sphinx', - 'ghpages', 'pypi', 'conda_forge', + 'tag', 'push_tag', + 'ghrelease', + 'sphinx', + 'ghpages', + 'pypi', + 'conda_forge', ] $PYPI_SIGN = False diff --git a/tests/test_history.py b/tests/test_history.py index fe8356a77..3f9162d86 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -483,3 +483,65 @@ def test__xhj_gc_xx_to_rmfiles( assert minute_diff <= 60 else: 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 diff --git a/tests/test_history_sqlite.py b/tests/test_history_sqlite.py index 762f36132..95661f667 100644 --- a/tests/test_history_sqlite.py +++ b/tests/test_history_sqlite.py @@ -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 else: 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 diff --git a/tests/test_ptk_shell.py b/tests/test_ptk_shell.py index ab84939aa..0eb54300f 100644 --- a/tests/test_ptk_shell.py +++ b/tests/test_ptk_shell.py @@ -8,7 +8,7 @@ from xonsh.platform import minimum_required_ptk_version # 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 @@ -105,4 +105,31 @@ def test_tokenize_ansi(prompt_tokens, ansi_string_parts): 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. diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py index d9d54189b..1e00b518c 100644 --- a/xonsh/built_ins.py +++ b/xonsh/built_ins.py @@ -179,16 +179,30 @@ def pathsearch(func, s, pymode=False, pathobj=False): 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): - with open(fname, "rb") as f: - for i in range(limit): - char = f.read(1) - if char == b"\0": - return True - if char == b"\n": - return False - if char == b"": - return False + try: + with open(fname, "rb") as f: + for i in range(limit): + char = f.read(1) + if char == b"\0": + return True + if char == b"\n": + return False + if char == b"": + return + except OSError as e: + if ON_WINDOWS and is_app_execution_alias(fname): + return True + raise e + return False diff --git a/xonsh/completers/tools.py b/xonsh/completers/tools.py index 809081b4c..17d2fccad 100644 --- a/xonsh/completers/tools.py +++ b/xonsh/completers/tools.py @@ -61,10 +61,7 @@ class RichCompletion(str): def __repr__(self): return "RichCompletion({}, prefix_len={}, display={}, description={})".format( - repr(str(self)), - self.prefix_len, - repr(self.display), - repr(self.description), + repr(str(self)), self.prefix_len, repr(self.display), repr(self.description) ) diff --git a/xonsh/environ.py b/xonsh/environ.py index 3e0d4c85d..61d4c0026 100644 --- a/xonsh/environ.py +++ b/xonsh/environ.py @@ -2099,7 +2099,7 @@ class Env(cabc.MutableMapping): pass else: 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( diff --git a/xonsh/history/base.py b/xonsh/history/base.py index e8816f3d1..3a0ef8ff9 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -71,6 +71,7 @@ class History: self.last_cmd_out = None self.hist_size = None self.hist_units = None + self.remember_history = True def __len__(self): """Return the number of items in current session.""" @@ -153,3 +154,9 @@ class History: If set blocking, then wait until gc action finished. """ pass + + def clear(self): + """Clears the history of the current session from both the disk and + memory. + """ + pass diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 6ccef96e5..438920005 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -202,7 +202,7 @@ class JsonHistoryGC(threading.Thread): # info: file size, closing timestamp, number of commands, filename ts = lj.get("ts", (0.0, None)) 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() if xonsh_debug: @@ -303,6 +303,9 @@ class JsonCommandField(cabc.Sequence): return len(self.hist) def __getitem__(self, key): + if not self.hist.remember_history: + return "" + size = len(self) if isinstance(key, slice): return [self[i] for i in range(*key.indices(size))] @@ -407,6 +410,8 @@ class JsonHistory(History): hf : JsonHistoryFlusher or None The thread that was spawned to flush history """ + if not self.remember_history: + return self.buffer.append(cmd) self._len += 1 # must come before flushing if len(self.buffer) >= self.buffersize: @@ -429,6 +434,7 @@ class JsonHistory(History): hf : JsonHistoryFlusher or None The thread that was spawned to flush history """ + # Implicitly covers case of self.remember_history being False. if len(self.buffer) == 0: return @@ -502,3 +508,18 @@ class JsonHistory(History): if blocking: while self.gc.is_alive(): # while waiting for gc. 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() diff --git a/xonsh/history/main.py b/xonsh/history/main.py index e572822e0..d2c76ce73 100644 --- a/xonsh/history/main.py +++ b/xonsh/history/main.py @@ -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() @@ -354,6 +365,17 @@ def _xh_create_parser(): # 'flush' subcommand 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 @@ -417,5 +439,17 @@ def history_main( hf = hist.flush() if isinstance(hf, threading.Thread): 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: print("Unknown history action {}".format(ns.action), file=sys.stderr) diff --git a/xonsh/history/sqlite.py b/xonsh/history/sqlite.py index 130d08dd9..29378c6db 100644 --- a/xonsh/history/sqlite.py +++ b/xonsh/history/sqlite.py @@ -240,6 +240,8 @@ class SqliteHistory(History): setattr(XH_SQLITE_CACHE, XH_SQLITE_CREATED_SQL_TBL, False) def append(self, cmd): + if not self.remember_history: + return envs = builtins.__xonsh__.env inp = cmd["inp"].rstrip() self.inps.append(inp) @@ -296,3 +298,18 @@ class SqliteHistory(History): if blocking: while self.gc.is_alive(): 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),)) diff --git a/xonsh/parsers/fstring_adaptor.py b/xonsh/parsers/fstring_adaptor.py index 13e92b5fe..8aa8b53dd 100644 --- a/xonsh/parsers/fstring_adaptor.py +++ b/xonsh/parsers/fstring_adaptor.py @@ -181,7 +181,7 @@ class FStringAdaptor: ) ) 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 diff --git a/xonsh/prompt/vc.py b/xonsh/prompt/vc.py index f4bcbd127..3e2a37374 100644 --- a/xonsh/prompt/vc.py +++ b/xonsh/prompt/vc.py @@ -24,26 +24,15 @@ RE_REMOVE_ANSI = LazyObject( def _get_git_branch(q): denv = builtins.__xonsh__.env.detype() try: - branches = xt.decode_bytes( - subprocess.check_output( - ["git", "branch"], env=denv, stderr=subprocess.DEVNULL - ) - ).splitlines() + cmd = ["git", "rev-parse", "--abbrev-ref", "HEAD"] + branch = xt.decode_bytes( + subprocess.check_output(cmd, env=denv, stderr=subprocess.DEVNULL) + ) + branch = branch.splitlines()[0] or None except (subprocess.CalledProcessError, OSError, FileNotFoundError): q.put(None) 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) - break - else: - q.put(None) + q.put(branch) def get_git_branch(): @@ -59,7 +48,6 @@ def get_git_branch(): t.join(timeout=timeout) try: branch = q.get_nowait() - # branch = RE_REMOVE_ANSI.sub("", branch or "") if branch: branch = RE_REMOVE_ANSI.sub("", branch) 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(): """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 @@ -156,17 +153,9 @@ def current_branch(): '' is returned. """ branch = None - cmds = builtins.__xonsh__.commands_cache - # 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: + if _vc_has("git"): branch = get_git_branch() - if not branch and has_hg: + if not branch and _vc_has("hg"): branch = get_hg_branch() if isinstance(branch, subprocess.TimeoutExpired): branch = "" @@ -175,19 +164,31 @@ def current_branch(): def _git_dirty_working_directory(q, include_untracked): - status = None denv = builtins.__xonsh__.env.detype() try: - cmd = ["git", "status", "--porcelain"] + # Borrowed from this conversation + # https://github.com/sindresorhus/pure/issues/115 if include_untracked: - cmd.append("--untracked-files=normal") + cmd = [ + "git", + "status", + "--porcelain", + "--quiet", + "--untracked-files=normal", + ] else: - cmd.append("--untracked-files=no") - status = subprocess.check_output(cmd, stderr=subprocess.DEVNULL, env=denv) + unindexed = ["git", "diff", "--no-ext-diff", "--quiet"] + 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): q.put(None) - if status is not None: - return q.put(bool(status)) + else: + q.put(dwd) def git_dirty_working_directory(include_untracked=False): @@ -241,10 +242,9 @@ def dirty_working_directory(): None. Currently supports git and hg. """ dwd = None - cmds = builtins.__xonsh__.commands_cache - if cmds.lazy_locate_binary("git", ignore_alias=True): + if _vc_has("git"): 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() return dwd diff --git a/xonsh/ptk_shell/key_bindings.py b/xonsh/ptk_shell/key_bindings.py index e578e2a46..33d7d8d02 100644 --- a/xonsh/ptk_shell/key_bindings.py +++ b/xonsh/ptk_shell/key_bindings.py @@ -206,6 +206,15 @@ def load_xonsh_bindings() -> KeyBindingsBase: env = builtins.__xonsh__.env 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) def open_editor(event): """ Open current buffer in editor """ diff --git a/xonsh/ptk_shell/shell.py b/xonsh/ptk_shell/shell.py index 621b933e8..ae38cfffd 100644 --- a/xonsh/ptk_shell/shell.py +++ b/xonsh/ptk_shell/shell.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """The prompt_toolkit based xonsh shell.""" import os +import re import sys import builtins from types import MethodType @@ -37,6 +38,7 @@ from prompt_toolkit.styles.pygments import ( ) +ANSI_OSC_PATTERN = re.compile("\x1b].*?\007") Token = _TokenType() events.transmogrify("on_ptk_create", "LoadEvent") @@ -76,6 +78,19 @@ def tokenize_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): """The xonsh shell for prompt_toolkit v2 and later.""" @@ -250,10 +265,21 @@ class PromptToolkitShell(BaseShell): p = self.prompt_formatter(p) except Exception: # pylint: disable=broad-except print_exception() + + p, osc_tokens = remove_ansi_osc(p) + toks = partial_color_tokenize(p) if self._first_prompt: carriage_return() 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() return tokenize_ansi(PygmentsTokens(toks)) diff --git a/xonsh/tools.py b/xonsh/tools.py index dfd876518..cf96f64a4 100644 --- a/xonsh/tools.py +++ b/xonsh/tools.py @@ -882,13 +882,10 @@ def suggest_commands(cmd, env, aliases): if levenshtein(alias.lower(), cmd, thresh) < thresh: suggested[alias] = "Alias" - for path in filter(os.path.isdir, env.get("PATH")): - for _file in executables_in(path): - if ( - _file not in suggested - and levenshtein(_file.lower(), cmd, thresh) < thresh - ): - suggested[_file] = "Command ({0})".format(os.path.join(path, _file)) + for _cmd in builtins.__xonsh__.commands_cache.all_commands: + if _cmd not in suggested: + if levenshtein(_cmd.lower(), cmd, thresh) < thresh: + suggested[_cmd] = "Command ({0})".format(_cmd) suggested = collections.OrderedDict( sorted( diff --git a/xonsh/xontribs.json b/xonsh/xontribs.json index e53399506..cbdce2dde 100644 --- a/xonsh/xontribs.json +++ b/xonsh/xontribs.json @@ -20,7 +20,7 @@ {"name": "argcomplete", "package": "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", "package": "xontrib-autojump", @@ -222,7 +222,7 @@ {"name": "pipeliner", "package": "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", "package": "xonsh", diff --git a/xonsh/xoreutils/ulimit.py b/xonsh/xoreutils/ulimit.py index ef801879d..7de42ff4e 100644 --- a/xonsh/xoreutils/ulimit.py +++ b/xonsh/xoreutils/ulimit.py @@ -159,13 +159,7 @@ def _ul_add_action(actions, opt, res_type, stderr): actions.append( [ _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 diff --git a/xontrib/whole_word_jumping.py b/xontrib/whole_word_jumping.py index 4330ee50d..ac72fc9c9 100644 --- a/xontrib/whole_word_jumping.py +++ b/xontrib/whole_word_jumping.py @@ -28,3 +28,14 @@ def custom_keybindings(bindings, **kw): pos = buff.document.find_next_word_ending(count=event.arg, WORD=True) if 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