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