Merge branch '3205_eta'
[midnight-commander.git] / src / editor / editcomplete.c
blob22a364ca91ed992974f717d77a239d7ab2a97f4c
1 /*
2 Editor word completion engine
4 Copyright (C) 2021-2024
5 Free Software Foundation, Inc.
7 Written by:
8 Andrew Borodin <aborodin@vmail.ru>, 2021-2022
10 This file is part of the Midnight Commander.
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.
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.
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/>.
26 #include <config.h>
28 #include <ctype.h> /* isspace() */
29 #include <string.h>
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"
40 #include "src/setup.h" /* verbose */
42 #include "editwidget.h"
43 #include "edit-impl.h"
44 #include "editsearch.h"
46 #include "editcomplete.h"
48 /*** global variables ****************************************************************************/
50 /*** file scope macro definitions ****************************************************************/
52 /*** file scope type declarations ****************************************************************/
54 /*** forward declarations (file scope functions) *************************************************/
56 /*** file scope variables ************************************************************************/
58 /* --------------------------------------------------------------------------------------------- */
59 /*** file scope functions ************************************************************************/
60 /* --------------------------------------------------------------------------------------------- */
62 /**
63 * Get current word under cursor
65 * @param esm status message window
66 * @param srch mc_search object
67 * @param word_start start word position
69 * @return newly allocated string or NULL if no any words under cursor
72 static GString *
73 edit_collect_completions_get_current_word (edit_search_status_msg_t *esm, mc_search_t *srch,
74 off_t word_start)
76 WEdit *edit = esm->edit;
77 gsize len = 0;
78 GString *temp = NULL;
80 if (mc_search_run (srch, (void *) esm, word_start, edit->buffer.size, &len))
82 off_t i;
84 for (i = 0; i < (off_t) len; i++)
86 int chr;
88 chr = edit_buffer_get_byte (&edit->buffer, word_start + i);
89 if (!isspace (chr))
91 if (temp == NULL)
92 temp = g_string_sized_new (len);
94 g_string_append_c (temp, chr);
99 return temp;
102 /* --------------------------------------------------------------------------------------------- */
104 * collect the possible completions from one buffer
107 static void
108 edit_collect_completion_from_one_buffer (gboolean active_buffer, GQueue **compl,
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)
113 GString *temp = NULL;
114 gsize len = 0;
115 off_t start = -1;
117 while (mc_search_run (srch, (void *) esm, start + 1, last_byte, &len))
119 gsize i;
120 int width;
122 if (temp == NULL)
123 temp = g_string_sized_new (8);
124 else
125 g_string_set_size (temp, 0);
127 start = srch->normal_offset;
129 /* add matched completion if not yet added */
130 for (i = 0; i < len; i++)
132 int ch;
134 ch = edit_buffer_get_byte (&esm->edit->buffer, start + i);
135 if (isspace (ch))
136 continue;
138 /* skip current word */
139 if (start + (off_t) i == word_start)
140 break;
142 g_string_append_c (temp, ch);
145 if (temp->len == 0)
146 continue;
148 if (current_word != NULL && g_string_equal (current_word, temp))
149 continue;
151 if (*compl == NULL)
152 *compl = g_queue_new ();
153 else
155 GList *l;
157 for (l = g_queue_peek_head_link (*compl); l != NULL; l = g_list_next (l))
159 GString *s = (GString *) l->data;
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;
167 if (l != NULL)
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))
173 /* move to the end */
174 g_queue_unlink (*compl, l);
175 g_queue_push_tail_link (*compl, l);
178 continue;
182 #ifdef HAVE_CHARSET
184 GString *recoded;
186 recoded = str_nconvert_to_display (temp->str, temp->len);
187 if (recoded != NULL)
189 if (recoded->len != 0)
190 mc_g_string_copy (temp, recoded);
192 g_string_free (recoded, TRUE);
195 #endif
197 if (active_buffer)
198 g_queue_push_tail (*compl, temp);
199 else
200 g_queue_push_head (*compl, temp);
202 start += len;
204 /* note the maximal length needed for the completion dialog */
205 width = str_term_width1 (temp->str);
206 *max_width = MAX (*max_width, width);
208 temp = NULL;
211 if (temp != NULL)
212 g_string_free (temp, TRUE);
215 /* --------------------------------------------------------------------------------------------- */
217 * collect the possible completions from all buffers
220 static GQueue *
221 edit_collect_completions (WEdit *edit, off_t word_start, gsize word_len,
222 const char *match_expr, int *max_width)
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;
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;
239 entire_file =
240 mc_config_get_bool (mc_global.main_config, CONFIG_APP_SECTION,
241 "editor_wordcompletion_collect_entire_file", FALSE);
243 last_byte = entire_file ? edit->buffer.size : word_start;
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;
250 esm.first = TRUE;
251 esm.edit = edit;
252 esm.offset = entire_file ? 0 : word_start;
254 status_msg_init (STATUS_MSG (&esm), _("Collect completions"), 1.0, simple_status_msg_init_cb,
255 edit_search_status_update_cb, NULL);
257 current_word = edit_collect_completions_get_current_word (&esm, srch, word_start);
259 *max_width = 0;
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);
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)
271 const WGroup *owner = CONST_GROUP (CONST_WIDGET (edit)->owner);
272 gboolean saved_verbose;
273 GList *w;
275 /* don't show incorrect percentage in edit_search_status_update_cb() */
276 saved_verbose = verbose;
277 verbose = FALSE;
279 for (w = owner->widgets; w != NULL; w = g_list_next (w))
281 Widget *ww = WIDGET (w->data);
282 WEdit *e;
284 if (!edit_widget_is_editor (ww))
285 continue;
287 e = EDIT (ww);
289 if (e == edit)
290 continue;
292 /* search in entire file */
293 word_start = 0;
294 last_byte = e->buffer.size;
295 esm.edit = e;
296 esm.offset = 0;
298 edit_collect_completion_from_one_buffer (FALSE, &compl, srch, &esm, word_start,
299 word_len, last_byte, current_word, max_width);
302 verbose = saved_verbose;
305 status_msg_deinit (STATUS_MSG (&esm));
306 mc_search_free (srch);
307 if (current_word != NULL)
308 g_string_free (current_word, TRUE);
310 return compl;
313 /* --------------------------------------------------------------------------------------------- */
316 * Insert autocompleted word into editor.
318 * @param edit editor object
319 * @param completion word for completion
320 * @param word_len offset from beginning for insert
323 static void
324 edit_complete_word_insert_recoded_completion (WEdit *edit, char *completion, gsize word_len)
326 #ifdef HAVE_CHARSET
327 GString *temp;
329 temp = str_convert_to_input (completion);
330 if (temp != NULL)
332 for (completion = temp->str + word_len; *completion != '\0'; completion++)
333 edit_insert (edit, *completion);
334 g_string_free (temp, TRUE);
336 #else
337 for (completion += word_len; *completion != '\0'; completion++)
338 edit_insert (edit, *completion);
339 #endif
342 /* --------------------------------------------------------------------------------------------- */
344 static void
345 edit_completion_string_free (gpointer data)
347 g_string_free ((GString *) data, TRUE);
350 /* --------------------------------------------------------------------------------------------- */
351 /*** public functions ****************************************************************************/
352 /* --------------------------------------------------------------------------------------------- */
353 /* let the user select its preferred completion */
355 /* Public function for unit tests */
356 char *
357 edit_completion_dialog_show (const WEdit *edit, GQueue *compl, int max_width)
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;
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;
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;
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;
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);
396 /* create the listbox */
397 compl_list = listbox_new (1, 1, compl_dlg_h - 2, compl_dlg_w - 2, FALSE, NULL);
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);
404 group_add_widget (GROUP (compl_dlg), compl_list);
406 /* pop up the dialog and apply the chosen completion */
407 if (dlg_run (compl_dlg) == B_ENTER)
409 listbox_get_current (compl_list, &curr, NULL);
410 curr = g_strdup (curr);
413 /* destroy dialog before return */
414 widget_destroy (WIDGET (compl_dlg));
416 return curr;
419 /* --------------------------------------------------------------------------------------------- */
422 * Complete current word using regular expression search
423 * backwards beginning at the current cursor position.
426 void
427 edit_complete_word_cmd (WEdit *edit)
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;
436 /* search start of word to be completed */
437 if (!edit_buffer_find_word_start (&edit->buffer, &word_start, &word_len))
438 return;
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\\.=\\+\\[\\]\\(\\)\\,\\;\\:\\\"\\'\\-\\?\\/\\|\\\\\\{\\}\\*\\&\\^\\%%\\$#@\\!]+");
448 /* collect possible completions */
449 compl = edit_collect_completions (edit, word_start, word_len, match_expr->str, &max_width);
451 g_string_free (match_expr, TRUE);
453 if (compl == NULL)
454 return;
456 if (g_queue_get_length (compl) == 1)
458 /* insert completed word if there is only one match */
460 GString *curr_compl;
462 curr_compl = (GString *) g_queue_peek_head (compl);
463 edit_complete_word_insert_recoded_completion (edit, curr_compl->str, word_len);
465 else
467 /* more than one possible completion => ask the user */
469 char *curr_compl;
471 /* let the user select the preferred completion */
472 curr_compl = edit_completion_dialog_show (edit, compl, max_width);
473 if (curr_compl != NULL)
475 edit_complete_word_insert_recoded_completion (edit, curr_compl, word_len);
476 g_free (curr_compl);
480 g_queue_free_full (compl, edit_completion_string_free);
483 /* --------------------------------------------------------------------------------------------- */