diff options
Diffstat (limited to '')
-rw-r--r-- | lib/widget/input_complete.c | 1484 |
1 files changed, 1484 insertions, 0 deletions
diff --git a/lib/widget/input_complete.c b/lib/widget/input_complete.c new file mode 100644 index 0000000..94a4c3b --- /dev/null +++ b/lib/widget/input_complete.c @@ -0,0 +1,1484 @@ +/* + Input line filename/username/hostname/variable/command completion. + (Let mc type for you...) + + Copyright (C) 1995-2023 + Free Software Foundation, Inc. + + Written by: + Jakub Jelinek, 1995 + Slava Zanko <slavazanko@gmail.com>, 2013 + Andrew Borodin <aborodin@vmail.ru>, 2013-2022 + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** \file lib/widget/input_complete.c + * \brief Source: Input line filename/username/hostname/variable/command completion + */ + +#include <config.h> + +#include <ctype.h> +#include <limits.h> /* MB_LEN_MAX */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <dirent.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <pwd.h> +#include <unistd.h> + +#include "lib/global.h" + +#include "lib/tty/tty.h" +#include "lib/tty/key.h" /* XCTRL and ALT macros */ +#include "lib/vfs/vfs.h" +#include "lib/strescape.h" +#include "lib/strutil.h" +#include "lib/util.h" +#include "lib/widget.h" + +/*** global variables ****************************************************************************/ + +/* Linux declares environ in <unistd.h>, so don't repeat it here. */ +#if (!(defined(__linux__) && defined (__USE_GNU)) && !defined(__CYGWIN__)) +extern char **environ; +#endif + +/*** file scope macro definitions ****************************************************************/ + +/* #define DO_COMPLETION_DEBUG */ +#ifdef DO_COMPLETION_DEBUG +#define SHOW_C_CTX(func) fprintf(stderr, "%s: text='%s' flags=%s\n", func, text, show_c_flags(flags)) +#else +#define SHOW_C_CTX(func) +#endif /* DO_CMPLETION_DEBUG */ + +#define DO_INSERTION 1 +#define DO_QUERY 2 + +/*** file scope type declarations ****************************************************************/ + +typedef char *CompletionFunction (const char *text, int state, input_complete_t flags); + +typedef struct +{ + size_t in_command_position; + char *word; + char *p; + char *q; + char *r; + gboolean is_cd; + input_complete_t flags; +} try_complete_automation_state_t; + +/*** forward declarations (file scope functions) *************************************************/ + +char **try_complete (char *text, int *lc_start, int *lc_end, input_complete_t flags); +void complete_engine_fill_completions (WInput * in); + +/*** file scope variables ************************************************************************/ + +static char **hosts = NULL; +static char **hosts_p = NULL; +static int hosts_alloclen = 0; + +static int complete_height, complete_width; +static WInput *input; +static int min_end; +static int start = 0; +static int end = 0; + +/* --------------------------------------------------------------------------------------------- */ +/*** file scope functions ************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +#ifdef DO_COMPLETION_DEBUG +/** + * Useful to print/debug completion flags + */ +static const char * +show_c_flags (input_complete_t flags) +{ + static char s_cf[] = "FHCVUDS"; + + s_cf[0] = (flags & INPUT_COMPLETE_FILENAMES) != 0 ? 'F' : ' '; + s_cf[1] = (flags & INPUT_COMPLETE_HOSTNAMES) != 0 ? 'H' : ' '; + s_cf[2] = (flags & INPUT_COMPLETE_COMMANDS) != 0 ? 'C' : ' '; + s_cf[3] = (flags & INPUT_COMPLETE_VARIABLES) != 0 ? 'V' : ' '; + s_cf[4] = (flags & INPUT_COMPLETE_USERNAMES) != 0 ? 'U' : ' '; + s_cf[5] = (flags & INPUT_COMPLETE_CD) != 0 ? 'D' : ' '; + s_cf[6] = (flags & INPUT_COMPLETE_SHELL_ESC) != 0 ? 'S' : ' '; + + return s_cf; +} +#endif /* DO_CMPLETION_DEBUG */ + +/* --------------------------------------------------------------------------------------------- */ + +static char * +filename_completion_function (const char *text, int state, input_complete_t flags) +{ + static DIR *directory = NULL; + static char *filename = NULL; + static char *dirname = NULL; + static char *users_dirname = NULL; + static size_t filename_len = 0; + static vfs_path_t *dirname_vpath = NULL; + + gboolean isdir = TRUE, isexec = FALSE; + struct vfs_dirent *entry = NULL; + + SHOW_C_CTX ("filename_completion_function"); + + if (text != NULL && (flags & INPUT_COMPLETE_SHELL_ESC) != 0) + { + char *u_text; + char *result; + char *e_result; + + u_text = strutils_shell_unescape (text); + + result = filename_completion_function (u_text, state, flags & (~INPUT_COMPLETE_SHELL_ESC)); + g_free (u_text); + + e_result = strutils_shell_escape (result); + g_free (result); + + return e_result; + } + + /* If we're starting the match process, initialize us a bit. */ + if (state == 0) + { + const char *temp; + + g_free (dirname); + g_free (filename); + g_free (users_dirname); + vfs_path_free (dirname_vpath, TRUE); + + if ((*text != '\0') && (temp = strrchr (text, PATH_SEP)) != NULL) + { + filename = g_strdup (++temp); + dirname = g_strndup (text, temp - text); + } + else + { + dirname = g_strdup ("."); + filename = g_strdup (text); + } + + /* We aren't done yet. We also support the "~user" syntax. */ + + /* Save the version of the directory that the user typed. */ + users_dirname = dirname; + dirname = tilde_expand (dirname); + canonicalize_pathname (dirname); + dirname_vpath = vfs_path_from_str (dirname); + + /* Here we should do something with variable expansion + and `command`. + Maybe a dream - UNIMPLEMENTED yet. */ + + directory = mc_opendir (dirname_vpath); + filename_len = strlen (filename); + } + + /* Now that we have some state, we can read the directory. */ + + while (directory != NULL && (entry = mc_readdir (directory)) != NULL) + { + if (!str_is_valid_string (entry->d_name)) + continue; + + /* Special case for no filename. + All entries except "." and ".." match. */ + if (filename_len == 0) + { + if (DIR_IS_DOT (entry->d_name) || DIR_IS_DOTDOT (entry->d_name)) + continue; + } + else + { + /* Otherwise, if these match up to the length of filename, then + it may be a match. */ + if ((entry->d_name[0] != filename[0]) || + ((NLENGTH (entry)) < filename_len) || + strncmp (filename, entry->d_name, filename_len) != 0) + continue; + } + + isdir = TRUE; + isexec = FALSE; + + { + struct stat tempstat; + vfs_path_t *tmp_vpath; + + tmp_vpath = vfs_path_build_filename (dirname, entry->d_name, (char *) NULL); + + /* Unix version */ + if (mc_stat (tmp_vpath, &tempstat) == 0) + { + uid_t my_uid; + gid_t my_gid; + + my_uid = getuid (); + my_gid = getgid (); + + if (!S_ISDIR (tempstat.st_mode)) + { + isdir = FALSE; + + if ((my_uid == 0 && (tempstat.st_mode & 0111) != 0) || + (my_uid == tempstat.st_uid && (tempstat.st_mode & 0100) != 0) || + (my_gid == tempstat.st_gid && (tempstat.st_mode & 0010) != 0) || + (tempstat.st_mode & 0001) != 0) + isexec = TRUE; + } + } + else + { + /* stat failed, strange. not a dir in any case */ + isdir = FALSE; + } + vfs_path_free (tmp_vpath, TRUE); + } + + if ((flags & INPUT_COMPLETE_COMMANDS) != 0 && (isexec || isdir)) + break; + if ((flags & INPUT_COMPLETE_CD) != 0 && isdir) + break; + if ((flags & INPUT_COMPLETE_FILENAMES) != 0) + break; + } + + if (entry == NULL) + { + if (directory != NULL) + { + mc_closedir (directory); + directory = NULL; + } + MC_PTR_FREE (dirname); + vfs_path_free (dirname_vpath, TRUE); + dirname_vpath = NULL; + MC_PTR_FREE (filename); + MC_PTR_FREE (users_dirname); + return NULL; + } + + { + GString *temp; + + temp = g_string_sized_new (16); + + if (users_dirname != NULL && (users_dirname[0] != '.' || users_dirname[1] != '\0')) + { + g_string_append (temp, users_dirname); + + /* We need a '/' at the end. */ + if (!IS_PATH_SEP (temp->str[temp->len - 1])) + g_string_append_c (temp, PATH_SEP); + } + g_string_append (temp, entry->d_name); + if (isdir) + g_string_append_c (temp, PATH_SEP); + + return g_string_free (temp, FALSE); + } +} + +/* --------------------------------------------------------------------------------------------- */ +/** We assume here that text[0] == '~' , if you want to call it in another way, + you have to change the code */ + +static char * +username_completion_function (const char *text, int state, input_complete_t flags) +{ + static struct passwd *entry = NULL; + static size_t userlen = 0; + + (void) flags; + SHOW_C_CTX ("username_completion_function"); + + if (text[0] == '\\' && text[1] == '~') + text++; + if (state == 0) + { /* Initialization stuff */ + setpwent (); + userlen = strlen (text + 1); + } + + while ((entry = getpwent ()) != NULL) + { + /* Null usernames should result in all users as possible completions. */ + if (userlen == 0) + break; + if (text[1] == entry->pw_name[0] && strncmp (text + 1, entry->pw_name, userlen) == 0) + break; + } + + if (entry != NULL) + return g_strconcat ("~", entry->pw_name, PATH_SEP_STR, (char *) NULL); + + endpwent (); + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ +/** We assume text [0] == '$' and want to have a look at text [1], if it is + equal to '{', so that we should append '}' at the end */ + +static char * +variable_completion_function (const char *text, int state, input_complete_t flags) +{ + static char **env_p = NULL; + static gboolean isbrace = FALSE; + static size_t varlen = 0; + const char *p = NULL; + + (void) flags; + SHOW_C_CTX ("variable_completion_function"); + + if (state == 0) + { /* Initialization stuff */ + isbrace = (text[1] == '{'); + varlen = strlen (text + 1 + isbrace); + env_p = environ; + } + + while (*env_p != NULL) + { + p = strchr (*env_p, '='); + if (p != NULL && ((size_t) (p - *env_p) >= varlen) + && strncmp (text + 1 + isbrace, *env_p, varlen) == 0) + break; + env_p++; + } + + if (*env_p == NULL) + return NULL; + + { + GString *temp; + + temp = g_string_new_len (*env_p, p - *env_p); + + if (isbrace) + { + g_string_prepend_c (temp, '{'); + g_string_append_c (temp, '}'); + } + g_string_prepend_c (temp, '$'); + + env_p++; + + return g_string_free (temp, FALSE); + } +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +fetch_hosts (const char *filename) +{ + FILE *file; + char buffer[256]; + char *name; + char *lc_start; + char *bi; + + file = fopen (filename, "r"); + if (file == NULL) + return; + + while (fgets (buffer, sizeof (buffer) - 1, file) != NULL) + { + /* Skip to first character. */ + for (bi = buffer; bi[0] != '\0' && str_isspace (bi); str_next_char (&bi)) + ; + + /* Ignore comments... */ + if (bi[0] == '#') + continue; + + /* Handle $include. */ + if (strncmp (bi, "$include ", 9) == 0) + { + char *includefile, *t; + + /* Find start of filename. */ + includefile = bi + 9; + while (*includefile != '\0' && whitespace (*includefile)) + includefile++; + t = includefile; + + /* Find end of filename. */ + while (t[0] != '\0' && !str_isspace (t)) + str_next_char (&t); + *t = '\0'; + + fetch_hosts (includefile); + continue; + } + + /* Skip IP #s. */ + while (bi[0] != '\0' && !str_isspace (bi)) + str_next_char (&bi); + + /* Get the host names separated by white space. */ + while (bi[0] != '\0' && bi[0] != '#') + { + while (bi[0] != '\0' && str_isspace (bi)) + str_next_char (&bi); + if (bi[0] == '#') + continue; + for (lc_start = bi; bi[0] != '\0' && !str_isspace (bi); str_next_char (&bi)) + ; + + if (bi == lc_start) + continue; + + name = g_strndup (lc_start, bi - lc_start); + + { + char **host_p; + int j; + + j = hosts_p - hosts; + + if (j >= hosts_alloclen) + { + hosts_alloclen += 30; + hosts = g_renew (char *, hosts, hosts_alloclen + 1); + hosts_p = hosts + j; + } + + for (host_p = hosts; host_p < hosts_p; host_p++) + if (strcmp (name, *host_p) == 0) + break; /* We do not want any duplicates */ + + if (host_p == hosts_p) + { + *(hosts_p++) = name; + *hosts_p = NULL; + } + else + g_free (name); + } + } + } + + fclose (file); +} + +/* --------------------------------------------------------------------------------------------- */ + +static char * +hostname_completion_function (const char *text, int state, input_complete_t flags) +{ + static char **host_p = NULL; + static size_t textstart = 0; + static size_t textlen = 0; + + (void) flags; + SHOW_C_CTX ("hostname_completion_function"); + + if (state == 0) + { /* Initialization stuff */ + const char *p; + + g_strfreev (hosts); + hosts_alloclen = 30; + hosts = g_new (char *, hosts_alloclen + 1); + *hosts = NULL; + hosts_p = hosts; + p = getenv ("HOSTFILE"); + fetch_hosts (p != NULL ? p : "/etc/hosts"); + host_p = hosts; + textstart = (*text == '@') ? 1 : 0; + textlen = strlen (text + textstart); + } + + for (; *host_p != NULL; host_p++) + { + if (textlen == 0) + break; /* Match all of them */ + if (strncmp (text + textstart, *host_p, textlen) == 0) + break; + } + + if (*host_p == NULL) + { + g_strfreev (hosts); + hosts = NULL; + return NULL; + } + + { + GString *temp; + + temp = g_string_sized_new (8); + + if (textstart != 0) + g_string_append_c (temp, '@'); + g_string_append (temp, *host_p); + host_p++; + + return g_string_free (temp, FALSE); + } +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * This is the function to call when the word to complete is in a position + * where a command word can be found. It looks around $PATH, looking for + * commands that match. It also scans aliases, function names, and the + * table of shell built-ins. + */ + +static char * +command_completion_function (const char *text, int state, input_complete_t flags) +{ + static const char *path_end = NULL; + static gboolean isabsolute = FALSE; + static int phase = 0; + static size_t text_len = 0; + static const char *const *words = NULL; + static char *path = NULL; + static char *cur_path = NULL; + static char *cur_word = NULL; + static int init_state = 0; + static const char *const bash_reserved[] = { + "if", "then", "else", "elif", "fi", "case", "esac", "for", + "select", "while", "until", "do", "done", "in", "function", 0 + }; + static const char *const bash_builtins[] = { + "alias", "bg", "bind", "break", "builtin", "cd", "command", + "continue", "declare", "dirs", "echo", "enable", "eval", + "exec", "exit", "export", "fc", "fg", "getopts", "hash", + "help", "history", "jobs", "kill", "let", "local", "logout", + "popd", "pushd", "pwd", "read", "readonly", "return", "set", + "shift", "source", "suspend", "test", "times", "trap", "type", + "typeset", "ulimit", "umask", "unalias", "unset", "wait", 0 + }; + + char *u_text; + char *p, *found; + + SHOW_C_CTX ("command_completion_function"); + + if ((flags & INPUT_COMPLETE_COMMANDS) == 0) + return NULL; + + u_text = strutils_shell_unescape (text); + flags &= ~INPUT_COMPLETE_SHELL_ESC; + + if (state == 0) + { /* Initialize us a little bit */ + isabsolute = strchr (u_text, PATH_SEP) != NULL; + if (!isabsolute) + { + words = bash_reserved; + phase = 0; + text_len = strlen (u_text); + + if (path == NULL) + { + path = g_strdup (getenv ("PATH")); + if (path != NULL) + { + p = path; + path_end = strchr (p, '\0'); + while ((p = strchr (p, PATH_ENV_SEP)) != NULL) + *p++ = '\0'; + } + } + } + } + + if (isabsolute) + { + p = filename_completion_function (u_text, state, flags); + + if (p != NULL) + { + char *temp_p = p; + + p = strutils_shell_escape (p); + g_free (temp_p); + } + + g_free (u_text); + return p; + } + + found = NULL; + switch (phase) + { + case 0: /* Reserved words */ + for (; *words != NULL; words++) + if (strncmp (*words, u_text, text_len) == 0) + { + g_free (u_text); + return g_strdup (*(words++)); + } + phase++; + words = bash_builtins; + MC_FALLTHROUGH; + case 1: /* Builtin commands */ + for (; *words != NULL; words++) + if (strncmp (*words, u_text, text_len) == 0) + { + g_free (u_text); + return g_strdup (*(words++)); + } + phase++; + if (path == NULL) + break; + cur_path = path; + cur_word = NULL; + MC_FALLTHROUGH; + case 2: /* And looking through the $PATH */ + while (found == NULL) + { + if (cur_word == NULL) + { + char *expanded; + + if (cur_path >= path_end) + break; + expanded = tilde_expand (*cur_path != '\0' ? cur_path : "."); + cur_word = mc_build_filename (expanded, u_text, (char *) NULL); + g_free (expanded); + cur_path = strchr (cur_path, '\0') + 1; + init_state = state; + } + found = filename_completion_function (cur_word, state - init_state, flags); + if (found == NULL) + MC_PTR_FREE (cur_word); + } + MC_FALLTHROUGH; + default: + break; + } + + if (found == NULL) + MC_PTR_FREE (path); + else + { + p = strrchr (found, PATH_SEP); + if (p != NULL) + { + char *tmp = found; + + found = strutils_shell_escape (p + 1); + g_free (tmp); + } + } + + g_free (u_text); + return found; +} + +/* --------------------------------------------------------------------------------------------- */ + +static int +match_compare (const void *a, const void *b) +{ + return strcmp (*(char *const *) a, *(char *const *) b); +} + +/* --------------------------------------------------------------------------------------------- */ +/** Returns an array of char * matches with the longest common denominator + in the 1st entry. Then a NULL terminated list of different possible + completions follows. + You have to supply your own CompletionFunction with the word you + want to complete as the first argument and an count of previous matches + as the second. + In case no matches were found we return NULL. */ + +static char ** +completion_matches (const char *text, CompletionFunction entry_function, input_complete_t flags) +{ + /* Number of slots in match_list. */ + size_t match_list_size = 30; + /* The list of matches. */ + char **match_list; + /* Number of matches actually found. */ + size_t matches = 0; + + /* Temporary string binder. */ + char *string; + + match_list = g_new (char *, match_list_size + 1); + match_list[1] = NULL; + + while ((string = (*entry_function) (text, matches, flags)) != NULL) + { + if (matches + 1 == match_list_size) + { + match_list_size += 30; + match_list = (char **) g_renew (char *, match_list, match_list_size + 1); + } + match_list[++matches] = string; + match_list[matches + 1] = NULL; + } + + /* If there were any matches, then look through them finding out the + lowest common denominator. That then becomes match_list[0]. */ + if (matches == 0) + MC_PTR_FREE (match_list); /* There were no matches. */ + else + { + /* If only one match, just use that. */ + if (matches == 1) + { + match_list[0] = match_list[1]; + match_list[1] = NULL; + } + else + { + size_t i = 1; + int low = 4096; /* Count of max-matched characters. */ + size_t j; + + qsort (match_list + 1, matches, sizeof (char *), match_compare); + + /* And compare each member of the list with + the next, finding out where they stop matching. + If we find two equal strings, we have to put one away... */ + + j = i + 1; + while (j < matches + 1) + { + char *si, *sj; + char *ni, *nj; + + for (si = match_list[i], sj = match_list[j]; si[0] != '\0' && sj[0] != '\0';) + { + + ni = str_get_next_char (si); + nj = str_get_next_char (sj); + + if (ni - si != nj - sj) + break; + if (strncmp (si, sj, ni - si) != 0) + break; + + si = ni; + sj = nj; + } + + if (si[0] == '\0' && sj[0] == '\0') + { /* Two equal strings */ + g_free (match_list[j]); + j++; + if (j > matches) + break; + continue; /* Look for a run of equal strings */ + } + else if (low > si - match_list[i]) + low = si - match_list[i]; + if (i + 1 != j) /* So there's some gap */ + match_list[i + 1] = match_list[j]; + i++; + j++; + } + matches = i; + match_list[matches + 1] = NULL; + match_list[0] = g_strndup (match_list[1], low); + } + } + + return match_list; +} + +/* --------------------------------------------------------------------------------------------- */ +/** Check if directory completion is needed */ +static gboolean +check_is_cd (const char *text, int lc_start, input_complete_t flags) +{ + const char *p, *q; + + SHOW_C_CTX ("check_is_cd"); + + if ((flags & INPUT_COMPLETE_CD) == 0) + return FALSE; + + /* Skip initial spaces */ + p = text; + q = text + lc_start; + while (p < q && p[0] != '\0' && str_isspace (p)) + str_cnext_char (&p); + + /* Check if the command is "cd" and the cursor is after it */ + return (p[0] == 'c' && p[1] == 'd' && str_isspace (p + 2) && p + 2 < q); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +try_complete_commands_prepare (try_complete_automation_state_t * state, char *text, int *lc_start) +{ + const char *command_separator_chars = ";|&{(`"; + char *ti; + + if (*lc_start == 0) + ti = text; + else + { + ti = str_get_prev_char (&text[*lc_start]); + while (ti > text && whitespace (ti[0])) + str_prev_char (&ti); + } + + if (ti == text) + state->in_command_position++; + else if (strchr (command_separator_chars, ti[0]) != NULL) + { + state->in_command_position++; + if (ti != text) + { + int this_char, prev_char; + + /* Handle the two character tokens '>&', '<&', and '>|'. + We are not in a command position after one of these. */ + this_char = ti[0]; + prev_char = str_get_prev_char (ti)[0]; + + /* Quoted */ + if ((this_char == '&' && (prev_char == '<' || prev_char == '>')) + || (this_char == '|' && prev_char == '>') || (ti != text + && str_get_prev_char (ti)[0] == '\\')) + state->in_command_position = 0; + } + } +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +try_complete_find_start_sign (try_complete_automation_state_t * state) +{ + if ((state->flags & INPUT_COMPLETE_COMMANDS) != 0) + state->p = strrchr (state->word, '`'); + if ((state->flags & (INPUT_COMPLETE_COMMANDS | INPUT_COMPLETE_VARIABLES)) != 0) + { + state->q = strrchr (state->word, '$'); + + /* don't substitute variable in \$ case */ + if (strutils_is_char_escaped (state->word, state->q)) + { + /* drop '\\' */ + str_move (state->q - 1, state->q); + /* adjust flags */ + state->flags &= ~INPUT_COMPLETE_VARIABLES; + state->q = NULL; + } + } + if ((state->flags & INPUT_COMPLETE_HOSTNAMES) != 0) + state->r = strrchr (state->word, '@'); + if (state->q != NULL && state->q[1] == '(' && (state->flags & INPUT_COMPLETE_COMMANDS) != 0) + { + if (state->q > state->p) + state->p = str_get_next_char (state->q); + state->q = NULL; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +static char ** +try_complete_all_possible (try_complete_automation_state_t * state, char *text, int *lc_start) +{ + char **matches = NULL; + + if (state->in_command_position != 0) + { + SHOW_C_CTX ("try_complete:cmd_subst"); + matches = + completion_matches (state->word, command_completion_function, + state->flags & (~INPUT_COMPLETE_FILENAMES)); + } + else if ((state->flags & INPUT_COMPLETE_FILENAMES) != 0) + { + if (state->is_cd) + state->flags &= ~(INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_COMMANDS); + SHOW_C_CTX ("try_complete:filename_subst_1"); + matches = completion_matches (state->word, filename_completion_function, state->flags); + + if (matches == NULL && state->is_cd && !IS_PATH_SEP (*state->word) && *state->word != '~') + { + state->q = text + *lc_start; + for (state->p = text; + *state->p != '\0' && state->p < state->q && whitespace (*state->p); + str_next_char (&state->p)) + ; + if (strncmp (state->p, "cd", 2) == 0) + for (state->p += 2; + *state->p != '\0' && state->p < state->q && whitespace (*state->p); + str_next_char (&state->p)) + ; + if (state->p == state->q) + { + char *cdpath_ref, *cdpath; + char c; + + cdpath_ref = g_strdup (getenv ("CDPATH")); + cdpath = cdpath_ref; + c = (cdpath == NULL) ? '\0' : ':'; + + while (matches == NULL && c == ':') + { + char *s; + + s = strchr (cdpath, ':'); + /* cppcheck-suppress nullPointer */ + if (s == NULL) + s = strchr (cdpath, '\0'); + c = *s; + *s = '\0'; + if (*cdpath != '\0') + { + state->r = mc_build_filename (cdpath, state->word, (char *) NULL); + SHOW_C_CTX ("try_complete:filename_subst_2"); + matches = + completion_matches (state->r, filename_completion_function, + state->flags); + g_free (state->r); + } + *s = c; + cdpath = str_get_next_char (s); + } + g_free (cdpath_ref); + } + } + } + return matches; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +insert_text (WInput * in, char *text, ssize_t size) +{ + size_t text_len; + int buff_len; + ssize_t new_size; + + text_len = strlen (text); + buff_len = str_length (in->buffer->str); + if (size < 0) + size = (ssize_t) text_len; + else + size = MIN (size, (ssize_t) text_len); + + new_size = size + start - end; + if (new_size != 0) + { + /* make a hole within buffer */ + + size_t tail_len; + + tail_len = in->buffer->len - end; + if (tail_len != 0) + { + char *tail; + size_t hole_end; + + tail = g_strndup (in->buffer->str + end, tail_len); + + hole_end = end + new_size; + if (in->buffer->len < hole_end) + g_string_set_size (in->buffer, hole_end + tail_len); + + g_string_overwrite_len (in->buffer, hole_end, tail, tail_len); + + g_free (tail); + } + } + + g_string_overwrite_len (in->buffer, start, text, size); + + in->point += str_length (in->buffer->str) - buff_len; + input_update (in, TRUE); + end += new_size; + + return new_size != 0; +} + +/* --------------------------------------------------------------------------------------------- */ + +static cb_ret_t +complete_callback (Widget * w, Widget * sender, widget_msg_t msg, int parm, void *data) +{ + static int bl = 0; + + WGroup *g = GROUP (w); + WDialog *h = DIALOG (w); + + switch (msg) + { + case MSG_KEY: + switch (parm) + { + case KEY_LEFT: + case KEY_RIGHT: + bl = 0; + h->ret_value = 0; + dlg_close (h); + return MSG_HANDLED; + + case KEY_BACKSPACE: + bl = 0; + /* exit from completion list if input line is empty */ + if (end == 0) + { + h->ret_value = 0; + dlg_close (h); + } + /* Refill the list box and start again */ + else if (end == min_end) + { + end = str_get_prev_char (input->buffer->str + end) - input->buffer->str; + input_handle_char (input, parm); + h->ret_value = B_USER; + dlg_close (h); + } + else + { + int new_end; + int i; + GList *e; + + new_end = str_get_prev_char (input->buffer->str + end) - input->buffer->str; + + for (i = 0, e = listbox_get_first_link (LISTBOX (g->current->data)); + e != NULL; i++, e = g_list_next (e)) + { + WLEntry *le = LENTRY (e->data); + + if (strncmp (input->buffer->str + start, le->text, new_end - start) == 0) + { + listbox_set_current (LISTBOX (g->current->data), i); + end = new_end; + input_handle_char (input, parm); + widget_draw (WIDGET (g->current->data)); + break; + } + } + } + return MSG_HANDLED; + + default: + if (parm < 32 || parm > 255) + { + bl = 0; + if (widget_lookup_key (WIDGET (input), parm) != CK_Complete) + return MSG_NOT_HANDLED; + + if (end == min_end) + return MSG_HANDLED; + + /* This means we want to refill the list box and start again */ + h->ret_value = B_USER; + dlg_close (h); + } + else + { + static char buff[MB_LEN_MAX] = ""; + GList *e; + int i; + int need_redraw = 0; + int low = 4096; + char *last_text = NULL; + + buff[bl++] = (char) parm; + buff[bl] = '\0'; + + switch (str_is_valid_char (buff, bl)) + { + case -1: + bl = 0; + MC_FALLTHROUGH; + case -2: + return MSG_HANDLED; + default: + break; + } + + for (i = 0, e = listbox_get_first_link (LISTBOX (g->current->data)); + e != NULL; i++, e = g_list_next (e)) + { + WLEntry *le = LENTRY (e->data); + + if (strncmp (input->buffer->str + start, le->text, end - start) == 0 + && strncmp (le->text + end - start, buff, bl) == 0) + { + if (need_redraw == 0) + { + need_redraw = 1; + listbox_set_current (LISTBOX (g->current->data), i); + last_text = le->text; + } + else + { + char *si, *sl; + int si_num = 0; + int sl_num = 0; + + /* count symbols between start and end */ + for (si = le->text + start; si < le->text + end; + str_next_char (&si), si_num++) + ; + for (sl = last_text + start; sl < last_text + end; + str_next_char (&sl), sl_num++) + ; + + /* pointers to next symbols */ + si = &le->text[str_offset_to_pos (le->text, ++si_num)]; + sl = &last_text[str_offset_to_pos (last_text, ++sl_num)]; + + while (si[0] != '\0' && sl[0] != '\0') + { + char *nexti, *nextl; + + nexti = str_get_next_char (si); + nextl = str_get_next_char (sl); + + if (nexti - si != nextl - sl || strncmp (si, sl, nexti - si) != 0) + break; + + si = nexti; + sl = nextl; + + si_num++; + } + + last_text = le->text; + + si = &last_text[str_offset_to_pos (last_text, si_num)]; + if (low > si - last_text) + low = si - last_text; + + need_redraw = 2; + } + } + } + + if (need_redraw == 2) + { + insert_text (input, last_text, low); + widget_draw (WIDGET (g->current->data)); + } + else if (need_redraw == 1) + { + h->ret_value = B_ENTER; + dlg_close (h); + } + bl = 0; + } + } + return MSG_HANDLED; + + default: + return dlg_default_callback (w, sender, msg, parm, data); + } +} + +/* --------------------------------------------------------------------------------------------- */ + +/** Returns TRUE if the user would like to see us again */ +static gboolean +complete_engine (WInput * in, int what_to_do) +{ + if (in->completions != NULL && str_offset_to_pos (in->buffer->str, in->point) != end) + input_complete_free (in); + + if (in->completions == NULL) + complete_engine_fill_completions (in); + + if (in->completions == NULL) + tty_beep (); + else + { + if ((what_to_do & DO_INSERTION) != 0 + || ((what_to_do & DO_QUERY) != 0 && in->completions[1] == NULL)) + { + char *lc_complete = in->completions[0]; + + if (!insert_text (in, lc_complete, -1) || in->completions[1] != NULL) + tty_beep (); + else + input_complete_free (in); + } + + if ((what_to_do & DO_QUERY) != 0 && in->completions != NULL && in->completions[1] != NULL) + { + int maxlen = 0, count = 0, i; + int x, y, w, h; + int start_x, start_y; + char **p, *q; + WDialog *complete_dlg; + WListbox *complete_list; + + for (p = in->completions + 1; *p != NULL; count++, p++) + { + i = str_term_width1 (*p); + if (i > maxlen) + maxlen = i; + } + + start_x = WIDGET (in)->rect.x; + start_y = WIDGET (in)->rect.y; + if (start_y - 2 >= count) + { + y = start_y - 2 - count; + h = 2 + count; + } + else if (start_y >= LINES - start_y - 1) + { + y = 0; + h = start_y; + } + else + { + y = start_y + 1; + h = LINES - start_y - 1; + } + x = start - in->term_first_shown - 2 + start_x; + w = maxlen + 4; + if (x + w > COLS) + x = COLS - w; + if (x < 0) + x = 0; + if (x + w > COLS) + w = COLS; + + input = in; + min_end = end; + complete_height = h; + complete_width = w; + + complete_dlg = + dlg_create (TRUE, y, x, complete_height, complete_width, WPOS_KEEP_DEFAULT, TRUE, + dialog_colors, complete_callback, NULL, "[Completion]", NULL); + complete_list = listbox_new (1, 1, h - 2, w - 2, FALSE, NULL); + group_add_widget (GROUP (complete_dlg), complete_list); + + for (p = in->completions + 1; *p != NULL; p++) + listbox_add_item (complete_list, LISTBOX_APPEND_AT_END, 0, *p, NULL, FALSE); + + i = dlg_run (complete_dlg); + q = NULL; + if (i == B_ENTER) + { + listbox_get_current (complete_list, &q, NULL); + if (q != NULL) + insert_text (in, q, -1); + } + if (q != NULL || end != min_end) + input_complete_free (in); + widget_destroy (WIDGET (complete_dlg)); + + /* B_USER if user wants to start over again */ + return (i == B_USER); + } + } + + return FALSE; +} + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/** Returns an array of matches, or NULL if none. */ +char ** +try_complete (char *text, int *lc_start, int *lc_end, input_complete_t flags) +{ + try_complete_automation_state_t state; + char **matches = NULL; + + memset (&state, 0, sizeof (state)); + state.flags = flags; + + SHOW_C_CTX ("try_complete"); + state.word = g_strndup (text + *lc_start, *lc_end - *lc_start); + + state.is_cd = check_is_cd (text, *lc_start, state.flags); + + /* Determine if this could be a command word. It is if it appears at + the start of the line (ignoring preceding whitespace), or if it + appears after a character that separates commands. And we have to + be in a INPUT_COMPLETE_COMMANDS flagged Input line. */ + if (!state.is_cd && (flags & INPUT_COMPLETE_COMMANDS) != 0) + try_complete_commands_prepare (&state, text, lc_start); + + try_complete_find_start_sign (&state); + + /* Command substitution? */ + if (state.p > state.q && state.p > state.r) + { + SHOW_C_CTX ("try_complete:cmd_backq_subst"); + matches = completion_matches (str_cget_next_char (state.p), + command_completion_function, + state.flags & (~INPUT_COMPLETE_FILENAMES)); + if (matches != NULL) + *lc_start += str_get_next_char (state.p) - state.word; + } + + /* Variable name? */ + else if (state.q > state.p && state.q > state.r) + { + SHOW_C_CTX ("try_complete:var_subst"); + matches = completion_matches (state.q, variable_completion_function, state.flags); + if (matches != NULL) + *lc_start += state.q - state.word; + } + + /* Starts with '@', then look through the known hostnames for + completion first. */ + else if (state.r > state.p && state.r > state.q) + { + SHOW_C_CTX ("try_complete:host_subst"); + matches = completion_matches (state.r, hostname_completion_function, state.flags); + if (matches != NULL) + *lc_start += state.r - state.word; + } + + /* Starts with '~' and there is no slash in the word, then + try completing this word as a username. */ + if (matches == NULL && *state.word == '~' && (state.flags & INPUT_COMPLETE_USERNAMES) != 0 + && strchr (state.word, PATH_SEP) == NULL) + { + SHOW_C_CTX ("try_complete:user_subst"); + matches = completion_matches (state.word, username_completion_function, state.flags); + } + + /* If this word is in a command position, then + complete over possible command names, including aliases, functions, + and command names. */ + if (matches == NULL) + matches = try_complete_all_possible (&state, text, lc_start); + + /* And finally if nothing found, try complete directory name */ + if (matches == NULL) + { + state.in_command_position = 0; + matches = try_complete_all_possible (&state, text, lc_start); + } + + g_free (state.word); + + if (matches != NULL && + (flags & (INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_SHELL_ESC)) != + (INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_SHELL_ESC)) + { + /* FIXME: HACK? INPUT_COMPLETE_SHELL_ESC is used only in command line. */ + char **m; + + for (m = matches; *m != NULL; m++) + { + char *p; + + p = *m; + *m = strutils_shell_escape (*m); + g_free (p); + } + } + + return matches; +} + +/* --------------------------------------------------------------------------------------------- */ + +void +complete_engine_fill_completions (WInput * in) +{ + char *s; + const char *word_separators; + + word_separators = (in->completion_flags & INPUT_COMPLETE_SHELL_ESC) ? " \t;|<>" : "\t;|<>"; + + end = str_offset_to_pos (in->buffer->str, in->point); + + s = in->buffer->str; + if (in->point != 0) + { + /* get symbol before in->point */ + size_t i; + + for (i = in->point - 1; i > 0; i--) + str_next_char (&s); + } + + for (; s >= in->buffer->str; str_prev_char (&s)) + { + start = s - in->buffer->str; + if (strchr (word_separators, *s) != NULL && !strutils_is_char_escaped (in->buffer->str, s)) + break; + } + + if (start < end) + { + str_next_char (&s); + start = s - in->buffer->str; + } + + in->completions = try_complete (in->buffer->str, &start, &end, in->completion_flags); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* declared in lib/widget/input.h */ +void +input_complete (WInput * in) +{ + int engine_flags; + + if (!str_is_valid_string (in->buffer->str)) + return; + + if (in->completions != NULL) + engine_flags = DO_QUERY; + else + { + engine_flags = DO_INSERTION; + + if (mc_global.widget.show_all_if_ambiguous) + engine_flags |= DO_QUERY; + } + + while (complete_engine (in, engine_flags)) + ; +} + +/* --------------------------------------------------------------------------------------------- */ + +void +input_complete_free (WInput * in) +{ + g_strfreev (in->completions); + in->completions = NULL; +} + +/* --------------------------------------------------------------------------------------------- */ |