feat: 添加代码高亮功能

This commit is contained in:
Baobhan Sith
2025-06-04 17:14:24 +08:00
parent 39808e5abb
commit 79b94e479f
3 changed files with 182 additions and 17 deletions
+128 -1
View File
@@ -103,6 +103,41 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@codemirror/language": {
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz",
"integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.37.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.1.tgz",
"integrity": "sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -797,6 +832,30 @@
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp": { "node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@@ -833,6 +892,12 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@nexus-terminal/backend": { "node_modules/@nexus-terminal/backend": {
"resolved": "packages/backend", "resolved": "packages/backend",
"link": true "link": true
@@ -1978,6 +2043,49 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@uiw/codemirror-theme-github": {
"version": "4.23.12",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.23.12.tgz",
"integrity": "sha512-yxgycQxA1fNVdrjIZ7H7pq+9Q+BeKLmD5oq5oOlw7kVJrnToOMBylv5oIWplVd2s2LFo47lIhWrVC9Ay3b6Baw==",
"license": "MIT",
"dependencies": {
"@uiw/codemirror-themes": "4.23.12"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
}
},
"node_modules/@uiw/codemirror-theme-vscode": {
"version": "4.23.12",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.23.12.tgz",
"integrity": "sha512-ePBaUQiixrpmSoZJWCGXUStKmcM8G0VBv3UqwPR+kNGBjqDife76Gbhv77izSeEI3zRPzL+683BOdclkvWnsMg==",
"license": "MIT",
"dependencies": {
"@uiw/codemirror-themes": "4.23.12"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
}
},
"node_modules/@uiw/codemirror-themes": {
"version": "4.23.12",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.23.12.tgz",
"integrity": "sha512-8etEByfS9yttFZW0rcWhdZc7/JXJKRWlU5lHmJCI3GydZNGCzydNA+HtK9nWKpJUndVc58Q2sqSC5OIcwq8y6A==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/language": ">=6.0.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
@@ -3706,6 +3814,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/cross-env": { "node_modules/cross-env": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@@ -8044,6 +8158,12 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
"node_modules/superjson": { "node_modules/superjson": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
@@ -8962,6 +9082,12 @@
"vue": "^3.0.1" "vue": "^3.0.1"
} }
}, },
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -9375,12 +9501,13 @@
}, },
"packages/frontend": { "packages/frontend": {
"name": "@nexus-terminal/frontend", "name": "@nexus-terminal/frontend",
"version": "0.7.5", "version": "0.7.13",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@hcaptcha/vue3-hcaptcha": "^1.3.0", "@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/browser": "^9.0.1",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@uiw/codemirror-theme-vscode": "^4.23.12",
"@vscode/iconv-lite-umd": "^0.7.0", "@vscode/iconv-lite-umd": "^0.7.0",
"@vueuse/core": "^13.1.0", "@vueuse/core": "^13.1.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
+1
View File
@@ -13,6 +13,7 @@
"@hcaptcha/vue3-hcaptcha": "^1.3.0", "@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/browser": "^9.0.1",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@uiw/codemirror-theme-vscode": "^4.23.12",
"@vscode/iconv-lite-umd": "^0.7.0", "@vscode/iconv-lite-umd": "^0.7.0",
"@vueuse/core": "^13.1.0", "@vueuse/core": "^13.1.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
@@ -1,13 +1,17 @@
<template> <template>
<div ref="editorRef" class="codemirror-mobile-editor-container" :style="{ fontSize: currentFontSize + 'px' }"></div> <div ref="editorRef" class="codemirror-mobile-editor-container" :style="{ fontSize: currentFontSize + 'px', fontFamily: editorFontFamily }"></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, shallowRef, computed } from 'vue'; import { ref, onMounted, onBeforeUnmount, watch, shallowRef, computed } from 'vue';
import { EditorState, Compartment } from '@codemirror/state'; import { EditorState, Compartment } from '@codemirror/state';
import { useAppearanceStore } from '../stores/appearance.store'; import { useAppearanceStore } from '../stores/appearance.store';
import { EditorView, keymap } from '@codemirror/view'; import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightActiveLine, drawSelection, dropCursor } from '@codemirror/view';
import { basicSetup } from 'codemirror'; // Use basicSetup from the main 'codemirror' package 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 } from '@codemirror/search';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@@ -16,7 +20,7 @@ const props = defineProps({
}, },
language: { language: {
type: String, type: String,
default: 'plaintext', // Default to plaintext if no language is specified default: 'plaintext',
}, },
}); });
@@ -25,9 +29,7 @@ const emit = defineEmits(['update:modelValue', 'request-save']);
const appearanceStore = useAppearanceStore(); const appearanceStore = useAppearanceStore();
const editorRef = ref<HTMLDivElement | null>(null); const editorRef = ref<HTMLDivElement | null>(null);
const view = shallowRef<EditorView | null>(null); const view = shallowRef<EditorView | null>(null);
const languageCompartment = new Compartment(); // For dynamic language switching const languageCompartment = new Compartment();
// Pinch to zoom state and handlers
// Initialize with a default, will be overwritten by store value in onMounted
const currentFontSize = ref(appearanceStore.currentMobileEditorFontSize); const currentFontSize = ref(appearanceStore.currentMobileEditorFontSize);
const MIN_FONT_SIZE = 8; const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 40; const MAX_FONT_SIZE = 40;
@@ -35,6 +37,8 @@ let lastPinchDistance = 0;
const debounceTimeout = ref<number | null>(null); const debounceTimeout = ref<number | null>(null);
const DEBOUNCE_DELAY = 500; // 500ms 防抖延迟 const DEBOUNCE_DELAY = 500; // 500ms 防抖延迟
const editorFontFamily = computed(() => appearanceStore.currentEditorFontFamily);
const getDistance = (touches: TouchList): number => { const getDistance = (touches: TouchList): number => {
if (touches.length < 2) return 0; if (touches.length < 2) return 0;
const touch1 = touches[0]; const touch1 = touches[0];
@@ -75,7 +79,6 @@ const onTouchMove = (event: TouchEvent) => {
if (Math.abs(currentFontSize.value - newFontSize) > 0.1) { // Only update if change is meaningful if (Math.abs(currentFontSize.value - newFontSize) > 0.1) { // Only update if change is meaningful
currentFontSize.value = newFontSize; currentFontSize.value = newFontSize;
// Persist the new font size to the store with debounce
debouncedSetMobileEditorFontSize(newFontSize); debouncedSetMobileEditorFontSize(newFontSize);
} }
} }
@@ -93,21 +96,39 @@ const onTouchEnd = (event: TouchEvent) => {
lastPinchDistance = 0; lastPinchDistance = 0;
} }
}; };
const createEditorState = (doc: string, languageExtension: any) => { const createEditorState = (doc: string, languageExtension: any) => {
return EditorState.create({ return EditorState.create({
doc, doc,
extensions: [ extensions: [
basicSetup, // Includes many common features like line numbers, history, default keymaps etc. // Minimal set of extensions for testing highlighting
keymap.of([ languageCompartment.of(languageExtension), // Crucial: applies the CSS language pack
{ key: "Mod-s", run: () => { emit('request-save'); return true; } } // oneDark, // REMOVING oneDark theme
]), vscodeDark, // Use the pre-built vscodeDark theme
lineNumbers(), // RE-ADDING lineNumbers
history(), // RE-ADDING history
highlightActiveLineGutter(), // RE-ADDING highlightActiveLineGutter
foldGutter(), // RE-ADDING foldGutter
drawSelection(), // RE-ADDING drawSelection
dropCursor(), // RE-ADDING dropCursor
EditorState.allowMultipleSelections.of(true), // RE-ADDING allowMultipleSelections
indentOnInput(), // RE-ADDING indentOnInput
bracketMatching(), // RE-ADDING bracketMatching
highlightActiveLine(), // RE-ADDING highlightActiveLine
closeBrackets(), // RE-ADDING closeBrackets
autocompletion(), // RE-ADDING autocompletion
highlightSelectionMatches(), // RE-ADDING highlightSelectionMatches
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
if (update.docChanged) { if (update.docChanged) {
emit('update:modelValue', update.state.doc.toString()); emit('update:modelValue', update.state.doc.toString());
} }
}), }),
languageCompartment.of(languageExtension), // Initial language keymap.of([
...closeBracketsKeymap, // RE-ADDING closeBracketsKeymap
...defaultKeymap, // RE-ADDING defaultKeymap
...historyKeymap, // RE-ADDING historyKeymap
...foldKeymap, // RE-ADDING foldKeymap
{ key: "Mod-s", run: () => { emit('request-save'); return true; } } // Optional: keep for testing save
]),
], ],
}); });
}; };
@@ -118,8 +139,22 @@ const getLanguageExtension = async (lang: string) => {
return javascript(); return javascript();
} }
if (lang === 'css') { if (lang === 'css') {
const { css } = await import('@codemirror/lang-css'); try {
return css(); console.log('[CodeMirrorMobileEditor DEBUG] Attempting to import @codemirror/lang-css for language:', lang);
const cssModule = await import('@codemirror/lang-css');
console.log('[CodeMirrorMobileEditor DEBUG] @codemirror/lang-css imported:', cssModule);
if (cssModule && typeof cssModule.css === 'function') {
const cssExtension = cssModule.css();
console.log('[CodeMirrorMobileEditor DEBUG] CSS extension object created:', cssExtension);
return cssExtension;
} else {
console.error('[CodeMirrorMobileEditor DEBUG] @codemirror/lang-css module or css function is invalid. Module:', cssModule);
return [];
}
} catch (error) {
console.error('[CodeMirrorMobileEditor DEBUG] Error loading/initializing CSS language support:', error);
return [];
}
} }
if (lang === 'html') { if (lang === 'html') {
const { html } = await import('@codemirror/lang-html'); const { html } = await import('@codemirror/lang-html');
@@ -135,6 +170,7 @@ onMounted(async () => {
if (editorRef.value) { if (editorRef.value) {
const langExt = await getLanguageExtension(props.language); const langExt = await getLanguageExtension(props.language);
console.log('[CodeMirrorMobileEditor DEBUG] onMounted - Initial language:', props.language, 'Fetched langExt:', langExt);
const startState = createEditorState(props.modelValue, langExt); const startState = createEditorState(props.modelValue, langExt);
view.value = new EditorView({ view.value = new EditorView({
@@ -177,6 +213,7 @@ watch(() => props.language, async (newLanguage, oldLanguage) => {
if (view.value && newLanguage !== oldLanguage) { if (view.value && newLanguage !== oldLanguage) {
console.log(`Language changing from ${oldLanguage} to: ${newLanguage}.`); console.log(`Language changing from ${oldLanguage} to: ${newLanguage}.`);
const langExt = await getLanguageExtension(newLanguage); const langExt = await getLanguageExtension(newLanguage);
console.log(`[CodeMirrorMobileEditor DEBUG] watch props.language - New language: ${newLanguage}, Fetched langExt:`, langExt);
view.value.dispatch({ view.value.dispatch({
effects: languageCompartment.reconfigure(langExt) effects: languageCompartment.reconfigure(langExt)
}); });