From e1804536832a6d6b1802a91941de1f3035b98eac Mon Sep 17 00:00:00 2001
From: pigmagicfly <2831850183@qq.com>
Date: Mon, 3 Aug 2026 15:38:13 +0800
Subject: [PATCH 1/8] [2085] add recent document search development notes
---
devel/2085.md | 37 +++++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 devel/2085.md
diff --git a/devel/2085.md b/devel/2085.md
new file mode 100644
index 0000000000..949e67c9a5
--- /dev/null
+++ b/devel/2085.md
@@ -0,0 +1,37 @@
+# [2085] 编辑菜单中的最近文档搜索
+
+## 相关文档
+- [Issue #4143](https://github.com/MoganLab/mogan/issues/4143) - 采用 QML 重构并修复最近文档搜索。
+
+## 任务相关的代码文件
+- `TeXmacs/progs/doc/docgrep.scm`
+- `TeXmacs/progs/texmacs/menus/edit-menu.scm`
+- `src/Plugins/Qt/QTMQmlDialog.cpp`
+- `src/Plugins/Qt/qml/SearchRecentDocuments.qml`
+
+## 如何测试
+```powershell
+xmake build stem
+xmake install stem
+& '.\build\packages\stem\data\bin\MoganSTEM.exe'
+```
+
+在“编辑 -> 搜索最近打开的文档”中输入文件名的一部分,确认结果按模糊匹配显示。选中条目并点击“打开”,确认文件被加载。
+
+## 2026/08/03 采用 QML 重构最近文档搜索
+
+### What
+- 用 QML 对话框替换旧的交互式全文 grep 输入。
+- 显示最近文档的文件名和路径,并支持实时模糊筛选与显式选择。
+- 对最近文件路径去重;初始和筛选后均不默认选中,避免重复项同时高亮或误打开。
+- 补充 QML 加载与桥接测试。
+- 在 `ai-docs/qml/qml-dialog.html` 中补充该对话框的交互设计稿。
+
+### Why
+旧交互式输入在中文输入法确认候选字时会提前执行,且其全文搜索语义不符合按最近文件选择文档的预期。
+
+### How
+- Scheme 侧提供最近文件列表,并在确认选择后加载对应路径。
+- C++ 将最近文件模型注入 QML,保持 UTF-8 路径编码。
+- C++ 以规范化路径去重,保留最近记录中的第一次出现。
+- QML 按文件名优先、完整路径兜底执行子序列模糊匹配;只有显式选中并点击“打开”才加载选定文件。
From 58b73b970631cff7825b43bef64bb53f3828a889 Mon Sep 17 00:00:00 2001
From: pigmagicfly <2831850183@qq.com>
Date: Mon, 3 Aug 2026 15:41:19 +0800
Subject: [PATCH 2/8] [2085] refactor recent document search with QML
---
TeXmacs/progs/doc/docgrep.scm | 12 ++
TeXmacs/progs/texmacs/menus/edit-menu.scm | 2 +-
ai-docs/qml/qml-dialog.html | 155 ++++++++++++++-
src/Plugins/Qt/QTMQmlDialog.cpp | 52 +++++
src/Plugins/Qt/QTMQmlDialog.hpp | 9 +
src/Plugins/Qt/moganqml.qrc | 1 +
src/Plugins/Qt/qml/SearchRecentDocuments.qml | 194 +++++++++++++++++++
src/Plugins/Qt/qml/qmldir | 1 +
src/Scheme/L5/glue_qt.lua | 8 +-
tests/Plugins/Qt/qml_dialog_test.cpp | 14 ++
tests/Plugins/Qt/qml_load_test.cpp | 31 +++
11 files changed, 473 insertions(+), 6 deletions(-)
create mode 100644 src/Plugins/Qt/qml/SearchRecentDocuments.qml
diff --git a/TeXmacs/progs/doc/docgrep.scm b/TeXmacs/progs/doc/docgrep.scm
index 6c568b96ce..076e3186b8 100644
--- a/TeXmacs/progs/doc/docgrep.scm
+++ b/TeXmacs/progs/doc/docgrep.scm
@@ -247,3 +247,15 @@
(load-document (string-append "tmfs://grep/" query))
) ;with
) ;tm-define
+
+(tm-define (recent-documents-for-qml) (map url->system (recent-file-list 25)))
+
+(tm-define (open-search-recent-documents)
+ (:interactive #t)
+ (with path
+ (cpp-search-recent-documents-dialog)
+ (when (!= path "")
+ (load-document (system->url path))
+ ) ;when
+ ) ;with
+) ;tm-define
diff --git a/TeXmacs/progs/texmacs/menus/edit-menu.scm b/TeXmacs/progs/texmacs/menus/edit-menu.scm
index dd7314b5da..1490ba42fe 100644
--- a/TeXmacs/progs/texmacs/menus/edit-menu.scm
+++ b/TeXmacs/progs/texmacs/menus/edit-menu.scm
@@ -99,7 +99,7 @@
) ;->
) ;if
---
- ("Search recent documents" (interactive docgrep-in-recent))
+ ("Search recent documents" (open-search-recent-documents))
---
(if (use-menus?) (-> "Preferences" (link preferences-menu)))
(if (use-popups?) ("Preferences" (open-preferences)))
diff --git a/ai-docs/qml/qml-dialog.html b/ai-docs/qml/qml-dialog.html
index 926a5d9ec1..bce953ef51 100644
--- a/ai-docs/qml/qml-dialog.html
+++ b/ai-docs/qml/qml-dialog.html
@@ -299,6 +299,76 @@
box-shadow: var(--shadow);
}
+ .recent-search-shell {
+ width: min(620px, 100%);
+ background: #ffffff;
+ border: 1px solid #d6dfdf;
+ border-radius: var(--radius);
+ padding: 22px;
+ box-shadow: var(--shadow);
+ }
+
+ .recent-search-shell h3 {
+ text-align: center;
+ font-size: 18px;
+ margin-bottom: 18px;
+ }
+
+ .recent-search-input {
+ width: 100%;
+ height: 38px;
+ border: 1px solid #9fcac7;
+ border-radius: 10px;
+ padding: 0 12px;
+ background: #f9fcfc;
+ color: #25353a;
+ font: inherit;
+ }
+
+ .recent-results {
+ list-style: none;
+ margin-top: 14px;
+ border: 1px solid #cad7d7;
+ border-radius: 10px;
+ overflow: hidden;
+ }
+
+ .recent-result {
+ padding: 10px 12px;
+ border-bottom: 1px solid #e4eeee;
+ cursor: pointer;
+ }
+
+ .recent-result:last-child {
+ border-bottom: 0;
+ }
+
+ .recent-result.active {
+ background: #dff3f1;
+ color: #194f53;
+ }
+
+ .recent-result strong,
+ .recent-result span {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .recent-result span {
+ margin-top: 3px;
+ color: #60787e;
+ font-size: 12px;
+ }
+
+ .recent-empty {
+ display: none;
+ padding: 18px;
+ color: var(--muted);
+ text-align: center;
+ }
+
.field {
display: grid;
grid-template-columns: 120px 1fr;
@@ -993,6 +1063,7 @@
QML 组件模拟器
+
@@ -1577,6 +1648,36 @@ 切换界面语言
+
@@ -1591,7 +1692,8 @@ 切换界面语言
paragraph: document.getElementById('panel-paragraph'),
statistics: document.getElementById('panel-statistics'),
preferences: document.getElementById('panel-preferences'),
- confirmRestart: document.getElementById('panel-confirm-restart')
+ confirmRestart: document.getElementById('panel-confirm-restart'),
+ recentSearch: document.getElementById('panel-recent-search')
};
const titleMap = {
@@ -1601,7 +1703,8 @@ 切换界面语言
paragraph: 'ParagraphFormat.qml 模拟',
statistics: 'Statistics.qml 模拟',
preferences: 'Preferences.qml 模拟',
- confirmRestart: 'ConfirmRestart.qml 模拟'
+ confirmRestart: 'ConfirmRestart.qml 模拟',
+ recentSearch: 'SearchRecentDocuments.qml 模拟'
};
const codeMap = {
@@ -1611,7 +1714,8 @@ 切换界面语言
paragraph: 'EnumCombo x N + tabs(基础/高级) + make-multi-line-with 写回',
statistics: 'statsModel: [{label, value}, ...] + closeBridge.choose(0) 关闭',
preferences: 'prefTabs(5) + subTabs(6) + EnumCombo + Toggle + 纯 OK 提交',
- confirmRestart: 'message + buttonLabels + closeBridge.choose(index)'
+ confirmRestart: 'message + buttonLabels + closeBridge.choose(index)',
+ recentSearch: 'recentDocuments + fuzzyScore() + closeBridge.submit({ path })'
};
const viewTitle = document.getElementById('view-title');
@@ -1646,6 +1750,49 @@ 切换界面语言
});
});
+ const recentSearchInput = document.getElementById('recent-search-input');
+ const recentResults = document.querySelectorAll('.recent-result');
+ const recentEmpty = document.getElementById('recent-empty');
+ let recentSelected = document.querySelector('.recent-result.active');
+
+ function fuzzyMatch(text, query) {
+ let cursor = 0;
+ const haystack = text.toLowerCase();
+ for (const character of query.toLowerCase()) {
+ cursor = haystack.indexOf(character, cursor);
+ if (cursor < 0) return false;
+ cursor += 1;
+ }
+ return true;
+ }
+
+ recentSearchInput.addEventListener('input', () => {
+ const query = recentSearchInput.value.trim();
+ let visibleCount = 0;
+ recentResults.forEach((row) => {
+ const matched = !query || fuzzyMatch(row.dataset.name, query) || fuzzyMatch(row.dataset.path, query);
+ row.style.display = matched ? '' : 'none';
+ if (matched) visibleCount += 1;
+ });
+ recentEmpty.style.display = visibleCount ? 'none' : 'block';
+ const selectedVisible = recentSelected && recentSelected.style.display !== 'none';
+ if (!selectedVisible) {
+ recentSelected = Array.from(recentResults).find((row) => row.style.display !== 'none') || null;
+ recentResults.forEach((row) => row.classList.toggle('active', row === recentSelected));
+ }
+ });
+
+ recentResults.forEach((row) => {
+ row.addEventListener('click', () => {
+ recentSelected = row;
+ recentResults.forEach((item) => item.classList.toggle('active', item === row));
+ });
+ });
+
+ document.getElementById('recent-open').addEventListener('click', () => {
+ if (recentSelected) log('SearchRecentDocuments: submit({ path: "' + recentSelected.dataset.path + '" })');
+ });
+
document.getElementById('form-submit').addEventListener('click', () => {
const values = {
language: document.getElementById('field-language').value,
@@ -1871,4 +2018,4 @@ 切换界面语言
})();