🎨 Update
This commit is contained in:
@@ -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,
|
||||
};
|
||||
});
|
@@ -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>
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
];
|
||||
}
|
Reference in New Issue
Block a user