Skip to content

Commit 540d164

Browse files
committed
[222_100] add inline spell checking
1 parent a2c6f52 commit 540d164

13 files changed

Lines changed: 349 additions & 10 deletions

File tree

TeXmacs/progs/generic/spell-widgets.scm

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,80 @@
4343
(tm-define (inside-spell-buffer?)
4444
(== (current-buffer) (spell-buffer)))
4545

46+
(define inline-spell-underlines-serial 0)
47+
(define inline-spell-underlines-buffer #f)
48+
49+
(define (inline-spell-current-word)
50+
(let* ((t (buffer-tree))
51+
(p (tree->path t))
52+
(lan (get-init "language"))
53+
(cp (cursor-path))
54+
(pos (and cp (cDr cp))))
55+
(if (and pos (list-starts? pos p))
56+
(tree-spell-at lan t p (list-tail pos (length p)) 1000)
57+
(list))))
58+
59+
(define (inline-spell-underlines-active?)
60+
(and (current-view)
61+
(get-boolean-preference "spell underlines")
62+
(current-buffer)
63+
(not (inside-spell-buffer?))
64+
(not (buffer-aux? (current-buffer)))))
65+
66+
(define (clear-inline-spell-underlines)
67+
(set! inline-spell-underlines-serial (+ inline-spell-underlines-serial 1))
68+
(set! inline-spell-underlines-buffer #f)
69+
(when (current-view)
70+
(clear-spell-errors)))
71+
72+
(tm-define (inline-spell-underlines-refresh)
73+
(if (not (inline-spell-underlines-active?))
74+
(clear-inline-spell-underlines)
75+
(let* ((t (buffer-tree))
76+
(buf (current-buffer))
77+
(p (tree->path t)))
78+
(when (not (== inline-spell-underlines-buffer buf))
79+
(set! inline-spell-underlines-buffer buf))
80+
(let ((sels (inline-spell-current-word)))
81+
(if (null? sels)
82+
(clear-spell-errors)
83+
(set-spell-errors sels))))))
84+
85+
(define (inline-spell-underlines-key? key)
86+
(or (in? key (list "space" "return" "tab" "backspace" "delete"
87+
"left" "right" "up" "down"
88+
"home" "end" "pageup" "pagedown"))
89+
(and (== (string-length key) 1)
90+
(not (or (char-alphabetic? (string-ref key 0))
91+
(char-numeric? (string-ref key 0)))))))
92+
93+
(define (inline-spell-underlines-typing-key? key)
94+
(and (== (string-length key) 1)
95+
(or (char-alphabetic? (string-ref key 0))
96+
(char-numeric? (string-ref key 0)))))
97+
98+
(define (schedule-inline-spell-underlines-after delay)
99+
(when (current-view)
100+
(set! inline-spell-underlines-serial (+ inline-spell-underlines-serial 1))
101+
(let ((ticket inline-spell-underlines-serial)
102+
(buf (current-buffer)))
103+
(delayed
104+
(:idle delay)
105+
(when (and (== ticket inline-spell-underlines-serial)
106+
(== buf (current-buffer)))
107+
(inline-spell-underlines-refresh))))))
108+
109+
(define (schedule-inline-spell-underlines)
110+
(schedule-inline-spell-underlines-after 350))
111+
112+
(define (schedule-inline-spell-underlines-slow)
113+
(schedule-inline-spell-underlines-after 900))
114+
115+
(tm-define (inline-spell-underlines-preference-changed which val)
116+
(if (== val "on")
117+
(schedule-inline-spell-underlines)
118+
(clear-inline-spell-underlines)))
119+
46120
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
47121
;; Highlighting the spell results
48122
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -130,6 +204,60 @@
130204
(if (not p) lan
131205
(tm->stree (tree-descendant-env bt (cDr p) "language" lan))))))
132206

