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