summaryrefslogtreecommitdiff
path: root/speller.cc
blob: f1b4c6b3d08d95c9d30699b28c13de9e6230b717 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
#include <unistd.h>	// exec, pipe, fork...
#include <algorithm>	// std::sort

#include "speller.h"
#include "mk_wcwidth.h"
#include "converters.h"
#include "editor.h"
#include "dialogline.h"
#include "dbg.h"

// A Correction class encapsulates an incorrect word, its position
// in the text, and a list of seggested corrections.

class Correction {

    bool valid;

public:

    Correction(const char *s, int aLine);
    bool is_valid() { return valid; }

    unistring incorrect;
    // we also save a version of the word represented
    // in the speller encoding, so we don't have to convert
    // the word back later.
    cstring   incorrect_original;
    std::vector<unistring> suggestions;

    // The position of the incorrect word: line number and offset within.

    int line;
    int offset;

    // hspell sometimes returns spelling-hints (short textual explanation
    // of why the word is incorrect).

    unistring hint;
    
    void add_hint(const unistring &s) {
	if (!hint.empty())
	    hint.push_back('\n');
	hint.append(s);
    }
};

// A Corrections class holds a list of Correction objects pertaining
// to one paragraph of text.

class Corrections {

    std::vector<Correction *> array;

    // A function object to sort the Correction objects by their offset
    // within the paragraph.
    struct cmp_corrections {
	bool operator() (const Correction *a, const Correction *b) const {
	    return a->offset < b->offset;
	}
    };

public:

    Corrections() {}
    ~Corrections();

    void clear();
    void add(Correction *crctn);
    bool empty() const	{ return array.empty(); }
    int size() const	{ return (int)array.size(); }
    Correction *operator[] (int i)
			{ return array[i]; }

    // The speller (e.g. hspell) may not report incorrect words in
    // the order in which they appear in the paragraph. This is because
    // hspell delegates the work to [ia]spell after it finishes reporting
    // the incorrect Hebrew words. However, since we want to present
    // the user the words in the right order, we have to sort them first.

    void sort() {
	std::sort(array.begin(), array.end(), cmp_corrections());
    }
};

Corrections::~Corrections()
{
    clear();
}

void Corrections::clear()
{
    for (int i = 0; i < size(); i++)
	delete array[i];
    array.clear();
}

void Corrections::add(Correction *crctn)
{
    array.push_back(crctn);
}

// A Correction constructor parses an ispell-a line.
// 
// The detailed description of the ispell-a protocol can be found
// in the ispell man page. In short, when the speller finds an incorrect
// word and has some spell suggestions, it returns:
//
// [&?] incorrect-word count offset: word, word, word, word
//
// When it has no suggestions, it returns:
// 
// # <<incorrect>> <<offset>>
//
// If the protocol-line does not conform to the above syntaxes, we
// ignore it and mark the object as invalid.

Correction::Correction(const char *s, int aLine)
{
    if (*s != '&' && *s != '?' && *s != '#') {
	valid = false;
	return;
    }
    valid  = true;
    line   = aLine;
    offset = -1;

    bool has_suggestions = (*s != '#');
    
    const char *pos, *start;
    start = pos = s + 2;
    while (*pos != ' ')
	pos++;
    incorrect.init_from_utf8(start, pos);

    offset = strtol(pos, (char **)&pos, 10);
    if (has_suggestions)
	offset = strtol(pos, (char **)&pos, 10);
    // we sent the speller lines prefixed with "^", so we need
    // to decrease by one.
    offset--;

    // the following post[1,2] tests are needed because
    // hspell returns "?" instead of "#" when there are
    // no suggestions.
    if (has_suggestions && pos[1] && pos[2]) {
	unistring word;
	do {
	    start = pos += 2;
	    while (*pos && *pos != ',')
		pos++;
	    word.init_from_utf8(start, pos);
	    suggestions.push_back(word);
	} while (*pos);
    }
}

//////////////////////////// SpellerWnd //////////////////////////////////

SpellerWnd::SpellerWnd(Editor &aApp) :
    app(aApp)
{
    create_window();
    label.highlight();
    label.set_text(_("Speller Results"));
    // The following are the keys the user presses to select
    // a spelling suggestion. These can be modified using gettext's
    // message catalogs.
    word_keys.init_from_utf8(
	_("1234567890:;<=>@bcdefhijklmnopqstuvwxyz[\\]^_`"
	  "BCDEFHIJKLMNOPQSTUVWXYZ{|}~"));
}

