Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions TeXmacs/progs/generic/generic-edit.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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))))
Comment on lines +233 to +248
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general-cancel does not appear to signal the new interrupt mechanism (it never calls editor-interrupt). Given search-widgets.scm now relies on editor-interrupted? to stop toolbar replace-all, it would help to set the interrupt flag here (or otherwise ensure Ctrl+G sets it) so long-running operations can observe the cancel request.

Copilot uses AI. Check for mistakes.

#|
ocr-paste
剪贴板中的内容是图像时,OCR并插入已识别的内容到当前光标处
Expand Down
4 changes: 2 additions & 2 deletions TeXmacs/progs/generic/generic-kbd.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
10 changes: 6 additions & 4 deletions TeXmacs/progs/generic/search-widgets.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace-all now stops when (editor-interrupted?) becomes true, but nothing in this PR appears to ever set that flag during execution: interrupt_editor is only defined on the C++ side and exposed via editor-interrupt, yet there are no callers of editor-interrupt in Scheme and no C++ call sites of interrupt_editor. As a result, Ctrl+G is unlikely to interrupt toolbar replace-all as intended. Consider setting the interrupt flag from the Ctrl+G handler (e.g., have general-cancel call editor-interrupt early), or alternatively make replace-all poll gui_interrupted(true) via a glue binding instead of editor-interrupted?.

Suggested change
(while (and (not (editor-interrupted?)) (replace-next by))
(while (and (not (editor-interrupted?))
(not (gui-interrupted? #t))
(replace-next by))

Copilot uses AI. Check for mistakes.
(perform-search*))
(end-editing))
(perform-search*)
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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)
Expand Down
56 changes: 56 additions & 0 deletions devel/201_85.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions src/Edit/Interface/edit_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
4 changes: 4 additions & 0 deletions src/Edit/Interface/edit_interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on editor_interrupted says it is “set by Ctrl+G, checked by long-running loops”, but in the current changes the long-running C++ loops are checking gui_interrupted(true) (and Scheme replace-all checks editor-interrupted?). Either update the comment to match actual usage, or switch the loops to check is_editor_interrupted() if that was the intended mechanism.

Suggested change
bool editor_interrupted; // set by Ctrl+G, checked by long-running loops
bool editor_interrupted; // set by Ctrl+G to signal an editor interruption

Copilot uses AI. Check for mistakes.

protected:
SI last_x, last_y;
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions src/Edit/Interface/edit_keyboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ edit_interface_rep::set_input_normal () {
}
prev_math_comb= "";
hide_math_completion_popup ();
editor_interrupted= false;
}

bool
Expand Down Expand Up @@ -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;
Expand Down
19 changes: 18 additions & 1 deletion src/Edit/Replace/edit_search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "Interface/edit_interface.hpp"
#include "Replace/edit_replace.hpp"
#include "analyze.hpp"
#include "gui.hpp"

#include <moebius/drd/drd_mode.hpp>
#include <moebius/drd/drd_std.hpp>
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions src/Edit/Replace/edit_spell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ();
Expand Down
3 changes: 3 additions & 0 deletions src/Edit/editor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions src/Scheme/Glue/glue_editor.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down