Added tab indentation

This commit is contained in:
2025-04-24 21:43:07 +08:00
parent ab0255a775
commit f41a13c217
3 changed files with 337 additions and 142 deletions

View File

@@ -21,6 +21,17 @@ const editorStore = useEditorStore();
<span class="font-size" title="字体大小 (Ctrl+滚轮调整)"> <span class="font-size" title="字体大小 (Ctrl+滚轮调整)">
{{ editorStore.fontSize }}px {{ editorStore.fontSize }}px
</span> </span>
<span class="tab-settings">
<label title="启用Tab键缩进" class="tab-toggle">
<input type="checkbox" :checked="editorStore.enableTabIndent" @change="editorStore.toggleTabIndent"/>
<span>Tab</span>
</label>
<span class="tab-size" title="Tab大小">
<button class="tab-btn" @click="editorStore.decreaseTabSize" :disabled="editorStore.tabSize <= 2">-</button>
<span>{{ editorStore.tabSize }}</span>
<button class="tab-btn" @click="editorStore.increaseTabSize" :disabled="editorStore.tabSize >= 8">+</button>
</span>
</span>
<span class="encoding">{{ editorStore.encoding }}</span> <span class="encoding">{{ editorStore.encoding }}</span>
<button class="settings-btn" @click="editorStore.openSettings"> <button class="settings-btn" @click="editorStore.openSettings">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
@@ -70,6 +81,47 @@ const editorStore = useEditorStore();
cursor: help; cursor: help;
} }
.tab-settings {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-muted);
font-size: 11px;
.tab-toggle {
display: flex;
align-items: center;
gap: 3px;
cursor: pointer;
input {
cursor: pointer;
}
}
.tab-size {
display: flex;
align-items: center;
gap: 2px;
.tab-btn {
background: none;
border: none;
color: var(--text-primary);
cursor: pointer;
padding: 0 3px;
font-size: 12px;
line-height: 1;
&:disabled {
color: var(--text-muted);
opacity: 0.5;
cursor: not-allowed;
}
}
}
}
.encoding { .encoding {
color: var(--text-muted); color: var(--text-muted);
font-size: 11px; font-size: 11px;

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import {onBeforeUnmount, onMounted, ref} from 'vue'; import {onBeforeUnmount, onMounted, ref, watch} from 'vue';
import { import {
crosshairCursor, crosshairCursor,
drawSelection, drawSelection,
@@ -11,7 +11,7 @@ import {
lineNumbers, lineNumbers,
rectangularSelection, rectangularSelection,
} from '@codemirror/view'; } from '@codemirror/view';
import {EditorState} from '@codemirror/state'; import {Compartment, EditorState, Extension} from '@codemirror/state';
import {baseDark} from "@/editor/theme/base-dark"; import {baseDark} from "@/editor/theme/base-dark";
import { import {
bracketMatching, bracketMatching,
@@ -19,16 +19,21 @@ import {
foldGutter, foldGutter,
foldKeymap, foldKeymap,
indentOnInput, indentOnInput,
syntaxHighlighting syntaxHighlighting,
indentUnit
} from '@codemirror/language'; } from '@codemirror/language';
import {defaultKeymap, history, historyKeymap} from '@codemirror/commands'; import {
defaultKeymap,
history,
historyKeymap,
indentSelection,
} from '@codemirror/commands';
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search'; import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
import {autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete'; import {autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
import {lintKeymap} from '@codemirror/lint'; import {lintKeymap} from '@codemirror/lint';
import {fontTheme} from "@/editor/font/font"; import {fontTheme} from "@/editor/font/font";
import { useEditorStore } from '@/stores/editor'; import {useEditorStore} from '@/stores/editor';
// 使用Pinia store
const editorStore = useEditorStore(); const editorStore = useEditorStore();
const props = defineProps({ const props = defineProps({
@@ -38,6 +43,10 @@ const props = defineProps({
} }
}); });
// 使用Compartment可以动态更新特定的配置而不重建整个编辑器
const tabSizeCompartment = new Compartment();
const tabKeyCompartment = new Compartment();
const editorElement = ref<HTMLElement | null>(null); const editorElement = ref<HTMLElement | null>(null);
// 处理滚轮缩放字体 // 处理滚轮缩放字体
@@ -58,38 +67,50 @@ const handleWheel = (event: WheelEvent) => {
} }
}; };
// 更新统计信息 // 自定义Tab键处理函数
const updateStats = () => { const tabHandler = (view: EditorView): boolean => {
if (!editorStore.editorView) return; // 如果有选中文本使用indentSelection
if (!view.state.selection.main.empty) {
const view = editorStore.editorView; return indentSelection(view);
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;
}
} }
editorStore.updateDocumentStats({ // 获取当前的tabSize值
lines: doc.lines, const currentTabSize = editorStore.tabSize;
characters: text.length, // 创建相应数量的空格
selectedCharacters: selectedChars const spaces = ' '.repeat(currentTabSize);
});
// 在光标位置插入空格
const { state, dispatch } = view;
dispatch(state.update(state.replaceSelection(spaces), { scrollIntoView: true }));
return true;
}; };
onMounted(() => { // 获取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 = () => {
if (!editorElement.value) return; if (!editorElement.value) return;
const state = EditorState.create({ const extensions = [
doc: props.initialDoc,
extensions: [
baseDark, baseDark,
fontTheme, fontTheme,
lineNumbers(), lineNumbers(),
@@ -122,8 +143,13 @@ onMounted(() => {
if (update.docChanged || update.selectionSet) { if (update.docChanged || update.selectionSet) {
updateStats(); updateStats();
} }
}) }),
] ...getTabExtensions()
];
const state = EditorState.create({
doc: props.initialDoc,
extensions
}); });
const view = new EditorView({ const view = new EditorView({
@@ -139,9 +165,83 @@ onMounted(() => {
// 应用初始字体大小 // 应用初始字体大小
editorStore.applyFontSize(); editorStore.applyFontSize();
};
// 更新统计信息
const updateStats = () => {
if (!editorStore.editorView) return;
const view = editorStore.editorView;
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;
}
}
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设置变化
watch(() => editorStore.tabSize, () => {
reconfigureEditor();
});
// 监听Tab缩进设置变化
watch(() => editorStore.enableTabIndent, () => {
reconfigureEditor();
});
onMounted(() => {
// 创建编辑器
createEditor();
// 添加滚轮事件监听 // 添加滚轮事件监听
if (editorElement.value) {
editorElement.value.addEventListener('wheel', handleWheel, { passive: false }); editorElement.value.addEventListener('wheel', handleWheel, { passive: false });
}
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@@ -1,12 +1,17 @@
import { defineStore } from 'pinia'; import {defineStore} from 'pinia';
import { ref } from 'vue'; import {ref} from 'vue';
import { DocumentStats } from '@/types/editor'; import {DocumentStats} from '@/types/editor';
import { EditorView } from '@codemirror/view'; import {EditorView} from '@codemirror/view';
// 字体大小范围 // 字体大小范围
const MIN_FONT_SIZE = 12; const MIN_FONT_SIZE = 12;
const MAX_FONT_SIZE = 28; const MAX_FONT_SIZE = 28;
const DEFAULT_FONT_SIZE = 12; const DEFAULT_FONT_SIZE = 13;
// Tab设置
const DEFAULT_TAB_SIZE = 4;
const MIN_TAB_SIZE = 2;
const MAX_TAB_SIZE = 8;
export const useEditorStore = defineStore('editor', () => { export const useEditorStore = defineStore('editor', () => {
// 状态 // 状态
@@ -15,20 +20,28 @@ export const useEditorStore = defineStore('editor', () => {
characters: 0, characters: 0,
selectedCharacters: 0 selectedCharacters: 0
}); });
// 编码
const encoding = ref('UTF-8'); const encoding = ref('UTF-8');
// 编辑器视图
const editorView = ref<EditorView | null>(null); const editorView = ref<EditorView | null>(null);
// 字体大小
const fontSize = ref(DEFAULT_FONT_SIZE); const fontSize = ref(DEFAULT_FONT_SIZE);
// Tab键设置
const enableTabIndent = ref(true);
// Tab键大小
const tabSize = ref(DEFAULT_TAB_SIZE);
// 方法 // 方法
function setEditorView(view: EditorView | null) { function setEditorView(view: EditorView | null) {
editorView.value = view; editorView.value = view;
} }
// 更新文档统计信息
function updateDocumentStats(stats: DocumentStats) { function updateDocumentStats(stats: DocumentStats) {
documentStats.value = stats; documentStats.value = stats;
} }
// 设置编码
function setEncoding(newEncoding: string) { function setEncoding(newEncoding: string) {
encoding.value = newEncoding; encoding.value = newEncoding;
} }
@@ -41,6 +54,7 @@ export const useEditorStore = defineStore('editor', () => {
} }
} }
// 字体缩放
function decreaseFontSize() { function decreaseFontSize() {
if (fontSize.value > MIN_FONT_SIZE) { if (fontSize.value > MIN_FONT_SIZE) {
fontSize.value -= 1; fontSize.value -= 1;
@@ -48,14 +62,15 @@ export const useEditorStore = defineStore('editor', () => {
} }
} }
// 重置字体大小
function resetFontSize() { function resetFontSize() {
fontSize.value = DEFAULT_FONT_SIZE; fontSize.value = DEFAULT_FONT_SIZE;
applyFontSize(); applyFontSize();
} }
// 应用字体大小
function applyFontSize() { function applyFontSize() {
if (!editorView.value) return; if (!editorView.value) return;
// 更新编辑器的字体大小 // 更新编辑器的字体大小
const editorDOM = editorView.value.dom; const editorDOM = editorView.value.dom;
if (editorDOM) { if (editorDOM) {
@@ -63,6 +78,23 @@ export const useEditorStore = defineStore('editor', () => {
} }
} }
// Tab相关方法
function toggleTabIndent() {
enableTabIndent.value = !enableTabIndent.value;
}
// 增加Tab大小
function increaseTabSize() {
if (tabSize.value < MAX_TAB_SIZE) {
tabSize.value += 1;
}
}
// 减少Tab大小
function decreaseTabSize() {
if (tabSize.value > MIN_TAB_SIZE) {
tabSize.value -= 1;
}
}
// 设置按钮操作 // 设置按钮操作
function openSettings() { function openSettings() {
console.log('打开设置面板'); console.log('打开设置面板');
@@ -75,6 +107,8 @@ export const useEditorStore = defineStore('editor', () => {
encoding, encoding,
editorView, editorView,
fontSize, fontSize,
enableTabIndent,
tabSize,
// 方法 // 方法
setEditorView, setEditorView,
@@ -84,6 +118,15 @@ export const useEditorStore = defineStore('editor', () => {
increaseFontSize, increaseFontSize,
decreaseFontSize, decreaseFontSize,
resetFontSize, resetFontSize,
applyFontSize applyFontSize,
toggleTabIndent,
increaseTabSize,
decreaseTabSize
}; };
}, {
persist: {
key: 'editor',
storage: localStorage,
pick: ['fontSize', 'encoding', 'enableTabIndent', 'tabSize']
}
}); });