void SpellerWnd::resize(int lines, int columns, int y, int x)
{
    Widget::resize(lines, columns, y, x);
    label.resize(1, columns, y, x);
    editbox.resize(lines - 1, columns, y + 1, x);
}

void SpellerWnd::update()
{
    label.update();
    editbox.update();
}

bool SpellerWnd::is_dirty() const
{
    return label.is_dirty() || editbox.is_dirty();
}

void SpellerWnd::invalidate_view()
{
    label.invalidate_view();
    editbox.invalidate_view();
}

INTERACTIVE void SpellerWnd::layout_windows()
{
    app.layout_windows();
}

INTERACTIVE void SpellerWnd::refresh()
{
    app.refresh();
}

void SpellerWnd::clear()
{
    editbox.new_document();
}

void SpellerWnd::append(const unistring &us)
{
    editbox.insert_text(us);
}

void SpellerWnd::append(const char *s)
{
    unistring us;
    us.init_from_utf8(s);
    editbox.insert_text(us);
}

void SpellerWnd::end_menu(MenuResult result)
{
    menu_result = result;
    finished = true;
}

INTERACTIVE void SpellerWnd::ignore_word()
{
    end_menu(splIgnore);
}

INTERACTIVE void SpellerWnd::add_to_dict()
{
    end_menu(splAdd);
}

INTERACTIVE void SpellerWnd::edit_replacement()
{
    end_menu(splEdit);
}

INTERACTIVE void SpellerWnd::abort_spelling()
{
    end_menu(splAbort);
}

INTERACTIVE void SpellerWnd::abort_spelling_restore_cursor()
{
    end_menu(splAbortRestoreCursor);
}

INTERACTIVE void SpellerWnd::set_global_decision()
{
    global_decision = true;
    editbox.set_read_only(false);
    editbox.move_beginning_of_buffer();
    append(_("--GLOBAL DECISION--\n"));
    editbox.set_read_only(true);
}

// handle_event() - 
//
// A typical SpellerWnd window displays:
// 
// (1) begging (2) begin (3) begun (4) bagging (5) beguine
//
// In brackets are the keys the user presses to choose a
// spelling suggestion. We handle these keys here. 

bool SpellerWnd::handle_event(const Event &evt)
{
    if (Widget::handle_event(evt))
	return true;
    if (evt.is_literal()) {
	int idx = word_keys.index(evt.ch);
	if (idx != -1 && idx < (int)correction->suggestions.size()) {
	    suggestion_choice = idx;
	    end_menu(splChoice);
	}
	return true;
    }
    return editbox.handle_event(evt);
}

// exec_correction_menu() - Setup the SpellerWnd contents and then
// execute a modal menu (using an event loop). It returns the user's
// action.

MenuResult SpellerWnd::exec_correction_menu(Correction &crctn)
{
    // we save the Correction object in a member variable because
    // other methods (e.g. handle_event) use it.
    correction = &crctn;

    u8string title;
    title.cformat(_("Suggestions for '%s'"),
		     u8string(correction->incorrect).c_str());
    label.set_text(title.c_str());

    editbox.set_read_only(false);
    clear();
    for (int i = 0; i < (int)correction->suggestions.size()
			&& i < word_keys.len(); i++)
    {
	u8string utf8_word(correction->suggestions[i]);
	u8string utf8_key(word_keys.substr(i, 1));
	u8string word_tmplt;
	if (i != 0)
	    append("\xC2\xA0 "); // UNI_NO_BREAK_SPACE
	word_tmplt.cformat(_("(%s)\xC2\xA0%s"),
			     utf8_key.c_str(), utf8_word.c_str()); 
	append(word_tmplt.c_str());
    }
    if (correction->suggestions.empty())
	append(_("No suggestions for this word."));
    append("\n\n");
    if (!correction->hint.empty()) {
	append(correction->hint);
	append("\n\n");
    }
    append(_("[SPC to leave unchanged, 'a' to add to private dictionary, "
	     "'r' to edit word, 'q' to exit and restore cursor, ^C to "
	     "exit and leave cursor, or one of the above characters "
	     "to replace.  'g' to make your decision global.]"));
    editbox.set_read_only(true);
    editbox.move_beginning_of_buffer();
	    
    global_decision = false;
    finished = false;
    while (!finished) {
	Event evt;
	app.update_terminal();
	get_next_event(evt, editbox.wnd);
	handle_event(evt);
    }
    return menu_result;
}

