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