2 Editor word completion engine
4 Copyright (C) 2021-2024
5 Free Software Foundation, Inc.
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/>.
28 #include <ctype.h> /* isspace() */
31 #include "lib/global.h"
32 #include "lib/search.h"
33 #include "lib/strutil.h"
35 #include "lib/charsets.h" /* str_convert_to_input() */
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 /* --------------------------------------------------------------------------------------------- */
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
73 edit_collect_completions_get_current_word (edit_search_status_msg_t
*esm
, mc_search_t
*srch
,
76 WEdit
*edit
= esm
->edit
;
80 if (mc_search_run (srch
, (void *) esm
, word_start
, edit
->buffer
.size
, &len
))
84 for (i
= 0; i
< (off_t
) len
; i
++)
88 chr
= edit_buffer_get_byte (&edit
->buffer
, word_start
+ i
);
92 temp
= g_string_sized_new (len
);
94 g_string_append_c (temp
, chr
);
102 /* --------------------------------------------------------------------------------------------- */
104 * collect the possible completions from one buffer
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
;
117 while (mc_search_run (srch
, (void *) esm
, start
+ 1, last_byte
, &len
))
123 temp
= g_string_sized_new (8);
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
++)
134 ch
= edit_buffer_get_byte (&esm
->edit
->buffer
, start
+ i
);
138 /* skip current word */
139 if (start
+ (off_t
) i
== word_start
)
142 g_string_append_c (temp
, ch
);
148 if (current_word
!= NULL
&& g_string_equal (current_word
, temp
))
152 *compl = g_queue_new ();
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)
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
);
186 recoded
= str_nconvert_to_display (temp
->str
, temp
->len
);
189 if (recoded
->len
!= 0)
190 mc_g_string_copy (temp
, recoded
);
192 g_string_free (recoded
, TRUE
);
198 g_queue_push_tail (*compl, temp
);
200 g_queue_push_head (*compl, temp
);
204 /* note the maximal length needed for the completion dialog */
205 width
= str_term_width1 (temp
->str
);
206 *max_width
= MAX (*max_width
, width
);
212 g_string_free (temp
, TRUE
);
215 /* --------------------------------------------------------------------------------------------- */
217 * collect the possible completions from all buffers
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
;
227 GString
*current_word
;
228 gboolean entire_file
, all_files
;
229 edit_search_status_msg_t esm
;
232 srch
= mc_search_new (match_expr
, cp_source
);
234 srch
= mc_search_new (match_expr
, NULL
);
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
;
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
);
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 */
267 mc_config_get_bool (mc_global
.main_config
, CONFIG_APP_SECTION
,
268 "editor_wordcompletion_collect_all_files", TRUE
);
271 const WGroup
*owner
= CONST_GROUP (CONST_WIDGET (edit
)->owner
);
272 gboolean saved_verbose
;
275 /* don't show incorrect percentage in edit_search_status_update_cb() */
276 saved_verbose
= verbose
;
279 for (w
= owner
->widgets
; w
!= NULL
; w
= g_list_next (w
))
281 Widget
*ww
= WIDGET (w
->data
);
284 if (!edit_widget_is_editor (ww
))
292 /* search in entire file */
294 last_byte
= e
->buffer
.size
;
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
);
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
324 edit_complete_word_insert_recoded_completion (WEdit
*edit
, char *completion
, gsize word_len
)
329 temp
= str_convert_to_input (completion
);
332 for (completion
= temp
->str
+ word_len
; *completion
!= '\0'; completion
++)
333 edit_insert (edit
, *completion
);
334 g_string_free (temp
, TRUE
);
337 for (completion
+= word_len
; *completion
!= '\0'; completion
++)
338 edit_insert (edit
, *completion
);
342 /* --------------------------------------------------------------------------------------------- */
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 */
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
;
363 WListbox
*compl_list
;
364 int compl_dlg_h
; /* completion dialog height */
365 int compl_dlg_w
; /* completion dialog width */
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;
377 if (start_x
< we
->x
+ 1)
378 start_x
= we
->x
+ 1 + edit_options
.line_state_width
;
379 if (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
;
387 offset
= start_y
+ compl_dlg_h
- LINES
;
391 /* create the dialog */
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
,
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
));
419 /* --------------------------------------------------------------------------------------------- */
422 * Complete current word using regular expression search
423 * backwards beginning at the current cursor position.
427 edit_complete_word_cmd (WEdit
*edit
)
429 off_t word_start
= 0;
433 GQueue
*compl; /* completions: list of GString* */
436 /* search start of word to be completed */
437 if (!edit_buffer_find_word_start (&edit
->buffer
, &word_start
, &word_len
))
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
);
456 if (g_queue_get_length (compl) == 1)
458 /* insert completed word if there is only one match */
462 curr_compl
= (GString
*) g_queue_peek_head (compl);
463 edit_complete_word_insert_recoded_completion (edit
, curr_compl
->str
, word_len
);
467 /* more than one possible completion => ask the user */
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
);
480 g_queue_free_full (compl, edit_completion_string_free
);
483 /* --------------------------------------------------------------------------------------------- */