///////////////////////////// Speller ////////////////////////////////////

#define SPELER_REPLACE_HISTORY	110

// the following UNLOAD_SPELLER routine is a temporary hack to
// a pipe problem (see TODO).
static Speller *global_speller_instance = NULL;
void UNLOAD_SPELLER()
{
    if (global_speller_instance)
	global_speller_instance->unload();
}

// replace_table is a hash-table that matches any incorrect word
// with its correct spelling. It is used to implement the "Replace
// All" function. Also, when the value of the key is the empty
// string, it means to ignore the word (that's how "Ignore All" is
// implemented).

std::map<unistring, unistring> replace_table;

Speller::Speller(Editor &aApp, DialogLine &aDialog) :
    app(aApp),
    dialog(aDialog)
{
    loaded = false;
    global_speller_instance = this;
}

// load() - loads the speller. it forks and execs the speller. it setups
// pipes for communication.
//
// Warning: the code is not foolproof! it expects the child process to
// print an identity string. if the child prints nothing, this function
// hangs!

bool Speller::load(const char *cmd, const char *encoding)
{
    if (is_loaded())
	return true;

    conv_to_speller =
	    ConverterFactory::get_converter_to(encoding);
    conv_from_speller =
	    ConverterFactory::get_converter_from(encoding);
    if (!conv_to_speller || !conv_from_speller) {
	dialog.show_message_fmt(_("Can't find converter '%s'"), encoding);
	return false;
    }
    conv_to_speller->enable_ilseq_repr();

    dialog.show_message(_("Loading speller..."));
    dialog.immediate_update();

    if (pipe(fd_to_spl) < 0 || pipe(fd_from_spl) < 0) {
	dialog.show_message(_("pipe() error"));
	return false;
    }
    pid_t pid;
    if ((pid = fork()) < 0) {
	dialog.show_message(_("fork() error"));
	return false;
    }
    if (pid == 0) {
	DISABLE_SIGTSTP();
	// we're in the child.
	dup2(fd_to_spl[0],   STDIN_FILENO);
	dup2(fd_from_spl[1], STDOUT_FILENO);
	dup2(fd_from_spl[1], STDERR_FILENO);

	close(fd_from_spl[0]); close(fd_to_spl[0]);
	close(fd_from_spl[1]); close(fd_to_spl[1]);

	execlp("/bin/sh", "sh", "-c", cmd, NULL);

	// write the error back to the parent
	u8string err;
	err.cformat(_("Error %d (%s)\n"), errno, strerror(errno));
	write(STDOUT_FILENO, err.c_str(), err.size());
	exit(1);
    }

    dialog.show_message(_("Waiting for the speller to finish loading..."));
    dialog.immediate_update();

    u8string identity = read_line();
    
    if (identity.c_str()[0] != '@') {
	dialog.show_message_fmt(_("Error: Not a speller: %s"),
				identity.c_str());
	unload();
	return false;
    } else {   
	// display the speller identity for a brief moment.
	dialog.show_message(identity.c_str());
	dialog.immediate_update();
	sleep(1);
	write_line("@ActivateExtendedProtocol\n"); // for future extensions :-)
	dialog.show_message(_("Speller loaded OK."));
	loaded = true;
	return true;
    }
}

void Speller::unload()
{
    if (loaded) {
	close(fd_from_spl[0]); close(fd_to_spl[0]);
	close(fd_from_spl[1]); close(fd_to_spl[1]);
	delete conv_to_speller;
	delete conv_from_speller;
	loaded = false;
    }
}

// convert_from_unistr() and convert_to_unistr() convert from unicode
// to the speller encoding and vice versa.

void convert_from_unistr(cstring &cstr, const unistring &str,
			 Converter *conv)
{
    char    *buf = new char[str.len() * 6 + 1]; // Max UTF-8 seq is 6.
    unichar *us_p = (unichar *)str.begin();
    char    *cs_p = buf;
    conv->convert(&cs_p, &us_p, str.len());
    cstr = cstring(buf, cs_p);
}

