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