Ticket #55: savannah: tab completion vs. spaces and escaping

* Split big functions.
* Add unit tests

Signed-off-by: Slava Zanko <slavazanko@gmail.com>
This commit is contained in:
Slava Zanko 2013-01-25 14:30:17 +03:00
parent 74d71e7523
commit 9935acea33
5 changed files with 470 additions and 160 deletions

View File

@ -644,6 +644,7 @@ tests/lib/Makefile
tests/lib/mcconfig/Makefile tests/lib/mcconfig/Makefile
tests/lib/search/Makefile tests/lib/search/Makefile
tests/lib/vfs/Makefile tests/lib/vfs/Makefile
tests/lib/widget/Makefile
tests/src/Makefile tests/src/Makefile
tests/src/filemanager/Makefile tests/src/filemanager/Makefile
]) ])

View File

@ -3,11 +3,12 @@
(Let mc type for you...) (Let mc type for you...)
Copyright (C) 1995, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, Copyright (C) 1995, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
2007, 2011 2007, 2011, 2013
the Free Software Foundation, Inc. the Free Software Foundation, Inc.
Written by: Written by:
Jakub Jelinek, 1995 Jakub Jelinek, 1995
Slava Zanko <slavazanko@gmail.com>, 2013
This file is part of the Midnight Commander. 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 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 ************************************************************************/ /*** file scope variables ************************************************************************/
static char **hosts = NULL; static char **hosts = NULL;
@ -94,6 +106,9 @@ static int end = 0;
/*** file scope functions ************************************************************************/ /*** 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 #ifdef DO_COMPLETION_DEBUG
/** /**
* Useful to print/debug completion flags * 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 ** static void
try_complete (char *text, int *lc_start, int *lc_end, input_complete_t flags) try_complete_commands_prepare (try_complete_automation_state_t * state, char *text, int *lc_start)
{ {
size_t in_command_position = 0; const char *command_separator_chars = ";|&{(`";
char *word; char *ti;
char **matches = NULL;
char *p = NULL, *q = NULL, *r = NULL;
gboolean is_cd;
SHOW_C_CTX ("try_complete"); if (*lc_start == 0)
word = g_strndup (text + *lc_start, *lc_end - *lc_start); ti = text;
else
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))
{ {
const char *command_separator_chars = ";|&{(`"; ti = str_get_prev_char (&text[*lc_start]);
char *ti; while (ti > text && (ti[0] == ' ' || ti[0] == '\t'))
str_prev_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;
}
}
} }
if (flags & INPUT_COMPLETE_COMMANDS) if (ti == text)
p = strrchr (word, '`'); state->in_command_position++;
if (flags & (INPUT_COMPLETE_COMMANDS | INPUT_COMPLETE_VARIABLES)) 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 */ /* 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; size_t qlen;
qlen = strlen (q); qlen = strlen (state->q);
/* drop '\\' */ /* drop '\\' */
memmove (q - 1, q, qlen + 1); memmove (state->q - 1, state->q, qlen + 1);
/* adjust flags */ /* adjust flags */
flags &= ~INPUT_COMPLETE_VARIABLES; state->flags &= ~INPUT_COMPLETE_VARIABLES;
q = NULL; state->q = NULL;
} }
} }
if (flags & INPUT_COMPLETE_HOSTNAMES) if (state->flags & INPUT_COMPLETE_HOSTNAMES)
r = strrchr (word, '@'); state->r = strrchr (state->word, '@');
if (q && q[1] == '(' && (flags & INPUT_COMPLETE_COMMANDS)) if (state->q && state->q[1] == '(' && (state->flags & INPUT_COMPLETE_COMMANDS))
{ {
if (q > p) if (state->q > state->p)
p = str_get_next_char (q); state->p = str_get_next_char (state->q);
q = NULL; 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? */ static char **
else if (q > p && q > r) try_complete_all_possible (try_complete_automation_state_t * state, char *text, int *lc_start)
{ {
SHOW_C_CTX ("try_complete:var_subst"); char **matches = NULL;
matches = completion_matches (q, variable_completion_function, flags);
if (matches)
*lc_start += q - word;
}
/* Starts with '@', then look through the known hostnames for if (state->in_command_position != 0)
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)
{ {
SHOW_C_CTX ("try_complete:cmd_subst"); SHOW_C_CTX ("try_complete:cmd_subst");
matches = matches =
completion_matches (word, command_completion_function, completion_matches (state->word, command_completion_function,
flags & (~INPUT_COMPLETE_FILENAMES)); state->flags & (~INPUT_COMPLETE_FILENAMES));
} }
else if ((state->flags & INPUT_COMPLETE_FILENAMES) != 0)
else if (!matches && (flags & INPUT_COMPLETE_FILENAMES))
{ {
if (is_cd) if (state->is_cd)
flags &= ~(INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_COMMANDS); state->flags &= ~(INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_COMMANDS);
SHOW_C_CTX ("try_complete:filename_subst_1"); SHOW_C_CTX ("try_complete:filename_subst_1");
matches = completion_matches (word, filename_completion_function, flags); matches = completion_matches (state->word, filename_completion_function, state->flags);
if (!matches && is_cd && *word != PATH_SEP && *word != '~')
if (matches == NULL && state->is_cd && *state->word != PATH_SEP && *state->word != '~')
{ {
q = text + *lc_start; state->q = text + *lc_start;
for (p = text; *p && p < q && (*p == ' ' || *p == '\t'); str_next_char (&p)); for (state->p = text;
if (!strncmp (p, "cd", 2)) *state->p && state->p < state->q && (*state->p == ' ' || *state->p == '\t');
for (p += 2; *p && p < q && (*p == ' ' || *p == '\t'); str_next_char (&p)); str_next_char (&state->p))
if (p == q) ;
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 *const cdpath_ref = g_strdup (getenv ("CDPATH"));
char *cdpath = cdpath_ref; char *cdpath = cdpath_ref;
@ -986,10 +960,12 @@ try_complete (char *text, int *lc_start, int *lc_end, input_complete_t flags)
*s = 0; *s = 0;
if (*cdpath) 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"); SHOW_C_CTX ("try_complete:filename_subst_2");
matches = completion_matches (r, filename_completion_function, flags); matches =
g_free (r); completion_matches (state->r, filename_completion_function,
state->flags);
g_free (state->r);
} }
*s = c; *s = c;
cdpath = str_get_next_char (s); 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; 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 static int
complete_engine (WInput * in, int what_to_do) complete_engine (WInput * in, int what_to_do)
{ {
if (in->completions != NULL && str_offset_to_pos (in->buffer, in->point) != end) if (in->completions != NULL && str_offset_to_pos (in->buffer, in->point) != end)
input_free_completions (in); input_free_completions (in);
if (in->completions == NULL) if (in->completions == NULL)
{ complete_engine_fill_completions (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);
}
if (in->completions != NULL) if (in->completions != NULL)
{ {
@ -1356,6 +1302,117 @@ complete_engine (WInput * in, int what_to_do)
/*** public functions ****************************************************************************/ /*** 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 */ /* declared in lib/widget/input.h */
void void
complete (WInput * in) complete (WInput * in)

View File

@ -1,4 +1,4 @@
SUBDIRS = . mcconfig search vfs SUBDIRS = . mcconfig search vfs widget
AM_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) @CHECK_CFLAGS@ AM_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) @CHECK_CFLAGS@

View File

@ -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

View File

@ -0,0 +1,233 @@
/*
lib/widget - tests for autocomplete feature
Copyright (C) 2013
The Free Software Foundation, Inc.
Written by:
Slava Zanko <slavazanko@gmail.com>, 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 <http://www.gnu.org/licenses/>.
*/
#define TEST_SUITE_NAME "/lib/widget"
#include <config.h>
#include <check.h>
#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
},
{
"some<string",
7,
INPUT_COMPLETE_DEFAULT,
5,
7
},
{
"some>string",
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;
}
/* --------------------------------------------------------------------------------------------- */