summaryrefslogtreecommitdiffstats
path: root/src/editor/editcomplete.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/editor/editcomplete.c483
1 files changed, 483 insertions, 0 deletions
diff --git a/src/editor/editcomplete.c b/src/editor/editcomplete.c
new file mode 100644
index 0000000..06f304d
--- /dev/null
+++ b/src/editor/editcomplete.c
@@ -0,0 +1,483 @@
+/*
+ Editor word completion engine
+
+ Copyright (C) 2021-2023
+ Free Software Foundation, Inc.
+
+ Written by:
+ Andrew Borodin <aborodin@vmail.ru>, 2021-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/>.
+ */
+
+#include <config.h>
+
+#include <ctype.h> /* isspace() */
+#include <string.h>
+
+#include "lib/global.h"
+#include "lib/search.h"
+#include "lib/strutil.h"
+#ifdef HAVE_CHARSET
+#include "lib/charsets.h" /* str_convert_to_input() */
+#endif
+#include "lib/tty/tty.h" /* LINES, COLS */
+#include "lib/widget.h"
+
+#include "src/setup.h" /* verbose */
+
+#include "editwidget.h"
+#include "edit-impl.h"
+#include "editsearch.h"
+
+#include "editcomplete.h"
+
+/*** global variables ****************************************************************************/
+
+/*** file scope macro definitions ****************************************************************/
+
+/*** file scope type declarations ****************************************************************/
+
+/*** forward declarations (file scope functions) *************************************************/
+
+/*** file scope variables ************************************************************************/
+
+/* --------------------------------------------------------------------------------------------- */
+/*** file scope functions ************************************************************************/
+/* --------------------------------------------------------------------------------------------- */
+
+/**
+ * Get current word under cursor
+ *
+ * @param esm status message window
+ * @param srch mc_search object
+ * @param word_start start word position
+ *
+ * @return newly allocated string or NULL if no any words under cursor
+ */
+
+static GString *
+edit_collect_completions_get_current_word (edit_search_status_msg_t * esm, mc_search_t * srch,
+ off_t word_start)
+{
+ WEdit *edit = esm->edit;
+ gsize len = 0;
+ GString *temp = NULL;
+
+ if (mc_search_run (srch, (void *) esm, word_start, edit->buffer.size, &len))
+ {
+ off_t i;
+
+ for (i = 0; i < (off_t) len; i++)
+ {
+ int chr;
+
+ chr = edit_buffer_get_byte (&edit->buffer, word_start + i);
+ if (!isspace (chr))
+ {
+ if (temp == NULL)
+ temp = g_string_sized_new (len);
+
+ g_string_append_c (temp, chr);
+ }
+ }
+ }
+
+ return temp;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+/**
+ * collect the possible completions from one buffer
+ */
+
+static void
+edit_collect_completion_from_one_buffer (gboolean active_buffer, GQueue ** compl,
+ mc_search_t * srch, edit_search_status_msg_t * esm,
+ off_t word_start, gsize word_len, off_t last_byte,
+ GString * current_word, int *max_width)
+{
+ GString *temp = NULL;
+ gsize len = 0;
+ off_t start = -1;
+
+ while (mc_search_run (srch, (void *) esm, start + 1, last_byte, &len))
+ {
+ gsize i;
+ int width;
+
+ if (temp == NULL)
+ temp = g_string_sized_new (8);
+ else
+ g_string_set_size (temp, 0);
+
+ start = srch->normal_offset;
+
+ /* add matched completion if not yet added */
+ for (i = 0; i < len; i++)
+ {
+ int ch;
+
+ ch = edit_buffer_get_byte (&esm->edit->buffer, start + i);
+ if (isspace (ch))
+ continue;
+
+ /* skip current word */
+ if (start + (off_t) i == word_start)
+ break;
+
+ g_string_append_c (temp, ch);
+ }
+
+ if (temp->len == 0)
+ continue;
+
+ if (current_word != NULL && g_string_equal (current_word, temp))
+ continue;
+
+ if (*compl == NULL)
+ *compl = g_queue_new ();
+ else
+ {
+ GList *l;
+
+ for (l = g_queue_peek_head_link (*compl); l != NULL; l = g_list_next (l))
+ {
+ GString *s = (GString *) l->data;
+
+ /* skip if already added */
+ if (strncmp (s->str + word_len, temp->str + word_len,
+ MAX (len, s->len) - word_len) == 0)
+ break;
+ }
+
+ if (l != NULL)
+ {
+ /* resort completion in main buffer only:
+ * these completions must be at the top of list in the completion dialog */
+ if (!active_buffer && l != g_queue_peek_tail_link (*compl))
+ {
+ /* move to the end */
+ g_queue_unlink (*compl, l);
+ g_queue_push_tail_link (*compl, l);
+ }
+
+ continue;
+ }
+ }
+
+#ifdef HAVE_CHARSET
+ {
+ GString *recoded;
+
+ recoded = str_nconvert_to_display (temp->str, temp->len);
+ if (recoded != NULL)
+ {
+ if (recoded->len != 0)
+ mc_g_string_copy (temp, recoded);
+
+ g_string_free (recoded, TRUE);
+ }
+ }
+#endif
+
+ if (active_buffer)
+ g_queue_push_tail (*compl, temp);
+ else
+ g_queue_push_head (*compl, temp);
+
+ start += len;
+
+ /* note the maximal length needed for the completion dialog */
+ width = str_term_width1 (temp->str);
+ *max_width = MAX (*max_width, width);
+
+ temp = NULL;
+ }
+
+ if (temp != NULL)
+ g_string_free (temp, TRUE);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+/**
+ * collect the possible completions from all buffers
+ */
+
+static GQueue *
+edit_collect_completions (WEdit * edit, off_t word_start, gsize word_len,
+ const char *match_expr, int *max_width)
+{
+ GQueue *compl = NULL;
+ mc_search_t *srch;
+ off_t last_byte;
+ GString *current_word;
+ gboolean entire_file, all_files;
+ edit_search_status_msg_t esm;
+
+#ifdef HAVE_CHARSET
+ srch = mc_search_new (match_expr, cp_source);
+#else
+ srch = mc_search_new (match_expr, NULL);
+#endif
+ if (srch == NULL)
+ return NULL;
+
+ entire_file =
+ mc_config_get_bool (mc_global.main_config, CONFIG_APP_SECTION,
+ "editor_wordcompletion_collect_entire_file", FALSE);
+
+ last_byte = entire_file ? edit->buffer.size : word_start;
+
+ srch->search_type = MC_SEARCH_T_REGEX;
+ srch->is_case_sensitive = TRUE;
+ srch->search_fn = edit_search_cmd_callback;
+ srch->update_fn = edit_search_update_callback;
+
+ esm.first = TRUE;
+ esm.edit = edit;
+ esm.offset = entire_file ? 0 : word_start;
+
+ status_msg_init (STATUS_MSG (&esm), _("Collect completions"), 1.0, simple_status_msg_init_cb,
+ edit_search_status_update_cb, NULL);
+
+ current_word = edit_collect_completions_get_current_word (&esm, srch, word_start);
+
+ *max_width = 0;
+
+ /* collect completions from current buffer at first */
+ edit_collect_completion_from_one_buffer (TRUE, &compl, srch, &esm, word_start, word_len,
+ last_byte, current_word, max_width);
+
+ /* collect completions from other buffers */
+ all_files =
+ mc_config_get_bool (mc_global.main_config, CONFIG_APP_SECTION,
+ "editor_wordcompletion_collect_all_files", TRUE);
+ if (all_files)
+ {
+ const WGroup *owner = CONST_GROUP (CONST_WIDGET (edit)->owner);
+ gboolean saved_verbose;
+ GList *w;
+
+ /* don't show incorrect percentage in edit_search_status_update_cb() */
+ saved_verbose = verbose;
+ verbose = FALSE;
+
+ for (w = owner->widgets; w != NULL; w = g_list_next (w))
+ {
+ Widget *ww = WIDGET (w->data);
+ WEdit *e;
+
+ if (!edit_widget_is_editor (ww))
+ continue;
+
+ e = EDIT (ww);
+
+ if (e == edit)
+ continue;
+
+ /* search in entire file */
+ word_start = 0;
+ last_byte = e->buffer.size;
+ esm.edit = e;
+ esm.offset = 0;
+
+ edit_collect_completion_from_one_buffer (FALSE, &compl, srch, &esm, word_start,
+ word_len, last_byte, current_word, max_width);
+ }
+
+ verbose = saved_verbose;
+ }
+
+ status_msg_deinit (STATUS_MSG (&esm));
+ mc_search_free (srch);
+ if (current_word != NULL)
+ g_string_free (current_word, TRUE);
+
+ return compl;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/**
+ * Insert autocompleted word into editor.
+ *
+ * @param edit editor object
+ * @param completion word for completion
+ * @param word_len offset from beginning for insert
+ */
+
+static void
+edit_complete_word_insert_recoded_completion (WEdit * edit, char *completion, gsize word_len)
+{
+#ifdef HAVE_CHARSET
+ GString *temp;
+
+ temp = str_convert_to_input (completion);
+ if (temp != NULL)
+ {
+ for (completion = temp->str + word_len; *completion != '\0'; completion++)
+ edit_insert (edit, *completion);
+ g_string_free (temp, TRUE);
+ }
+#else
+ for (completion += word_len; *completion != '\0'; completion++)
+ edit_insert (edit, *completion);
+#endif
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+edit_completion_string_free (gpointer data)
+{
+ g_string_free ((GString *) data, TRUE);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+/*** public functions ****************************************************************************/
+/* --------------------------------------------------------------------------------------------- */
+/* let the user select its preferred completion */
+
+/* Public function for unit tests */
+char *
+edit_completion_dialog_show (const WEdit * edit, GQueue * compl, int max_width)
+{
+ const WRect *we = &CONST_WIDGET (edit)->rect;
+ int start_x, start_y, offset;
+ char *curr = NULL;
+ WDialog *compl_dlg;
+ WListbox *compl_list;
+ int compl_dlg_h; /* completion dialog height */
+ int compl_dlg_w; /* completion dialog width */
+ GList *i;
+
+ /* calculate the dialog metrics */
+ compl_dlg_h = g_queue_get_length (compl) + 2;
+ compl_dlg_w = max_width + 4;
+ start_x = we->x + edit->curs_col + edit->start_col + EDIT_TEXT_HORIZONTAL_OFFSET +
+ (edit->fullscreen ? 0 : 1) + edit_options.line_state_width;
+ start_y = we->y + edit->curs_row + EDIT_TEXT_VERTICAL_OFFSET + (edit->fullscreen ? 0 : 1) + 1;
+
+ if (start_x < 0)
+ start_x = 0;
+ if (start_x < we->x + 1)
+ start_x = we->x + 1 + edit_options.line_state_width;
+ if (compl_dlg_w > COLS)
+ compl_dlg_w = COLS;
+ if (compl_dlg_h > LINES - 2)
+ compl_dlg_h = LINES - 2;
+
+ offset = start_x + compl_dlg_w - COLS;
+ if (offset > 0)
+ start_x -= offset;
+ offset = start_y + compl_dlg_h - LINES;
+ if (offset > 0)
+ start_y -= offset;
+
+ /* create the dialog */
+ compl_dlg =
+ dlg_create (TRUE, start_y, start_x, compl_dlg_h, compl_dlg_w, WPOS_KEEP_DEFAULT, TRUE,
+ dialog_colors, NULL, NULL, "[Completion]", NULL);
+
+ /* create the listbox */
+ compl_list = listbox_new (1, 1, compl_dlg_h - 2, compl_dlg_w - 2, FALSE, NULL);
+
+ /* fill the listbox with the completions in the reverse order */
+ for (i = g_queue_peek_tail_link (compl); i != NULL; i = g_list_previous (i))
+ listbox_add_item (compl_list, LISTBOX_APPEND_AT_END, 0, ((GString *) i->data)->str, NULL,
+ FALSE);
+
+ group_add_widget (GROUP (compl_dlg), compl_list);
+
+ /* pop up the dialog and apply the chosen completion */
+ if (dlg_run (compl_dlg) == B_ENTER)
+ {
+ listbox_get_current (compl_list, &curr, NULL);
+ curr = g_strdup (curr);
+ }
+
+ /* destroy dialog before return */
+ widget_destroy (WIDGET (compl_dlg));
+
+ return curr;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/**
+ * Complete current word using regular expression search
+ * backwards beginning at the current cursor position.
+ */
+
+void
+edit_complete_word_cmd (WEdit * edit)
+{
+ off_t word_start = 0;
+ gsize word_len = 0;
+ GString *match_expr;
+ gsize i;
+ GQueue *compl; /* completions: list of GString* */
+ int max_width;
+
+ /* search start of word to be completed */
+ if (!edit_buffer_find_word_start (&edit->buffer, &word_start, &word_len))
+ return;
+
+ /* prepare match expression */
+ /* match_expr = g_strdup_printf ("\\b%.*s[a-zA-Z_0-9]+", word_len, bufpos); */
+ match_expr = g_string_new ("(^|\\s+|\\b)");
+ for (i = 0; i < word_len; i++)
+ g_string_append_c (match_expr, edit_buffer_get_byte (&edit->buffer, word_start + i));
+ g_string_append (match_expr,
+ "[^\\s\\.=\\+\\[\\]\\(\\)\\,\\;\\:\\\"\\'\\-\\?\\/\\|\\\\\\{\\}\\*\\&\\^\\%%\\$#@\\!]+");
+
+ /* collect possible completions */
+ compl = edit_collect_completions (edit, word_start, word_len, match_expr->str, &max_width);
+
+ g_string_free (match_expr, TRUE);
+
+ if (compl == NULL)
+ return;
+
+ if (g_queue_get_length (compl) == 1)
+ {
+ /* insert completed word if there is only one match */
+
+ GString *curr_compl;
+
+ curr_compl = (GString *) g_queue_peek_head (compl);
+ edit_complete_word_insert_recoded_completion (edit, curr_compl->str, word_len);
+ }
+ else
+ {
+ /* more than one possible completion => ask the user */
+
+ char *curr_compl;
+
+ /* let the user select the preferred completion */
+ curr_compl = edit_completion_dialog_show (edit, compl, max_width);
+ if (curr_compl != NULL)
+ {
+ edit_complete_word_insert_recoded_completion (edit, curr_compl, word_len);
+ g_free (curr_compl);
+ }
+ }
+
+ g_queue_free_full (compl, edit_completion_string_free);
+}
+
+/* --------------------------------------------------------------------------------------------- */