]> git.neil.brown.name Git - edlib.git/blob - lib-aspell.c
TODO: clean out done items.
[edlib.git] / lib-aspell.c
1 /*
2  * Copyright Neil Brown ©2020-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * aspell: edlib interface for aspell library.
6  *
7  */
8
9 #include <aspell.h>
10 #include <wctype.h>
11 #define PANE_DATA_TYPE struct aspell_data
12 #include "core.h"
13
14 struct aspell_data {
15         AspellSpeller *speller safe;
16         bool need_save;
17 };
18 #include "core-pane.h"
19
20 static AspellConfig *spell_config;
21
22 static int trim(const char *safe *wordp safe)
23 {
24         const char *wp = *wordp;
25         const char *start;
26         int len;
27         wint_t ch;
28
29         start = wp;
30         while (*wp && (ch = get_utf8(&wp, NULL)) < WERR &&
31                !iswalpha(ch))
32                 start = wp;
33         if (!*start)
34                 return 0;
35         len = 1;
36         /* start is the first alphabetic, wp points beyond it */
37         while (*wp && (ch = get_utf8(&wp, NULL)) < WERR) {
38                 if (iswalpha(ch))
39                         len = wp - start;
40         }
41         *wordp = start;
42         return len;
43 }
44
45 static struct map *aspell_map safe;
46 DEF_LOOKUP_CMD(aspell_handle, aspell_map);
47
48 DEF_CMD(aspell_attach_helper)
49 {
50         struct aspell_data *as;
51         AspellCanHaveError *ret;
52         struct pane *p;
53
54         ret = new_aspell_speller(spell_config);
55         if (aspell_error_number(ret)) {
56                 LOG("Cannot create speller: %s", aspell_error_message(ret));
57                 return Efail;
58         }
59         p = pane_register(ci->focus, 0, &aspell_handle.c);
60         if (!p)
61                 return Efail;
62         as = p->data;
63         as->speller = safe_cast to_aspell_speller(ret);
64
65         call("doc:request:aspell:check", p);
66         call("doc:request:aspell:suggest", p);
67         call("doc:request:aspell:set-dict", p);
68         call("doc:request:aspell:add-word", p);
69         call("doc:request:aspell:save", p);
70
71         return 1;
72 }
73
74 DEF_CMD_CLOSED(aspell_close)
75 {
76         struct aspell_data *as = ci->home->data;
77
78         if (as->need_save)
79                 aspell_speller_save_all_word_lists(as->speller);
80         delete_aspell_speller(as->speller);
81         return 1;
82 }
83
84 DEF_CMD(aspell_check)
85 {
86         struct aspell_data *as = ci->home->data;
87         int correct;
88         const char *word = ci->str;
89         int len;
90
91         if (!word)
92                 return Enoarg;
93         len = trim(&word);
94         if (!len)
95                 return Efail;
96
97         correct = aspell_speller_check(as->speller, word, len);
98         return correct ? 1 : Efalse;
99 }
100
101 DEF_CMD(spell_check)
102 {
103         int rv = call("doc:notify:aspell:check", ci->focus, 0, NULL, ci->str);
104
105         if (rv != Efallthrough)
106                 return rv;
107         call_comm("doc:get-doc", ci->focus, &aspell_attach_helper);
108         return call("doc:notify:aspell:check", ci->focus, 0, NULL, ci->str);
109 }
110
111 DEF_CMD(aspell_suggest)
112 {
113         struct aspell_data *as = ci->home->data;
114         const AspellWordList *l;
115         AspellStringEnumeration *el;
116         const char *w;
117         const char *word = ci->str;
118         int len;
119
120         if (!word)
121                 return Enoarg;
122         len = trim(&word);
123         if (!len)
124                 return Efail;
125
126         l = aspell_speller_suggest(as->speller, word, len);
127         el = aspell_word_list_elements(l);
128         while ((w = aspell_string_enumeration_next(el)) != NULL)
129                 comm_call(ci->comm2, "suggest", ci->focus, 0, NULL, w);
130         delete_aspell_string_enumeration(el);
131         return 1;
132 }
133
134 DEF_CMD(spell_suggest)
135 {
136         int rv = call_comm("doc:notify:aspell:suggest", ci->focus, ci->comm2,
137                            0, NULL, ci->str);
138
139         if (rv != Efallthrough)
140                 return rv;
141         call_comm("doc:get-doc", ci->focus, &aspell_attach_helper);
142         return call_comm("doc:notify:aspell:suggest", ci->focus, ci->comm2,
143                          0, NULL, ci->str);
144 }
145
146 DEF_CMD(aspell_save)
147 {
148         struct aspell_data *as = ci->home->data;
149         if (as->need_save) {
150                 as->need_save = False;
151                 aspell_speller_save_all_word_lists(as->speller);
152         }
153         return Efalse;
154 }
155
156 DEF_CMD(aspell_do_save)
157 {
158         aspell_save_func(ci);
159         return 1;
160 }
161
162 DEF_CMD(spell_save)
163 {
164         return call_comm("doc:notify:aspell:save", ci->focus, ci->comm2,
165                          ci->num, NULL, ci->str);
166 }
167
168 DEF_CMD(aspell_add)
169 {
170         struct aspell_data *as = ci->home->data;
171         const char *word = ci->str;
172         int len;
173
174         if (!word)
175                 return Enoarg;
176         len = trim(&word);
177         if (!len)
178                 return Efail;
179
180         if (ci->num == 1) {
181                 aspell_speller_add_to_personal(as->speller, word, len);
182                 if (as->need_save)
183                         call_comm("event:free", ci->home, &aspell_save);
184                 as->need_save = True;
185                 call_comm("event:timer", ci->home, &aspell_save, 30*1000);
186         } else
187                 aspell_speller_add_to_session(as->speller, word, len);
188         call("doc:notify:spell:dict-changed", ci->home);
189         return 1;
190 }
191
192 DEF_CMD(spell_add)
193 {
194         int rv = call_comm("doc:notify:aspell:add-word", ci->focus, ci->comm2,
195                            ci->num, NULL, ci->str);
196
197         if (rv != Efallthrough)
198                 return rv;
199         call_comm("doc:get-doc", ci->focus, &aspell_attach_helper);
200         return call_comm("doc:notify:aspell:add-word", ci->focus, ci->comm2,
201                          ci->num, NULL, ci->str);
202 }
203
204 DEF_CMD(aspell_set_dict)
205 {
206         struct aspell_data *as = ci->home->data;
207         const char *lang = ci->str;
208         AspellConfig *conf2;
209         AspellCanHaveError *ret;
210
211         if (!lang)
212                 return Enoarg;
213         conf2 = aspell_config_clone(spell_config);
214         aspell_config_replace(conf2, "lang", lang);
215         ret = new_aspell_speller(conf2);
216         if (!aspell_error_number(ret)) {
217                 delete_aspell_speller(as->speller);
218                 as->speller = safe_cast to_aspell_speller(ret);
219                 call("doc:notify:spell:dict-changed", ci->focus);
220         }
221         delete_aspell_config(conf2);
222         return 1;
223 }
224
225 DEF_CMD(spell_dict)
226 {
227         int ret;
228         ret = call("doc:notify:aspell:set-dict", ci->focus, 0, NULL,
229                    ksuffix(ci, "interactive-cmd-dict-"));
230         if (ret != Efallthrough)
231                 return ret;
232         call_comm("doc:get-doc", ci->focus, &aspell_attach_helper);
233         return call("doc:notify:aspell:set-dict", ci->focus, 0, NULL,
234                     ksuffix(ci, "interactive-cmd-dict-"));
235 }
236
237 static inline bool is_word_body(wint_t ch)
238 {
239         /* alphabetics, appostrophies */
240         return iswalpha(ch) || ch == '\'';
241 }
242
243 static inline bool is_word_initial(wint_t ch)
244 {
245         /* Word must start with an alphabetic */
246         return iswalpha(ch);
247 }
248
249 static inline bool is_word_final(wint_t ch)
250 {
251         /* Word must end with an alphabetic */
252         return iswalpha(ch);
253 }
254
255 DEF_CMD(spell_this)
256 {
257         /* Find a word "here" to spell. It must include the first
258          * permitted character after '->mark'.
259          * '->mark' is moved to the end and if ->mark2 is available, it
260          * is moved to the start.
261          * If ->comm2 is available, the word is returned as a string.
262          * This command should ignore any view-specific or doc-specific rules
263          * about where words are allowed, but should honour any rules about
264          * what characters can constitute a word.
265          */
266         wint_t ch;
267         struct mark *m2;
268
269         if (!ci->mark)
270                 return Enoarg;
271         while ((ch = doc_next(ci->focus, ci->mark)) != WEOF &&
272                !is_word_initial(ch))
273                 ;
274         if (ch == WEOF)
275                 return Efalse;
276         if (ci->mark2) {
277                 m2 = ci->mark2;
278                 mark_to_mark(m2, ci->mark);
279         } else
280                 m2 = mark_dup(ci->mark);
281         while ((ch = doc_following(ci->focus, ci->mark)) != WEOF &&
282                is_word_body(ch))
283                 doc_next(ci->focus, ci->mark);
284         while ((ch = doc_prior(ci->focus, ci->mark)) != WEOF &&
285                !is_word_final(ch))
286                 doc_prev(ci->focus, ci->mark);
287
288         while ((ch = doc_prior(ci->focus, m2)) != WEOF &&
289                is_word_body(ch))
290                 doc_prev(ci->focus, m2);
291         while ((ch = doc_following(ci->focus, m2)) != WEOF &&
292                !is_word_initial(ch))
293                 doc_next(ci->focus, m2);
294         if (ci->comm2)
295                 call_comm("doc:get-str", ci->focus, ci->comm2,
296                           0, m2, NULL, 0, ci->mark);
297         if (m2 != ci->mark2)
298                 mark_free(m2);
299         return 1;
300 }
301
302 DEF_CMD(spell_next)
303 {
304         /* Find the next word-start after ->mark.
305          * A view or doc might over-ride this to skip over
306          * content that shouldn't be spell-checked.
307          */
308         wint_t ch;
309
310         if (!ci->mark)
311                 return Enoarg;
312         while ((ch = doc_next(ci->focus, ci->mark)) != WEOF &&
313                !is_word_initial(ch))
314                 ;
315         if (ch == WEOF)
316                 return Efalse;
317         doc_prev(ci->focus, ci->mark);
318         return 1;
319 }
320
321 void edlib_init(struct pane *ed safe)
322 {
323         spell_config = new_aspell_config();
324
325         aspell_config_replace(spell_config, "lang", "en_AU");
326
327         call_comm("global-set-command", ed, &spell_check,
328                   0, NULL, "Spell:Check");
329         call_comm("global-set-command", ed, &spell_suggest,
330                   0, NULL, "Spell:Suggest");
331         call_comm("global-set-command", ed, &spell_this,
332                   0, NULL, "Spell:ThisWord");
333         call_comm("global-set-command", ed, &spell_next,
334                   0, NULL, "Spell:NextWord");
335         call_comm("global-set-command", ed, &spell_add,
336                   0, NULL, "Spell:AddWord");
337         call_comm("global-set-command", ed, &spell_save,
338                   0, NULL, "Spell:Save");
339
340         call_comm("global-set-command-prefix", ed, &spell_dict,
341                   0, NULL, "interactive-cmd-dict-");
342
343         aspell_map = key_alloc();
344         key_add(aspell_map, "Close", &aspell_close);
345         key_add(aspell_map, "aspell:check", &aspell_check);
346         key_add(aspell_map, "aspell:suggest", &aspell_suggest);
347         key_add(aspell_map, "aspell:set-dict", &aspell_set_dict);
348         key_add(aspell_map, "aspell:add-word", &aspell_add);
349         key_add(aspell_map, "aspell:save", &aspell_do_save);
350 }