void convert_to_unistr(unistring &str, const cstring &cstr,
		       Converter *conv)
{
    str.resize(cstr.size());
    unichar *us_p = (unichar *)str.begin();
    char    *cs_p = (char *)&*cstr.begin(); // convert iterator to pointer
    conv->convert(&us_p, &cs_p, cstr.size());
    str.resize(us_p - str.begin());
}

void Speller::add_to_dictionary(Correction &correction)
{
    replace_table[correction.incorrect] = unistring(); // "Ignore All"
    cstring cstr;
    cstr.cformat("*%s\n", correction.incorrect_original.c_str());
    write_line(cstr.c_str());
    write_line("#\n");
}

// interactive_correct() - let the user interactively correct the
// spelling mistakes. For every incorrect word, it:
//
// 1. highlights the word
// 2. calls exec_correction_menu() to display the menu
// 3. acts based on the user action.
//
// returns 'false' if the user aborts.

bool Speller::interactive_correct(Corrections &corrections,
				  EditBox &wedit,
				  SpellerWnd &splwnd,
				  bool &restore_cursor)
{
    for (int cur_crctn = 0; cur_crctn < corrections.size(); cur_crctn++)
    {
	Correction &correction = *corrections[cur_crctn];

	MenuResult menu_result;
	unistring  replace_with;
	
	if (replace_table.find(correction.incorrect) != replace_table.end()) {
	    replace_with = replace_table[correction.incorrect];
	    menu_result  = splEdit;
	} else {
	    // highlight the word
	    wedit.unset_primary_mark();
	    wedit.set_cursor_position(Point(correction.line,
					    correction.offset));
	    wedit.set_primary_mark();
	    for (int i = 0; i < correction.incorrect.len(); i++)
		wedit.move_forward_char();

	    menu_result = splwnd.exec_correction_menu(correction);

	    if (menu_result == splChoice) {
		replace_with = correction.suggestions[
					    splwnd.get_suggestion_choice()];
	    } else if (menu_result == splEdit) {
		bool alt_kbd = wedit.get_alt_kbd();
		replace_with = dialog.query(_("Replace with:"),
			correction.incorrect, SPELER_REPLACE_HISTORY,
			InputLine::cmpltOff, &alt_kbd);
		wedit.set_alt_kbd(alt_kbd);
	    }
	}

	switch (menu_result) {
	case splAbort:
	    restore_cursor = false;
	    return false;
	    break;
	case splAbortRestoreCursor:
	    restore_cursor = true;
	    return false;
	    break;
	case splIgnore:
	    if (splwnd.is_global_decision())
		replace_table[correction.incorrect] = unistring();
	    break;
	case splAdd:
	    add_to_dictionary(correction);
	    break;

	case splChoice:
	case splEdit:
	    if (!replace_with.empty()) {
		wedit.set_cursor_position(Point(correction.line,
						correction.offset));
		wedit.replace_text(replace_with, correction.incorrect.len());
		if (splwnd.is_global_decision())
		    replace_table[correction.incorrect] = replace_with;
		// Since we modified the text, the offsets of the
		// following Correction objects must be adjusted.
		for (int i = cur_crctn + 1; i < corrections.size(); i++) {
		    if (corrections[i]->offset > correction.offset) {
			corrections[i]->offset +=
			    replace_with.len() - correction.incorrect.len();
		    }
		}
	    }
	    break;
	}

	app.update_terminal();
    }
    return true;
}

// adjust_word_offset() - the speller reports the offsets of incorrect
// words, but some spellers (like hspell) report incorrect offsets, so
// we need to detect these cases and find the words ourselves.

void adjust_word_offset(Correction &c, const unistring &str)
{
    if (str.index(c.incorrect, c.offset) != c.offset) {
	// first, search the word near the reported offset
	int from = c.offset - 10;
	c.offset = str.index(c.incorrect, (from < 0) ? 0 : from);
	if (c.offset == -1) {
	    // wasn't found, so search starting from the beginning
	    // of the paragraph.
	    if ((c.offset = str.index(c.incorrect, 0)) == -1)
		c.offset = 0;
	}
    }
}

