/* Editor spell checker Copyright (C) 2012-2024 Free Software Foundation, Inc. Written by: Ilia Maslakov , 2012 Andrew Borodin , 2013, 2021 This file is part of the Midnight Commander. The Midnight Commander is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. The Midnight Commander is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #ifdef HAVE_CHARSET #include "lib/charsets.h" #endif #include "lib/strutil.h" #include "lib/util.h" /* MC_PTR_FREE() */ #include "lib/tty/tty.h" /* COLS, LINES */ #include "src/setup.h" #include "editwidget.h" #include "spell.h" /*** global variables ****************************************************************************/ /*** file scope macro definitions ****************************************************************/ #define B_SKIP_WORD (B_USER+3) #define B_ADD_WORD (B_USER+4) /*** file scope type declarations ****************************************************************/ typedef struct aspell_struct { AspellConfig *config; AspellSpeller *speller; } spell_t; /*** forward declarations (file scope functions) *************************************************/ /*** file scope variables ************************************************************************/ static GModule *spell_module = NULL; static spell_t *global_speller = NULL; static AspellConfig *(*mc_new_aspell_config) (void); static int (*mc_aspell_config_replace) (AspellConfig * ths, const char *key, const char *value); static AspellCanHaveError *(*mc_new_aspell_speller) (AspellConfig * config); static unsigned int (*mc_aspell_error_number) (const AspellCanHaveError * ths); static const char *(*mc_aspell_speller_error_message) (const AspellSpeller * ths); static const AspellError *(*mc_aspell_speller_error) (const AspellSpeller * ths); static AspellSpeller *(*mc_to_aspell_speller) (AspellCanHaveError * obj); static int (*mc_aspell_speller_check) (AspellSpeller * ths, const char *word, int word_size); static const AspellWordList *(*mc_aspell_speller_suggest) (AspellSpeller * ths, const char *word, int word_size); static AspellStringEnumeration *(*mc_aspell_word_list_elements) (const struct AspellWordList * ths); static const char *(*mc_aspell_config_retrieve) (AspellConfig * ths, const char *key); static void (*mc_delete_aspell_speller) (AspellSpeller * ths); static void (*mc_delete_aspell_config) (AspellConfig * ths); static void (*mc_delete_aspell_can_have_error) (AspellCanHaveError * ths); static const char *(*mc_aspell_error_message) (const AspellCanHaveError * ths); static void (*mc_delete_aspell_string_enumeration) (AspellStringEnumeration * ths); static AspellDictInfoEnumeration *(*mc_aspell_dict_info_list_elements) (const AspellDictInfoList * ths); static AspellDictInfoList *(*mc_get_aspell_dict_info_list) (AspellConfig * config); static const AspellDictInfo *(*mc_aspell_dict_info_enumeration_next) (AspellDictInfoEnumeration * ths); static const char *(*mc_aspell_string_enumeration_next) (AspellStringEnumeration * ths); static void (*mc_delete_aspell_dict_info_enumeration) (AspellDictInfoEnumeration * ths); static unsigned int (*mc_aspell_word_list_size) (const AspellWordList * ths); static const AspellError *(*mc_aspell_error) (const AspellCanHaveError * ths); static int (*mc_aspell_speller_add_to_personal) (AspellSpeller * ths, const char *word, int word_size); static int (*mc_aspell_speller_save_all_word_lists) (AspellSpeller * ths); static struct { const char *code; const char *name; } spell_codes_map[] = { /* *INDENT-OFF* */ {"br", N_("Breton")}, {"cs", N_("Czech")}, {"cy", N_("Welsh")}, {"da", N_("Danish")}, {"de", N_("German")}, {"el", N_("Greek")}, {"en", N_("English")}, {"en_GB", N_("British English")}, {"en_CA", N_("Canadian English")}, {"en_US", N_("American English")}, {"eo", N_("Esperanto")}, {"es", N_("Spanish")}, {"fo", N_("Faroese")}, {"fr", N_("French")}, {"it", N_("Italian")}, {"nl", N_("Dutch")}, {"no", N_("Norwegian")}, {"pl", N_("Polish")}, {"pt", N_("Portuguese")}, {"ro", N_("Romanian")}, {"ru", N_("Russian")}, {"sk", N_("Slovak")}, {"sv", N_("Swedish")}, {"uk", N_("Ukrainian")}, {NULL, NULL} /* *INDENT-ON* */ }; /*** file scope functions ************************************************************************/ /* --------------------------------------------------------------------------------------------- */ /** * Found the language name by language code. For example: en_US -> American English. * * @param code Short name of the language (ru, en, pl, uk, etc...) * @return language name */ static const char * spell_decode_lang (const char *code) { size_t i; for (i = 0; spell_codes_map[i].code != NULL; i++) { if (strcmp (spell_codes_map[i].code, code) == 0) return _(spell_codes_map[i].name); } return code; } /* --------------------------------------------------------------------------------------------- */ /** * Checks if aspell library and symbols are available. * * @return FALSE or error */ static gboolean spell_available (void) { gchar *spell_module_fname; gboolean ret = FALSE; if (spell_module != NULL) return TRUE; spell_module_fname = g_module_build_path (NULL, "libaspell"); spell_module = g_module_open (spell_module_fname, G_MODULE_BIND_LAZY); g_free (spell_module_fname); if (spell_module == NULL) return FALSE; if (!g_module_symbol (spell_module, "new_aspell_config", (void *) &mc_new_aspell_config)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_dict_info_list_elements", (void *) &mc_aspell_dict_info_list_elements)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_dict_info_enumeration_next", (void *) &mc_aspell_dict_info_enumeration_next)) goto error_ret; if (!g_module_symbol (spell_module, "new_aspell_speller", (void *) &mc_new_aspell_speller)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_error_number", (void *) &mc_aspell_error_number)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_speller_error_message", (void *) &mc_aspell_speller_error_message)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_speller_error", (void *) &mc_aspell_speller_error)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_error", (void *) &mc_aspell_error)) goto error_ret; if (!g_module_symbol (spell_module, "to_aspell_speller", (void *) &mc_to_aspell_speller)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_speller_check", (void *) &mc_aspell_speller_check)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_speller_suggest", (void *) &mc_aspell_speller_suggest)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_word_list_elements", (void *) &mc_aspell_word_list_elements)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_string_enumeration_next", (void *) &mc_aspell_string_enumeration_next)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_config_replace", (void *) &mc_aspell_config_replace)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_error_message", (void *) &mc_aspell_error_message)) goto error_ret; if (!g_module_symbol (spell_module, "delete_aspell_speller", (void *) &mc_delete_aspell_speller)) goto error_ret; if (!g_module_symbol (spell_module, "delete_aspell_config", (void *) &mc_delete_aspell_config)) goto error_ret; if (!g_module_symbol (spell_module, "delete_aspell_string_enumeration", (void *) &mc_delete_aspell_string_enumeration)) goto error_ret; if (!g_module_symbol (spell_module, "get_aspell_dict_info_list", (void *) &mc_get_aspell_dict_info_list)) goto error_ret; if (!g_module_symbol (spell_module, "delete_aspell_can_have_error", (void *) &mc_delete_aspell_can_have_error)) goto error_ret; if (!g_module_symbol (spell_module, "delete_aspell_dict_info_enumeration", (void *) &mc_delete_aspell_dict_info_enumeration)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_config_retrieve", (void *) &mc_aspell_config_retrieve)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_word_list_size", (void *) &mc_aspell_word_list_size)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_speller_add_to_personal", (void *) &mc_aspell_speller_add_to_personal)) goto error_ret; if (!g_module_symbol (spell_module, "aspell_speller_save_all_word_lists", (void *) &mc_aspell_speller_save_all_word_lists)) goto error_ret; ret = TRUE; error_ret: if (!ret) { g_module_close (spell_module); spell_module = NULL; } return ret; } /* --------------------------------------------------------------------------------------------- */ /*** public functions ****************************************************************************/ /* --------------------------------------------------------------------------------------------- */ /** * Initialization of Aspell support. */ void aspell_init (void) { AspellCanHaveError *error = NULL; if (strcmp (spell_language, "NONE") == 0) return; if (global_speller != NULL) return; global_speller = g_try_malloc (sizeof (spell_t)); if (global_speller == NULL) return; if (!spell_available ()) { MC_PTR_FREE (global_speller); return; } global_speller->config = mc_new_aspell_config (); global_speller->speller = NULL; if (spell_language != NULL) mc_aspell_config_replace (global_speller->config, "lang", spell_language); error = mc_new_aspell_speller (global_speller->config); if (mc_aspell_error_number (error) == 0) global_speller->speller = mc_to_aspell_speller (error); else { edit_error_dialog (_("Error"), mc_aspell_error_message (error)); mc_delete_aspell_can_have_error (error); aspell_clean (); } } /* --------------------------------------------------------------------------------------------- */ /** * Deinitialization of Aspell support. */ void aspell_clean (void) { if (global_speller == NULL) return; if (global_speller->speller != NULL) mc_delete_aspell_speller (global_speller->speller); if (global_speller->config != NULL) mc_delete_aspell_config (global_speller->config); MC_PTR_FREE (global_speller); g_module_close (spell_module); spell_module = NULL; } /* --------------------------------------------------------------------------------------------- */ /** * Get array of available languages. * * @param lang_list Array of languages. Must be cleared before use * @return language list length */ unsigned int aspell_get_lang_list (GPtrArray * lang_list) { AspellDictInfoList *dlist; AspellDictInfoEnumeration *elem; const AspellDictInfo *entry; unsigned int i = 0; if (spell_module == NULL) return 0; /* the returned pointer should _not_ need to be deleted */ dlist = mc_get_aspell_dict_info_list (global_speller->config); elem = mc_aspell_dict_info_list_elements (dlist); while ((entry = mc_aspell_dict_info_enumeration_next (elem)) != NULL) if (entry->name != NULL) { g_ptr_array_add (lang_list, g_strdup (entry->name)); i++; } mc_delete_aspell_dict_info_enumeration (elem); return i; } /* --------------------------------------------------------------------------------------------- */ /** * Clear the array of languages. * * @param array Array of languages */ void aspell_array_clean (GPtrArray * array) { if (array != NULL) g_ptr_array_free (array, TRUE); } /* --------------------------------------------------------------------------------------------- */ /** * Get the current language name. * * @return language name */ const char * aspell_get_lang (void) { const char *code; code = mc_aspell_config_retrieve (global_speller->config, "lang"); return spell_decode_lang (code); } /* --------------------------------------------------------------------------------------------- */ /** * Set the language. * * @param lang Language name * @return FALSE or error */ gboolean aspell_set_lang (const char *lang) { if (lang != NULL) { AspellCanHaveError *error; const char *spell_codeset; g_free (spell_language); spell_language = g_strdup (lang); #ifdef HAVE_CHARSET if (mc_global.source_codepage > 0) spell_codeset = get_codepage_id (mc_global.source_codepage); else #endif spell_codeset = str_detect_termencoding (); mc_aspell_config_replace (global_speller->config, "lang", lang); mc_aspell_config_replace (global_speller->config, "encoding", spell_codeset); /* the returned pointer should _not_ need to be deleted */ if (global_speller->speller != NULL) mc_delete_aspell_speller (global_speller->speller); global_speller->speller = NULL; error = mc_new_aspell_speller (global_speller->config); if (mc_aspell_error (error) != 0) { mc_delete_aspell_can_have_error (error); return FALSE; } global_speller->speller = mc_to_aspell_speller (error); } return TRUE; } /* --------------------------------------------------------------------------------------------- */ /** * Check word. * * @param word Word for spell check * @param word_size Word size (in bytes) * @return FALSE if word is not in the dictionary */ gboolean aspell_check (const char *word, const int word_size) { int res = 0; if (word != NULL && global_speller != NULL && global_speller->speller != NULL) res = mc_aspell_speller_check (global_speller->speller, word, word_size); return (res == 1); } /* --------------------------------------------------------------------------------------------- */ /** * Examine dictionaries and suggest possible words that may repalce the incorrect word. * * @param suggest array of words to iterate through * @param word Word for spell check * @param word_size Word size (in bytes) * @return count of suggests for the word */ unsigned int aspell_suggest (GPtrArray * suggest, const char *word, const int word_size) { unsigned int size = 0; if (word != NULL && global_speller != NULL && global_speller->speller != NULL) { const AspellWordList *wordlist; wordlist = mc_aspell_speller_suggest (global_speller->speller, word, word_size); if (wordlist != NULL) { AspellStringEnumeration *elements = NULL; unsigned int i; elements = mc_aspell_word_list_elements (wordlist); size = mc_aspell_word_list_size (wordlist); for (i = 0; i < size; i++) { const char *cur_sugg_word; cur_sugg_word = mc_aspell_string_enumeration_next (elements); if (cur_sugg_word != NULL) g_ptr_array_add (suggest, g_strdup (cur_sugg_word)); } mc_delete_aspell_string_enumeration (elements); } } return size; } /* --------------------------------------------------------------------------------------------- */ /* * Add word to personal dictionary. * * @param word Word for spell check * @param word_size Word size (in bytes) * @return FALSE or error */ gboolean aspell_add_to_dict (const char *word, int word_size) { mc_aspell_speller_add_to_personal (global_speller->speller, word, word_size); if (mc_aspell_speller_error (global_speller->speller) != 0) { edit_error_dialog (_("Error"), mc_aspell_speller_error_message (global_speller->speller)); return FALSE; } mc_aspell_speller_save_all_word_lists (global_speller->speller); if (mc_aspell_speller_error (global_speller->speller) != 0) { edit_error_dialog (_("Error"), mc_aspell_speller_error_message (global_speller->speller)); return FALSE; } return TRUE; } /* --------------------------------------------------------------------------------------------- */ int edit_suggest_current_word (WEdit * edit) { gsize cut_len = 0; gsize word_len = 0; off_t word_start = 0; int retval = B_SKIP_WORD; GString *match_word; /* search start of word to spell check */ match_word = edit_buffer_get_word_from_pos (&edit->buffer, edit->buffer.curs1, &word_start, &cut_len); word_len = match_word->len; #ifdef HAVE_CHARSET if (mc_global.source_codepage >= 0 && mc_global.source_codepage != mc_global.display_codepage) { GString *tmp_word; tmp_word = str_convert_to_display (match_word->str); g_string_free (match_word, TRUE); match_word = tmp_word; } #endif if (match_word != NULL) { if (!aspell_check (match_word->str, (int) word_len)) { GPtrArray *suggest; unsigned int res; guint i; suggest = g_ptr_array_new_with_free_func (g_free); res = aspell_suggest (suggest, match_word->str, (int) word_len); if (res != 0) { char *new_word = NULL; edit->found_start = word_start; edit->found_len = word_len; edit->force |= REDRAW_PAGE; edit_scroll_screen_over_cursor (edit); edit_render_keypress (edit); retval = spell_dialog_spell_suggest_show (edit, match_word->str, &new_word, suggest); edit_cursor_move (edit, word_len - cut_len); if (retval == B_ENTER && new_word != NULL) { #ifdef HAVE_CHARSET if (mc_global.source_codepage >= 0 && (mc_global.source_codepage != mc_global.display_codepage)) { GString *tmp_word; tmp_word = str_convert_to_input (new_word); MC_PTR_FREE (new_word); if (tmp_word != NULL) new_word = g_string_free (tmp_word, FALSE); } #endif for (i = 0; i < word_len; i++) edit_backspace (edit, TRUE); if (new_word != NULL) { for (i = 0; new_word[i] != '\0'; i++) edit_insert (edit, new_word[i]); g_free (new_word); } } else if (retval == B_ADD_WORD) aspell_add_to_dict (match_word->str, (int) word_len); } g_ptr_array_free (suggest, TRUE); edit->found_start = 0; edit->found_len = 0; } g_string_free (match_word, TRUE); } return retval; } /* --------------------------------------------------------------------------------------------- */ void edit_spellcheck_file (WEdit * edit) { if (edit->buffer.curs_line > 0) { edit_cursor_move (edit, -edit->buffer.curs1); edit_move_to_prev_col (edit, 0); edit_update_curs_row (edit); } do { int c1, c2; c2 = edit_buffer_get_current_byte (&edit->buffer); do { if (edit->buffer.curs1 >= edit->buffer.size) return; c1 = c2; edit_cursor_move (edit, 1); c2 = edit_buffer_get_current_byte (&edit->buffer); } while (is_break_char (c1) || is_break_char (c2)); } while (edit_suggest_current_word (edit) != B_CANCEL); } /* --------------------------------------------------------------------------------------------- */ void edit_set_spell_lang (void) { GPtrArray *lang_list; lang_list = g_ptr_array_new_with_free_func (g_free); if (aspell_get_lang_list (lang_list) != 0) { const char *lang; lang = spell_dialog_lang_list_show (lang_list); if (lang != NULL) (void) aspell_set_lang (lang); } aspell_array_clean (lang_list); } /* --------------------------------------------------------------------------------------------- */ /** * Show suggests for the current word. * * @param edit Editor object * @param word Word for spell check * @param new_word Word to replace the incorrect word * @param suggest Array of suggests for current word * @return code of pressed button */ int spell_dialog_spell_suggest_show (WEdit * edit, const char *word, char **new_word, const GPtrArray * suggest) { int sug_dlg_h = 14; /* dialog height */ int sug_dlg_w = 29; /* dialog width */ int xpos, ypos; char *lang_label; char *word_label; unsigned int i; int res; char *curr = NULL; WDialog *sug_dlg; WGroup *g; WListbox *sug_list; int max_btn_len = 0; int replace_len; int skip_len; int cancel_len; WButton *add_btn; WButton *replace_btn; WButton *skip_btn; WButton *cancel_button; int word_label_len; /* calculate the dialog metrics */ xpos = (COLS - sug_dlg_w) / 2; ypos = (LINES - sug_dlg_h) * 2 / 3; /* Sometimes menu can hide replaced text. I don't like it */ if ((edit->curs_row >= ypos - 1) && (edit->curs_row <= ypos + sug_dlg_h - 1)) ypos -= sug_dlg_h; add_btn = button_new (5, 28, B_ADD_WORD, NORMAL_BUTTON, _("&Add word"), 0); replace_btn = button_new (7, 28, B_ENTER, NORMAL_BUTTON, _("&Replace"), 0); replace_len = button_get_len (replace_btn); skip_btn = button_new (9, 28, B_SKIP_WORD, NORMAL_BUTTON, _("&Skip"), 0); skip_len = button_get_len (skip_btn); cancel_button = button_new (11, 28, B_CANCEL, NORMAL_BUTTON, _("&Cancel"), 0); cancel_len = button_get_len (cancel_button); max_btn_len = MAX (replace_len, skip_len); max_btn_len = MAX (max_btn_len, cancel_len); lang_label = g_strdup_printf ("%s: %s", _("Language"), aspell_get_lang ()); word_label = g_strdup_printf ("%s: %s", _("Misspelled"), word); word_label_len = str_term_width1 (word_label) + 5; sug_dlg_w += max_btn_len; sug_dlg_w = MAX (sug_dlg_w, word_label_len) + 1; sug_dlg = dlg_create (TRUE, ypos, xpos, sug_dlg_h, sug_dlg_w, WPOS_KEEP_DEFAULT, TRUE, dialog_colors, NULL, NULL, "[ASpell]", _("Check word")); g = GROUP (sug_dlg); group_add_widget (g, label_new (1, 2, lang_label)); group_add_widget (g, label_new (3, 2, word_label)); group_add_widget (g, groupbox_new (4, 2, sug_dlg_h - 5, 25, _("Suggest"))); sug_list = listbox_new (5, 2, sug_dlg_h - 7, 24, FALSE, NULL); for (i = 0; i < suggest->len; i++) listbox_add_item (sug_list, LISTBOX_APPEND_AT_END, 0, g_ptr_array_index (suggest, i), NULL, FALSE); group_add_widget (g, sug_list); group_add_widget (g, add_btn); group_add_widget (g, replace_btn); group_add_widget (g, skip_btn); group_add_widget (g, cancel_button); res = dlg_run (sug_dlg); if (res == B_ENTER) { char *tmp = NULL; listbox_get_current (sug_list, &curr, NULL); if (curr != NULL) tmp = g_strdup (curr); *new_word = tmp; } widget_destroy (WIDGET (sug_dlg)); g_free (lang_label); g_free (word_label); return res; } /* --------------------------------------------------------------------------------------------- */ /** * Show dialog to select language for spell check. * * @param languages Array of available languages * @return name of chosen language */ const char * spell_dialog_lang_list_show (const GPtrArray * languages) { int lang_dlg_h = 12; /* dialog height */ int lang_dlg_w = 30; /* dialog width */ const char *selected_lang = NULL; unsigned int i; int res; Listbox *lang_list; /* Create listbox */ lang_list = listbox_window_centered_new (-1, -1, lang_dlg_h, lang_dlg_w, _("Select language"), "[ASpell]"); for (i = 0; i < languages->len; i++) LISTBOX_APPEND_TEXT (lang_list, 0, g_ptr_array_index (languages, i), NULL, FALSE); res = listbox_run (lang_list); if (res >= 0) selected_lang = g_ptr_array_index (languages, (unsigned int) res); return selected_lang; } /* --------------------------------------------------------------------------------------------- */