diff --git a/TeXmacs/progs/generic/spell-widgets.scm b/TeXmacs/progs/generic/spell-widgets.scm index 65d15ecfe5..f8ae2eaca7 100644 --- a/TeXmacs/progs/generic/spell-widgets.scm +++ b/TeXmacs/progs/generic/spell-widgets.scm @@ -43,6 +43,80 @@ (tm-define (inside-spell-buffer?) (== (current-buffer) (spell-buffer))) +(define inline-spell-underlines-serial 0) +(define inline-spell-underlines-buffer #f) + +(define (inline-spell-current-word) + (let* ((t (buffer-tree)) + (p (tree->path t)) + (lan (get-init "language")) + (cp (cursor-path)) + (pos (and cp (cDr cp)))) + (if (and pos (list-starts? pos p)) + (tree-spell-at lan t p (list-tail pos (length p)) 1000) + (list)))) + +(define (inline-spell-underlines-active?) + (and (current-view) + (get-boolean-preference "spell underlines") + (current-buffer) + (not (inside-spell-buffer?)) + (not (buffer-aux? (current-buffer))))) + +(define (clear-inline-spell-underlines) + (set! inline-spell-underlines-serial (+ inline-spell-underlines-serial 1)) + (set! inline-spell-underlines-buffer #f) + (when (current-view) + (clear-spell-errors))) + +(tm-define (inline-spell-underlines-refresh) + (if (not (inline-spell-underlines-active?)) + (clear-inline-spell-underlines) + (let* ((t (buffer-tree)) + (buf (current-buffer)) + (p (tree->path t))) + (when (not (== inline-spell-underlines-buffer buf)) + (set! inline-spell-underlines-buffer buf)) + (let ((sels (inline-spell-current-word))) + (if (null? sels) + (clear-spell-errors) + (set-spell-errors sels)))))) + +(define (inline-spell-underlines-key? key) + (or (in? key (list "space" "return" "tab" "backspace" "delete" + "left" "right" "up" "down" + "home" "end" "pageup" "pagedown")) + (and (== (string-length key) 1) + (not (or (char-alphabetic? (string-ref key 0)) + (char-numeric? (string-ref key 0))))))) + +(define (inline-spell-underlines-typing-key? key) + (and (== (string-length key) 1) + (or (char-alphabetic? (string-ref key 0)) + (char-numeric? (string-ref key 0))))) + +(define (schedule-inline-spell-underlines-after delay) + (when (current-view) + (set! inline-spell-underlines-serial (+ inline-spell-underlines-serial 1)) + (let ((ticket inline-spell-underlines-serial) + (buf (current-buffer))) + (delayed + (:idle delay) + (when (and (== ticket inline-spell-underlines-serial) + (== buf (current-buffer))) + (inline-spell-underlines-refresh)))))) + +(define (schedule-inline-spell-underlines) + (schedule-inline-spell-underlines-after 350)) + +(define (schedule-inline-spell-underlines-slow) + (schedule-inline-spell-underlines-after 900)) + +(tm-define (inline-spell-underlines-preference-changed which val) + (if (== val "on") + (schedule-inline-spell-underlines) + (clear-inline-spell-underlines))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Highlighting the spell results ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -130,6 +204,60 @@ (if (not p) lan (tm->stree (tree-descendant-env bt (cDr p) "language" lan)))))) +(define (inline-spell-selection-at-cursor) + (and-with cp (cursor-path) + (let loop ((sels (get-spell-errors))) + (cond ((or (null? sels) (null? (cdr sels))) #f) + ((and (path-less-eq? (car sels) cp) + (path-less? cp (cadr sels))) + (list (car sels) (cadr sels))) + (else (loop (cddr sels))))))) + +(define (inline-spell-get-language sel) + (let* ((bt (buffer-tree)) + (rp (tree->path bt)) + (sp (car sel)) + (p (and (list-starts? sp rp) (sublist sp (length rp) (length sp)))) + (lan (get-init "language"))) + (if (not p) lan + (tm->stree (tree-descendant-env bt (cDr p) "language" lan))))) + +(define (inline-spell-suggestions sel) + (and-with ss (selection->string sel) + (let* ((lan (inline-spell-get-language sel)) + (st (tm->stree (spell-check lan ss))) + (l0 (if (tm-func? st 'tuple) (cdr st) (list))) + (l1 (if (null? l0) l0 (cdr l0)))) + (if (<= (length l1) 5) l1 (sublist l1 0 5))))) + +(define (inline-spell-suggestions-at-cursor) + (and-with sel (inline-spell-selection-at-cursor) + (inline-spell-suggestions sel))) + +(define (inline-spell-toolbar-open sel) + (let* ((u (current-buffer)) + (aux (spell-buffer))) + (when (not toolbar-spell-active?) + (multi-spell-start) + (set! toolbar-spell-active? #t) + (set! spell-focus-hack? #t) + (set! spell-correct-string "") + (set! spell-suggestions (list)) + (set! spell-corrected 0) + (set! spell-accepted 0) + (set! spell-inserted 0) + (update-bottom-tools)) + (buffer-set-body aux `(document "")) + (buffer-set-master aux u) + (set! spell-window (current-window)) + (set-alt-selection "alternate" (get-spell-errors)) + (set-spell-reference (car sel)) + (spell-focus-on sel))) + +(tm-define (inline-spell-show-toolbar-at-cursor) + (and-with sel (inline-spell-selection-at-cursor) + (inline-spell-toolbar-open sel))) + (define (spell-focus-on sel) ;;(display* "spell-focus-on " sel "\n") (selection-set-range-set sel) @@ -170,6 +298,27 @@ (when (nin? key (list "pageup" "pagedown" "home" "end")) (former key time))) +(tm-define (keyboard-press key time) + (:require (get-boolean-preference "spell underlines")) + (former key time) + (cond ((inline-spell-underlines-key? key) + (schedule-inline-spell-underlines)) + ((inline-spell-underlines-typing-key? key) + (schedule-inline-spell-underlines-slow)))) + +(tm-define (keyboard-focus has-focus? time) + (:require (get-boolean-preference "spell underlines")) + (former has-focus? time) + (when has-focus? + (schedule-inline-spell-underlines))) + +(tm-define (mouse-event key x y mods time data) + (:require (get-boolean-preference "spell underlines")) + (former key x y mods time data) + (when (== key "release-left") + (inline-spell-underlines-refresh) + (inline-spell-show-toolbar-at-cursor))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Highlighting a particular next or previous spell result ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/TeXmacs/progs/init-research.scm b/TeXmacs/progs/init-research.scm index f4a26d6690..af20c61ad0 100644 --- a/TeXmacs/progs/init-research.scm +++ b/TeXmacs/progs/init-research.scm @@ -164,7 +164,10 @@ search-next-match) (lazy-keyboard (generic search-kbd)) (lazy-define (generic spell-widgets) spell-toolbar - open-spell toolbar-spell-start interactive-spell) + open-spell toolbar-spell-start interactive-spell + inline-spell-underlines-refresh + inline-spell-underlines-preference-changed + inline-spell-show-toolbar-at-cursor) (lazy-define (generic format-widgets) open-paragraph-format open-page-format) (lazy-define (generic pattern-selector) open-pattern-selector open-gradient-selector open-background-picture-selector) @@ -180,6 +183,14 @@ (tm-property (open-source-tree-preferences) (:interactive #t)) (tm-property (open-document-paragraph-format) (:interactive #t)) (tm-property (open-document-page-format) (:interactive #t)) + +(define-preferences + ("spell underlines" "off" inline-spell-underlines-preference-changed)) + +(delayed + (:idle 500) + (when (get-boolean-preference "spell underlines") + (inline-spell-underlines-refresh))) (tm-property (open-document-metadata) (:interactive #t)) (tm-property (open-document-colors) (:interactive #t)) (tm-property (open-page-headers-footers) (:interactive #t)) @@ -526,4 +537,3 @@ (display "Timing:") (display (- (texmacs-time) start-time)) (newline) ;(quit-TeXmacs) )))))))))))) - diff --git a/TeXmacs/progs/texmacs/menus/preferences-menu.scm b/TeXmacs/progs/texmacs/menus/preferences-menu.scm index d0f32a3243..8e9c50295a 100644 --- a/TeXmacs/progs/texmacs/menus/preferences-menu.scm +++ b/TeXmacs/progs/texmacs/menus/preferences-menu.scm @@ -267,7 +267,9 @@ --- ("Disable" "0")) (enum ("Bibtex command" "bibtex command") - "bibtex" "biber" "biblatex" "rubibtex" *))) + "bibtex" "biber" "biblatex" "rubibtex" *) + (-> "Experimental" + (toggle ("Inline spellcheck" "spell underlines"))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Computation of the preference menu diff --git a/TeXmacs/progs/texmacs/menus/preferences-widgets.scm b/TeXmacs/progs/texmacs/menus/preferences-widgets.scm index 86cb463a43..06827a53c4 100644 --- a/TeXmacs/progs/texmacs/menus/preferences-widgets.scm +++ b/TeXmacs/progs/texmacs/menus/preferences-widgets.scm @@ -859,7 +859,10 @@ pretty-val : string (get-boolean-preference "gui:print dialogue")))) (meti (hlist // (text "Use fonts in texlive")) (toggle (set-boolean-preference "texlive:fonts" answer) - (get-boolean-preference "texlive:fonts")))))) + (get-boolean-preference "texlive:fonts"))) + (meti (hlist // (text "Inline spellcheck")) + (toggle (set-boolean-preference "spell underlines" answer) + (get-boolean-preference "spell underlines")))))) (tm-widget (experimental-preferences-widget*) (aligned @@ -897,7 +900,10 @@ pretty-val : string (get-boolean-preference "use native menubar"))) (meti (hlist // (text "Use unified toolbars")) (toggle (set-boolean-preference "use unified toolbar" answer) - (get-boolean-preference "use unified toolbar")))))) + (get-boolean-preference "use unified toolbar")))) + (meti (hlist // (text "Inline spellcheck")) + (toggle (set-boolean-preference "spell underlines" answer) + (get-boolean-preference "spell underlines"))))) (tm-widget (other-preferences-widget) (centered diff --git a/devel/222_100.md b/devel/222_100.md new file mode 100644 index 0000000000..c2d4db50f3 --- /dev/null +++ b/devel/222_100.md @@ -0,0 +1,27 @@ +# [222_100] Inline spell checking + +Issue #3102 + +### Summary +Implemented inline spell highlighting for the current buffer. + +The implementation: + +1. Reuses the existing spell backend and tree spell traversal. +2. Stores spell-error ranges in a dedicated editor-side channel. +3. Repaints spell errors without routing updates through `THE_SELECTION`. +4. Shows misspellings with a squiggly underline rendered from the spell rectangles. +5. Refreshes spell results with the existing cursor-local `tree-spell-at` scan. +6. Does not refresh spell state from generic mouse movement. +7. Converts spell ranges into persistent editor-side positions so they survive tree edits safely. +8. Uses a fast idle refresh on word-boundary/navigation keys and a slower debounce while typing inside a word. +9. Reuses the existing bottom spell toolbar for inline misspellings when clicked. + +### How It Works +Scheme computes spell ranges and sends them to the editor through: + +1. `set-spell-errors` +2. `get-spell-errors` +3. `clear-spell-errors` + +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. \ No newline at end of file diff --git a/src/Edit/Interface/edit_interface.cpp b/src/Edit/Interface/edit_interface.cpp index 5d23bb8010..6f15aac3ee 100644 --- a/src/Edit/Interface/edit_interface.cpp +++ b/src/Edit/Interface/edit_interface.cpp @@ -840,6 +840,15 @@ edit_interface_rep::apply_changes () { if (is_empty (alt_sel)) alt_selection_rects= array (); } } + if (env_change & (THE_TREE + THE_ENVIRONMENT + THE_SPELL_ERRORS)) { + if (N (spell_error_rects) != 0) { + rectangles visible (rectangle (vx1, vy1, vx2, vy2)); + for (int i= 0; i < N (spell_error_rects); i++) + invalidate (spell_error_rects[i] & visible); + if (is_empty (get_spell_errors ())) + spell_error_rects= array (); + } + } // cout << "Handling environment\n"; if (env_change & THE_ENVIRONMENT) { @@ -1023,6 +1032,30 @@ edit_interface_rep::apply_changes () { for (int i= 0; i < N (alt_selection_rects); i++) invalidate (alt_selection_rects[i] & visible); } + else alt_selection_rects= array (); + } + + if (env_change & (THE_TREE + THE_ENVIRONMENT + THE_SPELL_ERRORS) || + new_visible != last_visible) { + range_set spell_sel= get_spell_errors (); + if (!is_empty (spell_sel)) { + spell_error_rects= array (); + int b= 0, e= N (spell_sel); + if (e - b >= 200) { + b= max (find_alt_selection_index (spell_sel, vy2, b, e) - 100, b); + e= min (find_alt_selection_index (spell_sel, vy1, b, e) + 100, e); + } + for (int i= b; i + 1 < e; i+= 2) { + range_set sub_sel= simple_range (spell_sel[i], spell_sel[i + 1]); + selection sel = compute_selection (sub_sel); + rectangles rs = sel->rs; + if (N (rs) != 0) spell_error_rects << rs; + } + rectangles visible (new_visible); + for (int i= 0; i < N (spell_error_rects); i++) + invalidate (spell_error_rects[i] & visible); + } + else spell_error_rects= array (); } // cout << "Handling locus highlighting\n"; diff --git a/src/Edit/Interface/edit_interface.hpp b/src/Edit/Interface/edit_interface.hpp index 9eeef6dbce..5fc5f1d1cc 100644 --- a/src/Edit/Interface/edit_interface.hpp +++ b/src/Edit/Interface/edit_interface.hpp @@ -112,6 +112,7 @@ class edit_interface_rep : virtual public editor_rep { void table_scale_apply (SI x, SI y); void table_scale_stop (); array alt_selection_rects; + array spell_error_rects; rectangle last_visible; rectangle last_image_brec; // 图片 bbox 缓存 SI last_image_hr; // 图片 handle 半径缓存 diff --git a/src/Edit/Interface/edit_repaint.cpp b/src/Edit/Interface/edit_repaint.cpp index 1f2e914fa0..94b779e37c 100644 --- a/src/Edit/Interface/edit_repaint.cpp +++ b/src/Edit/Interface/edit_repaint.cpp @@ -10,6 +10,7 @@ ******************************************************************************/ #include "Interface/edit_interface.hpp" +#include "colors.hpp" #include "gui.hpp" // for gui_interrupted #include "message.hpp" #include "preferences.hpp" @@ -150,6 +151,33 @@ edit_interface_rep::draw_context (renderer ren, rectangle r) { draw_surround (ren, r); } +static void +draw_spell_error_wave (renderer ren, rectangles rs) { + SI pixel= max ((SI) 1, ren->pixel); + SI width= max ((SI) 5, 5 * pixel); + SI step = max ((SI) 8, 8 * pixel); + SI amp = max ((SI) 4, 4 * pixel); + while (!is_nil (rs)) { + rectangle r = rs->item; + SI x = r->x1; + SI base = r->y1 + 2 * pixel; + SI y_near= base; + SI y_far = base - amp; + ren->set_pencil (pencil (ren->get_pencil ()->get_color (), width)); + if (r->x2 - r->x1 <= step) ren->line (r->x1, y_near, r->x2, y_near); + else { + bool up= true; + while (x < r->x2) { + SI nx= min (x + step, r->x2); + ren->line (x, up ? y_near : y_far, nx, up ? y_far : y_near); + x = nx; + up= !up; + } + } + rs= rs->next; + } +} + void edit_interface_rep::draw_selection (renderer ren, rectangle r) { rectangles visible (thicken (r, 2 * ren->pixel, 2 * ren->pixel)); @@ -173,6 +201,14 @@ edit_interface_rep::draw_selection (renderer ren, rectangle r) { ren->draw_rectangles (alt_selection_rects[i] & visible); #endif } + int spell_count= N (spell_error_rects); + if (spell_count > 0) { + color col= blend_colors (rgb_color (220, 70, 70, 220), + get_env_color (INCORRECT_COLOR)); + ren->set_pencil (pencil (col, ren->pixel)); + for (int i= 0; i < spell_count; i++) + draw_spell_error_wave (ren, spell_error_rects[i] & visible); + } if (!is_nil (selection_rects)) { color col= get_env_color (SELECTION_COLOR); if (table_selection) col= get_env_color (TABLE_SELECTION_COLOR); diff --git a/src/Edit/Replace/edit_select.cpp b/src/Edit/Replace/edit_select.cpp index d2b9e2cecf..31d27b9c38 100644 --- a/src/Edit/Replace/edit_select.cpp +++ b/src/Edit/Replace/edit_select.cpp @@ -63,7 +63,7 @@ edit_select_rep::edit_select_rep () : cur_sel (no_ranges ()), selecting (false), shift_selecting (false), mid_p (), selection_import ("default"), selection_export ("default"), focus_p (), focus_hold (false) {} -edit_select_rep::~edit_select_rep () {} +edit_select_rep::~edit_select_rep () { clear_spell_error_positions (); } /****************************************************************************** * Semantic selections @@ -1100,3 +1100,47 @@ edit_select_rep::cancel_alt_selections () { notify_change (THE_SELECTION); } } + +void +edit_select_rep::clear_spell_error_positions () { + for (int i= 0; i < N (spell_errors_pos); ++i) + if (!is_nil (spell_errors_pos[i])) position_delete (spell_errors_pos[i]); + spell_errors_pos= array (); +} + +range_set +edit_select_rep::resolve_spell_errors () { + range_set sel; + for (int i= 0; i + 1 < N (spell_errors_pos); i+= 2) { + path p1= position_get (spell_errors_pos[i]); + path p2= position_get (spell_errors_pos[i + 1]); + if (!is_nil (p1) && !is_nil (p2) && p1 != p2) sel << p1 << p2; + } + return sel; +} + +void +edit_select_rep::set_spell_errors (range_set sel) { + if (resolve_spell_errors () != sel) { + clear_spell_error_positions (); + for (int i= 0; i + 1 < N (sel); i+= 2) + if (sel[i] != sel[i + 1]) { + spell_errors_pos << position_new (sel[i]); + spell_errors_pos << position_new (sel[i + 1]); + } + notify_change (THE_SPELL_ERRORS); + } +} + +range_set +edit_select_rep::get_spell_errors () { + return resolve_spell_errors (); +} + +void +edit_select_rep::clear_spell_errors () { + if (N (spell_errors_pos) != 0) { + clear_spell_error_positions (); + notify_change (THE_SPELL_ERRORS); + } +} diff --git a/src/Edit/Replace/edit_select.hpp b/src/Edit/Replace/edit_select.hpp index df009345e0..627387a3d0 100644 --- a/src/Edit/Replace/edit_select.hpp +++ b/src/Edit/Replace/edit_select.hpp @@ -27,14 +27,17 @@ class edit_select_rep : virtual public editor_rep { string selection_export; path focus_p; bool focus_hold; + array spell_errors_pos; hashmap alt_sels; int total= 0; int index= 0; protected: - void get_selection (path& start, path& end); - void set_selection (path start, path end); - void raw_cut (path start, path end); + void get_selection (path& start, path& end); + void set_selection (path start, path end); + void raw_cut (path start, path end); + void clear_spell_error_positions (); + range_set resolve_spell_errors (); public: edit_select_rep (); @@ -111,6 +114,9 @@ class edit_select_rep : virtual public editor_rep { string get_alt_selection_index (string s, string action); void cancel_alt_selection (string s); void cancel_alt_selections (); + void set_spell_errors (range_set sel); + range_set get_spell_errors (); + void clear_spell_errors (); }; #endif // defined EDIT_SELECT_H diff --git a/src/Edit/editor.hpp b/src/Edit/editor.hpp index 61f8899fac..581579ca6c 100644 --- a/src/Edit/editor.hpp +++ b/src/Edit/editor.hpp @@ -40,6 +40,7 @@ #define THE_LOCUS 128 #define THE_MENUS 256 #define THE_FREEZE 512 +#define THE_SPELL_ERRORS 1024 class tm_buffer_rep; class tm_view_rep; @@ -557,6 +558,9 @@ class editor_rep : public simple_widget_rep { virtual string get_alt_selection_index (string s, string action)= 0; virtual void cancel_alt_selection (string s) = 0; virtual void cancel_alt_selections () = 0; + virtual void set_spell_errors (range_set sel) = 0; + virtual range_set get_spell_errors () = 0; + virtual void clear_spell_errors () = 0; /* public routines from edit_replace */ virtual bool inside (string what) = 0; diff --git a/src/Scheme/Glue/build-glue-editor.scm b/src/Scheme/Glue/build-glue-editor.scm index 21d1b40a3c..15ae7b10df 100644 --- a/src/Scheme/Glue/build-glue-editor.scm +++ b/src/Scheme/Glue/build-glue-editor.scm @@ -260,6 +260,9 @@ (get-alt-selection get_alt_selection (array_path string)) (cancel-alt-selection cancel_alt_selection (void string)) (cancel-alt-selections cancel_alt_selections (void)) + (set-spell-errors set_spell_errors (void array_path)) + (get-spell-errors get_spell_errors (array_path)) + (clear-spell-errors clear_spell_errors (void)) ;; undo and redo (clear-undo-history clear_undo_history (void)) diff --git a/src/Scheme/Glue/glue_editor.lua b/src/Scheme/Glue/glue_editor.lua index 143d8b1c86..e3a536bc52 100644 --- a/src/Scheme/Glue/glue_editor.lua +++ b/src/Scheme/Glue/glue_editor.lua @@ -1593,6 +1593,24 @@ function main() cpp_name = "cancel_alt_selections", ret_type = "void" }, + { + scm_name = "set-spell-errors", + cpp_name = "set_spell_errors", + ret_type = "void", + arg_list = { + "array_path" + } + }, + { + scm_name = "get-spell-errors", + cpp_name = "get_spell_errors", + ret_type = "array_path" + }, + { + scm_name = "clear-spell-errors", + cpp_name = "clear_spell_errors", + ret_type = "void" + }, -- undo and redo { @@ -2212,4 +2230,4 @@ function main() } } -end \ No newline at end of file +end