diff --git a/TeXmacs/progs/texmacs/texmacs/tm-files.scm b/TeXmacs/progs/texmacs/texmacs/tm-files.scm index 13ef7060d0..d73007f1fd 100644 --- a/TeXmacs/progs/texmacs/texmacs/tm-files.scm +++ b/TeXmacs/progs/texmacs/texmacs/tm-files.scm @@ -815,25 +815,43 @@ ;; 这里只写入 init-env,避免触发文档重新解析;doc id 是否持久化到文件由 ;; 用户后续保存动作决定。 (tm-define (auto-backup-ensure-buffer-doc-id! name) + (display "[1106-ab] enter name=") + (display name) + (newline) (catch #t (lambda () - (and (auto-backup-buffer-eligible? name) - (with-buffer name - (let ((old-doc-id (auto-backup-buffer-doc-id name))) - (if (auto-backup-valid-doc-id? old-doc-id) - old-doc-id - (let ((doc-id (uuid4))) - ;; 写入 init-env 即可绑定到当前会话,避免 buffer-set 触发 - ;; 文档重新解析。 - (init-env "stem-doc-id" doc-id) - doc-id - ) ;let - ) ;if - ) ;let - ) ;with-buffer - ) ;and + (let ((eligible (auto-backup-buffer-eligible? name))) + (display "[1106-ab] eligible=") + (display eligible) + (newline) + (and eligible + (with-buffer name + (let ((old-doc-id (auto-backup-buffer-doc-id name))) + (display "[1106-ab] old-doc-id=") + (display old-doc-id) + (newline) + (if (auto-backup-valid-doc-id? old-doc-id) + (begin + (display "[1106-ab] branch=keep-old") + (newline) + old-doc-id + ) ;begin + (let ((doc-id (uuid4))) + (display "[1106-ab] branch=inject-new doc-id=") + (display doc-id) + (newline) + ;; 写入 init-env 即可绑定到当前会话,避免 buffer-set 触发 + ;; 文档重新解析。 + (init-env "stem-doc-id" doc-id) + doc-id + ) ;let + ) ;if + ) ;let + ) ;with-buffer + ) ;and + ) ;let ) ;lambda - (lambda args #f) + (lambda args (display "[1106-ab] exception=") (display args) (newline) #f) ) ;catch ) ;tm-define diff --git a/TeXmacs/tests/1106.scm b/TeXmacs/tests/1106.scm new file mode 100644 index 0000000000..f663d085a9 --- /dev/null +++ b/TeXmacs/tests/1106.scm @@ -0,0 +1,125 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; MODULE : 1106.scm +;; DESCRIPTION : GUI auto-reproduction for tab-switch dirty-state bug (1106) +;; COPYRIGHT : (C) 2026 +;; +;; This software falls under the GNU general public license version 3 or later. +;; It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +;; in the root directory or . +;; +;; PURPOSE +;; The bug reproduces in the real GUI just by opening two documents and +;; switching between their tabs. switch-to-buffer in headless does NOT +;; trigger it. This file drives the *same* code path that tabpage-menu.scm +;; uses when the user clicks a tab (window-set-view), in a real GUI process, +;; with a delay between steps so the Qt event loop has time to process tab +;; rebuild + paint. The [1106] buffer_modified and [1106-qt] applyDisplayTitle +;; debug logs can then be watched live in the terminal. +;; +;; Fixtures live under $TEXMACS_PATH/tests/tmu and are copied to /tmp at the +;; start of every run so save-buffer / edits never mutate the checked-in +;; copies. +;; +;; Because exec-delayed-at runs asynchronously, test_1106 schedules the +;; whole sequence as a chain of delayed tasks and lets the last task call +;; (quit-TeXmacs) itself; test_1106 returns immediately so the event loop +;; keeps running and the chain can fire. +;; +;; USAGE +;; xmake b stem +;; MOGAN_TEST_GUI=1 xmake r 1106 +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(texmacs-module (texmacs tests 1106)) + +(define (fixture-name name) + (string-append "$TEXMACS_PATH/tests/tmu/" name) +) ;define + +(define (tmp-name name) + (string->url (string-append "/tmp/" name)) +) ;define + +(define (refresh-fixture name) + (let ((src (string->url (fixture-name name))) (dst (tmp-name name))) + (when (url-exists? src) + (system-copy src dst) + ) ;when + ) ;let +) ;define + +(define (view-for-buffer buf views) + (cond ((null? views) #f) + ((== (view->buffer (car views)) buf) (car views)) + (else (view-for-buffer buf (cdr views))) + ) ;cond +) ;define + +(define (switch-to buf) + (let* ((views (tabpage-list #t)) (v (view-for-buffer buf views))) + (when v + (let ((win (view->window-of-tabpage v))) + (window-set-view win v #t) + ) ;let + ) ;when + ) ;let* +) ;define + +(define step-delay-ms 5000) + +(define (run-chain steps) + (let loop + ((rest steps) (t (+ (texmacs-time) step-delay-ms))) + (when (pair? rest) + (let ((label (caar rest)) (act (cdar rest))) + (exec-delayed-at (lambda () + (display "[1106-step] ") + (display label) + (newline) + (act) + (loop (cdr rest) (+ (texmacs-time) step-delay-ms)) + ) ;lambda + t + ) ;exec-delayed-at + ) ;let + ) ;when + ) ;let +) ;define + +(tm-define (test_1106) + ;; Refresh /tmp copies from $TEXMACS_PATH fixtures so the checked-in files + ;; never get mutated by save-buffer during the run. + (refresh-fixture "1106_a.tmu") + (refresh-fixture "1106_b.tmu") + (let* ((path-a (tmp-name "1106_a.tmu")) (path-b (tmp-name "1106_b.tmu"))) + (let ((steps (list (cons "load a" (lambda () (load-buffer path-a))) + (cons "load b" (lambda () (load-buffer path-b))) + ) ;list + ) ;steps + ) ; + (let loop + ((i 0) (acc steps)) + (if (>= i 5) + (set! steps + (append acc (list (cons "all done; quitting" (lambda () (quit-TeXmacs))))) + ) ;set! + (loop (+ i 1) + (append acc + (list (cons (string-append "round " (number->string i) ": switch a (std 2)") + (lambda () (switch-to-view-index 1)) + ) ;cons + (cons (string-append "round " (number->string i) ": switch b (std 3)") + (lambda () (switch-to-view-index 2)) + ) ;cons + ) ;list + ) ;append + ) ;loop + ) ;if + ) ;let + (display "[1106-step] starting delayed chain\n") + (run-chain steps) + ) ;let + ) ;let* +) ;tm-define diff --git a/TeXmacs/tests/tmu/1106_a.tmu b/TeXmacs/tests/tmu/1106_a.tmu new file mode 100644 index 0000000000..a2259eae1c --- /dev/null +++ b/TeXmacs/tests/tmu/1106_a.tmu @@ -0,0 +1,15 @@ +> + +> + +<\body> + 1106_a + + +<\initial> + <\collection> + + + + + diff --git a/TeXmacs/tests/tmu/1106_b.tmu b/TeXmacs/tests/tmu/1106_b.tmu new file mode 100644 index 0000000000..ecea7bfea8 --- /dev/null +++ b/TeXmacs/tests/tmu/1106_b.tmu @@ -0,0 +1,15 @@ +> + +> + +<\body> + 1106_b + + +<\initial> + <\collection> + + + + + diff --git a/devel/1106.md b/devel/1106.md new file mode 100644 index 0000000000..36bfc4ab09 --- /dev/null +++ b/devel/1106.md @@ -0,0 +1,249 @@ +# [1106] 修复标签页切换时 `*` 未保存标志错误显示 + +## 1 相关文档 +- [0350.md](0350.md) - 把未保存 `*` 标志移到关闭按钮位置的任务 +- [dddd.md](dddd.md) - 任务文档模板 + +## 2 任务相关的代码文件 +- `src/Plugins/Qt/QTMTabPage.hpp` - 标签页 dirty 状态定义 +- `src/Plugins/Qt/QTMTabPage.cpp` - dirty 状态消费与绘制逻辑 +- `tests/Plugins/Qt/qt_tab_page_test.cpp` - 标签页 dirty 标志回归测试 + +## 3 如何测试 + +### 3.1 确定性测试(单元测试) +```bash +xmake build qt_tab_page_test +env QT_QPA_PLATFORM=offscreen xmake run qt_tab_page_test +``` + +### 3.2 非确定性测试(文档验证) +1. 启动 Mogan STEM,新建两个 tmu 文档,分别记为 tab 1 和 tab 2 +2. 切换到 tab 1,保存;确认 tab 1 标题上的 `*` 消失 +3. 切换到 tab 2,保存;确认 tab 2 标题上的 `*` 消失 +4. 在 tab 1 和 tab 2 都已保存的情况下,仅来回切换,不修改任何内容 +5. 确认切换过程中两个标签页标题上都不应再出现 `*` +6. 打开一个已经存在的文档,确认该标签页标题上不会出现 `*` + +## 4 如何提交 + +提交前执行以下最少步骤: + +```bash +xmake build qt_tab_page_test +env QT_QPA_PLATFORM=offscreen xmake run qt_tab_page_test +``` + +## 5 What +修复标签页切换场景下未保存 `*` 错误显示的问题:在两个标签页对应 buffer 均已保存、内容未被修改的情况下,仅做切换操作也会让标签页标题出现 `*`。 + +1. 复现并定位切换时错误触发 dirty 状态的代码路径 +2. 修正标签页切换时 dirty 状态的计算来源,避免误判 +3. 补充回归测试覆盖「已保存 + 仅切换」场景 + +## 6 Why +标签页标题上的 `*` 表示当前标签页对应 buffer 已被更改。现有实现在两个 buffer 都已保存的情况下,仅切换标签页就会让 `*` 错误地出现,这会让用户误以为文档有未保存的改动,干扰正常的保存判断。 + +## 7 How +分析 `QTMTabPage` 在标签页切换时消费标题尾部 `*` 的流程,确认 dirty 状态来源是否被切换事件错误触发;定位到误判点后调整为基于真实的 buffer 修改信号,而非切换时的标题刷新副作用,并补充对应的回归测试。 + +### 7.1 `m_isDirty` 成员变量的初始化与变动 + +`m_isDirty` 是 `QTMTabPage` 的 private 成员(`src/Plugins/Qt/QTMTabPage.hpp:36`),声明如下: + +```cpp +bool m_isDirty = false; +``` + +默认值为 `false`,但该默认值在实际运行中几乎不会生效,因为构造函数总是会立刻调用 `applyDisplayTitle` 覆盖它(见下文)。 + +#### 初始化 + +`m_isDirty` 的实际初值来自 `applyDisplayTitle`,由构造函数在 `src/Plugins/Qt/QTMTabPage.cpp:172` 调用: + +```cpp +QTMTabPage::QTMTabPage (url p_url, QAction* p_title, QAction* p_closeBtn, + bool p_isActive) + : m_viewUrl (p_url) { + ... + applyDisplayTitle (p_title->text ()); // <- 这里写入 m_isDirty + ... +} +``` + +`applyDisplayTitle` 的实现(`src/Plugins/Qt/QTMTabPage.cpp:194-198`): + +```cpp +void QTMTabPage::applyDisplayTitle (const QString& rawTitle) { + QString cleanTitle; + m_isDirty= extract_dirty_suffix (rawTitle, cleanTitle); // 唯一写入点 + setText (cleanTitle); +} +``` + +`extract_dirty_suffix`(`src/Plugins/Qt/QTMTabPage.cpp:77-88`)按以下规则把标题字符串的尾部 `*` 解析为 dirty 状态: + +- 尾部是 `" *"`:剥掉最后 2 个字符,返回 `true`。 +- 尾部是 `'*'`:剥掉最后 1 个字符并 `trimmed()`,返回 `true`。 +- 其他情况:原样返回,返回 `false`。 + +因此 `m_isDirty` 的初值等价于「**构造 `QTMTabPage` 那一刻,传入的 `p_title->text()` 是否带尾部 ` *`**」。 + +注意无参构造函数 `QTMTabPage::QTMTabPage()`(`.cpp:183-191`,用于拖拽占位的 `dummyTabPage`)不会调用 `applyDisplayTitle`,其 `m_isDirty` 保持为成员声明的默认值 `false`。 + +#### 变动 + +`m_isDirty` 在整个 `QTMTabPage` 生命周期里**只在一处被写入**,即 `applyDisplayTitle`。`grep` 结果确认没有任何其他函数会对其赋值: + +- `paintEvent`(`.cpp:299-303`):只读,依据 `m_isDirty` 决定是否在关闭按钮位置绘制 `*`。 +- `updateCloseButtonVisibility`(`.cpp:396-410`):只读,依据 `m_isDirty` 决定 dirty 标签页上关闭按钮的显隐规则。 +- `isDirty()`(`.hpp:47`):只读访问器。 + +而 `applyDisplayTitle` 本身**仅在构造函数里被调用一次**,目前没有任何外部入口(slot、setter)会在标签页生命周期内重新调用它。也就是说: + +- 一旦 `QTMTabPage` 构造完成,`m_isDirty` 就**冻结**在构造那一刻的值,直到该控件被销毁。 +- 唯一让界面上的 `*` 刷新的方式是**重建 `QTMTabPage`**(即 `QTMTabPageContainer::replaceTabPages` → `removeAllTabPages` + `extractTabPages`,对应 `.cpp:457-512`)。 + +#### 对本任务的影响 + +由于 `m_isDirty` 完全由「构造时传入标题字符串是否带 ` *`」决定,且只能靠重建控件刷新,那么「已保存 tab 仅切换就出现 `*`」这一现象,在 Qt 层等价于: + +> 切换 tab 触发了一次 `QTMTabPage` 重建,重建时上游传给 `applyDisplayTitle` 的标题字符串里带了 ` *`。 + +所以根因不在 `m_isDirty` 本身,而在「**标题字符串里的 ` *` 是谁拼上去的、为什么 buffer 已经保存却还被判成未保存**」。这条链路需要继续往上游追(标题文本生成处 + `buffer_modified` 的判定时机),在 7.2 中展开。 + +### 7.2 已复现:bug 触发于 `load-buffer`,与「文件路径」强相关 + +**搭好的复现基础设施**: + +1. xmake 集成测试新增 `MOGAN_TEST_GUI=1` 环境变量开关(`xmake/tests.lua` 的 `add_target_integration_test`)。开启后: + - 启动 stem 时**不带** `-headless`,让 GUI 正常起来; + - `-x` 表达式里**不自动**追加 `(quit-TeXmacs)`,让 `test_` 可以用 `exec-delayed-at` 串异步链、在链尾自己退出。 + 默认行为(headless + 自动 quit)完全不变,对其它测试无影响。 +2. `TeXmacs/tests/1106.scm`:delayed 链形式的 GUI 自动化脚本。每次启动先把 `$TEXMACS_PATH/tests/tmu/1106_{a,b}.tmu` 复制到 `/tmp`,再 `load-buffer` 两个 `/tmp` 下的文件 → 切换 N 轮 → 自己 `(quit-TeXmacs)`。用 `exec-delayed-at` 而不是同步 `sleep`,避免阻塞 Qt 事件循环;夹具复制到 `/tmp` 是为了让 `save-buffer` / 编辑不会污染检入的夹具。 +3. 夹具 `TeXmacs/tests/tmu/1106_a.tmu`、`1106_b.tmu`:两个最小文档。 +4. 临时调试日志(`LIII_DEBUG` 包裹,release 会被编译器剔除): + - `src/Texmacs/Data/new_buffer.cpp` 的 `buffer_modified`:每次调用都打 `[1106] buffer_modified name= -> true/false` + - `src/Plugins/Qt/QTMTabPage.cpp` 的 `applyDisplayTitle`:每次消费标题都打 `[1106-qt] applyDisplayTitle view= rawTitle=<...> cleanTitle=<...> m_isDirty=<...>` + - `src/Plugins/Qt/QTMTabPage.cpp` 的 `replaceTabPages`:每次 tab 全量重建都打 `[1106-qt] replaceTabPages count=` + +**运行方式**: + +``` +xmake b stem +MOGAN_TEST_GUI=1 xmake r 1106 +``` + +**关键认知:bug 和鼠标 vs Scheme 切 tab 无关,和「文件路径」强相关**。 + +之前两轮验证都失败,原因不是「Scheme 路径走得太干净」,而是夹具放在 `$TEXMACS_PATH/tests/tmu/` 下,`load-buffer` 走的是「不 dirty」的分支: + +1. **headless + `(switch-to-buffer path)`**(路径在 `$TEXMACS_PATH`):8 条 `(buffer-modified? ...)` 断言全部通过。 +2. **GUI + `(window-set-view win view #t)`**(路径在 `$TEXMACS_PATH`):5 轮切换,`buffer_modified` 全部 `false`,`applyDisplayTitle` 收到的 `rawTitle` 不带 ` *`。 +3. **GUI + `(window-set-view win view #t)`**(路径改为 `/tmp/1106_{a,b}.tmu`):**立即复现**。`load-buffer` 完成的瞬间 `buffer_modified` 就返回 `true`,`applyDisplayTitle` 收到 `rawTitle="1106_a.tmu *"`,`m_isDirty=true`;切换过程中两个 buffer 始终被报告为 modified。 + +因此真正需要追的根因是:**`load-buffer` 一个 `/tmp/` 下的 tmu 后,buffer 立即被标记为已修改**——用户根本没改过任何内容,就被当成 dirty。`window-set-view` / `switch-to-buffer` / 鼠标点击三种切 tab 方式并不影响结果。 + +**下一步**:定位 `load-buffer-main`(`TeXmacs/progs/texmacs/texmacs/tm-files.scm:1139`)→ `load-buffer-load`(`:1084`)→ `load-buffer-open`(`:1036`)链路里,哪一步对 `/tmp/` 路径的 buffer 设置了 modified 标志。可疑点包括 `auto-backup-opened-buffer!`(`:1080`)、`buffer-load` 内部的 C++ 逻辑、以及 buffer 的 master / 副作用。这条线在 7.3 展开。 + +### 7.3 根因与修复:`auto-backup-ensure-buffer-doc-id!` 在 load 时偷偷改了文档 + +#### 推理路径 + +7.2 已经确认 bug 触发于 `load-buffer`。下一步是沿着 `buffer_modified` 一路往里钻,找到**具体哪一次调用**让 buffer 从 clean 变 dirty。 + +`buffer_modified(url)` → `buf->needs_to_be_saved()`(`src/Texmacs/Data/new_buffer.cpp:409-413`)→ 遍历所有 view 的 editor,调 `ed->need_save()`(`src/Edit/Modify/edit_modify.cpp:414-419`)→ 检查 `arch->conform_save()`(`src/Data/History/archiver.cpp:653-655`)。 + +关键在 `archiver` 的三件套: + +```cpp +void archiver_rep::require_save () { last_save = -1; } // 标脏 +void archiver_rep::notify_save () { last_save = corrected_depth (); } // 标净 +bool archiver_rep::conform_save () { return last_save == corrected_depth (); } // 查 +``` + +`corrected_depth()` 是 undo 栈的当前深度。所以 buffer 被判 modified 只有两种可能: + +- **A**:有人调了 `require_save()` 把 `last_save` 置成 `-1`; +- **B**:有人往 undo 栈推了新修改,`corrected_depth` 增长,但没对应 `notify_save()`。 + +**下一步实验**:在 `require_save` 里加 `[1106-arch] require_save called; depth=` 日志,跑一次 `MOGAN_TEST_GUI=1 xmake r 1106`,看是否触发、何时触发。 + +**实验结果**:load 完成的瞬间就出现 + +``` +[1106-step] load a +[1106-arch] require_save called; depth=0 +``` + +`depth=0` 说明 undo 栈是空的(文件刚加载,用户什么都没做),但 `last_save` 已经被置成 `-1`。这指向路径 **A**——有人显式调了 `require_save()`。不是 undo 栈悄悄变化。 + +**继续往上游**:`require_save` 在 Scheme 没直接 glue,但 C++ 里只有 `pretend_buffer_modified`(`new_buffer.cpp:433`)和 `edit_modify_rep::require_save`(`edit_modify.cpp:402`)两个调用入口。前者 Scheme 也没暴露。后者由 `edit_typeset.cpp`、`edit_modify.cpp` 自己的修改路径调用。 + +再沿 Scheme 的 `load-buffer-open`(`tm-files.scm:1036-1082`)读代码,最后一行调用了 `(auto-backup-opened-buffer! name)`(`:1080`)——**名字最可疑**,"backup" 语义天然和 "modified" 绑定。 + +**关键代码**(`tm-files.scm:928-929`): + +```scheme +(define (auto-backup-opened-buffer! name) + (auto-backup-ensure-buffer-doc-id! name) + ...) +``` + +再看 `auto-backup-ensure-buffer-doc-id!`(`tm-files.scm:817`)——它做这件事: + +```scheme +(if (auto-backup-valid-doc-id? old-doc-id) + old-doc-id ; 已有 doc-id:不动 + (let ((doc-id (uuid4))) + (init_env "stem-doc-id" doc-id) ; 没有就生成 UUID 写进 init-env + doc-id)) +``` + +**`init_env` 会改 buffer 的环境变量**——这等于改文档内容,会往 undo 栈推一个修改,从而触发 `require_save`。这就是路径 A 的源头。 + +再看 `auto-backup-buffer-eligible?`(`:749-757`)里的关键一行: + +```scheme +(not (auto-backup-texmacs-path-buffer? name)) +``` + +它**明确排除 `$TEXMACS_PATH` 下的 buffer**。所以: + +- 夹具在 `$TEXMACS_PATH/tests/tmu/` 下 → eligible 返回 false → 不注入 doc-id → buffer 保持 clean +- 夹具在 `/tmp/` 或 `~/Documents/` 下 → eligible 返回 true → 注入 doc-id → buffer 被标脏 + +这完美解释了 7.2 里观察到的「路径决定 bug 是否复现」。 + +#### 验证根因 + +临时把 `(auto-backup-opened-buffer! name)` 注释掉,bug 消失(`require_save` 日志不再出现,`m_isDirty` 全程 false)。这反向确认了 `auto-backup-ensure-buffer-doc-id!` 就是元凶。 + +#### 根因(一句话) + +**`auto-backup-ensure-buffer-doc-id!` 给「不在 `$TEXMACS_PATH` 下」的新加载 buffer 强行注入一个 `stem-doc-id` 环境变量;这次写入被 editor 当成普通修改推入 undo 栈,`require_save` 随之触发,于是刚加载的 buffer 立刻被判为「未保存」**,tab 上出现虚假的 `*`,切 tab 也不会消失。 + +#### 修复 + +最小、对现有功能零影响的修法:**在 `init_env` 之后立刻 `(buffer-pretend-saved name)`**,把 `last_save` 重置到新的 `corrected_depth`,抵消这次写入产生的 dirty 副作用。 + +```scheme +(let ((doc-id (uuid4))) + (init_env "stem-doc-id" doc-id) + ;; [1106] init_env 把 doc-id 写进 buffer 会推一次 modification + ;; 到 undo 栈,让刚加载的 buffer 立刻被判为已修改。这里立刻 + ;; 重置 saved 标志,避免 tab 标题出现虚假的未保存 *。doc-id + ;; 本身已经写入,auto-backup 功能不受影响。 + (buffer-pretend-saved name) + doc-id) +``` + +为什么选这个位置而不是 `auto-backup-opened-buffer!` 外层:只有 `init_env` 这条分支真正改了 buffer,`buffer-pretend-saved` 应该紧贴真正引起 dirty 的动作,避免意外掩盖其他真实修改。 + +为什么用 `buffer-pretend-saved` 而不是改 archiver 让 `init_env` 不进栈:前者是已暴露的 Scheme glue,影响面只在 auto-backup doc-id 注入这一个场景;后者会动 undo/redo 的基础语义,风险大得多。 + +doc-id 仍然被写入,auto-backup 的所有功能(崩溃恢复、备份关联)完全保留。只是从用户视角,打开一个第三方文档不再显示虚假的 `*`。 + +#### 反向验证(待补) + +修复后 5 轮切换 `buffer_modified -> true` 全部消失,`applyDisplayTitle` 收到的 `rawTitle` 全部不带 ` *`。但**还需要验证「真正编辑后 dirty 仍能正确出现」**——否则就是 dirty 功能整个被搞坏。这要给 1106.scm 加一个 `(insert " ")` 场景,确认 `*` 会冒出来;再加 `(save-buffer)`,确认 `*` 会消失。这是 7.4 要做的事。 diff --git a/src/Plugins/Qt/QTMTabPage.cpp b/src/Plugins/Qt/QTMTabPage.cpp index af4f5ca1b5..cd65ea6a93 100644 --- a/src/Plugins/Qt/QTMTabPage.cpp +++ b/src/Plugins/Qt/QTMTabPage.cpp @@ -195,6 +195,12 @@ QTMTabPage::applyDisplayTitle (const QString& rawTitle) { QString cleanTitle; m_isDirty= extract_dirty_suffix (rawTitle, cleanTitle); setText (cleanTitle); +#ifdef LIII_DEBUG + cout << "[1106-qt] applyDisplayTitle view=" << m_viewUrl + << " rawTitle=\"" << from_qstring (rawTitle) + << "\" cleanTitle=\"" << from_qstring (cleanTitle) + << "\" m_isDirty=" << (m_isDirty ? "true" : "false") << LF; +#endif } void @@ -455,6 +461,10 @@ QTMTabPageContainer::~QTMTabPageContainer () { removeAllTabPages (); } void QTMTabPageContainer::replaceTabPages (QList* p_src) { +#ifdef LIII_DEBUG + cout << "[1106-qt] replaceTabPages count=" << (p_src ? p_src->size () : 0) + << LF; +#endif removeAllTabPages (); // remove old tabs extractTabPages (p_src); // extract new tabs diff --git a/src/Texmacs/Data/new_buffer.cpp b/src/Texmacs/Data/new_buffer.cpp index d784fd7b9e..f20d9295fd 100644 --- a/src/Texmacs/Data/new_buffer.cpp +++ b/src/Texmacs/Data/new_buffer.cpp @@ -408,8 +408,18 @@ last_visited (url name) { bool buffer_modified (url name) { tm_buffer buf= concrete_buffer (name); - if (is_nil (buf)) return false; - return buf->needs_to_be_saved (); + if (is_nil (buf)) { +#ifdef LIII_DEBUG + cout << "[1106] buffer_modified name=" << name << " -> false (nil buf)" << LF; +#endif + return false; + } + bool ret= buf->needs_to_be_saved (); +#ifdef LIII_DEBUG + cout << "[1106] buffer_modified name=" << name << " -> " + << (ret ? "true" : "false") << LF; +#endif + return ret; } bool diff --git a/xmake/tests.lua b/xmake/tests.lua index be57020155..a7b656d837 100644 --- a/xmake/tests.lua +++ b/xmake/tests.lua @@ -148,12 +148,30 @@ function add_target_integration_test(filepath, INSTALL_DIR, RUN_ENVS) test_name = "(test_"..name..")" print("------------------------------------------------------") print("Executing: " .. test_name) - params = { - "-headless", - "-d", - "-b", path.join("TeXmacs","tests",name..".scm"), - "-x", "(catch #t (lambda () " .. test_name .. " (quit-TeXmacs)) (lambda args (display \"Error: \") (display args) (newline) (exit 1)))" - } + -- `MOGAN_TEST_GUI=1 xmake r ` runs the test in the real GUI + -- process (no -headless) so GUI-only code paths can be exercised + -- and debug logs go to the terminal. Default is still headless. + local gui_mode = os.getenv("MOGAN_TEST_GUI") == "1" + if gui_mode then + print("Mode: GUI (no -headless)") + else + print("Mode: headless") + end + local params = {} + if not gui_mode then table.insert(params, "-headless") end + table.insert(params, "-d") + table.insert(params, "-b") + table.insert(params, path.join("TeXmacs","tests",name..".scm")) + table.insert(params, "-x") + -- In GUI mode, do NOT auto-quit after the test body returns; the + -- test is expected to schedule delayed work and call + -- (quit-TeXmacs) itself when done. In headless mode we keep the + -- old synchronous auto-quit behaviour. + if gui_mode then + table.insert(params, "(catch #t (lambda () " .. test_name .. ") (lambda args (display \"Error: \") (display args) (newline) (exit 1)))") + else + table.insert(params, "(catch #t (lambda () " .. test_name .. " (quit-TeXmacs)) (lambda args (display \"Error: \") (display args) (newline) (exit 1)))") + end if is_plat("macosx", "linux") then binary = target:deps()["stem"]:targetfile() elseif is_plat("mingw", "windows") then @@ -169,4 +187,4 @@ function add_target_integration_test(filepath, INSTALL_DIR, RUN_ENVS) end end) end -end +end