// get_word_boundaries() - get the boundaries of the word on which the
// cursor stands.

void get_word_boundaries(const unistring &str, int cursor, int &wbeg, int &wend)
{
    // If the cursor stands just past the word, treat it as if it
    // stants on the word.
    if ((cursor == str.len() || !BiDi::is_wordch(str[cursor]))
	    && cursor > 0 && BiDi::is_wordch(str[cursor-1]))
	cursor--;
    
    wbeg = wend = cursor;
	
    if (cursor < str.len() && BiDi::is_wordch(str[cursor])) {
	while (wbeg > 0 && BiDi::is_wordch(str[wbeg-1]))
	    wbeg--;
	while (wend < str.len()-1 && BiDi::is_wordch(str[wend+1]))
	    wend++;
	wend++;
    }
}

// erase_special_characters_words() -  erases/modifies characters
// or words that may cause problems to the speller:
// 
// 0. If we're checking emails and the line is quoted (">"), erase it.
// 1. remove words with combining characters (e.g. Hebrew points)
// 2. remove ispell's "\"
// 3. convert Hebrew maqaf to ASCII one.

void erase_special_characters_words(unistring &str, bool erase_quotes)
{
    if (erase_quotes) {
	// If we're checking emails, erase lines starting
	// with ">" (with optional preceding spaces).
	int i = 0;
	while (i < str.len() && str[i] == ' ')
	    i++;
	if (i < str.len() && str[i] == '>') {
	    for (i = 0; i < str.len(); i++)
		str[i] = ' ';
	}
    }
    for (int i = 0; i < str.len(); i++) {
	if (str[i] == UNI_HEB_MAQAF)
	    str[i] = '-';
	if (str[i] == '\\') // ispell's line continuation char.
	    str[i] = ' ';
    }
    for (int i = 0; i < str.len(); i++) {
	if (mk_wcwidth(str[i]) == 0) {
	    if (BiDi::is_nsm(str[i])) {
		// delete the word in which the NSM is.
		int wbeg, wend;
		get_word_boundaries(str, i, wbeg, wend);
		for (int j = wbeg; j < wend; j++)
		    str[j] = ' ';
	    } else {
		// probably some formatting code (RLM, LRM, etc)
		str[i] = ' ';
	    }
	}
    }   
}

// erase_before_after_word() - erases the text segment preceding or the
// text segment following the word on which the cursor stands. 
 
void erase_before_after_word(unistring &str, int cursor, bool bef, bool aft)
{
    int wbeg, wend;
    get_word_boundaries(str, cursor, wbeg, wend);
    if (bef)
	for (int i = 0; i < wbeg; i++)
	    str[i] = ' ';
    if (aft) {
	// but don't erase the hebrew maqaf (ascii-transliterated)
	if (wend < str.len() && str[wend] == '-')
	    wend++;
	for (int i = wend; i < str.len(); i++)
	    str[i] = ' ';
    }
}

// spell_check() - the principal method. 

