mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-14 05:53:59 +00:00
Reimplement abbreviation expansion to support quiet abbreviations
This reimplements abbreviation to support quiet abbreviations. Quiet abbreviations expand "in secret" before execution.
This commit is contained in:
parent
8135c52c13
commit
1d205d0bbd
10 changed files with 384 additions and 212 deletions
|
@ -14,9 +14,25 @@ abbreviation_t::abbreviation_t(wcstring name, wcstring key, wcstring replacement
|
|||
position(position),
|
||||
from_universal(from_universal) {}
|
||||
|
||||
bool abbreviation_t::matches(const wcstring &token, abbrs_position_t position) const {
|
||||
// We must either expands anywhere, or in the given position.
|
||||
if (this->position != position && this->position != abbrs_position_t::anywhere) {
|
||||
bool abbreviation_t::matches_position(abbrs_position_t position) const {
|
||||
return this->position == abbrs_position_t::anywhere || this->position == position;
|
||||
}
|
||||
|
||||
bool abbreviation_t::matches_phase(abbrs_phase_t phase) const {
|
||||
switch (phase) {
|
||||
case abbrs_phase_t::noisy:
|
||||
return !this->is_quiet;
|
||||
case abbrs_phase_t::quiet:
|
||||
return this->is_quiet;
|
||||
case abbrs_phase_t::any:
|
||||
return true;
|
||||
}
|
||||
DIE("Unreachable");
|
||||
}
|
||||
|
||||
bool abbreviation_t::matches(const wcstring &token, abbrs_position_t position,
|
||||
abbrs_phase_t phase) const {
|
||||
if (!this->matches_position(position) || !this->matches_phase(phase)) {
|
||||
return false;
|
||||
}
|
||||
if (this->is_regex()) {
|
||||
|
@ -31,21 +47,23 @@ acquired_lock<abbrs_set_t> abbrs_get_set() {
|
|||
return abbrs.acquire();
|
||||
}
|
||||
|
||||
abbrs_replacer_list_t abbrs_set_t::match(const wcstring &token, abbrs_position_t position) const {
|
||||
abbrs_replacer_list_t abbrs_set_t::match(const wcstring &token, abbrs_position_t position,
|
||||
abbrs_phase_t phase) const {
|
||||
abbrs_replacer_list_t result{};
|
||||
// Later abbreviations take precedence so walk backwards.
|
||||
for (auto it = abbrs_.rbegin(); it != abbrs_.rend(); ++it) {
|
||||
const abbreviation_t &abbr = *it;
|
||||
if (abbr.matches(token, position)) {
|
||||
if (abbr.matches(token, position, phase)) {
|
||||
result.push_back(abbrs_replacer_t{abbr.replacement, abbr.replacement_is_function});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool abbrs_set_t::has_match(const wcstring &token, abbrs_position_t position) const {
|
||||
bool abbrs_set_t::has_match(const wcstring &token, abbrs_position_t position,
|
||||
abbrs_phase_t phase) const {
|
||||
for (const auto &abbr : abbrs_) {
|
||||
if (abbr.matches(token, position)) {
|
||||
if (abbr.matches(token, position, phase)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
36
src/abbrs.h
36
src/abbrs.h
|
@ -18,6 +18,13 @@ enum class abbrs_position_t : uint8_t {
|
|||
anywhere, // expand in any token
|
||||
};
|
||||
|
||||
/// Describes a phase of expansion.
|
||||
enum class abbrs_phase_t : uint8_t {
|
||||
noisy, // expand noisy abbreviations, which visibly replace their tokens.
|
||||
quiet, // expand quiet abbreviations, which do not visibly replace their tokens.
|
||||
any, // phase is ignored - useful for syntax highlighting.
|
||||
};
|
||||
|
||||
struct abbreviation_t {
|
||||
// Abbreviation name. This is unique within the abbreviation set.
|
||||
// This is used as the token to match unless we have a regex.
|
||||
|
@ -44,17 +51,15 @@ struct abbreviation_t {
|
|||
/// Mark if we came from a universal variable.
|
||||
bool from_universal{};
|
||||
|
||||
/// Whether this abbrevation expands after entry (before execution).
|
||||
bool expand_on_entry{true};
|
||||
|
||||
/// Whether this abbrevation expands on execution.
|
||||
bool expand_on_execute{true};
|
||||
/// Whether this abbrevation is quiet. Noisy abbreviations visibly replace their tokens in the
|
||||
/// command line any history, quiet ones do not (unless expansion results in a syntax error).
|
||||
bool is_quiet{false};
|
||||
|
||||
// \return true if this is a regex abbreviation.
|
||||
bool is_regex() const { return this->regex.has_value(); }
|
||||
|
||||
// \return true if we match a token at a given position.
|
||||
bool matches(const wcstring &token, abbrs_position_t position) const;
|
||||
// \return true if we match a token at a given position in a given phase.
|
||||
bool matches(const wcstring &token, abbrs_position_t position, abbrs_phase_t phase) const;
|
||||
|
||||
// Construct from a name, a key which matches a token, a replacement token, a position, and
|
||||
// whether we are derived from a universal variable.
|
||||
|
@ -63,6 +68,13 @@ struct abbreviation_t {
|
|||
bool from_universal = false);
|
||||
|
||||
abbreviation_t() = default;
|
||||
|
||||
private:
|
||||
// \return if we expand in a given position.
|
||||
bool matches_position(abbrs_position_t position) const;
|
||||
|
||||
// \return if we expand in a given phase.
|
||||
bool matches_phase(abbrs_phase_t phase) const;
|
||||
};
|
||||
|
||||
/// The result of an abbreviation expansion.
|
||||
|
@ -79,10 +91,11 @@ class abbrs_set_t {
|
|||
public:
|
||||
/// \return the list of replacers for an input token, in priority order.
|
||||
/// The \p position is given to describe where the token was found.
|
||||
abbrs_replacer_list_t match(const wcstring &token, abbrs_position_t position) const;
|
||||
abbrs_replacer_list_t match(const wcstring &token, abbrs_position_t position,
|
||||
abbrs_phase_t phase) const;
|
||||
|
||||
/// \return whether we would have at least one replacer for a given token.
|
||||
bool has_match(const wcstring &token, abbrs_position_t position) const;
|
||||
bool has_match(const wcstring &token, abbrs_position_t position, abbrs_phase_t phase) const;
|
||||
|
||||
/// Add an abbreviation. Any abbreviation with the same name is replaced.
|
||||
void add(abbreviation_t &&abbr);
|
||||
|
@ -118,8 +131,9 @@ acquired_lock<abbrs_set_t> abbrs_get_set();
|
|||
|
||||
/// \return the list of replacers for an input token, in priority order, using the global set.
|
||||
/// The \p position is given to describe where the token was found.
|
||||
inline abbrs_replacer_list_t abbrs_match(const wcstring &token, abbrs_position_t position) {
|
||||
return abbrs_get_set()->match(token, position);
|
||||
inline abbrs_replacer_list_t abbrs_match(const wcstring &token, abbrs_position_t position,
|
||||
abbrs_phase_t phase) {
|
||||
return abbrs_get_set()->match(token, position, phase);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -42,8 +42,7 @@ struct abbr_options_t {
|
|||
maybe_t<wcstring> regex_pattern;
|
||||
maybe_t<abbrs_position_t> position{};
|
||||
|
||||
bool expand_on_entry{};
|
||||
bool expand_on_execute{};
|
||||
bool quiet{};
|
||||
|
||||
wcstring_list_t args;
|
||||
|
||||
|
@ -80,17 +79,10 @@ struct abbr_options_t {
|
|||
streams.err.append_format(_(L"%ls: --function option requires --add\n"), CMD);
|
||||
return false;
|
||||
}
|
||||
if (!add && (expand_on_entry || expand_on_execute)) {
|
||||
streams.err.append_format(_(L"%ls: --expand-on option requires --add\n"), CMD);
|
||||
if (!add && quiet) {
|
||||
streams.err.append_format(_(L"%ls: --quiet option requires --add\n"), CMD);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no expand-on is specified, expand on both entry and execute, to match historical
|
||||
// behavior.
|
||||
if (add && !expand_on_entry && !expand_on_execute) {
|
||||
expand_on_entry = true;
|
||||
expand_on_execute = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
@ -111,15 +103,8 @@ static int abbr_show(const abbr_options_t &, io_streams_t &streams) {
|
|||
comps.push_back(L"--regex");
|
||||
comps.push_back(escape_string(abbr.key));
|
||||
}
|
||||
// The default is to expand on both entry and execute.
|
||||
// Add flags if we're not the default.
|
||||
if (!(abbr.expand_on_entry && abbr.expand_on_execute)) {
|
||||
if (abbr.expand_on_entry) {
|
||||
comps.push_back(L"--expand-on entry");
|
||||
}
|
||||
if (abbr.expand_on_execute) {
|
||||
comps.push_back(L"--expand-on execute");
|
||||
}
|
||||
if (abbr.is_quiet) {
|
||||
comps.push_back(L"--quiet");
|
||||
}
|
||||
if (abbr.replacement_is_function) {
|
||||
comps.push_back(L"--function");
|
||||
|
@ -259,6 +244,7 @@ static int abbr_add(const abbr_options_t &opts, io_streams_t &streams) {
|
|||
abbreviation_t abbr{std::move(name), std::move(key), std::move(replacement), position};
|
||||
abbr.regex = std::move(regex);
|
||||
abbr.replacement_is_function = opts.function;
|
||||
abbr.is_quiet = opts.quiet;
|
||||
abbrs_get_set()->add(std::move(abbr));
|
||||
return STATUS_CMD_OK;
|
||||
}
|
||||
|
@ -287,27 +273,26 @@ maybe_t<int> builtin_abbr(parser_t &parser, io_streams_t &streams, const wchar_t
|
|||
const wchar_t *cmd = argv[0];
|
||||
abbr_options_t opts;
|
||||
// Note 1 is returned by wgetopt to indicate a non-option argument.
|
||||
enum { NON_OPTION_ARGUMENT = 1, REGEX_SHORT, EXPAND_ON_SHORT };
|
||||
enum { NON_OPTION_ARGUMENT = 1, REGEX_SHORT, EXPAND_ON_SHORT, QUIET_SHORT };
|
||||
|
||||
// Note the leading '-' causes wgetopter to return arguments in order, instead of permuting
|
||||
// them. We need this behavior for compatibility with pre-builtin abbreviations where options
|
||||
// could be given literally, for example `abbr e emacs -nw`.
|
||||
static const wchar_t *const short_options = L"-afrseqgUh";
|
||||
static const struct woption long_options[] = {
|
||||
{L"add", no_argument, 'a'},
|
||||
{L"position", required_argument, 'p'},
|
||||
{L"regex", required_argument, REGEX_SHORT},
|
||||
{L"expand-on", required_argument, EXPAND_ON_SHORT},
|
||||
{L"function", no_argument, 'f'},
|
||||
{L"rename", no_argument, 'r'},
|
||||
{L"erase", no_argument, 'e'},
|
||||
{L"query", no_argument, 'q'},
|
||||
{L"show", no_argument, 's'},
|
||||
{L"list", no_argument, 'l'},
|
||||
{L"global", no_argument, 'g'},
|
||||
{L"universal", no_argument, 'U'},
|
||||
{L"help", no_argument, 'h'},
|
||||
{}};
|
||||
static const struct woption long_options[] = {{L"add", no_argument, 'a'},
|
||||
{L"position", required_argument, 'p'},
|
||||
{L"regex", required_argument, REGEX_SHORT},
|
||||
{L"quiet", no_argument, QUIET_SHORT},
|
||||
{L"function", no_argument, 'f'},
|
||||
{L"rename", no_argument, 'r'},
|
||||
{L"erase", no_argument, 'e'},
|
||||
{L"query", no_argument, 'q'},
|
||||
{L"show", no_argument, 's'},
|
||||
{L"list", no_argument, 'l'},
|
||||
{L"global", no_argument, 'g'},
|
||||
{L"universal", no_argument, 'U'},
|
||||
{L"help", no_argument, 'h'},
|
||||
{}};
|
||||
|
||||
int argc = builtin_count_args(argv);
|
||||
int opt;
|
||||
|
@ -356,17 +341,8 @@ maybe_t<int> builtin_abbr(parser_t &parser, io_streams_t &streams, const wchar_t
|
|||
opts.regex_pattern = w.woptarg;
|
||||
break;
|
||||
}
|
||||
case EXPAND_ON_SHORT: {
|
||||
if (!wcscmp(w.woptarg, L"entry")) {
|
||||
opts.expand_on_entry = true;
|
||||
} else if (!wcscmp(w.woptarg, L"execute")) {
|
||||
opts.expand_on_execute = true;
|
||||
} else {
|
||||
streams.err.append_format(_(L"%ls: Invalid expand-on '%ls'\nexpand-on must be "
|
||||
L"one of: entry, execute.\n"),
|
||||
CMD, w.woptarg);
|
||||
return STATUS_INVALID_ARGS;
|
||||
}
|
||||
case QUIET_SHORT: {
|
||||
opts.quiet = true;
|
||||
break;
|
||||
}
|
||||
case 'f':
|
||||
|
|
|
@ -2481,7 +2481,7 @@ static void test_abbreviations() {
|
|||
|
||||
// Helper to expand an abbreviation, enforcing we have no more than one result.
|
||||
auto abbr_expand_1 = [](const wcstring &token, abbrs_position_t pos) -> maybe_t<wcstring> {
|
||||
auto result = abbrs_match(token, pos);
|
||||
auto result = abbrs_match(token, pos, abbrs_phase_t::any);
|
||||
if (result.size() > 1) {
|
||||
err(L"abbreviation expansion for %ls returned more than 1 result", token.c_str());
|
||||
}
|
||||
|
@ -2506,8 +2506,8 @@ static void test_abbreviations() {
|
|||
maybe_t<wcstring> result;
|
||||
auto expand_abbreviation_in_command = [](const wcstring &cmdline,
|
||||
size_t cursor_pos) -> maybe_t<wcstring> {
|
||||
if (auto edit = reader_expand_abbreviation_at_cursor(cmdline, cursor_pos,
|
||||
parser_t::principal_parser())) {
|
||||
if (auto edit = reader_expand_abbreviation_at_cursor(
|
||||
cmdline, cursor_pos, abbrs_phase_t::noisy, parser_t::principal_parser())) {
|
||||
wcstring cmdline_expanded = cmdline;
|
||||
std::vector<highlight_spec_t> colors{cmdline_expanded.size()};
|
||||
apply_edit(&cmdline_expanded, &colors, *edit);
|
||||
|
|
|
@ -109,6 +109,8 @@ class category_list_t {
|
|||
category_t path{L"path", L"Searching/using paths"};
|
||||
|
||||
category_t screen{L"screen", L"Screen repaints"};
|
||||
|
||||
category_t abbrs{L"abbrs", L"Abbreviation expansion"};
|
||||
};
|
||||
|
||||
/// The class responsible for logging.
|
||||
|
|
|
@ -1336,7 +1336,7 @@ static bool command_is_valid(const wcstring &cmd, enum statement_decoration_t de
|
|||
|
||||
// Abbreviations
|
||||
if (!is_valid && abbreviation_ok)
|
||||
is_valid = abbrs_get_set()->has_match(cmd, abbrs_position_t::command);
|
||||
is_valid = abbrs_get_set()->has_match(cmd, abbrs_position_t::command, abbrs_phase_t::any);
|
||||
|
||||
// Regular commands
|
||||
if (!is_valid && command_ok) is_valid = path_get_path(cmd, vars).has_value();
|
||||
|
|
383
src/reader.cpp
383
src/reader.cpp
|
@ -618,6 +618,10 @@ struct readline_loop_state_t {
|
|||
/// command.
|
||||
bool finished{false};
|
||||
|
||||
/// If the loop is finished, then the command to execute; this may differ from the command line
|
||||
/// itself due to quiet abbreviations.
|
||||
wcstring cmd;
|
||||
|
||||
/// Maximum number of characters to read.
|
||||
size_t nchars{std::numeric_limits<size_t>::max()};
|
||||
};
|
||||
|
@ -789,7 +793,7 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
|
|||
void pager_selection_changed();
|
||||
|
||||
/// Expand abbreviations at the current cursor position, minus backtrack_amt.
|
||||
bool expand_abbreviation_as_necessary(size_t cursor_backtrack);
|
||||
bool expand_abbreviation_at_cursor(size_t cursor_backtrack, abbrs_phase_t phase);
|
||||
|
||||
/// \return true if the command line has changed and repainting is needed. If \p colors is not
|
||||
/// null, then also return true if the colors have changed.
|
||||
|
@ -872,8 +876,19 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
|
|||
void handle_readline_command(readline_cmd_t cmd, readline_loop_state_t &rls);
|
||||
|
||||
// Handle readline_cmd_t::execute. This may mean inserting a newline if the command is
|
||||
// unfinished.
|
||||
void handle_execute(readline_loop_state_t &rls);
|
||||
// unfinished. It may also set 'finished' and 'cmd' inside the rls.
|
||||
// \return true on success, false if we got an error, in which case the caller should fire the
|
||||
// error event.
|
||||
bool handle_execute(readline_loop_state_t &rls);
|
||||
|
||||
// Add the current command line contents to history.
|
||||
void add_to_history() const;
|
||||
|
||||
// Expand abbreviations before execution.
|
||||
// Replace the command line with any noisy abbreviations as needed.
|
||||
// Populate \p to_exec with the command to execute, which may include silent abbreviations.
|
||||
// \return the test result, which may be incomplete to insert a newline, or an error.
|
||||
parser_test_error_bits_t expand_for_execute(wcstring *to_exec);
|
||||
|
||||
void clear_pager();
|
||||
void select_completion_in_direction(selection_motion_t dir,
|
||||
|
@ -1348,11 +1363,13 @@ void reader_data_t::pager_selection_changed() {
|
|||
}
|
||||
|
||||
/// Expand an abbreviation replacer, which means either returning its literal replacement or running
|
||||
/// its function. \return the replacement string, or none to skip it.
|
||||
/// its function. \return the replacement string, or none to skip it. This may run fish script!
|
||||
maybe_t<wcstring> expand_replacer(const wcstring &token, const abbrs_replacer_t &repl,
|
||||
parser_t &parser) {
|
||||
if (!repl.is_function) {
|
||||
// Literal replacement cannot fail.
|
||||
FLOGF(abbrs, L"Expanded literal abbreviation <%ls> -> <%ls>", token.c_str(),
|
||||
repl.replacement.c_str());
|
||||
return repl.replacement;
|
||||
}
|
||||
|
||||
|
@ -1360,76 +1377,98 @@ maybe_t<wcstring> expand_replacer(const wcstring &token, const abbrs_replacer_t
|
|||
cmd.push_back(L' ');
|
||||
cmd.append(escape_string(token));
|
||||
|
||||
scoped_push<bool> not_interactive(&parser.libdata().is_interactive, false);
|
||||
|
||||
wcstring_list_t outputs{};
|
||||
int ret = exec_subshell(cmd, parser, outputs, false /* not apply_exit_status */);
|
||||
if (ret != STATUS_CMD_OK) {
|
||||
return none();
|
||||
}
|
||||
return join_strings(outputs, L'\n');
|
||||
wcstring result = join_strings(outputs, L'\n');
|
||||
FLOGF(abbrs, L"Expanded function abbreviation <%ls> -> <%ls>", token.c_str(), result.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Extract all the token ranges in \p str, along with whether they are an undecorated command.
|
||||
// Tokens containing command substitutions are skipped; this ensures tokens are non-overlapping.
|
||||
struct positioned_token_t {
|
||||
source_range_t range;
|
||||
bool is_cmd;
|
||||
};
|
||||
static std::vector<positioned_token_t> extract_tokens(const wcstring &str) {
|
||||
using namespace ast;
|
||||
|
||||
parse_tree_flags_t ast_flags = parse_flag_continue_after_error |
|
||||
parse_flag_accept_incomplete_tokens |
|
||||
parse_flag_leave_unterminated;
|
||||
auto ast = ast::ast_t::parse(str, ast_flags);
|
||||
|
||||
// Helper to check if a node is the command portion of an undecorated statement.
|
||||
auto is_command = [&](const node_t *node) {
|
||||
for (const node_t *cursor = node; cursor; cursor = cursor->parent) {
|
||||
if (const auto *stmt = cursor->try_as<decorated_statement_t>()) {
|
||||
if (!stmt->opt_decoration && node == &stmt->command) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
wcstring cmdsub_contents;
|
||||
std::vector<positioned_token_t> result;
|
||||
traversal_t tv = ast.walk();
|
||||
while (const node_t *node = tv.next()) {
|
||||
// We are only interested in leaf nodes with source.
|
||||
if (node->category != category_t::leaf) continue;
|
||||
source_range_t r = node->source_range();
|
||||
if (r.length == 0) continue;
|
||||
|
||||
// If we have command subs, then we don't include this token; instead we recurse.
|
||||
bool has_cmd_subs = false;
|
||||
size_t cmdsub_cursor = r.start, cmdsub_start = 0, cmdsub_end = 0;
|
||||
while (parse_util_locate_cmdsubst_range(str, &cmdsub_cursor, &cmdsub_contents,
|
||||
&cmdsub_start, &cmdsub_end,
|
||||
true /* accept incomplete */) > 0) {
|
||||
if (cmdsub_start >= r.end()) {
|
||||
break;
|
||||
}
|
||||
has_cmd_subs = true;
|
||||
for (positioned_token_t t : extract_tokens(cmdsub_contents)) {
|
||||
// cmdsub_start is the open paren; the contents start one after it.
|
||||
t.range.start += static_cast<source_offset_t>(cmdsub_start + 1);
|
||||
result.push_back(t);
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_cmd_subs) {
|
||||
// Common case of no command substitutions in this leaf node.
|
||||
result.push_back(positioned_token_t{r, is_command(node)});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Expand abbreviations at the given cursor position. Does NOT inspect 'data'.
|
||||
maybe_t<edit_t> reader_expand_abbreviation_at_cursor(const wcstring &cmdline, size_t cursor_pos,
|
||||
parser_t &parser) {
|
||||
// Get the surrounding command substitution.
|
||||
const wchar_t *const buff = cmdline.c_str();
|
||||
const wchar_t *cmdsub_begin = nullptr, *cmdsub_end = nullptr;
|
||||
parse_util_cmdsubst_extent(buff, cursor_pos, &cmdsub_begin, &cmdsub_end);
|
||||
assert(cmdsub_begin != nullptr && cmdsub_begin >= buff);
|
||||
assert(cmdsub_end != nullptr && cmdsub_end >= cmdsub_begin);
|
||||
|
||||
// Determine the offset of this command substitution.
|
||||
const size_t subcmd_offset = cmdsub_begin - buff;
|
||||
|
||||
const wcstring subcmd = wcstring(cmdsub_begin, cmdsub_end - cmdsub_begin);
|
||||
const size_t subcmd_cursor_pos = cursor_pos - subcmd_offset;
|
||||
|
||||
// Parse this subcmd.
|
||||
using namespace ast;
|
||||
auto ast =
|
||||
ast_t::parse(subcmd, parse_flag_continue_after_error | parse_flag_accept_incomplete_tokens |
|
||||
parse_flag_leave_unterminated);
|
||||
|
||||
// Find a leaf node where the cursor is at its end.
|
||||
const node_t *leaf = nullptr;
|
||||
traversal_t tv = ast.walk();
|
||||
while (const node_t *node = tv.next()) {
|
||||
if (node->category == category_t::leaf) {
|
||||
auto r = node->try_source_range();
|
||||
if (r && r->start <= subcmd_cursor_pos && subcmd_cursor_pos <= r->end()) {
|
||||
leaf = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!leaf) {
|
||||
abbrs_phase_t phase, parser_t &parser) {
|
||||
// Find the token containing the cursor. Usually users edit from the end, so walk backwards.
|
||||
const auto tokens = extract_tokens(cmdline);
|
||||
auto iter = std::find_if(tokens.rbegin(), tokens.rend(), [&](const positioned_token_t &t) {
|
||||
return t.range.contains_inclusive(cursor_pos);
|
||||
});
|
||||
if (iter == tokens.rend()) {
|
||||
return none();
|
||||
}
|
||||
source_range_t range = iter->range;
|
||||
abbrs_position_t position =
|
||||
iter->is_cmd ? abbrs_position_t::command : abbrs_position_t::anywhere;
|
||||
|
||||
// We found the leaf node before the cursor.
|
||||
// Decide if this leaf is in "command position:" it is the command part of an undecorated
|
||||
// statement.
|
||||
bool leaf_is_command = false;
|
||||
for (const node_t *cursor = leaf; cursor; cursor = cursor->parent) {
|
||||
if (const auto *stmt = cursor->try_as<decorated_statement_t>()) {
|
||||
if (!stmt->opt_decoration && leaf == &stmt->command) {
|
||||
leaf_is_command = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
abbrs_position_t expand_position =
|
||||
leaf_is_command ? abbrs_position_t::command : abbrs_position_t::anywhere;
|
||||
|
||||
// Now we can expand the abbreviation.
|
||||
// Find the first which succeeds.
|
||||
wcstring token = leaf->source(subcmd);
|
||||
auto replacers = abbrs_match(token, expand_position);
|
||||
wcstring token_str = cmdline.substr(range.start, range.length);
|
||||
auto replacers = abbrs_match(token_str, position, phase);
|
||||
for (const auto &replacer : replacers) {
|
||||
if (auto replacement = expand_replacer(token, replacer, parser)) {
|
||||
// Replace the token in the full command. Maintain the relative position of the cursor.
|
||||
source_range_t r = leaf->source_range();
|
||||
return edit_t(subcmd_offset + r.start, r.length, replacement.acquire());
|
||||
if (auto replacement = expand_replacer(token_str, replacer, parser)) {
|
||||
return edit_t{range.start, range.length, replacement.acquire()};
|
||||
}
|
||||
}
|
||||
return none();
|
||||
|
@ -1438,7 +1477,7 @@ maybe_t<edit_t> reader_expand_abbreviation_at_cursor(const wcstring &cmdline, si
|
|||
/// Expand abbreviations at the current cursor position, minus the given cursor backtrack. This may
|
||||
/// change the command line but does NOT repaint it. This is to allow the caller to coalesce
|
||||
/// repaints.
|
||||
bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) {
|
||||
bool reader_data_t::expand_abbreviation_at_cursor(size_t cursor_backtrack, abbrs_phase_t phase) {
|
||||
bool result = false;
|
||||
editable_line_t *el = active_edit_line();
|
||||
|
||||
|
@ -1446,7 +1485,8 @@ bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) {
|
|||
// Try expanding abbreviations.
|
||||
this->update_commandline_state();
|
||||
size_t cursor_pos = el->position() - std::min(el->position(), cursor_backtrack);
|
||||
if (auto edit = reader_expand_abbreviation_at_cursor(el->text(), cursor_pos, parser())) {
|
||||
if (auto edit =
|
||||
reader_expand_abbreviation_at_cursor(el->text(), cursor_pos, phase, parser())) {
|
||||
push_edit(el, std::move(*edit));
|
||||
update_buff_pos(el);
|
||||
result = true;
|
||||
|
@ -1455,6 +1495,44 @@ bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) {
|
|||
return result;
|
||||
}
|
||||
|
||||
// Expand any quiet abbreviations, modifying \p str in place.
|
||||
// \return true if the string changed, false if not.
|
||||
static bool expand_quiet_abbreviations(wcstring *inout_str, parser_t &parser) {
|
||||
bool modified = false;
|
||||
const wcstring &str = *inout_str;
|
||||
wcstring result = str;
|
||||
// We may have multiple replacements.
|
||||
// Keep track of the change in length.
|
||||
size_t orig_lengths = 0;
|
||||
size_t repl_lengths = 0;
|
||||
|
||||
const std::vector<positioned_token_t> tokens = extract_tokens(result);
|
||||
wcstring token;
|
||||
for (positioned_token_t pt : tokens) {
|
||||
source_range_t orig_range = pt.range;
|
||||
token.assign(str, orig_range.start, orig_range.length);
|
||||
|
||||
abbrs_position_t position =
|
||||
pt.is_cmd ? abbrs_position_t::command : abbrs_position_t::anywhere;
|
||||
auto replacers = abbrs_match(token, position, abbrs_phase_t::quiet);
|
||||
for (const auto &replacer : replacers) {
|
||||
const auto replacement = expand_replacer(token, replacer, parser);
|
||||
if (replacement && *replacement != token) {
|
||||
modified = true;
|
||||
result.replace(orig_range.start + repl_lengths - orig_lengths, orig_range.length,
|
||||
*replacement);
|
||||
orig_lengths += orig_range.length;
|
||||
repl_lengths += replacement->length();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (modified) {
|
||||
*inout_str = std::move(result);
|
||||
}
|
||||
return modified;
|
||||
}
|
||||
|
||||
void reader_reset_interrupted() { interrupted = 0; }
|
||||
|
||||
int reader_test_and_clear_interrupted() {
|
||||
|
@ -3619,7 +3697,10 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
|
|||
break;
|
||||
}
|
||||
case rl::execute: {
|
||||
this->handle_execute(rls);
|
||||
if (!this->handle_execute(rls)) {
|
||||
event_fire_generic(parser(), L"fish_posterror", {command_line.text()});
|
||||
screen.reset_abandoning_line(termsize_last().width);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -4146,7 +4227,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
|
|||
}
|
||||
|
||||
case rl::expand_abbr: {
|
||||
if (expand_abbreviation_as_necessary(1)) {
|
||||
if (expand_abbreviation_at_cursor(1, abbrs_phase_t::noisy)) {
|
||||
inputter.function_set_status(true);
|
||||
} else {
|
||||
inputter.function_set_status(false);
|
||||
|
@ -4193,13 +4274,94 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
|
|||
}
|
||||
}
|
||||
|
||||
void reader_data_t::handle_execute(readline_loop_state_t &rls) {
|
||||
void reader_data_t::add_to_history() const {
|
||||
if (!history || conf.in_silent_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Historical behavior is to trim trailing spaces, unless escape (#7661).
|
||||
wcstring text = command_line.text();
|
||||
while (!text.empty() && text.back() == L' ' &&
|
||||
count_preceding_backslashes(text, text.size() - 1) % 2 == 0) {
|
||||
text.pop_back();
|
||||
}
|
||||
|
||||
// Remove ephemeral items - even if the text is empty.
|
||||
history->remove_ephemeral_items();
|
||||
|
||||
if (!text.empty()) {
|
||||
// Mark this item as ephemeral if there is a leading space (#615).
|
||||
history_persistence_mode_t mode;
|
||||
if (text.front() == L' ') {
|
||||
// Leading spaces are ephemeral (#615).
|
||||
mode = history_persistence_mode_t::ephemeral;
|
||||
} else if (in_private_mode(this->vars())) {
|
||||
// Private mode means in-memory only.
|
||||
mode = history_persistence_mode_t::memory;
|
||||
} else {
|
||||
mode = history_persistence_mode_t::disk;
|
||||
}
|
||||
history_t::add_pending_with_file_detection(history, text, this->vars().snapshot(), mode);
|
||||
}
|
||||
}
|
||||
|
||||
parser_test_error_bits_t reader_data_t::expand_for_execute(wcstring *to_exec) {
|
||||
// Expand abbreviations. First noisy abbreviations at the cursor, followed by quiet
|
||||
// abbreviations, anywhere that matches.
|
||||
// The first expansion is "user visible" and enters into history.
|
||||
// The second happens silently, unless it produces an error, in which case we show the text to
|
||||
// the user.
|
||||
// We update the command line with the user-visible result and then return the silent result by
|
||||
// reference.
|
||||
|
||||
assert(to_exec && "Null pointer");
|
||||
editable_line_t *el = &command_line;
|
||||
parser_test_error_bits_t test_res = 0;
|
||||
|
||||
// Syntax check before expanding abbreviations. We could consider relaxing this: a string may be
|
||||
// syntactically invalid but become valid after expanding abbreviations.
|
||||
if (conf.syntax_check_ok) {
|
||||
test_res = reader_shell_test(parser(), el->text());
|
||||
if (test_res & PARSER_TEST_ERROR) return test_res;
|
||||
}
|
||||
|
||||
// Noisy abbreviations at the cursor.
|
||||
// Note we want to expand abbreviations even if incomplete.
|
||||
if (expand_abbreviation_at_cursor(0, abbrs_phase_t::noisy)) {
|
||||
// Trigger syntax highlighting as we are likely about to execute this command.
|
||||
this->super_highlight_me_plenty();
|
||||
if (conf.syntax_check_ok) {
|
||||
test_res = reader_shell_test(parser(), el->text());
|
||||
}
|
||||
}
|
||||
|
||||
// We may have an error or be incomplete.
|
||||
if (test_res) return test_res;
|
||||
|
||||
// Quiet abbreviations anywhere.
|
||||
wcstring text_for_exec = el->text();
|
||||
if (expand_quiet_abbreviations(&text_for_exec, parser())) {
|
||||
if (conf.syntax_check_ok) {
|
||||
test_res = reader_shell_test(parser(), text_for_exec);
|
||||
if (test_res) {
|
||||
// Replace the command line with an undoable edit, to make the replacement visible.
|
||||
push_edit(el, edit_t(0, el->size(), std::move(text_for_exec)));
|
||||
return test_res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*to_exec = std::move(text_for_exec);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool reader_data_t::handle_execute(readline_loop_state_t &rls) {
|
||||
// Evaluate. If the current command is unfinished, or if the charater is escaped
|
||||
// using a backslash, insert a newline.
|
||||
// If the user hits return while navigating the pager, it only clears the pager.
|
||||
if (is_navigating_pager_contents()) {
|
||||
clear_pager();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Delete any autosuggestion.
|
||||
|
@ -4232,77 +4394,26 @@ void reader_data_t::handle_execute(readline_loop_state_t &rls) {
|
|||
// If the conditions are met, insert a new line at the position of the cursor.
|
||||
if (continue_on_next_line) {
|
||||
insert_char(el, L'\n');
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// See if this command is valid.
|
||||
parser_test_error_bits_t command_test_result = 0;
|
||||
if (conf.syntax_check_ok) {
|
||||
command_test_result = reader_shell_test(parser(), el->text());
|
||||
}
|
||||
if (command_test_result == 0 || command_test_result == PARSER_TEST_INCOMPLETE) {
|
||||
// This command is valid, but an abbreviation may make it invalid. If so, we
|
||||
// will have to test again.
|
||||
if (expand_abbreviation_as_necessary(0)) {
|
||||
// Trigger syntax highlighting as we are likely about to execute this command.
|
||||
this->super_highlight_me_plenty();
|
||||
if (conf.syntax_check_ok) {
|
||||
command_test_result = reader_shell_test(parser(), el->text());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (command_test_result == 0) {
|
||||
// Finished command, execute it. Don't add items in silent mode (#7230).
|
||||
wcstring text = command_line.text();
|
||||
if (text.empty()) {
|
||||
// Here the user just hit return. Make a new prompt, don't remove ephemeral
|
||||
// items.
|
||||
rls.finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Historical behavior is to trim trailing spaces.
|
||||
// However, escaped spaces ('\ ') should not be trimmed (#7661)
|
||||
// This can be done by counting pre-trailing '\'
|
||||
// If there's an odd number, this must be an escaped space.
|
||||
while (!text.empty() && text.back() == L' ' &&
|
||||
count_preceding_backslashes(text, text.size() - 1) % 2 == 0) {
|
||||
text.pop_back();
|
||||
}
|
||||
|
||||
if (history && !conf.in_silent_mode) {
|
||||
// Remove ephemeral items - even if the text is empty
|
||||
history->remove_ephemeral_items();
|
||||
|
||||
if (!text.empty()) {
|
||||
// Mark this item as ephemeral if there is a leading space (#615).
|
||||
history_persistence_mode_t mode;
|
||||
if (text.front() == L' ') {
|
||||
// Leading spaces are ephemeral (#615).
|
||||
mode = history_persistence_mode_t::ephemeral;
|
||||
} else if (in_private_mode(this->vars())) {
|
||||
// Private mode means in-memory only.
|
||||
mode = history_persistence_mode_t::memory;
|
||||
} else {
|
||||
mode = history_persistence_mode_t::disk;
|
||||
}
|
||||
history_t::add_pending_with_file_detection(history, text, this->vars().snapshot(),
|
||||
mode);
|
||||
}
|
||||
}
|
||||
|
||||
rls.finished = true;
|
||||
update_buff_pos(&command_line, command_line.size());
|
||||
} else if (command_test_result == PARSER_TEST_INCOMPLETE) {
|
||||
// We are incomplete, continue editing.
|
||||
// Expand the command line in preparation for execution.
|
||||
// to_exec is the command to execute; the command line itself has the command for history.
|
||||
wcstring to_exec;
|
||||
parser_test_error_bits_t test_res = this->expand_for_execute(&to_exec);
|
||||
if (test_res & PARSER_TEST_ERROR) {
|
||||
return false;
|
||||
} else if (test_res & PARSER_TEST_INCOMPLETE) {
|
||||
insert_char(el, L'\n');
|
||||
} else {
|
||||
// Result must be some combination including an error. The error message will
|
||||
// already be printed, all we need to do is repaint.
|
||||
event_fire_generic(parser(), L"fish_posterror", {el->text()});
|
||||
screen.reset_abandoning_line(termsize_last().width);
|
||||
return true;
|
||||
}
|
||||
assert(test_res == 0);
|
||||
|
||||
this->add_to_history();
|
||||
rls.cmd = std::move(to_exec);
|
||||
rls.finished = true;
|
||||
update_buff_pos(&command_line, command_line.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
|
||||
|
@ -4393,6 +4504,7 @@ maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
|
|||
|
||||
if (rls.nchars <= command_line.size()) {
|
||||
// We've already hit the specified character limit.
|
||||
rls.cmd = command_line.text();
|
||||
rls.finished = true;
|
||||
break;
|
||||
}
|
||||
|
@ -4516,8 +4628,7 @@ maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
|
|||
}
|
||||
outputter_t::stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset());
|
||||
}
|
||||
|
||||
return rls.finished ? maybe_t<wcstring>{command_line.text()} : none();
|
||||
return rls.finished ? maybe_t<wcstring>{std::move(rls.cmd)} : none();
|
||||
}
|
||||
|
||||
bool reader_data_t::jump(jump_direction_t dir, jump_precision_t precision, editable_line_t *el,
|
||||
|
|
|
@ -264,8 +264,9 @@ wcstring combine_command_and_autosuggestion(const wcstring &cmdline,
|
|||
/// Expand at most one abbreviation at the given cursor position. Use the parser to run any
|
||||
/// abbreviations which want function calls.
|
||||
/// \return none if no abbreviations were expanded, otherwise the resulting edit.
|
||||
enum class abbrs_phase_t : uint8_t;
|
||||
maybe_t<edit_t> reader_expand_abbreviation_at_cursor(const wcstring &cmdline, size_t cursor_pos,
|
||||
parser_t &parser);
|
||||
abbrs_phase_t phase, parser_t &parser);
|
||||
|
||||
/// Apply a completion string. Exposed for testing only.
|
||||
wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags,
|
||||
|
|
|
@ -159,3 +159,17 @@ abbr --add regex_name --regex 'A[0-9]B' bar
|
|||
abbr --show
|
||||
# CHECK: abbr -a -- nonregex_name foo
|
||||
# CHECK: abbr -a -- regex_name --regex 'A[0-9]B' bar
|
||||
abbr --erase (abbr --list)
|
||||
|
||||
abbr --add bogus --position never stuff
|
||||
# CHECKERR: abbr: Invalid position 'never'
|
||||
# CHECKERR: Position must be one of: command, anywhere.
|
||||
|
||||
abbr --add bogus --position anywhere --position command stuff
|
||||
# CHECKERR: abbr: Cannot specify multiple positions
|
||||
|
||||
abbr --add quiet-abbr --quiet foo1
|
||||
abbr --add loud-abbr foo2
|
||||
abbr --show
|
||||
# CHECK: abbr -a -- quiet-abbr --quiet foo1
|
||||
# CHECK: abbr -a -- loud-abbr foo2
|
||||
|
|
|
@ -68,17 +68,18 @@ send(r"A123Z ?")
|
|||
expect_str(r"<beta3 >")
|
||||
send(r"AZ ?")
|
||||
expect_str(r"<AZ >")
|
||||
send(r"QA123Z ?")
|
||||
send(r"QA123Z ?") # fails as the regex must match the entire string
|
||||
expect_str(r"<QA123Z >")
|
||||
send(r"A0000000000000000000009Z ?")
|
||||
expect_str(r"<beta3 >")
|
||||
|
||||
# Support functions. Here anything in between @@ is uppercased, except 'nope'.
|
||||
sendline(
|
||||
r"""function uppercaser;
|
||||
r"""function uppercaser
|
||||
string match --quiet '*nope*' $argv[1] && return 1
|
||||
string trim --chars @ $argv | string upper
|
||||
end"""
|
||||
end
|
||||
""".strip()
|
||||
)
|
||||
expect_prompt()
|
||||
sendline(r"abbr uppercaser --regex '@.+@' --function uppercaser")
|
||||
|
@ -100,6 +101,23 @@ expect_str(r"<@nope@ >")
|
|||
sendline(r"abbr --erase uppercaser2")
|
||||
expect_prompt()
|
||||
|
||||
# Abbreviations which cause the command line to become incomplete or invalid
|
||||
# are visibly expanded, even if they are quiet.
|
||||
sendline(r"abbr openparen --position anywhere --quiet '('")
|
||||
expect_prompt()
|
||||
sendline(r"abbr closeparen --position anywhere --quiet ')'")
|
||||
expect_prompt()
|
||||
sendline(r"echo openparen")
|
||||
expect_str(r"echo (")
|
||||
send(r"?")
|
||||
expect_str(r"<echo (>")
|
||||
sendline(r"echo closeparen")
|
||||
expect_str(r"echo )")
|
||||
send(r"?")
|
||||
expect_str(r"<echo )>")
|
||||
sendline(r"echo openparen seq 5 closeparen")
|
||||
expect_prompt(r"1 2 3 4 5")
|
||||
|
||||
# Verify that 'commandline' is accurate.
|
||||
# Abbreviation functions cannot usefully change the command line, but they can read it.
|
||||
sendline(
|
||||
|
@ -107,12 +125,30 @@ sendline(
|
|||
set -g last_cmdline (commandline)
|
||||
set -g last_cursor (commandline --cursor)
|
||||
false
|
||||
end"""
|
||||
end
|
||||
""".strip()
|
||||
)
|
||||
expect_prompt()
|
||||
sendline(r"abbr check_cmdline --regex '@.+@' --function check_cmdline")
|
||||
expect_prompt()
|
||||
send(r"@abc@ ?")
|
||||
expect_str(r"<@abc@ >")
|
||||
sendline(r"echo $last_cursor - $last_cmdline")
|
||||
expect_prompt(r"6 - @abc@ ")
|
||||
sendline(r"echo $last_cursor : $last_cmdline")
|
||||
expect_prompt(r"6 : @abc@ ")
|
||||
|
||||
|
||||
# Again but now we stack them. Noisy abbreviations expand first, then quiet ones.
|
||||
sendline(r"""function strip_percents; string trim --chars % -- $argv; end""")
|
||||
expect_prompt()
|
||||
sendline(r"""function do_upper; string upper -- $argv; end""")
|
||||
expect_prompt()
|
||||
|
||||
sendline(r"abbr noisy1 --regex '%.+%' --function strip_percents --position anywhere")
|
||||
expect_prompt()
|
||||
|
||||
sendline(r"abbr quiet1 --regex 'a.+z' --function do_upper --quiet --position anywhere")
|
||||
expect_prompt()
|
||||
# The noisy abbr strips the %
|
||||
# The quiet one sees a token starting with 'a' and ending with 'z' and uppercases it.
|
||||
sendline(r"echo %abcdez%")
|
||||
expect_prompt(r"ABCDEZ")
|
||||
|
|
Loading…
Reference in a new issue