]> git.lyx.org Git - lyx.git/blob - src/AspellChecker.cpp
Avoid full metrics computation with Update:FitCursor
[lyx.git] / src / AspellChecker.cpp
1 /**
2  * \file AspellChecker.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Kevin Atkinson
7  * \author John Levon
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "AspellChecker.h"
15 #include "PersonalWordList.h"
16
17 #include "LyXRC.h"
18 #include "WordLangTuple.h"
19
20 #include "support/lassert.h"
21 #include "support/debug.h"
22 #include "support/lstrings.h"
23 #include "support/docstring_list.h"
24
25 #include "support/filetools.h"
26 #include "support/Package.h"
27 #include "support/FileName.h"
28 #include "support/PathChanger.h"
29
30 #include <aspell.h>
31
32 #include <map>
33 #include <string>
34
35 using namespace std;
36 using namespace lyx::support;
37
38 namespace lyx {
39
40 namespace {
41
42 struct Speller {
43         AspellConfig * config;
44         AspellCanHaveError * e_speller;
45         bool accept_compound;
46         docstring_list ignored_words_;
47 };
48
49 typedef std::map<std::string, Speller> Spellers;
50 typedef map<std::string, PersonalWordList *> LangPersonalWordList;
51
52 } // namespace
53
54 struct AspellChecker::Private
55 {
56         Private()
57         {}
58
59         ~Private();
60
61         /// add a speller of the given language and variety
62         AspellSpeller * addSpeller(Language const * lang);
63
64         ///
65         AspellSpeller * speller(Language const * lang);
66
67         bool isValidDictionary(AspellConfig * config,
68                         string const & lang, string const & variety);
69         int numDictionaries() const;
70         bool checkAspellData(AspellConfig * config,
71                 string const & basepath, string const & datapath, string const & dictpath,
72                 string const & lang, string const & variety);
73         AspellConfig * getConfig(string const & lang, string const & variety);
74
75         string toAspellWord(docstring const & word) const;
76
77         SpellChecker::Result check(AspellSpeller * m,
78                 WordLangTuple const & word) const;
79
80         void initSessionDictionary(Speller const & speller, PersonalWordList * pd);
81         void addToSession(AspellCanHaveError * speller, docstring const & word);
82         void insert(WordLangTuple const & word);
83         void remove(WordLangTuple const & word);
84         bool learned(WordLangTuple const & word);
85
86         void accept(Speller & speller, WordLangTuple const & word);
87
88         /// the spellers
89         Spellers spellers_;
90
91         LangPersonalWordList personal_;
92
93         /// the location below system/user directory
94         /// there the rws files lookup will happen
95         const string dictDirectory(void)
96         {
97                 return "dicts";
98         }
99         /// there the dat+cmap files lookup will happen
100         const string dataDirectory(void)
101         {
102                 return "data";
103         }
104         /// os package directory constants
105         /// macports on Mac OS X or
106         /// aspell rpms on Linux
107         const string osPackageBase(void)
108         {
109 #ifdef USE_MACOSX_PACKAGING
110                 return "/opt/local";
111 #else
112                 return "/usr";
113 #endif
114         }
115         const string osPackageDictDirectory(void)
116         {
117 #ifdef USE_MACOSX_PACKAGING
118                 return "/share/aspell";
119 #else
120                 return "/lib/aspell-0.60";
121 #endif
122         }
123         const string osPackageDataDirectory(void)
124         {
125                 return "/lib/aspell-0.60";
126         }
127 };
128
129
130 AspellChecker::Private::~Private()
131 {
132         Spellers::iterator it = spellers_.begin();
133         Spellers::iterator end = spellers_.end();
134
135         for (; it != end; ++it) {
136                 if (it->second.e_speller) {
137                         AspellSpeller * speller = to_aspell_speller(it->second.e_speller);
138                         aspell_speller_save_all_word_lists(speller);
139                         delete_aspell_can_have_error(it->second.e_speller);
140                 }
141                 delete_aspell_config(it->second.config);
142         }
143
144         LangPersonalWordList::const_iterator pdit = personal_.begin();
145         LangPersonalWordList::const_iterator pdet = personal_.end();
146
147         for (; pdit != pdet; ++pdit) {
148                 if (0 == pdit->second)
149                         continue;
150                 PersonalWordList * pd = pdit->second;
151                 pd->save();
152                 delete pd;
153         }
154 }
155
156
157 bool AspellChecker::Private::isValidDictionary(AspellConfig * config,
158                 string const & lang, string const & variety)
159 {
160         bool have = false;
161         // code taken from aspell's list-dicts example
162         // the returned pointer should _not_ need to be deleted
163         AspellDictInfoList * dlist = get_aspell_dict_info_list(config);
164         AspellDictInfoEnumeration * dels = aspell_dict_info_list_elements(dlist);
165         const AspellDictInfo * entry;
166
167         while (0 != (entry = aspell_dict_info_enumeration_next(dels))) {
168                 LYXERR(Debug::DEBUG, "aspell dict:"
169                         << " name="    << entry->name
170                         << ",code="    << entry->code
171                         << ",variety=" << entry->jargon);
172                 if (entry->code == lang && (variety.empty() || entry->jargon == variety)) {
173                         have = true;
174                         break;
175                 }
176         }
177         delete_aspell_dict_info_enumeration(dels);
178         LYXERR(Debug::FILES, "aspell dictionary: " << lang << (have ? " yes" : " no"));
179         return have;
180 }
181
182
183 bool AspellChecker::Private::checkAspellData(AspellConfig * config,
184         string const & basepath, string const & datapath, string const & dictpath,
185         string const & lang, string const & variety)
186 {
187         FileName base(basepath);
188         bool have_dict = base.isDirectory();
189
190         if (have_dict) {
191                 FileName data(addPath(base.absFileName(), datapath));
192                 FileName dict(addPath(base.absFileName(), dictpath));
193                 have_dict = dict.isDirectory() && data.isDirectory();
194                 if (have_dict) {
195                         LYXERR(Debug::FILES, "aspell dict-dir: " << dict);
196                         LYXERR(Debug::FILES, "aspell data-dir: " << data);
197                         aspell_config_replace(config, "dict-dir", dict.absFileName().c_str());
198                         aspell_config_replace(config, "data-dir", data.absFileName().c_str());
199                         have_dict = isValidDictionary(config, lang, variety);
200                 }
201         }
202         return have_dict;
203 }
204
205
206 AspellConfig * AspellChecker::Private::getConfig(string const & lang, string const & variety)
207 {
208         AspellConfig * config = new_aspell_config();
209         bool have_dict = false;
210         string const sysdir = lyx::support::package().system_support().absFileName();
211         string const userdir = lyx::support::package().user_support().absFileName();
212
213         LYXERR(Debug::FILES, "aspell user dir: " << userdir);
214         have_dict = checkAspellData(config, userdir, dataDirectory(), dictDirectory(), lang, variety);
215         if (!have_dict) {
216                 LYXERR(Debug::FILES, "aspell sysdir dir: " << sysdir);
217                 have_dict = checkAspellData(config, sysdir, dataDirectory(), dictDirectory(), lang, variety);
218         }
219         if (!have_dict) {
220                 // check for package data of OS installation
221                 checkAspellData(config, osPackageBase(), osPackageDataDirectory(), osPackageDictDirectory(), lang, variety);
222         }
223         return config;
224 }
225
226
227 void AspellChecker::Private::addToSession(AspellCanHaveError * speller, docstring const & word)
228 {
229         string const word_to_add = toAspellWord(word);
230         if (1 != aspell_speller_add_to_session(to_aspell_speller(speller), word_to_add.c_str(), -1))
231                 LYXERR(Debug::GUI, "aspell add to session: " << aspell_error_message(speller));
232 }
233
234
235 void AspellChecker::Private::initSessionDictionary(
236         Speller const & speller,
237         PersonalWordList * pd)
238 {
239         AspellSpeller * aspell = to_aspell_speller(speller.e_speller);
240         aspell_speller_clear_session(aspell);
241         docstring_list::const_iterator it = pd->begin();
242         docstring_list::const_iterator et = pd->end();
243         for (; it != et; ++it) {
244                 addToSession(speller.e_speller, *it);
245         }
246         it = speller.ignored_words_.begin();
247         et = speller.ignored_words_.end();
248         for (; it != et; ++it) {
249                 addToSession(speller.e_speller, *it);
250         }
251 }
252
253
254 AspellSpeller * AspellChecker::Private::addSpeller(Language const * lang)
255 {
256         Speller m;
257         string const code = lang->code();
258         string const variety = lang->variety();
259         m.config = getConfig(code, variety);
260         // Aspell supports both languages and varieties (such as German
261         // old vs. new spelling). The respective naming convention is
262         // lang_REGION-variety (e.g. de_DE-alt).
263         aspell_config_replace(m.config, "lang", code.c_str());
264         if (!variety.empty())
265                 aspell_config_replace(m.config, "variety", variety.c_str());
266         // Set the encoding to utf-8.
267         // aspell does also understand "ucs-4", so we would not need a
268         // conversion in theory, but if this is used it expects all
269         // char const * arguments to be a cast from  uint const *, and it
270         // seems that this uint is not compatible with our char_type on some
271         // platforms (cygwin, OS X). Therefore we use utf-8, that does
272         // always work.
273         aspell_config_replace(m.config, "encoding", "utf-8");
274         if (lyxrc.spellchecker_accept_compound)
275                 // Consider run-together words as legal compounds
276                 aspell_config_replace(m.config, "run-together", "true");
277         else
278                 // Report run-together words as errors
279                 aspell_config_replace(m.config, "run-together", "false");
280
281         m.accept_compound = lyxrc.spellchecker_accept_compound;
282         m.e_speller = new_aspell_speller(m.config);
283         if (aspell_error_number(m.e_speller) != 0) {
284                 // FIXME: We should indicate somehow that this language is not supported.
285                 LYXERR(Debug::FILES, "aspell error: " << aspell_error_message(m.e_speller));
286                 delete_aspell_can_have_error(m.e_speller);
287                 delete_aspell_config(m.config);
288                 m.config = 0;
289                 m.e_speller = 0;
290         } else {
291                 PersonalWordList * pd = new PersonalWordList(lang->lang());
292                 pd->load();
293                 personal_[lang->lang()] = pd;
294                 initSessionDictionary(m, pd);
295         }
296
297         spellers_[lang->lang()] = m;
298         return m.e_speller ? to_aspell_speller(m.e_speller) : 0;
299 }
300
301
302 AspellSpeller * AspellChecker::Private::speller(Language const * lang)
303 {
304         Spellers::iterator it = spellers_.find(lang->lang());
305         if (it != spellers_.end()) {
306                 Speller aspell = it->second;
307                 if (lyxrc.spellchecker_accept_compound != aspell.accept_compound) {
308                         // spell checker setting changed... adjust run-together
309                         aspell.accept_compound = lyxrc.spellchecker_accept_compound;
310                         if (aspell.accept_compound)
311                                 // Consider run-together words as legal compounds
312                                 aspell_config_replace(aspell.config, "run-together", "true");
313                         else
314                                 // Report run-together words as errors
315                                 aspell_config_replace(aspell.config, "run-together", "false");
316                         AspellCanHaveError * e_speller = aspell.e_speller;
317                         aspell.e_speller = new_aspell_speller(aspell.config);
318                         delete_aspell_speller(to_aspell_speller(e_speller));
319                         spellers_[lang->lang()] = aspell;
320                 }
321                 return to_aspell_speller(aspell.e_speller);
322         }
323
324         return addSpeller(lang);
325 }
326
327
328 int AspellChecker::Private::numDictionaries() const
329 {
330         int result = 0;
331         Spellers::const_iterator it = spellers_.begin();
332         Spellers::const_iterator et = spellers_.end();
333
334         for (; it != et; ++it) {
335                 Speller aspell = it->second;
336                 result += aspell.e_speller != 0;
337         }
338         return result;
339 }
340
341
342 string AspellChecker::Private::toAspellWord(docstring const & word) const
343 {
344         size_t mpos;
345         string word_str = to_utf8(word);
346         while ((mpos = word_str.find('-')) != word_str.npos) {
347                 word_str.erase(mpos, 1);
348         }
349         return word_str;
350 }
351
352
353 SpellChecker::Result AspellChecker::Private::check(
354         AspellSpeller * m, WordLangTuple const & word)
355         const
356 {
357         SpellChecker::Result result = WORD_OK;
358         docstring w1;
359         LYXERR(Debug::GUI, "spellCheck: \"" <<
360                    word.word() << "\", lang = " << word.lang()->lang()) ;
361         docstring rest = split(word.word(), w1, '-');
362         for (; result == WORD_OK;) {
363                 string const word_str = toAspellWord(w1);
364                 int const word_ok = aspell_speller_check(m, word_str.c_str(), -1);
365                 LASSERT(word_ok != -1, return UNKNOWN_WORD);
366                 result = (word_ok) ? WORD_OK : UNKNOWN_WORD;
367                 if (rest.empty())
368                         break;
369                 rest = split(rest, w1, '-');
370         }
371         if (result == WORD_OK)
372                 return result;
373         string const word_str = toAspellWord(word.word());
374         int const word_ok = aspell_speller_check(m, word_str.c_str(), -1);
375         LASSERT(word_ok != -1, return UNKNOWN_WORD);
376         return (word_ok) ? WORD_OK : UNKNOWN_WORD;
377 }
378
379 void AspellChecker::Private::accept(Speller & speller, WordLangTuple const & word)
380 {
381         speller.ignored_words_.push_back(word.word());
382 }
383
384
385 /// personal word list interface
386 void AspellChecker::Private::remove(WordLangTuple const & word)
387 {
388         PersonalWordList * pd = personal_[word.lang()->lang()];
389         if (!pd)
390                 return;
391         pd->remove(word.word());
392         Spellers::iterator it = spellers_.find(word.lang()->lang());
393         if (it != spellers_.end()) {
394                 initSessionDictionary(it->second, pd);
395         }
396 }
397
398
399 void AspellChecker::Private::insert(WordLangTuple const & word)
400 {
401         Spellers::iterator it = spellers_.find(word.lang()->lang());
402         if (it != spellers_.end()) {
403                 addToSession(it->second.e_speller, word.word());
404                 PersonalWordList * pd = personal_[word.lang()->lang()];
405                 if (!pd)
406                         return;
407                 pd->insert(word.word());
408         }
409 }
410
411 bool AspellChecker::Private::learned(WordLangTuple const & word)
412 {
413         PersonalWordList * pd = personal_[word.lang()->lang()];
414         if (!pd)
415                 return false;
416         return pd->exists(word.word());
417 }
418
419
420 AspellChecker::AspellChecker()
421         : d(new Private)
422 {}
423
424
425 AspellChecker::~AspellChecker()
426 {
427         delete d;
428 }
429
430
431 SpellChecker::Result AspellChecker::check(WordLangTuple const & word,
432                                           vector<WordLangTuple> const & docdict)
433 {
434         AspellSpeller * m = d->speller(word.lang());
435
436         if (!m)
437                 return NO_DICTIONARY;
438
439         if (word.word().empty())
440                 // MSVC compiled Aspell doesn't like it.
441                 return WORD_OK;
442
443         vector<WordLangTuple>::const_iterator it = docdict.begin();
444         for (; it != docdict.end(); ++it) {
445                 if (it->lang()->code() != word.lang()->code())
446                         continue;
447                 if (it->word() == word.word())
448                         return DOCUMENT_LEARNED_WORD;
449         }
450         SpellChecker::Result rc = d->check(m, word);
451         return (rc == WORD_OK && d->learned(word)) ? LEARNED_WORD : rc;
452 }
453
454
455 void AspellChecker::advanceChangeNumber()
456 {
457         nextChangeNumber();
458 }
459
460
461 void AspellChecker::insert(WordLangTuple const & word)
462 {
463         d->insert(word);
464         advanceChangeNumber();
465 }
466
467
468 void AspellChecker::accept(WordLangTuple const & word)
469 {
470         Spellers::iterator it = d->spellers_.find(word.lang()->lang());
471         if (it != d->spellers_.end()) {
472                 d->addToSession(it->second.e_speller, word.word());
473                 d->accept(it->second, word);
474                 advanceChangeNumber();
475         }
476 }
477
478
479 void AspellChecker::suggest(WordLangTuple const & wl,
480         docstring_list & suggestions)
481 {
482         suggestions.clear();
483         AspellSpeller * m = d->speller(wl.lang());
484
485         if (!m)
486                 return;
487
488         string const word = d->toAspellWord(wl.word());
489         AspellWordList const * sugs =
490                 aspell_speller_suggest(m, word.c_str(), -1);
491         LASSERT(sugs != 0, return);
492         AspellStringEnumeration * els = aspell_word_list_elements(sugs);
493         if (!els || aspell_word_list_empty(sugs))
494                 return;
495
496         for (;;) {
497                 char const * str = aspell_string_enumeration_next(els);
498                 if (!str)
499                         break;
500                 suggestions.push_back(from_utf8(str));
501         }
502
503         delete_aspell_string_enumeration(els);
504 }
505
506
507 void AspellChecker::remove(WordLangTuple const & word)
508 {
509         d->remove(word);
510         advanceChangeNumber();
511 }
512
513
514 bool AspellChecker::hasDictionary(Language const * lang) const
515 {
516         bool have = false;
517         Spellers::iterator it = d->spellers_.begin();
518         Spellers::iterator end = d->spellers_.end();
519
520         if (lang) {
521                 for (; it != end && !have; ++it) {
522                         have = it->second.config && d->isValidDictionary(it->second.config, lang->code(), lang->variety());
523                 }
524                 if (!have) {
525                         AspellConfig * config = d->getConfig(lang->code(), lang->variety());
526                         have = d->isValidDictionary(config, lang->code(), lang->variety());
527                         delete_aspell_config(config);
528                 }
529         }
530         return have;
531 }
532
533
534 int AspellChecker::numDictionaries() const
535 {
536         return d->numDictionaries();
537 }
538
539
540 docstring const AspellChecker::error()
541 {
542         Spellers::iterator it = d->spellers_.begin();
543         Spellers::iterator end = d->spellers_.end();
544         char const * err = 0;
545
546         for (; it != end && 0 == err; ++it) {
547                 if (it->second.e_speller && aspell_error_number(it->second.e_speller) != 0)
548                         err = aspell_error_message(it->second.e_speller);
549         }
550
551         // FIXME UNICODE: err is not in UTF8, but probably the locale encoding
552         return (err ? from_utf8(err) : docstring());
553 }
554
555
556 } // namespace lyx