🎨 Update

This commit is contained in:
2025-06-17 19:15:59 +08:00
parent 1d6cf7cf68
commit 87fe9d48b1
4 changed files with 499 additions and 580 deletions

View File

@@ -1,12 +1,34 @@
import {defineStore} from 'pinia';
import {ref} from 'vue';
import {ref, watch} from 'vue';
import {DocumentStats} from '@/types/editor';
import {EditorView} from '@codemirror/view';
import {EditorState, Extension} from '@codemirror/state';
import {useConfigStore} from './configStore';
import {useDocumentStore} from './documentStore';
import {useLogStore} from './logStore';
import {createBasicSetup} from '@/views/editor/extensions/basicSetup';
import {
createStatsUpdateExtension,
getTabExtensions,
updateStats,
updateTabConfig,
createAutoSavePlugin,
createSaveShortcutPlugin,
createFontExtensionFromBackend,
updateFontConfig,
} from '@/views/editor/extensions';
import { useEditorTheme } from '@/composables/useEditorTheme';
import { useI18n } from 'vue-i18n';
import type { ThemeType } from '@/types';
import { DocumentService } from '../../bindings/voidraft/internal/services';
export const useEditorStore = defineStore('editor', () => {
// 引用配置store
const configStore = useConfigStore();
const documentStore = useDocumentStore();
const logStore = useLogStore();
const { t } = useI18n();
const { createThemeExtension, updateTheme } = useEditorTheme();
// 状态
const documentStats = ref<DocumentStats>({
@@ -16,12 +38,27 @@ export const useEditorStore = defineStore('editor', () => {
});
// 编辑器视图
const editorView = ref<EditorView | null>(null);
// 编辑器是否已初始化
const isEditorInitialized = ref(false);
// 编辑器容器元素
const editorContainer = ref<HTMLElement | null>(null);
// 方法
function setEditorView(view: EditorView | null) {
editorView.value = view;
}
// 设置编辑器容器
function setEditorContainer(container: HTMLElement | null) {
editorContainer.value = container;
// 如果编辑器已经创建但容器改变了,需要重新挂载
if (editorView.value && container && editorView.value.dom.parentElement !== container) {
container.appendChild(editorView.value.dom);
// 重新挂载后立即滚动到底部
scrollEditorToBottom();
}
}
// 更新文档统计信息
function updateDocumentStats(stats: DocumentStats) {
documentStats.value = stats;
@@ -38,14 +75,215 @@ export const useEditorStore = defineStore('editor', () => {
}
}
return {
// 状态
documentStats,
editorView,
// 滚动到文档底部的辅助函数
const scrollToBottom = (view: EditorView) => {
if (!view) return;
// 方法
setEditorView,
updateDocumentStats,
applyFontSize
const lines = view.state.doc.lines;
if (lines > 0) {
const lastLinePos = view.state.doc.line(lines).to;
view.dispatch({
effects: EditorView.scrollIntoView(lastLinePos)
});
}
};
// 滚动到底部的公共方法
const scrollEditorToBottom = () => {
if (editorView.value) {
scrollToBottom(editorView.value as any);
}
};
// 手动保存文档
const handleManualSave = async () => {
if (!editorView.value) return;
const view = editorView.value as EditorView;
const content = view.state.doc.toString();
// 先更新内容
await DocumentService.UpdateActiveDocumentContent(content);
// 然后调用强制保存方法
const success = await documentStore.forceSaveDocument();
if (success) {
logStore.info(t('document.manualSaveSuccess'));
}
};
// 创建编辑器
const createEditor = async (initialDoc: string = '') => {
if (isEditorInitialized.value || !editorContainer.value) return;
// 加载文档内容
await documentStore.initialize();
const docContent = documentStore.documentContent || initialDoc;
// 获取基本扩展
const basicExtensions = createBasicSetup();
// 获取主题扩展
const themeExtension = await createThemeExtension(
configStore.config.appearance.theme || 'default-dark' as ThemeType
);
// 获取Tab相关扩展
const tabExtensions = getTabExtensions(
configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent,
configStore.config.editing.tabType
);
// 创建字体扩展
const fontExtension = createFontExtensionFromBackend({
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
});
// 创建统计信息更新扩展
const statsExtension = createStatsUpdateExtension(
updateDocumentStats
);
// 创建保存快捷键插件
const saveShortcutPlugin = createSaveShortcutPlugin(() => {
if (editorView.value) {
handleManualSave();
}
});
// 创建自动保存插件
const autoSavePlugin = createAutoSavePlugin({
debounceDelay: 300, // 300毫秒的输入防抖
onSave: (success) => {
if (success) {
documentStore.lastSaved = new Date();
}
}
});
// 组合所有扩展
const extensions: Extension[] = [
themeExtension,
...basicExtensions,
...tabExtensions,
fontExtension,
statsExtension,
saveShortcutPlugin,
autoSavePlugin
];
// 创建编辑器状态
const state = EditorState.create({
doc: docContent,
extensions
});
// 创建编辑器视图
const view = new EditorView({
state,
parent: editorContainer.value
});
// 将编辑器实例保存到store
setEditorView(view);
isEditorInitialized.value = true;
// 确保编辑器已渲染后再滚动到底部
scrollToBottom(view);
// 应用初始字体大小
applyFontSize();
// 立即更新统计信息
updateStats(view, updateDocumentStats);
};
// 重新配置编辑器
const reconfigureTabSettings = () => {
if (!editorView.value) return;
updateTabConfig(
editorView.value as EditorView,
configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent,
configStore.config.editing.tabType
);
};
// 重新配置字体设置
const reconfigureFontSettings = () => {
if (!editorView.value) return;
updateFontConfig(editorView.value as EditorView, {
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
});
};
// 更新编辑器主题
const updateEditorTheme = async (newTheme: ThemeType) => {
if (newTheme && editorView.value) {
await updateTheme(editorView.value as EditorView, newTheme);
}
};
// 销毁编辑器
const destroyEditor = () => {
if (editorView.value) {
editorView.value.destroy();
editorView.value = null;
isEditorInitialized.value = false;
}
};
// 监听Tab设置变化
watch([
() => configStore.config.editing.tabSize,
() => configStore.config.editing.enableTabIndent,
() => configStore.config.editing.tabType,
], () => {
reconfigureTabSettings();
});
// 监听字体大小变化
watch([
() => configStore.config.editing.fontFamily,
() => configStore.config.editing.fontSize,
() => configStore.config.editing.lineHeight,
() => configStore.config.editing.fontWeight,
], () => {
reconfigureFontSettings();
applyFontSize();
});
// 监听主题变化
watch(() => configStore.config.appearance.theme, async (newTheme) => {
if (newTheme) {
await updateEditorTheme(newTheme);
}
});
return {
// 状态
documentStats,
editorView,
isEditorInitialized,
editorContainer,
// 方法
setEditorView,
setEditorContainer,
updateDocumentStats,
applyFontSize,
createEditor,
reconfigureTabSettings,
reconfigureFontSettings,
updateEditorTheme,
handleManualSave,
destroyEditor,
scrollEditorToBottom,
};
});

View File

@@ -1,35 +1,12 @@
<script setup lang="ts">
import {onBeforeUnmount, onMounted, ref, watch} from 'vue';
import {EditorState, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {onBeforeUnmount, onMounted, ref} from 'vue';
import {useEditorStore} from '@/stores/editorStore';
import {useConfigStore} from '@/stores/configStore';
import {useDocumentStore} from '@/stores/documentStore';
import {useLogStore} from '@/stores/logStore';
import {createBasicSetup} from './extensions/basicSetup';
import {
createStatsUpdateExtension,
createWheelZoomHandler,
getTabExtensions,
updateStats,
updateTabConfig,
createAutoSavePlugin,
createSaveShortcutPlugin,
createFontExtensionFromBackend,
updateFontConfig,
} from './extensions';
import { useEditorTheme } from '@/composables/useEditorTheme';
import { useI18n } from 'vue-i18n';
import type { ThemeType } from '@/types';
import { DocumentService } from '../../../bindings/voidraft/internal/services';
import {createWheelZoomHandler} from './extensions';
import Toolbar from '@/components/toolbar/Toolbar.vue';
const editorStore = useEditorStore();
const configStore = useConfigStore();
const documentStore = useDocumentStore();
const logStore = useLogStore();
const { t } = useI18n();
const { createThemeExtension, updateTheme } = useEditorTheme();
const props = defineProps({
initialDoc: {
@@ -39,197 +16,32 @@ const props = defineProps({
});
const editorElement = ref<HTMLElement | null>(null);
const editorCreated = ref(false);
let isDestroying = false;
// 创建编辑
const createEditor = async () => {
if (!editorElement.value || editorCreated.value) return;
editorCreated.value = true;
// 加载文档内容
await documentStore.initialize();
const docContent = documentStore.documentContent || props.initialDoc;
// 获取基本扩展
const basicExtensions = createBasicSetup();
// 获取主题扩展
const themeExtension = await createThemeExtension(
configStore.config.appearance.theme || 'default-dark' as ThemeType
);
// 获取Tab相关扩展
const tabExtensions = getTabExtensions(
configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent,
configStore.config.editing.tabType
);
// 创建字体扩展
const fontExtension = createFontExtensionFromBackend({
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
});
// 创建统计信息更新扩展
const statsExtension = createStatsUpdateExtension(
editorStore.updateDocumentStats
);
// 创建保存快捷键插件
const saveShortcutPlugin = createSaveShortcutPlugin(() => {
if (editorStore.editorView) {
handleManualSave();
}
});
// 创建自动保存插件
const autoSavePlugin = createAutoSavePlugin({
debounceDelay: 300, // 300毫秒的输入防抖
onSave: (success) => {
if (success) {
documentStore.lastSaved = new Date();
}
}
});
// 组合所有扩展
const extensions: Extension[] = [
themeExtension,
...basicExtensions,
...tabExtensions,
fontExtension,
statsExtension,
saveShortcutPlugin,
autoSavePlugin
];
// 创建编辑器状态
const state = EditorState.create({
doc: docContent,
extensions
});
// 创建编辑器视图
const view = new EditorView({
state,
parent: editorElement.value
});
// 将编辑器实例保存到store
editorStore.setEditorView(view);
// 应用初始字体大小
editorStore.applyFontSize();
// 立即更新统计信息
updateStats(view, editorStore.updateDocumentStats);
};
// 创建滚轮事件处理器
const handleWheel = createWheelZoomHandler(
configStore.increaseFontSize,
configStore.decreaseFontSize
// 创建滚轮缩放处理
const wheelHandler = createWheelZoomHandler(
configStore.increaseFontSize,
configStore.decreaseFontSize
);
// 手动保存文档
const handleManualSave = async () => {
if (!editorStore.editorView || isDestroying) return;
const view = editorStore.editorView as EditorView;
const content = view.state.doc.toString();
onMounted(async () => {
if (!editorElement.value) return;
// 先更新内容
await DocumentService.UpdateActiveDocumentContent(content);
// 然后调用强制保存方法不再传递content参数
const success = await documentStore.forceSaveDocument();
if (success) {
logStore.info(t('document.manualSaveSuccess'));
// 设置编辑器容器
editorStore.setEditorContainer(editorElement.value);
// 如果编辑器还没有初始化,创建编辑器
if (!editorStore.isEditorInitialized) {
await editorStore.createEditor(props.initialDoc);
}
};
// 重新配置编辑器(仅在必要时)
const reconfigureTabSettings = () => {
if (!editorStore.editorView) return;
updateTabConfig(
editorStore.editorView as EditorView,
configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent,
configStore.config.editing.tabType
);
};
// 重新配置字体设置
const reconfigureFontSettings = () => {
if (!editorStore.editorView) return;
updateFontConfig(editorStore.editorView as EditorView, {
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
});
};
// 监听Tab设置变化
watch([
() => configStore.config.editing.tabSize,
() => configStore.config.editing.enableTabIndent,
() => configStore.config.editing.tabType,
], () => {
reconfigureTabSettings();
});
// 监听字体大小变化
watch([
() => configStore.config.editing.fontFamily,
() => configStore.config.editing.fontSize,
() => configStore.config.editing.lineHeight,
() => configStore.config.editing.fontWeight,
], () => {
reconfigureFontSettings();
editorStore.applyFontSize();
});
// 监听主题变化
watch(() => configStore.config.appearance.theme, async (newTheme) => {
if (newTheme && editorStore.editorView) {
await updateTheme(editorStore.editorView as EditorView, newTheme);
}
});
onMounted(() => {
// 创建编辑器
createEditor();
// 添加滚轮事件监听
if (editorElement.value) {
editorElement.value.addEventListener('wheel', handleWheel, {passive: false});
}
// 确保统计信息已更新
if (editorStore.editorView) {
setTimeout(() => {
updateStats(editorStore.editorView as EditorView, editorStore.updateDocumentStats);
}, 100);
}
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
});
onBeforeUnmount(() => {
isDestroying = true;
// 移除滚轮事件监听
if (editorElement.value) {
editorElement.value.removeEventListener('wheel', handleWheel);
}
// 直接销毁编辑器
if (editorStore.editorView) {
editorStore.editorView.destroy();
editorStore.setEditorView(null);
editorElement.value.removeEventListener('wheel', wheelHandler);
}
});
</script>

View File

@@ -27,7 +27,7 @@ import {searchVisibilityField, vscodeSearch, customSearchKeymap} from './vscodeS
import {hyperLink} from './hyperlink';
import {color} from './colorSelector';
import {textHighlighter} from './textHighlightExtension';
import {createTextHighlighter} from './textHighlightExtension';
import {minimap} from './minimap';
// 基本编辑器设置
@@ -38,7 +38,7 @@ export const createBasicSetup = (): Extension[] => {
hyperLink,
color,
textHighlighter,
...createTextHighlighter('hl'),
minimap({
displayText: 'characters',
showOverlay: 'always',

View File

@@ -1,46 +1,121 @@
import {EditorState, StateEffect, StateField} from "@codemirror/state";
import {Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType} from "@codemirror/view";
import {keymap} from "@codemirror/view";
import {Text} from "@codemirror/state";
import { EditorState, StateEffect, StateField, Transaction, Range } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
import { keymap } from "@codemirror/view";
// 定义高亮标记的语法
const HIGHLIGHT_MARKER_START = "<hl>";
const HIGHLIGHT_MARKER_END = "</hl>";
// 全局高亮存储 - 以文档ID为键高亮范围数组为值
interface HighlightInfo {
from: number;
to: number;
}
class GlobalHighlightStore {
private static instance: GlobalHighlightStore;
private highlightMap: Map<string, HighlightInfo[]> = new Map();
private constructor() {}
public static getInstance(): GlobalHighlightStore {
if (!GlobalHighlightStore.instance) {
GlobalHighlightStore.instance = new GlobalHighlightStore();
}
return GlobalHighlightStore.instance;
}
// 保存文档的高亮
saveHighlights(documentId: string, highlights: HighlightInfo[]): void {
this.highlightMap.set(documentId, [...highlights]);
}
// 获取文档的高亮
getHighlights(documentId: string): HighlightInfo[] {
return this.highlightMap.get(documentId) || [];
}
// 添加高亮
addHighlight(documentId: string, highlight: HighlightInfo): void {
const highlights = this.getHighlights(documentId);
highlights.push(highlight);
this.saveHighlights(documentId, highlights);
}
// 移除高亮
removeHighlights(documentId: string, from: number, to: number): void {
const highlights = this.getHighlights(documentId);
const filtered = highlights.filter(h => !(h.from < to && h.to > from));
this.saveHighlights(documentId, filtered);
}
// 清除文档的所有高亮
clearHighlights(documentId: string): void {
this.highlightMap.delete(documentId);
}
}
// 获取全局高亮存储实例
const highlightStore = GlobalHighlightStore.getInstance();
// 定义添加和移除高亮的状态效果
const addHighlight = StateEffect.define<{from: number, to: number, documentId: string}>({
map: ({from, to, documentId}, change) => ({
from: change.mapPos(from),
to: change.mapPos(to),
documentId
})
});
const removeHighlight = StateEffect.define<{from: number, to: number, documentId: string}>({
map: ({from, to, documentId}, change) => ({
from: change.mapPos(from),
to: change.mapPos(to),
documentId
})
});
// 初始化高亮效果 - 用于页面加载时恢复高亮
const initHighlights = StateEffect.define<{highlights: HighlightInfo[], documentId: string}>();
// 高亮样式
const highlightMark = Decoration.mark({
attributes: {style: `background-color: rgba(255, 215, 0, 0.3)`}
});
// 空白Widget用于隐藏标记
class EmptyWidget extends WidgetType {
toDOM() {
return document.createElement("span");
}
}
const emptyWidget = new EmptyWidget();
// 定义效果用于触发高亮视图刷新
const refreshHighlightEffect = StateEffect.define<null>();
// 存储高亮范围的状态字段
const highlightState = StateField.define<DecorationSet>({
create() {
return Decoration.none;
},
update(decorations, tr) {
// 先映射现有的装饰,以适应文档变化
decorations = decorations.map(tr.changes);
// 检查是否有刷新效果
// 处理添加和移除高亮的效果
for (const effect of tr.effects) {
if (effect.is(refreshHighlightEffect)) {
return findHighlights(tr.state);
if (effect.is(addHighlight)) {
const { from, to, documentId } = effect.value;
decorations = decorations.update({
add: [highlightMark.range(from, to)]
});
// 同步到全局存储
highlightStore.addHighlight(documentId, { from, to });
}
else if (effect.is(removeHighlight)) {
const { from, to, documentId } = effect.value;
decorations = decorations.update({
filter: (rangeFrom, rangeTo) => {
// 移除与指定范围重叠的装饰
return !(rangeFrom < to && rangeTo > from);
}
});
// 同步到全局存储
highlightStore.removeHighlights(documentId, from, to);
}
else if (effect.is(initHighlights)) {
const { highlights } = effect.value;
const ranges = highlights.map(h => highlightMark.range(h.from, h.to));
if (ranges.length > 0) {
decorations = decorations.update({ add: ranges });
}
}
}
if (tr.docChanged) {
return findHighlights(tr.state);
}
return decorations;
@@ -48,335 +123,120 @@ const highlightState = StateField.define<DecorationSet>({
provide: field => EditorView.decorations.from(field)
});
// 从文档中查找高亮标记并创建装饰
function findHighlights(state: EditorState): DecorationSet {
const decorations: any[] = [];
const doc = state.doc;
const text = doc.toString();
let pos = 0;
// 定义高亮范围接口
interface HighlightRange {
from: number;
to: number;
decoration: Decoration;
}
// 查找指定位置包含的高亮
function findHighlightsAt(state: EditorState, pos: number): HighlightRange[] {
const highlights: HighlightRange[] = [];
while (pos < text.length) {
const startMarkerPos = text.indexOf(HIGHLIGHT_MARKER_START, pos);
if (startMarkerPos === -1) break;
const contentStart = startMarkerPos + HIGHLIGHT_MARKER_START.length;
const endMarkerPos = text.indexOf(HIGHLIGHT_MARKER_END, contentStart);
if (endMarkerPos === -1) {
pos = contentStart;
continue;
state.field(highlightState).between(pos, pos, (from, to, deco) => {
highlights.push({ from, to, decoration: deco });
});
return highlights;
}
// 查找与给定范围重叠的所有高亮
function findHighlightsInRange(state: EditorState, from: number, to: number): HighlightRange[] {
const highlights: HighlightRange[] = [];
state.field(highlightState).between(from, to, (rangeFrom, rangeTo, deco) => {
// 只添加与指定范围有重叠的高亮
if (rangeFrom < to && rangeTo > from) {
highlights.push({ from: rangeFrom, to: rangeTo, decoration: deco });
}
// 创建装饰,隐藏标记,高亮中间内容
decorations.push(Decoration.replace({
widget: emptyWidget
}).range(startMarkerPos, contentStart));
decorations.push(highlightMark.range(contentStart, endMarkerPos));
decorations.push(Decoration.replace({
widget: emptyWidget
}).range(endMarkerPos, endMarkerPos + HIGHLIGHT_MARKER_END.length));
pos = endMarkerPos + HIGHLIGHT_MARKER_END.length;
}
});
return Decoration.set(decorations, true);
return highlights;
}
// 检查文本是否已经被高亮标记包围
function isAlreadyHighlighted(text: string): boolean {
// 检查是否有嵌套标记
let startIndex = 0;
let markerCount = 0;
// 收集当前所有高亮信息
function collectAllHighlights(state: EditorState): HighlightInfo[] {
const highlights: HighlightInfo[] = [];
while (true) {
const nextStart = text.indexOf(HIGHLIGHT_MARKER_START, startIndex);
if (nextStart === -1) break;
markerCount++;
startIndex = nextStart + HIGHLIGHT_MARKER_START.length;
}
state.field(highlightState).between(0, state.doc.length, (from, to) => {
highlights.push({ from, to });
});
// 如果有多个开始标记,表示存在嵌套
if (markerCount > 1) return true;
// 检查简单的包围情况
return text.startsWith(HIGHLIGHT_MARKER_START) && text.endsWith(HIGHLIGHT_MARKER_END);
return highlights;
}
// 添加高亮标记到文本
function addHighlightMarker(view: EditorView, from: number, to: number) {
const text = view.state.sliceDoc(from, to);
// 添加高亮
function addHighlightRange(view: EditorView, from: number, to: number, documentId: string): boolean {
if (from === to) return false; // 不高亮空选择
// 检查文本是否已经被高亮,防止嵌套高亮
if (isAlreadyHighlighted(text)) {
return false;
}
// 检查是否已经完全高亮
const overlappingHighlights = findHighlightsInRange(view.state, from, to);
const isFullyHighlighted = overlappingHighlights.some(range =>
range.from <= from && range.to >= to
);
if (isFullyHighlighted) return false;
view.dispatch({
changes: {
from,
to,
insert: `${HIGHLIGHT_MARKER_START}${text}${HIGHLIGHT_MARKER_END}`
},
effects: refreshHighlightEffect.of(null)
effects: addHighlight.of({from, to, documentId})
});
return true;
}
// 移除文本的高亮标记
function removeHighlightMarker(view: EditorView, region: {from: number, to: number, content: string}) {
// 移除高亮
function removeHighlightRange(view: EditorView, from: number, to: number, documentId: string): boolean {
const highlights = findHighlightsInRange(view.state, from, to);
if (highlights.length === 0) return false;
view.dispatch({
changes: {
from: region.from,
to: region.to,
insert: region.content
},
effects: refreshHighlightEffect.of(null)
effects: removeHighlight.of({from, to, documentId})
});
return true;
}
// 清理嵌套高亮标记
function cleanNestedHighlights(view: EditorView, from: number, to: number) {
const text = view.state.sliceDoc(from, to);
// 如果没有嵌套标记,直接返回
if (text.indexOf(HIGHLIGHT_MARKER_START) === -1 ||
text.indexOf(HIGHLIGHT_MARKER_END) === -1) {
return false;
}
// 尝试清理嵌套标记
let cleanedText = text;
let changed = false;
// 从内到外清理嵌套标记
while (true) {
const startPos = cleanedText.indexOf(HIGHLIGHT_MARKER_START);
if (startPos === -1) break;
const contentStart = startPos + HIGHLIGHT_MARKER_START.length;
const endPos = cleanedText.indexOf(HIGHLIGHT_MARKER_END, contentStart);
if (endPos === -1) break;
// 提取标记中的内容
const content = cleanedText.substring(contentStart, endPos);
// 替换带标记的部分为纯内容
cleanedText = cleanedText.substring(0, startPos) + content + cleanedText.substring(endPos + HIGHLIGHT_MARKER_END.length);
changed = true;
}
if (changed) {
view.dispatch({
changes: {
from,
to,
insert: cleanedText
},
effects: refreshHighlightEffect.of(null)
});
return true;
}
return false;
}
// 检查选中区域是否包含高亮标记
function isHighlightedRegion(doc: Text, from: number, to: number): {from: number, to: number, content: string} | null {
const fullText = doc.toString();
// 向前搜索起始标记
let startPos = from;
while (startPos > 0) {
const textBefore = fullText.substring(Math.max(0, startPos - 100), startPos);
const markerPos = textBefore.lastIndexOf(HIGHLIGHT_MARKER_START);
if (markerPos !== -1) {
startPos = startPos - textBefore.length + markerPos;
break;
}
if (startPos - 100 <= 0) {
// 没找到标记
return null;
}
startPos = Math.max(0, startPos - 100);
}
// 确认找到的标记范围包含选中区域
const contentStart = startPos + HIGHLIGHT_MARKER_START.length;
// 向后搜索结束标记
const textAfter = fullText.substring(contentStart, Math.min(fullText.length, to + 100));
const endMarkerPos = textAfter.indexOf(HIGHLIGHT_MARKER_END);
if (endMarkerPos === -1) {
return null;
}
const contentEnd = contentStart + endMarkerPos;
const regionEnd = contentEnd + HIGHLIGHT_MARKER_END.length;
// 确保选中区域在高亮区域内
if (from < startPos || to > regionEnd) {
return null;
}
// 获取高亮内容
const content = fullText.substring(contentStart, contentEnd);
return {
from: startPos,
to: regionEnd,
content
};
}
// 查找光标位置是否在高亮区域内
function findHighlightAtCursor(view: EditorView, pos: number): {from: number, to: number, content: string} | null {
const doc = view.state.doc;
const fullText = doc.toString();
// 向前搜索起始标记
let startPos = pos;
let foundStart = false;
while (startPos > 0) {
const textBefore = fullText.substring(Math.max(0, startPos - 100), startPos);
const markerPos = textBefore.lastIndexOf(HIGHLIGHT_MARKER_START);
if (markerPos !== -1) {
startPos = startPos - textBefore.length + markerPos;
foundStart = true;
break;
}
if (startPos - 100 <= 0) {
break;
}
startPos = Math.max(0, startPos - 100);
}
if (!foundStart) {
return null;
}
const contentStart = startPos + HIGHLIGHT_MARKER_START.length;
// 如果光标在开始标记之前,不在高亮区域内
if (pos < contentStart) {
return null;
}
// 向后搜索结束标记
const textAfter = fullText.substring(contentStart);
const endMarkerPos = textAfter.indexOf(HIGHLIGHT_MARKER_END);
if (endMarkerPos === -1) {
return null;
}
const contentEnd = contentStart + endMarkerPos;
// 如果光标在结束标记之后,不在高亮区域内
if (pos > contentEnd) {
return null;
}
// 获取高亮内容
const content = fullText.substring(contentStart, contentEnd);
return {
from: startPos,
to: contentEnd + HIGHLIGHT_MARKER_END.length,
content
};
}
// 切换高亮状态
function toggleHighlight(view: EditorView) {
function toggleHighlight(view: EditorView, documentId: string): boolean {
const selection = view.state.selection.main;
// 如果有选择文本
if (!selection.empty) {
// 先尝试清理选择区域内的嵌套高亮
if (cleanNestedHighlights(view, selection.from, selection.to)) {
return true;
}
const {from, to} = selection;
// 检查选中区域是否已经高亮区域内
const highlightRegion = isHighlightedRegion(view.state.doc, selection.from, selection.to);
if (highlightRegion) {
removeHighlightMarker(view, highlightRegion);
return true;
}
// 检查选择范围内是否已经高亮
const highlights = findHighlightsInRange(view.state, from, to);
// 检查是否选择了带有标记的文本
const selectedText = view.state.sliceDoc(selection.from, selection.to);
if (selectedText.indexOf(HIGHLIGHT_MARKER_START) !== -1 ||
selectedText.indexOf(HIGHLIGHT_MARKER_END) !== -1) {
return cleanNestedHighlights(view, selection.from, selection.to);
if (highlights.length > 0) {
// 如果已有高亮,则移除
return removeHighlightRange(view, from, to, documentId);
} else {
// 如果没有高亮,则添加
return addHighlightRange(view, from, to, documentId);
}
// 如果选择的是干净文本,添加高亮
addHighlightMarker(view, selection.from, selection.to);
return true;
}
}
// 如果是光标
else {
// 查找光标位置是否在高亮区域内
const highlightAtCursor = findHighlightAtCursor(view, selection.from);
if (highlightAtCursor) {
removeHighlightMarker(view, highlightAtCursor);
return true;
const pos = selection.from;
const highlightsAtCursor = findHighlightsAt(view.state, pos);
if (highlightsAtCursor.length > 0) {
// 移除光标位置的高亮
const highlight = highlightsAtCursor[0];
return removeHighlightRange(view, highlight.from, highlight.to, documentId);
}
}
return false;
}
// 定义快捷键
const highlightKeymap = keymap.of([
{key: "Mod-h", run: toggleHighlight}
]);
// 处理复制事件,移除高亮标记
function handleCopy(view: EditorView, event: ClipboardEvent) {
if (!event.clipboardData || view.state.selection.main.empty) return false;
const { from, to } = view.state.selection.main;
const selectedText = view.state.sliceDoc(from, to);
// 如果选中的内容包含高亮标记,则处理复制
if (selectedText.indexOf(HIGHLIGHT_MARKER_START) !== -1 ||
selectedText.indexOf(HIGHLIGHT_MARKER_END) !== -1) {
// 清理文本中的所有标记
let cleanText = selectedText;
while (true) {
const startPos = cleanText.indexOf(HIGHLIGHT_MARKER_START);
if (startPos === -1) break;
const contentStart = startPos + HIGHLIGHT_MARKER_START.length;
const endPos = cleanText.indexOf(HIGHLIGHT_MARKER_END, contentStart);
if (endPos === -1) break;
const content = cleanText.substring(contentStart, endPos);
cleanText = cleanText.substring(0, startPos) + content + cleanText.substring(endPos + HIGHLIGHT_MARKER_END.length);
}
// 将清理后的文本设置为剪贴板内容
event.clipboardData.setData('text/plain', cleanText);
event.preventDefault();
return true;
}
return false;
// 创建高亮快捷键需要文档ID
function createHighlightKeymap(documentId: string) {
return keymap.of([
{key: "Mod-h", run: (view) => toggleHighlight(view, documentId)}
]);
}
// 高亮刷新管理器类
@@ -385,59 +245,67 @@ class HighlightRefreshManager {
private refreshPending = false;
private initialSetupDone = false;
private rafId: number | null = null;
private documentId: string;
constructor(view: EditorView) {
constructor(view: EditorView, documentId: string) {
this.view = view;
this.documentId = documentId;
}
/**
* 使用requestAnimationFrame安排高亮刷
* 确保在适当的时机执行,且不会重复触发
* 使用requestAnimationFrame安排视图更
*/
scheduleRefresh(): void {
if (this.refreshPending) return;
this.refreshPending = true;
// 使用requestAnimationFrame确保在下一帧渲染前执行
this.rafId = requestAnimationFrame(() => {
this.executeRefresh();
});
}
/**
* 执行高亮刷
* 执行视图更
*/
private executeRefresh(): void {
this.refreshPending = false;
this.rafId = null;
// 确保视图仍然有效
if (!this.view.state) return;
try {
this.view.dispatch({
effects: refreshHighlightEffect.of(null)
});
// 触发一个空的更新,确保视图刷新
this.view.dispatch({});
} catch (e) {
console.debug("highlight refresh error:", e);
}
}
/**
* 初始化高亮 - 应用保存的高亮
*/
initHighlights(): void {
const savedHighlights = highlightStore.getHighlights(this.documentId);
if (savedHighlights.length > 0) {
this.view.dispatch({
effects: initHighlights.of({
highlights: savedHighlights,
documentId: this.documentId
})
});
}
}
/**
* 执行初始化设置
*/
performInitialSetup(): void {
if (this.initialSetupDone) return;
// 使用Promise.resolve().then确保在当前执行栈清空后运行
Promise.resolve().then(() => {
this.initHighlights();
this.scheduleRefresh();
// 在DOM完全加载后再次刷新以确保稳定性
window.addEventListener('load', () => {
this.scheduleRefresh();
}, { once: true });
});
this.initialSetupDone = true;
@@ -453,38 +321,39 @@ class HighlightRefreshManager {
}
}
// 确保编辑器初始化时立即扫描高亮
const highlightSetupPlugin = ViewPlugin.define((view) => {
// 添加复制事件监听器
const copyHandler = (event: ClipboardEvent) => handleCopy(view, event);
view.dom.addEventListener('copy', copyHandler);
// 创建刷新管理器实例
const refreshManager = new HighlightRefreshManager(view);
// 执行初始化设置
refreshManager.performInitialSetup();
return {
update(update: ViewUpdate) {
// 不在update回调中直接调用dispatch
if ((update.docChanged || update.selectionSet) && !update.transactions.some(tr =>
tr.effects.some(e => e.is(refreshHighlightEffect)))) {
// 安排一个未来的刷新
refreshManager.scheduleRefresh();
// 创建高亮扩展
export function createTextHighlighter(documentId: string) {
// 视图插件
const highlightSetupPlugin = ViewPlugin.define((view) => {
// 创建刷新管理器实例
const refreshManager = new HighlightRefreshManager(view, documentId);
// 执行初始化设置
refreshManager.performInitialSetup();
return {
update(update: ViewUpdate) {
// 页面有内容变化时,保存最新的高亮状态
if (update.docChanged || update.transactions.some(tr =>
tr.effects.some(e => e.is(addHighlight) || e.is(removeHighlight))
)) {
// 延迟收集高亮信息,确保所有效果都已应用
setTimeout(() => {
const allHighlights = collectAllHighlights(view.state);
highlightStore.saveHighlights(documentId, allHighlights);
}, 0);
}
},
destroy() {
// 清理资源
refreshManager.dispose();
}
},
destroy() {
// 清理资源
refreshManager.dispose();
view.dom.removeEventListener('copy', copyHandler);
}
};
});
// 导出完整扩展
export const textHighlighter = [
highlightState,
highlightKeymap,
highlightSetupPlugin
];
};
});
return [
highlightState,
createHighlightKeymap(documentId),
highlightSetupPlugin
];
}