Files
nexus-terminal/packages/frontend/src/components/CodeMirrorMobileEditor.vue
T
2025-06-04 18:00:51 +08:00

307 lines
9.3 KiB
Vue

<template>
<div ref="editorRef" class="codemirror-mobile-editor-container" :style="{ fontSize: currentFontSize + 'px', fontFamily: editorFontFamily }"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, shallowRef, computed } from 'vue';
import { EditorState, Compartment } from '@codemirror/state';
import { useAppearanceStore } from '../stores/appearance.store';
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightActiveLine, drawSelection, dropCursor } from '@codemirror/view';
import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching, foldGutter, foldKeymap } from '@codemirror/language';
import { vscodeDark } from '@uiw/codemirror-theme-vscode';
import { history, historyKeymap, defaultKeymap } from '@codemirror/commands';
import { autocompletion, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
import { highlightSelectionMatches, searchKeymap, openSearchPanel } from '@codemirror/search'; // + Import search functionalities
const props = defineProps({
modelValue: {
type: String,
default: '',
},
language: {
type: String,
default: 'plaintext',
},
});
const emit = defineEmits(['update:modelValue', 'request-save']);
const appearanceStore = useAppearanceStore();
const editorRef = ref<HTMLDivElement | null>(null);
const view = shallowRef<EditorView | null>(null);
const languageCompartment = new Compartment();
const currentFontSize = ref(appearanceStore.currentMobileEditorFontSize);
const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 40;
let lastPinchDistance = 0;
const debounceTimeout = ref<number | null>(null);
const DEBOUNCE_DELAY = 500; // 500ms 防抖延迟
const editorFontFamily = computed(() => appearanceStore.currentEditorFontFamily);
const getDistance = (touches: TouchList): number => {
if (touches.length < 2) return 0;
const touch1 = touches[0];
const touch2 = touches[1];
return Math.sqrt(
Math.pow(touch2.pageX - touch1.pageX, 2) +
Math.pow(touch2.pageY - touch1.pageY, 2)
);
};
const onTouchStart = (event: TouchEvent) => {
if (editorRef.value && editorRef.value.contains(event.target as Node)) {
if (event.touches.length === 2) {
event.preventDefault();
lastPinchDistance = getDistance(event.touches);
}
}
};
const debouncedSetMobileEditorFontSize = (size: number) => {
if (debounceTimeout.value !== null) {
clearTimeout(debounceTimeout.value);
}
debounceTimeout.value = window.setTimeout(() => {
appearanceStore.setMobileEditorFontSize(size);
}, DEBOUNCE_DELAY);
};
const onTouchMove = (event: TouchEvent) => {
if (editorRef.value && editorRef.value.contains(event.target as Node)) {
if (event.touches.length === 2) {
event.preventDefault();
const newPinchDistance = getDistance(event.touches);
if (lastPinchDistance > 0 && newPinchDistance > 0) {
const scale = newPinchDistance / lastPinchDistance;
let newFontSize = currentFontSize.value * scale;
newFontSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, newFontSize));
if (Math.abs(currentFontSize.value - newFontSize) > 0.1) {
currentFontSize.value = newFontSize;
debouncedSetMobileEditorFontSize(newFontSize);
}
}
if (newPinchDistance > 0) {
lastPinchDistance = newPinchDistance;
} else if (event.touches.length === 2) {
lastPinchDistance = getDistance(event.touches);
}
}
}
};
const onTouchEnd = (event: TouchEvent) => {
if (event.touches.length < 2) {
lastPinchDistance = 0;
}
};
const createEditorState = (doc: string, languageExtension: any) => {
return EditorState.create({
doc,
extensions: [
languageCompartment.of(languageExtension),
vscodeDark,
lineNumbers(),
history(),
highlightActiveLineGutter(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
bracketMatching(),
highlightActiveLine(),
closeBrackets(),
autocompletion(),
highlightSelectionMatches(),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
emit('update:modelValue', update.state.doc.toString());
}
}),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...foldKeymap,
...searchKeymap, // + Add search keymap
{ key: "Mod-s", run: () => { emit('request-save'); return true; } }
]),
],
});
};
const getLanguageExtension = async (lang: string) => {
if (lang === 'javascript') {
const { javascript } = await import('@codemirror/lang-javascript');
return javascript();
}
if (lang === 'css') {
try {
const cssModule = await import('@codemirror/lang-css');
if (cssModule && typeof cssModule.css === 'function') {
const cssExtension = cssModule.css();
return cssExtension;
} else {
return [];
}
} catch (error) {
return [];
}
}
if (lang === 'html') {
const { html } = await import('@codemirror/lang-html');
return html();
}
if (lang === 'python') {
const { python } = await import('@codemirror/lang-python');
return python();
}
if (lang === 'java') {
const { java } = await import('@codemirror/lang-java');
return java();
}
if (lang === 'cpp') {
const { cpp } = await import('@codemirror/lang-cpp');
return cpp();
}
if (lang === 'php') {
const { php } = await import('@codemirror/lang-php');
return php();
}
if (lang === 'go') {
const { go } = await import('@codemirror/lang-go');
return go();
}
if (lang === 'rust') {
const { rust } = await import('@codemirror/lang-rust');
return rust();
}
if (lang === 'sql') {
const { sql } = await import('@codemirror/lang-sql');
return sql();
}
if (lang === 'json') {
const { json } = await import('@codemirror/lang-json');
return json();
}
if (lang === 'yaml') {
const { yaml } = await import('@codemirror/lang-yaml');
return yaml();
}
if (lang === 'xml') {
const { xml } = await import('@codemirror/lang-xml');
return xml();
}
if (lang === 'shell' || lang === 'bash') {
const { StreamLanguage } = await import('@codemirror/language');
const { shell } = await import('@codemirror/legacy-modes/mode/shell');
return StreamLanguage.define(shell);
}
if (lang === 'markdown') {
const { markdown, commonmarkLanguage } = await import('@codemirror/lang-markdown');
const { GFM } = await import('@lezer/markdown');
return markdown({
base: commonmarkLanguage,
extensions: GFM
});
}
if (lang === 'typescript' || lang === 'ts' || lang === 'tsx') {
const { javascript } = await import('@codemirror/lang-javascript');
return javascript({ typescript: true, jsx: true });
}
return [];
};
onMounted(async () => {
// Initialize font size from store
currentFontSize.value = appearanceStore.currentMobileEditorFontSize;
if (editorRef.value) {
const langExt = await getLanguageExtension(props.language);
console.log('[CodeMirrorMobileEditor DEBUG] onMounted - Initial language:', props.language, 'Fetched langExt:', langExt);
const startState = createEditorState(props.modelValue, langExt);
view.value = new EditorView({
state: startState,
parent: editorRef.value,
});
editorRef.value.addEventListener('touchstart', onTouchStart, { passive: false });
editorRef.value.addEventListener('touchmove', onTouchMove, { passive: false });
editorRef.value.addEventListener('touchend', onTouchEnd, { passive: false });
}
});
onBeforeUnmount(() => {
if (view.value) {
view.value.destroy();
view.value = null;
}
if (editorRef.value) {
editorRef.value.removeEventListener('touchstart', onTouchStart);
editorRef.value.removeEventListener('touchmove', onTouchMove);
editorRef.value.removeEventListener('touchend', onTouchEnd);
}
if (debounceTimeout.value !== null) {
clearTimeout(debounceTimeout.value);
}
});
watch(() => props.modelValue, (newValue) => {
if (view.value && newValue !== view.value.state.doc.toString()) {
view.value.dispatch({
changes: { from: 0, to: view.value.state.doc.length, insert: newValue },
});
}
});
watch(() => props.language, async (newLanguage, oldLanguage) => {
if (view.value && newLanguage !== oldLanguage) {
const langExt = await getLanguageExtension(newLanguage);
view.value.dispatch({
effects: languageCompartment.reconfigure(langExt)
});
}
});
watch(() => appearanceStore.currentMobileEditorFontSize, (newSize) => {
if (newSize !== currentFontSize.value) {
currentFontSize.value = newSize;
}
});
const openSearch = () => {
if (view.value) {
openSearchPanel(view.value);
}
};
defineExpose({
focus: () => view.value?.focus(),
openSearch, // + Expose openSearch method
});
</script>
<style scoped>
.codemirror-mobile-editor-container {
width: 100%;
height: 100%;
min-height: 200px;
text-align: left;
overflow: auto;
}
.codemirror-mobile-editor-container :deep(.cm-gutters) {
background-color: #1E1E1E !important;
color: #858585 !important;
border-right: 1px solid var(--border-color, #cccccc) !important;
}
.codemirror-mobile-editor-container :deep(.cm-selectionBackground) {
background-color: #5264ac !important;
}
</style>