]> git.lyx.org Git - lyx.git/blob - src/ISpell.cpp
28358c828b4bbddeef0bb8af519d2c499dc90e26
[lyx.git] / src / ISpell.cpp
1 /**
2  * \file ISpell.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author unknown
7  * \author Angus Leeming
8  * \author John Levon
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "ISpell.h"
16
17 #include "BufferParams.h"
18 #include "Encoding.h"
19 #include "Language.h"
20 #include "LyXRC.h"
21 #include "WordLangTuple.h"
22
23 #include "support/debug.h"
24 #include "support/gettext.h"
25 #include "support/ForkedCalls.h"
26 #include "support/lstrings.h"
27 #include "support/unicode.h"
28
29 // HP-UX 11.x doesn't have this header
30 #ifdef HAVE_SYS_SELECT_H
31 # include <sys/select.h>
32 #endif
33 #ifdef HAVE_SYS_TIME_H
34 # include <sys/time.h>
35 #endif
36
37 using boost::shared_ptr;
38
39 using namespace std;
40
41 namespace lyx {
42
43 using support::bformat;
44
45 namespace {
46
47 class LaunchIspell : public support::ForkedProcess {
48         typedef support::ForkedProcess ForkedProcess;
49 public:
50         ///
51         LaunchIspell(BufferParams const & p, string const & l,
52                      int * in, int * out, int * err)
53                 : params(p), lang(l), pipein(in), pipeout(out), pipeerr(err) {}
54         ///
55         virtual shared_ptr<ForkedProcess> clone() const {
56                 return shared_ptr<ForkedProcess>(new LaunchIspell(*this));
57         }
58         ///
59         int start();
60 private:
61         ///
62         virtual int generateChild();
63
64         ///
65         BufferParams const & params;
66         string const & lang;
67         int * const pipein;
68         int * const pipeout;
69         int * const pipeerr;
70 };
71
72
73 int LaunchIspell::start()
74 {
75         command_ = lyxrc.isp_command;
76         return run(DontWait);
77 }
78
79
80 int LaunchIspell::generateChild()
81 {
82         pid_t isp_pid = fork();
83
84         if (isp_pid != 0) {
85                 // failed (-1) or parent process (>0)
86                 return isp_pid;
87         }
88
89         // child process
90         dup2(pipein[0], STDIN_FILENO);
91         dup2(pipeout[1], STDOUT_FILENO);
92         dup2(pipeerr[1], STDERR_FILENO);
93         close(pipein[0]);
94         close(pipein[1]);
95         close(pipeout[0]);
96         close(pipeout[1]);
97         close(pipeerr[0]);
98         close(pipeerr[1]);
99
100         char * argv[14];
101         int argc = 0;
102
103         char * tmp = new char[lyxrc.isp_command.length() + 1];
104         lyxrc.isp_command.copy(tmp, lyxrc.isp_command.length());
105         tmp[lyxrc.isp_command.length()] = '\0';
106         argv[argc++] = tmp;
107         tmp = new char[3];
108         string("-a").copy(tmp, 2); tmp[2] = '\0'; // pipe mode
109         argv[argc++] = tmp;
110
111         if (lang != "default") {
112                 tmp = new char[3];
113                 string("-d").copy(tmp, 2); tmp[2] = '\0'; // Dictionary file
114                 argv[argc++] = tmp;
115                 tmp = new char[lang.length() + 1];
116                 lang.copy(tmp, lang.length()); tmp[lang.length()] = '\0';
117                 argv[argc++] = tmp;
118         }
119
120         if (lyxrc.isp_accept_compound) {
121                 // Consider run-together words as legal compounds
122                 tmp = new char[3];
123                 string("-C").copy(tmp, 2); tmp[2] = '\0';
124                 argv[argc++] = tmp;
125         } else {
126                 // Report run-together words with
127                 // missing blanks as errors
128                 tmp = new char[3];
129                 string("-B").copy(tmp, 2); tmp[2] = '\0';
130                 argv[argc++] = tmp;
131         }
132         if (lyxrc.isp_use_esc_chars) {
133                 // Specify additional characters that
134                 // can be part of a word
135                 tmp = new char[3];
136                 string("-w").copy(tmp, 2); tmp[2] = '\0';
137                 argv[argc++] = tmp;
138                 // Put the escape chars in ""s
139                 string tms = '"' + lyxrc.isp_esc_chars + '"';
140                 tmp = new char[tms.length() + 1];
141                 tms.copy(tmp, tms.length()); tmp[tms.length()] = '\0';
142                 argv[argc++] = tmp;
143         }
144         if (lyxrc.isp_use_pers_dict) {
145                 // Specify an alternate personal dictionary
146                 tmp = new char[3];
147                 string("-p").copy(tmp, 2);
148                 tmp[2]= '\0';
149                 argv[argc++] = tmp;
150                 tmp = new char[lyxrc.isp_pers_dict.length() + 1];
151                 lyxrc.isp_pers_dict.copy(tmp, lyxrc.isp_pers_dict.length());
152                 tmp[lyxrc.isp_pers_dict.length()] = '\0';
153                 argv[argc++] = tmp;
154         }
155         if (lyxrc.isp_use_input_encoding &&
156             params.inputenc != "default") {
157                 string enc = (params.inputenc == "auto")
158                         ? params.language->encoding()->latexName()
159                         : params.inputenc;
160                 size_t const n = enc.length();
161                 tmp = new char[3];
162                 string("-T").copy(tmp, 2);
163                 tmp[2] = '\0';
164                 argv[argc++] = tmp; // Input encoding
165                 tmp = new char[n + 1];
166                 enc.copy(tmp, n);
167                 tmp[n] = '\0';
168                 argv[argc++] = tmp;
169         }
170
171         argv[argc++] = 0;
172
173         execvp(argv[0], const_cast<char * const *>(argv));
174
175         // free the memory used by string::copy in the
176         // setup of argv
177         for (int i = 0; i < argc - 1; ++i)
178                 delete[] argv[i];
179
180         lyxerr << "LyX: Failed to start ispell!" << endl;
181         _exit(0);
182 }
183
184
185 string const to_iconv_encoding(docstring const & s, string const & encoding)
186 {
187         if (lyxrc.isp_use_input_encoding) {
188                 std::vector<char> const encoded =
189                         ucs4_to_eightbit(s.data(), s.length(), encoding);
190                 return string(encoded.begin(), encoded.end());
191         }
192         // FIXME UNICODE: we don't need to convert to UTF8, but probably to the locale encoding
193         return to_utf8(s);
194 }
195
196
197 docstring const from_iconv_encoding(string const & s, string const & encoding)
198 {
199         if (lyxrc.isp_use_input_encoding) {
200                 std::vector<char_type> const ucs4 =
201                         eightbit_to_ucs4(s.data(), s.length(), encoding);
202                 return docstring(ucs4.begin(), ucs4.end());
203         }
204         // FIXME UNICODE: s is not in UTF8, but probably the locale encoding
205         return from_utf8(s);
206 }
207
208 } // namespace anon
209
210
211 ISpell::ISpell(BufferParams const & params, string const & lang)
212         : in(0), out(0), inerr(0), str(0)
213 {
214         //LYXERR(Debug::GUI, "Created ispell");
215
216         encoding = params.encoding().iconvName();
217
218         // static due to the setvbuf. Ugly.
219         static char o_buf[BUFSIZ];
220
221         // We need to throw an exception not do this
222         pipein[0] = pipein[1] = pipeout[0] = pipeout[1]
223                 = pipeerr[0] = pipeerr[1] = -1;
224
225         // This is what happens when goto gets banned.
226
227         if (pipe(pipein) == -1) {
228                 error_ = _("Can't create pipe for spellchecker.");
229                 return;
230         }
231
232         if (pipe(pipeout) == -1) {
233                 close(pipein[0]);
234                 close(pipein[1]);
235                 error_ = _("Can't create pipe for spellchecker.");
236                 return;
237         }
238
239         if (pipe(pipeerr) == -1) {
240                 close(pipein[0]);
241                 close(pipein[1]);
242                 close(pipeout[0]);
243                 close(pipeout[1]);
244                 error_ = _("Can't create pipe for spellchecker.");
245                 return;
246         }
247
248         if ((out = fdopen(pipein[1], "w")) == 0) {
249                 error_ = _("Can't open pipe for spellchecker.");
250                 return;
251         }
252
253         if ((in = fdopen(pipeout[0], "r")) == 0) {
254                 error_ = _("Can't open pipe for spellchecker.");
255                 return;
256         }
257
258         if ((inerr = fdopen(pipeerr[0], "r")) == 0) {
259                 error_ = _("Can't open pipe for spellchecker.");
260                 return;
261         }
262
263         setvbuf(out, o_buf, _IOLBF, BUFSIZ);
264
265         LaunchIspell * li = new LaunchIspell(params, lang, pipein, pipeout, pipeerr);
266         child_.reset(li);
267         if (li->start() == -1) {
268                 error_ = _("Could not create an ispell process.\nYou may not have "
269                                         "the right languages installed.");
270                 child_.reset(0);
271                 return;
272         }
273
274         // Parent process: Read ispells identification message
275
276         bool err_read;
277         bool error = select(err_read);
278
279         if (!error) {
280                 if (!err_read) {
281                         // Set terse mode (silently accept correct words)
282                         fputs("!\n", out);
283                         return;
284                 }
285
286                 // must have read something from stderr
287                 // FIXME UNICODE: buf is not in UTF8, but probably the locale encoding
288                 error_ = from_utf8(buf);
289         } else {
290                 // select returned error
291                 error_ = _("The ispell process returned an error.\nPerhaps "
292                            "it has been configured wrongly ?");
293         }
294
295         close(pipein[0]);
296         close(pipein[1]);
297         close(pipeout[0]);
298         close(pipeout[1]);
299         close(pipeerr[0]);
300         close(pipeerr[1]);
301         child_->kill();
302         child_.reset(0);
303 }
304
305
306 ISpell::~ISpell()
307 {
308         //LYXERR(Debug::GUI, "Killing ispell");
309
310         if (in)
311                 fclose(in);
312
313         if (inerr)
314                 fclose(inerr);
315
316         if (out) {
317                 fputs("#\n", out); // Save personal dictionary
318
319                 fflush(out);
320                 fclose(out);
321         }
322
323         close(pipein[0]);
324         close(pipein[1]);
325         close(pipeout[0]);
326         close(pipeout[1]);
327         close(pipeerr[0]);
328         close(pipeerr[1]);
329         delete [] str;
330 }
331
332
333 bool ISpell::select(bool & err_read)
334 {
335         fd_set infds;
336         struct timeval tv;
337         int retval = 0;
338         FD_ZERO(&infds);
339         FD_SET(pipeout[0], &infds);
340         FD_SET(pipeerr[0], &infds);
341         tv.tv_sec = 2;
342         tv.tv_usec = 0;
343
344         retval = ::select(SELECT_TYPE_ARG1 (max(pipeout[0], pipeerr[0]) + 1),
345                         SELECT_TYPE_ARG234 (&infds),
346                         0,
347                         0,
348                         SELECT_TYPE_ARG5 (&tv));
349
350         // error
351         if (retval <= 0)
352                 return true;
353
354         if (FD_ISSET(pipeerr[0], &infds)) {
355                 fgets(buf, BUFSIZ, inerr);
356                 err_read = true;
357                 return false;
358         }
359
360         fgets(buf, BUFSIZ, in);
361         err_read = false;
362         return false;
363 }
364
365
366 docstring const ISpell::nextMiss()
367 {
368         // Well, somebody is a sick fuck.
369
370         if (str == 0 || *(e+1) == '\0')
371                 return docstring();
372         char * b = e + 2;
373         e = strpbrk(b, ",\n");
374         *e = '\0';
375         if (b)
376                 return from_iconv_encoding(b, encoding);
377         return docstring();
378 }
379
380
381 bool ISpell::alive()
382 {
383         return child_.get() && child_->running();
384 }
385
386
387 enum ISpell::Result ISpell::check(WordLangTuple const & word)
388 {
389         // FIXME Please rewrite to use string.
390
391         Result res;
392
393         string const encoded = to_iconv_encoding(word.word(), encoding);
394         if (encoded.empty()) {
395                 error_ = bformat(
396                         _("Could not check word `%1$s' because it could not be converted to encoding `%2$s'."),
397                         word.word(), from_ascii(encoding));
398                 return UNKNOWN_WORD;
399         }
400         ::fputs(encoded.c_str(), out);
401         ::fputc('\n', out);
402
403         bool err_read;
404         bool error = select(err_read);
405
406         if (error) {
407                 error_ = _("Could not communicate with the ispell spellchecker process.");
408                 return UNKNOWN_WORD;
409         }
410
411         if (err_read) {
412                 // FIXME UNICODE: buf is not in UTF8, but probably the locale encoding
413                 error_ = from_utf8(buf);
414                 return UNKNOWN_WORD;
415         }
416
417         // I think we have to check if ispell is still alive here because
418         // the signal-handler could have disabled blocking on the fd
419         if (!alive())
420                 return UNKNOWN_WORD;
421
422         switch (*buf) {
423         case '*':
424                 res = OK;
425                 break;
426         case '+':
427                 res = ROOT;
428                 break;
429         case '-':
430                 res = COMPOUND_WORD;
431                 break;
432         case '\n':
433                 res = IGNORED_WORD;
434                 break;
435         case '#': // Not found, no near misses and guesses
436                 res = UNKNOWN_WORD;
437                 break;
438         case '?': // Not found, and no near misses, but guesses (guesses are ignored)
439         case '&': // Not found, but we have near misses
440         {
441                 res = SUGGESTED_WORDS;
442                 char * p = strpbrk(buf, ":");
443                 str = new char[strlen(p) + 1];
444                 e   = str;
445                 strcpy(str, p);
446                 break;
447         }
448         default: // This shouldn't happen, but you know Murphy
449                 res = UNKNOWN_WORD;
450         }
451
452         *buf = 0;
453         if (res != IGNORED_WORD) {
454                 /* wait for ispell to finish */
455                 while (*buf!= '\n')
456                         fgets(buf, 255, in);
457         }
458         return res;
459 }
460
461
462 void ISpell::insert(WordLangTuple const & word)
463 {
464         string const encoded = to_iconv_encoding(word.word(), encoding);
465         if (encoded.empty()) {
466                 error_ = bformat(
467                         _("Could not insert word `%1$s' because it could not be converted to encoding `%2$s'."),
468                         word.word(), from_ascii(encoding));
469                 return;
470         }
471         ::fputc('*', out); // Insert word in personal dictionary
472         ::fputs(encoded.c_str(), out);
473         ::fputc('\n', out);
474 }
475
476
477 void ISpell::accept(WordLangTuple const & word)
478 {
479         string const encoded = to_iconv_encoding(word.word(), encoding);
480         if (encoded.empty()) {
481                 error_ = bformat(
482                         _("Could not accept word `%1$s' because it could not be converted to encoding `%2$s'."),
483                         word.word(), from_ascii(encoding));
484                 return;
485         }
486         ::fputc('@', out); // Accept in this session
487         ::fputs(encoded.c_str(), out);
488         ::fputc('\n', out);
489 }
490
491
492 docstring const ISpell::error()
493 {
494         return error_;
495 }
496
497
498 } // namespace lyx