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:
ridiculousfish 2022-08-07 13:31:10 -07:00
parent 8135c52c13
commit 1d205d0bbd
10 changed files with 384 additions and 212 deletions

View file

@ -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;
}
}

View file

@ -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

View file

@ -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':

View file

@ -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);

View file

@ -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.

View file

@ -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();

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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")