🐛 Fixed some known issues

This commit is contained in:
2025-12-12 22:56:22 +08:00
parent 4e611db349
commit d16905c0a3
6 changed files with 214 additions and 72 deletions

View File

@@ -158,6 +158,10 @@ export const useDocumentStore = defineStore('document', () => {
currentDocument.value.updatedAt = new Date().toISOString(); currentDocument.value.updatedAt = new Date().toISOString();
} }
// 同步更新标签页标题
const tabStore = useTabStore();
tabStore.updateTabTitle(docId, title);
return true; return true;
} catch (error) { } catch (error) {
console.error('Failed to update document metadata:', error); console.error('Failed to update document metadata:', error);
@@ -178,6 +182,12 @@ export const useDocumentStore = defineStore('document', () => {
// 更新本地状态 // 更新本地状态
delete documents.value[docId]; delete documents.value[docId];
// 同步清理标签页
const tabStore = useTabStore();
if (tabStore.hasTab(docId)) {
tabStore.closeTab(docId);
}
// 如果删除的是当前文档,切换到第一个可用文档 // 如果删除的是当前文档,切换到第一个可用文档
if (currentDocumentId.value === docId) { if (currentDocumentId.value === docId) {
const availableDocs = Object.values(documents.value); const availableDocs = Object.values(documents.value);

View File

@@ -15,7 +15,7 @@ export const useTabStore = defineStore('tab', () => {
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
// === 核心状态 === // === 核心状态 ===
const tabsMap = ref<Map<number, Tab>>(new Map()); const tabsMap = ref<Record<number, Tab>>({});
const tabOrder = ref<number[]>([]); // 维护标签页顺序 const tabOrder = ref<number[]>([]); // 维护标签页顺序
const draggedTabId = ref<number | null>(null); const draggedTabId = ref<number | null>(null);
@@ -28,21 +28,21 @@ export const useTabStore = defineStore('tab', () => {
// 按顺序返回标签页数组用于UI渲染 // 按顺序返回标签页数组用于UI渲染
const tabs = computed(() => { const tabs = computed(() => {
return tabOrder.value return tabOrder.value
.map(documentId => tabsMap.value.get(documentId)) .map(documentId => tabsMap.value[documentId])
.filter(tab => tab !== undefined) as Tab[]; .filter(tab => tab !== undefined) as Tab[];
}); });
// === 私有方法 === // === 私有方法 ===
const hasTab = (documentId: number): boolean => { const hasTab = (documentId: number): boolean => {
return tabsMap.value.has(documentId); return documentId in tabsMap.value;
}; };
const getTab = (documentId: number): Tab | undefined => { const getTab = (documentId: number): Tab | undefined => {
return tabsMap.value.get(documentId); return tabsMap.value[documentId];
}; };
const updateTabTitle = (documentId: number, title: string) => { const updateTabTitle = (documentId: number, title: string) => {
const tab = tabsMap.value.get(documentId); const tab = tabsMap.value[documentId];
if (tab) { if (tab) {
tab.title = title; tab.title = title;
} }
@@ -67,7 +67,7 @@ export const useTabStore = defineStore('tab', () => {
title: document.title title: document.title
}; };
tabsMap.value.set(documentId, newTab); tabsMap.value[documentId] = newTab;
tabOrder.value.push(documentId); tabOrder.value.push(documentId);
}; };
@@ -81,7 +81,7 @@ export const useTabStore = defineStore('tab', () => {
if (tabIndex === -1) return; if (tabIndex === -1) return;
// 从映射和顺序数组中移除 // 从映射和顺序数组中移除
tabsMap.value.delete(documentId); delete tabsMap.value[documentId];
tabOrder.value.splice(tabIndex, 1); tabOrder.value.splice(tabIndex, 1);
// 如果关闭的是当前文档,需要切换到其他文档 // 如果关闭的是当前文档,需要切换到其他文档
@@ -111,7 +111,7 @@ export const useTabStore = defineStore('tab', () => {
if (tabIndex === -1) return; if (tabIndex === -1) return;
// 从映射和顺序数组中移除 // 从映射和顺序数组中移除
tabsMap.value.delete(documentId); delete tabsMap.value[documentId];
tabOrder.value.splice(tabIndex, 1); tabOrder.value.splice(tabIndex, 1);
}); });
}; };
@@ -121,12 +121,12 @@ export const useTabStore = defineStore('tab', () => {
*/ */
const switchToTabAndDocument = (documentId: number) => { const switchToTabAndDocument = (documentId: number) => {
if (!hasTab(documentId)) return; if (!hasTab(documentId)) return;
// 如果点击的是当前已激活的文档,不需要重复请求 // 如果点击的是当前已激活的文档,不需要重复请求
if (documentStore.currentDocumentId === documentId) { if (documentStore.currentDocumentId === documentId) {
return; return;
} }
documentStore.openDocument(documentId); documentStore.openDocument(documentId);
}; };
@@ -150,10 +150,31 @@ export const useTabStore = defineStore('tab', () => {
return tabOrder.value.indexOf(documentId); 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 = () => { const initializeTab = () => {
// 先验证并清理无效的标签页(处理持久化的脏数据)
validateTabs();
if (isTabsEnabled.value) { if (isTabsEnabled.value) {
const currentDoc = documentStore.currentDocument; const currentDoc = documentStore.currentDocument;
if (currentDoc) { if (currentDoc) {
@@ -169,13 +190,13 @@ export const useTabStore = defineStore('tab', () => {
*/ */
const closeOtherTabs = (keepDocumentId: number) => { const closeOtherTabs = (keepDocumentId: number) => {
if (!hasTab(keepDocumentId)) return; if (!hasTab(keepDocumentId)) return;
// 获取所有其他标签页的ID // 获取所有其他标签页的ID
const otherTabIds = tabOrder.value.filter(id => id !== keepDocumentId); const otherTabIds = tabOrder.value.filter(id => id !== keepDocumentId);
// 批量关闭其他标签页 // 批量关闭其他标签页
closeTabs(otherTabIds); closeTabs(otherTabIds);
// 如果当前打开的文档在被关闭的标签中,需要切换到保留的文档 // 如果当前打开的文档在被关闭的标签中,需要切换到保留的文档
if (otherTabIds.includes(documentStore.currentDocumentId!)) { if (otherTabIds.includes(documentStore.currentDocumentId!)) {
switchToTabAndDocument(keepDocumentId); switchToTabAndDocument(keepDocumentId);
@@ -188,13 +209,13 @@ export const useTabStore = defineStore('tab', () => {
const closeTabsToRight = (documentId: number) => { const closeTabsToRight = (documentId: number) => {
const index = getTabIndex(documentId); const index = getTabIndex(documentId);
if (index === -1) return; if (index === -1) return;
// 获取右侧所有标签页的ID // 获取右侧所有标签页的ID
const rightTabIds = tabOrder.value.slice(index + 1); const rightTabIds = tabOrder.value.slice(index + 1);
// 批量关闭右侧标签页 // 批量关闭右侧标签页
closeTabs(rightTabIds); closeTabs(rightTabIds);
// 如果当前打开的文档在被关闭的右侧标签中,需要切换到指定的文档 // 如果当前打开的文档在被关闭的右侧标签中,需要切换到指定的文档
if (rightTabIds.includes(documentStore.currentDocumentId!)) { if (rightTabIds.includes(documentStore.currentDocumentId!)) {
switchToTabAndDocument(documentId); switchToTabAndDocument(documentId);
@@ -207,13 +228,13 @@ export const useTabStore = defineStore('tab', () => {
const closeTabsToLeft = (documentId: number) => { const closeTabsToLeft = (documentId: number) => {
const index = getTabIndex(documentId); const index = getTabIndex(documentId);
if (index <= 0) return; if (index <= 0) return;
// 获取左侧所有标签页的ID // 获取左侧所有标签页的ID
const leftTabIds = tabOrder.value.slice(0, index); const leftTabIds = tabOrder.value.slice(0, index);
// 批量关闭左侧标签页 // 批量关闭左侧标签页
closeTabs(leftTabIds); closeTabs(leftTabIds);
// 如果当前打开的文档在被关闭的左侧标签中,需要切换到指定的文档 // 如果当前打开的文档在被关闭的左侧标签中,需要切换到指定的文档
if (leftTabIds.includes(documentStore.currentDocumentId!)) { if (leftTabIds.includes(documentStore.currentDocumentId!)) {
switchToTabAndDocument(documentId); switchToTabAndDocument(documentId);
@@ -224,12 +245,15 @@ export const useTabStore = defineStore('tab', () => {
* 清空所有标签页 * 清空所有标签页
*/ */
const clearAllTabs = () => { const clearAllTabs = () => {
tabsMap.value.clear(); tabsMap.value = {};
tabOrder.value = []; tabOrder.value = [];
}; };
// === 公共API === // === 公共API ===
return { return {
tabsMap,
tabOrder,
// 状态 // 状态
tabs: readonly(tabs), tabs: readonly(tabs),
draggedTabId, draggedTabId,
@@ -251,9 +275,16 @@ export const useTabStore = defineStore('tab', () => {
initializeTab, initializeTab,
clearAllTabs, clearAllTabs,
updateTabTitle, updateTabTitle,
validateTabs,
// 工具方法 // 工具方法
hasTab, hasTab,
getTab getTab
}; };
}); }, {
persist: {
key: 'voidraft-tabs',
storage: localStorage,
pick: ['tabsMap', 'tabOrder'],
},
});

View File

@@ -150,15 +150,19 @@ const blockLayer = layer({
// 转换为视口坐标进行后续计算 // 转换为视口坐标进行后续计算
const fromCoordsTop = fromLineBlock.top + view.documentTop; const fromCoordsTop = fromLineBlock.top + view.documentTop;
let toCoordsBottom = toLineBlock.bottom + view.documentTop; let toCoordsBottom = toLineBlock.bottom + view.documentTop;
// 对最后一个块进行特殊处理,让它直接延伸到底部
if (idx === blocks.length - 1) { if (idx === blocks.length - 1) {
const editorHeight = view.dom.clientHeight; // 计算需要添加到最后一个块的额外高度,以覆盖 scrollPastEnd 添加的额外滚动空间
const contentBottom = toCoordsBottom - view.documentTop + view.documentPadding.top; // scrollPastEnd 会在文档底部添加相当于 scrollDOM.clientHeight 的额外空间
// 当滚动到最底部时顶部仍会显示一行defaultLineHeight需要减去这部分
const editorHeight = view.scrollDOM.clientHeight;
const extraHeight = editorHeight - (
view.defaultLineHeight + // 当滚动到最底部时,顶部仍显示一行
view.documentPadding.top +
8 // 额外的边距调整
);
// 让最后一个块直接延伸到编辑器底部 if (extraHeight > 0) {
if (contentBottom < editorHeight) {
const extraHeight = editorHeight - contentBottom - 10;
toCoordsBottom += extraHeight; toCoordsBottom += extraHeight;
} }
} }

View File

@@ -11,6 +11,7 @@ import {
Highlight, Highlight,
LineSpan, LineSpan,
FontInfo, FontInfo,
UpdateFontInfoRequest,
} from './worker/protocol'; } from './worker/protocol';
import crelt from 'crelt'; import crelt from 'crelt';
@@ -26,6 +27,11 @@ interface Block {
rendering: boolean; rendering: boolean;
requestId: number; requestId: number;
lastUsed: number; // LRU 时间戳 lastUsed: number; // LRU 时间戳
// 高亮缓存
cachedHighlights: Highlight[] | null;
cachedLines: LineSpan[][] | null;
cachedTextSlice: string | null;
cachedTextOffset: number;
} }
export class BlockManager { export class BlockManager {
@@ -34,6 +40,7 @@ export class BlockManager {
private fontInfoMap = new Map<string, FontInfo>(); private fontInfoMap = new Map<string, FontInfo>();
private fontDirty = true; private fontDirty = true;
private fontVersion = 0; private fontVersion = 0;
private sentFontTags = new Set<string>(); // 已发送给 Worker 的字体标签
private measureCache: { charWidth: number; lineHeight: number; version: number } | null = null; private measureCache: { charWidth: number; lineHeight: number; version: number } | null = null;
private displayText: 'blocks' | 'characters' = 'characters'; private displayText: 'blocks' | 'characters' = 'characters';
private themeClasses: Set<string>; private themeClasses: Set<string>;
@@ -150,6 +157,10 @@ export class BlockManager {
markAllDirty(): void { markAllDirty(): void {
for (const block of this.blocks.values()) { for (const block of this.blocks.values()) {
block.dirty = true; block.dirty = true;
// 清除缓存,强制重新收集数据
block.cachedHighlights = null;
block.cachedLines = null;
block.cachedTextSlice = null;
} }
} }
@@ -185,11 +196,19 @@ export class BlockManager {
this.blocks.delete(index); this.blocks.delete(index);
} else if (affectedBlocks.has(index)) { } else if (affectedBlocks.has(index)) {
block.dirty = true; block.dirty = true;
// 清除缓存
block.cachedHighlights = null;
block.cachedLines = null;
block.cachedTextSlice = null;
if (hasLineCountChange) { if (hasLineCountChange) {
markRest = true; // 从这个块开始,后续块都需要更新 markRest = true; // 从这个块开始,后续块都需要更新
} }
} else if (markRest) { } else if (markRest) {
block.dirty = true; block.dirty = true;
// 清除缓存
block.cachedHighlights = null;
block.cachedLines = null;
block.cachedTextSlice = null;
} }
} }
@@ -320,6 +339,10 @@ export class BlockManager {
rendering: false, rendering: false,
requestId: 0, requestId: 0,
lastUsed: now, lastUsed: now,
cachedHighlights: null,
cachedLines: null,
cachedTextSlice: null,
cachedTextOffset: 0,
}; };
this.blocks.set(index, block); this.blocks.set(index, block);
} else { } else {
@@ -344,51 +367,65 @@ export class BlockManager {
this.renderingCount++; this.renderingCount++;
const { startLine, endLine } = block; const { startLine, endLine } = block;
const linesSnapshot = getLinesSnapshot(state);
const tree = syntaxTree(state);
// Collect highlights let highlights: Highlight[];
const highlights: Highlight[] = []; let lines: LineSpan[][];
if (tree.length > 0 && startLine <= state.doc.lines) { let textSlice: string;
const highlighter: Highlighter = { let textOffset: number;
style: (tags) => highlightingFor(state, tags),
};
const startPos = state.doc.line(startLine).from;
const endPos = state.doc.line(Math.min(endLine, state.doc.lines)).to;
highlightTree(tree, highlighter, (from, to, tags) => { // 只有当块是 dirty 时才重新收集数据,否则使用缓存
highlights.push({ from, to, tags }); if (block.dirty || !block.cachedHighlights) {
}, startPos, endPos); const linesSnapshot = getLinesSnapshot(state);
} const tree = syntaxTree(state);
// Extract relevant lines // Collect highlights
const startIdx = startLine - 1; highlights = [];
const endIdx = Math.min(endLine, linesSnapshot.length); if (tree.length > 0 && startLine <= state.doc.lines) {
const lines: LineSpan[][] = linesSnapshot.slice(startIdx, endIdx).map(line => const highlighter: Highlighter = {
line.map(span => ({ from: span.from, to: span.to, folded: span.folded })) 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 highlightTree(tree, highlighter, (from, to, tags) => {
let textOffset = 0; highlights.push({ from, to, tags });
let textEnd = 0; }, startPos, endPos);
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;
} }
}
const textSlice = state.doc.sliceString(textOffset, textEnd);
// Build font info map // Extract relevant lines
const fontInfoMap: Record<string, FontInfo> = {}; const startIdx = startLine - 1;
for (const hl of highlights) { const endIdx = Math.min(endLine, linesSnapshot.length);
if (!fontInfoMap[hl.tags]) { lines = linesSnapshot.slice(startIdx, endIdx).map(line =>
const info = this.getFontInfo(hl.tags); line.map(span => ({ from: span.from, to: span.to, folded: span.folded }))
fontInfoMap[hl.tags] = info; );
// 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 blockLines = endLine - startLine + 1;
const request: BlockRequest = { const request: BlockRequest = {
@@ -403,8 +440,6 @@ export class BlockManager {
lines, lines,
textSlice, textSlice,
textOffset, textOffset,
fontInfoMap,
defaultFont: fontInfoMap[''],
displayText: this.displayText, displayText: this.displayText,
charWidth, charWidth,
lineHeight, lineHeight,
@@ -414,6 +449,43 @@ export class BlockManager {
this.worker.postMessage(request); 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<string, FontInfo> = {};
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 { private evictOldBlocks(): void {
if (this.blocks.size <= MAX_BLOCKS) return; if (this.blocks.size <= MAX_BLOCKS) return;
@@ -432,6 +504,7 @@ export class BlockManager {
private refreshFontCache(): void { private refreshFontCache(): void {
this.fontInfoMap.clear(); this.fontInfoMap.clear();
this.measureCache = null; this.measureCache = null;
this.sentFontTags.clear(); // 需要重新发送字体信息给 Worker
// 注意fontDirty 在成功渲染块后才设为 false // 注意fontDirty 在成功渲染块后才设为 false
this.fontVersion++; this.fontVersion++;
this.markAllDirty(); this.markAllDirty();
@@ -496,3 +569,4 @@ export class BlockManager {
} }
} }

View File

@@ -6,6 +6,10 @@ import {
FontInfo, FontInfo,
} from './protocol'; } from './protocol';
// 缓存字体信息,只在主题变化时更新
let cachedFontInfoMap: Record<string, FontInfo> = {};
let cachedDefaultFont: FontInfo = { color: '#000', font: '12px monospace', lineHeight: 14 };
function post(msg: ToMainMessage, transfer?: Transferable[]): void { function post(msg: ToMainMessage, transfer?: Transferable[]): void {
self.postMessage(msg, { transfer }); self.postMessage(msg, { transfer });
} }
@@ -107,14 +111,16 @@ function renderBlock(request: BlockRequest): void {
endLine, endLine,
width, width,
height, height,
fontInfoMap,
defaultFont,
displayText, displayText,
charWidth, charWidth,
lineHeight, lineHeight,
gutterOffset, gutterOffset,
} = request; } = request;
// 使用缓存的字体信息
const fontInfoMap = cachedFontInfoMap;
const defaultFont = cachedDefaultFont;
// Create OffscreenCanvas for this block // Create OffscreenCanvas for this block
const canvas = new OffscreenCanvas(width, height); const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@@ -245,12 +251,22 @@ function drawTextBlocks(
function handleMessage(msg: ToWorkerMessage): void { function handleMessage(msg: ToWorkerMessage): void {
switch (msg.type) { switch (msg.type) {
case 'init': case 'init':
// 重置字体缓存
cachedFontInfoMap = {};
cachedDefaultFont = { color: '#000', font: '12px monospace', lineHeight: 14 };
post({ type: 'ready' }); post({ type: 'ready' });
break; break;
case 'updateFontInfo':
// 增量合并字体信息
Object.assign(cachedFontInfoMap, msg.fontInfoMap);
cachedDefaultFont = msg.defaultFont;
break;
case 'renderBlock': case 'renderBlock':
renderBlock(msg); renderBlock(msg);
break; break;
case 'destroy': case 'destroy':
// 清理缓存
cachedFontInfoMap = {};
break; break;
} }
} }

View File

@@ -25,6 +25,15 @@ export interface FontInfo {
lineHeight: number; lineHeight: number;
} }
/**
* 更新字体信息(主题变化时发送一次)
*/
export interface UpdateFontInfoRequest {
type: 'updateFontInfo';
fontInfoMap: Record<string, FontInfo>;
defaultFont: FontInfo;
}
export interface BlockRequest { export interface BlockRequest {
type: 'renderBlock'; type: 'renderBlock';
blockId: number; blockId: number;
@@ -37,8 +46,6 @@ export interface BlockRequest {
lines: LineSpan[][]; lines: LineSpan[][];
textSlice: string; textSlice: string;
textOffset: number; textOffset: number;
fontInfoMap: Record<string, FontInfo>;
defaultFont: FontInfo;
displayText: 'blocks' | 'characters'; displayText: 'blocks' | 'characters';
charWidth: number; charWidth: number;
lineHeight: number; lineHeight: number;
@@ -53,7 +60,7 @@ export interface DestroyRequest {
type: 'destroy'; type: 'destroy';
} }
export type ToWorkerMessage = BlockRequest | InitRequest | DestroyRequest; export type ToWorkerMessage = BlockRequest | InitRequest | DestroyRequest | UpdateFontInfoRequest;
export interface BlockComplete { export interface BlockComplete {
type: 'blockComplete'; type: 'blockComplete';