From 74e2022ebfa022b7a7df273ea68ffe9490699391 Mon Sep 17 00:00:00 2001 From: zhengyouge Date: Wed, 29 Apr 2026 10:29:45 +0800 Subject: [PATCH] wip: lazy tab restore for faster startup Refactor tab recovery to only immediately load the focused tab on startup; non-focused tabs remain in a pending state and are loaded on demand when the user switches to them. This avoids blocking the UI with N file loads during session restore. --- src/editor/editwrapper.cpp | 89 +++++--- src/editor/editwrapper.h | 3 + src/main.cpp | 1 + src/startmanager.cpp | 353 +++++++++++++++++--------------- src/widgets/window.cpp | 177 +++++++++++++++- src/widgets/window.h | 25 ++- tests/src/widgets/ut_window.cpp | 67 +++++- 7 files changed, 509 insertions(+), 206 deletions(-) diff --git a/src/editor/editwrapper.cpp b/src/editor/editwrapper.cpp index e43015cb3..e9e7dfb80 100644 --- a/src/editor/editwrapper.cpp +++ b/src/editor/editwrapper.cpp @@ -1022,44 +1022,60 @@ void EditWrapper::handleFileLoadFinished(const QByteArray &encode, const QByteAr m_pTextEdit->setTextFinished(); - QStringList temFileList = Settings::instance()->settings->option("advance.editor.browsing_history_temfile")->value().toStringList(); - - for (int var = 0; var < temFileList.count(); ++var) { - QJsonParseError jsonError; - // 转化为 JSON 文档 - QJsonDocument doucment = QJsonDocument::fromJson(temFileList.value(var).toUtf8(), &jsonError); - // 解析未发生错误 - if (!doucment.isNull() && (jsonError.error == QJsonParseError::NoError)) { - qDebug() << "EditWrapper handleFileLoadFinished, doucment is not null"; - if (doucment.isObject()) { - qDebug() << "EditWrapper handleFileLoadFinished, doucment is object"; - // JSON 文档为对象 - QJsonObject object = doucment.object(); // 转化为对象 - - if (object.contains("localPath") || object.contains("temFilePath")) { - qDebug() << "EditWrapper handleFileLoadFinished, doucment contains localPath or temFilePath"; - // 包含指定的 key - QJsonValue localPathValue = object.value("localPath"); // 获取指定 key 对应的 value - QJsonValue temFilePathValue = object.value("temFilePath"); // 获取指定 key 对应的 value - - if (localPathValue.toString() == m_pTextEdit->getFilePath() - || temFilePathValue.toString() == m_pTextEdit->getFilePath()) { - qDebug() << "EditWrapper handleFileLoadFinished, localPathValue or temFilePathValue is equal to m_pTextEdit->getFilePath()"; - QJsonValue value = object.value("cursorPosition"); // 获取指定 key 对应的 value - - if (value.isString()) { - qDebug() << "EditWrapper handleFileLoadFinished, value is string"; - QTextCursor cursor = m_pTextEdit->textCursor(); - cursor.setPosition(value.toString().toInt()); - m_pTextEdit->setTextCursor(cursor); - OnUpdateHighlighter(); - break; + // 仅在文件加载成功时恢复光标位置 + if (!error) { + // 恢复光标位置:优先使用预设值(O(1)),否则遍历历史记录(O(N)) + bool cursorRestored = false; + if (m_nRestoreCursorPosition >= 0) { + QTextCursor cursor = m_pTextEdit->textCursor(); + cursor.setPosition(m_nRestoreCursorPosition); + m_pTextEdit->setTextCursor(cursor); + OnUpdateHighlighter(); + m_nRestoreCursorPosition = -1; // 重置,仅使用一次 + cursorRestored = true; + } + + if (!cursorRestored) { + QStringList temFileList = Settings::instance()->settings->option("advance.editor.browsing_history_temfile")->value().toStringList(); + + for (int var = 0; var < temFileList.count(); ++var) { + QJsonParseError jsonError; + // 转化为 JSON 文档 + QJsonDocument doucment = QJsonDocument::fromJson(temFileList.value(var).toUtf8(), &jsonError); + // 解析未发生错误 + if (!doucment.isNull() && (jsonError.error == QJsonParseError::NoError)) { + qDebug() << "EditWrapper handleFileLoadFinished, doucment is not null"; + if (doucment.isObject()) { + qDebug() << "EditWrapper handleFileLoadFinished, doucment is object"; + // JSON 文档为对象 + QJsonObject object = doucment.object(); // 转化为对象 + + if (object.contains("localPath") || object.contains("temFilePath")) { + qDebug() << "EditWrapper handleFileLoadFinished, doucment contains localPath or temFilePath"; + // 包含指定的 key + QJsonValue localPathValue = object.value("localPath"); // 获取指定 key 对应的 value + QJsonValue temFilePathValue = object.value("temFilePath"); // 获取指定 key 对应的 value + + if (localPathValue.toString() == m_pTextEdit->getFilePath() + || temFilePathValue.toString() == m_pTextEdit->getFilePath()) { + qDebug() << "EditWrapper handleFileLoadFinished, localPathValue or temFilePathValue is equal to m_pTextEdit->getFilePath()"; + QJsonValue value = object.value("cursorPosition"); // 获取指定 key 对应的 value + + if (value.isString()) { + qDebug() << "EditWrapper handleFileLoadFinished, value is string"; + QTextCursor cursor = m_pTextEdit->textCursor(); + cursor.setPosition(value.toString().toInt()); + m_pTextEdit->setTextCursor(cursor); + OnUpdateHighlighter(); + break; + } + } } } } } - } - } + } // end if (!cursorRestored) + } // end if (!error) //备份显示修改状态 if (m_bIsTemFile) { @@ -1242,6 +1258,11 @@ void EditWrapper::setTemFile(bool value) qDebug() << "EditWrapper setTemFile, exit"; } +void EditWrapper::setRestoreCursorPosition(int position) +{ + m_nRestoreCursorPosition = position; +} + void EditWrapper::updateHighlighterAll() { qDebug() << "EditWrapper updateHighlighterAll"; diff --git a/src/editor/editwrapper.h b/src/editor/editwrapper.h index cd91a4deb..d419e54ae 100644 --- a/src/editor/editwrapper.h +++ b/src/editor/editwrapper.h @@ -145,6 +145,8 @@ public slots: void OnUpdateHighlighter(); //set the value of m_bIsTemFile void setTemFile(bool value); + // 设置恢复光标位置(用于懒加载恢复,避免 O(N²) 扫描) + void setRestoreCursorPosition(int position); private: //第一次打开文件编码 @@ -180,6 +182,7 @@ public slots: bool m_bAsyncReadFileFinished = false; bool m_bHasPreProcess = false; // 预处理标识 + int m_nRestoreCursorPosition = -1; // 恢复光标位置提示(-1 表示不指定) }; #endif diff --git a/src/main.cpp b/src/main.cpp index 2fe6fe22d..6237d71ba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -99,6 +99,7 @@ int main(int argc, char *argv[]) #endif StartManager *startManager = StartManager::instance(); + //埋点记录启动数据 QJsonObject objStartEvent{ {"tid", Eventlogutils::StartUp}, diff --git a/src/startmanager.cpp b/src/startmanager.cpp index a4f59384a..0a7beda0b 100644 --- a/src/startmanager.cpp +++ b/src/startmanager.cpp @@ -177,8 +177,15 @@ void StartManager::autoBackupFile() //记录所有的文件信息 for (int var = 0; var < m_windows.count(); ++var) { + // 懒加载兼容:先按 TabBar 顺序构建完整列表(包括 pending 标签的占位), + // 再逐一填入已加载 wrapper 的 JSON 数据。 + Tabbar *tabbar = m_windows.value(var)->getTabbar(); + QStringList list; + for (int t = 0; t < tabbar->count(); t++) { + list.append(tabbar->fileAt(t)); + } + wrappers = m_windows.value(var)->getWrappers(); - QStringList list = wrappers.keys(); for (EditWrapper *wrapper : wrappers) { //大文件加载时不备份 @@ -190,7 +197,12 @@ void StartManager::autoBackupFile() filePath = wrapper->textEditor()->getFilePath(); localPath = wrapper->textEditor()->getTruePath(); - StartManager::FileTabInfo tabInfo = StartManager::instance()->getFileTabInfo(filePath); + // 懒加载兼容:使用当前窗口 TabBar 的本地索引,避免跨窗口索引错位 + int tabIndex = tabbar->indexOf(filePath); + if (tabIndex < 0) { + qWarning() << "autoBackupFile: tab not found for path:" << filePath << ", skip"; + continue; + } curPos = QString::number(wrapper->textEditor()->textCursor().position()); fileInfo.setFile(localPath); @@ -252,14 +264,44 @@ void StartManager::autoBackupFile() //使用json串形式保存 document.setObject(jsonObject); QByteArray byteArray = document.toJson(QJsonDocument::Compact); - list.replace(tabInfo.tabIndex, byteArray); - qInfo() << "Auto backup completed, files backed up:" << m_qlistTemFile.size(); - qDebug() << "Exit autoBackupFile"; + // 安全替换:确保索引不越界 + if (tabIndex < list.size()) { + list.replace(tabIndex, byteArray); + } else { + qWarning() << "autoBackupFile: tabIndex" << tabIndex << "out of range (size" << list.size() << "), appending"; + list.append(byteArray); + } + } + + // 懒加载兼容:为 pending 标签页从配置中恢复原始记录 + // 复用上面已获取的 wrappers 变量,无需重复调用 getWrappers() + for (int t = 0; t < tabbar->count(); t++) { + QString tabPath = tabbar->fileAt(t); + if (!wrappers.contains(tabPath)) { + // pending 标签:从 listBackupInfo 中查找原始 JSON 记录 + for (const QString &entry : listBackupInfo) { + QJsonDocument doc = QJsonDocument::fromJson(entry.toUtf8()); + if (doc.isObject()) { + const QJsonObject object = doc.object(); + if (object.value("localPath").toString() != tabPath + && object.value("temFilePath").toString() != tabPath) { + continue; + } + if (t < list.size()) { + list[t] = entry; + } else { + list.append(entry); + } + break; + } + } + } } m_qlistTemFile.append(list); } + qDebug() << "Exit autoBackupFile"; //将json串列表写入配置文件 qInfo() << __func__ << "history file counts:" << m_qlistTemFile.size(); Settings::instance()->settings->option("advance.editor.browsing_history_temfile")->setValue(m_qlistTemFile); @@ -271,15 +313,13 @@ void StartManager::autoBackupFile() int StartManager::recoverFile(Window *window) { qDebug() << "Enter recoverFile"; + Window *pFocusWindow = nullptr; QString focusPath; - bool bIsTemFile = false; QStringList blankFiles = QDir(m_blankFileDir).entryList(QStringList(), QDir::Files); qDebug() << "Found" << blankFiles.size() << "blank files in directory:" << m_blankFileDir; int recFilesSum = 0; QStringList files = blankFiles; - QFileInfo fileInfo; - QString lastmodifiedtime; //去除非新建文件 for (auto file : blankFiles) { if (!file.contains("blank_file")) { @@ -289,179 +329,166 @@ int StartManager::recoverFile(Window *window) } qDebug() << "After filtering, found" << files.size() << "blank files"; - int windowIndex = -1; - - //根据备份信息恢复文件 + // 第一阶段:解析所有 JSON 记录 qDebug() << "Processing" << m_qlistTemFile.count() << "temporary files for recovery"; + QVector tabRecords; for (int i = 0; i < m_qlistTemFile.count(); i++) { QJsonParseError jsonError; - // 转化为 JSON 文档 QJsonDocument doucment = QJsonDocument::fromJson(m_qlistTemFile.value(i).toUtf8(), &jsonError); - // 解析未发生错误 - if (!doucment.isNull() && (jsonError.error == QJsonParseError::NoError)) { - if (doucment.isObject()) { - QString temFilePath; - QString localPath; - // JSON 文档为对象 - QJsonObject object = doucment.object(); // 转化为对象 - qDebug() << "Processing JSON object for recovery, item" << (i+1) << "of" << m_qlistTemFile.count(); - - //得到恢复文件对应的窗口 - if (object.contains("window")) { // 包含指定的 key - QJsonValue value = object.value("window"); // 获取指定 key 对应的 value - - if (value.isDouble()) { - if (windowIndex == -1) { - windowIndex = static_cast(value.toDouble()); - qDebug() << "First window index found:" << windowIndex; - } else { - if (windowIndex != static_cast(value.toDouble())) { - windowIndex = static_cast(value.toDouble()); - qDebug() << "New window index found:" << windowIndex << ", creating new window"; - window = createWindow(true); - window->showCenterWindow(false); - } - } - } - } - - //得到备份文件路径 - if (object.contains("temFilePath")) { // 包含指定的 key - QJsonValue value = object.value("temFilePath"); // 获取指定 key 对应的 value - - if (value.isString()) { // 判断 value 是否为字符串 - temFilePath = value.toString(); // 将 value 转化为字符串 - qDebug() << "Found temporary file path:" << temFilePath; - } - } - - //得到文件修改状态 - if (object.contains("modify")) { // 包含指定的 key - QJsonValue value = object.value("modify"); // 获取指定 key 对应的 value - - if (value.isBool()) { // 判断 value 是否为字符串 - bIsTemFile = value.toBool(); - qDebug() << "File modification status:" << (bIsTemFile ? "modified" : "unmodified"); - } - } - if (object.contains("lastModifiedTime")) { - auto v = object.value("lastModifiedTime"); - if (v.isString()) { - lastmodifiedtime = v.toString(); - qDebug() << "Last modified time:" << lastmodifiedtime; - } - } - - //得到真实文件路径 - if (object.contains("localPath")) { // 包含指定的 key - QJsonValue value = object.value("localPath"); // 获取指定 key 对应的 value - - if (value.isString()) { // 判断 value 是否为字符串 - localPath = value.toString(); // 将 value 转化为字符串 - fileInfo.setFile(localPath); - qDebug() << "Local file path:" << localPath << "exists:" << fileInfo.exists(); - if (!fileInfo.exists()) { - qWarning() << "Local file does not exist, skipping recovery for:" << localPath; - continue; - } - } - } - - //打开文件 - if (!temFilePath.isEmpty()) { - if (Utils::fileExists(temFilePath)) { - qDebug() << "Opening temporary file:" << temFilePath << "for" << fileInfo.fileName(); - window->addTemFileTab(temFilePath, fileInfo.fileName(), localPath, lastmodifiedtime, bIsTemFile); - - //打开文件后设置书签 - if (object.contains("bookMark")) { // 包含指定的 key - QJsonValue value = object.value("bookMark"); // 获取指定 key 对应的 value - - if (value.isString()) { - QList bookmarkList; - bookmarkList = analyzeBookmakeInfo(value.toString()); - qDebug() << "Setting bookmarks from file config:" << bookmarkList; - window->wrapper(temFilePath)->textEditor()->setBookMarkList(bookmarkList); - } - } else if (m_bookmarkTable.contains(temFilePath)) { - // 若文件已有配置,则以文件为准,否则以全局配置为准 - qDebug() << "Setting bookmarks from global config for:" << temFilePath; - window->wrapper(localPath)->textEditor()->setBookMarkList(m_bookmarkTable.value(temFilePath)); - } - - if (object.contains("focus")) { // 包含指定的 key - QJsonValue value = object.value("focus"); // 获取指定 key 对应的 value - - if (value.isBool() && value.toBool()) { - qDebug() << "Setting focus to file:" << temFilePath; - pFocusWindow = window; - focusPath = temFilePath; - } - } + if (!doucment.isNull() && (jsonError.error == QJsonParseError::NoError) && doucment.isObject()) { + tabRecords.append(doucment.object()); + } + } + qDebug() << "Parsed" << tabRecords.size() << "valid tab records"; + + // 找到焦点标签页 + int focusIndex = -1; + for (int i = 0; i < tabRecords.size(); i++) { + const QJsonObject &obj = tabRecords[i]; + if (obj.contains("focus") && obj.value("focus").toBool()) { + focusIndex = i; + qDebug() << "Found focus tab at index:" << focusIndex; + break; + } + } + // 如果没有焦点标签页,默认第一个 + if (focusIndex == -1 && !tabRecords.isEmpty()) { + focusIndex = 0; + qDebug() << "No focus tab found, using first tab as focus"; + } - recFilesSum++; - } - } else { - if (!localPath.isEmpty() && Utils::fileExists(localPath)) { - qDebug() << "Opening local file:" << localPath; - // 若为草稿文件或不支持的MIMETYPE文件,显示默认名称标签 - if (Utils::isDraftFile(localPath) || !Utils::isMimeTypeSupport(localPath)) { - //得到新建文件名称 - int index = files.indexOf(QFileInfo(localPath).fileName()); - - if (index >= 0) { - QString fileName = tr("Untitled %1").arg(index + 1); - qDebug() << "Using untitled name for draft file:" << fileName; - window->addTemFileTab(localPath, fileName, localPath, lastmodifiedtime, bIsTemFile); - - } - } else { - qDebug() << "Using file name for regular file:" << fileInfo.fileName(); - window->addTemFileTab(localPath, fileInfo.fileName(), localPath, lastmodifiedtime, bIsTemFile); - } + int windowIndex = -1; - //打开文件后设置书签 - if (object.contains("bookMark")) { // 包含指定的 key - QJsonValue value = object.value("bookMark"); // 获取指定 key 对应的 value - - if (value.isString()) { - QList bookmarkList; - bookmarkList = analyzeBookmakeInfo(value.toString()); - qDebug() << "Setting bookmarks from file config:" << bookmarkList; - window->wrapper(localPath)->textEditor()->setBookMarkList(bookmarkList); - } - } else if (m_bookmarkTable.contains(localPath)) { - // 若文件已有配置,则以文件为准,否则以全局配置为准 - qDebug() << "Setting bookmarks from global config for:" << localPath; - window->wrapper(localPath)->textEditor()->setBookMarkList(m_bookmarkTable.value(localPath)); - } + // 记录所有参与恢复的窗口,确保批量结束后都恢复 handleCurrentChanged 的懒加载能力 + QList recoveryWindows; + recoveryWindows.append(window); + window->setBatchAddingPendingTabs(true); + + // 第二阶段:恢复标签页(焦点标签页立即加载,其余懒加载) + for (int i = 0; i < tabRecords.size(); i++) { + const QJsonObject &object = tabRecords[i]; + QString temFilePath; + QString localPath; + bool bIsTemFile = false; + QString lastmodifiedtime; + int cursorPosition = -1; + QList bookmarks; + + qDebug() << "Processing record" << (i + 1) << "of" << tabRecords.size() + << "(focus:" << (i == focusIndex) << ")"; + + // 解析公共字段 + if (object.contains("temFilePath") && object.value("temFilePath").isString()) + temFilePath = object.value("temFilePath").toString(); + + if (object.contains("modify") && object.value("modify").isBool()) + bIsTemFile = object.value("modify").toBool(); + + if (object.contains("lastModifiedTime") && object.value("lastModifiedTime").isString()) + lastmodifiedtime = object.value("lastModifiedTime").toString(); + + if (object.contains("cursorPosition") && object.value("cursorPosition").isString()) + cursorPosition = object.value("cursorPosition").toString().toInt(); + + if (object.contains("bookMark") && object.value("bookMark").isString()) + bookmarks = analyzeBookmakeInfo(object.value("bookMark").toString()); + + if (object.contains("localPath") && object.value("localPath").isString()) { + localPath = object.value("localPath").toString(); + QFileInfo fi(localPath); + qDebug() << "Local file path:" << localPath << "exists:" << fi.exists(); + if (!fi.exists()) { + qWarning() << "Local file does not exist, skipping:" << localPath; + continue; + } + } - if (object.contains("focus")) { // 包含指定的 key - QJsonValue value = object.value("focus"); // 获取指定 key 对应的 value + // 确定实际打开路径和显示名称 + QString openPath; + QString displayName; + if (!temFilePath.isEmpty() && Utils::fileExists(temFilePath)) { + openPath = temFilePath; + displayName = QFileInfo(localPath).fileName(); + } else if (!localPath.isEmpty() && Utils::fileExists(localPath)) { + openPath = localPath; + if (Utils::isDraftFile(localPath) || !Utils::isMimeTypeSupport(localPath)) { + int idx = files.indexOf(QFileInfo(localPath).fileName()); + displayName = (idx >= 0) ? tr("Untitled %1").arg(idx + 1) : QFileInfo(localPath).fileName(); + } else { + displayName = QFileInfo(localPath).fileName(); + } + } else { + continue; + } - if (value.isBool() && value.toBool()) { - qDebug() << "Setting focus to file:" << localPath; - pFocusWindow = window; - focusPath = localPath; - } - } + // 处理多窗口:当窗口索引变化时创建新窗口 + if (object.contains("window") && object.value("window").isDouble()) { + int newWindowIdx = static_cast(object.value("window").toDouble()); + if (windowIndex == -1) { + windowIndex = newWindowIdx; + qDebug() << "First window index:" << windowIndex; + } else if (windowIndex != newWindowIdx) { + windowIndex = newWindowIdx; + qDebug() << "New window index:" << windowIndex << ", creating window"; + window = createWindow(true); + window->showCenterWindow(false); + window->setBatchAddingPendingTabs(true); + recoveryWindows.append(window); + } + } - recFilesSum++; - } - } - qInfo() << "Recovered files count:" << recFilesSum; - qDebug() << "Exit recoverFile"; + bool isFocusTab = (i == focusIndex); + + if (isFocusTab) { + // 焦点标签页:立即完整加载 + window->addTemFileTab(openPath, displayName, localPath, lastmodifiedtime, bIsTemFile, -1, cursorPosition); + + // 恢复书签 + if (!bookmarks.isEmpty()) { + qDebug() << "Setting bookmarks for focus tab:" << bookmarks; + if (auto *w = window->wrapper(openPath)) + w->textEditor()->setBookMarkList(bookmarks); + } else if (m_bookmarkTable.contains(openPath)) { + qDebug() << "Setting bookmarks from global config for focus tab:" << openPath; + if (auto *w = window->wrapper(openPath)) + w->textEditor()->setBookMarkList(m_bookmarkTable.value(openPath)); } + + pFocusWindow = window; + focusPath = openPath; + recFilesSum++; + } else { + // 非焦点标签页:添加为待加载状态(懒加载) + Window::PendingTabInfo pendingInfo; + pendingInfo.filepath = openPath; + pendingInfo.truePath = localPath; + pendingInfo.displayName = displayName; + pendingInfo.cursorPosition = cursorPosition; + pendingInfo.bookmarks = bookmarks; + pendingInfo.isTemFile = bIsTemFile; + pendingInfo.lastModifiedTime = lastmodifiedtime; + window->addPendingTab(pendingInfo); + recFilesSum++; } } + qDebug() << "Pending tabs added, total:" << recFilesSum << "tabs"; + + // 恢复所有参与恢复窗口的 handleCurrentChanged 懒加载能力 + for (Window *w : recoveryWindows) { + w->setBatchAddingPendingTabs(false); + } - //当前活动页 + // 激活焦点标签页 if (pFocusWindow != nullptr && !focusPath.isNull()) { - qDebug() << "Setting focus to window and tab for path:" << focusPath; + qDebug() << "Activating focus tab:" << focusPath; FileTabInfo info; info.windowIndex = m_windows.indexOf(pFocusWindow); info.tabIndex = pFocusWindow->getTabIndex(focusPath); popupExistTabs(info); + } else if (recFilesSum > 0 && window->getTabbar()->count() > 0) { + // 焦点标签页文件不存在或无焦点记录,加载第一个可用标签页 + qDebug() << "No focus tab available, loading first tab"; + window->activeTab(0); } qInfo() << "Recovered files count:" << recFilesSum; diff --git a/src/widgets/window.cpp b/src/widgets/window.cpp index 47f2190a2..01bac8a67 100644 --- a/src/widgets/window.cpp +++ b/src/widgets/window.cpp @@ -386,14 +386,14 @@ void Window::loadIflytekaiassistantConfig() qDebug() << "iflytekaiassistant config dir not exist, return"; return; } - QString key = "base/enable"; + QString configKey = "base/enable"; dir.setFilter(QDir::Files); QStringList nameList = dir.entryList(); for (auto name : nameList) { if (name.contains("-iat") || name.contains("-tts") || name.contains("-trans")) { QString filename = configPath + "/" + name; QSettings file(filename, QSettings::IniFormat); - m_IflytekAiassistantState[name.split(".").first()] = file.value(key).toBool(); + m_IflytekAiassistantState[name.split(".").first()] = file.value(configKey).toBool(); } } qDebug() << "loadIflytekaiassistantConfig end"; @@ -844,6 +844,22 @@ bool Window::closeTab() bool Window::closeTab(const QString &filePath) { qDebug() << "Enter closeTab with path:" << filePath; + + // 懒加载:如果标签页处于待加载状态,直接移除 + if (isPendingTab(filePath)) { + qInfo() << "Closing pending tab:" << filePath; + m_pendingTabs.remove(filePath); + int index = m_tabbar->indexOf(filePath); + if (index >= 0) { + m_tabbar->removeTab(index); + } + // 如果是最后一个标签页,添加空白标签页 + if (m_tabbar->count() == 0) { + addBlankTab(); + } + return true; + } + EditWrapper *wrapper = m_wrappers.value(filePath); if (!wrapper) { qWarning() << "No wrapper found for path:" << filePath; @@ -2835,7 +2851,12 @@ void Window::backupFile() QString filePath, localPath, curPos; QFileInfo fileInfo; m_qlistTemFile.clear(); - m_qlistTemFile = wrappers.keys(); + + // 懒加载兼容:m_qlistTemFile 的索引必须与 TabBar 标签顺序一一对应。 + // 先按 TabBar 顺序添加所有标签页路径(包括 pending 的),再逐一填入 JSON 数据。 + for (int i = 0; i < m_tabbar->count(); i++) { + m_qlistTemFile.append(m_tabbar->fileAt(i)); + } for (EditWrapper *wrapper : wrappers) { if (nullptr == wrapper) { @@ -2858,7 +2879,12 @@ void Window::backupFile() } qInfo() << "begin backupFile()"; - StartManager::FileTabInfo tabInfo = StartManager::instance()->getFileTabInfo(filePath); + // 使用当前窗口的 TabBar 索引,避免跨窗口索引错位 + int tabIndex = m_tabbar->indexOf(filePath); + if (tabIndex < 0) { + qWarning() << "backupFile: tab not found for path:" << filePath << ", skip"; + continue; + } curPos = QString::number(wrapper->textEditor()->textCursor().position()); fileInfo.setFile(localPath); @@ -2910,7 +2936,49 @@ void Window::backupFile() //使用json串形式保存 document.setObject(jsonObject); QByteArray byteArray = document.toJson(QJsonDocument::Compact); - m_qlistTemFile.replace(tabInfo.tabIndex, byteArray); + // 安全替换:确保索引不越界 + if (tabIndex < m_qlistTemFile.size()) { + m_qlistTemFile.replace(tabIndex, byteArray); + } else { + qWarning() << "backupFile: tabIndex" << tabIndex << "out of range (size" << m_qlistTemFile.size() << "), appending"; + m_qlistTemFile.append(byteArray); + } + } + + // 懒加载兼容:为 pending 标签页生成备份记录 + for (int i = 0; i < m_tabbar->count(); i++) { + QString tabPath = m_tabbar->fileAt(i); + if (!m_wrappers.contains(tabPath) && m_pendingTabs.contains(tabPath)) { + const PendingTabInfo &info = m_pendingTabs.value(tabPath); + QJsonObject jsonObject; + QJsonDocument document; + jsonObject.insert("localPath", info.truePath); + if (!info.filepath.isEmpty() && info.filepath != info.truePath) { + jsonObject.insert("temFilePath", info.filepath); + } + jsonObject.insert("cursorPosition", QString::number(info.cursorPosition)); + jsonObject.insert("modify", info.isTemFile); + if (!info.lastModifiedTime.isEmpty()) + jsonObject.insert("lastModifiedTime", info.lastModifiedTime); + if (!info.bookmarks.isEmpty()) { + QString bookmarkInfo; + for (int j = 0; j < info.bookmarks.count(); j++) { + bookmarkInfo.append((j == info.bookmarks.count() - 1) + ? QString::number(info.bookmarks.value(j)) + : QString::number(info.bookmarks.value(j)) + ","); + } + jsonObject.insert("bookMark", bookmarkInfo); + } + if (tabPath == m_tabbar->currentPath()) + jsonObject.insert("focus", true); + document.setObject(jsonObject); + QByteArray byteArray = document.toJson(QJsonDocument::Compact); + if (i < m_qlistTemFile.size()) { + m_qlistTemFile.replace(i, byteArray); + } else { + m_qlistTemFile.append(byteArray); + } + } } //将json串列表写入配置文件 @@ -2928,6 +2996,17 @@ void Window::backupFile() bool Window::closeAllFiles() { qInfo() << "begin closeAllFiles()"; + + // 先关闭所有待加载标签页(无需保存提示) + QMap pendingCopy = m_pendingTabs; + for (const QString &path : pendingCopy.keys()) { + m_pendingTabs.remove(path); + int idx = m_tabbar->indexOf(path); + if (idx >= 0) { + m_tabbar->removeTab(idx); + } + } + QMap wrappers = m_wrappers; // 被删除的窗口索引已变更,需要计算其范围 @@ -2996,7 +3075,8 @@ bool Window::saveAllFloatingFiles() * @param qstrTruePath 最后一次修改时间 * @param bIsTemFile 是否修改 */ -void Window::addTemFileTab(const QString &qstrPath, const QString &qstrName, const QString &qstrTruePath, const QString &lastModifiedTime, bool bIsTemFile) +void Window::addTemFileTab(const QString &qstrPath, const QString &qstrName, const QString &qstrTruePath, const QString &lastModifiedTime, + bool bIsTemFile, int index, int restoreCursorPosition) { qInfo() << "begin addTemFileTab()"; if (qstrPath.isEmpty() || !Utils::fileExists(qstrPath)) { @@ -3022,7 +3102,13 @@ void Window::addTemFileTab(const QString &qstrPath, const QString &qstrName, con } EditWrapper *wrapper = createEditor(); - m_tabbar->addTab(qstrPath, qstrName, qstrTruePath); + if (index < 0 || index > m_tabbar->count()) { + index = m_tabbar->count(); + } + m_tabbar->addTabWithIndex(index, qstrPath, qstrName, qstrTruePath); + if (restoreCursorPosition >= 0) { + wrapper->setRestoreCursorPosition(restoreCursorPosition); + } wrapper->openFile(qstrPath, qstrTruePath, bIsTemFile); // 查找文件是否存在书签,临时文件同样可标记书签 @@ -3039,6 +3125,72 @@ void Window::addTemFileTab(const QString &qstrPath, const QString &qstrName, con qInfo() << "end addTemFileTab()"; } +void Window::addPendingTab(const PendingTabInfo &info, int index) +{ + qInfo() << "addPendingTab:" << info.filepath << info.displayName; + // 阻止信号:addTab 内部会调用 setCurrentIndex 触发 currentChanged, + // 导致 handleCurrentChanged 立即加载 pending 标签页,懒加载失效 + m_tabbar->blockSignals(true); + if (index < 0 || index > m_tabbar->count()) { + index = m_tabbar->count(); + } + m_tabbar->addTabWithIndex(index, info.filepath, info.displayName, info.truePath); + m_tabbar->blockSignals(false); + m_pendingTabs[info.filepath] = info; +} + +void Window::setBatchAddingPendingTabs(bool enabled) +{ + m_bBatchAddingPendingTabs = enabled; +} + +bool Window::isPendingTab(const QString &filepath) const +{ + return m_pendingTabs.contains(filepath); +} + +bool Window::loadPendingTab(const QString &filepath) +{ + if (!m_pendingTabs.contains(filepath)) { + return false; + } + + PendingTabInfo info = m_pendingTabs.take(filepath); + qInfo() << "loadPendingTab: loading" << info.filepath; + + if (!Utils::fileExists(info.filepath)) { + qWarning() << "loadPendingTab: file does not exist" << info.filepath; + m_pendingTabs.insert(info.filepath, info); + return false; + } + + // 先移除 TabBar 上的占位标签,避免 addTemFileTab 重复添加 + int existingIndex = m_tabbar->indexOf(info.filepath); + if (existingIndex >= 0) { + m_tabbar->removeTab(existingIndex); + } + + // 完整加载文件内容并添加标签 + addTemFileTab(info.filepath, info.displayName, info.truePath, info.lastModifiedTime, info.isTemFile, existingIndex, info.cursorPosition); + + // 检查文件是否成功加载(addTemFileTab 可能因 MimeType 不支持等原因 early return) + EditWrapper *wrapper = m_wrappers.value(info.filepath); + if (!wrapper) { + qWarning() << "loadPendingTab: addTemFileTab failed, restoring pending info"; + // 恢复 pending 信息和占位标签 + m_pendingTabs.insert(info.filepath, info); + addPendingTab(info, existingIndex); + return false; + } + + // 覆盖书签(记录中的书签优先于全局书签) + if (!info.bookmarks.isEmpty()) { + wrapper->textEditor()->setBookMarkList(info.bookmarks); + } + + return true; +} + QMap Window::getWrappers() { return m_wrappers; @@ -3195,6 +3347,17 @@ void Window::handleCurrentChanged(const int &index) const QString &filepath = m_tabbar->fileAt(index); + // 懒加载:如果标签页处于待加载状态,先触发完整加载 + // 批量添加 pending 标签页期间跳过(addPendingTab 会触发 currentChanged) + if (!m_bBatchAddingPendingTabs && isPendingTab(filepath)) { + if (!loadPendingTab(filepath)) { + // 文件不存在或加载失败,移除该标签页 + qWarning() << "Failed to load pending tab:" << filepath; + handleTabCloseRequested(index); + return; + } + } + if (m_wrappers.contains(filepath)) { bool bIsContains = false; EditWrapper *wrapper = m_wrappers.value(filepath); diff --git a/src/widgets/window.h b/src/widgets/window.h index b3a36d23c..a0f9471d1 100644 --- a/src/widgets/window.h +++ b/src/widgets/window.h @@ -120,8 +120,29 @@ class Window : public DMainWindow bool closeAllFiles(); // 保存窗口中所有漂浮文件(本地文件已被删除) bool saveAllFloatingFiles(); + // 待加载标签页信息(懒加载恢复用) + struct PendingTabInfo { + QString filepath; // 标签页显示路径(可能是临时文件路径) + QString truePath; // 真实文件路径 + QString displayName; // 标签页显示名称 + int cursorPosition = -1; // 光标位置(-1 表示不指定) + QList bookmarks; // 书签列表 + bool isTemFile = false; // 是否为临时备份文件 + QString lastModifiedTime; // 最后修改时间(临时文件用) + }; + + // 添加待加载标签页(懒加载,仅在 TabBar 添加标签,不加载文件内容) + void addPendingTab(const PendingTabInfo &info, int index = -1); + // 检查指定路径是否为待加载标签页 + bool isPendingTab(const QString &filepath) const; + // 加载指定路径的待加载标签页,返回是否成功加载 + bool loadPendingTab(const QString &filepath); + // 设置/获取批量添加 pending 标签页模式(阻止 currentChanged 触发懒加载) + void setBatchAddingPendingTabs(bool enabled); + // 恢复备份文件标签页 - void addTemFileTab(const QString &qstrPath, const QString &qstrName, const QString &qstrTruePath, const QString &lastModifiedTime, bool bIsTemFile = false); + void addTemFileTab(const QString &qstrPath, const QString &qstrName, const QString &qstrTruePath, const QString &lastModifiedTime, + bool bIsTemFile = false, int index = -1, int restoreCursorPosition = -1); QMap getWrappers(); @@ -261,6 +282,8 @@ public Q_SLOTS: Settings *m_settings {nullptr}; QMap m_wrappers; + QMap m_pendingTabs; // 待加载标签页(懒加载) + bool m_bBatchAddingPendingTabs = false; // 批量添加 pending 标签页期间,阻止 handleCurrentChanged 触发加载 DMenu *m_menu {nullptr}; diff --git a/tests/src/widgets/ut_window.cpp b/tests/src/widgets/ut_window.cpp index ec68eaaaa..570ce9ef2 100644 --- a/tests/src/widgets/ut_window.cpp +++ b/tests/src/widgets/ut_window.cpp @@ -7,6 +7,8 @@ #include "ddialog.h" #include "qfileinfo.h" #include "qfile.h" +#include +#include int exec_ret = 1; int QDialog_exec_stub() @@ -1073,6 +1075,30 @@ TEST(UT_Window_backupFile, UT_Window_backupFile) } +TEST(UT_Window_backupFile, UT_Window_backupFile_pendingTabKeepsTempPath) +{ + Window *window = new Window(); + + Window::PendingTabInfo info; + info.filepath = "/tmp/deepin-editor-pending-backup.tmp"; + info.truePath = "/tmp/deepin-editor-pending-backup.txt"; + info.displayName = "deepin-editor-pending-backup.txt"; + info.cursorPosition = 42; + info.isTemFile = true; + window->addPendingTab(info); + + window->backupFile(); + + ASSERT_EQ(window->m_qlistTemFile.size(), 1); + QJsonDocument doc = QJsonDocument::fromJson(window->m_qlistTemFile.first().toUtf8()); + ASSERT_TRUE(doc.isObject()); + EXPECT_EQ(doc.object().value("localPath").toString(), info.truePath); + EXPECT_EQ(doc.object().value("temFilePath").toString(), info.filepath); + EXPECT_EQ(doc.object().value("cursorPosition").toString(), QString::number(info.cursorPosition)); + + window->deleteLater(); +} + //void closeAllFiles(); TEST(UT_Window_closeAllFiles, UT_Window_closeAllFiles) { @@ -1100,6 +1126,46 @@ TEST(UT_Window_addTemFileTab, UT_Window_addTemFileTab) } + +TEST(UT_Window_addPendingTab, UT_Window_addPendingTab_keepsRequestedIndex) +{ + Window *window = new Window(); + + Window::PendingTabInfo first; + first.filepath = "/tmp/deepin-editor-first.txt"; + first.truePath = first.filepath; + first.displayName = "first.txt"; + window->addPendingTab(first); + + Window::PendingTabInfo second; + second.filepath = "/tmp/deepin-editor-second.txt"; + second.truePath = second.filepath; + second.displayName = "second.txt"; + window->addPendingTab(second, 0); + + EXPECT_EQ(window->m_tabbar->fileAt(0), second.filepath); + EXPECT_EQ(window->m_tabbar->fileAt(1), first.filepath); + + window->deleteLater(); +} + +TEST(UT_Window_loadPendingTab, UT_Window_loadPendingTab_missingFileCanStillClose) +{ + Window *window = new Window(); + + Window::PendingTabInfo info; + info.filepath = "/tmp/deepin-editor-missing-pending.txt"; + info.truePath = info.filepath; + info.displayName = "missing.txt"; + window->addPendingTab(info); + + EXPECT_FALSE(window->loadPendingTab(info.filepath)); + EXPECT_TRUE(window->isPendingTab(info.filepath)); + EXPECT_TRUE(window->closeTab(info.filepath)); + EXPECT_FALSE(window->isPendingTab(info.filepath)); + + window->deleteLater(); +} //Window(DMainWindow *parent = nullptr); //~Window() override; @@ -2198,4 +2264,3 @@ TEST(UT_Window_rehighlightPrintDoc, rehighlightPrintDoc_HighlightCpp_pass) w->deleteLater(); } -