From 2440706100c84d0934752d272d783e3b3ec588be Mon Sep 17 00:00:00 2001 From: xiepengfei Date: Tue, 27 Oct 2026 11:59:14 +0800 Subject: [PATCH] fix: center search result line in read-only markdown view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Root cause: in read-only markdown view search next/prev called jumpToLine with keepLineAtCenter=false, so the hidden TextEdit only scrolled minimally and kept the match at the bottom edge of its own viewport, while render-page sync only aligns the top visible block; since the 800px rendered column is taller per source line, the match fell below the rendered fold (reliably reproducible when maximized) 2. Fix: pass keepLineAtCenter=true in searchKeywordSeletion so keepCurrentLineAtCenter centers the anchor line; the explicit scrollbar setValue also drives valueChanged render-page scroll sync 3. Test: add EditWrapper UT cases (maximized/normal window) asserting the renderer receives scrollToLine near the matched line; all 107 editor_wrapper UT cases pass Log: fix search result not visible in read-only markdown view Influence: 1. Test search next/prev in read-only markdown view, verify match is centered on screen, especially with a maximized window 2. Test search next/prev in edit mode, verify no regression 3. Verify render-page scroll sync follows editor scrollbar fix: 修复只读Markdown视图下查找结果不在当前屏显示 1. 根因:只读Markdown视图下查找下一个/上一个调用jumpToLine时传入 keepLineAtCenter=false,隐藏的TextEdit仅做最小滚动、匹配项停在 其视口底边缘,而渲染页滚动同步仅按顶部可见行对齐;800px定宽 渲染列每源码行渲染高度更大,匹配项落到渲染视口折叠线下 (窗口最大化时必现) 2. 方案:searchKeywordSeletion中jumpToLine的keepLineAtCenter改为 true,keepCurrentLineAtCenter将锚点行居中,显式scrollbar setValue同时驱动valueChanged渲染页滚动同步 3. 测试:新增EditWrapper单测(最大化/普通窗口两场景),断言渲染器 收到的scrollToLine行号贴近匹配行;editor_wrapper 107例全部通过 Log: 修复只读Markdown视图下查找结果不在当前屏显示的问题 Influence: 1. 在只读Markdown视图下测试查找下一个/上一个,验证匹配内容居中 显示,重点验证窗口最大化场景 2. 在编辑模式下测试查找功能,验证无回归 3. 验证渲染页滚动与编辑器滚动条同步 PMS: BUG-380705 --- src/editor/dtextedit.cpp | 4 +- tests/editor_wrapper/test_editwrapper.cpp | 84 +++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/editor/dtextedit.cpp b/src/editor/dtextedit.cpp index 79003ad6..00bc6d77 100644 --- a/src/editor/dtextedit.cpp +++ b/src/editor/dtextedit.cpp @@ -2724,7 +2724,7 @@ bool TextEdit::searchKeywordSeletion(QString keyword, QTextCursor cursor, bool f if (!next.isNull()) { qCDebug(appLog) << "Searching keyword seletion with find next and not null"; m_findHighlightSelection.cursor = next; - jumpToLine(next.blockNumber() + offsetLines, false); + jumpToLine(next.blockNumber() + offsetLines, true); setTextCursor(next); ret = true; } @@ -2745,7 +2745,7 @@ bool TextEdit::searchKeywordSeletion(QString keyword, QTextCursor cursor, bool f if (!prev.isNull()) { qCDebug(appLog) << "Searching keyword seletion with find previous and not null"; m_findHighlightSelection.cursor = prev; - jumpToLine(prev.blockNumber() + offsetLines, false); + jumpToLine(prev.blockNumber() + offsetLines, true); setTextCursor(prev); ret = true; } diff --git a/tests/editor_wrapper/test_editwrapper.cpp b/tests/editor_wrapper/test_editwrapper.cpp index cf1fa0d0..6f9673bd 100644 --- a/tests/editor_wrapper/test_editwrapper.cpp +++ b/tests/editor_wrapper/test_editwrapper.cpp @@ -638,6 +638,90 @@ TEST_F(EditWrapperTest, PlainTextEmpty_DefaultTrue_AfterInsertFalse) EXPECT_FALSE(m_wrapper->isPlainTextEmpty()); } +// ============================================================================ +// V-6705 验证用例:md 只读视图下查找"下一个",渲染页 scrollToLine 同步行为 +// 场景:隐藏 TextEdit(ReadView)+ 大窗口(模拟最大化),匹配项在远处 +// 断言(修复后期望):1) 渲染器收到 scrollToLine; +// 2) 同步行号与匹配行距离 ≤ 半屏(居中语义),保证渲染页顶部对齐后匹配仍在屏内 +// ============================================================================ +static void v6705FindNextScenario(EditWrapper *wrapper, RecordingRenderer *renderer, + int winW, int winH, const char *tag) +{ + renderer->ready = true; + wrapper->setMarkdownRendererForTest(renderer); + + // 长文档:500 行,needle 只在第 400 行(0 基块号 399) + QString text; + for (int i = 1; i <= 500; ++i) + text += (i == 400 ? QString("line %1 needle here\n").arg(i) + : QString("line %1 plain text content\n").arg(i)); + + QTemporaryDir dir; + const QString mdPath = dir.filePath("v6705.md"); + { + QFile f(mdPath); + ASSERT_TRUE(f.open(QIODevice::WriteOnly)); + f.write(text.toUtf8()); + f.close(); + } + wrapper->updatePath(mdPath, mdPath); + wrapper->textEditor()->setPlainText(text); + + // 真实应用时序:编辑页先以真实尺寸布局(Edit 模式可见),再切 ReadView 隐藏。 + // 未 show 的 widget 不派发 Resize 事件,隐藏页几何不会更新,必须先 show 顶层容器。 + if (QWidget *top = wrapper->parentWidget() ? wrapper->parentWidget() : wrapper) + top->resize(winW, winH); + wrapper->resize(winW, winH); + if (wrapper->parentWidget()) + wrapper->parentWidget()->show(); + else + wrapper->show(); + QApplication::processEvents(); + + wrapper->updateMarkdownRecognition(QStringLiteral("v6705.md"), QString()); + wrapper->setViewMode(ViewMode::ReadView); + ASSERT_EQ(wrapper->viewMode(), ViewMode::ReadView); + + TextEdit *ed = wrapper->textEditor(); + ASSERT_EQ(ed->toPlainText().split('\n').size(), 501); // 500 行 + 末尾空串 + + ed->verticalScrollBar()->setValue(0); + QApplication::processEvents(); + renderer->scrollCalls = 0; + renderer->lastLine = -1.0; + + // Act:真实查找路径(Window::handleFindKeyword → updateCursorKeywordSelection) + ed->updateCursorKeywordSelection(QStringLiteral("needle"), true); + QApplication::processEvents(); + + const int matchBlock = ed->textCursor().blockNumber(); + const int lineHeight = ed->cursorRect().height(); + const int viewportH = ed->viewport()->height(); + const int visibleLines = lineHeight > 0 ? viewportH / lineHeight : 0; + const double dist = matchBlock - renderer->lastLine; + + qInfo("[%s] win=%dx%d scrollCalls=%d lastLine=%.1f matchBlock=%d vbarValue=%d " + "topVisibleBlock=%d viewportH=%d lineHeight=%d visibleLines=%d matchToSyncTopDist=%.1f", + tag, winW, winH, renderer->scrollCalls, renderer->lastLine, matchBlock, + ed->verticalScrollBar()->value(), ed->topVisibleBlockNumber(), + viewportH, lineHeight, visibleLines, dist); + + // 观测点 1:渲染页确实收到滚动指令(Qt6 下 setTextCursor 内部滚动即触发) + EXPECT_GE(renderer->scrollCalls, 1); + // 观测点 2(修复语义):同步行号与匹配行相距不超过半屏 + 少量余量 + EXPECT_LE(dist, visibleLines / 2 + 3); +} + +TEST_F(EditWrapperTest, V6705_ReadViewFindNext_SyncsRendererNearMatch_Maximized) +{ + v6705FindNextScenario(m_wrapper, &m_renderer, 1920, 1040, "maximized"); +} + +TEST_F(EditWrapperTest, V6705_ReadViewFindNext_SyncsRendererNearMatch_Normal) +{ + v6705FindNextScenario(m_wrapper, &m_renderer, 900, 600, "normal"); +} + // ============================================================================ // isModified(TEST_P:m_bIsTemFile × 撤销栈状态,B28) // ============================================================================