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

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