Manual pages: mcmcdiffmceditmcview

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-2025
   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 <https://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 #include "lib/charsets.h"  // str_convert_to_input()
  35 #include "lib/tty/tty.h"   // LINES, COLS
  36 #include "lib/widget.h"
  37 
  38 #include "src/setup.h"  // verbose
  39 
  40 #include "editwidget.h"
  41 #include "edit-impl.h"
  42 #include "editsearch.h"
  43 
  44 #include "editcomplete.h"
  45 
  46 /*** global variables ****************************************************************************/
  47 
  48 /*** file scope macro definitions ****************************************************************/
  49 
  50 /*** file scope type declarations ****************************************************************/
  51 
  52 /*** forward declarations (file scope functions) *************************************************/
  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, MAX (len, s->len) - word_len)
 161                     == 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         {
 181             GString *recoded;
 182 
 183             recoded = str_nconvert_to_display (temp->str, temp->len);
 184             if (recoded != NULL)
 185             {
 186                 if (recoded->len != 0)
 187                     mc_g_string_copy (temp, recoded);
 188 
 189                 g_string_free (recoded, TRUE);
 190             }
 191         }
 192 
 193         if (active_buffer)
 194             g_queue_push_tail (*compl, temp);
 195         else
 196             g_queue_push_head (*compl, temp);
 197 
 198         start += len;
 199 
 200         // note the maximal length needed for the completion dialog
 201         width = str_term_width1 (temp->str);
 202         *max_width = MAX (*max_width, width);
 203 
 204         temp = NULL;
 205     }
 206 
 207     if (temp != NULL)
 208         g_string_free (temp, TRUE);
 209 }
 210 
 211 /* --------------------------------------------------------------------------------------------- */
 212 /**
 213  * collect the possible completions from all buffers
 214  */
 215 
 216 static GQueue *
 217 edit_collect_completions (WEdit *edit, off_t word_start, gsize word_len, const char *match_expr,
     /* [previous][next][first][last][top][bottom][index][help]  */
 218                           int *max_width)
 219 {
 220     GQueue *compl = NULL;
 221     mc_search_t *srch;
 222     off_t last_byte;
 223     GString *current_word;
 224     gboolean entire_file, all_files;
 225     edit_search_status_msg_t esm;
 226 
 227     srch = mc_search_new (match_expr, cp_source);
 228     if (srch == NULL)
 229         return NULL;
 230 
 231     entire_file = mc_config_get_bool (mc_global.main_config, CONFIG_APP_SECTION,
 232                                       "editor_wordcompletion_collect_entire_file", FALSE);
 233 
 234     last_byte = entire_file ? edit->buffer.size : word_start;
 235 
 236     srch->search_type = MC_SEARCH_T_REGEX;
 237     srch->is_case_sensitive = TRUE;
 238     srch->search_fn = edit_search_cmd_callback;
 239     srch->update_fn = edit_search_update_callback;
 240 
 241     esm.first = TRUE;
 242     esm.edit = edit;
 243     esm.offset = entire_file ? 0 : word_start;
 244 
 245     status_msg_init (STATUS_MSG (&esm), _ ("Collect completions"), 1.0, simple_status_msg_init_cb,
 246                      edit_search_status_update_cb, NULL);
 247 
 248     current_word = edit_collect_completions_get_current_word (&esm, srch, word_start);
 249 
 250     *max_width = 0;
 251 
 252     // collect completions from current buffer at first
 253     edit_collect_completion_from_one_buffer (TRUE, &compl, srch, &esm, word_start, word_len,
 254                                              last_byte, current_word, max_width);
 255 
 256     // collect completions from other buffers
 257     all_files = mc_config_get_bool (mc_global.main_config, CONFIG_APP_SECTION,
 258                                     "editor_wordcompletion_collect_all_files", TRUE);
 259     if (all_files)
 260     {
 261         const WGroup *owner = CONST_GROUP (CONST_WIDGET (edit)->owner);
 262         gboolean saved_verbose;
 263         GList *w;
 264 
 265         // don't show incorrect percentage in edit_search_status_update_cb()
 266         saved_verbose = verbose;
 267         verbose = FALSE;
 268 
 269         for (w = owner->widgets; w != NULL; w = g_list_next (w))
 270         {
 271             Widget *ww = WIDGET (w->data);
 272             WEdit *e;
 273 
 274             if (!edit_widget_is_editor (ww))
 275                 continue;
 276 
 277             e = EDIT (ww);
 278 
 279             if (e == edit)
 280                 continue;
 281 
 282             // search in entire file
 283             word_start = 0;
 284             last_byte = e->buffer.size;
 285             esm.edit = e;
 286             esm.offset = 0;
 287 
 288             edit_collect_completion_from_one_buffer (FALSE, &compl, srch, &esm, word_start,
 289                                                      word_len, last_byte, current_word, max_width);
 290         }
 291 
 292         verbose = saved_verbose;
 293     }
 294 
 295     status_msg_deinit (STATUS_MSG (&esm));
 296     mc_search_free (srch);
 297     if (current_word != NULL)
 298         g_string_free (current_word, TRUE);
 299 
 300     return compl;
 301 }
 302 
 303 /* --------------------------------------------------------------------------------------------- */
 304 
 305 /**
 306  * Insert autocompleted word into editor.
 307  *
 308  * @param edit       editor object
 309  * @param completion word for completion
 310  * @param word_len   offset from beginning for insert
 311  */
 312 
 313 static void
 314 edit_complete_word_insert_recoded_completion (WEdit *edit, char *completion, gsize word_len)
     /* [previous][next][first][last][top][bottom][index][help]  */
 315 {
 316     GString *temp;
 317 
 318     temp = str_convert_to_input (completion);
 319     if (temp != NULL)
 320     {
 321         for (completion = temp->str + word_len; *completion != '\0'; completion++)
 322             edit_insert (edit, *completion);
 323         g_string_free (temp, TRUE);
 324     }
 325 }
 326 
 327 /* --------------------------------------------------------------------------------------------- */
 328 
 329 static void
 330 edit_completion_string_free (gpointer data)
     /* [previous][next][first][last][top][bottom][index][help]  */
 331 {
 332     g_string_free ((GString *) data, TRUE);
 333 }
 334 
 335 /* --------------------------------------------------------------------------------------------- */
 336 /*** public functions ****************************************************************************/
 337 /* --------------------------------------------------------------------------------------------- */
 338 /* let the user select its preferred completion */
 339 
 340 /* Public function for unit tests */
 341 char *
 342 edit_completion_dialog_show (const WEdit *edit, GQueue * compl, int max_width)
     /* [previous][next][first][last][top][bottom][index][help]  */
 343 {
 344     const WRect *we = &CONST_WIDGET (edit)->rect;
 345     int start_x, start_y, offset;
 346     char *curr = NULL;
 347     WDialog *compl_dlg;
 348     WListbox *compl_list;
 349     int compl_dlg_h;  // completion dialog height
 350     int compl_dlg_w;  // completion dialog width
 351     GList *i;
 352 
 353     // calculate the dialog metrics
 354     compl_dlg_h = g_queue_get_length (compl) + 2;
 355     compl_dlg_w = max_width + 4;
 356     start_x = we->x + edit->curs_col + edit->start_col + EDIT_TEXT_HORIZONTAL_OFFSET
 357         + (edit->fullscreen != 0 ? 0 : 1) + edit_options.line_state_width;
 358     start_y =
 359         we->y + edit->curs_row + EDIT_TEXT_VERTICAL_OFFSET + (edit->fullscreen != 0 ? 0 : 1) + 1;
 360 
 361     if (start_x < 0)
 362         start_x = 0;
 363     if (start_x < we->x + 1)
 364         start_x = we->x + 1 + edit_options.line_state_width;
 365     if (compl_dlg_w > COLS)
 366         compl_dlg_w = COLS;
 367     if (compl_dlg_h > LINES - 2)
 368         compl_dlg_h = LINES - 2;
 369 
 370     offset = start_x + compl_dlg_w - COLS;
 371     if (offset > 0)
 372         start_x -= offset;
 373     offset = start_y + compl_dlg_h - LINES;
 374     if (offset > 0)
 375         start_y -= offset;
 376 
 377     // create the dialog
 378     compl_dlg = dlg_create (TRUE, start_y, start_x, compl_dlg_h, compl_dlg_w, WPOS_KEEP_DEFAULT,
 379                             TRUE, dialog_colors, NULL, NULL, "[Completion]", NULL);
 380 
 381     // create the listbox
 382     compl_list = listbox_new (1, 1, compl_dlg_h - 2, compl_dlg_w - 2, FALSE, NULL);
 383 
 384     // fill the listbox with the completions in the reverse order
 385     for (i = g_queue_peek_tail_link (compl); i != NULL; i = g_list_previous (i))
 386         listbox_add_item (compl_list, LISTBOX_APPEND_AT_END, 0, ((GString *) i->data)->str, NULL,
 387                           FALSE);
 388 
 389     group_add_widget (GROUP (compl_dlg), compl_list);
 390 
 391     // pop up the dialog and apply the chosen completion
 392     if (dlg_run (compl_dlg) == B_ENTER)
 393     {
 394         listbox_get_current (compl_list, &curr, NULL);
 395         curr = g_strdup (curr);
 396     }
 397 
 398     // destroy dialog before return
 399     widget_destroy (WIDGET (compl_dlg));
 400 
 401     return curr;
 402 }
 403 
 404 /* --------------------------------------------------------------------------------------------- */
 405 
 406 /**
 407  * Complete current word using regular expression search
 408  * backwards beginning at the current cursor position.
 409  */
 410 
 411 void
 412 edit_complete_word_cmd (WEdit *edit)
     /* [previous][next][first][last][top][bottom][index][help]  */
 413 {
 414     off_t word_start = 0;
 415     gsize word_len = 0;
 416     GString *match_expr;
 417     gsize i;
 418     GQueue * compl;  // completions: list of GString*
 419     int max_width;
 420 
 421     // search start of word to be completed
 422     if (!edit_buffer_find_word_start (&edit->buffer, &word_start, &word_len))
 423         return;
 424 
 425     // prepare match expression
 426     // match_expr = g_strdup_printf ("\\b%.*s[a-zA-Z_0-9]+", word_len, bufpos);
 427     match_expr = g_string_new ("(^|\\s+|\\b)");
 428     for (i = 0; i < word_len; i++)
 429         g_string_append_c (match_expr, edit_buffer_get_byte (&edit->buffer, word_start + i));
 430     g_string_append (
 431         match_expr,
 432         "[^\\s\\.=\\+\\[\\]\\(\\)\\,\\;\\:\\\"\\'\\-\\?\\/\\|\\\\\\{\\}\\*\\&\\^\\%%\\$#@\\!]+");
 433 
 434     // collect possible completions
 435     compl = edit_collect_completions (edit, word_start, word_len, match_expr->str, &max_width);
 436 
 437     g_string_free (match_expr, TRUE);
 438 
 439     if (compl== NULL)
 440         return;
 441 
 442     if (g_queue_get_length (compl) == 1)
 443     {
 444         // insert completed word if there is only one match
 445 
 446         GString *curr_compl;
 447 
 448         curr_compl = (GString *) g_queue_peek_head (compl);
 449         edit_complete_word_insert_recoded_completion (edit, curr_compl->str, word_len);
 450     }
 451     else
 452     {
 453         // more than one possible completion => ask the user
 454 
 455         char *curr_compl;
 456 
 457         // let the user select the preferred completion
 458         curr_compl = edit_completion_dialog_show (edit, compl, max_width);
 459         if (curr_compl != NULL)
 460         {
 461             edit_complete_word_insert_recoded_completion (edit, curr_compl, word_len);
 462             g_free (curr_compl);
 463         }
 464     }
 465 
 466     g_queue_free_full (compl, edit_completion_string_free);
 467 }
 468 
 469 /* --------------------------------------------------------------------------------------------- */

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