diff --git a/TeXmacs/progs/generic/generic-edit.scm b/TeXmacs/progs/generic/generic-edit.scm index b5eef67813..dd22aa1885 100644 --- a/TeXmacs/progs/generic/generic-edit.scm +++ b/TeXmacs/progs/generic/generic-edit.scm @@ -230,6 +230,23 @@ (tm-define (kbd-cancel) (clipboard-clear "primary")) +(tm-define (general-cancel) + (:synopsis "Cancel the current ongoing operation or selection") + (cond + ((or (and (defined? 'toolbar-search-active?) toolbar-search-active?) + (and (defined? 'toolbar-replace-active?) toolbar-replace-active?)) + (toolbar-search-end)) + ((and (defined? 'toolbar-spell-active?) toolbar-spell-active?) + (toolbar-spell-end)) + ((in-search-mode?) + (key-press-search "C-g")) + ((in-replace-mode?) + (key-press-replace "C-g")) + ((in-spell-mode?) + (key-press-spell "C-g")) + (else + (selection-cancel)))) + #| ocr-paste 剪贴板中的内容是图像时,OCR并插入已识别的内容到当前光标处 diff --git a/TeXmacs/progs/generic/generic-kbd.scm b/TeXmacs/progs/generic/generic-kbd.scm index 0d5db61f69..f4a182a230 100644 --- a/TeXmacs/progs/generic/generic-kbd.scm +++ b/TeXmacs/progs/generic/generic-kbd.scm @@ -254,7 +254,7 @@ ("emacs d" (kbd-delete)) ("emacs e" (kbd-end-line)) ("emacs f" (kbd-right)) - ("emacs g" (selection-cancel)) + ("emacs g" (general-cancel)) ("emacs j" (insert-return)) ("emacs k" (kill-paragraph)) ("emacs l" (refresh-window)) @@ -620,7 +620,7 @@ ("C-e" (kbd-end-line)) ("C-b" (kbd-left)) ("C-f" (kbd-right)) - ("C-g" (selection-cancel)) + ("C-g" (general-cancel)) ("C-k" (kill-paragraph)) ("C-l" (refresh-window)) ("C-y" (yank-paragraph)) diff --git a/TeXmacs/progs/generic/search-widgets.scm b/TeXmacs/progs/generic/search-widgets.scm index e440f02668..e273edadb0 100644 --- a/TeXmacs/progs/generic/search-widgets.scm +++ b/TeXmacs/progs/generic/search-widgets.scm @@ -696,9 +696,10 @@ tree 或 #f (let ((u (if (null? args) (master-buffer) (car args))) (raux (if (null? args) (replace-buffer) (cadr args)))) (and-with by (or (by-tree raux) current-replace) + (editor-clear-interrupt) (with-buffer u (start-editing) - (while (replace-next by) + (while (and (not (editor-interrupted?)) (replace-next by)) (perform-search*)) (end-editing)) (perform-search*) @@ -1043,10 +1044,11 @@ tree 或 #f ((== key "pagedown") (search-next-match #t)) ((== key "S-F3") (search-next-match #f)) ((== key "F3") (search-next-match #t)) - ((in? key '("C-F" "A-F" "M-F" "C-G" "A-G" "M-G" "C-r" "A-r" "M-r")) + ((in? key '("C-F" "A-F" "M-F" "A-G" "M-G" "C-r" "A-r" "M-r")) (search-next-match #f)) - ((in? key '("C-f" "A-f" "M-f" "C-g" "A-g" "M-g" "C-s" "A-s" "M-s")) + ((in? key '("C-f" "A-f" "M-f" "A-g" "M-g" "C-s" "A-s" "M-s")) (search-next-match #t)) + ((in? key '("C-g" "C-G")) (toolbar-search-end)) ((and r? (in? key (list "tab" "S-tab" "return"))) (search-toolbar-search what) (keyboard-focus-on "replace-by")) @@ -1149,7 +1151,7 @@ tree 或 #f ((== key "return") (replace-toolbar-replace by)) ((== key "S-return") (undo 0) (perform-search*)) ((== key "C-return") (replace-all)) - ((== key "escape") (toolbar-search-end)) + ((in? key '("escape" "C-g")) (toolbar-search-end)) (else (perform-search*))))) (tm-widget (replace-toolbar) diff --git a/devel/201_85.md b/devel/201_85.md new file mode 100644 index 0000000000..fb6e345744 --- /dev/null +++ b/devel/201_85.md @@ -0,0 +1,56 @@ +# [201_85] Ctrl+G interrupt for ongoing commands + +## Issue +#315 — Ctrl+G should interrupt ongoing commands such as find-replace and search. + +## Problem +There was no general-purpose mechanism to interrupt long-running editor operations (find, replace-all, spell-check) via Ctrl+G. Several C++ loops (`next_match`, replace-all, `spell_next`) run unbounded `while(true)` with no way to break out once started. Additionally, Ctrl+G behavior was inconsistent: it cancelled selection in normal mode but triggered "next match" in toolbar search. + +## Solution +### 1. C++ interrupt infrastructure +Added an `editor_interrupted` flag on `edit_interface_rep` with methods `interrupt_editor()`, `is_editor_interrupted()`, and `clear_editor_interrupt()`. The flag is reset when returning to normal input mode. + +### 2. Interruptible blocking loops +Added `gui_interrupted(true)` checks to all unbounded C++ loops: +- `next_match()` — exits cleanly with "Search interrupted" message +- Replace-all loop (`a`/`!` key) — stops mid-replace with partial count +- `search_previous_compound()` / `search_next_compound()` +- `spell_next()` — ends spell check early with "Spell check interrupted" + +### 3. Scheme glue +Exposed `editor-interrupt`, `editor-interrupted?`, `editor-clear-interrupt` to Scheme via `glue_editor.lua`. + +### 4. Unified `general-cancel` command +Added `general-cancel` in `generic-edit.scm` that dispatches to the right cancel handler based on current mode: toolbar search/replace end, old-style search/replace/spell stop, or `selection-cancel` as fallback. + +### 5. Consistent Ctrl+G behavior +- Toolbar search: Changed C-g/C-G from "next match" to `toolbar-search-end` +- Toolbar replace: Added C-g as cancel alongside escape +- Scheme replace-all: Made interruptible via `editor-interrupted?` check +- Emacs/macOS profiles: Updated C-g binding to use `general-cancel` +- Old-style search: Added missing `escape` handler to `search_keypress` + +## Changed Files +- `src/Edit/Interface/edit_interface.hpp` — `editor_interrupted` flag and methods +- `src/Edit/Interface/edit_interface.cpp` — Initialize flag in constructor +- `src/Edit/Interface/edit_keyboard.cpp` — Implement interrupt methods, reset in `set_input_normal` +- `src/Edit/editor.hpp` — Pure virtual declarations +- `src/Edit/Replace/edit_search.cpp` — Interrupt checks in loops, escape in search_keypress +- `src/Edit/Replace/edit_spell.cpp` — Interrupt check in `spell_next` +- `src/Scheme/Glue/glue_editor.lua` — Expose interrupt methods to Scheme +- `TeXmacs/progs/generic/generic-edit.scm` — `general-cancel` command +- `TeXmacs/progs/generic/generic-kbd.scm` — C-g bindings use `general-cancel` +- `TeXmacs/progs/generic/search-widgets.scm` — C-g cancels toolbar search/replace + +## How to Test +1. Open a large document +2. Use Ctrl+H (or menu Edit > Replace) to open find-replace +3. Enter a common pattern and press Ctrl+Return (replace-all) +4. While replacement is running, press Ctrl+G — it should stop +5. Verify partial replacements can be undone with Ctrl+Z +6. In toolbar search (Ctrl+F), press Ctrl+G — toolbar should close +7. In normal mode, press Ctrl+G — selection should be cancelled + +## 2026/02/26 +### What +Implement Ctrl+G as a universal interrupt/cancel command for ongoing operations. diff --git a/src/Edit/Interface/edit_interface.cpp b/src/Edit/Interface/edit_interface.cpp index 66bd4d305d..dc729bb368 100644 --- a/src/Edit/Interface/edit_interface.cpp +++ b/src/Edit/Interface/edit_interface.cpp @@ -71,8 +71,9 @@ edit_interface_rep::edit_interface_rep () cursor_blink_period (500), table_selection (false), mouse_adjusting (false), oc (0, 0), temp_invalid_cursor (false), shadow (NULL), stored (NULL), cur_sb (2), cur_wb (2) { - user_active= false; - input_mode = INPUT_NORMAL; + user_active = false; + input_mode = INPUT_NORMAL; + editor_interrupted= false; gui_root_extents (cur_wx, cur_wy); } diff --git a/src/Edit/Interface/edit_interface.hpp b/src/Edit/Interface/edit_interface.hpp index db5e7cfb45..810f2e2fde 100644 --- a/src/Edit/Interface/edit_interface.hpp +++ b/src/Edit/Interface/edit_interface.hpp @@ -58,6 +58,7 @@ class edit_interface_rep : virtual public editor_rep { SI zpixel; // pixel multiplied by zoom factor rectangles copy_always; // for wiping out cursor int input_mode; // INPUT_NORMAL, INPUT_SEARCH, INPUT_REPLACE + bool editor_interrupted; // set by Ctrl+G, checked by long-running loops protected: SI last_x, last_y; @@ -218,6 +219,9 @@ class edit_interface_rep : virtual public editor_rep { bool in_spell_mode (); bool kbd_get_command (string which, string& help, command& cmd); void interrupt_shortcut (); + void interrupt_editor (); + bool is_editor_interrupted (); + void clear_editor_interrupt (); bool try_shortcut (string comb); tree kbd (string s); tree kbd_shortcut (string s); diff --git a/src/Edit/Interface/edit_keyboard.cpp b/src/Edit/Interface/edit_keyboard.cpp index 7997903d43..d84cd0f9a5 100644 --- a/src/Edit/Interface/edit_keyboard.cpp +++ b/src/Edit/Interface/edit_keyboard.cpp @@ -61,6 +61,7 @@ edit_interface_rep::set_input_normal () { } prev_math_comb= ""; hide_math_completion_popup (); + editor_interrupted= false; } bool @@ -99,6 +100,21 @@ edit_interface_rep::interrupt_shortcut () { sh_mark= 0; } +void +edit_interface_rep::interrupt_editor () { + editor_interrupted= true; +} + +bool +edit_interface_rep::is_editor_interrupted () { + return editor_interrupted; +} + +void +edit_interface_rep::clear_editor_interrupt () { + editor_interrupted= false; +} + bool edit_interface_rep::try_shortcut (string comb) { int status; diff --git a/src/Edit/Replace/edit_search.cpp b/src/Edit/Replace/edit_search.cpp index 445cfe2985..7f2c150f7b 100644 --- a/src/Edit/Replace/edit_search.cpp +++ b/src/Edit/Replace/edit_search.cpp @@ -12,6 +12,7 @@ #include "Interface/edit_interface.hpp" #include "Replace/edit_replace.hpp" #include "analyze.hpp" +#include "gui.hpp" #include #include @@ -130,6 +131,7 @@ path edit_replace_rep::search_previous_compound (path init, string which) { path p= init; while (true) { + if (gui_interrupted (true)) return init; if (p == rp) return init; if (last_item (p) == 0) p= path_up (p); else { @@ -149,6 +151,7 @@ path edit_replace_rep::search_next_compound (path init, string which) { path p= init; while (true) { + if (gui_interrupted (true)) return init; if (p == rp) return init; if (last_item (p) == (N (subtree (et, path_up (p))) - 1)) p= path_up (p); else { @@ -344,6 +347,13 @@ void edit_replace_rep::next_match (bool forward) { // cout << "Next match at " << search_at << "\n"; while (true) { + if (gui_interrupted (true)) { + search_at= rp; + set_selection (tp, tp); + notify_change (THE_SELECTION); + set_message ("Search interrupted", ""); + return; + } if (search_at == rp) { set_selection (tp, tp); notify_change (THE_SELECTION); @@ -438,7 +448,7 @@ edit_replace_rep::search_keypress (string s) { search_stop (); return false; } - else if ((s == "C-c") || (s == "C-g")) search_stop (); + else if ((s == "C-c") || (s == "C-g") || (s == "escape")) search_stop (); else if ((s == "next") || (s == "previous")) { if (search_what == "") { tree t= selection_raw_get ("search"); @@ -549,6 +559,13 @@ edit_replace_rep::replace_keypress (string s) { } else if (s == "a" || s == "!") { while (search_at != rp) { + if (gui_interrupted (true)) { + set_message (concat ("Replaced ", as_string (nr_replaced), + " occurrences (interrupted)"), + "replace"); + set_input_normal (); + return true; + } nr_replaced++; go_to (copy (search_end)); cut (search_at, search_end); diff --git a/src/Edit/Replace/edit_spell.cpp b/src/Edit/Replace/edit_spell.cpp index 0d2159dd25..73ce86d5e4 100644 --- a/src/Edit/Replace/edit_spell.cpp +++ b/src/Edit/Replace/edit_spell.cpp @@ -12,6 +12,7 @@ #include "Interface/edit_interface.hpp" #include "Replace/edit_replace.hpp" #include "analyze.hpp" +#include "gui.hpp" #ifdef USE_PLUGIN_ISPELL #ifdef MACOSX_EXTENSIONS @@ -124,6 +125,11 @@ message_ispell (tree t) { void edit_replace_rep::spell_next () { while (true) { + if (gui_interrupted (true)) { + spell_end (); + set_message ("Spell check interrupted", "correct text"); + return; + } if (path_inf (spell_end_p, search_at)) search_at= rp; if (search_at == rp) { spell_end (); diff --git a/src/Edit/editor.hpp b/src/Edit/editor.hpp index 61f8899fac..cd53e35906 100644 --- a/src/Edit/editor.hpp +++ b/src/Edit/editor.hpp @@ -180,6 +180,9 @@ class editor_rep : public simple_widget_rep { virtual bool in_replace_mode () = 0; virtual bool in_spell_mode () = 0; virtual void interrupt_shortcut () = 0; + virtual void interrupt_editor () = 0; + virtual bool is_editor_interrupted () = 0; + virtual void clear_editor_interrupt () = 0; virtual bool kbd_get_command (string cmd_s, string& help, command& cmd)= 0; virtual void key_press (string key) = 0; virtual void emulate_keyboard (string keys, string action= "") = 0; diff --git a/src/Scheme/Glue/glue_editor.lua b/src/Scheme/Glue/glue_editor.lua index 143d8b1c86..67adbf2940 100644 --- a/src/Scheme/Glue/glue_editor.lua +++ b/src/Scheme/Glue/glue_editor.lua @@ -1821,6 +1821,21 @@ function main() "string" } }, + { + scm_name = "editor-interrupt", + cpp_name = "interrupt_editor", + ret_type = "void" + }, + { + scm_name = "editor-interrupted?", + cpp_name = "is_editor_interrupted", + ret_type = "bool" + }, + { + scm_name = "editor-clear-interrupt", + cpp_name = "clear_editor_interrupt", + ret_type = "void" + }, -- sessions {