diff --git a/CMakeLists.txt b/CMakeLists.txt index ee3834c70..7fdfe9eaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,6 +118,7 @@ SET(FISH_SRCS src/wcstringutil.cpp src/wgetopt.cpp src/wildcard.cpp src/wutil.cpp src/future_feature_flags.cpp src/redirection.cpp src/topic_monitor.cpp src/flog.cpp src/trace.cpp src/timer.cpp src/null_terminated_array.cpp + src/operation_context.cpp ) # Header files are just globbed. diff --git a/src/builtin_complete.cpp b/src/builtin_complete.cpp index d12fd0da0..831c3fa51 100644 --- a/src/builtin_complete.cpp +++ b/src/builtin_complete.cpp @@ -347,8 +347,8 @@ int builtin_complete(parser_t &parser, io_streams_t &streams, wchar_t **argv) { if (!have_do_complete_param) parser.libdata().builtin_complete_current_commandline = true; - completion_list_t comp = complete(do_complete_param, completion_request_t::fuzzy_match, - parser.vars(), parser.shared()); + completion_list_t comp = + complete(do_complete_param, completion_request_t::fuzzy_match, parser.context()); for (const auto &next : comp) { // Make a fake commandline, and then apply the completion to it. @@ -392,7 +392,7 @@ int builtin_complete(parser_t &parser, io_streams_t &streams, wchar_t **argv) { if (!streams.out_is_redirected && isatty(STDOUT_FILENO)) { std::vector colors; size_t len = repr.size(); - highlight_shell_no_io(repr, colors, len, nullptr, env_stack_t::globals()); + highlight_shell_no_io(repr, colors, len, operation_context_t::globals()); streams.out.append(str2wcstring(colorize(repr, colors))); } else { streams.out.append(repr); diff --git a/src/builtin_functions.cpp b/src/builtin_functions.cpp index c1d936351..951a1d5b0 100644 --- a/src/builtin_functions.cpp +++ b/src/builtin_functions.cpp @@ -23,6 +23,7 @@ #include "function.h" #include "highlight.h" #include "io.h" +#include "parser.h" #include "parser_keywords.h" #include "proc.h" #include "signal.h" @@ -263,8 +264,9 @@ static int report_function_metadata(const wchar_t *funcname, bool verbose, io_st append_format(comment, L"# Defined in %ls @ line %d\n", path, line_number); if (!streams.out_is_redirected && isatty(STDOUT_FILENO)) { std::vector colors; - highlight_shell_no_io(comment, colors, comment.size(), nullptr, - env_stack_t::globals()); + highlight_shell_no_io( + comment, colors, comment.size(), + operation_context_t{nullptr, env_stack_t::globals(), no_cancel}); streams.out.append(str2wcstring(colorize(comment, colors))); } else { streams.out.append(comment); @@ -437,7 +439,7 @@ int builtin_functions(parser_t &parser, io_streams_t &streams, wchar_t **argv) { if (!streams.out_is_redirected && isatty(STDOUT_FILENO)) { std::vector colors; - highlight_shell_no_io(def, colors, def.size(), nullptr, env_stack_t::globals()); + highlight_shell_no_io(def, colors, def.size(), operation_context_t::globals()); streams.out.append(str2wcstring(colorize(def, colors))); } else { streams.out.append(def); diff --git a/src/builtin_read.cpp b/src/builtin_read.cpp index c3880cac7..67ac9afe8 100644 --- a/src/builtin_read.cpp +++ b/src/builtin_read.cpp @@ -208,7 +208,7 @@ static int read_interactive(parser_t &parser, wcstring &buff, int nchars, bool s reader_set_left_prompt(prompt); reader_set_right_prompt(right_prompt); if (shell) { - reader_set_complete_function(&complete); + reader_set_complete_ok(true); reader_set_highlight_function(&highlight_shell); reader_set_test_function(&reader_shell_test); } diff --git a/src/common.h b/src/common.h index 988d5b55b..d453e4b1d 100644 --- a/src/common.h +++ b/src/common.h @@ -266,6 +266,10 @@ std::shared_ptr move_to_sharedptr(T &&v) { return std::make_shared(std::move(v)); } +/// A function type to check for cancellation. +/// \return true if execution should cancel. +using cancel_checker_t = std::function; + /// Print a stack trace to stderr. void show_stackframe(const wchar_t msg_level, int frame_count = 100, int skip_levels = 0); diff --git a/src/complete.cpp b/src/complete.cpp index 1858a8973..d4058368f 100644 --- a/src/complete.cpp +++ b/src/complete.cpp @@ -311,12 +311,8 @@ void completions_sort_and_prioritize(completion_list_t *comps, completion_reques /// Class representing an attempt to compute completions. class completer_t { - /// Environment inside which we are completing. - const environment_t &vars; - - /// The parser used for condition testing and subshell expansion. - /// If null, these features are disabled. - std::shared_ptr parser; + /// The operation context for this completion. + const operation_context_t &ctx; /// The command to complete. const wcstring cmd; @@ -395,9 +391,8 @@ class completer_t { const std::vector &args); public: - completer_t(const environment_t &vars, std::shared_ptr parser, wcstring c, - completion_request_flags_t f) - : vars(vars), parser(std::move(parser)), cmd(std::move(c)), flags(f) {} + completer_t(const operation_context_t &ctx, wcstring c, completion_request_flags_t f) + : ctx(ctx), cmd(std::move(c)), flags(f) {} void perform(); @@ -421,7 +416,7 @@ bool completer_t::condition_test(const wcstring &condition) { // std::fwprintf( stderr, L"No condition specified\n" ); return true; } - if (!parser) { + if (!ctx.parser) { return false; } @@ -430,7 +425,8 @@ bool completer_t::condition_test(const wcstring &condition) { condition_cache_t::iterator cached_entry = condition_cache.find(condition); if (cached_entry == condition_cache.end()) { // Compute new value and reinsert it. - test_res = (0 == exec_subshell(condition, *parser, false /* don't apply exit status */)); + test_res = + (0 == exec_subshell(condition, *ctx.parser, false /* don't apply exit status */)); condition_cache[condition] = test_res; } else { // Use the old value. @@ -558,7 +554,7 @@ void completer_t::complete_strings(const wcstring &wc_escaped, const description wcstring tmp = wc_escaped; if (!expand_one(tmp, this->expand_flags() | expand_flag::skip_cmdsubst | expand_flag::skip_wildcards, - vars, parser)) + ctx)) return; const wcstring wc = parse_util_unescape_wildcards(tmp); @@ -576,7 +572,7 @@ void completer_t::complete_strings(const wcstring &wc_escaped, const description /// for the executable. void completer_t::complete_cmd_desc(const wcstring &str) { ASSERT_IS_MAIN_THREAD(); - if (!parser) return; + if (!ctx.parser) return; wcstring cmd; size_t pos = str.find_last_of(L'/'); @@ -619,7 +615,7 @@ void completer_t::complete_cmd_desc(const wcstring &str) { // systems with a large set of manuals, but it should be ok since apropos is only called once. lookup.clear(); list.clear(); - if (exec_subshell(lookup_cmd, *parser, list, false /* don't apply exit status */) != -1) { + if (exec_subshell(lookup_cmd, *ctx.parser, list, false /* don't apply exit status */) != -1) { // Then discard anything that is not a possible completion and put the result into a // hashtable with the completion as key and the description as value. // @@ -683,7 +679,7 @@ void completer_t::complete_cmd(const wcstring &str_cmd) { expand_string(str_cmd, &this->completions, this->expand_flags() | expand_flag::special_for_command | expand_flag::for_completions | expand_flag::executables_only, - vars, parser, nullptr); + ctx); if (result != expand_result_t::error && this->wants_descriptions()) { this->complete_cmd_desc(str_cmd); } @@ -695,7 +691,7 @@ void completer_t::complete_cmd(const wcstring &str_cmd) { expand_string( str_cmd, &this->completions, this->expand_flags() | expand_flag::for_completions | expand_flag::directories_only, - vars, parser, nullptr); + ctx); UNUSED(ignore); if (str_cmd.empty() || (str_cmd.find(L'/') == wcstring::npos && str_cmd.at(0) != L'~')) { @@ -717,7 +713,7 @@ void completer_t::complete_cmd(const wcstring &str_cmd) { } void completer_t::complete_abbr(const wcstring &cmd) { - std::map abbrs = get_abbreviations(vars); + std::map abbrs = get_abbreviations(ctx.vars); completion_list_t possible_comp; possible_comp.reserve(abbrs.size()); for (const auto &kv : abbrs) { @@ -752,9 +748,9 @@ void completer_t::complete_from_args(const wcstring &str, const wcstring &args, bool is_autosuggest = (this->type() == COMPLETE_AUTOSUGGEST); bool saved_interactive = false; - if (parser) { - saved_interactive = parser->libdata().is_interactive; - parser->libdata().is_interactive = false; + if (ctx.parser) { + saved_interactive = ctx.parser->libdata().is_interactive; + ctx.parser->libdata().is_interactive = false; } expand_flags_t eflags{}; @@ -763,10 +759,10 @@ void completer_t::complete_from_args(const wcstring &str, const wcstring &args, eflags |= expand_flag::skip_cmdsubst; } - completion_list_t possible_comp = parser_t::expand_argument_list(args, eflags, vars, parser); + completion_list_t possible_comp = parser_t::expand_argument_list(args, eflags, ctx); - if (parser) { - parser->libdata().is_interactive = saved_interactive; + if (ctx.parser) { + ctx.parser->libdata().is_interactive = saved_interactive; } this->complete_strings(escape_string(str, ESCAPE_ALL), const_desc(desc), possible_comp, flags); @@ -883,7 +879,7 @@ bool completer_t::complete_param(const wcstring &cmd_orig, const wcstring &popt, bool use_common = true, use_files = true, has_force = false; wcstring cmd, path; - parse_cmd_string(cmd_orig, &path, &cmd, vars); + parse_cmd_string(cmd_orig, &path, &cmd, ctx.vars); // mqudsi: run_on_main_thread() already just runs `func` if we're on the main thread, // but it makes a kcall to get the current thread id to ascertain that. Perhaps even @@ -909,7 +905,7 @@ bool completer_t::complete_param(const wcstring &cmd_orig, const wcstring &popt, // may be faster, path_get_path can potentially do a lot of FS/IO access, so env.get() + // function_exists() should still be faster. // Use cmd_orig here as it is potentially pathed. - head_exists = head_exists || path_get_path(cmd_orig, nullptr, vars); + head_exists = head_exists || path_get_path(cmd_orig, nullptr, ctx.vars); } if (!head_exists) { @@ -1103,7 +1099,7 @@ bool completer_t::complete_param(const wcstring &cmd_orig, const wcstring &popt, /// Perform generic (not command-specific) expansions on the specified string. void completer_t::complete_param_expand(const wcstring &str, bool do_file, bool handle_as_special_cd) { - if (reader_test_should_cancel()) return; + if (ctx.check_cancel()) return; expand_flags_t flags = this->expand_flags() | expand_flag::skip_cmdsubst | expand_flag::for_completions; @@ -1137,8 +1133,7 @@ void completer_t::complete_param_expand(const wcstring &str, bool do_file, // See #4954. const wcstring sep_string = wcstring(str, sep_index + 1); completion_list_t local_completions; - if (expand_string(sep_string, &local_completions, flags, vars, parser, nullptr) == - expand_result_t::error) { + if (expand_string(sep_string, &local_completions, flags, ctx) == expand_result_t::error) { debug(3, L"Error while expanding string '%ls'", sep_string.c_str()); } @@ -1157,8 +1152,7 @@ void completer_t::complete_param_expand(const wcstring &str, bool do_file, // consider relaxing this if there was a preceding double-dash argument. if (string_prefixes_string(L"-", str)) flags.clear(expand_flag::fuzzy_match); - if (expand_string(str, &this->completions, flags, vars, parser, nullptr) == - expand_result_t::error) { + if (expand_string(str, &this->completions, flags, ctx) == expand_result_t::error) { debug(3, L"Error while expanding string '%ls'", str.c_str()); } } @@ -1171,7 +1165,7 @@ bool completer_t::complete_variable(const wcstring &str, size_t start_offset) { size_t varlen = str.length() - start_offset; bool res = false; - for (const wcstring &env_name : vars.get_names(0)) { + for (const wcstring &env_name : ctx.vars.get_names(0)) { string_fuzzy_match_t match = string_fuzzy_match_string(var, env_name, this->max_fuzzy_match_type()); if (match.type == fuzzy_match_none) { @@ -1197,14 +1191,14 @@ bool completer_t::complete_variable(const wcstring &str, size_t start_offset) { // #6288. if (env_name == L"history") { history_t *history = - &history_t::history_with_name(history_session_id(parser->vars())); + &history_t::history_with_name(history_session_id(ctx.vars)); for (size_t i = 1; i < history->size() && desc.size() < 64; i++) { if (i > 1) desc += L' '; desc += expand_escape_string(history->item_at_index(i).str()); } } else { - // Can't use this->vars here, it could be any variable. - auto var = vars.get(env_name); + // Can't use ctx.vars here, it could be any variable. + auto var = ctx.vars.get(env_name); if (!var) continue; wcstring value = expand_escape_variable(*var); @@ -1311,7 +1305,7 @@ bool completer_t::try_complete_user(const wcstring &str) { setpwent(); // cppcheck-suppress getpwentCalled while (struct passwd *pw = getpwent()) { - if (reader_test_should_cancel()) { + if (ctx.check_cancel()) { break; } const wcstring pw_name_str = str2wcstring(pw->pw_name); @@ -1349,6 +1343,7 @@ using wrap_chain_visited_set_t = std::set>; // Recursive implementation of walk_wrap_chain(). static void walk_wrap_chain_recursive(const wcstring &command_line, source_range_t command_range, const wrap_chain_visitor_t &visitor, + const cancel_checker_t &cancel_checker, wrap_chain_visited_set_t *visited, size_t depth) { // Limit our recursion depth. This prevents cycles in the wrap chain graph from overflowing. if (depth > 24) return; @@ -1377,8 +1372,8 @@ static void walk_wrap_chain_recursive(const wcstring &command_line, source_range // Recurse with our new command and command line. source_range_t faux_source_range{uint32_t(where), uint32_t(wrapped_command.size())}; - walk_wrap_chain_recursive(faux_commandline, faux_source_range, visitor, visited, - depth + 1); + walk_wrap_chain_recursive(faux_commandline, faux_source_range, visitor, + cancel_checker, visited, depth + 1); } } } @@ -1391,9 +1386,10 @@ static void walk_wrap_chain_recursive(const wcstring &command_line, source_range // target wrapped by the given command, update the command line with that target and invoke this // recursively. static void walk_wrap_chain(const wcstring &command_line, source_range_t command_range, - const wrap_chain_visitor_t &visitor) { + const wrap_chain_visitor_t &visitor, + const cancel_checker_t &cancel_checker) { wrap_chain_visited_set_t visited; - walk_wrap_chain_recursive(command_line, command_range, visitor, &visited, 0); + walk_wrap_chain_recursive(command_line, command_range, visitor, cancel_checker, &visited, 0); } /// If the argument contains a '[' typed by the user, completion by appending to the argument might @@ -1574,31 +1570,29 @@ void completer_t::perform() { // we're doing autosuggestions. bool wants_transient = depth > 0 && !(flags & completion_request_t::autosuggestion); if (wants_transient) { - parser->libdata().transient_commandlines.push_back(cmdline); + ctx.parser->libdata().transient_commandlines.push_back(cmdline); } bool is_variable_assignment = bool(variable_assignment_equals_pos(cmd)); - if (is_variable_assignment && parser) { + if (is_variable_assignment && ctx.parser) { // To avoid issues like #2705 we complete commands starting with variable // assignments by recursively calling complete for the command suffix // without the first variable assignment token. wcstring unaliased_cmd; - if (parser->libdata().transient_commandlines.empty()) { + if (ctx.parser->libdata().transient_commandlines.empty()) { unaliased_cmd = cmdline; } else { - unaliased_cmd = parser->libdata().transient_commandlines.back(); + unaliased_cmd = ctx.parser->libdata().transient_commandlines.back(); } tokenizer_t tok(unaliased_cmd.c_str(), TOK_ACCEPT_UNFINISHED); maybe_t cmd_tok = tok.next(); assert(cmd_tok); unaliased_cmd = unaliased_cmd.replace(0, cmd_tok->offset + cmd_tok->length, L""); - parser->libdata().transient_commandlines.push_back(unaliased_cmd); + ctx.parser->libdata().transient_commandlines.push_back(unaliased_cmd); cleanup_t remove_transient( - [&] { parser->libdata().transient_commandlines.pop_back(); }); - completion_list_t comp = - complete(unaliased_cmd, completion_request_t::fuzzy_match, parser->vars(), - parser->shared()); - this->completions.insert(completions.end(), comp.begin(), comp.end()); + [&] { ctx.parser->libdata().transient_commandlines.pop_back(); }); + vec_append(this->completions, + complete(unaliased_cmd, completion_request_t::fuzzy_match, ctx)); do_file = false; } else if (!complete_param( cmd, previous_argument_unescape, current_argument_unescape, @@ -1606,14 +1600,14 @@ void completer_t::perform() { do_file = false; } if (wants_transient) { - parser->libdata().transient_commandlines.pop_back(); + ctx.parser->libdata().transient_commandlines.pop_back(); } }; assert(cmd_tok.offset < std::numeric_limits::max()); assert(cmd_tok.length < std::numeric_limits::max()); source_range_t range = {static_cast(cmd_tok.offset), static_cast(cmd_tok.length)}; - walk_wrap_chain(cmd, range, receiver); + walk_wrap_chain(cmd, range, receiver, ctx.cancel_checker); } // Hack. If we're cd, handle it specially (issue #1059, others). @@ -1636,14 +1630,14 @@ void completer_t::perform() { } completion_list_t complete(const wcstring &cmd_with_subcmds, completion_request_flags_t flags, - const environment_t &vars, const std::shared_ptr &parser) { + const operation_context_t &ctx) { // Determine the innermost subcommand. const wchar_t *cmdsubst_begin, *cmdsubst_end; parse_util_cmdsubst_extent(cmd_with_subcmds.c_str(), cmd_with_subcmds.size(), &cmdsubst_begin, &cmdsubst_end); assert(cmdsubst_begin != nullptr && cmdsubst_end != nullptr && cmdsubst_end >= cmdsubst_begin); wcstring cmd = wcstring(cmdsubst_begin, cmdsubst_end - cmdsubst_begin); - completer_t completer(vars, parser, std::move(cmd), flags); + completer_t completer(ctx, std::move(cmd), flags); completer.perform(); return completer.acquire_completions(); } diff --git a/src/complete.h b/src/complete.h index 7a1e69bfe..d7849eeba 100644 --- a/src/complete.h +++ b/src/complete.h @@ -117,6 +117,9 @@ struct enum_info_t { using completion_request_flags_t = enum_set_t; +class completion_t; +using completion_list_t = std::vector; + enum complete_option_type_t { option_type_args_only, // no option option_type_short, // -x @@ -172,9 +175,9 @@ void complete_remove(const wcstring &cmd, bool cmd_is_path, const wcstring &opti void complete_remove_all(const wcstring &cmd, bool cmd_is_path); /// \return all completions of the command cmd. -class parser_t; +class operation_context_t; completion_list_t complete(const wcstring &cmd, completion_request_flags_t flags, - const environment_t &vars, const std::shared_ptr &parser); + const operation_context_t &ctx); /// Return a list of all current completions. wcstring complete_print(); diff --git a/src/expand.cpp b/src/expand.cpp index ee342e29f..e76227e10 100644 --- a/src/expand.cpp +++ b/src/expand.cpp @@ -850,11 +850,8 @@ static void remove_internal_separator(wcstring *str, bool conv) { namespace { /// A type that knows how to perform expansions. class expander_t { - /// Variables to use in expansion. - const environment_t &vars; - - /// The parser for expanding command substitutions, or nullptr if not enabled. - std::shared_ptr parser; + /// Operation context for this expansion. + const operation_context_t &ctx; /// Flags to use during expansion. const expand_flags_t flags; @@ -873,14 +870,12 @@ class expander_t { expand_result_t stage_home_and_self(wcstring input, completion_list_t *out); expand_result_t stage_wildcards(wcstring path_to_expand, completion_list_t *out); - expander_t(const environment_t &vars, std::shared_ptr parser, expand_flags_t flags, - parse_error_list_t *errors) - : vars(vars), parser(std::move(parser)), flags(flags), errors(errors) {} + expander_t(const operation_context_t &ctx, expand_flags_t flags, parse_error_list_t *errors) + : ctx(ctx), flags(flags), errors(errors) {} public: static expand_result_t expand_string(wcstring input, completion_list_t *out_completions, - expand_flags_t flags, const environment_t &vars, - const std::shared_ptr &parser, + expand_flags_t flags, const operation_context_t &ctx, parse_error_list_t *errors); }; @@ -899,8 +894,8 @@ expand_result_t expander_t::stage_cmdsubst(wcstring input, completion_list_t *ou return expand_result_t::error; } } else { - assert(parser && "Must have a parser to expand command substitutions"); - bool cmdsubst_ok = expand_cmdsubst(std::move(input), *parser, out, errors); + assert(ctx.parser && "Must have a parser to expand command substitutions"); + bool cmdsubst_ok = expand_cmdsubst(std::move(input), *ctx.parser, out, errors); if (!cmdsubst_ok) return expand_result_t::error; } @@ -922,7 +917,7 @@ expand_result_t expander_t::stage_variables(wcstring input, completion_list_t *o append_completion(out, std::move(next)); } else { size_t size = next.size(); - if (!expand_variables(std::move(next), out, size, vars, errors)) { + if (!expand_variables(std::move(next), out, size, ctx.vars, errors)) { return expand_result_t::error; } } @@ -930,13 +925,12 @@ expand_result_t expander_t::stage_variables(wcstring input, completion_list_t *o } expand_result_t expander_t::stage_braces(wcstring input, completion_list_t *out) { - UNUSED(vars); return expand_braces(input, flags, out, errors); } expand_result_t expander_t::stage_home_and_self(wcstring input, completion_list_t *out) { if (!(flags & expand_flag::skip_home_directories)) { - expand_home_directory(input, vars); + expand_home_directory(input, ctx.vars); } expand_percent_self(input); append_completion(out, std::move(input)); @@ -961,7 +955,7 @@ expand_result_t expander_t::stage_wildcards(wcstring path_to_expand, completion_ // // So we're going to treat this input as a file path. Compute the "working directories", // which may be CDPATH if the special flag is set. - const wcstring working_dir = vars.get_pwd_slash(); + const wcstring working_dir = ctx.vars.get_pwd_slash(); wcstring_list_t effective_working_dirs; bool for_cd = flags & expand_flag::special_for_cd; bool for_command = flags & expand_flag::special_for_command; @@ -993,7 +987,7 @@ expand_result_t expander_t::stage_wildcards(wcstring path_to_expand, completion_ // Get the PATH/CDPATH and CWD. Perhaps these should be passed in. An empty CDPATH // implies just the current directory, while an empty PATH is left empty. wcstring_list_t paths; - if (auto paths_var = vars.get(for_cd ? L"CDPATH" : L"PATH")) { + if (auto paths_var = ctx.vars.get(for_cd ? L"CDPATH" : L"PATH")) { paths = paths_var->as_list(); } if (paths.empty()) { @@ -1009,8 +1003,8 @@ expand_result_t expander_t::stage_wildcards(wcstring path_to_expand, completion_ result = expand_result_t::wildcard_no_match; completion_list_t expanded; for (const auto &effective_working_dir : effective_working_dirs) { - wildcard_expand_result_t expand_res = - wildcard_expand_string(path_to_expand, effective_working_dir, flags, &expanded); + wildcard_expand_result_t expand_res = wildcard_expand_string( + path_to_expand, effective_working_dir, flags, ctx.cancel_checker, &expanded); switch (expand_res) { case wildcard_expand_result_t::match: // Something matched,so overall we matched. @@ -1038,10 +1032,9 @@ expand_result_t expander_t::stage_wildcards(wcstring path_to_expand, completion_ } expand_result_t expander_t::expand_string(wcstring input, completion_list_t *out_completions, - expand_flags_t flags, const environment_t &vars, - const std::shared_ptr &parser, + expand_flags_t flags, const operation_context_t &ctx, parse_error_list_t *errors) { - assert(((flags & expand_flag::skip_cmdsubst) || parser) && + assert(((flags & expand_flag::skip_cmdsubst) || ctx.parser) && "Must have a parser if not skipping command substitutions"); // Early out. If we're not completing, and there's no magic in the input, we're done. if (!(flags & expand_flag::for_completions) && expand_is_clean(input)) { @@ -1049,7 +1042,7 @@ expand_result_t expander_t::expand_string(wcstring input, completion_list_t *out return expand_result_t::ok; } - expander_t expand(vars, parser, flags, errors); + expander_t expand(ctx, flags, errors); // Our expansion stages. const stage_t stages[] = {&expander_t::stage_cmdsubst, &expander_t::stage_variables, @@ -1087,7 +1080,7 @@ expand_result_t expander_t::expand_string(wcstring input, completion_list_t *out if (total_result != expand_result_t::error) { // Hack to un-expand tildes (see #647). if (!(flags & expand_flag::skip_home_directories)) { - unexpand_tildes(input, vars, &completions); + unexpand_tildes(input, ctx.vars, &completions); } out_completions->insert(out_completions->end(), std::make_move_iterator(completions.begin()), @@ -1098,21 +1091,20 @@ expand_result_t expander_t::expand_string(wcstring input, completion_list_t *out } // namespace expand_result_t expand_string(wcstring input, completion_list_t *out_completions, - expand_flags_t flags, const environment_t &vars, - const shared_ptr &parser, parse_error_list_t *errors) { - return expander_t::expand_string(std::move(input), out_completions, flags, vars, parser, - errors); + expand_flags_t flags, const operation_context_t &ctx, + parse_error_list_t *errors) { + return expander_t::expand_string(std::move(input), out_completions, flags, ctx, errors); } -bool expand_one(wcstring &string, expand_flags_t flags, const environment_t &vars, - const shared_ptr &parser, parse_error_list_t *errors) { +bool expand_one(wcstring &string, expand_flags_t flags, const operation_context_t &ctx, + parse_error_list_t *errors) { completion_list_t completions; if (!flags.get(expand_flag::for_completions) && expand_is_clean(string)) { return true; } - if (expand_string(string, &completions, flags | expand_flag::no_descriptions, vars, parser, + if (expand_string(std::move(string), &completions, flags | expand_flag::no_descriptions, ctx, errors) != expand_result_t::error && completions.size() == 1) { string = std::move(completions.at(0).completion); @@ -1121,7 +1113,7 @@ bool expand_one(wcstring &string, expand_flags_t flags, const environment_t &var return false; } -expand_result_t expand_to_command_and_args(const wcstring &instr, const environment_t &vars, +expand_result_t expand_to_command_and_args(const wcstring &instr, const operation_context_t &ctx, wcstring *out_cmd, wcstring_list_t *out_args, parse_error_list_t *errors) { // Fast path. @@ -1133,8 +1125,8 @@ expand_result_t expand_to_command_and_args(const wcstring &instr, const environm completion_list_t completions; expand_result_t expand_err = expand_string( instr, &completions, - {expand_flag::skip_cmdsubst, expand_flag::no_descriptions, expand_flag::skip_jobs}, vars, - nullptr, errors); + {expand_flag::skip_cmdsubst, expand_flag::no_descriptions, expand_flag::skip_jobs}, ctx, + errors); if (expand_err == expand_result_t::ok || expand_err == expand_result_t::wildcard_match) { // The first completion is the command, any remaning are arguments. bool first = true; diff --git a/src/expand.h b/src/expand.h index 6f7e56e27..221bffec9 100644 --- a/src/expand.h +++ b/src/expand.h @@ -21,6 +21,7 @@ class environment_t; class env_var_t; class environment_t; +class operation_context_t; /// Set of flags controlling expansions. enum class expand_flag { @@ -125,18 +126,15 @@ enum class expand_result_t { /// \param output The list to which the result will be appended. /// \param flags Specifies if any expansion pass should be skipped. Legal values are any combination /// of skip_cmdsubst skip_variables and skip_wildcards -/// \param vars variables used during expansion. -/// \param parser the parser to use for command substitutions, or nullptr to disable. -/// \param errors Resulting errors, or NULL to ignore +/// \param ctx The parser, variables, and cancellation checker for this operation. The parser may +/// be null. \param errors Resulting errors, or nullptr to ignore /// /// \return An expand_result_t. /// wildcard_no_match and wildcard_match are normal exit conditions used only on /// strings containing wildcards to tell if the wildcard produced any matches. -class parser_t; __warn_unused expand_result_t expand_string(wcstring input, completion_list_t *output, - expand_flags_t flags, const environment_t &vars, - const std::shared_ptr &parser, - parse_error_list_t *errors); + expand_flags_t flags, const operation_context_t &ctx, + parse_error_list_t *errors = nullptr); /// expand_one is identical to expand_string, except it will fail if in expands to more than one /// string. This is used for expanding command names. @@ -144,12 +142,12 @@ __warn_unused expand_result_t expand_string(wcstring input, completion_list_t *o /// \param inout_str The parameter to expand in-place /// \param flags Specifies if any expansion pass should be skipped. Legal values are any combination /// of skip_cmdsubst skip_variables and skip_wildcards -/// \param parser the parser to use for command substitutions, or nullptr to disable. -/// \param errors Resulting errors, or NULL to ignore +/// \param ctx The parser, variables, and cancellation checker for this operation. The parser may be +/// null. \param errors Resulting errors, or nullptr to ignore /// /// \return Whether expansion succeeded. -bool expand_one(wcstring &string, expand_flags_t flags, const environment_t &vars, - const std::shared_ptr &parser, parse_error_list_t *errors = nullptr); +bool expand_one(wcstring &string, expand_flags_t flags, const operation_context_t &ctx, + parse_error_list_t *errors = nullptr); /// Expand a command string like $HOME/bin/cmd into a command and list of arguments. /// Return the command and arguments by reference. @@ -157,7 +155,7 @@ bool expand_one(wcstring &string, expand_flags_t flags, const environment_t &var /// that API does not distinguish between expansion resulting in an empty command (''), and /// expansion resulting in no command (e.g. unset variable). // \return an expand error. -expand_result_t expand_to_command_and_args(const wcstring &instr, const environment_t &vars, +expand_result_t expand_to_command_and_args(const wcstring &instr, const operation_context_t &ctx, wcstring *out_cmd, wcstring_list_t *out_args, parse_error_list_t *errors = nullptr); diff --git a/src/fish_indent.cpp b/src/fish_indent.cpp index 8d23f8142..35a2546b0 100644 --- a/src/fish_indent.cpp +++ b/src/fish_indent.cpp @@ -38,6 +38,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA #include "env.h" #include "fish_version.h" #include "highlight.h" +#include "operation_context.h" #include "output.h" #include "parse_constants.h" #include "print_help.h" @@ -313,7 +314,7 @@ static const char *highlight_role_to_string(highlight_role_t role) { static std::string make_pygments_csv(const wcstring &src) { const size_t len = src.size(); std::vector colors; - highlight_shell_no_io(src, colors, src.size(), nullptr, env_stack_t::globals()); + highlight_shell_no_io(src, colors, src.size(), operation_context_t::globals()); assert(colors.size() == len && "Colors and src should have same size"); struct token_range_t { @@ -633,8 +634,8 @@ int main(int argc, char *argv[]) { // Maybe colorize. std::vector colors; if (output_type != output_type_plain_text) { - highlight_shell_no_io(output_wtext, colors, output_wtext.size(), nullptr, - env_stack_t::globals()); + highlight_shell_no_io(output_wtext, colors, output_wtext.size(), + operation_context_t::globals()); } std::string colored_output; diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index c9e265d0f..d3ecdfaf6 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -59,6 +59,7 @@ #include "iothread.h" #include "lru.h" #include "maybe.h" +#include "operation_context.h" #include "pager.h" #include "parse_constants.h" #include "parse_tree.h" @@ -1015,8 +1016,8 @@ static void test_parser() { parser->eval(L"function '' ; echo fail; exit 42 ; end ; ''", io_chain_t()); say(L"Testing eval_args"); - completion_list_t comps = parser_t::expand_argument_list( - L"alpha 'beta gamma' delta", expand_flags_t{}, parser->vars(), parser); + completion_list_t comps = parser_t::expand_argument_list(L"alpha 'beta gamma' delta", + expand_flags_t{}, parser->context()); do_test(comps.size() == 3); do_test(comps.at(0).completion == L"alpha"); do_test(comps.at(1).completion == L"beta gamma"); @@ -1083,6 +1084,7 @@ static void test_cancellation() { // Ensure that we don't think we should cancel. reader_reset_interrupted(); + parser.clear_cancel(); } static void test_indents() { @@ -1644,14 +1646,14 @@ static bool expand_test(const wchar_t *in, expand_flags_t flags, ...) { bool res = true; wchar_t *arg; parse_error_list_t errors; - auto parser = parser_t::principal_parser().shared(); + pwd_environment_t pwd{}; + operation_context_t ctx{parser_t::principal_parser().shared(), pwd, no_cancel}; - if (expand_string(in, &output, flags, pwd_environment_t{}, parser, &errors) == - expand_result_t::error) { + if (expand_string(in, &output, flags, ctx, &errors) == expand_result_t::error) { if (errors.empty()) { err(L"Bug: Parse error reported but no error text found."); } else { - err(L"%ls", errors.at(0).describe(in, parser->is_interactive()).c_str()); + err(L"%ls", errors.at(0).describe(in, ctx.parser->is_interactive()).c_str()); } return false; } @@ -2311,8 +2313,9 @@ static bool run_test_test(int expected, const wcstring &str) { // We need to tokenize the string in the same manner a normal shell would do. This is because we // need to test things like quoted strings that have leading and trailing whitespace. auto parser = parser_t::principal_parser().shared(); - completion_list_t comps = - parser_t::expand_argument_list(str, expand_flags_t{}, null_environment_t{}, parser); + null_environment_t nullenv{}; + operation_context_t ctx{parser, nullenv, no_cancel}; + completion_list_t comps = parser_t::expand_argument_list(str, expand_flags_t{}, ctx); wcstring_list_t argv; for (const auto &c : comps) { @@ -2613,7 +2616,7 @@ static void test_complete() { auto parser = parser_t::principal_parser().shared(); auto do_complete = [&](const wcstring &cmd, completion_request_flags_t flags) { - return complete(cmd, flags, vars, parser); + return complete(cmd, flags, operation_context_t{parser, vars, no_cancel}); }; completion_list_t completions; @@ -2787,7 +2790,7 @@ static void test_complete() { auto &pvars = parser_t::principal_parser().vars(); function_add(L"testabbrsonetwothreefour", {}, nullptr, {}); int ret = pvars.set_one(L"_fish_abbr_testabbrsonetwothreezero", ENV_LOCAL, L"expansion"); - completions = complete(L"testabbrsonetwothree", {}, pvars, parser); + completions = complete(L"testabbrsonetwothree", {}, parser->context()); do_test(ret == 0); do_test(completions.size() == 2); do_test(completions.at(0).completion == L"four"); @@ -2872,7 +2875,7 @@ static void test_completion_insertions() { static void perform_one_autosuggestion_cd_test(const wcstring &command, const wcstring &expected, const environment_t &vars, long line) { completion_list_t comps = - complete(command, completion_request_t::autosuggestion, vars, nullptr); + complete(command, completion_request_t::autosuggestion, operation_context_t{vars}); bool expects_error = (expected == L""); @@ -2907,7 +2910,7 @@ static void perform_one_autosuggestion_cd_test(const wcstring &command, const wc static void perform_one_completion_cd_test(const wcstring &command, const wcstring &expected, const environment_t &vars, long line) { - completion_list_t comps = complete(command, {}, vars, nullptr); + completion_list_t comps = complete(command, {}, operation_context_t{vars}); bool expects_error = (expected == L""); @@ -3047,7 +3050,7 @@ static void test_autosuggest_suggest_special() { static void perform_one_autosuggestion_should_ignore_test(const wcstring &command, long line) { completion_list_t comps = - complete(command, completion_request_t::autosuggestion, null_environment_t{}, nullptr); + complete(command, completion_request_t::autosuggestion, operation_context_t::empty()); do_test(comps.empty()); if (!comps.empty()) { const wcstring &suggestion = comps.front().completion; @@ -4647,7 +4650,7 @@ static void test_highlighting() { do_test(expected_colors.size() == text.size()); std::vector colors(text.size()); - highlight_shell(text, colors, 20, NULL, vars); + highlight_shell(text, colors, 20, operation_context_t{vars}); if (expected_colors.size() != colors.size()) { err(L"Color vector has wrong size! Expected %lu, actual %lu", expected_colors.size(), diff --git a/src/highlight.cpp b/src/highlight.cpp index 102648ecf..a15fb732d 100644 --- a/src/highlight.cpp +++ b/src/highlight.cpp @@ -29,6 +29,7 @@ #include "output.h" #include "parse_constants.h" #include "parse_util.h" +#include "parser.h" #include "path.h" #include "tnode.h" #include "tokenizer.h" @@ -340,11 +341,12 @@ static bool is_potential_cd_path(const wcstring &path, const wcstring &working_d // appropriately for commands. If we succeed, return true. static bool plain_statement_get_expanded_command(const wcstring &src, tnode_t stmt, - const environment_t &vars, wcstring *out_cmd) { + const operation_context_t &ctx, + wcstring *out_cmd) { // Get the command. Try expanding it. If we cannot, it's an error. maybe_t cmd = command_for_plain_statement(stmt, src); if (!cmd) return false; - expand_result_t err = expand_to_command_and_args(*cmd, vars, out_cmd, nullptr); + expand_result_t err = expand_to_command_and_args(*cmd, ctx, out_cmd, nullptr); return err == expand_result_t::ok || err == expand_result_t::wildcard_match; } @@ -396,7 +398,7 @@ static bool has_expand_reserved(const wcstring &str) { // Parse a command line. Return by reference the first command, and the first argument to that // command (as a string), if any. This is used to validate autosuggestions. -static bool autosuggest_parse_command(const wcstring &buff, const environment_t &vars, +static bool autosuggest_parse_command(const wcstring &buff, const operation_context_t &ctx, wcstring *out_expanded_command, wcstring *out_arg) { // Parse the buffer. parse_node_tree_t parse_tree; @@ -414,7 +416,7 @@ static bool autosuggest_parse_command(const wcstring &buff, const environment_t } if (first_statement && - plain_statement_get_expanded_command(buff, first_statement, vars, out_expanded_command)) { + plain_statement_get_expanded_command(buff, first_statement, ctx, out_expanded_command)) { // Find the first argument. auto args_and_redirs = first_statement.child<1>(); if (auto arg = args_and_redirs.next_in_list()) { @@ -427,7 +429,7 @@ static bool autosuggest_parse_command(const wcstring &buff, const environment_t bool autosuggest_validate_from_history(const history_item_t &item, const wcstring &working_directory, - const environment_t &vars) { + const operation_context_t &ctx) { ASSERT_IS_BACKGROUND_THREAD(); bool handled = false, suggestionOK = false; @@ -435,16 +437,16 @@ bool autosuggest_validate_from_history(const history_item_t &item, // Parse the string. wcstring parsed_command; wcstring cd_dir; - if (!autosuggest_parse_command(item.str(), vars, &parsed_command, &cd_dir)) return false; + if (!autosuggest_parse_command(item.str(), ctx, &parsed_command, &cd_dir)) return false; if (parsed_command == L"cd" && !cd_dir.empty()) { // We can possibly handle this specially. - if (expand_one(cd_dir, expand_flag::skip_cmdsubst, vars, nullptr)) { + if (expand_one(cd_dir, expand_flag::skip_cmdsubst, ctx)) { handled = true; bool is_help = string_prefixes_string(cd_dir, L"--help") || string_prefixes_string(cd_dir, L"-h"); if (!is_help) { - auto path = path_get_cdpath(cd_dir, working_directory, vars); + auto path = path_get_cdpath(cd_dir, working_directory, ctx.vars); if (path && !paths_are_same_file(working_directory, *path)) { suggestionOK = true; } @@ -458,7 +460,7 @@ bool autosuggest_validate_from_history(const history_item_t &item, // Not handled specially so handle it here. bool cmd_ok = false; - if (path_get_path(parsed_command, nullptr, vars)) { + if (path_get_path(parsed_command, nullptr, ctx.vars)) { cmd_ok = true; } else if (builtin_exists(parsed_command) || function_exists_no_autoload(parsed_command)) { cmd_ok = true; @@ -768,8 +770,8 @@ class highlighter_t { const wcstring &buff; // Cursor position. const size_t cursor_pos; - // Environment variables. Again, a reference member variable! - const environment_t &vars; + // The operation context. Again, a reference member variable! + const operation_context_t &ctx; // Whether it's OK to do I/O. const bool io_ok; // Working directory. @@ -800,11 +802,11 @@ class highlighter_t { public: // Constructor - highlighter_t(const wcstring &str, size_t pos, const environment_t &ev, wcstring wd, + highlighter_t(const wcstring &str, size_t pos, const operation_context_t &ctx, wcstring wd, bool can_do_io) : buff(str), cursor_pos(pos), - vars(ev), + ctx(ctx), io_ok(can_do_io), working_directory(std::move(wd)), color_array(str.size()) { @@ -891,7 +893,7 @@ void highlighter_t::color_argument(tnode_t node) { } // Highlight it recursively. - highlighter_t cmdsub_highlighter(cmdsub_contents, cursor_subpos, this->vars, + highlighter_t cmdsub_highlighter(cmdsub_contents, cursor_subpos, this->ctx, this->working_directory, this->io_ok); const color_array_t &subcolors = cmdsub_highlighter.highlight(); @@ -927,7 +929,7 @@ bool highlighter_t::is_cd(tnode_t stmt) const { bool cmd_is_cd = false; if (this->io_ok && stmt.has_source()) { wcstring cmd_str; - if (plain_statement_get_expanded_command(this->buff, stmt, vars, &cmd_str)) { + if (plain_statement_get_expanded_command(this->buff, stmt, ctx, &cmd_str)) { cmd_is_cd = (cmd_str == L"cd"); } } @@ -944,11 +946,11 @@ void highlighter_t::color_arguments(const std::vector> &arg if (cmd_is_cd) { // Mark this as an error if it's not 'help' and not a valid cd path. wcstring param = arg.get_source(this->buff); - if (expand_one(param, expand_flag::skip_cmdsubst, vars, nullptr)) { + if (expand_one(param, expand_flag::skip_cmdsubst, ctx)) { bool is_help = string_prefixes_string(param, L"--help") || string_prefixes_string(param, L"-h"); if (!is_help && this->io_ok && - !is_potential_cd_path(param, working_directory, vars, PATH_EXPAND_TILDE)) { + !is_potential_cd_path(param, working_directory, ctx.vars, PATH_EXPAND_TILDE)) { this->color_node(arg, highlight_role_t::error); } } @@ -988,7 +990,7 @@ void highlighter_t::color_redirection(tnode_t redirection_node) // I/O is disallowed, so we don't have much hope of catching anything but gross // errors. Assume it's valid. target_is_valid = true; - } else if (!expand_one(target, expand_flag::skip_cmdsubst, vars, nullptr)) { + } else if (!expand_one(target, expand_flag::skip_cmdsubst, ctx)) { // Could not be expanded. target_is_valid = false; } else { @@ -1239,10 +1241,10 @@ highlighter_t::color_array_t highlighter_t::highlight() { // Check to see if the command is valid. // Try expanding it. If we cannot, it's an error. bool expanded = - plain_statement_get_expanded_command(buff, stmt, vars, &expanded_cmd); + plain_statement_get_expanded_command(buff, stmt, ctx, &expanded_cmd); if (expanded && !has_expand_reserved(expanded_cmd)) { is_valid_cmd = - command_is_valid(expanded_cmd, decoration, working_directory, vars); + command_is_valid(expanded_cmd, decoration, working_directory, ctx.vars); } } if (!is_valid_cmd) { @@ -1298,7 +1300,7 @@ highlighter_t::color_array_t highlighter_t::highlight() { if (node.type != symbol_argument || !node.has_source()) continue; // Underline every valid path. - if (node_is_potential_path(buff, node, vars, working_directory)) { + if (node_is_potential_path(buff, node, ctx.vars, working_directory)) { // It is, underline it. for (size_t i = node.source_start; i < node.source_start + node.source_length; i++) { // Don't color highlight_role_t::error because it looks dorky. For example, @@ -1333,26 +1335,16 @@ std::string colorize(const wcstring &text, const std::vector & } void highlight_shell(const wcstring &buff, std::vector &color, size_t pos, - wcstring_list_t *error, const environment_t &vars) { - UNUSED(error); - // Do something sucky and get the current working directory on this background thread. This - // should really be passed in. - const wcstring working_directory = vars.get_pwd_slash(); - - // Highlight it! - highlighter_t highlighter(buff, pos, vars, working_directory, true /* can do IO */); + const operation_context_t &ctx) { + const wcstring working_directory = ctx.vars.get_pwd_slash(); + highlighter_t highlighter(buff, pos, ctx, working_directory, true /* can do IO */); color = highlighter.highlight(); } void highlight_shell_no_io(const wcstring &buff, std::vector &color, size_t pos, - wcstring_list_t *error, const environment_t &vars) { - UNUSED(error); - // Do something sucky and get the current working directory on this background thread. This - // should really be passed in. - const wcstring working_directory = vars.get_pwd_slash(); - - // Highlight it! - highlighter_t highlighter(buff, pos, vars, working_directory, false /* no IO allowed */); + const operation_context_t &ctx) { + const wcstring working_directory = ctx.vars.get_pwd_slash(); + highlighter_t highlighter(buff, pos, ctx, working_directory, false /* no IO allowed */); color = highlighter.highlight(); } @@ -1442,9 +1434,8 @@ static void highlight_universal_internal(const wcstring &buffstr, } void highlight_universal(const wcstring &buff, std::vector &color, size_t pos, - wcstring_list_t *error, const environment_t &vars) { - UNUSED(error); - UNUSED(vars); + const operation_context_t &ctx) { + UNUSED(ctx); assert(buff.size() == color.size()); std::fill(color.begin(), color.end(), highlight_spec_t{}); highlight_universal_internal(buff, color, pos); diff --git a/src/highlight.h b/src/highlight.h index 79cc2e846..43c5588da 100644 --- a/src/highlight.h +++ b/src/highlight.h @@ -71,6 +71,7 @@ struct highlight_spec_t { }; class history_item_t; +class operation_context_t; std::string colorize(const wcstring &text, const std::vector &colors); @@ -81,15 +82,14 @@ std::string colorize(const wcstring &text, const std::vector & /// \param color The array in which to store the color codes. The first 8 bits are used for fg /// color, the next 8 bits for bg color. /// \param pos the cursor position. Used for quote matching, etc. -/// \param error a list in which a description of each error will be inserted. May be 0, in whcich -/// case no error descriptions will be generated. +/// \param ctx The variables and cancellation check for this operation. void highlight_shell(const wcstring &buffstr, std::vector &color, size_t pos, - wcstring_list_t *error, const environment_t &vars); + const operation_context_t &ctx); /// Perform a non-blocking shell highlighting. The function will not do any I/O that may block. As a /// result, invalid commands may not be detected, etc. void highlight_shell_no_io(const wcstring &buffstr, std::vector &color, - size_t pos, wcstring_list_t *error, const environment_t &vars); + size_t pos, const operation_context_t &ctx); /// Perform syntax highlighting for the text in buff. Matching quotes and parenthesis are /// highlighted. The result is stored in the color array as a color_code from the HIGHLIGHT_ enum @@ -99,10 +99,9 @@ void highlight_shell_no_io(const wcstring &buffstr, std::vector &color, size_t pos, - wcstring_list_t *error, const environment_t &vars); + const operation_context_t &ctx); /// \return an RGB color for a given highlight spec. rgb_color_t highlight_get_color(const highlight_spec_t &highlight, bool is_background); @@ -112,7 +111,7 @@ rgb_color_t highlight_get_color(const highlight_spec_t &highlight, bool is_backg /// reference whether the suggestion is valid or not. bool autosuggest_validate_from_history(const history_item_t &item, const wcstring &working_directory, - const environment_t &vars); + const operation_context_t &ctx); // Tests whether the specified string cpath is the prefix of anything we could cd to. directories is // a list of possible parent directories (typically either the working directory, or the cdpath). diff --git a/src/operation_context.cpp b/src/operation_context.cpp new file mode 100644 index 000000000..3440fd884 --- /dev/null +++ b/src/operation_context.cpp @@ -0,0 +1,23 @@ +// Utilities for io redirection. +#include "config.h" // IWYU pragma: keep + +#include "operation_context.h" + +#include "env.h" + +bool no_cancel() { return false; } + +operation_context_t::operation_context_t(std::shared_ptr parser, + const environment_t &vars, cancel_checker_t cancel_checker) + : parser(std::move(parser)), vars(vars), cancel_checker(std::move(cancel_checker)) {} + +operation_context_t operation_context_t::empty() { + static const null_environment_t nullenv{}; + return operation_context_t{nullenv}; +} + +operation_context_t operation_context_t::globals() { + return operation_context_t{env_stack_t::globals()}; +} + +operation_context_t::~operation_context_t() = default; diff --git a/src/operation_context.h b/src/operation_context.h new file mode 100644 index 000000000..a230596a8 --- /dev/null +++ b/src/operation_context.h @@ -0,0 +1,56 @@ +#ifndef FISH_OPERATION_CONTEXT_H +#define FISH_OPERATION_CONTEXT_H + +#include + +#include "common.h" + +class environment_t; +class parser_t; + +/// A common helper which always returns false. +bool no_cancel(); + +/// A operation_context_t is a simple property bag which wraps up data needed for highlighting, +/// expansion, completion, and more. +/// It contains the following triple: +/// 1. A parser. This is often null. If not null, it may be used to execute fish script. If null, +/// then this is a background operation and fish script must not be executed. +/// 2. A variable set. This is never null. This may differ from the variables in the parser. +/// 3. A cancellation checker. This is a function which you may call to detect that the operation +/// is no longer necessary and should be cancelled. +class operation_context_t { + public: + // The parser, if this is a foreground operation. If this is a background operation, this may be + // nullptr. + std::shared_ptr parser; + + // The set of variables. It is the creator's responsibility to ensure this lives as log as the + // context itself. + const environment_t &vars; + + // A function which may be used to poll for cancellation. + cancel_checker_t cancel_checker; + + // Invoke the cancel checker. \return if we should cancel. + bool check_cancel() const { return cancel_checker(); } + + // \return an "empty" context which contains no variables, no parser, and never cancels. + static operation_context_t empty(); + + // \return an operation context that contains only global variables, no parser, and never + // cancels. + static operation_context_t globals(); + + /// Construct from the full triple of a parser, vars, and cancel checker. + operation_context_t(std::shared_ptr parser, const environment_t &vars, + cancel_checker_t cancel_checker); + + /// Construct from vars alone. + explicit operation_context_t(const environment_t &vars) + : operation_context_t(nullptr, vars, no_cancel) {} + + ~operation_context_t(); +}; + +#endif diff --git a/src/parse_execution.cpp b/src/parse_execution.cpp index 56bcfcf0d..faec8ff0a 100644 --- a/src/parse_execution.cpp +++ b/src/parse_execution.cpp @@ -89,8 +89,9 @@ static redirection_spec_t get_stderr_merge() { } parse_execution_context_t::parse_execution_context_t(parsed_source_ref_t pstree, parser_t *p, + const operation_context_t &ctx, job_lineage_t lineage) - : pstree(std::move(pstree)), parser(p), lineage(std::move(lineage)) {} + : pstree(std::move(pstree)), parser(p), ctx(ctx), lineage(std::move(lineage)) {} // Utilities @@ -136,8 +137,7 @@ tnode_t parse_execution_context_t::infinite_recursive_statem if (plain_statement) { maybe_t cmd = command_for_plain_statement(plain_statement, pstree->src); if (cmd && - expand_one(*cmd, {expand_flag::skip_cmdsubst, expand_flag::skip_variables}, nullenv, - nullptr) && + expand_one(*cmd, {expand_flag::skip_cmdsubst, expand_flag::skip_variables}, ctx) && cmd == forbidden_function_name) { // This is it. infinite_recursive_statement = plain_statement; @@ -382,7 +382,7 @@ eval_result_t parse_execution_context_t::run_for_statement( // in just one. tnode_t var_name_node = header.child<1>(); wcstring for_var_name = get_source(var_name_node); - if (!expand_one(for_var_name, expand_flags_t{}, parser->vars(), parser->shared())) { + if (!expand_one(for_var_name, expand_flags_t{}, ctx)) { report_error(var_name_node, FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG, for_var_name.c_str()); return eval_result_t::error; } @@ -458,9 +458,8 @@ eval_result_t parse_execution_context_t::run_switch_statement( // Expand it. We need to offset any errors by the position of the string. completion_list_t switch_values_expanded; parse_error_list_t errors; - auto expand_ret = - expand_string(switch_value, &switch_values_expanded, expand_flag::no_descriptions, - parser->vars(), parser->shared(), &errors); + auto expand_ret = expand_string(switch_value, &switch_values_expanded, + expand_flag::no_descriptions, ctx, &errors); parse_error_offset_source_start(&errors, switch_value_n.source_range()->start); switch (expand_ret) { @@ -720,7 +719,7 @@ eval_result_t parse_execution_context_t::expand_command(tnode_tvars(), out_cmd, out_args, &errors); + expand_to_command_and_args(unexp_cmd, ctx, out_cmd, out_args, &errors); if (expand_err == expand_result_t::error) { parser->set_last_statuses(statuses_t::just(STATUS_ILLEGAL_CMD)); // Issue #5812 - the expansions were done on the command token, @@ -877,8 +876,8 @@ eval_result_t parse_execution_context_t::expand_arguments_from_nodes( // Expand this string. parse_error_list_t errors; arg_expanded.clear(); - auto expand_ret = expand_string(arg_str, &arg_expanded, expand_flag::no_descriptions, - parser->vars(), parser->shared(), &errors); + auto expand_ret = + expand_string(arg_str, &arg_expanded, expand_flag::no_descriptions, ctx, &errors); parse_error_offset_source_start(&errors, arg_node.source_range()->start); switch (expand_ret) { case expand_result_t::error: { @@ -934,8 +933,7 @@ bool parse_execution_context_t::determine_redirections( // PCA: I can't justify this skip_variables flag. It was like this when I got here. bool target_expanded = - expand_one(target, no_exec() ? expand_flag::skip_variables : expand_flags_t{}, - parser->vars(), parser->shared()); + expand_one(target, no_exec() ? expand_flag::skip_variables : expand_flags_t{}, ctx); if (!target_expanded || target.empty()) { // TODO: Improve this error message. report_error(redirect_node, _(L"Invalid redirection target: %ls"), target.c_str()); @@ -1026,9 +1024,8 @@ eval_result_t parse_execution_context_t::apply_variable_assignments( completion_list_t expression_expanded; parse_error_list_t errors; // TODO this is mostly copied from expand_arguments_from_nodes, maybe extract to function - auto expand_ret = - expand_string(expression, &expression_expanded, expand_flag::no_descriptions, - parser->vars(), parser->shared(), &errors); + auto expand_ret = expand_string(expression, &expression_expanded, + expand_flag::no_descriptions, ctx, &errors); parse_error_offset_source_start( &errors, variable_assignment.source_range()->start + *equals_pos + 1); switch (expand_ret) { diff --git a/src/parse_execution.h b/src/parse_execution.h index ea4be2f63..8a87b0305 100644 --- a/src/parse_execution.h +++ b/src/parse_execution.h @@ -11,6 +11,7 @@ #include "proc.h" class block_t; +class operation_context_t; class parser_t; /// An eval_result represents evaluation errors including wildcards which failed to match, syntax @@ -34,6 +35,7 @@ class parse_execution_context_t { private: parsed_source_ref_t pstree; parser_t *const parser; + const operation_context_t &ctx; // The currently executing job node, used to indicate the line number. tnode_t executing_job_node{}; // Cached line number information. @@ -137,7 +139,8 @@ class parse_execution_context_t { int line_offset_of_character_at_offset(size_t offset); public: - parse_execution_context_t(parsed_source_ref_t pstree, parser_t *p, job_lineage_t lineage); + parse_execution_context_t(parsed_source_ref_t pstree, parser_t *p, + const operation_context_t &ctx, job_lineage_t lineage); /// Returns the current line number, indexed from 1. Not const since it touches /// cached_lineno_offset. diff --git a/src/parse_util.cpp b/src/parse_util.cpp index 1b9ec383d..794b981ea 100644 --- a/src/parse_util.cpp +++ b/src/parse_util.cpp @@ -1129,8 +1129,8 @@ static bool detect_errors_in_plain_statement(const wcstring &buff_src, if (maybe_t unexp_command = command_for_plain_statement(pst, buff_src)) { wcstring command; // Check that we can expand the command. - if (expand_to_command_and_args(*unexp_command, null_environment_t{}, &command, nullptr, - parse_errors) == expand_result_t::error) { + if (expand_to_command_and_args(*unexp_command, operation_context_t::empty(), &command, + nullptr, parse_errors) == expand_result_t::error) { errored = true; parse_error_offset_source_start(parse_errors, source_start); } @@ -1193,7 +1193,7 @@ static bool detect_errors_in_plain_statement(const wcstring &buff_src, // Check that we don't do an invalid builtin (issue #1252). if (!errored && decoration == parse_statement_decoration_builtin && - expand_one(*unexp_command, expand_flag::skip_cmdsubst, null_environment_t{}, nullptr, + expand_one(*unexp_command, expand_flag::skip_cmdsubst, operation_context_t::empty(), parse_errors) && !builtin_exists(*unexp_command)) { errored = append_syntax_error(parse_errors, source_start, UNKNOWN_BUILTIN_ERR_MSG, diff --git a/src/parser.cpp b/src/parser.cpp index 1ebb6a2ea..381b8ac06 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -306,8 +306,8 @@ void parser_t::emit_profiling(const char *path) const { } completion_list_t parser_t::expand_argument_list(const wcstring &arg_list_src, - expand_flags_t eflags, const environment_t &vars, - const std::shared_ptr &parser) { + expand_flags_t eflags, + const operation_context_t &ctx) { // Parse the string as an argument list. parse_node_tree_t tree; if (!parse_tree_from_string(arg_list_src, parse_flag_none, &tree, nullptr /* errors */, @@ -322,8 +322,7 @@ completion_list_t parser_t::expand_argument_list(const wcstring &arg_list_src, tnode_t arg_list(&tree, &tree.at(0)); while (auto arg = arg_list.next_in_list()) { const wcstring arg_src = arg.get_source(arg_list_src); - if (expand_string(arg_src, &result, eflags, vars, parser, nullptr /* errors */) == - expand_result_t::error) { + if (expand_string(arg_src, &result, eflags, ctx) == expand_result_t::error) { break; // failed to expand a string } } @@ -332,6 +331,14 @@ completion_list_t parser_t::expand_argument_list(const wcstring &arg_list_src, std::shared_ptr parser_t::shared() { return shared_from_this(); } +cancel_checker_t parser_t::cancel_checker() const { + return [this]() { return this->cancellation_signal != 0; }; +} + +operation_context_t parser_t::context() { + return operation_context_t{this->shared(), this->vars(), this->cancel_checker()}; +} + /// Append stack trace info for the block \p b to \p trace. static void append_block_description_to_stack_trace(const block_t &b, wcstring &trace, const environment_t &vars) { @@ -660,12 +667,13 @@ eval_result_t parser_t::eval_node(const parsed_source_ref_t &ps, tnode_t node job_reap(*this, false); // not sure why we reap jobs here // Start it up + operation_context_t op_ctx = this->context(); block_t *scope_block = this->push_block(block_t::scope_block(block_type)); // Create and set a new execution context. using exc_ctx_ref_t = std::unique_ptr; - scoped_push exc( - &execution_context, make_unique(ps, this, std::move(lineage))); + scoped_push exc(&execution_context, make_unique( + ps, this, op_ctx, std::move(lineage))); eval_result_t res = execution_context->eval_node(node, scope_block); exc.restore(); this->pop_block(scope_block); diff --git a/src/parser.h b/src/parser.h index c5863927e..4ae3cc920 100644 --- a/src/parser.h +++ b/src/parser.h @@ -14,6 +14,7 @@ #include "common.h" #include "event.h" #include "expand.h" +#include "operation_context.h" #include "parse_constants.h" #include "parse_execution.h" #include "parse_tree.h" @@ -197,6 +198,8 @@ struct library_data_t { std::shared_ptr cwd_fd{}; }; +class operation_context_t; + class parser_t : public std::enable_shared_from_this { friend class parse_execution_context_t; @@ -251,6 +254,9 @@ class parser_t : public std::enable_shared_from_this { /// Indicates that we should stop execution due to the given signal. static void cancel_requested(int sig); + /// Clear any cancel. + void clear_cancel() { cancellation_signal = 0; } + /// Global event blocks. event_blockage_list_t global_event_blocks; @@ -280,8 +286,8 @@ class parser_t : public std::enable_shared_from_this { /// cmdsubst execution on the tokens. Errors are ignored. If a parser is provided, it is used /// for command substitution expansion. static completion_list_t expand_argument_list(const wcstring &arg_list_src, - expand_flags_t flags, const environment_t &vars, - const std::shared_ptr &parser); + expand_flags_t flags, + const operation_context_t &ctx); /// Returns a string describing the current parser position in the format 'FILENAME (line /// LINE_NUMBER): LINE'. Example: @@ -370,6 +376,12 @@ class parser_t : public std::enable_shared_from_this { /// \return a shared pointer reference to this parser. std::shared_ptr shared(); + /// \return a cancel poller for checking if this parser has been signalled. + cancel_checker_t cancel_checker() const; + + /// \return the operation context for this parser. + operation_context_t context(); + ~parser_t(); }; diff --git a/src/reader.cpp b/src/reader.cpp index d4ae22743..86b98ccec 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -136,6 +136,18 @@ static inline unsigned read_generation_count() { return s_generation.load(std::memory_order_relaxed); } +/// \return an operation context for a background operation.. +/// Crucially the operation context itself does not contain a parser. +/// It is the caller's responsibility to ensure the environment lives as long as the result. +operation_context_t get_bg_context(const std::shared_ptr &env, + unsigned int generation_count) { + cancel_checker_t cancel_checker = [generation_count] { + // Cancel if the generation count changed. + return generation_count != read_generation_count(); + }; + return operation_context_t{nullptr, *env, std::move(cancel_checker)}; +} + void editable_line_t::insert_string(const wcstring &str, size_t start, size_t len) { // Clamp the range to something valid. size_t string_length = str.size(); @@ -380,8 +392,8 @@ class reader_data_t : public std::enable_shared_from_this { std::vector colors; /// An array defining the block level at each character. std::vector indents; - /// Function for tab completion. - complete_function_t complete_func{nullptr}; + /// Whether tab completion is allowed. + bool complete_ok{false}; /// Function for syntax highlighting. highlight_function_t highlight_func{highlight_universal}; /// Function for testing if the string can be returned. @@ -1283,11 +1295,9 @@ static std::function get_autosuggestion_performer // this should use shared_ptr return [=]() -> autosuggestion_result_t { ASSERT_IS_BACKGROUND_THREAD(); - const autosuggestion_result_t nothing = {}; - // If the main thread has moved on, skip all the work. - // Otherwise record the generation. - if (generation_count != read_generation_count()) { + operation_context_t ctx = get_bg_context(vars, generation_count); + if (ctx.check_cancel()) { return nothing; } s_thread_generation = generation_count; @@ -1297,21 +1307,22 @@ static std::function get_autosuggestion_performer return nothing; } - history_search_t searcher(*history, search_string, history_search_type_t::prefix); - while (!reader_test_should_cancel() && searcher.go_backwards()) { + history_search_t searcher(*history, search_string, history_search_type_t::prefix, + history_search_flags_t{}); + while (!ctx.check_cancel() && searcher.go_backwards()) { const history_item_t &item = searcher.current_item(); // Skip items with newlines because they make terrible autosuggestions. if (item.str().find(L'\n') != wcstring::npos) continue; - if (autosuggest_validate_from_history(item, working_directory, *vars)) { + if (autosuggest_validate_from_history(item, working_directory, ctx)) { // The command autosuggestion was handled specially, so we're done. return {searcher.current_string(), search_string}; } } // Maybe cancel here. - if (reader_test_should_cancel()) return nothing; + if (ctx.check_cancel()) return nothing; // Here we do something a little funny. If the line ends with a space, and the cursor is not // at the end, don't use completion autosuggestions. It ends up being pretty weird seeing @@ -1325,7 +1336,7 @@ static std::function get_autosuggestion_performer // Try normal completions. completion_request_flags_t complete_flags = completion_request_t::autosuggestion; - completion_list_t completions = complete(search_string, complete_flags, *vars, nullptr); + completion_list_t completions = complete(search_string, complete_flags, ctx); completions_sort_and_prioritize(&completions, complete_flags); if (!completions.empty()) { const completion_t &comp = completions.at(0); @@ -2036,16 +2047,12 @@ static std::function get_highlight_performer( parser_t &parser, const wcstring &text, long match_highlight_pos, highlight_function_t highlight_func) { auto vars = parser.vars().snapshot(); - unsigned int generation_count = read_generation_count(); + unsigned generation_count = read_generation_count(); return [=]() -> highlight_result_t { if (text.empty()) return {}; - if (generation_count != read_generation_count()) { - // The gen count has changed, so don't do anything. - return {}; - } - s_thread_generation = generation_count; + operation_context_t ctx = get_bg_context(vars, generation_count); std::vector colors(text.size(), highlight_spec_t{}); - highlight_func(text, colors, match_highlight_pos, nullptr /* error */, *vars); + highlight_func(text, colors, match_highlight_pos, ctx); return {std::move(colors), text}; }; } @@ -2152,7 +2159,7 @@ void reader_set_allow_autosuggesting(bool flag) { current_data()->allow_autosugg void reader_set_expand_abbreviations(bool flag) { current_data()->expand_abbreviations = flag; } -void reader_set_complete_function(complete_function_t f) { current_data()->complete_func = f; } +void reader_set_complete_ok(bool flag) { current_data()->complete_ok = flag; } void reader_set_highlight_function(highlight_function_t func) { current_data()->highlight_func = func; @@ -2255,7 +2262,7 @@ uint64_t reader_run_count() { return run_count; } /// Read interactively. Read input from stdin while providing editing facilities. static int read_i(parser_t &parser) { reader_push(parser, history_session_id(parser.vars())); - reader_set_complete_function(&complete); + reader_set_complete_ok(true); reader_set_highlight_function(&highlight_shell); reader_set_test_function(&reader_shell_test); reader_set_allow_autosuggesting(true); @@ -2514,7 +2521,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::complete: case rl::complete_and_search: { - if (!complete_func) break; + if (!complete_ok) break; // Use the command line only; it doesn't make sense to complete in any other line. editable_line_t *el = &command_line; @@ -2540,9 +2547,6 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // Get the string; we have to do this after removing any trailing backslash. const wchar_t *const buff = el->text.c_str(); - // Clear the completion list. - rls.comp.clear(); - // Figure out the extent of the command substitution surrounding the cursor. // This is because we only look at the current command substitution to form // completions - stuff happening outside of it is not interesting. @@ -2566,7 +2570,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // std::fwprintf(stderr, L"Complete (%ls)\n", buffcpy.c_str()); completion_request_flags_t complete_flags = {completion_request_t::descriptions, completion_request_t::fuzzy_match}; - rls.comp = complete_func(buffcpy, complete_flags, vars, parser_ref); + rls.comp = complete(buffcpy, complete_flags, parser_ref->context()); // User-supplied completions may have changed the commandline - prevent buffer // overflow. diff --git a/src/reader.h b/src/reader.h index 3d8d722a5..b09e9a1fb 100644 --- a/src/reader.h +++ b/src/reader.h @@ -17,6 +17,8 @@ class environment_t; class history_t; class io_chain_t; +class operation_context_t; +class parser_t; /// Helper class for storing a command line. class editable_line_t { @@ -146,16 +148,12 @@ void reader_push(parser_t &parser, const wcstring &name); /// Return to previous reader environment. void reader_pop(); -/// Specify function to use for finding possible tab completions. -typedef completion_list_t (*complete_function_t)(const wcstring &, completion_request_flags_t, - const environment_t &, - const std::shared_ptr &parser); - -void reader_set_complete_function(complete_function_t); +/// Mark whether tab completion is enabled. +void reader_set_complete_ok(bool flag); /// The type of a highlight function. using highlight_function_t = void (*)(const wcstring &, std::vector &, size_t, - wcstring_list_t *, const environment_t &vars); + const operation_context_t &ctx); /// Function type for testing if a string is valid for the reader to return. using test_function_t = parser_test_error_bits_t (*)(parser_t &, const wcstring &); diff --git a/src/wildcard.cpp b/src/wildcard.cpp index 52c33ad0c..147f08af7 100644 --- a/src/wildcard.cpp +++ b/src/wildcard.cpp @@ -456,6 +456,8 @@ static bool wildcard_test_flags_then_complete(const wcstring &filepath, const wc } class wildcard_expander_t { + // A function to call to check cancellation. + cancel_checker_t cancel_checker; // The working directory to resolve paths against const wcstring working_directory; // The set of items we have resolved, used to efficiently avoid duplication. @@ -502,7 +504,7 @@ class wildcard_expander_t { /// Indicate whether we should cancel wildcard expansion. This latches 'interrupt'. bool interrupted() { - did_interrupt = did_interrupt || reader_test_should_cancel(); + did_interrupt = did_interrupt || cancel_checker(); return did_interrupt; } @@ -629,8 +631,12 @@ class wildcard_expander_t { } public: - wildcard_expander_t(wcstring wd, expand_flags_t f, completion_list_t *r) - : working_directory(std::move(wd)), flags(f), resolved_completions(r) { + wildcard_expander_t(wcstring wd, expand_flags_t f, const cancel_checker_t &cancel_checker, + completion_list_t *r) + : cancel_checker(cancel_checker), + working_directory(std::move(wd)), + flags(f), + resolved_completions(r) { assert(resolved_completions != nullptr); // Insert initial completions into our set to avoid duplicates. @@ -897,7 +903,9 @@ void wildcard_expander_t::expand(const wcstring &base_dir, const wchar_t *wc, wildcard_expand_result_t wildcard_expand_string(const wcstring &wc, const wcstring &working_directory, - expand_flags_t flags, completion_list_t *output) { + expand_flags_t flags, + const cancel_checker_t &cancel_checker, + completion_list_t *output) { assert(output != nullptr); // Fuzzy matching only if we're doing completions. assert(flags.get(expand_flag::for_completions) || !flags.get(expand_flag::fuzzy_match)); @@ -934,7 +942,7 @@ wildcard_expand_result_t wildcard_expand_string(const wcstring &wc, effective_wc = wc; } - wildcard_expander_t expander(prefix, flags, output); + wildcard_expander_t expander(prefix, flags, cancel_checker, output); expander.expand(base_dir, effective_wc.c_str(), base_dir); return expander.status_code(); } diff --git a/src/wildcard.h b/src/wildcard.h index 1b03e6e2f..eb95cd4d4 100644 --- a/src/wildcard.h +++ b/src/wildcard.h @@ -48,7 +48,9 @@ enum class wildcard_expand_result_t { }; wildcard_expand_result_t wildcard_expand_string(const wcstring &wc, const wcstring &working_directory, - expand_flags_t flags, completion_list_t *out); + expand_flags_t flags, + const cancel_checker_t &cancel_checker, + completion_list_t *out); /// Test whether the given wildcard matches the string. Does not perform any I/O. ///