🎨 Modular extensions

This commit is contained in:
2025-04-24 23:46:34 +08:00
parent dcf07c9106
commit 8673b36f98
9 changed files with 298 additions and 206 deletions

View File

@@ -18,7 +18,7 @@ const editorStore = useEditorStore();
</span> </span>
</div> </div>
<div class="actions"> <div class="actions">
<span class="font-size" title="字体大小 (Ctrl+滚轮调整)"> <span class="font-size" title="字体大小 (Ctrl+滚轮调整)" @click="editorStore.resetFontSize">
{{ editorStore.fontSize }}px {{ editorStore.fontSize }}px
</span> </span>
<span class="tab-settings"> <span class="tab-settings">

View File

@@ -0,0 +1,79 @@
import {Extension} from '@codemirror/state';
import {
crosshairCursor,
drawSelection,
dropCursor,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
highlightSpecialChars,
keymap,
lineNumbers,
rectangularSelection,
} from '@codemirror/view';
import {
bracketMatching,
defaultHighlightStyle,
foldGutter,
foldKeymap,
indentOnInput,
syntaxHighlighting,
} from '@codemirror/language';
import {
defaultKeymap,
history,
historyKeymap,
} from '@codemirror/commands';
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
import {autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
import {lintKeymap} from '@codemirror/lint';
import {baseDark, customHighlightActiveLine} from '@/editor/theme/base-dark';
// 基本编辑器设置,包含常用扩展
export const createBasicSetup = (): Extension[] => {
return [
// 主题相关
baseDark,
// 基础UI
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
dropCursor(),
EditorView.lineWrapping,
// 历史记录
history(),
// 代码折叠
foldGutter(),
// 选择与高亮
drawSelection(),
customHighlightActiveLine,
highlightActiveLine(),
highlightSelectionMatches(),
rectangularSelection(),
crosshairCursor(),
// 缩进和编辑辅助
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
bracketMatching(),
closeBrackets(),
// 自动完成
autocompletion(),
// 键盘映射
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
]),
];
};

View File

@@ -0,0 +1,4 @@
// 统一导出所有扩展
export * from './tabExtension';
export * from './wheelZoomExtension';
export * from './statsExtension';

View File

@@ -0,0 +1,42 @@
import {Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {DocumentStats} from '@/types/editor';
// 更新编辑器文档统计信息
export const updateStats = (
view: EditorView,
updateDocumentStats: (stats: DocumentStats) => void
) => {
if (!view) return;
const state = view.state;
const doc = state.doc;
const text = doc.toString();
// 计算选中的字符数
let selectedChars = 0;
const selections = state.selection;
if (selections) {
for (let i = 0; i < selections.ranges.length; i++) {
const range = selections.ranges[i];
selectedChars += range.to - range.from;
}
}
updateDocumentStats({
lines: doc.lines,
characters: text.length,
selectedCharacters: selectedChars
});
};
// 创建统计信息更新监听器扩展
export const createStatsUpdateExtension = (
updateDocumentStats: (stats: DocumentStats) => void
): Extension => {
return EditorView.updateListener.of(update => {
if (update.docChanged || update.selectionSet) {
updateStats(update.view, updateDocumentStats);
}
});
};

View File

@@ -0,0 +1,74 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView, keymap} from '@codemirror/view';
import {indentSelection} from '@codemirror/commands';
import {indentUnit} from '@codemirror/language';
// Tab设置相关的compartment
export const tabSizeCompartment = new Compartment();
export const tabKeyCompartment = new Compartment();
// 自定义Tab键处理函数
export const tabHandler = (view: EditorView, tabSize: number): boolean => {
// 如果有选中文本使用indentSelection
if (!view.state.selection.main.empty) {
return indentSelection(view);
}
// 创建相应数量的空格
const spaces = ' '.repeat(tabSize);
// 在光标位置插入空格
const {state, dispatch} = view;
dispatch(state.update(state.replaceSelection(spaces), {scrollIntoView: true}));
return true;
};
// 获取Tab相关的扩展
export const getTabExtensions = (tabSize: number, enableTabIndent: boolean): Extension[] => {
const extensions: Extension[] = [];
// 设置缩进单位
extensions.push(tabSizeCompartment.of(indentUnit.of(' '.repeat(tabSize))));
// 如果启用了Tab缩进添加自定义Tab键映射
if (enableTabIndent) {
extensions.push(
tabKeyCompartment.of(
keymap.of([{
key: "Tab",
run: (view) => tabHandler(view, tabSize)
}])
)
);
} else {
extensions.push(tabKeyCompartment.of([]));
}
return extensions;
};
// 更新Tab配置
export const updateTabConfig = (
view: EditorView | null,
tabSize: number,
enableTabIndent: boolean
) => {
if (!view) return;
// 更新indentUnit配置
view.dispatch({
effects: tabSizeCompartment.reconfigure(indentUnit.of(' '.repeat(tabSize)))
});
// 更新Tab键映射
const tabKeymap = enableTabIndent
? keymap.of([{
key: "Tab",
run: (view) => tabHandler(view, tabSize)
}])
: [];
view.dispatch({
effects: tabKeyCompartment.reconfigure(tabKeymap)
});
};

