✨ Added tab indentation
This commit is contained in:
@@ -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;
|
||||||
|
@@ -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(() => {
|
||||||
|
@@ -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']
|
||||||
|
}
|
||||||
});
|
});
|
Reference in New Issue
Block a user