From 9935acea33dae2dd694ad54b05c3d76f79c7a0ad Mon Sep 17 00:00:00 2001 From: Slava Zanko Date: Fri, 25 Jan 2013 14:30:17 +0300 Subject: [PATCH] Ticket #55: savannah: tab completion vs. spaces and escaping * Split big functions. * Add unit tests Signed-off-by: Slava Zanko --- configure.ac | 1 + lib/widget/input_complete.c | 375 +++++++++++++++++------------ tests/lib/Makefile.am | 2 +- tests/lib/widget/Makefile.am | 19 ++ tests/lib/widget/complete_engine.c | 233 ++++++++++++++++++ 5 files changed, 470 insertions(+), 160 deletions(-) create mode 100644 tests/lib/widget/Makefile.am create mode 100644 tests/lib/widget/complete_engine.c diff --git a/configure.ac b/configure.ac index fdece2791..09332567f 100644 --- a/configure.ac +++ b/configure.ac @@ -644,6 +644,7 @@ tests/lib/Makefile tests/lib/mcconfig/Makefile tests/lib/search/Makefile tests/lib/vfs/Makefile +tests/lib/widget/Makefile tests/src/Makefile tests/src/filemanager/Makefile ]) diff --git a/lib/widget/input_complete.c b/lib/widget/input_complete.c index b47a506a4..934fb96fc 100644 --- a/lib/widget/input_complete.c +++ b/lib/widget/input_complete.c @@ -3,11 +3,12 @@ (Let mc type for you...) Copyright (C) 1995, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, - 2007, 2011 + 2007, 2011, 2013 the Free Software Foundation, Inc. Written by: Jakub Jelinek, 1995 + Slava Zanko , 2013 This file is part of the Midnight Commander. @@ -79,6 +80,17 @@ extern char **environ; 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; + /*** file scope variables ************************************************************************/ static char **hosts = NULL; @@ -94,6 +106,9 @@ static int end = 0; /*** 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); + #ifdef DO_COMPLETION_DEBUG /** * Useful to print/debug completion flags @@ -819,155 +834,114 @@ check_is_cd (const char *text, int lc_start, input_complete_t flags) } /* --------------------------------------------------------------------------------------------- */ -/** Returns an array of matches, or NULL if none. */ -static char ** -try_complete (char *text, int *lc_start, int *lc_end, input_complete_t flags) + +static void +try_complete_commands_prepare (try_complete_automation_state_t * state, char *text, int *lc_start) { - size_t in_command_position = 0; - char *word; - char **matches = NULL; - char *p = NULL, *q = NULL, *r = NULL; - gboolean is_cd; + const char *command_separator_chars = ";|&{(`"; + char *ti; - SHOW_C_CTX ("try_complete"); - word = g_strndup (text + *lc_start, *lc_end - *lc_start); - - is_cd = check_is_cd (text, *lc_start, 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 (!is_cd && (flags & INPUT_COMPLETE_COMMANDS)) + if (*lc_start == 0) + ti = text; + else { - 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 && (ti[0] == ' ' || ti[0] == '\t')) - str_prev_char (&ti); - } - - if (ti == text) - in_command_position++; - else if (strchr (command_separator_chars, ti[0]) != NULL) - { - int this_char, prev_char; - - in_command_position++; - - if (ti != text) - { - /* 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] == - '\\')) - in_command_position = 0; - } - } + ti = str_get_prev_char (&text[*lc_start]); + while (ti > text && (ti[0] == ' ' || ti[0] == '\t')) + str_prev_char (&ti); } - if (flags & INPUT_COMPLETE_COMMANDS) - p = strrchr (word, '`'); - if (flags & (INPUT_COMPLETE_COMMANDS | INPUT_COMPLETE_VARIABLES)) + if (ti == text) + state->in_command_position++; + else if (strchr (command_separator_chars, ti[0]) != NULL) { - q = strrchr (word, '$'); + int this_char, prev_char; + + state->in_command_position++; + + if (ti != text) + { + /* 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) + state->p = strrchr (state->word, '`'); + if (state->flags & (INPUT_COMPLETE_COMMANDS | INPUT_COMPLETE_VARIABLES)) + { + state->q = strrchr (state->word, '$'); /* don't substitute variable in \$ case */ - if (q != NULL && q != word && q[-1] == '\\') + if (strutils_is_char_escaped (state->word, state->q)) { size_t qlen; - qlen = strlen (q); + qlen = strlen (state->q); /* drop '\\' */ - memmove (q - 1, q, qlen + 1); + memmove (state->q - 1, state->q, qlen + 1); /* adjust flags */ - flags &= ~INPUT_COMPLETE_VARIABLES; - q = NULL; + state->flags &= ~INPUT_COMPLETE_VARIABLES; + state->q = NULL; } } - if (flags & INPUT_COMPLETE_HOSTNAMES) - r = strrchr (word, '@'); - if (q && q[1] == '(' && (flags & INPUT_COMPLETE_COMMANDS)) + if (state->flags & INPUT_COMPLETE_HOSTNAMES) + state->r = strrchr (state->word, '@'); + if (state->q && state->q[1] == '(' && (state->flags & INPUT_COMPLETE_COMMANDS)) { - if (q > p) - p = str_get_next_char (q); - q = NULL; + if (state->q > state->p) + state->p = str_get_next_char (state->q); + state->q = NULL; } +} - /* Command substitution? */ - if (p > q && p > r) - { - SHOW_C_CTX ("try_complete:cmd_backq_subst"); - matches = completion_matches (str_cget_next_char (p), - command_completion_function, - flags & (~INPUT_COMPLETE_FILENAMES)); - if (matches) - *lc_start += str_get_next_char (p) - word; - } +/* --------------------------------------------------------------------------------------------- */ - /* Variable name? */ - else if (q > p && q > r) - { - SHOW_C_CTX ("try_complete:var_subst"); - matches = completion_matches (q, variable_completion_function, flags); - if (matches) - *lc_start += q - word; - } +static char ** +try_complete_all_possible (try_complete_automation_state_t * state, char *text, int *lc_start) +{ + char **matches = NULL; - /* Starts with '@', then look through the known hostnames for - completion first. */ - else if (r > p && r > q) - { - SHOW_C_CTX ("try_complete:host_subst"); - matches = completion_matches (r, hostname_completion_function, flags); - if (matches) - *lc_start += r - word; - } - - /* Starts with `~' and there is no slash in the word, then - try completing this word as a username. */ - if (!matches && *word == '~' && (flags & INPUT_COMPLETE_USERNAMES) && !strchr (word, PATH_SEP)) - { - SHOW_C_CTX ("try_complete:user_subst"); - matches = completion_matches (word, username_completion_function, flags); - } - - - /* And finally if this word is in a command position, then - complete over possible command names, including aliases, functions, - and command names. */ - if (!matches && in_command_position != 0) + if (state->in_command_position != 0) { SHOW_C_CTX ("try_complete:cmd_subst"); matches = - completion_matches (word, command_completion_function, - flags & (~INPUT_COMPLETE_FILENAMES)); + completion_matches (state->word, command_completion_function, + state->flags & (~INPUT_COMPLETE_FILENAMES)); } - - else if (!matches && (flags & INPUT_COMPLETE_FILENAMES)) + else if ((state->flags & INPUT_COMPLETE_FILENAMES) != 0) { - if (is_cd) - flags &= ~(INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_COMMANDS); + if (state->is_cd) + state->flags &= ~(INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_COMMANDS); SHOW_C_CTX ("try_complete:filename_subst_1"); - matches = completion_matches (word, filename_completion_function, flags); - if (!matches && is_cd && *word != PATH_SEP && *word != '~') + matches = completion_matches (state->word, filename_completion_function, state->flags); + + if (matches == NULL && state->is_cd && *state->word != PATH_SEP && *state->word != '~') { - q = text + *lc_start; - for (p = text; *p && p < q && (*p == ' ' || *p == '\t'); str_next_char (&p)); - if (!strncmp (p, "cd", 2)) - for (p += 2; *p && p < q && (*p == ' ' || *p == '\t'); str_next_char (&p)); - if (p == q) + state->q = text + *lc_start; + for (state->p = text; + *state->p && state->p < state->q && (*state->p == ' ' || *state->p == '\t'); + str_next_char (&state->p)) + ; + if (!strncmp (state->p, "cd", 2)) + for (state->p += 2; + *state->p && state->p < state->q && (*state->p == ' ' || *state->p == '\t'); + str_next_char (&state->p)) + ; + if (state->p == state->q) { char *const cdpath_ref = g_strdup (getenv ("CDPATH")); char *cdpath = cdpath_ref; @@ -986,10 +960,12 @@ try_complete (char *text, int *lc_start, int *lc_end, input_complete_t flags) *s = 0; if (*cdpath) { - r = mc_build_filename (cdpath, word, NULL); + state->r = mc_build_filename (cdpath, state->word, NULL); SHOW_C_CTX ("try_complete:filename_subst_2"); - matches = completion_matches (r, filename_completion_function, flags); - g_free (r); + matches = + completion_matches (state->r, filename_completion_function, + state->flags); + g_free (state->r); } *s = c; cdpath = str_get_next_char (s); @@ -998,9 +974,6 @@ try_complete (char *text, int *lc_start, int *lc_end, input_complete_t flags) } } } - - g_free (word); - return matches; } @@ -1224,43 +1197,16 @@ query_callback (Widget * w, Widget * sender, widget_msg_t msg, int parm, void *d } /* --------------------------------------------------------------------------------------------- */ -/** Returns 1 if the user would like to see us again */ +/** Returns 1 if the user would like to see us again */ static int complete_engine (WInput * in, int what_to_do) { if (in->completions != NULL && str_offset_to_pos (in->buffer, in->point) != end) input_free_completions (in); + if (in->completions == NULL) - { - char *s; - - end = str_offset_to_pos (in->buffer, in->point); - - s = in->buffer; - 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_prev_char (&s)) - { - start = s - in->buffer; - if (strchr (" \t;|<>", *s) != NULL) - break; - } - - if (start < end) - { - str_next_char (&s); - start = s - in->buffer; - } - - in->completions = try_complete (in->buffer, &start, &end, in->completion_flags); - } + complete_engine_fill_completions (in); if (in->completions != NULL) { @@ -1356,6 +1302,117 @@ complete_engine (WInput * in, int what_to_do) /*** 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 (try_complete_automation_state_t)); + 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)) + 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) + *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) + *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) + *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 && *state.word == '~' && (state.flags & INPUT_COMPLETE_USERNAMES) + && !strchr (state.word, PATH_SEP)) + { + SHOW_C_CTX ("try_complete:user_subst"); + matches = completion_matches (state.word, username_completion_function, state.flags); + } + + /* And finally 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); + + g_free (state.word); + + return matches; +} + +/* --------------------------------------------------------------------------------------------- */ + +void +complete_engine_fill_completions (WInput * in) +{ + char *s; + + end = str_offset_to_pos (in->buffer, in->point); + + s = in->buffer; + 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_prev_char (&s)) + { + start = s - in->buffer; + if (strchr (" \t;|<>", *s) != NULL) + break; + } + + if (start < end) + { + str_next_char (&s); + start = s - in->buffer; + } + + in->completions = try_complete (in->buffer, &start, &end, in->completion_flags); +} + +/* --------------------------------------------------------------------------------------------- */ + /* declared in lib/widget/input.h */ void complete (WInput * in) diff --git a/tests/lib/Makefile.am b/tests/lib/Makefile.am index 8dd721a3b..f5ed3a3d8 100644 --- a/tests/lib/Makefile.am +++ b/tests/lib/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = . mcconfig search vfs +SUBDIRS = . mcconfig search vfs widget AM_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) @CHECK_CFLAGS@ diff --git a/tests/lib/widget/Makefile.am b/tests/lib/widget/Makefile.am new file mode 100644 index 000000000..37a91efab --- /dev/null +++ b/tests/lib/widget/Makefile.am @@ -0,0 +1,19 @@ + +AM_CPPFLAGS = \ + $(GLIB_CFLAGS) \ + -I$(top_srcdir) \ + -I$(top_srcdir)/lib/vfs \ + @CHECK_CFLAGS@ + +AM_LDFLAGS = @TESTS_LDFLAGS@ + +LIBS=@CHECK_LIBS@ \ + $(top_builddir)/lib/libmc.la + +TESTS = \ + complete_engine + +check_PROGRAMS = $(TESTS) + +complete_engine_SOURCES = \ + complete_engine.c diff --git a/tests/lib/widget/complete_engine.c b/tests/lib/widget/complete_engine.c new file mode 100644 index 000000000..656f1573d --- /dev/null +++ b/tests/lib/widget/complete_engine.c @@ -0,0 +1,233 @@ +/* + lib/widget - tests for autocomplete feature + + Copyright (C) 2013 + The Free Software Foundation, Inc. + + Written by: + Slava Zanko , 2013 + + 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 . + */ + +#define TEST_SUITE_NAME "/lib/widget" + +#include + +#include + +#include "lib/global.h" +#include "lib/strutil.h" +#include "lib/widget.h" + +/* --------------------------------------------------------------------------------------------- */ + +void complete_engine_fill_completions (WInput * in); +char **try_complete (char *text, int *lc_start, int *lc_end, input_complete_t flags); + +/* --------------------------------------------------------------------------------------------- */ + +/* @CapturedValue */ +static char *try_complete__text__captured; +/* @CapturedValue */ +static int try_complete__lc_start__captured; +/* @CapturedValue */ +static int try_complete__lc_end__captured; +/* @CapturedValue */ +static input_complete_t try_complete__flags__captured; + +/* @ThenReturnValue */ +static char **try_complete__return_value; + +/* @Mock */ +char ** +try_complete (char *text, int *lc_start, int *lc_end, input_complete_t flags) +{ + try_complete__text__captured = g_strdup (text); + try_complete__lc_start__captured = *lc_start; + try_complete__lc_end__captured = *lc_end; + try_complete__flags__captured = flags; + + return try_complete__return_value; +} + +static void +try_complete__init (void) +{ + try_complete__text__captured = NULL; + try_complete__lc_start__captured = 0; + try_complete__lc_end__captured = 0; + try_complete__flags__captured = 0; +} + +static void +try_complete__deinit (void) +{ + g_free (try_complete__text__captured); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Before */ +static void +setup (void) +{ + str_init_strings (NULL); + try_complete__init (); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @After */ +static void +teardown (void) +{ + try_complete__deinit (); + str_uninit_strings (); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @DataSource("test_complete_engine_fill_completions_ds") */ +/* *INDENT-OFF* */ +static const struct test_complete_engine_fill_completions_ds +{ + const char *input_buffer; + const int input_point; + const input_complete_t input_completion_flags; + int expected_start; + int expected_end; +} test_complete_engine_fill_completions_ds[] = +{ + { + "string", + 3, + INPUT_COMPLETE_DEFAULT, + 0, + 3 + }, + { + "some string", + 7, + INPUT_COMPLETE_DEFAULT, + 5, + 7 + }, + { + "some\tstring", + 7, + INPUT_COMPLETE_DEFAULT, + 5, + 7 + }, + { + "some;string", + 7, + INPUT_COMPLETE_DEFAULT, + 5, + 7 + }, + { + "some|string", + 7, + INPUT_COMPLETE_DEFAULT, + 5, + 7 + }, + { + "somestring", + 7, + INPUT_COMPLETE_DEFAULT, + 5, + 7 + }, + { + "some!@#$%^&*()_\\+~`\"',./?:string", + 30, + INPUT_COMPLETE_DEFAULT, + 0, + 30 + }, +}; +/* *INDENT-ON* */ +// " \t;|<>" + +/* @Test(dataSource = "test_complete_engine_fill_completions_ds") */ +/* *INDENT-OFF* */ +START_TEST (test_complete_engine_fill_completions) +/* *INDENT-ON* */ +{ + /* given */ + + WInput *w_input; + const struct test_complete_engine_fill_completions_ds *data = + &test_complete_engine_fill_completions_ds[_i]; + + w_input = g_new (WInput, 1); + w_input->buffer = g_strdup (data->input_buffer); + w_input->point = data->input_point; + w_input->completion_flags = data->input_completion_flags; + + /* when */ + complete_engine_fill_completions (w_input); + + /* then */ + g_assert_cmpstr (try_complete__text__captured, ==, data->input_buffer); + ck_assert_int_eq (try_complete__lc_start__captured, data->expected_start); + ck_assert_int_eq (try_complete__lc_end__captured, data->expected_end); + ck_assert_int_eq (try_complete__flags__captured, data->input_completion_flags); + +} +/* *INDENT-OFF* */ +END_TEST +/* *INDENT-ON* */ + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + int number_failed; + + Suite *s = suite_create (TEST_SUITE_NAME); + TCase *tc_core = tcase_create ("Core"); + SRunner *sr; + + tcase_add_checked_fixture (tc_core, setup, teardown); + + /* Add new tests here: *************** */ + tcase_add_loop_test (tc_core, test_complete_engine_fill_completions, 0, + sizeof (test_complete_engine_fill_completions_ds) / + sizeof (test_complete_engine_fill_completions_ds[0])); + /* *********************************** */ + + suite_add_tcase (s, tc_core); + sr = srunner_create (s); + srunner_set_log (sr, "complete_engine.log"); + srunner_run_all (sr, CK_NORMAL); + number_failed = srunner_ntests_failed (sr); + srunner_free (sr); + return (number_failed == 0) ? 0 : 1; +} + +/* --------------------------------------------------------------------------------------------- */