From d16905c0a3b682f300660eff8885ded63eddc93c Mon Sep 17 00:00:00 2001 From: landaiqing Date: Fri, 12 Dec 2025 22:56:22 +0800 Subject: [PATCH] :bug: Fixed some known issues --- frontend/src/stores/documentStore.ts | 10 ++ frontend/src/stores/tabStore.ts | 73 ++++++--- .../extensions/codeblock/decorations.ts | 18 ++- .../editor/extensions/minimap/blockManager.ts | 152 +++++++++++++----- .../extensions/minimap/worker/block.worker.ts | 20 ++- .../extensions/minimap/worker/protocol.ts | 13 +- 6 files changed, 214 insertions(+), 72 deletions(-) diff --git a/frontend/src/stores/documentStore.ts b/frontend/src/stores/documentStore.ts index 0fd1686..b4a353b 100644 --- a/frontend/src/stores/documentStore.ts +++ b/frontend/src/stores/documentStore.ts @@ -158,6 +158,10 @@ export const useDocumentStore = defineStore('document', () => { currentDocument.value.updatedAt = new Date().toISOString(); } + // 同步更新标签页标题 + const tabStore = useTabStore(); + tabStore.updateTabTitle(docId, title); + return true; } catch (error) { console.error('Failed to update document metadata:', error); @@ -178,6 +182,12 @@ export const useDocumentStore = defineStore('document', () => { // 更新本地状态 delete documents.value[docId]; + // 同步清理标签页 + const tabStore = useTabStore(); + if (tabStore.hasTab(docId)) { + tabStore.closeTab(docId); + } + // 如果删除的是当前文档,切换到第一个可用文档 if (currentDocumentId.value === docId) { const availableDocs = Object.values(documents.value); diff --git a/frontend/src/stores/tabStore.ts b/frontend/src/stores/tabStore.ts index 9c95948..4d18012 100644 --- a/frontend/src/stores/tabStore.ts +++ b/frontend/src/stores/tabStore.ts @@ -15,7 +15,7 @@ export const useTabStore = defineStore('tab', () => { const documentStore = useDocumentStore(); // === 核心状态 === - const tabsMap = ref>(new Map()); + const tabsMap = ref>({}); const tabOrder = ref([]); // 维护标签页顺序 const draggedTabId = ref(null); @@ -28,21 +28,21 @@ export const useTabStore = defineStore('tab', () => { // 按顺序返回标签页数组(用于UI渲染) const tabs = computed(() => { return tabOrder.value - .map(documentId => tabsMap.value.get(documentId)) + .map(documentId => tabsMap.value[documentId]) .filter(tab => tab !== undefined) as Tab[]; }); // === 私有方法 === const hasTab = (documentId: number): boolean => { - return tabsMap.value.has(documentId); + return documentId in tabsMap.value; }; const getTab = (documentId: number): Tab | undefined => { - return tabsMap.value.get(documentId); + return tabsMap.value[documentId]; }; const updateTabTitle = (documentId: number, title: string) => { - const tab = tabsMap.value.get(documentId); + const tab = tabsMap.value[documentId]; if (tab) { tab.title = title; } @@ -67,7 +67,7 @@ export const useTabStore = defineStore('tab', () => { title: document.title }; - tabsMap.value.set(documentId, newTab); + tabsMap.value[documentId] = newTab; tabOrder.value.push(documentId); }; @@ -81,7 +81,7 @@ export const useTabStore = defineStore('tab', () => { if (tabIndex === -1) return; // 从映射和顺序数组中移除 - tabsMap.value.delete(documentId); + delete tabsMap.value[documentId]; tabOrder.value.splice(tabIndex, 1); // 如果关闭的是当前文档,需要切换到其他文档 @@ -111,7 +111,7 @@ export const useTabStore = defineStore('tab', () => { if (tabIndex === -1) return; // 从映射和顺序数组中移除 - tabsMap.value.delete(documentId); + delete tabsMap.value[documentId]; tabOrder.value.splice(tabIndex, 1); }); }; @@ -121,12 +121,12 @@ export const useTabStore = defineStore('tab', () => { */ const switchToTabAndDocument = (documentId: number) => { if (!hasTab(documentId)) return; - + // 如果点击的是当前已激活的文档,不需要重复请求 if (documentStore.currentDocumentId === documentId) { return; } - + documentStore.openDocument(documentId); }; @@ -150,10 +150,31 @@ export const useTabStore = defineStore('tab', () => { return tabOrder.value.indexOf(documentId); }; + /** + * 验证并清理无效的标签页 + */ + const validateTabs = () => { + const validDocIds = Object.keys(documentStore.documents).map(Number); + + // 找出无效的标签页(文档已被删除) + const invalidTabIds = tabOrder.value.filter(docId => !validDocIds.includes(docId)); + + if (invalidTabIds.length > 0) { + // 批量清理无效标签页 + invalidTabIds.forEach(docId => { + delete tabsMap.value[docId]; + }); + tabOrder.value = tabOrder.value.filter(docId => validDocIds.includes(docId)); + } + }; + /** * 初始化标签页(当前文档) */ const initializeTab = () => { + // 先验证并清理无效的标签页(处理持久化的脏数据) + validateTabs(); + if (isTabsEnabled.value) { const currentDoc = documentStore.currentDocument; if (currentDoc) { @@ -169,13 +190,13 @@ export const useTabStore = defineStore('tab', () => { */ const closeOtherTabs = (keepDocumentId: number) => { if (!hasTab(keepDocumentId)) return; - + // 获取所有其他标签页的ID const otherTabIds = tabOrder.value.filter(id => id !== keepDocumentId); - + // 批量关闭其他标签页 closeTabs(otherTabIds); - + // 如果当前打开的文档在被关闭的标签中,需要切换到保留的文档 if (otherTabIds.includes(documentStore.currentDocumentId!)) { switchToTabAndDocument(keepDocumentId); @@ -188,13 +209,13 @@ export const useTabStore = defineStore('tab', () => { const closeTabsToRight = (documentId: number) => { const index = getTabIndex(documentId); if (index === -1) return; - + // 获取右侧所有标签页的ID const rightTabIds = tabOrder.value.slice(index + 1); - + // 批量关闭右侧标签页 closeTabs(rightTabIds); - + // 如果当前打开的文档在被关闭的右侧标签中,需要切换到指定的文档 if (rightTabIds.includes(documentStore.currentDocumentId!)) { switchToTabAndDocument(documentId); @@ -207,13 +228,13 @@ export const useTabStore = defineStore('tab', () => { const closeTabsToLeft = (documentId: number) => { const index = getTabIndex(documentId); if (index <= 0) return; - + // 获取左侧所有标签页的ID const leftTabIds = tabOrder.value.slice(0, index); - + // 批量关闭左侧标签页 closeTabs(leftTabIds); - + // 如果当前打开的文档在被关闭的左侧标签中,需要切换到指定的文档 if (leftTabIds.includes(documentStore.currentDocumentId!)) { switchToTabAndDocument(documentId); @@ -224,12 +245,15 @@ export const useTabStore = defineStore('tab', () => { * 清空所有标签页 */ const clearAllTabs = () => { - tabsMap.value.clear(); + tabsMap.value = {}; tabOrder.value = []; }; // === 公共API === return { + tabsMap, + tabOrder, + // 状态 tabs: readonly(tabs), draggedTabId, @@ -251,9 +275,16 @@ export const useTabStore = defineStore('tab', () => { initializeTab, clearAllTabs, updateTabTitle, + validateTabs, // 工具方法 hasTab, getTab }; -}); \ No newline at end of file +}, { + persist: { + key: 'voidraft-tabs', + storage: localStorage, + pick: ['tabsMap', 'tabOrder'], + }, +}); diff --git a/frontend/src/views/editor/extensions/codeblock/decorations.ts b/frontend/src/views/editor/extensions/codeblock/decorations.ts index 3b78782..174f2c1 100644 --- a/frontend/src/views/editor/extensions/codeblock/decorations.ts +++ b/frontend/src/views/editor/extensions/codeblock/decorations.ts @@ -150,15 +150,19 @@ const blockLayer = layer({ // 转换为视口坐标进行后续计算 const fromCoordsTop = fromLineBlock.top + view.documentTop; let toCoordsBottom = toLineBlock.bottom + view.documentTop; - - // 对最后一个块进行特殊处理,让它直接延伸到底部 + if (idx === blocks.length - 1) { - const editorHeight = view.dom.clientHeight; - const contentBottom = toCoordsBottom - view.documentTop + view.documentPadding.top; + // 计算需要添加到最后一个块的额外高度,以覆盖 scrollPastEnd 添加的额外滚动空间 + // scrollPastEnd 会在文档底部添加相当于 scrollDOM.clientHeight 的额外空间 + // 当滚动到最底部时,顶部仍会显示一行(defaultLineHeight),需要减去这部分 + const editorHeight = view.scrollDOM.clientHeight; + const extraHeight = editorHeight - ( + view.defaultLineHeight + // 当滚动到最底部时,顶部仍显示一行 + view.documentPadding.top + + 8 // 额外的边距调整 + ); - // 让最后一个块直接延伸到编辑器底部 - if (contentBottom < editorHeight) { - const extraHeight = editorHeight - contentBottom - 10; + if (extraHeight > 0) { toCoordsBottom += extraHeight; } } diff --git a/frontend/src/views/editor/extensions/minimap/blockManager.ts b/frontend/src/views/editor/extensions/minimap/blockManager.ts index 85fc48b..eba781c 100644 --- a/frontend/src/views/editor/extensions/minimap/blockManager.ts +++ b/frontend/src/views/editor/extensions/minimap/blockManager.ts @@ -11,6 +11,7 @@ import { Highlight, LineSpan, FontInfo, + UpdateFontInfoRequest, } from './worker/protocol'; import crelt from 'crelt'; @@ -26,6 +27,11 @@ interface Block { rendering: boolean; requestId: number; lastUsed: number; // LRU 时间戳 + // 高亮缓存 + cachedHighlights: Highlight[] | null; + cachedLines: LineSpan[][] | null; + cachedTextSlice: string | null; + cachedTextOffset: number; } export class BlockManager { @@ -34,6 +40,7 @@ export class BlockManager { private fontInfoMap = new Map(); private fontDirty = true; private fontVersion = 0; + private sentFontTags = new Set(); // 已发送给 Worker 的字体标签 private measureCache: { charWidth: number; lineHeight: number; version: number } | null = null; private displayText: 'blocks' | 'characters' = 'characters'; private themeClasses: Set; @@ -150,6 +157,10 @@ export class BlockManager { markAllDirty(): void { for (const block of this.blocks.values()) { block.dirty = true; + // 清除缓存,强制重新收集数据 + block.cachedHighlights = null; + block.cachedLines = null; + block.cachedTextSlice = null; } } @@ -185,11 +196,19 @@ export class BlockManager { this.blocks.delete(index); } else if (affectedBlocks.has(index)) { block.dirty = true; + // 清除缓存 + block.cachedHighlights = null; + block.cachedLines = null; + block.cachedTextSlice = null; if (hasLineCountChange) { markRest = true; // 从这个块开始,后续块都需要更新 } } else if (markRest) { block.dirty = true; + // 清除缓存 + block.cachedHighlights = null; + block.cachedLines = null; + block.cachedTextSlice = null; } } @@ -320,6 +339,10 @@ export class BlockManager { rendering: false, requestId: 0, lastUsed: now, + cachedHighlights: null, + cachedLines: null, + cachedTextSlice: null, + cachedTextOffset: 0, }; this.blocks.set(index, block); } else { @@ -344,51 +367,65 @@ export class BlockManager { this.renderingCount++; const { startLine, endLine } = block; - const linesSnapshot = getLinesSnapshot(state); - const tree = syntaxTree(state); - // Collect highlights - const highlights: Highlight[] = []; - if (tree.length > 0 && startLine <= state.doc.lines) { - const highlighter: Highlighter = { - style: (tags) => highlightingFor(state, tags), - }; - const startPos = state.doc.line(startLine).from; - const endPos = state.doc.line(Math.min(endLine, state.doc.lines)).to; + let highlights: Highlight[]; + let lines: LineSpan[][]; + let textSlice: string; + let textOffset: number; - highlightTree(tree, highlighter, (from, to, tags) => { - highlights.push({ from, to, tags }); - }, startPos, endPos); - } + // 只有当块是 dirty 时才重新收集数据,否则使用缓存 + if (block.dirty || !block.cachedHighlights) { + const linesSnapshot = getLinesSnapshot(state); + const tree = syntaxTree(state); - // Extract relevant lines - const startIdx = startLine - 1; - const endIdx = Math.min(endLine, linesSnapshot.length); - const lines: LineSpan[][] = linesSnapshot.slice(startIdx, endIdx).map(line => - line.map(span => ({ from: span.from, to: span.to, folded: span.folded })) - ); + // Collect highlights + highlights = []; + if (tree.length > 0 && startLine <= state.doc.lines) { + const highlighter: Highlighter = { + style: (tags) => highlightingFor(state, tags), + }; + const startPos = state.doc.line(startLine).from; + const endPos = state.doc.line(Math.min(endLine, state.doc.lines)).to; - // Get text slice - let textOffset = 0; - let textEnd = 0; - if (lines.length > 0 && lines[0].length > 0) { - textOffset = lines[0][0].from; - const lastLine = lines[lines.length - 1]; - if (lastLine.length > 0) { - textEnd = lastLine[lastLine.length - 1].to; + highlightTree(tree, highlighter, (from, to, tags) => { + highlights.push({ from, to, tags }); + }, startPos, endPos); } - } - const textSlice = state.doc.sliceString(textOffset, textEnd); - // Build font info map - const fontInfoMap: Record = {}; - for (const hl of highlights) { - if (!fontInfoMap[hl.tags]) { - const info = this.getFontInfo(hl.tags); - fontInfoMap[hl.tags] = info; + // Extract relevant lines + const startIdx = startLine - 1; + const endIdx = Math.min(endLine, linesSnapshot.length); + lines = linesSnapshot.slice(startIdx, endIdx).map(line => + line.map(span => ({ from: span.from, to: span.to, folded: span.folded })) + ); + + // Get text slice + textOffset = 0; + let textEnd = 0; + if (lines.length > 0 && lines[0].length > 0) { + textOffset = lines[0][0].from; + const lastLine = lines[lines.length - 1]; + if (lastLine.length > 0) { + textEnd = lastLine[lastLine.length - 1].to; + } } + textSlice = state.doc.sliceString(textOffset, textEnd); + + // 缓存数据 + block.cachedHighlights = highlights; + block.cachedLines = lines; + block.cachedTextSlice = textSlice; + block.cachedTextOffset = textOffset; + } else { + // 使用缓存的数据 + highlights = block.cachedHighlights; + lines = block.cachedLines!; + textSlice = block.cachedTextSlice!; + textOffset = block.cachedTextOffset; } - fontInfoMap[''] = this.getFontInfo(''); + + // 确保字体信息已发送给 Worker + this.ensureFontInfoSent(highlights); const blockLines = endLine - startLine + 1; const request: BlockRequest = { @@ -403,8 +440,6 @@ export class BlockManager { lines, textSlice, textOffset, - fontInfoMap, - defaultFont: fontInfoMap[''], displayText: this.displayText, charWidth, lineHeight, @@ -414,6 +449,43 @@ export class BlockManager { this.worker.postMessage(request); } + /** + * 确保字体信息已发送给 Worker + * 增量发送:只发送新的标签 + */ + private ensureFontInfoSent(highlights: Highlight[]): void { + if (!this.worker) return; + + // 收集新的标签 + const newTags: string[] = []; + for (const hl of highlights) { + if (!this.sentFontTags.has(hl.tags)) { + newTags.push(hl.tags); + } + } + // 默认字体标签 + if (!this.sentFontTags.has('')) { + newTags.push(''); + } + + // 如果没有新标签,不需要发送 + if (newTags.length === 0) return; + + // 构建新标签的字体信息 + const fontInfoMap: Record = {}; + for (const tag of newTags) { + fontInfoMap[tag] = this.getFontInfo(tag); + this.sentFontTags.add(tag); + } + + const updateRequest: UpdateFontInfoRequest = { + type: 'updateFontInfo', + fontInfoMap, + defaultFont: this.getFontInfo(''), + }; + this.worker.postMessage(updateRequest); + } + private evictOldBlocks(): void { if (this.blocks.size <= MAX_BLOCKS) return; @@ -432,6 +504,7 @@ export class BlockManager { private refreshFontCache(): void { this.fontInfoMap.clear(); this.measureCache = null; + this.sentFontTags.clear(); // 需要重新发送字体信息给 Worker // 注意:fontDirty 在成功渲染块后才设为 false this.fontVersion++; this.markAllDirty(); @@ -496,3 +569,4 @@ export class BlockManager { } } + diff --git a/frontend/src/views/editor/extensions/minimap/worker/block.worker.ts b/frontend/src/views/editor/extensions/minimap/worker/block.worker.ts index 9c24c11..8943ba3 100644 --- a/frontend/src/views/editor/extensions/minimap/worker/block.worker.ts +++ b/frontend/src/views/editor/extensions/minimap/worker/block.worker.ts @@ -6,6 +6,10 @@ import { FontInfo, } from './protocol'; +// 缓存字体信息,只在主题变化时更新 +let cachedFontInfoMap: Record = {}; +let cachedDefaultFont: FontInfo = { color: '#000', font: '12px monospace', lineHeight: 14 }; + function post(msg: ToMainMessage, transfer?: Transferable[]): void { self.postMessage(msg, { transfer }); } @@ -107,14 +111,16 @@ function renderBlock(request: BlockRequest): void { endLine, width, height, - fontInfoMap, - defaultFont, displayText, charWidth, lineHeight, gutterOffset, } = request; + // 使用缓存的字体信息 + const fontInfoMap = cachedFontInfoMap; + const defaultFont = cachedDefaultFont; + // Create OffscreenCanvas for this block const canvas = new OffscreenCanvas(width, height); const ctx = canvas.getContext('2d'); @@ -245,12 +251,22 @@ function drawTextBlocks( function handleMessage(msg: ToWorkerMessage): void { switch (msg.type) { case 'init': + // 重置字体缓存 + cachedFontInfoMap = {}; + cachedDefaultFont = { color: '#000', font: '12px monospace', lineHeight: 14 }; post({ type: 'ready' }); break; + case 'updateFontInfo': + // 增量合并字体信息 + Object.assign(cachedFontInfoMap, msg.fontInfoMap); + cachedDefaultFont = msg.defaultFont; + break; case 'renderBlock': renderBlock(msg); break; case 'destroy': + // 清理缓存 + cachedFontInfoMap = {}; break; } } diff --git a/frontend/src/views/editor/extensions/minimap/worker/protocol.ts b/frontend/src/views/editor/extensions/minimap/worker/protocol.ts index f459229..75343e6 100644 --- a/frontend/src/views/editor/extensions/minimap/worker/protocol.ts +++ b/frontend/src/views/editor/extensions/minimap/worker/protocol.ts @@ -25,6 +25,15 @@ export interface FontInfo { lineHeight: number; } +/** + * 更新字体信息(主题变化时发送一次) + */ +export interface UpdateFontInfoRequest { + type: 'updateFontInfo'; + fontInfoMap: Record; + defaultFont: FontInfo; +} + export interface BlockRequest { type: 'renderBlock'; blockId: number; @@ -37,8 +46,6 @@ export interface BlockRequest { lines: LineSpan[][]; textSlice: string; textOffset: number; - fontInfoMap: Record; - defaultFont: FontInfo; displayText: 'blocks' | 'characters'; charWidth: number; lineHeight: number; @@ -53,7 +60,7 @@ export interface DestroyRequest { type: 'destroy'; } -export type ToWorkerMessage = BlockRequest | InitRequest | DestroyRequest; +export type ToWorkerMessage = BlockRequest | InitRequest | DestroyRequest | UpdateFontInfoRequest; export interface BlockComplete { type: 'blockComplete';