diff --git a/src/autoload.cpp b/src/autoload.cpp index b3f1ed5ad..fb51235bb 100644 --- a/src/autoload.cpp +++ b/src/autoload.cpp @@ -72,6 +72,9 @@ class autoload_file_cache_t { /// If \p allow_stale is true, allow stale entries; otherwise discard them. /// This returns an autoloadable file, or none() if there is no such file. maybe_t check(const wcstring &cmd, bool allow_stale = false); + + /// \return true if a command is cached (either as a hit or miss). + bool is_cached(const wcstring &cmd) const; }; maybe_t autoload_file_cache_t::locate_file(const wcstring &cmd) const { @@ -142,6 +145,10 @@ maybe_t autoload_file_cache_t::check(const wcstring &cmd, b return file; } +bool autoload_file_cache_t::is_cached(const wcstring &cmd) const { + return known_files_.count(cmd) > 0 || misses_cache_.contains(cmd); +} + autoload_t::autoload_t(wcstring env_var_name) : env_var_name_(std::move(env_var_name)), cache_(make_unique()) {} @@ -157,6 +164,8 @@ bool autoload_t::can_autoload(const wcstring &cmd) { return cache_->check(cmd, true /* allow stale */).has_value(); } +bool autoload_t::has_attempted_autoload(const wcstring &cmd) { return cache_->is_cached(cmd); } + wcstring_list_t autoload_t::get_autoloaded_commands() const { wcstring_list_t result; result.reserve(autoloaded_files_.size()); diff --git a/src/autoload.h b/src/autoload.h index 64efce9c4..a08cfde61 100644 --- a/src/autoload.h +++ b/src/autoload.h @@ -88,6 +88,9 @@ class autoload_t { /// This does not actually mark the command as being autoloaded. bool can_autoload(const wcstring &cmd); + /// \return whether autoloading has been attempted for a command. + bool has_attempted_autoload(const wcstring &cmd); + /// \return the names of all commands that have been autoloaded. Note this includes "in-flight" /// commands. wcstring_list_t get_autoloaded_commands() const; diff --git a/src/complete.cpp b/src/complete.cpp index b1ac46e36..0f616478b 100644 --- a/src/complete.cpp +++ b/src/complete.cpp @@ -332,6 +332,9 @@ class completer_t { /// The output completions. completion_receiver_t completions; + /// Commands which we would have tried to load, if we had a parser. + wcstring_list_t needs_load; + /// Table of completions conditions that have already been tested and the corresponding test /// results. using condition_cache_t = std::unordered_map; @@ -424,6 +427,8 @@ class completer_t { void perform_for_commandline(wcstring cmdline); completion_list_t acquire_completions() { return completions.take(); } + + wcstring_list_t acquire_needs_load() { return std::move(needs_load); } }; // Autoloader for completions. @@ -799,25 +804,6 @@ static size_t short_option_pos(const wcstring &arg, const option_list_t &options return arg.size() - 1; } -/// Load command-specific completions for the specified command. -static void complete_load(const wcstring &name) { - // We have to load this as a function, since it may define a --wraps or signature. - // See issue #2466. - auto &parser = parser_t::principal_parser(); - function_get_props_autoload(name, parser); - - // It's important to NOT hold the lock around completion loading. - // We need to take the lock to decide what to load, drop it to perform the load, then reacquire - // it. - // Note we only look at the global fish_function_path and fish_complete_path. - maybe_t path_to_load = - completion_autoloader.acquire()->resolve_command(name, env_stack_t::globals()); - if (path_to_load) { - autoload_t::perform_autoload(*path_to_load, parser); - completion_autoloader.acquire()->mark_autoload_finished(name); - } -} - /// complete_param: Given a command, find completions for the argument str of command cmd_orig with /// previous option popt. If file completions should be disabled, then mark *out_do_file as false. /// @@ -845,8 +831,10 @@ bool completer_t::complete_param_for_command(const wcstring &cmd_orig, const wcs // tools that do not exist. Applies to both manual completions ("cm", "cmd ") // and automatic completions ("gi" autosuggestion provider -> git) FLOG(complete, "Skipping completions for non-existent command"); - } else { - iothread_perform_on_main([&]() { complete_load(cmd); }); + } else if (ctx.parser) { + complete_load(cmd, *ctx.parser); + } else if (!completion_autoloader.acquire()->has_attempted_autoload(cmd)) { + needs_load.push_back(cmd); } // Make a list of lists of all options that we care about. @@ -1713,7 +1701,7 @@ void complete_remove_all(const wcstring &cmd, bool cmd_is_path) { } completion_list_t complete(const wcstring &cmd_with_subcmds, completion_request_options_t flags, - const operation_context_t &ctx) { + const operation_context_t &ctx, wcstring_list_t *out_needs_loads) { // 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, @@ -1722,6 +1710,9 @@ completion_list_t complete(const wcstring &cmd_with_subcmds, completion_request_ wcstring cmd = wcstring(cmdsubst_begin, cmdsubst_end - cmdsubst_begin); completer_t completer(ctx, flags); completer.perform_for_commandline(std::move(cmd)); + if (out_needs_loads) { + *out_needs_loads = completer.acquire_needs_load(); + } return completer.acquire_completions(); } @@ -1788,6 +1779,30 @@ static wcstring completion2string(const completion_key_t &key, const complete_en return out; } +bool complete_load(const wcstring &cmd, parser_t &parser) { + bool loaded_new = false; + + // We have to load this as a function, since it may define a --wraps or signature. + // See issue #2466. + if (function_load(cmd, parser)) { + // We autoloaded something; check if we have a --wraps. + loaded_new |= complete_get_wrap_targets(cmd).size() > 0; + } + + // It's important to NOT hold the lock around completion loading. + // We need to take the lock to decide what to load, drop it to perform the load, then reacquire + // it. + // Note we only look at the global fish_function_path and fish_complete_path. + maybe_t path_to_load = + completion_autoloader.acquire()->resolve_command(cmd, env_stack_t::globals()); + if (path_to_load) { + autoload_t::perform_autoload(*path_to_load, parser); + completion_autoloader.acquire()->mark_autoload_finished(cmd); + loaded_new = true; + } + return loaded_new; +} + /// Use by the bare `complete`, loaded completions are printed out as commands wcstring complete_print(const wcstring &cmd) { wcstring out; diff --git a/src/complete.h b/src/complete.h index 7fff34387..f2ac9a65d 100644 --- a/src/complete.h +++ b/src/complete.h @@ -26,6 +26,7 @@ struct completion_mode_t { #define PROG_COMPLETE_SEP L'\t' class environment_t; +class parser_t; enum { /// Do not insert space afterwards if this is the only completion. (The default is to try insert @@ -243,10 +244,18 @@ void complete_remove(const wcstring &cmd, bool cmd_is_path, const wcstring &opti /// Removes all completions for a given command. void complete_remove_all(const wcstring &cmd, bool cmd_is_path); +/// Load command-specific completions for the specified command. +/// \return true if something new was loaded, false if not. +bool complete_load(const wcstring &cmd, parser_t &parser); + /// \return all completions of the command cmd. +/// If \p ctx contains a parser, this will autoload functions and completions as needed. +/// If it does not contain a parser, then any completions which need autoloading will be returned in +/// \p needs_load, if not null. class operation_context_t; completion_list_t complete(const wcstring &cmd, completion_request_options_t flags, - const operation_context_t &ctx); + const operation_context_t &ctx, + wcstring_list_t *out_needs_load = nullptr); /// Return a list of all current completions. wcstring complete_print(const wcstring &cmd = L""); diff --git a/src/function.cpp b/src/function.cpp index bad7b8d01..db7203a48 100644 --- a/src/function.cpp +++ b/src/function.cpp @@ -85,7 +85,7 @@ static std::shared_ptr copy_props(const function_properti /// Make sure that if the specified function is a dynamically loaded function, it has been fully /// loaded. /// Note this executes fish script code. -static void try_autoload(const wcstring &name, parser_t &parser) { +bool function_load(const wcstring &name, parser_t &parser) { ASSERT_IS_MAIN_THREAD(); maybe_t path_to_autoload; // Note we can't autoload while holding the funcset lock. @@ -99,10 +99,11 @@ static void try_autoload(const wcstring &name, parser_t &parser) { // Release the lock and perform any autoload, then reacquire the lock and clean up. if (path_to_autoload) { - // Crucially, the lock is acquired *after* do_autoload_file_at_path(). + // Crucially, the lock is acquired after perform_autoload(). autoload_t::perform_autoload(*path_to_autoload, parser); function_set.acquire()->autoloader.mark_autoload_finished(name); } + return path_to_autoload.has_value(); } /// Insert a list of all dynamically loaded functions into the specified list. @@ -166,7 +167,7 @@ function_properties_ref_t function_get_props(const wcstring &name) { function_properties_ref_t function_get_props_autoload(const wcstring &name, parser_t &parser) { ASSERT_IS_MAIN_THREAD(); if (parser_keywords_is_reserved(name)) return nullptr; - try_autoload(name, parser); + function_load(name, parser); return function_get_props(name); } @@ -217,7 +218,7 @@ static wcstring get_function_body_source(const function_properties_t &props) { void function_set_desc(const wcstring &name, const wcstring &desc, parser_t &parser) { ASSERT_IS_MAIN_THREAD(); - try_autoload(name, parser); + function_load(name, parser); auto funcset = function_set.acquire(); auto iter = funcset->funcs.find(name); if (iter != funcset->funcs.end()) { diff --git a/src/function.h b/src/function.h index e4bf9f065..ba422df84 100644 --- a/src/function.h +++ b/src/function.h @@ -74,6 +74,10 @@ function_properties_ref_t function_get_props(const wcstring &name); /// \return the properties for a function, or nullptr if none, perhaps triggering autoloading. function_properties_ref_t function_get_props_autoload(const wcstring &name, parser_t &parser); +/// Try autoloading a function. +/// \return true if something new was autoloaded, false if it was already loaded or did not exist. +bool function_load(const wcstring &name, parser_t &parser); + /// Sets the description of the function with the name \c name. /// This triggers autoloading. void function_set_desc(const wcstring &name, const wcstring &desc, parser_t &parser); diff --git a/src/lru.h b/src/lru.h index e840330e7..6f2469e24 100644 --- a/src/lru.h +++ b/src/lru.h @@ -169,6 +169,11 @@ class lru_cache_t { return &where->second.value; } + /// \return true if we contain a value for a key. + bool contains(const wcstring &key) const { + return this->node_map.find(key) != this->node_map.end(); + } + // Evicts the node for a given key, returning true if a node was evicted. bool evict_node(const wcstring &key) { auto where = this->node_map.find(key); diff --git a/src/reader.cpp b/src/reader.cpp index 771f3b669..be6ec7c6e 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -504,6 +504,9 @@ struct autosuggestion_t { // The string which was searched for. wcstring search_string{}; + // The list of completions which may need loading. + wcstring_list_t needs_load{}; + // Whether the autosuggestion should be case insensitive. // This is true for file-generated autosuggestions, but not for history. bool icase{false}; @@ -1778,18 +1781,21 @@ static std::function get_autosuggestion_performer( // Try normal completions. completion_request_options_t complete_flags = completion_request_options_t::autosuggest(); - completion_list_t completions = complete(search_string, complete_flags, ctx); - completions_sort_and_prioritize(&completions, complete_flags); + wcstring_list_t needs_load; + completion_list_t completions = complete(search_string, complete_flags, ctx, &needs_load); + + autosuggestion_t result{}; + result.search_string = search_string; + result.needs_load = std::move(needs_load); + result.icase = true; // normal completions are case-insensitive. if (!completions.empty()) { + completions_sort_and_prioritize(&completions, complete_flags); const completion_t &comp = completions.at(0); size_t cursor = cursor_pos; - wcstring suggestion = completion_apply_to_command_line( + result.text = completion_apply_to_command_line( comp.completion, comp.flags, search_string, &cursor, true /* append only */); - // Normal completions are case-insensitive. - return autosuggestion_t{std::move(suggestion), search_string, true /* icase */}; } - - return nothing; + return result; }; } @@ -1805,10 +1811,28 @@ bool reader_data_t::can_autosuggest() const { // Called after an autosuggestion has been computed on a background thread. void reader_data_t::autosuggest_completed(autosuggestion_t result) { ASSERT_IS_MAIN_THREAD(); - if (result.search_string == in_flight_autosuggest_request) + if (result.search_string == in_flight_autosuggest_request) { in_flight_autosuggest_request.clear(); - if (!result.empty() && can_autosuggest() && result.search_string == command_line.text() && - string_prefixes_string_case_insensitive(result.search_string, result.text)) { + } + if (result.search_string != command_line.text()) { + // This autosuggestion is stale. + return; + } + // Maybe load completions for commands discovered by this autosuggestion. + bool loaded_new = false; + for (const wcstring &to_load : result.needs_load) { + if (complete_load(to_load, this->parser())) { + FLOGF(complete, "Autosuggest found new completions for %ls, restarting", + to_load.c_str()); + loaded_new = true; + } + } + if (loaded_new) { + // We loaded new completions for this command. + // Re-do our autosuggestion. + this->update_autosuggestion(); + } else if (!result.empty() && can_autosuggest() && + string_prefixes_string_case_insensitive(result.search_string, result.text)) { // Autosuggestion is active and the search term has not changed, so we're good to go. autosuggestion = std::move(result); if (this->is_repaint_needed()) {