From 92099c7af23d0cebf52f89de4f9d829825e53ac8 Mon Sep 17 00:00:00 2001 From: ridiculousfish Date: Wed, 17 Jul 2013 00:38:04 -0700 Subject: [PATCH] Initial abbreviation work. Tests currently fail. --- builtin.cpp | 3 +- expand.cpp | 39 +++++++++++ expand.h | 4 ++ fish_tests.cpp | 62 ++++++++++++++++++ reader.cpp | 175 +++++++++++++++++++++++++++++++++++++++++++++++-- reader.h | 7 ++ 6 files changed, 285 insertions(+), 5 deletions(-) diff --git a/builtin.cpp b/builtin.cpp index 52c06ac85..d230080a4 100644 --- a/builtin.cpp +++ b/builtin.cpp @@ -2370,8 +2370,9 @@ static int builtin_read(parser_t &parser, wchar_t **argv) reader_set_highlight_function(&highlight_shell); reader_set_test_function(&reader_shell_test); } - /* No autosuggestions in builtin_read */ + /* No autosuggestions or abbreviations in builtin_read */ reader_set_allow_autosuggesting(false); + reader_set_expand_abbreviations(false); reader_set_exit_on_interrupt(true); reader_set_buffer(commandline, wcslen(commandline)); diff --git a/expand.cpp b/expand.cpp index e25109fc0..dc83c385d 100644 --- a/expand.cpp +++ b/expand.cpp @@ -1933,3 +1933,42 @@ bool fish_openSUSE_dbus_hack_hack_hack_hack(std::vector *args) } return result; } + +bool expand_abbreviation(const wcstring &src, wcstring *output) +{ + if (src.empty()) + return false; + + /* Get the abbreviations. Return false if we have none */ + env_var_t var = env_get_string(USER_ABBREVIATIONS_VARIABLE_NAME); + if (var.missing_or_empty()) + return false; + + bool result = false; + wcstring line; + wcstokenizer tokenizer(var, ARRAY_SEP_STR); + while (tokenizer.next(line)) + { + /* Line is expected to be of the form 'foo=bar'. Parse out the first =. Be forgiving about spaces, but silently skip on failure (no equals, or equals at the end or beginning). Try to avoid copying any strings until we are sure this is a match. */ + size_t equals = line.find(L'='); + if (equals == wcstring::npos || equals == 0 || equals + 1 == line.size()) + continue; + + /* Find the character just past the end of the command. Walk backwards, skipping spaces. */ + size_t cmd_end = equals; + while (cmd_end > 0 && iswspace(line.at(cmd_end - 1))) + cmd_end--; + + /* See if this command matches */ + if (line.compare(0, cmd_end, src) == 0) + { + /* Success. Set output to everythign past the end of the string. */ + if (output != NULL) + output->assign(line, equals + 1, wcstring::npos); + + result = true; + break; + } + } + return result; +} diff --git a/expand.h b/expand.h index 1968e0ee5..51e149dfe 100644 --- a/expand.h +++ b/expand.h @@ -206,6 +206,10 @@ void expand_variable_error(parser_t &parser, const wchar_t *token, size_t token_ */ std::vector expand_get_all_process_names(void); +/** Abbreviation support. Expand src as an abbreviation, returning true if one was found, false if not. If result is not-null, returns the abbreviation by reference. */ +#define USER_ABBREVIATIONS_VARIABLE_NAME L"fish_user_abbreviations" +bool expand_abbreviation(const wcstring &src, wcstring *output); + /* Terrible hacks */ bool fish_xdm_login_hack_hack_hack_hack(std::vector *cmds, int argc, const char * const *argv); bool fish_openSUSE_dbus_hack_hack_hack_hack(std::vector *args); diff --git a/fish_tests.cpp b/fish_tests.cpp index fad9b7ecb..bbcd35c38 100644 --- a/fish_tests.cpp +++ b/fish_tests.cpp @@ -703,6 +703,67 @@ static void test_fuzzy_match(void) if (string_fuzzy_match_string(L"BB", L"ALPHA!").type != fuzzy_match_none) err(L"test_fuzzy_match failed on line %ld", __LINE__); } +static void test_abbreviations(void) +{ + say(L"Testing abbreviations"); + + const wchar_t *buff = L"echo (echo (echo (gc"; + size_t cursor_pos = wcslen(buff) - 2; + const wchar_t *cmdsub_begin = NULL, *cmdsub_end = NULL; + parse_util_cmdsubst_extent(buff, cursor_pos, &cmdsub_begin, &cmdsub_end); + assert(cmdsub_begin != NULL && cmdsub_begin >= buff); + assert(cmdsub_end != NULL && cmdsub_end >= cmdsub_begin); + fprintf(stderr, "cmdsub of '%ls' at %lu is '%ls'\n", buff, cursor_pos, wcstring(cmdsub_begin, cmdsub_end).c_str()); + exit(0); + + const wchar_t *abbreviations = + L"gc=git checkout" ARRAY_SEP_STR + L"foo=" ARRAY_SEP_STR + L"gc=something else" ARRAY_SEP_STR + L"=" ARRAY_SEP_STR + L"=foo" ARRAY_SEP_STR + L"foo" ARRAY_SEP_STR + L"foo=bar"; + + env_push(true); + + int ret = env_set(USER_ABBREVIATIONS_VARIABLE_NAME, abbreviations, ENV_LOCAL); + if (ret != 0) err(L"Unable to set abbreviation variable"); + + wcstring result; + if (expand_abbreviation(L"", &result)) err(L"Unexpected success with empty abbreviation"); + if (expand_abbreviation(L"nothing", &result)) err(L"Unexpected success with missing abbreviation"); + + if (! expand_abbreviation(L"gc", &result)) err(L"Unexpected failure with gc abbreviation"); + if (result != L"git checkout") err(L"Wrong abbreviation result for gc"); + result.clear(); + + if (! expand_abbreviation(L"foo", &result)) err(L"Unexpected failure with foo abbreviation"); + if (result != L"bar") err(L"Wrong abbreviation result for foo"); + + bool expanded; + expanded = reader_expand_abbreviation_in_command(L"just a command", wcslen(L"just "), &result); + if (expanded) err(L"Command wrongly expanded on line %ld", (long)__LINE__); + expanded = reader_expand_abbreviation_in_command(L"gc somebranch", 0, &result); + if (expanded) err(L"Command wrongly expanded on line %ld", (long)__LINE__); + expanded = reader_expand_abbreviation_in_command(L"gc somebranch", 1, &result); + if (expanded) err(L"Command wrongly expanded on line %ld", (long)__LINE__); + + expanded = reader_expand_abbreviation_in_command(L"gc somebranch", wcslen(L"gc "), &result); + if (! expanded) err(L"gc not expanded"); + if (result != L"git checkout somebranch") err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result.c_str()); + + expanded = reader_expand_abbreviation_in_command(L"echo hi ; gc somebranch", wcslen(L"echo hi ; gc "), &result); + if (! expanded) err(L"gc not expanded on line %ld", (long)__LINE__); + if (result != L"echo hi ; git checkout somebranch") err(L"gc incorrectly expanded on line %ld", (long)__LINE__); + + expanded = reader_expand_abbreviation_in_command(L"echo (echo (echo (echo (gc ", wcslen(L"echo (echo (echo (echo (gc "), &result); + if (! expanded) err(L"gc not expanded on line %ld", (long)__LINE__); + if (result != L"echo (echo (git checkout ") err(L"gc incorrectly expanded on line %ld", (long)__LINE__); + + env_pop(); +} + /** Test path functions */ static void test_path() { @@ -1776,6 +1837,7 @@ int main(int argc, char **argv) test_lru(); test_expand(); test_fuzzy_match(); + test_abbreviations(); test_test(); test_path(); test_word_motion(); diff --git a/reader.cpp b/reader.cpp index 9b2eea14e..d9cb363bd 100644 --- a/reader.cpp +++ b/reader.cpp @@ -182,6 +182,8 @@ static pthread_key_t generation_count_key; /* A color is an int */ typedef int color_t; +static void set_command_line_and_position(const wcstring &new_str, size_t pos); + /** A struct describing the state of the interactive reader. These states can be stacked, in case reader_readline() calls are @@ -203,6 +205,9 @@ public: /** When backspacing, we temporarily suppress autosuggestions */ bool suppress_autosuggestion; + /** Whether abbreviations are expanded */ + bool expand_abbreviations; + /** The representation of the current screen contents */ screen_t screen; @@ -244,6 +249,9 @@ public: /** Do what we need to do whenever our command line changes */ void command_line_changed(void); + /** Expand abbreviations at the current cursor position. Returns true if the command line changed. */ + bool expand_abbreviation_as_necessary(void); + /** The current position of the cursor in buff. */ size_t buff_pos; @@ -326,6 +334,7 @@ public: reader_data_t() : allow_autosuggestion(0), suppress_autosuggestion(0), + expand_abbreviations(0), history(0), token_history_pos(0), search_pos(0), @@ -635,6 +644,148 @@ void reader_data_t::command_line_changed() s_generation_count++; } +/* Expand abbreviations at the given cursor position. Does NOT inspect 'data'. */ +bool reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos, wcstring *output) +{ + /* Can't have the cursor at the beginning */ + if (cursor_pos == 0) + return false; + + /* See if we are at "command position". Get the surrounding command substitution, and get the extent of the first token. */ + const wchar_t * const buff = cmdline.c_str(); + const wchar_t *cmdsub_begin = NULL, *cmdsub_end = NULL; + parse_util_cmdsubst_extent(buff, cursor_pos, &cmdsub_begin, &cmdsub_end); + assert(cmdsub_begin != NULL && cmdsub_begin >= buff); + assert(cmdsub_end != NULL && cmdsub_end >= cmdsub_begin); + fprintf(stderr, "cmdsub of '%ls' at %lu is '%ls'\n", cmdline.c_str(), cursor_pos, wcstring(cmdsub_begin, cmdsub_end).c_str()); + + /* Determine the offset of this command substitution */ + const size_t subcmd_offset = cmdsub_begin - buff; + + const wcstring subcmd = wcstring(cmdsub_begin, cmdsub_end - cmdsub_begin); + const wchar_t *subcmd_cstr = subcmd.c_str(); + + /* Get the token before the cursor */ + const wchar_t *subcmd_tok_begin = NULL, *subcmd_tok_end = NULL; + size_t subcmd_cursor_pos = cursor_pos + subcmd_offset; + parse_util_token_extent(subcmd_cstr, subcmd_cursor_pos, NULL, NULL, &subcmd_tok_begin, &subcmd_tok_end); + + /* Compute the offset of the token before the cursor within the subcmd */ + assert(subcmd_tok_begin >= subcmd_cstr); + assert(subcmd_tok_end >= subcmd_tok_begin); + const size_t subcmd_tok_begin_offset = subcmd_tok_begin - subcmd_cstr; + const size_t subcmd_tok_length = subcmd_tok_end - subcmd_tok_begin; + + /* Now parse the subcmd, looking for commands */ + bool had_cmd = false, previous_token_is_cmd = false; + tokenizer_t tok(subcmd_cstr, TOK_ACCEPT_UNFINISHED | TOK_SQUASH_ERRORS); + for (; tok_has_next(&tok); tok_next(&tok)) + { + size_t tok_pos = static_cast(tok_get_pos(&tok)); + if (tok_pos > subcmd_tok_begin_offset) + { + /* We've passed the token we're interested in */ + break; + } + + int last_type = tok_last_type(&tok); + + switch (last_type) + { + case TOK_STRING: + { + if (had_cmd) + { + /* Parameter to the command. */ + } + else + { + /* Command. */ + had_cmd = true; + if (tok_pos == subcmd_tok_begin_offset) + { + /* This is the token we care about! */ + previous_token_is_cmd = true; + } + } + break; + } + + case TOK_REDIRECT_NOCLOB: + case TOK_REDIRECT_OUT: + case TOK_REDIRECT_IN: + case TOK_REDIRECT_APPEND: + case TOK_REDIRECT_FD: + { + if (!had_cmd) + { + break; + } + tok_next(&tok); + break; + } + + case TOK_PIPE: + case TOK_BACKGROUND: + case TOK_END: + { + had_cmd = false; + break; + } + + case TOK_COMMENT: + case TOK_ERROR: + default: + { + break; + } + } + } + + bool result = false; + if (previous_token_is_cmd) + { + /* The token is a command. Try expanding it as an abbreviation. */ + const wcstring token = wcstring(subcmd, subcmd_tok_begin_offset, subcmd_tok_length); + wcstring abbreviation; + if (expand_abbreviation(token, &abbreviation)) + { + /* There was an abbreviation! Replace the token in the full command. Maintain the relative position of the cursor. */ + if (output != NULL) + { + size_t cmd_tok_begin_offset = subcmd_tok_begin_offset + subcmd_offset; + output->assign(cmdline); + output->replace(cmd_tok_begin_offset, subcmd_tok_length, abbreviation); + } + result = true; + } + } + return result; +} + +/* Expand abbreviations */ +bool reader_data_t::expand_abbreviation_as_necessary(void) +{ + bool result = false; + if (this->expand_abbreviations) + { + wcstring new_cmdline; + if (reader_expand_abbreviation_in_command(this->command_line, this->buff_pos, &new_cmdline)) + { + /* We expanded an abbreviation! The cursor moves by the difference in the command line lengths. */ + size_t new_buff_pos = this->buff_pos + new_cmdline.size() - this->command_line.size(); + + this->command_line.swap(new_cmdline); + data->command_line_changed(); + data->buff_pos = new_buff_pos; + reader_super_highlight_me_plenty(data->buff_pos); + reader_repaint(); + + result = true; + } + } + return result; +} /** Sorts and remove any duplicate completions in the list. */ static void sort_and_make_unique(std::vector &l) @@ -1980,8 +2131,7 @@ void reader_sanity_check() } /** - Set the specified string from the history as the current buffer. Do - not modify prefix_width. + Set the specified string as the current buffer. */ static void set_command_line_and_position(const wcstring &new_str, size_t pos) { @@ -2462,6 +2612,11 @@ void reader_set_allow_autosuggesting(bool flag) data->allow_autosuggestion = flag; } +void reader_set_expand_abbreviations(bool flag) +{ + data->expand_abbreviations = flag; +} + void reader_set_complete_function(complete_function_t f) { data->complete_func = f; @@ -2712,6 +2867,7 @@ static int read_i(void) reader_set_highlight_function(&highlight_shell); reader_set_test_function(&reader_shell_test); reader_set_allow_autosuggesting(true); + reader_set_expand_abbreviations(true); reader_import_history_if_necessary(); parser_t &parser = parser_t::principal_parser(); @@ -2851,7 +3007,6 @@ const wchar_t *reader_readline(void) data->search_buff.clear(); data->search_mode = NO_SEARCH; - exec_prompt(); reader_super_highlight_me_plenty(data->buff_pos); @@ -3261,7 +3416,19 @@ const wchar_t *reader_readline(void) } } - switch (data->test_func(data->command_line.c_str())) + /* See if this command is valid */ + int command_test_result = data->test_func(data->command_line.c_str()); + if (command_test_result == 0) + { + /* This command is valid, but an abbreviation may make it invalid. If so, we will have to test again. */ + bool abbreviation_expanded = data->expand_abbreviation_as_necessary(); + if (abbreviation_expanded) + { + command_test_result = data->test_func(data->command_line.c_str()); + } + } + + switch (command_test_result) { case 0: diff --git a/reader.h b/reader.h index dfe8d1ed5..0ead2aa40 100644 --- a/reader.h +++ b/reader.h @@ -204,6 +204,10 @@ void reader_set_right_prompt(const wcstring &prompt); /** Sets whether autosuggesting is allowed. */ void reader_set_allow_autosuggesting(bool flag); +/** Sets whether abbreviation expansion is performed. */ +void reader_set_expand_abbreviations(bool flag); + + /** Sets whether the reader should exit on ^C. */ void reader_set_exit_on_interrupt(bool flag); @@ -243,6 +247,9 @@ int reader_search_mode(); /* Given a command line and an autosuggestion, return the string that gets shown to the user. Exposed for testing purposes only. */ wcstring combine_command_and_autosuggestion(const wcstring &cmdline, const wcstring &autosuggestion); +/* Expand abbreviations at the given cursor position. Exposed for testing purposes only. */ +bool reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos, wcstring *output); + /* Apply a completion string. Exposed for testing only. */ wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags, const wcstring &command_line, size_t *inout_cursor_pos, bool append_only);