Merge branch '4562_mcedit_macros_paste'
[midnight-commander.git] / src / editor / spell.c
blobf316d0fa6af96db605c4bf9ac3e734d4bdeb9f47
1 /*
2 Editor spell checker
4 Copyright (C) 2012-2024
5 Free Software Foundation, Inc.
7 Written by:
8 Ilia Maslakov <il.smind@gmail.com>, 2012
9 Andrew Borodin <aborodin@vmail.ru>, 2013-2024
12 This file is part of the Midnight Commander.
14 The Midnight Commander is free software: you can redistribute it
15 and/or modify it under the terms of the GNU General Public License as
16 published by the Free Software Foundation, either version 3 of the License,
17 or (at your option) any later version.
19 The Midnight Commander is distributed in the hope that it will be useful,
20 but WITHOUT ANY WARRANTY; without even the implied warranty of
21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 GNU General Public License for more details.
24 You should have received a copy of the GNU General Public License
25 along with this program. If not, see <http://www.gnu.org/licenses/>.
28 #include <config.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <gmodule.h>
33 #include <aspell.h>
35 #ifdef HAVE_CHARSET
36 #include "lib/charsets.h"
37 #endif
38 #include "lib/strutil.h"
39 #include "lib/util.h" /* MC_PTR_FREE() */
40 #include "lib/tty/tty.h" /* COLS, LINES */
42 #include "src/setup.h"
44 #include "editwidget.h"
46 #include "spell.h"
48 /*** global variables ****************************************************************************/
50 /*** file scope macro definitions ****************************************************************/
52 #define B_SKIP_WORD (B_USER+3)
53 #define B_ADD_WORD (B_USER+4)
55 #define ASPELL_FUNCTION_AVAILABLE(f) \
56 g_module_symbol (spell_module, #f, (void *) &mc_##f)
58 /*** file scope type declarations ****************************************************************/
60 typedef struct aspell_struct
62 AspellConfig *config;
63 AspellSpeller *speller;
64 } spell_t;
66 /*** forward declarations (file scope functions) *************************************************/
68 /*** file scope variables ************************************************************************/
70 static GModule *spell_module = NULL;
71 static spell_t *global_speller = NULL;
73 static AspellConfig *(*mc_new_aspell_config) (void);
74 static int (*mc_aspell_config_replace) (AspellConfig * ths, const char *key, const char *value);
75 static AspellCanHaveError *(*mc_new_aspell_speller) (AspellConfig * config);
76 static unsigned int (*mc_aspell_error_number) (const AspellCanHaveError * ths);
77 static const char *(*mc_aspell_speller_error_message) (const AspellSpeller * ths);
78 static const AspellError *(*mc_aspell_speller_error) (const AspellSpeller * ths);
80 static AspellSpeller *(*mc_to_aspell_speller) (AspellCanHaveError * obj);
81 static int (*mc_aspell_speller_check) (AspellSpeller * ths, const char *word, int word_size);
82 static const AspellWordList *(*mc_aspell_speller_suggest) (AspellSpeller * ths,
83 const char *word, int word_size);
84 static AspellStringEnumeration *(*mc_aspell_word_list_elements) (const struct AspellWordList * ths);
85 static const char *(*mc_aspell_config_retrieve) (AspellConfig * ths, const char *key);
86 static void (*mc_delete_aspell_speller) (AspellSpeller * ths);
87 static void (*mc_delete_aspell_config) (AspellConfig * ths);
88 static void (*mc_delete_aspell_can_have_error) (AspellCanHaveError * ths);
89 static const char *(*mc_aspell_error_message) (const AspellCanHaveError * ths);
90 static void (*mc_delete_aspell_string_enumeration) (AspellStringEnumeration * ths);
91 static AspellDictInfoEnumeration *(*mc_aspell_dict_info_list_elements)
92 (const AspellDictInfoList * ths);
93 static AspellDictInfoList *(*mc_get_aspell_dict_info_list) (AspellConfig * config);
94 static const AspellDictInfo *(*mc_aspell_dict_info_enumeration_next)
95 (AspellDictInfoEnumeration * ths);
96 static const char *(*mc_aspell_string_enumeration_next) (AspellStringEnumeration * ths);
97 static void (*mc_delete_aspell_dict_info_enumeration) (AspellDictInfoEnumeration * ths);
98 static unsigned int (*mc_aspell_word_list_size) (const AspellWordList * ths);
99 static const AspellError *(*mc_aspell_error) (const AspellCanHaveError * ths);
100 static int (*mc_aspell_speller_add_to_personal) (AspellSpeller * ths, const char *word,
101 int word_size);
102 static int (*mc_aspell_speller_save_all_word_lists) (AspellSpeller * ths);
104 static struct
106 const char *code;
107 const char *name;
108 } spell_codes_map[] = {
109 /* *INDENT-OFF* */
110 {"br", N_("Breton")},
111 {"cs", N_("Czech")},
112 {"cy", N_("Welsh")},
113 {"da", N_("Danish")},
114 {"de", N_("German")},
115 {"el", N_("Greek")},
116 {"en", N_("English")},
117 {"en_GB", N_("British English")},
118 {"en_CA", N_("Canadian English")},
119 {"en_US", N_("American English")},
120 {"eo", N_("Esperanto")},
121 {"es", N_("Spanish")},
122 {"fo", N_("Faroese")},
123 {"fr", N_("French")},
124 {"it", N_("Italian")},
125 {"nl", N_("Dutch")},
126 {"no", N_("Norwegian")},
127 {"pl", N_("Polish")},
128 {"pt", N_("Portuguese")},
129 {"ro", N_("Romanian")},
130 {"ru", N_("Russian")},
131 {"sk", N_("Slovak")},
132 {"sv", N_("Swedish")},
133 {"uk", N_("Ukrainian")},
134 {NULL, NULL}
135 /* *INDENT-ON* */
138 /* --------------------------------------------------------------------------------------------- */
139 /*** file scope functions ************************************************************************/
140 /* --------------------------------------------------------------------------------------------- */
142 * Found the language name by language code. For example: en_US -> American English.
144 * @param code Short name of the language (ru, en, pl, uk, etc...)
145 * @return language name
148 static const char *
149 spell_decode_lang (const char *code)
151 size_t i;
153 for (i = 0; spell_codes_map[i].code != NULL; i++)
155 if (strcmp (spell_codes_map[i].code, code) == 0)
156 return _(spell_codes_map[i].name);
159 return code;
162 /* --------------------------------------------------------------------------------------------- */
164 * Checks if aspell library and symbols are available.
166 * @return FALSE or error
169 static gboolean
170 spell_available (void)
172 gchar *spell_module_fname;
174 if (spell_module != NULL)
175 return TRUE;
177 spell_module_fname = g_module_build_path (NULL, "libaspell");
178 spell_module = g_module_open (spell_module_fname, G_MODULE_BIND_LAZY);
179 g_free (spell_module_fname);
181 if (spell_module != NULL
182 && ASPELL_FUNCTION_AVAILABLE (new_aspell_config)
183 && ASPELL_FUNCTION_AVAILABLE (aspell_dict_info_list_elements)
184 && ASPELL_FUNCTION_AVAILABLE (aspell_dict_info_enumeration_next)
185 && ASPELL_FUNCTION_AVAILABLE (new_aspell_speller)
186 && ASPELL_FUNCTION_AVAILABLE (aspell_error_number)
187 && ASPELL_FUNCTION_AVAILABLE (aspell_speller_error_message)
188 && ASPELL_FUNCTION_AVAILABLE (aspell_speller_error)
189 && ASPELL_FUNCTION_AVAILABLE (aspell_error)
190 && ASPELL_FUNCTION_AVAILABLE (to_aspell_speller)
191 && ASPELL_FUNCTION_AVAILABLE (aspell_speller_check)
192 && ASPELL_FUNCTION_AVAILABLE (aspell_speller_suggest)
193 && ASPELL_FUNCTION_AVAILABLE (aspell_word_list_elements)
194 && ASPELL_FUNCTION_AVAILABLE (aspell_string_enumeration_next)
195 && ASPELL_FUNCTION_AVAILABLE (aspell_config_replace)
196 && ASPELL_FUNCTION_AVAILABLE (aspell_error_message)
197 && ASPELL_FUNCTION_AVAILABLE (delete_aspell_speller)
198 && ASPELL_FUNCTION_AVAILABLE (delete_aspell_config)
199 && ASPELL_FUNCTION_AVAILABLE (delete_aspell_string_enumeration)
200 && ASPELL_FUNCTION_AVAILABLE (get_aspell_dict_info_list)
201 && ASPELL_FUNCTION_AVAILABLE (delete_aspell_can_have_error)
202 && ASPELL_FUNCTION_AVAILABLE (delete_aspell_dict_info_enumeration)
203 && ASPELL_FUNCTION_AVAILABLE (aspell_config_retrieve)
204 && ASPELL_FUNCTION_AVAILABLE (aspell_word_list_size)
205 && ASPELL_FUNCTION_AVAILABLE (aspell_speller_add_to_personal)
206 && ASPELL_FUNCTION_AVAILABLE (aspell_speller_save_all_word_lists))
207 return TRUE;
209 g_module_close (spell_module);
210 spell_module = NULL;
211 return FALSE;
214 /* --------------------------------------------------------------------------------------------- */
216 * Get the current language name.
218 * @return language name
221 static const char *
222 aspell_get_lang (void)
224 const char *code;
226 code = mc_aspell_config_retrieve (global_speller->config, "lang");
227 return spell_decode_lang (code);
230 /* --------------------------------------------------------------------------------------------- */
232 * Get array of available languages.
234 * @param lang_list Array of languages. Must be cleared before use
235 * @return language list length
238 static unsigned int
239 aspell_get_lang_list (GPtrArray *lang_list)
241 AspellDictInfoList *dlist;
242 AspellDictInfoEnumeration *elem;
243 const AspellDictInfo *entry;
244 unsigned int i = 0;
246 if (spell_module == NULL)
247 return 0;
249 /* the returned pointer should _not_ need to be deleted */
250 dlist = mc_get_aspell_dict_info_list (global_speller->config);
251 elem = mc_aspell_dict_info_list_elements (dlist);
253 while ((entry = mc_aspell_dict_info_enumeration_next (elem)) != NULL)
254 if (entry->name != NULL)
256 g_ptr_array_add (lang_list, g_strdup (entry->name));
257 i++;
260 mc_delete_aspell_dict_info_enumeration (elem);
262 return i;
265 /* --------------------------------------------------------------------------------------------- */
267 * Set the language.
269 * @param lang Language name
270 * @return FALSE or error
273 static gboolean
274 aspell_set_lang (const char *lang)
276 if (lang != NULL)
278 AspellCanHaveError *error;
279 const char *spell_codeset;
281 g_free (spell_language);
282 spell_language = g_strdup (lang);
284 #ifdef HAVE_CHARSET
285 if (mc_global.source_codepage > 0)
286 spell_codeset = get_codepage_id (mc_global.source_codepage);
287 else
288 #endif
289 spell_codeset = str_detect_termencoding ();
291 mc_aspell_config_replace (global_speller->config, "lang", lang);
292 mc_aspell_config_replace (global_speller->config, "encoding", spell_codeset);
294 /* the returned pointer should _not_ need to be deleted */
295 if (global_speller->speller != NULL)
296 mc_delete_aspell_speller (global_speller->speller);
298 global_speller->speller = NULL;
300 error = mc_new_aspell_speller (global_speller->config);
301 if (mc_aspell_error (error) != 0)
303 mc_delete_aspell_can_have_error (error);
304 return FALSE;
307 global_speller->speller = mc_to_aspell_speller (error);
309 return TRUE;
312 /* --------------------------------------------------------------------------------------------- */
314 * Show suggests for the current word.
316 * @param edit Editor object
317 * @param word Word for spell check
318 * @param new_word Word to replace the incorrect word
319 * @param suggest Array of suggests for current word
320 * @return code of pressed button
323 static int
324 spell_dialog_spell_suggest_show (WEdit *edit, const char *word, char **new_word,
325 const GPtrArray *suggest)
328 int sug_dlg_h = 14; /* dialog height */
329 int sug_dlg_w = 29; /* dialog width */
330 int xpos, ypos;
331 char *lang_label;
332 char *word_label;
333 unsigned int i;
334 int res;
335 char *curr = NULL;
336 WDialog *sug_dlg;
337 WGroup *g;
338 WListbox *sug_list;
339 int max_btn_len = 0;
340 int replace_len;
341 int skip_len;
342 int cancel_len;
343 WButton *add_btn;
344 WButton *replace_btn;
345 WButton *skip_btn;
346 WButton *cancel_button;
347 int word_label_len;
349 /* calculate the dialog metrics */
350 xpos = (COLS - sug_dlg_w) / 2;
351 ypos = (LINES - sug_dlg_h) * 2 / 3;
353 /* Sometimes menu can hide replaced text. I don't like it */
354 if ((edit->curs_row >= ypos - 1) && (edit->curs_row <= ypos + sug_dlg_h - 1))
355 ypos -= sug_dlg_h;
357 add_btn = button_new (5, 28, B_ADD_WORD, NORMAL_BUTTON, _("&Add word"), 0);
358 replace_btn = button_new (7, 28, B_ENTER, NORMAL_BUTTON, _("&Replace"), 0);
359 replace_len = button_get_len (replace_btn);
360 skip_btn = button_new (9, 28, B_SKIP_WORD, NORMAL_BUTTON, _("&Skip"), 0);
361 skip_len = button_get_len (skip_btn);
362 cancel_button = button_new (11, 28, B_CANCEL, NORMAL_BUTTON, _("&Cancel"), 0);
363 cancel_len = button_get_len (cancel_button);
365 max_btn_len = MAX (replace_len, skip_len);
366 max_btn_len = MAX (max_btn_len, cancel_len);
368 lang_label = g_strdup_printf ("%s: %s", _("Language"), aspell_get_lang ());
369 word_label = g_strdup_printf ("%s: %s", _("Misspelled"), word);
370 word_label_len = str_term_width1 (word_label) + 5;
372 sug_dlg_w += max_btn_len;
373 sug_dlg_w = MAX (sug_dlg_w, word_label_len) + 1;
375 sug_dlg = dlg_create (TRUE, ypos, xpos, sug_dlg_h, sug_dlg_w, WPOS_KEEP_DEFAULT, TRUE,
376 dialog_colors, NULL, NULL, "[ASpell]", _("Check word"));
377 g = GROUP (sug_dlg);
379 group_add_widget (g, label_new (1, 2, lang_label));
380 group_add_widget (g, label_new (3, 2, word_label));
382 group_add_widget (g, groupbox_new (4, 2, sug_dlg_h - 5, 25, _("Suggest")));
384 sug_list = listbox_new (5, 2, sug_dlg_h - 7, 24, FALSE, NULL);
385 for (i = 0; i < suggest->len; i++)
386 listbox_add_item (sug_list, LISTBOX_APPEND_AT_END, 0, g_ptr_array_index (suggest, i), NULL,
387 FALSE);
388 group_add_widget (g, sug_list);
390 group_add_widget (g, add_btn);
391 group_add_widget (g, replace_btn);
392 group_add_widget (g, skip_btn);
393 group_add_widget (g, cancel_button);
395 res = dlg_run (sug_dlg);
396 if (res == B_ENTER)
398 char *tmp = NULL;
399 listbox_get_current (sug_list, &curr, NULL);
401 if (curr != NULL)
402 tmp = g_strdup (curr);
403 *new_word = tmp;
406 widget_destroy (WIDGET (sug_dlg));
407 g_free (lang_label);
408 g_free (word_label);
410 return res;
413 /* --------------------------------------------------------------------------------------------- */
415 * Add word to personal dictionary.
417 * @param word Word for spell check
418 * @param word_size Word size (in bytes)
419 * @return FALSE or error
421 static gboolean
422 aspell_add_to_dict (const char *word, int word_size)
424 mc_aspell_speller_add_to_personal (global_speller->speller, word, word_size);
426 if (mc_aspell_speller_error (global_speller->speller) != 0)
428 edit_error_dialog (_("Error"), mc_aspell_speller_error_message (global_speller->speller));
429 return FALSE;
432 mc_aspell_speller_save_all_word_lists (global_speller->speller);
434 if (mc_aspell_speller_error (global_speller->speller) != 0)
436 edit_error_dialog (_("Error"), mc_aspell_speller_error_message (global_speller->speller));
437 return FALSE;
440 return TRUE;
443 /* --------------------------------------------------------------------------------------------- */
445 * Examine dictionaries and suggest possible words that may repalce the incorrect word.
447 * @param suggest array of words to iterate through
448 * @param word Word for spell check
449 * @param word_size Word size (in bytes)
450 * @return count of suggests for the word
453 static unsigned int
454 aspell_suggest (GPtrArray *suggest, const char *word, const int word_size)
456 unsigned int size = 0;
458 if (word != NULL && global_speller != NULL && global_speller->speller != NULL)
460 const AspellWordList *wordlist;
462 wordlist = mc_aspell_speller_suggest (global_speller->speller, word, word_size);
463 if (wordlist != NULL)
465 AspellStringEnumeration *elements = NULL;
466 unsigned int i;
468 elements = mc_aspell_word_list_elements (wordlist);
469 size = mc_aspell_word_list_size (wordlist);
471 for (i = 0; i < size; i++)
473 const char *cur_sugg_word;
475 cur_sugg_word = mc_aspell_string_enumeration_next (elements);
476 if (cur_sugg_word != NULL)
477 g_ptr_array_add (suggest, g_strdup (cur_sugg_word));
480 mc_delete_aspell_string_enumeration (elements);
484 return size;
487 /* --------------------------------------------------------------------------------------------- */
489 * Check word.
491 * @param word Word for spell check
492 * @param word_size Word size (in bytes)
493 * @return FALSE if word is not in the dictionary
496 static gboolean
497 aspell_check (const char *word, const int word_size)
499 int res = 0;
501 if (word != NULL && global_speller != NULL && global_speller->speller != NULL)
502 res = mc_aspell_speller_check (global_speller->speller, word, word_size);
504 return (res == 1);
507 /* --------------------------------------------------------------------------------------------- */
509 * Clear the array of languages.
511 * @param array Array of languages
514 static void
515 aspell_array_clean (GPtrArray *array)
517 if (array != NULL)
518 g_ptr_array_free (array, TRUE);
521 /* --------------------------------------------------------------------------------------------- */
522 /*** public functions ****************************************************************************/
523 /* --------------------------------------------------------------------------------------------- */
525 * Initialization of Aspell support.
528 void
529 aspell_init (void)
531 AspellCanHaveError *error = NULL;
533 if (strcmp (spell_language, "NONE") == 0)
534 return;
536 if (global_speller != NULL)
537 return;
539 global_speller = g_try_malloc (sizeof (spell_t));
540 if (global_speller == NULL)
541 return;
543 if (!spell_available ())
545 MC_PTR_FREE (global_speller);
546 return;
549 global_speller->config = mc_new_aspell_config ();
550 global_speller->speller = NULL;
552 if (spell_language != NULL)
553 mc_aspell_config_replace (global_speller->config, "lang", spell_language);
555 error = mc_new_aspell_speller (global_speller->config);
557 if (mc_aspell_error_number (error) == 0)
558 global_speller->speller = mc_to_aspell_speller (error);
559 else
561 edit_error_dialog (_("Error"), mc_aspell_error_message (error));
562 mc_delete_aspell_can_have_error (error);
563 aspell_clean ();
567 /* --------------------------------------------------------------------------------------------- */
569 * Deinitialization of Aspell support.
572 void
573 aspell_clean (void)
575 if (global_speller == NULL)
576 return;
578 if (global_speller->speller != NULL)
579 mc_delete_aspell_speller (global_speller->speller);
581 if (global_speller->config != NULL)
582 mc_delete_aspell_config (global_speller->config);
584 MC_PTR_FREE (global_speller);
586 g_module_close (spell_module);
587 spell_module = NULL;
590 /* --------------------------------------------------------------------------------------------- */
593 edit_suggest_current_word (WEdit *edit)
595 gsize cut_len = 0;
596 gsize word_len = 0;
597 off_t word_start = 0;
598 int retval = B_SKIP_WORD;
599 GString *match_word;
601 /* search start of word to spell check */
602 match_word = edit_buffer_get_word_from_pos (&edit->buffer, edit->buffer.curs1, &word_start,
603 &cut_len);
604 word_len = match_word->len;
606 #ifdef HAVE_CHARSET
607 if (mc_global.source_codepage >= 0 && mc_global.source_codepage != mc_global.display_codepage)
609 GString *tmp_word;
611 tmp_word = str_convert_to_display (match_word->str);
612 g_string_free (match_word, TRUE);
613 match_word = tmp_word;
615 #endif
616 if (match_word != NULL)
618 if (!aspell_check (match_word->str, (int) word_len))
620 GPtrArray *suggest;
621 unsigned int res;
622 guint i;
624 suggest = g_ptr_array_new_with_free_func (g_free);
626 res = aspell_suggest (suggest, match_word->str, (int) word_len);
627 if (res != 0)
629 char *new_word = NULL;
631 edit->found_start = word_start;
632 edit->found_len = word_len;
633 edit->force |= REDRAW_PAGE;
634 edit_scroll_screen_over_cursor (edit);
635 edit_render_keypress (edit);
637 retval =
638 spell_dialog_spell_suggest_show (edit, match_word->str, &new_word, suggest);
639 edit_cursor_move (edit, word_len - cut_len);
641 if (retval == B_ENTER && new_word != NULL)
643 #ifdef HAVE_CHARSET
644 if (mc_global.source_codepage >= 0 &&
645 (mc_global.source_codepage != mc_global.display_codepage))
647 GString *tmp_word;
649 tmp_word = str_convert_to_input (new_word);
650 MC_PTR_FREE (new_word);
651 if (tmp_word != NULL)
652 new_word = g_string_free (tmp_word, FALSE);
654 #endif
655 for (i = 0; i < word_len; i++)
656 edit_backspace (edit, TRUE);
657 if (new_word != NULL)
659 for (i = 0; new_word[i] != '\0'; i++)
660 edit_insert (edit, new_word[i]);
661 g_free (new_word);
664 else if (retval == B_ADD_WORD)
665 aspell_add_to_dict (match_word->str, (int) word_len);
668 g_ptr_array_free (suggest, TRUE);
669 edit->found_start = 0;
670 edit->found_len = 0;
673 g_string_free (match_word, TRUE);
676 return retval;
679 /* --------------------------------------------------------------------------------------------- */
681 void
682 edit_spellcheck_file (WEdit *edit)
684 if (edit->buffer.curs_line > 0)
686 edit_cursor_move (edit, -edit->buffer.curs1);
687 edit_move_to_prev_col (edit, 0);
688 edit_update_curs_row (edit);
693 int c1, c2;
695 c2 = edit_buffer_get_current_byte (&edit->buffer);
699 if (edit->buffer.curs1 >= edit->buffer.size)
700 return;
702 c1 = c2;
703 edit_cursor_move (edit, 1);
704 c2 = edit_buffer_get_current_byte (&edit->buffer);
706 while (is_break_char (c1) || is_break_char (c2));
708 while (edit_suggest_current_word (edit) != B_CANCEL);
711 /* --------------------------------------------------------------------------------------------- */
713 void
714 edit_set_spell_lang (void)
716 GPtrArray *lang_list;
718 lang_list = g_ptr_array_new_with_free_func (g_free);
719 if (aspell_get_lang_list (lang_list) != 0)
721 const char *lang;
723 lang = spell_dialog_lang_list_show (lang_list);
724 if (lang != NULL)
725 (void) aspell_set_lang (lang);
727 aspell_array_clean (lang_list);
730 /* --------------------------------------------------------------------------------------------- */
732 * Show dialog to select language for spell check.
734 * @param languages Array of available languages
735 * @return name of chosen language
738 const char *
739 spell_dialog_lang_list_show (const GPtrArray *languages)
742 int lang_dlg_h = 12; /* dialog height */
743 int lang_dlg_w = 30; /* dialog width */
744 const char *selected_lang = NULL;
745 unsigned int i;
746 int res;
747 Listbox *lang_list;
749 /* Create listbox */
750 lang_list = listbox_window_centered_new (-1, -1, lang_dlg_h, lang_dlg_w,
751 _("Select language"), "[ASpell]");
753 for (i = 0; i < languages->len; i++)
754 LISTBOX_APPEND_TEXT (lang_list, 0, g_ptr_array_index (languages, i), NULL, FALSE);
756 res = listbox_run (lang_list);
757 if (res >= 0)
758 selected_lang = g_ptr_array_index (languages, (unsigned int) res);
760 return selected_lang;
763 /* --------------------------------------------------------------------------------------------- */