root/src/editor/editcomplete.c

/* [previous][next][first][last][top][bottom][index][help]  */

DEFINITIONS

This source file includes following definitions.
  1. edit_collect_completions_get_current_word
  2. edit_collect_completion_from_one_buffer
  3. edit_collect_completions
  4. edit_complete_word_insert_recoded_completion
  5. edit_completion_string_free
  6. edit_completion_dialog_show
  7. edit_complete_word_cmd

   1 /*
   2    Editor word completion engine
   3 
   4    Copyright (C) 2021
   5    Free Software Foundation, Inc.
   6 
   7    Written by:
   8    Andrew Borodin <aborodin@vmail.ru>, 2021
   9 
  10    This file is part of the Midnight Commander.
  11 
  12    The Midnight Commander is free software: you can redistribute it
  13    and/or modify it under the terms of the GNU General Public License as
  14    published by the Free Software Foundation, either version 3 of the License,
  15    or (at your option) any later version.
  16 
  17    The Midnight Commander is distributed in the hope that it will be useful,
  18    but WITHOUT ANY WARRANTY; without even the implied warranty of
  19    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  20    GNU General Public License for more details.
  21 
  22    You should have received a copy of the GNU General Public License
  23    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  24  */
  25 
  26 #include <config.h>
  27 
  28 #include <ctype.h>              /* isspace() */
  29 #include <string.h>
  30 
  31 #include "lib/global.h"
  32 #include "lib/search.h"
  33 #include "lib/strutil.h"
  34 #ifdef HAVE_CHARSET
  35 #include "lib/charsets.h"       /* str_convert_to_input() */
  36 #endif
  37 #include "lib/tty/tty.h"        /* LINES, COLS */
  38 #include "lib/widget.h"
  39 
  40 #include "src/setup.h"          /* verbose */
  41 
  42 #include "editwidget.h"
  43 #include "edit-impl.h"
  44 #include "editsearch.h"
  45 
  46 #include "editcomplete.h"
  47 
  48 /*** global variables ****************************************************************************/
  49 
  50 /*** file scope macro definitions ****************************************************************/
  51 
  52 /*** file scope type declarations ****************************************************************/
  53 
  54 /*** file scope variables ************************************************************************/
  55 
  56 /* --------------------------------------------------------------------------------------------- */
  57 /*** file scope functions ************************************************************************/
  58 /* --------------------------------------------------------------------------------------------- */
  59 
  60 /**
  61  * Get current word under cursor
  62  *
  63  * @param esm status message window
  64  * @param srch mc_search object
  65  * @param word_start start word position
  66  *
  67  * @return newly allocated string or NULL if no any words under cursor
  68  */
  69 
  70 static GString *
  71 edit_collect_completions_get_current_word (edit_search_status_msg_t * esm, mc_search_t * srch,
     /* [previous][next][first][last][top][bottom][index][help]  */
  72                                            off_t word_start)
  73 {
  74     WEdit *edit = esm->edit;
  75     gsize len = 0;
  76     GString *temp = NULL;
  77 
  78     if (mc_search_run (srch, (void *) esm, word_start, edit->buffer.size, &len))
  79     {
  80         off_t i;
  81 
  82         for (i = 0; i < (off_t) len; i++)
  83         {
  84             int chr;
  85 
  86             chr = edit_buffer_get_byte (&edit->buffer, word_start + i);
  87             if (!isspace (chr))
  88             {
  89                 if (temp == NULL)
  90                     temp = g_string_sized_new (len);
  91 
  92                 g_string_append_c (temp, chr);
  93             }
  94         }
  95     }
  96 
  97     return temp;
  98 }
  99 
 100 /* --------------------------------------------------------------------------------------------- */
 101 /**
 102  * collect the possible completions from one buffer
 103  */
 104 
 105 static void
 106 edit_collect_completion_from_one_buffer (gboolean active_buffer, GQueue ** compl,
     /* [previous][next][first][last][top][bottom][index][help]  */
 107                                          mc_search_t * srch, edit_search_status_msg_t * esm,
 108                                          off_t word_start, gsize word_len, off_t last_byte,
 109                                          GString * current_word, int *max_width)
 110 {
 111     GString *temp = NULL;
 112     gsize len = 0;
 113     off_t start = -1;
 114 
 115     while (mc_search_run (srch, (void *) esm, start + 1, last_byte, &len))
 116     {
 117         gsize i;
 118         int width;
 119 
 120         if (temp == NULL)
 121             temp = g_string_sized_new (8);
 122         else
 123             g_string_set_size (temp, 0);
 124 
 125         start = srch->normal_offset;
 126 
 127         /* add matched completion if not yet added */
 128         for (i = 0; i < len; i++)
 129         {
 130             int ch;
 131 
 132             ch = edit_buffer_get_byte (&esm->edit->buffer, start + i);
 133             if (isspace (ch))
 134                 continue;
 135 
 136             /* skip current word */
 137             if (start + (off_t) i == word_start)
 138                 break;
 139 
 140             g_string_append_c (temp, ch);
 141         }
 142 
 143         if (temp->len == 0)
 144             continue;
 145 
 146         if (current_word != NULL && g_string_equal (current_word, temp))
 147             continue;
 148 
 149         if (*compl == NULL)
 150             *compl = g_queue_new ();
 151         else
 152         {
 153             GList *l;
 154 
 155             for (l = g_queue_peek_head_link (*compl); l != NULL; l = g_list_next (l))
 156             {
 157                 GString *s = (GString *) l->data;
 158 
 159                 /* skip if already added */
 160                 if (strncmp (s->str + word_len, temp->str + word_len,
 161                              MAX (len, s->len) - word_len) == 0)
 162                     break;
 163             }
 164 
 165             if (l != NULL)
 166             {
 167                 /* resort completion in main buffer only:
 168                  * these completions must be at the top of list in the completion dialog */
 169                 if (!active_buffer && l != g_queue_peek_tail_link (*compl))
 170                 {
 171                     /* move to the end */
 172                     g_queue_unlink (*compl, l);
 173                     g_queue_push_tail_link (*compl, l);
 174                 }
 175 
 176                 continue;
 177             }
 178         }
 179 
 180 #ifdef HAVE_CHARSET
 181         {
 182             GString *recoded;
 183 
 184             recoded = str_convert_to_display (temp->str);
 185             if (recoded->len != 0)
 186                 mc_g_string_copy (temp, recoded);
 187 
 188             g_string_free (recoded, TRUE);
 189         }
 190 #endif
 191         if (active_buffer)
 192             g_queue_push_tail (*compl, temp);
 193         else
 194             g_queue_push_head (*compl, temp);
 195 
 196         start += len;
 197 
 198         /* note the maximal length needed for the completion dialog */
 199         width = str_term_width1 (temp->str);
 200         *max_width = MAX (*max_width, width);
 201 
 202         temp = NULL;
 203     }
 204 
 205     if (temp != NULL)
 206         g_string_free (temp, TRUE);
 207 }
 208 
 209 /* --------------------------------------------------------------------------------------------- */
 210 /**
 211  * collect the possible completions from all buffers
 212  */
 213 
 214 static GQueue *
 215 edit_collect_completions (WEdit * edit, off_t word_start, gsize word_len,
     /* [previous][next][first][last][top][bottom][index][help]  */
 216                           const char *match_expr, int *max_width)
 217 {
 218     GQueue *compl = NULL;
 219     mc_search_t *srch;
 220     off_t last_byte;
 221     GString *current_word;
 222     gboolean entire_file, all_files;
 223     edit_search_status_msg_t esm;
 224 
 225 #ifdef HAVE_CHARSET
 226     srch = mc_search_new (match_expr, cp_source);
 227 #else
 228     srch = mc_search_new (match_expr, NULL);
 229 #endif
 230     if (srch == NULL)
 231         return NULL;
 232 
 233     entire_file =
 234         mc_config_get_bool (mc_global.main_config, CONFIG_APP_SECTION,
 235                             "editor_wordcompletion_collect_entire_file", FALSE);
 236 
 237     last_byte = entire_file ? edit->buffer.size : word_start;
 238 
 239     srch->search_type = MC_SEARCH_T_REGEX;
 240     srch->is_case_sensitive = TRUE;
 241     srch->search_fn = edit_search_cmd_callback;
 242     srch->update_fn = edit_search_update_callback;
 243 
 244     esm.first = TRUE;
 245     esm.edit = edit;
 246     esm.offset = entire_file ? 0 : word_start;
 247 
 248     status_msg_init (STATUS_MSG (&esm), _("Collect completions"), 1.0, simple_status_msg_init_cb,
 249                      edit_search_status_update_cb, NULL);
 250 
 251     current_word = edit_collect_completions_get_current_word (&esm, srch, word_start);
 252 
 253     *max_width = 0;
 254 
 255     /* collect completions from current buffer at first */
 256     edit_collect_completion_from_one_buffer (TRUE, &compl, srch, &esm, word_start, word_len,
 257                                              last_byte, current_word, max_width);
 258 
 259     /* collect completions from other buffers */
 260     all_files =
 261         mc_config_get_bool (mc_global.main_config, CONFIG_APP_SECTION,
 262                             "editor_wordcompletion_collect_all_files", TRUE);
 263     if (all_files)
 264     {
 265         const WGroup *owner = CONST_GROUP (CONST_WIDGET (edit)->owner);
 266         gboolean saved_verbose;
 267         GList *w;
 268 
 269         /* don't show incorrect percentage in edit_search_status_update_cb() */
 270         saved_verbose = verbose;
 271         verbose = FALSE;
 272 
 273         for (w = owner->widgets; w != NULL; w = g_list_next (w))
 274         {
 275             Widget *ww = WIDGET (w->data);
 276             WEdit *e;
 277 
 278             if (!edit_widget_is_editor (ww))
 279                 continue;
 280 
 281             e = (WEdit *) ww;
 282 
 283             if (e == edit)
 284                 continue;
 285 
 286             /* search in entire file */
 287             word_start = 0;
 288             last_byte = e->buffer.size;
 289             esm.edit = e;
 290             esm.offset = 0;
 291 
 292             edit_collect_completion_from_one_buffer (FALSE, &compl, srch, &esm, word_start,
 293                                                      word_len, last_byte, current_word, max_width);
 294         }
 295 
 296         verbose = saved_verbose;
 297     }
 298 
 299     status_msg_deinit (STATUS_MSG (&esm));
 300     mc_search_free (srch);
 301     if (current_word != NULL)
 302         g_string_free (current_word, TRUE);
 303 
 304     return compl;
 305 }
 306 
 307 /* --------------------------------------------------------------------------------------------- */
 308 
 309 /**
 310  * Insert autocompleted word into editor.
 311  *
 312  * @param edit       editor object
 313  * @param completion word for completion
 314  * @param word_len   offset from beginning for insert
 315  */
 316 
 317 static void
 318 edit_complete_word_insert_recoded_completion (WEdit * edit, char *completion, gsize word_len)
     /* [previous][next][first][last][top][bottom][index][help]  */
 319 {
 320 #ifdef HAVE_CHARSET
 321     GString *temp;
 322 
 323     temp = str_convert_to_input (completion);
 324 
 325     for (completion = temp->str + word_len; *completion != '\0'; completion++)
 326         edit_insert (edit, *completion);
 327     g_string_free (temp, TRUE);
 328 #else
 329     for (completion += word_len; *completion != '\0'; completion++)
 330         edit_insert (edit, *completion);
 331 #endif
 332 }
 333 
 334 /* --------------------------------------------------------------------------------------------- */
 335 
 336 static void
 337 edit_completion_string_free (gpointer data)
     /* [previous][next][first][last][top][bottom][index][help]  */
 338 {
 339     g_string_free ((GString *) data, TRUE);
 340 }
 341 
 342 /* --------------------------------------------------------------------------------------------- */
 343 /*** public functions ****************************************************************************/
 344 /* --------------------------------------------------------------------------------------------- */
 345 /* let the user select its preferred completion */
 346 
 347 /* Public function for unit tests */
 348 char *
 349 edit_completion_dialog_show (const WEdit * edit, GQueue * compl, int max_width)
     /* [previous][next][first][last][top][bottom][index][help]  */
 350 {
 351     const Widget *we = CONST_WIDGET (edit);
 352     int start_x, start_y, offset;
 353     char *curr = NULL;
 354     WDialog *compl_dlg;
 355     WListbox *compl_list;
 356     int compl_dlg_h;            /* completion dialog height */
 357     int compl_dlg_w;            /* completion dialog width */
 358     GList *i;
 359 
 360     /* calculate the dialog metrics */
 361     compl_dlg_h = g_queue_get_length (compl) + 2;
 362     compl_dlg_w = max_width + 4;
 363     start_x = we->x + edit->curs_col + edit->start_col + EDIT_TEXT_HORIZONTAL_OFFSET +
 364         (edit->fullscreen ? 0 : 1) + option_line_state_width;
 365     start_y = we->y + edit->curs_row + EDIT_TEXT_VERTICAL_OFFSET + (edit->fullscreen ? 0 : 1) + 1;
 366 
 367     if (start_x < 0)
 368         start_x = 0;
 369     if (start_x < we->x + 1)
 370         start_x = we->x + 1 + option_line_state_width;
 371     if (compl_dlg_w > COLS)
 372         compl_dlg_w = COLS;
 373     if (compl_dlg_h > LINES - 2)
 374         compl_dlg_h = LINES - 2;
 375 
 376     offset = start_x + compl_dlg_w - COLS;
 377     if (offset > 0)
 378         start_x -= offset;
 379     offset = start_y + compl_dlg_h - LINES;
 380     if (offset > 0)
 381         start_y -= offset;
 382 
 383     /* create the dialog */
 384     compl_dlg =
 385         dlg_create (TRUE, start_y, start_x, compl_dlg_h, compl_dlg_w, WPOS_KEEP_DEFAULT, TRUE,
 386                     dialog_colors, NULL, NULL, "[Completion]", NULL);
 387 
 388     /* create the listbox */
 389     compl_list = listbox_new (1, 1, compl_dlg_h - 2, compl_dlg_w - 2, FALSE, NULL);
 390 
 391     /* fill the listbox with the completions in the reverse order */
 392     for (i = g_queue_peek_tail_link (compl); i != NULL; i = g_list_previous (i))
 393         listbox_add_item (compl_list, LISTBOX_APPEND_AT_END, 0, ((GString *) i->data)->str, NULL,
 394                           FALSE);
 395 
 396     group_add_widget (GROUP (compl_dlg), compl_list);
 397 
 398     /* pop up the dialog and apply the chosen completion */
 399     if (dlg_run (compl_dlg) == B_ENTER)
 400     {
 401         listbox_get_current (compl_list, &curr, NULL);
 402         curr = g_strdup (curr);
 403     }
 404 
 405     /* destroy dialog before return */
 406     widget_destroy (WIDGET (compl_dlg));
 407 
 408     return curr;
 409 }
 410 
 411 /* --------------------------------------------------------------------------------------------- */
 412 
 413 /**
 414  * Complete current word using regular expression search
 415  * backwards beginning at the current cursor position.
 416  */
 417 
 418 void
 419 edit_complete_word_cmd (WEdit * edit)
     /* [previous][next][first][last][top][bottom][index][help]  */
 420 {
 421     off_t word_start = 0;
 422     gsize word_len = 0;
 423     GString *match_expr;
 424     gsize i;
 425     GQueue *compl;              /* completions: list of GString* */
 426     int max_width;
 427 
 428     /* search start of word to be completed */
 429     if (!edit_buffer_find_word_start (&edit->buffer, &word_start, &word_len))
 430         return;
 431 
 432     /* prepare match expression */
 433     /* match_expr = g_strdup_printf ("\\b%.*s[a-zA-Z_0-9]+", word_len, bufpos); */
 434     match_expr = g_string_new ("(^|\\s+|\\b)");
 435     for (i = 0; i < word_len; i++)
 436         g_string_append_c (match_expr, edit_buffer_get_byte (&edit->buffer, word_start + i));
 437     g_string_append (match_expr,
 438                      "[^\\s\\.=\\+\\[\\]\\(\\)\\,\\;\\:\\\"\\'\\-\\?\\/\\|\\\\\\{\\}\\*\\&\\^\\%%\\$#@\\!]+");
 439 
 440     /* collect possible completions */
 441     compl = edit_collect_completions (edit, word_start, word_len, match_expr->str, &max_width);
 442 
 443     g_string_free (match_expr, TRUE);
 444 
 445     if (compl == NULL)
 446         return;
 447 
 448     if (g_queue_get_length (compl) == 1)
 449     {
 450         /* insert completed word if there is only one match */
 451 
 452         GString *curr_compl;
 453 
 454         curr_compl = (GString *) g_queue_peek_head (compl);
 455         edit_complete_word_insert_recoded_completion (edit, curr_compl->str, word_len);
 456     }
 457     else
 458     {
 459         /* more than one possible completion => ask the user */
 460 
 461         char *curr_compl;
 462 
 463         /* let the user select the preferred completion */
 464         curr_compl = edit_completion_dialog_show (edit, compl, max_width);
 465         if (curr_compl != NULL)
 466         {
 467             edit_complete_word_insert_recoded_completion (edit, curr_compl, word_len);
 468             g_free (curr_compl);
 469         }
 470     }
 471 
 472     g_queue_free_full (compl, edit_completion_string_free);
 473 }
 474 
 475 /* --------------------------------------------------------------------------------------------- */

/* [previous][next][first][last][top][bottom][index][help]  */