🐛 Fixed some known issues
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user