From 3886b4c90fce12c7f03566bc2e5ae19e0a4f755d Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 19:53:45 +0800 Subject: [PATCH 01/10] wip --- devel/1106.md | 113 ++++++++++++++++++++++++++++++++++ src/Plugins/Qt/QTMTabPage.cpp | 5 ++ 2 files changed, 118 insertions(+) create mode 100644 devel/1106.md diff --git a/devel/1106.md b/devel/1106.md new file mode 100644 index 0000000000..f143c56cd5 --- /dev/null +++ b/devel/1106.md @@ -0,0 +1,113 @@ +# [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 中展开。 diff --git a/src/Plugins/Qt/QTMTabPage.cpp b/src/Plugins/Qt/QTMTabPage.cpp index af4f5ca1b5..a11b76c602 100644 --- a/src/Plugins/Qt/QTMTabPage.cpp +++ b/src/Plugins/Qt/QTMTabPage.cpp @@ -195,6 +195,11 @@ QTMTabPage::applyDisplayTitle (const QString& rawTitle) { QString cleanTitle; m_isDirty= extract_dirty_suffix (rawTitle, cleanTitle); setText (cleanTitle); +#ifdef LIII_DEBUG + cout << "[1106] applyDisplayTitle rawTitle=\"" << from_qstring (rawTitle) + << "\" cleanTitle=\"" << from_qstring (cleanTitle) + << "\" m_isDirty=" << (m_isDirty ? "true" : "false") << LF; +#endif } void From c86a04c0e6be25cb0df13cef14e2c3ee59ffff87 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 20:26:09 +0800 Subject: [PATCH 02/10] =?UTF-8?q?[1106]=20=E8=A1=A5=E5=85=85=207.1/7.2?= =?UTF-8?q?=EF=BC=9Am=5FisDirty=20=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F?= =?UTF-8?q?=E4=B8=8E=20GUI=20=E8=87=AA=E5=8A=A8=E5=8C=96=E5=A4=8D=E7=8E=B0?= =?UTF-8?q?=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 7.1 详述 m_isDirty 的初始化与变动(唯一写入点 applyDisplayTitle) - 7.2 记录已验证结论:Scheme window-set-view 不能复现 bug,真实鼠标 点击走的副作用链才是触发路径;已搭好 MOGAN_TEST_GUI=1 + delayed chain 的 GUI 自动化骨架作为下一步 QTest 的工作基础 Co-Authored-By: Claude Opus 4.7 --- devel/1106.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/devel/1106.md b/devel/1106.md index f143c56cd5..0ab6df19b8 100644 --- a/devel/1106.md +++ b/devel/1106.md @@ -111,3 +111,42 @@ void QTMTabPage::applyDisplayTitle (const QString& rawTitle) { > 切换 tab 触发了一次 `QTMTabPage` 重建,重建时上游传给 `applyDisplayTitle` 的标题字符串里带了 ` *`。 所以根因不在 `m_isDirty` 本身,而在「**标题字符串里的 ` *` 是谁拼上去的、为什么 buffer 已经保存却还被判成未保存**」。这条链路需要继续往上游追(标题文本生成处 + `buffer_modified` 的判定时机),在 7.2 中展开。 + +### 7.2 复现路径:GUI 自动化 + 调试日志 + +**关键认知:`switch-to` / `window-set-view` 和鼠标点 tab 是两条不同的路径**。 + +验证过程(都确认**不能**触发 bug): + +1. **headless + `(switch-to-buffer path)`**:`TeXmacs/tests/1106.scm` 走 `xmake r 1106` 的 headless 模式,8 条 `(buffer-modified? ...)` 断言全部通过,`buffer-get-title` 始终不带 ` *`。 +2. **GUI + `(window-set-view win view #t)`**:给 xmake 集成测试加了 `MOGAN_TEST_GUI=1` 开关(`xmake/tests.lua`),以真实 GUI 进程启动;`1106.scm` 用 `exec-delayed-at` 串成异步链(每步之间 5s 间隔,保证 Qt 事件循环把 tab 重建和 paint 跑完),在 GUI 模式下打开两个文档、来回切换 5 轮。结果 `[1106-qt] applyDisplayTitle` 每次收到的 `rawTitle` 都不带 ` *`,`m_isDirty=false`,`buffer_modified` 也始终返回 `false`。 + +也就是说:**bug 触发的代码路径里有 Scheme 路径绕过的副作用**,最可疑的是 `QTMTabPage::mousePressEvent`(`src/Plugins/Qt/QTMTabPage.cpp:319-337`)——它除了最终触发 `QAction` → `(window-set-view ...)` 之外,还会: + +- 写 `g_mostRecentlyDraggedTab`、`g_mostRecentlyDraggedBar`、`g_mostRecentlyEnteredBar` 三个全局变量 +- 设 `m_dragStartPos` +- 让 `QToolButton` 进入 pressed/checked 状态,走 Qt 自己的信号槽分发 + +这些副作用只有真实的 `QMouseEvent` 才能触发。Scheme 直接调 `window-set-view` 完全绕过了它们。 + +**已搭好的基础设施**: + +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 自动化脚本,open 两个 tmu → 切换 N 轮 → 自己 `(quit-TeXmacs)`。用 `exec-delayed-at` 而不是同步 `sleep`,避免阻塞 Qt 事件循环。 +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 +``` + +**下一步方向**:把 `1106.scm` 里的 `(window-set-view ...)` 换成真正的 `QTest::mouseClick`(或者通过 Scheme 桥接 C++ 的方式派发一个 `QMouseEvent` 到目标 `QTMTabPage`),让鼠标路径里的副作用被完整触发。在那之前,先把当前这套已经能跑通的 GUI 自动化骨架和调试日志固化为一次提交。 From fbd5945de8a0200b89c1fab4a68f74bdf3c9bd77 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 20:26:21 +0800 Subject: [PATCH 03/10] =?UTF-8?q?[1106]=20=E6=96=B0=E5=A2=9E=20GUI=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E5=A4=8D=E7=8E=B0=E9=AA=A8=E6=9E=B6?= =?UTF-8?q?=E4=B8=8E=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TeXmacs/tests/1106.scm + tmu/1106_{a,b}.tmu:用 exec-delayed-at 串异步链在 GUI 进程里 open 两个文档并来回切 tab,链尾自退出 - xmake/tests.lua:集成测试新增 MOGAN_TEST_GUI=1 环境变量开关, 开启后以真实 GUI 进程(不带 -headless)启动且不在 -x 外层自动 quit,默认行为不变 - buffer_modified / applyDisplayTitle / replaceTabPages 加 LIII_DEBUG 临时日志,方便定位 tab 切换时 buffer / 标题的实际状态 当前结论:Scheme window-set-view 不能复现 bug,需要真正的鼠标事件 路径(QTMTabPage::mousePressEvent 副作用)才会触发。 Co-Authored-By: Claude Opus 4.7 --- TeXmacs/tests/1106.scm | 107 ++++++++++++++++++++++++++++++++ TeXmacs/tests/tmu/1106_a.tmu | 14 +++++ TeXmacs/tests/tmu/1106_b.tmu | 14 +++++ src/Plugins/Qt/QTMTabPage.cpp | 7 ++- src/Texmacs/Data/new_buffer.cpp | 14 ++++- xmake/tests.lua | 32 +++++++--- 6 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 TeXmacs/tests/1106.scm create mode 100644 TeXmacs/tests/tmu/1106_a.tmu create mode 100644 TeXmacs/tests/tmu/1106_b.tmu diff --git a/TeXmacs/tests/1106.scm b/TeXmacs/tests/1106.scm new file mode 100644 index 0000000000..284fe19048 --- /dev/null +++ b/TeXmacs/tests/1106.scm @@ -0,0 +1,107 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; 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. +;; +;; 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 (tmu-path name) + (string-append "$TEXMACS_PATH/tests/tmu/" name) +) ;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) + (let* ((path-a (string->url (tmu-path "1106_a.tmu"))) + (path-b (string->url (tmu-path "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") + (lambda () (switch-to path-a)) + ) ;cons + (cons (string-append "round " (number->string i) ": switch b") + (lambda () (switch-to path-b)) + ) ;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..7a112d9029 --- /dev/null +++ b/TeXmacs/tests/tmu/1106_a.tmu @@ -0,0 +1,14 @@ +> + +> + +<\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..2fa77d7aa8 --- /dev/null +++ b/TeXmacs/tests/tmu/1106_b.tmu @@ -0,0 +1,14 @@ +> + +> + +<\body> + 1106_b + + +<\initial> + <\collection> + + + + diff --git a/src/Plugins/Qt/QTMTabPage.cpp b/src/Plugins/Qt/QTMTabPage.cpp index a11b76c602..cd65ea6a93 100644 --- a/src/Plugins/Qt/QTMTabPage.cpp +++ b/src/Plugins/Qt/QTMTabPage.cpp @@ -196,7 +196,8 @@ QTMTabPage::applyDisplayTitle (const QString& rawTitle) { m_isDirty= extract_dirty_suffix (rawTitle, cleanTitle); setText (cleanTitle); #ifdef LIII_DEBUG - cout << "[1106] applyDisplayTitle rawTitle=\"" << from_qstring (rawTitle) + cout << "[1106-qt] applyDisplayTitle view=" << m_viewUrl + << " rawTitle=\"" << from_qstring (rawTitle) << "\" cleanTitle=\"" << from_qstring (cleanTitle) << "\" m_isDirty=" << (m_isDirty ? "true" : "false") << LF; #endif @@ -460,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 From ffcf286183b0f6a0fae38b43f65b73897ee56237 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 20:29:16 +0800 Subject: [PATCH 04/10] =?UTF-8?q?[1106]=207.2=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=9A=E5=B7=B2=E5=A4=8D=E7=8E=B0=EF=BC=8Cbug=20=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E4=BA=8E=20load-buffer=20=E4=B8=8E=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 关键认知更新:之前两轮失败不是 Scheme 切换路径太干净,而是因为 夹具在 $TEXMACS_PATH/tests/tmu/ 下走的是 load-buffer 的 clean 分支。 改用 /tmp 下的副本立即复现,load-buffer 完成的瞬间 buffer 就被标记 为 modified,切换过程持续 true。下一步在 7.3 定位 load-buffer-main 链路里哪一步误置 modified。 Co-Authored-By: Claude Opus 4.7 --- devel/1106.md | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/devel/1106.md b/devel/1106.md index 0ab6df19b8..3ed4b30fe0 100644 --- a/devel/1106.md +++ b/devel/1106.md @@ -112,30 +112,15 @@ void QTMTabPage::applyDisplayTitle (const QString& rawTitle) { 所以根因不在 `m_isDirty` 本身,而在「**标题字符串里的 ` *` 是谁拼上去的、为什么 buffer 已经保存却还被判成未保存**」。这条链路需要继续往上游追(标题文本生成处 + `buffer_modified` 的判定时机),在 7.2 中展开。 -### 7.2 复现路径:GUI 自动化 + 调试日志 +### 7.2 已复现:bug 触发于 `load-buffer`,与「文件路径」强相关 -**关键认知:`switch-to` / `window-set-view` 和鼠标点 tab 是两条不同的路径**。 - -验证过程(都确认**不能**触发 bug): - -1. **headless + `(switch-to-buffer path)`**:`TeXmacs/tests/1106.scm` 走 `xmake r 1106` 的 headless 模式,8 条 `(buffer-modified? ...)` 断言全部通过,`buffer-get-title` 始终不带 ` *`。 -2. **GUI + `(window-set-view win view #t)`**:给 xmake 集成测试加了 `MOGAN_TEST_GUI=1` 开关(`xmake/tests.lua`),以真实 GUI 进程启动;`1106.scm` 用 `exec-delayed-at` 串成异步链(每步之间 5s 间隔,保证 Qt 事件循环把 tab 重建和 paint 跑完),在 GUI 模式下打开两个文档、来回切换 5 轮。结果 `[1106-qt] applyDisplayTitle` 每次收到的 `rawTitle` 都不带 ` *`,`m_isDirty=false`,`buffer_modified` 也始终返回 `false`。 - -也就是说:**bug 触发的代码路径里有 Scheme 路径绕过的副作用**,最可疑的是 `QTMTabPage::mousePressEvent`(`src/Plugins/Qt/QTMTabPage.cpp:319-337`)——它除了最终触发 `QAction` → `(window-set-view ...)` 之外,还会: - -- 写 `g_mostRecentlyDraggedTab`、`g_mostRecentlyDraggedBar`、`g_mostRecentlyEnteredBar` 三个全局变量 -- 设 `m_dragStartPos` -- 让 `QToolButton` 进入 pressed/checked 状态,走 Qt 自己的信号槽分发 - -这些副作用只有真实的 `QMouseEvent` 才能触发。Scheme 直接调 `window-set-view` 完全绕过了它们。 - -**已搭好的基础设施**: +**搭好的复现基础设施**: 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 自动化脚本,open 两个 tmu → 切换 N 轮 → 自己 `(quit-TeXmacs)`。用 `exec-delayed-at` 而不是同步 `sleep`,避免阻塞 Qt 事件循环。 +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` @@ -149,4 +134,14 @@ xmake b stem MOGAN_TEST_GUI=1 xmake r 1106 ``` -**下一步方向**:把 `1106.scm` 里的 `(window-set-view ...)` 换成真正的 `QTest::mouseClick`(或者通过 Scheme 桥接 C++ 的方式派发一个 `QMouseEvent` 到目标 `QTMTabPage`),让鼠标路径里的副作用被完整触发。在那之前,先把当前这套已经能跑通的 GUI 自动化骨架和调试日志固化为一次提交。 +**关键认知: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 展开。 From f385d1fa2b41ed4e15d50e794c5b90268b584f9d Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 20:29:29 +0800 Subject: [PATCH 05/10] =?UTF-8?q?[1106]=20=E5=A4=B9=E5=85=B7=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E5=88=B0=20/tmp=20=E5=90=8E=E5=A4=8D=E7=8E=B0=20tab?= =?UTF-8?q?=20=E5=88=87=E6=8D=A2=20dirty=20=E8=AF=AF=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1106.scm:每次启动把 $TEXMACS_PATH 夹具复制到 /tmp 再 load-buffer, 避免检入夹具被污染;改用 /tmp 路径后 buffer_modified 在 load 完成 瞬间就返回 true,applyDisplayTitle 收到 rawTitle 带 " *",bug 复现 - 1106_a.tmu / 1106_b.tmu:恢复成干净最小内容(前一轮 GUI 跑污染过) Co-Authored-By: Claude Opus 4.7 --- TeXmacs/tests/1106.scm | 26 ++++++++++++++++++++++---- TeXmacs/tests/tmu/1106_a.tmu | 4 ++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/TeXmacs/tests/1106.scm b/TeXmacs/tests/1106.scm index 284fe19048..b5622a6f80 100644 --- a/TeXmacs/tests/1106.scm +++ b/TeXmacs/tests/1106.scm @@ -17,6 +17,10 @@ ;; 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 @@ -30,10 +34,22 @@ (texmacs-module (texmacs tests 1106)) -(define (tmu-path name) +(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)) @@ -73,9 +89,11 @@ ) ;define (tm-define (test_1106) - (let* ((path-a (string->url (tmu-path "1106_a.tmu"))) - (path-b (string->url (tmu-path "1106_b.tmu"))) - ) ; + ;; 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 diff --git a/TeXmacs/tests/tmu/1106_a.tmu b/TeXmacs/tests/tmu/1106_a.tmu index 7a112d9029..2f885f6dff 100644 --- a/TeXmacs/tests/tmu/1106_a.tmu +++ b/TeXmacs/tests/tmu/1106_a.tmu @@ -1,9 +1,9 @@ -> +> > <\body> - 1106_a\ + 1106_a <\initial> From 775ef6050e5eb7db1e1010b1dc1a3ee3ebe75450 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 20:35:58 +0800 Subject: [PATCH 06/10] =?UTF-8?q?[1106]=20=E8=A1=A5=E5=85=85=207.3?= =?UTF-8?q?=EF=BC=9A=E6=A0=B9=E5=9B=A0=E5=AE=9A=E4=BD=8D=E4=B8=8E=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:auto-backup-ensure-buffer-doc-id! 给不在 $TEXMACS_PATH 下的 新加载 buffer 注入 stem-doc-id 环境变量,init_env 写入动作被 editor 当成普通修改推入 undo 栈并触发 require_save,导致刚加载的 buffer 立 刻被判为未保存。 修复:在 init_env 之后立刻 (buffer-pretend-saved name) 重置 saved 标志,doc-id 仍然写入,auto-backup 功能不受影响。 Co-Authored-By: Claude Opus 4.7 --- devel/1106.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/devel/1106.md b/devel/1106.md index 3ed4b30fe0..36bfc4ab09 100644 --- a/devel/1106.md +++ b/devel/1106.md @@ -145,3 +145,105 @@ MOGAN_TEST_GUI=1 xmake r 1106 因此真正需要追的根因是:**`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 要做的事。 From 2f18733f538f676a5a8df5c6f17beea05b6bac17 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 20:36:08 +0800 Subject: [PATCH 07/10] =?UTF-8?q?[1106]=20=E4=BF=AE=E5=A4=8D=EF=BC=9A?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=20stem-doc-id=20=E5=90=8E=E7=AB=8B=E5=8D=B3?= =?UTF-8?q?=20pretend-saved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tm-files.scm: auto-backup-ensure-buffer-doc-id! 在 init_env 写入 doc-id 后调用 (buffer-pretend-saved name),抵消 init_env 推入 undo 栈的修改,避免 load-buffer 后 buffer 被错误标记为未保存 - archiver.cpp: 临时在 require_save 加 [1106-arch] 调试日志 (LIII_DEBUG),后续验证稳定后清理 验证:MOGAN_TEST_GUI=1 xmake r 1106 跑 5 轮切换,buffer_modified 全部 false,applyDisplayTitle 收到的 rawTitle 不再带 " *"。 Co-Authored-By: Claude Opus 4.7 --- TeXmacs/progs/texmacs/texmacs/tm-files.scm | 7 ++++++- src/Data/History/archiver.cpp | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/TeXmacs/progs/texmacs/texmacs/tm-files.scm b/TeXmacs/progs/texmacs/texmacs/tm-files.scm index 13ef7060d0..4377f4dd0c 100644 --- a/TeXmacs/progs/texmacs/texmacs/tm-files.scm +++ b/TeXmacs/progs/texmacs/texmacs/tm-files.scm @@ -825,7 +825,12 @@ (let ((doc-id (uuid4))) ;; 写入 init-env 即可绑定到当前会话,避免 buffer-set 触发 ;; 文档重新解析。 - (init-env "stem-doc-id" doc-id) + (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 ) ;let ) ;if diff --git a/src/Data/History/archiver.cpp b/src/Data/History/archiver.cpp index 9f226ae7d7..a892cd4396 100644 --- a/src/Data/History/archiver.cpp +++ b/src/Data/History/archiver.cpp @@ -641,6 +641,9 @@ archiver_rep::corrected_depth () { void archiver_rep::require_save () { +#ifdef LIII_DEBUG + cout << "[1106-arch] require_save called; depth=" << corrected_depth () << LF; +#endif last_save= -1; } From 89647e9f84e86d9d7b68f4d7911ad134a7adff93 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 20:40:01 +0800 Subject: [PATCH 08/10] =?UTF-8?q?Revert=20"[1106]=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=9A=E6=B3=A8=E5=85=A5=20stem-doc-id=20=E5=90=8E=E7=AB=8B?= =?UTF-8?q?=E5=8D=B3=20pretend-saved"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2f18733f538f676a5a8df5c6f17beea05b6bac17. --- TeXmacs/progs/texmacs/texmacs/tm-files.scm | 7 +------ src/Data/History/archiver.cpp | 3 --- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/TeXmacs/progs/texmacs/texmacs/tm-files.scm b/TeXmacs/progs/texmacs/texmacs/tm-files.scm index 4377f4dd0c..13ef7060d0 100644 --- a/TeXmacs/progs/texmacs/texmacs/tm-files.scm +++ b/TeXmacs/progs/texmacs/texmacs/tm-files.scm @@ -825,12 +825,7 @@ (let ((doc-id (uuid4))) ;; 写入 init-env 即可绑定到当前会话,避免 buffer-set 触发 ;; 文档重新解析。 - (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) + (init-env "stem-doc-id" doc-id) doc-id ) ;let ) ;if diff --git a/src/Data/History/archiver.cpp b/src/Data/History/archiver.cpp index a892cd4396..9f226ae7d7 100644 --- a/src/Data/History/archiver.cpp +++ b/src/Data/History/archiver.cpp @@ -641,9 +641,6 @@ archiver_rep::corrected_depth () { void archiver_rep::require_save () { -#ifdef LIII_DEBUG - cout << "[1106-arch] require_save called; depth=" << corrected_depth () << LF; -#endif last_save= -1; } From 00cf560cf684776c351a6abb3a84a26487ea84f1 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 20:46:09 +0800 Subject: [PATCH 09/10] wip --- TeXmacs/tests/tmu/1106_a.tmu | 3 ++- TeXmacs/tests/tmu/1106_b.tmu | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/TeXmacs/tests/tmu/1106_a.tmu b/TeXmacs/tests/tmu/1106_a.tmu index 2f885f6dff..a2259eae1c 100644 --- a/TeXmacs/tests/tmu/1106_a.tmu +++ b/TeXmacs/tests/tmu/1106_a.tmu @@ -1,4 +1,4 @@ -> +> > @@ -10,5 +10,6 @@ <\collection> + diff --git a/TeXmacs/tests/tmu/1106_b.tmu b/TeXmacs/tests/tmu/1106_b.tmu index 2fa77d7aa8..ecea7bfea8 100644 --- a/TeXmacs/tests/tmu/1106_b.tmu +++ b/TeXmacs/tests/tmu/1106_b.tmu @@ -1,4 +1,4 @@ -> +> > @@ -10,5 +10,6 @@ <\collection> + From 452afa481b9b5b652c1fa311a4435c2bf60b8bb3 Mon Sep 17 00:00:00 2001 From: Da Shen Date: Mon, 22 Jun 2026 20:52:18 +0800 Subject: [PATCH 10/10] =?UTF-8?q?[1106]=20=E5=88=87=E6=8D=A2=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E6=94=B9=E7=94=A8=20switch-to-view-index=20=E5=B9=B6?= =?UTF-8?q?=E5=8A=A0=20auto-backup=20=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1106.scm:切换调用从 (window-set-view win v #t) 改为 (switch-to-view-index N),与 std 2/std 3 (ctrl+2/ctrl+3) 完全 相同的代码路径。但实测仍不能复现手动按键时的 dirty 误报,说明 bug 触发于按键链路的附属副作用(焦点/光标/装饰通知),不在 switch-to-view-index 本身。 - tm-files.scm:在 auto-backup-ensure-buffer-doc-id! 加 [1106-ab] 调试日志,用于确认 init-env 分支执行时机。已验证:夹具带 doc-id 时走 keep-old 分支,buffer 保持 clean;夹具不带 doc-id 时走 inject-new 分支,init-env 触发 require_save,buffer 被误判为 modified。 下一步:需要真实按键事件路径来复现 switch-to-view-index 之外的 副作用。 Co-Authored-By: Claude Opus 4.7 --- TeXmacs/progs/texmacs/texmacs/tm-files.scm | 50 +++++++++++++++------- TeXmacs/tests/1106.scm | 8 ++-- 2 files changed, 38 insertions(+), 20 deletions(-) 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 index b5622a6f80..f663d085a9 100644 --- a/TeXmacs/tests/1106.scm +++ b/TeXmacs/tests/1106.scm @@ -107,11 +107,11 @@ ) ;set! (loop (+ i 1) (append acc - (list (cons (string-append "round " (number->string i) ": switch a") - (lambda () (switch-to path-a)) + (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") - (lambda () (switch-to path-b)) + (cons (string-append "round " (number->string i) ": switch b (std 3)") + (lambda () (switch-to-view-index 2)) ) ;cons ) ;list ) ;append