View File

@@ -0,0 +1,35 @@
import {EditorView} from '@codemirror/view';
// 处理滚轮缩放字体的事件处理函数
export const createWheelZoomHandler = (
increaseFontSize: () => void,
decreaseFontSize: () => void
) => {
return (event: WheelEvent) => {
// 检查是否按住了Ctrl键
if (event.ctrlKey) {
// 阻止默认行为(防止页面缩放)
event.preventDefault();
// 根据滚轮方向增大或减小字体
if (event.deltaY < 0) {
// 向上滚动,增大字体
increaseFontSize();
} else {
// 向下滚动,减小字体
decreaseFontSize();
}
}
};
};
// 应用字体大小到编辑器
export const applyFontSize = (view: EditorView, fontSize: number) => {
if (!view) return;
// 更新编辑器的字体大小
const editorDOM = view.dom;
if (editorDOM) {
editorDOM.style.fontSize = `${fontSize}px`;
}
};

View File

@@ -1,8 +0,0 @@
import {EditorView} from '@codemirror/view'
export const fontTheme = EditorView.theme({
'&': {
fontFamily: '"Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", Arial, sans-serif',
fontSize: '12px'
}
})

View File

@@ -1,34 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import {onBeforeUnmount, onMounted, ref, watch} from 'vue'; import {onBeforeUnmount, onMounted, ref, watch} from 'vue';
import { import {EditorState, Extension} from '@codemirror/state';
crosshairCursor, import {EditorView} from '@codemirror/view';
drawSelection,
dropCursor,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
highlightSpecialChars,
keymap,
lineNumbers,
rectangularSelection,
} from '@codemirror/view';
import {Compartment, EditorState, Extension} from '@codemirror/state';
import {baseDark, customHighlightActiveLine} from "@/editor/theme/base-dark";
import {
bracketMatching,
defaultHighlightStyle,
foldGutter,
foldKeymap,
indentOnInput,
indentUnit,
syntaxHighlighting
} from '@codemirror/language';
import {defaultKeymap, history, historyKeymap, indentSelection,} from '@codemirror/commands';
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
import {autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
import {lintKeymap} from '@codemirror/lint';
import {fontTheme} from "@/editor/font/font";
import {useEditorStore} from '@/stores/editor'; import {useEditorStore} from '@/stores/editor';
import {createBasicSetup} from './extensions/basicSetup';
import {
createStatsUpdateExtension,
getTabExtensions,
updateTabConfig,
createWheelZoomHandler,
applyFontSize,
updateStats
} from './extensions';
const editorStore = useEditorStore(); const editorStore = useEditorStore();
@@ -39,207 +22,90 @@ const props = defineProps({
} }
}); });
// 使用Compartment可以动态更新特定的配置而不重建整个编辑器
const tabSizeCompartment = new Compartment();
const tabKeyCompartment = new Compartment();
const editorElement = ref<HTMLElement | null>(null); const editorElement = ref<HTMLElement | null>(null);
// 处理滚轮缩放字体
const handleWheel = (event: WheelEvent) => {
// 检查是否按住了Ctrl键
if (event.ctrlKey) {
// 阻止默认行为(防止页面缩放)
event.preventDefault();
// 根据滚轮方向增大或减小字体
if (event.deltaY < 0) {
// 向上滚动,增大字体
editorStore.increaseFontSize();
} else {
// 向下滚动,减小字体
editorStore.decreaseFontSize();
}
}
};
// 自定义Tab键处理函数
const tabHandler = (view: EditorView): boolean => {
// 如果有选中文本使用indentSelection
if (!view.state.selection.main.empty) {
return indentSelection(view);
}
// 获取当前的tabSize值
const currentTabSize = editorStore.tabSize;
// 创建相应数量的空格
const spaces = ' '.repeat(currentTabSize);
// 在光标位置插入空格
const {state, dispatch} = view;
dispatch(state.update(state.replaceSelection(spaces), {scrollIntoView: true}));
return true;
};
// 获取Tab相关的扩展
const getTabExtensions = (): Extension[] => {
const extensions: Extension[] = [];
// 设置缩进单位
const tabSize = editorStore.tabSize;
extensions.push(tabSizeCompartment.of(indentUnit.of(' '.repeat(tabSize))));
// 如果启用了Tab缩进添加自定义Tab键映射
if (editorStore.enableTabIndent) {
extensions.push(tabKeyCompartment.of(keymap.of([{
key: "Tab",
run: tabHandler
}])));
} else {
extensions.push(tabKeyCompartment.of([]));
}
return extensions;
};
// 创建编辑器 // 创建编辑器
const createEditor = () => { const createEditor = () => {
if (!editorElement.value) return; if (!editorElement.value) return;
const extensions = [ // 获取基本扩展
baseDark, const basicExtensions = createBasicSetup();
fontTheme,
lineNumbers(), // 获取Tab相关扩展
highlightActiveLineGutter(), const tabExtensions = getTabExtensions(
highlightSpecialChars(), editorStore.tabSize,
history(), editorStore.enableTabIndent
foldGutter(), );
drawSelection(),
dropCursor(), // 创建统计信息更新扩展
EditorView.lineWrapping, const statsExtension = createStatsUpdateExtension(
EditorState.allowMultipleSelections.of(true), editorStore.updateDocumentStats
indentOnInput(), );
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
bracketMatching(), // 组合所有扩展
closeBrackets(), const extensions: Extension[] = [
autocompletion(), ...basicExtensions,
rectangularSelection(), ...tabExtensions,
crosshairCursor(), statsExtension
highlightSelectionMatches(),
customHighlightActiveLine,
highlightActiveLine(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
]),
EditorView.updateListener.of(update => {
if (update.docChanged || update.selectionSet) {
updateStats();
}
}),
...getTabExtensions()
]; ];
// 创建编辑器状态
const state = EditorState.create({ const state = EditorState.create({
doc: props.initialDoc, doc: props.initialDoc,
extensions extensions
}); });
// 创建编辑器视图
const view = new EditorView({ const view = new EditorView({
state, state,
parent: editorElement.value parent: editorElement.value
}); });
// 将编辑器实例保存到store // 将编辑器实例保存到store
editorStore.setEditorView(view); editorStore.setEditorView(view);
// 初始化统计
updateStats();
// 应用初始字体大小 // 应用初始字体大小
editorStore.applyFontSize(); applyFontSize(view, editorStore.fontSize);
// 立即更新统计信息,不等待用户交互
updateStats(view, editorStore.updateDocumentStats);
}; };
// 更新统计信息 // 创建滚轮事件处理器
const updateStats = () => { const handleWheel = createWheelZoomHandler(
editorStore.increaseFontSize,
editorStore.decreaseFontSize
);
// 重新配置编辑器(仅在必要时)
const reconfigureTabSettings = () => {
if (!editorStore.editorView) return; if (!editorStore.editorView) return;
updateTabConfig(
const view = editorStore.editorView; editorStore.editorView as EditorView,
const state = view.state; editorStore.tabSize,
const doc = state.doc; editorStore.enableTabIndent
const text = doc.toString(); );
// 计算选中的字符数
let selectedChars = 0;
const selections = state.selection;
if (selections) {
for (let i = 0; i < selections.ranges.length; i++) {
const range = selections.ranges[i];
selectedChars += range.to - range.from;
}
}
editorStore.updateDocumentStats({
lines: doc.lines,
characters: text.length,
selectedCharacters: selectedChars
});
};
// 动态更新Tab配置不需要重建编辑器
const updateTabConfig = () => {
if (!editorStore.editorView) return;
// 更新Tab大小配置
const tabSize = editorStore.tabSize;
const view = editorStore.editorView;
// 更新indentUnit配置
view.dispatch({
effects: tabSizeCompartment.reconfigure(indentUnit.of(' '.repeat(tabSize)))
});
// 更新Tab键映射
const tabKeymap = editorStore.enableTabIndent
? keymap.of([{key: "Tab", run: tabHandler}])
: [];
view.dispatch({
effects: tabKeyCompartment.reconfigure(tabKeymap)
});
};
// 重新配置编辑器(仅在必要时完全重建)
const reconfigureEditor = () => {
if (!editorStore.editorView) return;
// 尝试动态更新配置
updateTabConfig();
}; };
// 监听Tab设置变化 // 监听Tab设置变化
watch(() => editorStore.tabSize, () => { watch(() => editorStore.tabSize, reconfigureTabSettings);
reconfigureEditor(); watch(() => editorStore.enableTabIndent, reconfigureTabSettings);
});
// 监听Tab缩进设置变化
watch(() => editorStore.enableTabIndent, () => {
reconfigureEditor();
});
onMounted(() => { onMounted(() => {
// 创建编辑器 // 创建编辑器
createEditor(); createEditor();
// 添加滚轮事件监听 // 添加滚轮事件监听
if (editorElement.value) { if (editorElement.value) {
editorElement.value.addEventListener('wheel', handleWheel, {passive: false}); editorElement.value.addEventListener('wheel', handleWheel, {passive: false});
} }
// 确保统计信息已更新
if (editorStore.editorView) {
setTimeout(() => {
updateStats(editorStore.editorView as EditorView, editorStore.updateDocumentStats);
}, 100);
}
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -247,7 +113,8 @@ onBeforeUnmount(() => {
if (editorElement.value) { if (editorElement.value) {
editorElement.value.removeEventListener('wheel', handleWheel); editorElement.value.removeEventListener('wheel', handleWheel);
} }
// 销毁编辑器
if (editorStore.editorView) { if (editorStore.editorView) {
editorStore.editorView.destroy(); editorStore.editorView.destroy();
editorStore.setEditorView(null); editorStore.setEditorView(null);

View File

@@ -118,7 +118,6 @@ export const useEditorStore = defineStore('editor', () => {
increaseFontSize, increaseFontSize,
decreaseFontSize, decreaseFontSize,
resetFontSize, resetFontSize,
applyFontSize,
toggleTabIndent, toggleTabIndent,
increaseTabSize, increaseTabSize,
decreaseTabSize decreaseTabSize