void Speller::spell_check(splRng range, EditBox &wedit, SpellerWnd &splwnd)
{
    if (!is_loaded()) {
	dialog.show_message(_("Speller is not loaded"));
	return;
    }

    bool cancel_spelling = false;

    if (range == splRngWord)
	write_line("%\n"); // exit terse mode
    else
	write_line("!\n"); // enter terse mode
    
    // Find the start and end paragraphs corresponding to
    // the requested range.
    int start_para, end_para;
    Point cursor_origin;
    wedit.get_cursor_position(cursor_origin);
    if (range == splRngAll) {
	start_para = 0;
	end_para   = wedit.get_number_of_paragraphs() - 1;
    } else {
	start_para = cursor_origin.para;
	if (range == splRngForward)
	    end_para = wedit.get_number_of_paragraphs() - 1;
	else
	    end_para = start_para;
    }
    
    // Some variabls that are used when range==splRngWord
    bool      sole_word_correct = false;
    unistring sole_word;
    unistring sole_word_root;

    bool restore_cursor = true;

    for (int i = start_para; i <= end_para && !cancel_spelling; i++)
    {
	dialog.show_message_fmt(_("Spell checking... %d/%d"),
				  i+1, wedit.get_number_of_paragraphs());
	dialog.immediate_update();
	
	unistring para = wedit.get_paragraph_text(i);

	// erase/modify some characters/words
	erase_special_characters_words(para,
		(wedit.get_syn_hlt() == EditBox::synhltEmail) && (range != splRngWord));

	if (i == start_para) {
	    if (range != splRngAll) {
		// erase text we're not supposed to check.
		erase_before_after_word(para, cursor_origin.pos,
			true, range != splRngForward);

		// after finishing checking splRgnForward/splRgnWord,
		// we restore the cursor to the start of the word on
		// which it stood.
		int wbeg, wend;
		get_word_boundaries(para, cursor_origin.pos, wbeg, wend);
		cursor_origin.pos = wbeg;

		// also, when checking a sole word, keep it because
		// we need to display it later in the dialog-line.
		if (range == splRngWord)
		    sole_word = para.substr(wbeg, wend - wbeg);
	    } else {
		// after finishing checking the whole document, we
		// restore cursor position to the first column of
		// the paragraph.
		cursor_origin.pos = 0;
	    }
	}
    
	// Convert the text to the speller encoding
	// :TODO: special treatment for UTF-8.
	cstring cstr;
	convert_from_unistr(cstr, para, conv_to_speller);

	// Send "^text" to speller
	cstr.insert(0, "^");
	cstr += "\n";
	write_line(cstr.c_str());
	
	// Read the speller reply, till encountering the empty string,
	// and construct a Corrections collection.
	Corrections corrections;
	Correction *last_corretion = NULL;
	do {
	    cstr = read_line();
	    if (cstr.size() != 0) {
		unistring ustr;
		convert_to_unistr(ustr, cstr, conv_from_speller);
		Correction *c = new Correction(u8string(ustr).c_str(), i);
		if (c->is_valid()) {
		    // store the speller-encoded word too, in case
		    // we need to feed it back (like in the "*<<word>>"
		    // command).
		    convert_from_unistr(c->incorrect_original, c->incorrect,
					conv_to_speller);
		    adjust_word_offset(*c, para);
		    corrections.add(c);
		    last_corretion = c;
		} else {
		    delete c;
    
		    // Special support for hspell's hints.
		    if ((ustr[0] == ' ' || ustr[0] == 'H') && last_corretion)
			last_corretion->add_hint(ustr.substr(1));

		    // When spell-checking a sole word, we're in
		    // non-terse mode.
		    if (range == splRngWord) {
			if (ustr[0] == '*' || ustr[0] == '+') {
			    sole_word_correct = true;
			    if (ustr[0] == '+' && ustr.len() > 2)
				sole_word_root = ustr.substr(2);
			}
		    }
		}
	    }
	} while (cstr.size() != 0);

	corrections.sort();

	// :TODO: adjust UTF-8 offsets.

	if ((cancel_spelling = terminal::was_ctrl_c_pressed()))
	    restore_cursor = false;

	// hand the Corrections collection to the method that interacts
	// with the user.
	if (!cancel_spelling && !corrections.empty()) {
	    dialog.show_message_fmt(_("A misspelling was found at %d/%d"),
				    i+1, wedit.get_number_of_paragraphs());
	    cancel_spelling = !interactive_correct(corrections,
				    wedit, splwnd, restore_cursor);
	}
    }

    wedit.unset_primary_mark();

    if (restore_cursor && range != splRngWord)
	wedit.set_cursor_position(cursor_origin);

    if (sole_word_correct) {
	if (sole_word_root.empty())
	    dialog.show_message_fmt(_("Word '%s' is correct"),
				    u8string(sole_word).c_str());
	else
	    dialog.show_message_fmt(_("Word '%s' is correct because of %s"),
				    u8string(sole_word).c_str(),
				    u8string(sole_word_root).c_str());
    } else {
	dialog.show_message(_("Spell cheking done"));
    }
}

// read_line() - read a line from the speller

cstring Speller::read_line()
{
    u8string str;
    char ch;
    while (read(fd_from_spl[0], &ch, 1)) {
	if (ch != '\n')
	    str += ch;
	else
	    break;
    }
    return str;
}

// write_line() - write a line to the speller

void Speller::write_line(const char *s)
{
    write(fd_to_spl[1], s, strlen(s));
}