From 6fb7f9b6b828ee904db81e10c2fd89dc55d37f0f Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Tue, 29 Oct 2019 13:32:26 +0100 Subject: [PATCH] Fix completion for builtins (with subcommands) Presently the completion engine ignores builtins that are part of the fish syntax. This can be a problem when completing a string that was based on the output of `commandline -p`. This changes completions to treat these builtins like any other command. This also disables generic (filename) completion inside comments and after strings that do not tokenize. Additionally, comments are stripped off the output of `commandline -p`. Fixes #5415 Fixes #2705 --- CHANGELOG.md | 1 + share/completions/begin.fish | 2 + share/completions/builtin.fish | 9 +- share/completions/command.fish | 11 +- share/completions/exec.fish | 5 +- share/completions/for.fish | 3 + share/completions/if.fish | 2 + share/completions/while.fish | 2 + .../__fish_complete_external_command.fish | 3 + .../functions/__fish_config_interactive.fish | 3 +- src/builtin_commandline.cpp | 2 +- src/complete.cpp | 410 +++++++----------- src/parse_tree.h | 10 + src/parse_util.cpp | 19 +- src/parse_util.h | 7 +- src/tnode.h | 5 - src/tokenizer.h | 7 + tests/checks/complete.fish | 26 ++ 18 files changed, 248 insertions(+), 279 deletions(-) create mode 100644 share/completions/begin.fish create mode 100644 share/completions/for.fish create mode 100644 share/completions/if.fish create mode 100644 share/completions/while.fish create mode 100644 share/functions/__fish_complete_external_command.fish diff --git a/CHANGELOG.md b/CHANGELOG.md index fcebe1e2b..250d4d9e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - fish only parses `/etc/paths` on macOS in login shells, matching the bash implementation (#5637) and avoiding changes to path ordering in child shells (#5456). - The locale is now reloaded when the `LOCPATH` variable is changed (#5815). - `read` no longer keeps a history, making it suitable for operations that shouldn't end up there, like password entry (#5904). +- Completion of subcommands to builtins like `and` or `not` has been fixed (#6249). #### New or improved bindings - Pasting strips leading spaces to avoid pasted commands being omitted from the history (#4327). diff --git a/share/completions/begin.fish b/share/completions/begin.fish new file mode 100644 index 000000000..e95aa4bbc --- /dev/null +++ b/share/completions/begin.fish @@ -0,0 +1,2 @@ +complete -c begin -s h -l help -d 'Display help and exit' +complete -c begin -xa '(__fish_complete_subcommand)' diff --git a/share/completions/builtin.fish b/share/completions/builtin.fish index 9a24eb3b5..27800d3b3 100644 --- a/share/completions/builtin.fish +++ b/share/completions/builtin.fish @@ -1,5 +1,4 @@ - -complete -c builtin -s h -l help -d 'Display help and exit' -complete -c builtin -s n -l names -d 'Print names of all existing builtins' -complete -c builtin -xa '(builtin -n)' -complete -c builtin -n '__fish_use_subcommand' -xa '(__fish_complete_subcommand)' +complete -c builtin -n 'test (count (commandline -opc)) -eq 1' -s h -l help -d 'Display help and exit' +complete -c builtin -n 'test (count (commandline -opc)) -eq 1' -s n -l names -d 'Print names of all existing builtins' +complete -c builtin -n 'test (count (commandline -opc)) -eq 1' -xa '(builtin -n)' +complete -c builtin -n 'test (count (commandline -opc)) -ge 2' -xa '(__fish_complete_subcommand)' diff --git a/share/completions/command.fish b/share/completions/command.fish index 1ed4cbf7d..7c3e6956b 100644 --- a/share/completions/command.fish +++ b/share/completions/command.fish @@ -1,4 +1,7 @@ - -complete -c command -s h -l help -d 'Display help and exit' -complete -c command -s s -l search -d 'Print the file that would be executed' -complete -c command -d "Command to run" -xa "(__fish_complete_subcommand)" +complete -c command -n 'test (count (commandline -opc)) -eq 1' -s h -l help -d 'Display help and exit' +complete -c command -n 'test (count (commandline -opc)) -eq 1' -s a -l all -d 'Print all external commands by the given name' +complete -c command -n 'test (count (commandline -opc)) -eq 1' -s q -l quiet -d 'Do not print anything, only set exit status' +complete -c command -n 'test (count (commandline -opc)) -eq 1' -s s -l search -d 'Print the file that would be executed' +complete -c command -n 'test (count (commandline -opc)) -eq 1' -s s -l search -d 'Print the file that would be executed' +complete -c command -n 'test (count (commandline -opc)) -eq 1' -xa "(__fish_complete_external_command)" +complete -c command -n 'test (count (commandline -opc)) -ge 2' -xa "(__fish_complete_subcommand)" diff --git a/share/completions/exec.fish b/share/completions/exec.fish index a2abf228a..1123f1e3e 100644 --- a/share/completions/exec.fish +++ b/share/completions/exec.fish @@ -1,2 +1,3 @@ -complete -c exec -s h -l help -d 'Display help and exit' -complete -c exec -r -a '(__fish_complete_subcommand)' +complete -c exec -n 'test (count (commandline -opc)) -eq 1' -s h -l help -d 'Display help and exit' +complete -c exec -n 'test (count (commandline -opc)) -eq 1' -xa "(__fish_complete_external_command)" +complete -c exec -n 'test (count (commandline -opc)) -ge 2' -xa "(__fish_complete_subcommand)" diff --git a/share/completions/for.fish b/share/completions/for.fish new file mode 100644 index 000000000..e7d74a390 --- /dev/null +++ b/share/completions/for.fish @@ -0,0 +1,3 @@ +complete -c for -n 'test (count (commandline -opc)) -eq 1' -s h -l help -d 'Display help and exit' +complete -c for -n 'test (count (commandline -opc)) -eq 1' -f +complete -c for -n 'test (count (commandline -opc)) -eq 2' -xa in diff --git a/share/completions/if.fish b/share/completions/if.fish new file mode 100644 index 000000000..978b1868d --- /dev/null +++ b/share/completions/if.fish @@ -0,0 +1,2 @@ +complete -c if -s h -l help -d 'Display help and exit' +complete -c if -xa '(__fish_complete_subcommand)' diff --git a/share/completions/while.fish b/share/completions/while.fish new file mode 100644 index 000000000..d85e72092 --- /dev/null +++ b/share/completions/while.fish @@ -0,0 +1,2 @@ +complete -c while -s h -l help -d 'Display help and exit' +complete -c while -xa '(__fish_complete_subcommand)' diff --git a/share/functions/__fish_complete_external_command.fish b/share/functions/__fish_complete_external_command.fish new file mode 100644 index 000000000..218aee4f3 --- /dev/null +++ b/share/functions/__fish_complete_external_command.fish @@ -0,0 +1,3 @@ +function __fish_complete_external_command + command find $PATH/ -maxdepth 1 -perm +u+x 2>&- | string match -r '[^/]*$' +end diff --git a/share/functions/__fish_config_interactive.fish b/share/functions/__fish_config_interactive.fish index ec059f3c9..af03801cf 100644 --- a/share/functions/__fish_config_interactive.fish +++ b/share/functions/__fish_config_interactive.fish @@ -163,11 +163,12 @@ function __fish_config_interactive -d "Initializations that should be performed # the user tries [ interactively. # complete -c [ --wraps test + complete -c ! --wraps not # # Only a few builtins take filenames; initialize the rest with no file completions # - complete -c(builtin -n | string match -rv 'source|cd|exec|realpath|set|\[|test') --no-files + complete -c(builtin -n | string match -rv '(source|cd|exec|realpath|set|\\[|test|for)') --no-files # Reload key bindings when binding variable change function __fish_reload_key_bindings -d "Reload key bindings when binding variable change" --on-variable fish_key_bindings diff --git a/src/builtin_commandline.cpp b/src/builtin_commandline.cpp index a8d3e112f..bb924a4e5 100644 --- a/src/builtin_commandline.cpp +++ b/src/builtin_commandline.cpp @@ -398,7 +398,7 @@ int builtin_commandline(parser_t &parser, io_streams_t &streams, wchar_t **argv) break; } case PROCESS_MODE: { - parse_util_process_extent(current_buffer, current_cursor_pos, &begin, &end); + parse_util_process_extent(current_buffer, current_cursor_pos, &begin, &end, nullptr); break; } case JOB_MODE: { diff --git a/src/complete.cpp b/src/complete.cpp index 28eed5b96..495bedc9f 100644 --- a/src/complete.cpp +++ b/src/complete.cpp @@ -119,8 +119,6 @@ typedef struct complete_entry_opt { } complete_entry_opt_t; -using arg_list_t = std::vector>; - /// Last value used in the order field of completion_entry_t. static std::atomic k_complete_order{0}; @@ -352,8 +350,7 @@ class completer_t { void complete_param_expand(const wcstring &str, bool do_file, bool handle_as_special_cd = false); - void complete_cmd(const wcstring &str, bool use_function, bool use_builtin, bool use_command, - bool use_implicit_cd); + void complete_cmd(const wcstring &str); /// Attempt to complete an abbreviation for the given string. void complete_abbr(const wcstring &str); @@ -386,7 +383,8 @@ class completer_t { void escape_opening_brackets(const wcstring &argument); - void mark_completions_duplicating_arguments(const wcstring &prefix, const arg_list_t &args); + void mark_completions_duplicating_arguments(const wcstring &prefix, + const std::vector &args); public: completer_t(const environment_t &vars, const std::shared_ptr &parser, wcstring c, @@ -665,54 +663,45 @@ static wcstring complete_function_desc(const wcstring &fn) { /// using an absolute path, functions, builtins and directories for implicit cd commands. /// /// \param str_cmd the command string to find completions for -void completer_t::complete_cmd(const wcstring &str_cmd, bool use_function, bool use_builtin, - bool use_command, bool use_implicit_cd) { +void completer_t::complete_cmd(const wcstring &str_cmd) { if (str_cmd.empty()) return; std::vector possible_comp; - if (use_command) { - // Append all possible executables - expand_result_t result = - expand_string(str_cmd, &this->completions, - this->expand_flags() | expand_flag::special_for_command | - expand_flag::for_completions | expand_flag::executables_only, - vars, parser, NULL); - if (result != expand_result_t::error && this->wants_descriptions()) { - this->complete_cmd_desc(str_cmd); - } + // Append all possible executables + expand_result_t result = + expand_string(str_cmd, &this->completions, + this->expand_flags() | expand_flag::special_for_command | + expand_flag::for_completions | expand_flag::executables_only, + vars, parser, NULL); + if (result != expand_result_t::error && this->wants_descriptions()) { + this->complete_cmd_desc(str_cmd); } - if (use_implicit_cd) { - // We don't really care if this succeeds or fails. If it succeeds this->completions will be - // updated with choices for the user. - expand_result_t ignore = - // Append all matching directories - expand_string( - str_cmd, &this->completions, - this->expand_flags() | expand_flag::for_completions | expand_flag::directories_only, - vars, parser, NULL); - UNUSED(ignore); - } + // We don't really care if this succeeds or fails. If it succeeds this->completions will be + // updated with choices for the user. + expand_result_t ignore = + // Append all matching directories + expand_string( + str_cmd, &this->completions, + this->expand_flags() | expand_flag::for_completions | expand_flag::directories_only, + vars, parser, NULL); + UNUSED(ignore); if (str_cmd.find(L'/') == wcstring::npos && str_cmd.at(0) != L'~') { - if (use_function) { - wcstring_list_t names = function_get_names(str_cmd.at(0) == L'_'); - for (wcstring &name : names) { - // Append all known matching functions - append_completion(&possible_comp, std::move(name)); - } - - this->complete_strings(str_cmd, complete_function_desc, possible_comp, 0); + wcstring_list_t names = function_get_names(str_cmd.at(0) == L'_'); + for (wcstring &name : names) { + // Append all known matching functions + append_completion(&possible_comp, std::move(name)); } + this->complete_strings(str_cmd, complete_function_desc, possible_comp, 0); + possible_comp.clear(); - if (use_builtin) { - // Append all matching builtins - builtin_get_names(&possible_comp); - this->complete_strings(str_cmd, builtin_get_desc, possible_comp, 0); - } + // Append all matching builtins + builtin_get_names(&possible_comp); + this->complete_strings(str_cmd, builtin_get_desc, possible_comp, 0); } } @@ -1429,7 +1418,7 @@ void completer_t::escape_opening_brackets(const wcstring &argument) { /// Set the DUPLICATES_ARG flag in any completion that duplicates an argument. void completer_t::mark_completions_duplicating_arguments(const wcstring &prefix, - const arg_list_t &args) { + const std::vector &args) { // Get all the arguments, unescaped, into an array that we're going to bsearch. wcstring_list_t arg_strs; for (const auto &arg : args) { @@ -1453,234 +1442,153 @@ void completer_t::mark_completions_duplicating_arguments(const wcstring &prefix, } } -/// Return the index of an argument from \p args containing the position \p pos, or none if none. -static maybe_t find_argument_containing_position(const arg_list_t &args, size_t pos) { - size_t idx = 0; - for (const auto &arg : args) { - if (arg.location_in_or_at_end_of_source_range(pos)) { - return idx; - } - idx++; - } - return none(); -} - void completer_t::perform() { - wcstring current_command; - const size_t pos = cmd.size(); - // debug( 1, L"Complete '%ls'", cmd.c_str() ); - - const wchar_t *tok_begin = nullptr; - parse_util_token_extent(cmd.c_str(), cmd.size(), &tok_begin, nullptr, nullptr, nullptr); - assert(tok_begin != nullptr); - - // If we are completing a variable name or a tilde expansion user name, we do that and return. - // No need for any other completions. - // Unconditionally complete variables and processes. - const wcstring current_token = tok_begin; - if (try_complete_variable(current_token) || try_complete_user(current_token)) { - return; - } - - parse_node_tree_t tree; - parse_tree_from_string(cmd, - parse_flag_continue_after_error | parse_flag_accept_incomplete_tokens | - parse_flag_include_comments, - &tree, NULL); + const size_t cursor_pos = cmd.size(); // Find the plain statement to operate on. The cursor may be past it (#1261), so backtrack // until we know we're no longer in a space. But the space may actually be part of the // argument (#2477). - size_t position_in_statement = pos; + size_t position_in_statement = cursor_pos; while (position_in_statement > 0 && cmd.at(position_in_statement - 1) == L' ') { position_in_statement--; } - auto plain_statement = tnode_t::find_node_matching_source_location( - &tree, position_in_statement, nullptr); - if (!plain_statement) { - // Not part of a plain statement. This could be e.g. a for loop header, case expression, - // etc. Do generic file completions (issue #1309). If we had to backtrack, it means - // there was whitespace; don't do an autosuggestion in that case. Also don't do it if we - // are just after a pipe, semicolon, or & (issue #1631), or in a comment. - // - // Overall this logic is a total mess. A better approach would be to return the - // "possible next token" from the parse tree directly (this data is available as the - // first of the sequence of nodes without source locations at the very end of the parse - // tree). - bool do_file = true; - if (flags & completion_request_t::autosuggestion) { - if (position_in_statement < pos) { - do_file = false; - } else if (pos > 0) { - // If the previous character is in one of these types, we don't do file - // suggestions. - const parse_token_type_t bad_types[] = {parse_token_type_pipe, parse_token_type_end, - parse_token_type_background, - parse_special_type_comment}; - for (parse_token_type_t type : bad_types) { - if (tree.find_node_matching_source_location(type, pos - 1, NULL)) { - do_file = false; - break; - } - } - } - } - complete_param_expand(current_token, do_file); - } else { - assert(plain_statement && plain_statement.has_source()); - bool use_command = true; - bool use_function = true; - bool use_builtin = true; - bool use_implicit_cd = true; - bool use_abbr = true; + // Get all the arguments. + std::vector tokens; + parse_util_process_extent(cmd.c_str(), position_in_statement, nullptr, nullptr, &tokens); - // Get the command node. - tnode_t cmd_node = plain_statement.child<0>(); - assert(cmd_node && cmd_node.has_source() && "Expected command node to be valid"); + // Hack: fix autosuggestion by removing prefixing "and"s #6249. + if (flags & completion_request_t::autosuggestion) { + constexpr const wchar_t *prefix_cmds[] = {L"and", L"begin", L"command", L"exec", + L"if", L"not", L"or", L"while"}; + while (!tokens.empty()) { + auto cmd_string = tokens.front().get_source(cmd); + bool is_subcommand = std::find_if(std::begin(prefix_cmds), std::end(prefix_cmds), + [&cmd_string](const wchar_t *prefix) { + return cmd_string == prefix; + }) != std::end(prefix_cmds); + if (!is_subcommand) break; + tokens.erase(tokens.begin()); + }; + } + // Empty process (cursor is after one of ;, &, |, \n, &&, || modulo whitespace). + if (tokens.empty()) { + // Don't autosuggest anything based on the empty string (generalizes #1631). + if (flags & completion_request_t::autosuggestion) return; - // Get the actual command string. - current_command = cmd_node.get_source(cmd); + // fish has been using generic completion of filenames relative to the current directory. + // TODO there's some discussion in issue #5418 on what else we could do here. + complete_param_expand(L"", true /* do_file */); + return; + } - // Check the decoration. - switch (get_decoration(plain_statement)) { - case parse_statement_decoration_none: { - use_command = true; - use_function = true; - use_builtin = true; - use_implicit_cd = true; - use_abbr = true; - break; - } - case parse_statement_decoration_command: - case parse_statement_decoration_exec: { - use_command = true; - use_function = false; - use_builtin = false; - use_implicit_cd = false; - use_abbr = false; - break; - } - case parse_statement_decoration_builtin: { - use_command = false; - use_function = false; - use_builtin = true; - use_implicit_cd = false; - use_abbr = false; - break; - } - } + const tok_t &cmd_tok = tokens.front(); + const tok_t &cur_tok = tokens.back(); + // Since fish does not currently support redirect in command position, we return here. + if (cmd_tok.type != token_type_t::string) return; + if (cur_tok.type == token_type_t::error) return; + for (const auto &tok : tokens) { // If there was an error, it was in the last token. + assert(tok.type == token_type_t::string || tok.type == token_type_t::redirect); + } + // If we are completing a variable name or a tilde expansion user name, we do that and return. + // No need for any other completions. + const wcstring current_token = cur_tok.get_source(cmd); + if (try_complete_variable(current_token) || try_complete_user(current_token)) { + return; + } - if (cmd_node.location_in_or_at_end_of_source_range(pos)) { - // Complete command filename. - complete_cmd(current_token, use_function, use_builtin, use_command, use_implicit_cd); - if (use_abbr) complete_abbr(current_token); + if (cmd_tok.location_in_or_at_end_of_source_range(cursor_pos)) { + // Complete command filename. + complete_cmd(current_token); + complete_abbr(current_token); + return; + } + // See whether we are in an argument, in a redirection or in the whitespace in between. + bool in_redirection = cur_tok.type == token_type_t::redirect; + + bool had_ddash = false; + wcstring current_argument, previous_argument; + if (cur_tok.type == token_type_t::string && + cur_tok.location_in_or_at_end_of_source_range(position_in_statement)) { + // If the cursor is in whitespace, then the "current" argument is empty and the + // previous argument is the matching one. But if the cursor was in or at the end + // of the argument, then the current argument is the matching one, and the + // previous argument is the one before it. + bool cursor_in_whitespace = !cur_tok.location_in_or_at_end_of_source_range(cursor_pos); + if (cursor_in_whitespace) { + current_argument.clear(); + previous_argument = current_token; } else { - // Get all the arguments. - arg_list_t all_arguments = plain_statement.descendants(); - - // See whether we are in an argument. We may also be in a redirection, or nothing at - // all. - maybe_t matching_arg_index = - find_argument_containing_position(all_arguments, position_in_statement); - - bool had_ddash = false; - wcstring current_argument, previous_argument; - if (matching_arg_index) { - const wcstring matching_arg = all_arguments.at(*matching_arg_index).get_source(cmd); - - // If the cursor is in whitespace, then the "current" argument is empty and the - // previous argument is the matching one. But if the cursor was in or at the end - // of the argument, then the current argument is the matching one, and the - // previous argument is the one before it. - bool cursor_in_whitespace = - !plain_statement.location_in_or_at_end_of_source_range(pos); - if (cursor_in_whitespace) { - current_argument.clear(); - previous_argument = matching_arg; - } else { - current_argument = matching_arg; - if (*matching_arg_index > 0) { - previous_argument = - all_arguments.at(*matching_arg_index - 1).get_source(cmd); - } - } - - // Check to see if we have a preceding double-dash. - for (size_t i = 0; i < *matching_arg_index; i++) { - if (all_arguments.at(i).get_source(cmd) == L"--") { - had_ddash = true; - break; - } - } + current_argument = current_token; + if (tokens.size() >= 2) { + tok_t prev_tok = tokens.at(tokens.size() - 2); + if (prev_tok.type == token_type_t::string) previous_argument = prev_tok.get_source(cmd); } + } - // If we are not in an argument, we may be in a redirection. - bool in_redirection = false; - if (!matching_arg_index) { - if (tnode_t::find_node_matching_source_location( - &tree, position_in_statement, plain_statement)) { - in_redirection = true; - } + // Check to see if we have a preceding double-dash. + for (size_t i = 0; i < tokens.size() - 1; i++) { + if (tokens.at(i).get_source(cmd) == L"--") { + had_ddash = true; + break; } - - bool do_file = false, handle_as_special_cd = false; - if (in_redirection) { - do_file = true; - } else { - // Try completing as an argument. - wcstring current_command_unescape, previous_argument_unescape, - current_argument_unescape; - if (unescape_string(current_command, ¤t_command_unescape, UNESCAPE_DEFAULT) && - unescape_string(previous_argument, &previous_argument_unescape, - UNESCAPE_DEFAULT) && - unescape_string(current_argument, ¤t_argument_unescape, - UNESCAPE_INCOMPLETE)) { - // Have to walk over the command and its entire wrap chain. If any command - // disables do_file, then they all do. - do_file = true; - auto receiver = [&](const wcstring &cmd, const wcstring &cmdline, - size_t depth) { - // Perhaps set a transient commandline so that custom completions - // buitin_commandline will refer to the wrapped command. But not if - // we're doing autosuggestions. - bool wants_transient = - depth > 0 && !(flags & completion_request_t::autosuggestion); - if (wants_transient) { - parser->libdata().transient_commandlines.push_back(cmdline); - } - // Now invoke any custom completions for this command. - if (!complete_param(cmd, previous_argument_unescape, - current_argument_unescape, !had_ddash)) { - do_file = false; - } - if (wants_transient) { - parser->libdata().transient_commandlines.pop_back(); - } - }; - walk_wrap_chain(cmd, *cmd_node.source_range(), receiver); - } - - // Hack. If we're cd, handle it specially (issue #1059, others). - handle_as_special_cd = (current_command_unescape == L"cd"); - - // And if we're autosuggesting, and the token is empty, don't do file suggestions. - if ((flags & completion_request_t::autosuggestion) && - current_argument_unescape.empty()) { - do_file = false; - } - } - - // This function wants the unescaped string. - complete_param_expand(current_token, do_file, handle_as_special_cd); - - // Escape '[' in the argument before completing it. - escape_opening_brackets(current_argument); - - // Lastly mark any completions that appear to already be present in arguments. - mark_completions_duplicating_arguments(current_token, all_arguments); } } + + bool do_file = false, handle_as_special_cd = false; + if (in_redirection) { + do_file = true; + } else { + // Try completing as an argument. + wcstring current_command = cmd_tok.get_source(cmd), current_command_unescape, + previous_argument_unescape, current_argument_unescape; + if (unescape_string(current_command, ¤t_command_unescape, UNESCAPE_DEFAULT) && + unescape_string(previous_argument, &previous_argument_unescape, UNESCAPE_DEFAULT) && + unescape_string(current_argument, ¤t_argument_unescape, UNESCAPE_INCOMPLETE)) { + // Have to walk over the command and its entire wrap chain. If any command + // disables do_file, then they all do. + do_file = true; + auto receiver = [&](const wcstring &cmd, const wcstring &cmdline, size_t depth) { + // Perhaps set a transient commandline so that custom completions + // buitin_commandline will refer to the wrapped command. But not if + // we're doing autosuggestions. + bool wants_transient = depth > 0 && !(flags & completion_request_t::autosuggestion); + if (wants_transient) { + parser->libdata().transient_commandlines.push_back(cmdline); + } + // Now invoke any custom completions for this command. + if (!complete_param(cmd, previous_argument_unescape, current_argument_unescape, + !had_ddash)) { + do_file = false; + } + if (wants_transient) { + 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); + } + + // Hack. If we're cd, handle it specially (issue #1059, others). + handle_as_special_cd = (current_command_unescape == L"cd"); + + // And if we're autosuggesting, and the token is empty, don't do file suggestions. + if ((flags & completion_request_t::autosuggestion) && current_argument_unescape.empty()) { + do_file = false; + } + } + + // This function wants the unescaped string. + complete_param_expand(current_argument, do_file, handle_as_special_cd); + + // Escape '[' in the argument before completing it. + escape_opening_brackets(current_argument); + + // Lastly mark any completions that appear to already be present in arguments. + mark_completions_duplicating_arguments(current_token, tokens); } void complete(const wcstring &cmd_with_subcmds, std::vector *out_comps, diff --git a/src/parse_tree.h b/src/parse_tree.h index d813249cb..1eb654b72 100644 --- a/src/parse_tree.h +++ b/src/parse_tree.h @@ -25,6 +25,11 @@ typedef uint32_t source_offset_t; constexpr source_offset_t SOURCE_OFFSET_INVALID = static_cast(-1); +struct source_range_t { + uint32_t start; + uint32_t length; +}; + /// A struct representing the token type that we use internally. struct parse_token_t { enum parse_token_type_t type; // The type of the token as represented by the parser @@ -137,6 +142,11 @@ class parse_node_t { return this->flags & parse_node_flag_preceding_escaped_nl; } + source_range_t source_range() const { + assert(has_source()); + return {source_start, source_length}; + } + /// Gets source for the node, or the empty string if it has no source. wcstring get_source(const wcstring &str) const { if (!has_source()) diff --git a/src/parse_util.cpp b/src/parse_util.cpp index eb7a94f59..90846f474 100644 --- a/src/parse_util.cpp +++ b/src/parse_util.cpp @@ -288,8 +288,9 @@ void parse_util_cmdsubst_extent(const wchar_t *buff, size_t cursor_pos, const wc } /// Get the beginning and end of the job or process definition under the cursor. -static void job_or_process_extent(const wchar_t *buff, size_t cursor_pos, const wchar_t **a, - const wchar_t **b, bool process) { +static void job_or_process_extent(bool process, const wchar_t *buff, size_t cursor_pos, + const wchar_t **a, const wchar_t **b, + std::vector *tokens) { assert(buff && "Null buffer"); const wchar_t *begin = nullptr, *end = nullptr; int finished = 0; @@ -323,29 +324,33 @@ static void job_or_process_extent(const wchar_t *buff, size_t cursor_pos, const case token_type_t::end: case token_type_t::background: case token_type_t::andand: - case token_type_t::oror: { + case token_type_t::oror: + case token_type_t::comment: { if (tok_begin >= pos) { finished = 1; if (b) *b = (wchar_t *)begin + tok_begin; } else { + // Statement at cursor might start after this token. if (a) *a = (wchar_t *)begin + tok_begin + token->length; + if (tokens) tokens->clear(); } - break; + continue; // Do not add this to tokens } default: { break; } } + if (tokens) tokens->push_back(*token); } } void parse_util_process_extent(const wchar_t *buff, size_t pos, const wchar_t **a, - const wchar_t **b) { - job_or_process_extent(buff, pos, a, b, true); + const wchar_t **b, std::vector *tokens) { + job_or_process_extent(true, buff, pos, a, b, tokens); } void parse_util_job_extent(const wchar_t *buff, size_t pos, const wchar_t **a, const wchar_t **b) { - job_or_process_extent(buff, pos, a, b, false); + job_or_process_extent(false, buff, pos, a, b, nullptr); } void parse_util_token_extent(const wchar_t *buff, size_t cursor_pos, const wchar_t **tok_begin, diff --git a/src/parse_util.h b/src/parse_util.h index a3e7a849e..3ce8fb3ab 100644 --- a/src/parse_util.h +++ b/src/parse_util.h @@ -55,10 +55,11 @@ void parse_util_cmdsubst_extent(const wchar_t *buff, size_t cursor_pos, const wc /// /// \param buff the string to search for subshells /// \param cursor_pos the position of the cursor -/// \param a the start of the searched string -/// \param b the end of the searched string +/// \param a the start of the process +/// \param b the end of the process +/// \param tokens the tokens in the process void parse_util_process_extent(const wchar_t *buff, size_t cursor_pos, const wchar_t **a, - const wchar_t **b); + const wchar_t **b, std::vector *tokens); /// Find the beginning and end of the job definition under the cursor /// diff --git a/src/tnode.h b/src/tnode.h index e7b2939e9..491541336 100644 --- a/src/tnode.h +++ b/src/tnode.h @@ -5,11 +5,6 @@ #include "parse_grammar.h" #include "parse_tree.h" -struct source_range_t { - uint32_t start; - uint32_t length; -}; - // Check if a child type is possible for a parent type at a given index. template constexpr bool child_type_possible_at_index() { diff --git a/src/tokenizer.h b/src/tokenizer.h index 964e9afd6..d1689f0a5 100644 --- a/src/tokenizer.h +++ b/src/tokenizer.h @@ -87,6 +87,13 @@ struct tok_t { // Construct from a token type. explicit tok_t(token_type_t type); + + /// Returns whether the given location is within the source range or at its end. + bool location_in_or_at_end_of_source_range(size_t loc) const { + return offset <= loc && loc - offset <= length; + } + /// Gets source for the token, or the empty string if it has no source. + wcstring get_source(const wcstring &str) const { return {str, offset, length}; } }; /// The tokenizer struct. diff --git a/tests/checks/complete.fish b/tests/checks/complete.fish index 48095f39d..93d5ba31d 100644 --- a/tests/checks/complete.fish +++ b/tests/checks/complete.fish @@ -124,3 +124,29 @@ complete -C'foo -y' | string match -- -y-single-long # CHECK: -zARGZ complete -C'foo -z' + +# Builtins (with subcommands; #2705) +complete -c complete_test_subcommand -n 'test (commandline -op)[1] = complete_test_subcommand' -xa 'ok' +complete -C'not complete_test_subcommand ' +# CHECK: ok +complete -C'echo; and complete_test_subcommand ' +# CHECK: ok +complete -C'or complete_test_subcommand ' +# CHECK: ok +complete -C'echo && command complete_test_subcommand ' +# CHECK: ok +complete -C'echo || exec complete_test_subcommand ' +# CHECK: ok +complete -C'echo | builtin complete_test_subcommand ' +# CHECK: ok +complete -C'echo & complete_test_subcommand ' +# CHECK: ok +complete -C'if while begin begin complete_test_subcommand ' +# CHECK: ok + +complete -C'for _ in ' | string collect >&- && echo completed some files +# CHECK: completed some files + +# function; #5415 +complete -C'function : --arg' +# CHECK: --argument-names {{.*}}