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 *
edit_collect_completions_get_current_word(edit_search_status_msg_t * esm,mc_search_t * srch,off_t word_start)71 edit_collect_completions_get_current_word (edit_search_status_msg_t * esm, mc_search_t * srch,
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
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)106 edit_collect_completion_from_one_buffer (gboolean active_buffer, GQueue ** compl,
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 *
edit_collect_completions(WEdit * edit,off_t word_start,gsize word_len,const char * match_expr,int * max_width)215 edit_collect_completions (WEdit * edit, off_t word_start, gsize word_len,
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
edit_complete_word_insert_recoded_completion(WEdit * edit,char * completion,gsize word_len)318 edit_complete_word_insert_recoded_completion (WEdit * edit, char *completion, gsize word_len)
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
edit_completion_string_free(gpointer data)337 edit_completion_string_free (gpointer data)
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 *
edit_completion_dialog_show(const WEdit * edit,GQueue * compl,int max_width)349 edit_completion_dialog_show (const WEdit * edit, GQueue * compl, int max_width)
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
edit_complete_word_cmd(WEdit * edit)419 edit_complete_word_cmd (WEdit * edit)
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 /* --------------------------------------------------------------------------------------------- */
476