207+
(define (inline-spell-selection-at-cursor)
208+
(and-with cp (cursor-path)
209+
(let loop ((sels (get-spell-errors)))
210+
(cond ((or (null? sels) (null? (cdr sels))) #f)
211+
((and (path-less-eq? (car sels) cp)
212+
(path-less? cp (cadr sels)))
213+
(list (car sels) (cadr sels)))
214+
(else (loop (cddr sels)))))))
215+
216+
(define (inline-spell-get-language sel)
217+
(let* ((bt (buffer-tree))
218+
(rp (tree->path bt))
219+
(sp (car sel))
220+
(p (and (list-starts? sp rp) (sublist sp (length rp) (length sp))))
221+
(lan (get-init "language")))
222+
(if (not p) lan
223+
(tm->stree (tree-descendant-env bt (cDr p) "language" lan)))))
224+
225+
(define (inline-spell-suggestions sel)
226+
(and-with ss (selection->string sel)
227+
(let* ((lan (inline-spell-get-language sel))
228+
(st (tm->stree (spell-check lan ss)))
229+
(l0 (if (tm-func? st 'tuple) (cdr st) (list)))
230+
(l1 (if (null? l0) l0 (cdr l0))))
231+
(if (<= (length l1) 5) l1 (sublist l1 0 5)))))
232+
233+
(define (inline-spell-suggestions-at-cursor)
234+
(and-with sel (inline-spell-selection-at-cursor)
235+
(inline-spell-suggestions sel)))
236+
237+
(define (inline-spell-toolbar-open sel)
238+
(let* ((u (current-buffer))
239+
(aux (spell-buffer)))
240+
(when (not toolbar-spell-active?)
241+
(multi-spell-start)
242+
(set! toolbar-spell-active? #t)
243+
(set! spell-focus-hack? #t)
244+
(set! spell-correct-string "")
245+
(set! spell-suggestions (list))
246+
(set! spell-corrected 0)
247+
(set! spell-accepted 0)
248+
(set! spell-inserted 0)
249+
(update-bottom-tools))
250+
(buffer-set-body aux `(document ""))
251+
(buffer-set-master aux u)
252+
(set! spell-window (current-window))
253+
(set-alt-selection "alternate" (get-spell-errors))
254+
(set-spell-reference (car sel))
255+
(spell-focus-on sel)))
256+
257+
(tm-define (inline-spell-show-toolbar-at-cursor)
258+
(and-with sel (inline-spell-selection-at-cursor)
259+
(inline-spell-toolbar-open sel)))
260+
133261
(define (spell-focus-on sel)
134262
;;(display* "spell-focus-on " sel "\n")
135263
(selection-set-range-set sel)
@@ -170,6 +298,27 @@
170298
(when (nin? key (list "pageup" "pagedown" "home" "end"))
171299
(former key time)))
172300

301+
(tm-define (keyboard-press key time)
302+
(:require (get-boolean-preference "spell underlines"))
303+
(former key time)
304+
(cond ((inline-spell-underlines-key? key)
305+
(schedule-inline-spell-underlines))
306+
((inline-spell-underlines-typing-key? key)
307+
(schedule-inline-spell-underlines-slow))))
308+
309+
(tm-define (keyboard-focus has-focus? time)
310+
(:require (get-boolean-preference "spell underlines"))
311+
(former has-focus? time)
312+
(when has-focus?
313+
(schedule-inline-spell-underlines)))
314+
315+
(tm-define (mouse-event key x y mods time data)
316+
(:require (get-boolean-preference "spell underlines"))
317+
(former key x y mods time data)
318+
(when (== key "release-left")
319+
(inline-spell-underlines-refresh)
320+
(inline-spell-show-toolbar-at-cursor)))
321+
173322
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
174323
;; Highlighting a particular next or previous spell result
175324
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

TeXmacs/progs/init-research.scm

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,10 @@
164164
search-next-match)
165165
(lazy-keyboard (generic search-kbd))
166166
(lazy-define (generic spell-widgets) spell-toolbar
167-
open-spell toolbar-spell-start interactive-spell)
167+
open-spell toolbar-spell-start interactive-spell
168+
inline-spell-underlines-refresh
169+
inline-spell-underlines-preference-changed
170+
inline-spell-show-toolbar-at-cursor)
168171
(lazy-define (generic format-widgets) open-paragraph-format open-page-format)
169172
(lazy-define (generic pattern-selector) open-pattern-selector
170173
open-gradient-selector open-background-picture-selector)
@@ -180,6 +183,14 @@
180183
(tm-property (open-source-tree-preferences) (:interactive #t))
181184
(tm-property (open-document-paragraph-format) (:interactive #t))
182185
(tm-property (open-document-page-format) (:interactive #t))
186+
187+
(define-preferences
188+
("spell underlines" "off" inline-spell-underlines-preference-changed))
189+
190+
(delayed
191+
(:idle 500)
192+
(when (get-boolean-preference "spell underlines")
193+
(inline-spell-underlines-refresh)))
183194
(tm-property (open-document-metadata) (:interactive #t))
184195
(tm-property (open-document-colors) (:interactive #t))
185196
(tm-property (open-page-headers-footers) (:interactive #t))
@@ -526,4 +537,3 @@
526537
(display "Timing:") (display (- (texmacs-time) start-time)) (newline)
527538
;(quit-TeXmacs)
528539
))))))))))))
529-

TeXmacs/progs/texmacs/menus/preferences-menu.scm

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@
267267
---
268268
("Disable" "0"))
269269
(enum ("Bibtex command" "bibtex command")
270-
"bibtex" "biber" "biblatex" "rubibtex" *)))
270+
"bibtex" "biber" "biblatex" "rubibtex" *)
271+
(-> "Experimental"
272+
(toggle ("Inline spellcheck" "spell underlines")))))
271273

272274
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
273275
;; Computation of the preference menu

TeXmacs/progs/texmacs/menus/preferences-widgets.scm

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,10 @@ pretty-val : string
859859
(get-boolean-preference "gui:print dialogue"))))
860860
(meti (hlist // (text "Use fonts in texlive"))
861861
(toggle (set-boolean-preference "texlive:fonts" answer)
862-
(get-boolean-preference "texlive:fonts"))))))
862+
(get-boolean-preference "texlive:fonts")))
863+
(meti (hlist // (text "Inline spellcheck"))
864+
(toggle (set-boolean-preference "spell underlines" answer)
865+
(get-boolean-preference "spell underlines"))))))
863866

864867
(tm-widget (experimental-preferences-widget*)
865868
(aligned
@@ -897,7 +900,10 @@ pretty-val : string
897900
(get-boolean-preference "use native menubar")))
898901
(meti (hlist // (text "Use unified toolbars"))
899902
(toggle (set-boolean-preference "use unified toolbar" answer)
900-
(get-boolean-preference "use unified toolbar"))))))
903+
(get-boolean-preference "use unified toolbar"))))
904+
(meti (hlist // (text "Inline spellcheck"))
905+
(toggle (set-boolean-preference "spell underlines" answer)
906+
(get-boolean-preference "spell underlines")))))
901907

902908
(tm-widget (other-preferences-widget)
903909
(centered

devel/222_100.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# [222_100] Inline spell checking
2+
3+
Issue #3102
4+
5+
### Summary
6+
Implemented inline spell highlighting for the current buffer.
7+
8+
The implementation:
9+
10+
1. Reuses the existing spell backend and tree spell traversal.
11+
2. Stores spell-error ranges in a dedicated editor-side channel.
12+
3. Repaints spell errors without routing updates through `THE_SELECTION`.
13+
4. Shows misspellings with a squiggly underline rendered from the spell rectangles.
14+
5. Refreshes spell results with the existing cursor-local `tree-spell-at` scan.
15+
6. Does not refresh spell state from generic mouse movement.
16+
7. Converts spell ranges into persistent editor-side positions so they survive tree edits safely.
17+
8. Uses a fast idle refresh on word-boundary/navigation keys and a slower debounce while typing inside a word.
18+
9. Reuses the existing bottom spell toolbar for inline misspellings when clicked.
19+
20+
### How It Works
21+
Scheme computes spell ranges and sends them to the editor through:
22+
23+
1. `set-spell-errors`
24+
2. `get-spell-errors`
25+
3. `clear-spell-errors`
26+
27+
The editor converts incoming spell ranges into persistent tree positions, keeps them separately from normal selections, resolves them back into rectangles when the tree or visible region changes, and paints a wave near the bottom of each rectangle using a red-tinted incorrect-color theme value. Clicking an inline misspelled word opens the existing bottom spell toolbar and seeds it with the current word's suggestion list and replacement actions from the existing spell backend.

src/Edit/Interface/edit_interface.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,15 @@ edit_interface_rep::apply_changes () {
840840
if (is_empty (alt_sel)) alt_selection_rects= array<rectangles> ();
841841
}
842842
}
843+
if (env_change & (THE_TREE + THE_ENVIRONMENT + THE_SPELL_ERRORS)) {
844+
if (N (spell_error_rects) != 0) {
845+
rectangles visible (rectangle (vx1, vy1, vx2, vy2));
846+
for (int i= 0; i < N (spell_error_rects); i++)
847+
invalidate (spell_error_rects[i] & visible);
848+
if (is_empty (get_spell_errors ()))
849+
spell_error_rects= array<rectangles> ();
850+
}
851+
}
843852

844853
// cout << "Handling environment\n";
845854
if (env_change & THE_ENVIRONMENT) {
@@ -1023,6 +1032,30 @@ edit_interface_rep::apply_changes () {
10231032
for (int i= 0; i < N (alt_selection_rects); i++)
10241033
invalidate (alt_selection_rects[i] & visible);
10251034
}
1035+
else alt_selection_rects= array<rectangles> ();
1036+
}
1037+
1038+
if (env_change & (THE_TREE + THE_ENVIRONMENT + THE_SPELL_ERRORS) ||
1039+
new_visible != last_visible) {
1040+
range_set spell_sel= get_spell_errors ();
1041+
if (!is_empty (spell_sel)) {
1042+
spell_error_rects= array<rectangles> ();
1043+
int b= 0, e= N (spell_sel);
1044+
if (e - b >= 200) {
1045+
b= max (find_alt_selection_index (spell_sel, vy2, b, e) - 100, b);
1046+
e= min (find_alt_selection_index (spell_sel, vy1, b, e) + 100, e);
1047+
}
1048+
for (int i= b; i + 1 < e; i+= 2) {
1049+
range_set sub_sel= simple_range (spell_sel[i], spell_sel[i + 1]);
1050+
selection sel = compute_selection (sub_sel);
1051+
rectangles rs = sel->rs;
1052+
if (N (rs) != 0) spell_error_rects << rs;
1053+
}
1054+
rectangles visible (new_visible);
1055+
for (int i= 0; i < N (spell_error_rects); i++)
1056+
invalidate (spell_error_rects[i] & visible);
1057+
}
1058+
else spell_error_rects= array<rectangles> ();
10261059
}
10271060

10281061
// cout << "Handling locus highlighting\n";

src/Edit/Interface/edit_interface.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ class edit_interface_rep : virtual public editor_rep {
112112
void table_scale_apply (SI x, SI y);
113113
void table_scale_stop ();
114114
array<rectangles> alt_selection_rects;
115+
array<rectangles> spell_error_rects;
115116
rectangle last_visible;
116117
rectangle last_image_brec; // 图片 bbox 缓存
117118
SI last_image_hr; // 图片 handle 半径缓存

src/Edit/Interface/edit_repaint.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
******************************************************************************/
1111

1212
#include "Interface/edit_interface.hpp"
13+
#include "colors.hpp"
1314
#include "gui.hpp" // for gui_interrupted
1415
#include "message.hpp"
1516
#include "preferences.hpp"
@@ -150,6 +151,33 @@ edit_interface_rep::draw_context (renderer ren, rectangle r) {
150151
draw_surround (ren, r);
151152
}
152153

154+
static void
155+
draw_spell_error_wave (renderer ren, rectangles rs) {
156+
SI pixel= max ((SI) 1, ren->pixel);
157+
SI width= max ((SI) 5, 5 * pixel);
158+
SI step = max ((SI) 8, 8 * pixel);
159+
SI amp = max ((SI) 4, 4 * pixel);
160+
while (!is_nil (rs)) {
161+
rectangle r = rs->item;
162+
SI x = r->x1;
163+
SI base = r->y1 + 2 * pixel;
164+
SI y_near= base;
165+
SI y_far = base - amp;
166+
ren->set_pencil (pencil (ren->get_pencil ()->get_color (), width));
167+
if (r->x2 - r->x1 <= step) ren->line (r->x1, y_near, r->x2, y_near);
168+
else {
169+
bool up= true;
170+
while (x < r->x2) {
171+
SI nx= min (x + step, r->x2);
172+
ren->line (x, up ? y_near : y_far, nx, up ? y_far : y_near);
173+
x = nx;
174+
up= !up;
175+
}
176+
}
177+
rs= rs->next;
178+
}
179+
}
180+
153181
void
154182
edit_interface_rep::draw_selection (renderer ren, rectangle r) {
155183
rectangles visible (thicken (r, 2 * ren->pixel, 2 * ren->pixel));
@@ -173,6 +201,14 @@ edit_interface_rep::draw_selection (renderer ren, rectangle r) {
173201
ren->draw_rectangles (alt_selection_rects[i] & visible);
174202
#endif
175203
}
204+
int spell_count= N (spell_error_rects);
205+
if (spell_count > 0) {
206+
color col= blend_colors (rgb_color (220, 70, 70, 220),
207+
get_env_color (INCORRECT_COLOR));
208+
ren->set_pencil (pencil (col, ren->pixel));
209+
for (int i= 0; i < spell_count; i++)
210+
draw_spell_error_wave (ren, spell_error_rects[i] & visible);
211+
}
176212
if (!is_nil (selection_rects)) {
177213
color col= get_env_color (SELECTION_COLOR);
178214
if (table_selection) col= get_env_color (TABLE_SELECTION_COLOR);

0 commit comments

Comments
 (0)