From 8d6184fd8b03cd0c2edf1c96475b43e87f96e1f7 Mon Sep 17 00:00:00 2001 From: Afiq Date: Thu, 7 Aug 2025 19:50:17 +0800 Subject: [PATCH] Enhance RsCodeMirror Component with Quick Dev Features and Improved Formatting - Added quick development features including toggles for line numbers, word wrap, and active line highlighting to enhance the coding experience. - Implemented search functionality with keyboard shortcuts for improved code navigation and editing. - Enhanced JSON formatting capabilities to handle JSON code without relying on Prettier, ensuring better performance for JSON files. - Updated the component's template and styles to accommodate new features, including a responsive layout for quick dev tools. - Improved cursor position tracking and display for better user feedback during code editing. --- components/RsCodeMirror.vue | 229 +++++++++++++++++++++++++++++------- 1 file changed, 189 insertions(+), 40 deletions(-) diff --git a/components/RsCodeMirror.vue b/components/RsCodeMirror.vue index d4907bc..1bafde5 100644 --- a/components/RsCodeMirror.vue +++ b/components/RsCodeMirror.vue @@ -11,6 +11,9 @@ import { amy, ayuLight, barf, clouds, cobalt, dracula } from "thememirror"; import { autocompletion } from "@codemirror/autocomplete"; import { indentUnit } from "@codemirror/language"; import { indentOnInput } from "@codemirror/language"; +import { EditorView } from "@codemirror/view"; +import { searchKeymap, search } from "@codemirror/search"; +import { keymap } from "@codemirror/view"; import { useDebounceFn } from "@vueuse/core"; // Dynamically import Prettier and its plugins @@ -90,6 +93,12 @@ const dropdownThemes = ref([ const value = ref(props.modelValue); const extensions = ref([]); +// Quick dev features +const showLineNumbers = ref(true); +const enableWordWrap = ref(false); +const highlightActiveLine = ref(true); +const enableSearch = ref(true); + // Helper function to get theme extension const getThemeExtension = (themeVal) => { switch (themeVal) { @@ -121,6 +130,8 @@ const getLanguageExtension = () => { return css(); case "html": return css(); // Use CSS for HTML highlighting + case "json": + return javascript(); case "javascript": case "js": default: @@ -134,13 +145,49 @@ const initializeExtensions = () => { const themeExtension = getThemeExtension(currentTheme); const languageExtension = getLanguageExtension(); - extensions.value = [ + const baseExtensions = [ languageExtension, themeExtension, autocompletion(), indentUnit.of(" "), indentOnInput(), ]; + + // Don't add lineNumbers() extension - vue-codemirror already has them + // We'll control visibility via CSS instead + + if (enableWordWrap.value) { + baseExtensions.push(EditorView.lineWrapping); + } + + if (highlightActiveLine.value) { + baseExtensions.push(EditorView.theme({ + '.cm-activeLine': { backgroundColor: 'rgba(255, 255, 255, 0.1)' }, + '.cm-focused .cm-activeLine': { backgroundColor: 'rgba(255, 255, 255, 0.1)' } + })); + } + + if (enableSearch.value) { + baseExtensions.push(search()); + baseExtensions.push(keymap.of(searchKeymap)); + } + + extensions.value = baseExtensions; +}; + +// Toggle functions +const toggleLineNumbers = () => { + showLineNumbers.value = !showLineNumbers.value; +}; + +const toggleWordWrap = () => { + enableWordWrap.value = !enableWordWrap.value; + initializeExtensions(); +}; + +const toggleActiveLine = () => { + highlightActiveLine.value = !highlightActiveLine.value; + initializeExtensions(); }; // Initialize extensions on component creation @@ -148,6 +195,7 @@ initializeExtensions(); const totalLines = ref(0); const totalLength = ref(0); +const cursorPosition = ref({ line: 1, column: 1 }); // Codemirror EditorView instance ref const view = shallowRef(); @@ -155,6 +203,23 @@ const handleReady = (payload) => { view.value = payload.view; totalLines.value = view.value.state.doc.lines; totalLength.value = view.value.state.doc.length; + + // Track cursor position + const updateCursor = () => { + if (view.value) { + const state = view.value.state; + const pos = state.selection.main.head; + const line = state.doc.lineAt(pos); + cursorPosition.value = { + line: line.number, + column: pos - line.from + 1 + }; + } + }; + + view.value.dom.addEventListener('click', updateCursor); + view.value.dom.addEventListener('keyup', updateCursor); + updateCursor(); }; // Watch for theme changes @@ -162,16 +227,7 @@ watch( () => props.theme || editorTheme.value, (newTheme) => { if (newTheme) { - const themeExtension = getThemeExtension(newTheme); - const languageExtension = getLanguageExtension(); - - extensions.value = [ - languageExtension, - themeExtension, - autocompletion(), - indentUnit.of(" "), - indentOnInput(), - ]; + initializeExtensions(); } }, { immediate: true } @@ -190,17 +246,7 @@ watch( () => editorTheme.value, (newTheme) => { if (!props.theme && newTheme) { - // Only update if no explicit theme prop is provided - const themeExtension = getThemeExtension(newTheme); - const languageExtension = getLanguageExtension(); - - extensions.value = [ - languageExtension, - themeExtension, - autocompletion(), - indentUnit.of(" "), - indentOnInput(), - ]; + initializeExtensions(); } } ); @@ -253,19 +299,53 @@ const loadPrettier = async () => { // Function Format Code const formatCode = async (code) => { + // Handle JSON formatting without Prettier + if (props.language === 'json') { + try { + const parsed = JSON.parse(code); + return JSON.stringify(parsed, null, 2); + } catch (error) { + console.error('JSON formatting error:', error); + return code; + } + } + + // Use Prettier for other languages await loadPrettier(); try { + let parser = "babel"; + let plugins = [parserBabel.value]; + + switch (props.language) { + case "vue": + parser = "vue"; + plugins = [parserHTML.value, parserBabel.value, parserPostCSS.value, pluginVue.value]; + break; + case "css": + case "scss": + parser = "css"; + plugins = [parserPostCSS.value]; + break; + case "html": + parser = "html"; + plugins = [parserHTML.value]; + break; + case "javascript": + case "js": + default: + parser = "babel"; + plugins = [parserBabel.value]; + break; + } + const formattedCode = await prettier.value.format(code, { - parser: "vue", - plugins: [ - parserHTML.value, - parserBabel.value, - parserPostCSS.value, - pluginVue.value, - ], + parser: parser, + plugins: plugins, semi: false, singleQuote: true, trailingComma: "es5", + printWidth: 80, + tabWidth: 2, }); return formattedCode; } catch (error) { @@ -280,8 +360,12 @@ const formatCurrentCode = async () => { value.value = formattedCode; emits("update:modelValue", formattedCode); emits("format-code"); + + // Show success message based on language + const langName = props.language === 'json' ? 'JSON' : props.language.toUpperCase(); + console.log(`${langName} formatted successfully`); } catch (error) { - console.log("Error formatting code:", error); + console.error("Error formatting code:", error); } }; @@ -364,7 +448,8 @@ watch( props.class, { 'fullscreen-editor': isFullscreen, - 'fixed inset-0 z-[9999] bg-[#282C34]': isFullscreen + 'fixed inset-0 z-[9999] bg-[#282C34]': isFullscreen, + 'hide-line-numbers': !showLineNumbers } ]" > @@ -387,6 +472,35 @@ watch( }" /> + + +
+ + + +
+
Press Esc or @@ -408,9 +522,10 @@ watch( - - Format Code (Shift + Alt + F) + + {{ getFormatButtonText() }}
@@ -438,16 +553,19 @@ watch( class="footer flex justify-between items-center gap-2 p-2 bg-[#282C34] text-[#abb2bf] border-t border-gray-600" >
- +
Shortcuts: - Ctrl+Shift+F Format, - F11 Toggle Fullscreen - + Ctrl+F Search, + Ctrl+H Replace, + Shift+Alt+F Format + , F11 Exit Fullscreen +
-
- Lines: {{ numberComma(totalLines) }} - Length: {{ numberComma(totalLength) }} +
+ Lines: {{ numberComma(totalLines) }} + Length: {{ numberComma(totalLength) }} + Ln {{ cursorPosition.line }}, Col {{ cursorPosition.column }}
@@ -536,6 +654,37 @@ button:focus { outline: 2px solid #3b82f6; outline-offset: -2px; } + +/* Hide line numbers when toggle is off */ +.hide-line-numbers :deep(.cm-lineNumbers) { + display: none; +} + +.hide-line-numbers :deep(.cm-gutters) { + display: none; +} + +/* Adjust content when line numbers are hidden */ +.hide-line-numbers :deep(.cm-content) { + padding-left: 8px; +} + +/* Quick dev tools styling */ +.dev-toggle { + @apply px-2 py-1 text-xs rounded transition-colors; +} + +.dev-toggle.active { + @apply bg-blue-600 text-white; +} + +.dev-toggle.inactive { + @apply bg-gray-600 text-gray-300; +} + +.dev-toggle.inactive:hover { + @apply bg-gray-500; +}