1 /* vim: set sw=8: -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * pluma-spell-checker.c
4 * This file is part of pluma
5 *
6 * Copyright (C) 2002-2006 Paolo Maggi
7 * Copyright (C) 2012-2021 MATE Developers
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin St, Fifth Floor,
22 * Boston, MA 02110-1301, USA.
23 */
24
25 /*
26 * Modified by the pluma Team, 2002-2006. See the AUTHORS file for a
27 * list of people on the pluma Team.
28 * See the ChangeLog files for a list of changes.
29 */
30
31 #ifdef HAVE_CONFIG_H
32 #include <config.h>
33 #endif
34
35 #include <string.h>
36
37 #include <enchant.h>
38
39 #include <glib/gi18n.h>
40 #include <glib.h>
41
42 #include "pluma-spell-checker.h"
43 #include "pluma-spell-utils.h"
44
45 struct _PlumaSpellChecker
46 {
47 GObject parent_instance;
48
49 EnchantDict *dict;
50 EnchantBroker *broker;
51 const PlumaSpellCheckerLanguage *active_lang;
52 };
53
54 /* GObject properties */
55 enum {
56 PROP_0 = 0,
57 PROP_LANGUAGE,
58 LAST_PROP
59 };
60
61 /* Signals */
62 enum {
63 ADD_WORD_TO_PERSONAL = 0,
64 ADD_WORD_TO_SESSION,
65 SET_LANGUAGE,
66 CLEAR_SESSION,
67 LAST_SIGNAL
68 };
69
70 static guint signals[LAST_SIGNAL] = { 0 };
71
G_DEFINE_TYPE(PlumaSpellChecker,pluma_spell_checker,G_TYPE_OBJECT)72 G_DEFINE_TYPE(PlumaSpellChecker, pluma_spell_checker, G_TYPE_OBJECT)
73
74 static void
75 pluma_spell_checker_set_property (GObject *object,
76 guint prop_id,
77 const GValue *value,
78 GParamSpec *pspec)
79 {
80 /*
81 PlumaSpellChecker *spell = PLUMA_SPELL_CHECKER (object);
82 */
83
84 switch (prop_id)
85 {
86 case PROP_LANGUAGE:
87 /* TODO */
88 break;
89 default:
90 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
91 }
92 }
93
94 static void
pluma_spell_checker_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)95 pluma_spell_checker_get_property (GObject *object,
96 guint prop_id,
97 GValue *value,
98 GParamSpec *pspec)
99 {
100 /*
101 PlumaSpellChecker *spell = PLUMA_SPELL_CHECKER (object);
102 */
103
104 switch (prop_id)
105 {
106 case PROP_LANGUAGE:
107 /* TODO */
108 default:
109 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
110 }
111 }
112
113 static void
pluma_spell_checker_finalize(GObject * object)114 pluma_spell_checker_finalize (GObject *object)
115 {
116 PlumaSpellChecker *spell_checker;
117
118 g_return_if_fail (PLUMA_IS_SPELL_CHECKER (object));
119
120 spell_checker = PLUMA_SPELL_CHECKER (object);
121
122 if (spell_checker->dict != NULL)
123 enchant_broker_free_dict (spell_checker->broker, spell_checker->dict);
124
125 if (spell_checker->broker != NULL)
126 enchant_broker_free (spell_checker->broker);
127
128 G_OBJECT_CLASS (pluma_spell_checker_parent_class)->finalize (object);
129 }
130
131 static void
pluma_spell_checker_class_init(PlumaSpellCheckerClass * klass)132 pluma_spell_checker_class_init (PlumaSpellCheckerClass * klass)
133 {
134 GObjectClass *object_class;
135
136 object_class = G_OBJECT_CLASS (klass);
137
138 object_class->set_property = pluma_spell_checker_set_property;
139 object_class->get_property = pluma_spell_checker_get_property;
140
141 object_class->finalize = pluma_spell_checker_finalize;
142
143 g_object_class_install_property (object_class,
144 PROP_LANGUAGE,
145 g_param_spec_pointer ("language",
146 "Language",
147 "The language used by the spell checker",
148 G_PARAM_READWRITE));
149
150 signals[ADD_WORD_TO_PERSONAL] =
151 g_signal_new ("add_word_to_personal",
152 G_OBJECT_CLASS_TYPE (object_class),
153 G_SIGNAL_RUN_LAST,
154 G_STRUCT_OFFSET (PlumaSpellCheckerClass, add_word_to_personal),
155 NULL, NULL, NULL,
156 G_TYPE_NONE,
157 2,
158 G_TYPE_STRING,
159 G_TYPE_INT);
160
161 signals[ADD_WORD_TO_SESSION] =
162 g_signal_new ("add_word_to_session",
163 G_OBJECT_CLASS_TYPE (object_class),
164 G_SIGNAL_RUN_LAST,
165 G_STRUCT_OFFSET (PlumaSpellCheckerClass, add_word_to_session),
166 NULL, NULL, NULL,
167 G_TYPE_NONE,
168 2,
169 G_TYPE_STRING,
170 G_TYPE_INT);
171
172 signals[SET_LANGUAGE] =
173 g_signal_new ("set_language",
174 G_OBJECT_CLASS_TYPE (object_class),
175 G_SIGNAL_RUN_LAST,
176 G_STRUCT_OFFSET (PlumaSpellCheckerClass, set_language),
177 NULL, NULL, NULL,
178 G_TYPE_NONE,
179 1,
180 G_TYPE_POINTER);
181
182 signals[CLEAR_SESSION] =
183 g_signal_new ("clear_session",
184 G_OBJECT_CLASS_TYPE (object_class),
185 G_SIGNAL_RUN_LAST,
186 G_STRUCT_OFFSET (PlumaSpellCheckerClass, clear_session),
187 NULL, NULL, NULL,
188 G_TYPE_NONE,
189 0);
190 }
191
192 static void
pluma_spell_checker_init(PlumaSpellChecker * spell_checker)193 pluma_spell_checker_init (PlumaSpellChecker *spell_checker)
194 {
195 spell_checker->broker = enchant_broker_init ();
196 spell_checker->dict = NULL;
197 spell_checker->active_lang = NULL;
198 }
199
200 PlumaSpellChecker *
pluma_spell_checker_new(void)201 pluma_spell_checker_new (void)
202 {
203 PlumaSpellChecker *spell;
204
205 spell = PLUMA_SPELL_CHECKER (
206 g_object_new (PLUMA_TYPE_SPELL_CHECKER, NULL));
207
208 g_return_val_if_fail (spell != NULL, NULL);
209
210 return spell;
211 }
212
213 static gboolean
lazy_init(PlumaSpellChecker * spell,const PlumaSpellCheckerLanguage * language)214 lazy_init (PlumaSpellChecker *spell,
215 const PlumaSpellCheckerLanguage *language)
216 {
217 if (spell->dict != NULL)
218 return TRUE;
219
220 g_return_val_if_fail (spell->broker != NULL, FALSE);
221
222 spell->active_lang = NULL;
223
224 if (language != NULL)
225 {
226 spell->active_lang = language;
227 }
228 else
229 {
230 /* First try to get a default language */
231 const PlumaSpellCheckerLanguage *l;
232 gint i = 0;
233 const gchar * const *lang_tags = g_get_language_names ();
234
235 while (lang_tags [i])
236 {
237 l = pluma_spell_checker_language_from_key (lang_tags [i]);
238
239 if (l != NULL)
240 {
241 spell->active_lang = l;
242 break;
243 }
244
245 i++;
246 }
247 }
248
249 /* Second try to get a default language */
250 if (spell->active_lang == NULL)
251 {
252 spell->active_lang = pluma_spell_checker_language_from_key ("en_US");
253
254 /* Last try to get a default language */
255 if (spell->active_lang == NULL)
256 {
257 const GSList *langs;
258 langs = pluma_spell_checker_get_available_languages ();
259 if (langs != NULL)
260 spell->active_lang = (const PlumaSpellCheckerLanguage *)langs->data;
261 }
262 }
263
264 if (spell->active_lang != NULL)
265 {
266 const gchar *key;
267
268 key = pluma_spell_checker_language_to_key (spell->active_lang);
269
270 spell->dict = enchant_broker_request_dict (spell->broker,
271 key);
272 }
273
274 if (spell->dict == NULL)
275 {
276 spell->active_lang = NULL;
277
278 if (language != NULL)
279 g_warning ("Spell checker plugin: cannot select a default language.");
280
281 return FALSE;
282 }
283
284 return TRUE;
285 }
286
287 gboolean
pluma_spell_checker_set_language(PlumaSpellChecker * spell,const PlumaSpellCheckerLanguage * language)288 pluma_spell_checker_set_language (PlumaSpellChecker *spell,
289 const PlumaSpellCheckerLanguage *language)
290 {
291 gboolean ret;
292
293 g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
294
295 if (spell->dict != NULL)
296 {
297 enchant_broker_free_dict (spell->broker, spell->dict);
298 spell->dict = NULL;
299 }
300
301 ret = lazy_init (spell, language);
302
303 if (ret)
304 g_signal_emit (G_OBJECT (spell), signals[SET_LANGUAGE], 0, language);
305 else
306 g_warning ("Spell checker plugin: cannot use language %s.",
307 pluma_spell_checker_language_to_string (language));
308
309 return ret;
310 }
311
312 const PlumaSpellCheckerLanguage *
pluma_spell_checker_get_language(PlumaSpellChecker * spell)313 pluma_spell_checker_get_language (PlumaSpellChecker *spell)
314 {
315 g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), NULL);
316
317 if (!lazy_init (spell, spell->active_lang))
318 return NULL;
319
320 return spell->active_lang;
321 }
322
323 gboolean
pluma_spell_checker_check_word(PlumaSpellChecker * spell,const gchar * word,gssize len)324 pluma_spell_checker_check_word (PlumaSpellChecker *spell,
325 const gchar *word,
326 gssize len)
327 {
328 gint enchant_result;
329 gboolean res = FALSE;
330
331 g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
332 g_return_val_if_fail (word != NULL, FALSE);
333
334 if (!lazy_init (spell, spell->active_lang))
335 return FALSE;
336
337 if (len < 0)
338 len = strlen (word);
339
340 if (strcmp (word, "pluma") == 0)
341 return TRUE;
342
343 if (pluma_spell_utils_is_digit (word, len))
344 return TRUE;
345
346 g_return_val_if_fail (spell->dict != NULL, FALSE);
347 enchant_result = enchant_dict_check (spell->dict, word, len);
348
349 switch (enchant_result)
350 {
351 case -1:
352 /* error */
353 res = FALSE;
354
355 g_warning ("Spell checker plugin: error checking word '%s' (%s).",
356 word, enchant_dict_get_error (spell->dict));
357
358 break;
359 case 1:
360 /* it is not in the directory */
361 res = FALSE;
362 break;
363 case 0:
364 /* is is in the directory */
365 res = TRUE;
366 break;
367 default:
368 g_return_val_if_reached (FALSE);
369 }
370
371 return res;
372 }
373
374
375 /* return NULL on error or if no suggestions are found */
376 GSList *
pluma_spell_checker_get_suggestions(PlumaSpellChecker * spell,const gchar * word,gssize len)377 pluma_spell_checker_get_suggestions (PlumaSpellChecker *spell,
378 const gchar *word,
379 gssize len)
380 {
381 gchar **suggestions;
382 size_t n_suggestions = 0;
383 GSList *suggestions_list = NULL;
384 gint i;
385
386 g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), NULL);
387 g_return_val_if_fail (word != NULL, NULL);
388
389 if (!lazy_init (spell, spell->active_lang))
390 return NULL;
391
392 g_return_val_if_fail (spell->dict != NULL, NULL);
393
394 if (len < 0)
395 len = strlen (word);
396
397 suggestions = enchant_dict_suggest (spell->dict, word, len, &n_suggestions);
398
399 if (n_suggestions == 0)
400 return NULL;
401
402 g_return_val_if_fail (suggestions != NULL, NULL);
403
404 for (i = 0; i < (gint)n_suggestions; i++)
405 {
406 suggestions_list = g_slist_prepend (suggestions_list,
407 suggestions[i]);
408 }
409
410 /* The single suggestions will be freed by the caller */
411 g_free (suggestions);
412
413 suggestions_list = g_slist_reverse (suggestions_list);
414
415 return suggestions_list;
416 }
417
418 gboolean
pluma_spell_checker_add_word_to_personal(PlumaSpellChecker * spell,const gchar * word,gssize len)419 pluma_spell_checker_add_word_to_personal (PlumaSpellChecker *spell,
420 const gchar *word,
421 gssize len)
422 {
423 g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
424 g_return_val_if_fail (word != NULL, FALSE);
425
426 if (!lazy_init (spell, spell->active_lang))
427 return FALSE;
428
429 g_return_val_if_fail (spell->dict != NULL, FALSE);
430
431 if (len < 0)
432 len = strlen (word);
433
434 enchant_dict_add (spell->dict, word, len);
435
436
437 g_signal_emit (G_OBJECT (spell), signals[ADD_WORD_TO_PERSONAL], 0, word, len);
438
439 return TRUE;
440 }
441
442 gboolean
pluma_spell_checker_add_word_to_session(PlumaSpellChecker * spell,const gchar * word,gssize len)443 pluma_spell_checker_add_word_to_session (PlumaSpellChecker *spell,
444 const gchar *word,
445 gssize len)
446 {
447 g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
448 g_return_val_if_fail (word != NULL, FALSE);
449
450 if (!lazy_init (spell, spell->active_lang))
451 return FALSE;
452
453 g_return_val_if_fail (spell->dict != NULL, FALSE);
454
455 if (len < 0)
456 len = strlen (word);
457
458 enchant_dict_add_to_session (spell->dict, word, len);
459
460 g_signal_emit (G_OBJECT (spell), signals[ADD_WORD_TO_SESSION], 0, word, len);
461
462 return TRUE;
463 }
464
465 gboolean
pluma_spell_checker_clear_session(PlumaSpellChecker * spell)466 pluma_spell_checker_clear_session (PlumaSpellChecker *spell)
467 {
468 g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
469
470 /* free and re-request dictionary */
471 if (spell->dict != NULL)
472 {
473 enchant_broker_free_dict (spell->broker, spell->dict);
474 spell->dict = NULL;
475 }
476
477 if (!lazy_init (spell, spell->active_lang))
478 return FALSE;
479
480 g_signal_emit (G_OBJECT (spell), signals[CLEAR_SESSION], 0);
481
482 return TRUE;
483 }
484
485 /*
486 * Informs dictionary, that word 'word' will be replaced/corrected by word
487 * 'replacement'
488 */
489 gboolean
pluma_spell_checker_set_correction(PlumaSpellChecker * spell,const gchar * word,gssize w_len,const gchar * replacement,gssize r_len)490 pluma_spell_checker_set_correction (PlumaSpellChecker *spell,
491 const gchar *word,
492 gssize w_len,
493 const gchar *replacement,
494 gssize r_len)
495 {
496 g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
497 g_return_val_if_fail (word != NULL, FALSE);
498 g_return_val_if_fail (replacement != NULL, FALSE);
499
500 if (!lazy_init (spell, spell->active_lang))
501 return FALSE;
502
503 g_return_val_if_fail (spell->dict != NULL, FALSE);
504
505 if (w_len < 0)
506 w_len = strlen (word);
507
508 if (r_len < 0)
509 r_len = strlen (replacement);
510
511 enchant_dict_store_replacement (spell->dict,
512 word,
513 w_len,
514 replacement,
515 r_len);
516
517 return TRUE;
518 }
519
520