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.
This commit is contained in:
Afiq 2025-08-07 19:50:17 +08:00
parent 4b23da5239
commit 8d6184fd8b

View File

@ -11,6 +11,9 @@ import { amy, ayuLight, barf, clouds, cobalt, dracula } from "thememirror";
import { autocompletion } from "@codemirror/autocomplete"; import { autocompletion } from "@codemirror/autocomplete";
import { indentUnit } from "@codemirror/language"; import { indentUnit } from "@codemirror/language";
import { indentOnInput } 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"; import { useDebounceFn } from "@vueuse/core";
// Dynamically import Prettier and its plugins // Dynamically import Prettier and its plugins
@ -90,6 +93,12 @@ const dropdownThemes = ref([
const value = ref(props.modelValue); const value = ref(props.modelValue);
const extensions = ref([]); 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 // Helper function to get theme extension
const getThemeExtension = (themeVal) => { const getThemeExtension = (themeVal) => {
switch (themeVal) { switch (themeVal) {
@ -121,6 +130,8 @@ const getLanguageExtension = () => {
return css(); return css();
case "html": case "html":
return css(); // Use CSS for HTML highlighting return css(); // Use CSS for HTML highlighting
case "json":
return javascript();
case "javascript": case "javascript":
case "js": case "js":
default: default:
@ -134,13 +145,49 @@ const initializeExtensions = () => {
const themeExtension = getThemeExtension(currentTheme); const themeExtension = getThemeExtension(currentTheme);
const languageExtension = getLanguageExtension(); const languageExtension = getLanguageExtension();
extensions.value = [ const baseExtensions = [
languageExtension, languageExtension,
themeExtension, themeExtension,
autocompletion(), autocompletion(),
indentUnit.of(" "), indentUnit.of(" "),
indentOnInput(), 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 // Initialize extensions on component creation
@ -148,6 +195,7 @@ initializeExtensions();
const totalLines = ref(0); const totalLines = ref(0);
const totalLength = ref(0); const totalLength = ref(0);
const cursorPosition = ref({ line: 1, column: 1 });
// Codemirror EditorView instance ref // Codemirror EditorView instance ref
const view = shallowRef(); const view = shallowRef();
@ -155,6 +203,23 @@ const handleReady = (payload) => {
view.value = payload.view; view.value = payload.view;
totalLines.value = view.value.state.doc.lines; totalLines.value = view.value.state.doc.lines;
totalLength.value = view.value.state.doc.length; 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 // Watch for theme changes
@ -162,16 +227,7 @@ watch(
() => props.theme || editorTheme.value, () => props.theme || editorTheme.value,
(newTheme) => { (newTheme) => {
if (newTheme) { if (newTheme) {
const themeExtension = getThemeExtension(newTheme); initializeExtensions();
const languageExtension = getLanguageExtension();
extensions.value = [
languageExtension,
themeExtension,
autocompletion(),
indentUnit.of(" "),
indentOnInput(),
];
} }
}, },
{ immediate: true } { immediate: true }
@ -190,17 +246,7 @@ watch(
() => editorTheme.value, () => editorTheme.value,
(newTheme) => { (newTheme) => {
if (!props.theme && newTheme) { if (!props.theme && newTheme) {
// Only update if no explicit theme prop is provided initializeExtensions();
const themeExtension = getThemeExtension(newTheme);
const languageExtension = getLanguageExtension();
extensions.value = [
languageExtension,
themeExtension,
autocompletion(),
indentUnit.of(" "),
indentOnInput(),
];
} }
} }
); );
@ -253,19 +299,53 @@ const loadPrettier = async () => {
// Function Format Code // Function Format Code
const formatCode = async (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(); await loadPrettier();
try { 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, { const formattedCode = await prettier.value.format(code, {
parser: "vue", parser: parser,
plugins: [ plugins: plugins,
parserHTML.value,
parserBabel.value,
parserPostCSS.value,
pluginVue.value,
],
semi: false, semi: false,
singleQuote: true, singleQuote: true,
trailingComma: "es5", trailingComma: "es5",
printWidth: 80,
tabWidth: 2,
}); });
return formattedCode; return formattedCode;
} catch (error) { } catch (error) {
@ -280,8 +360,12 @@ const formatCurrentCode = async () => {
value.value = formattedCode; value.value = formattedCode;
emits("update:modelValue", formattedCode); emits("update:modelValue", formattedCode);
emits("format-code"); 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) { } catch (error) {
console.log("Error formatting code:", error); console.error("Error formatting code:", error);
} }
}; };
@ -364,7 +448,8 @@ watch(
props.class, props.class,
{ {
'fullscreen-editor': isFullscreen, '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(
}" }"
/> />
</div> </div>
<!-- Quick dev tools -->
<div class="flex items-center gap-1 ml-3 border-l border-gray-600 pl-3">
<button
@click="toggleLineNumbers"
:title="showLineNumbers ? 'Hide line numbers' : 'Show line numbers'"
class="dev-toggle"
:class="showLineNumbers ? 'active' : 'inactive'"
>
#
</button>
<button
@click="toggleWordWrap"
:title="enableWordWrap ? 'Disable word wrap' : 'Enable word wrap'"
class="dev-toggle"
:class="enableWordWrap ? 'active' : 'inactive'"
>
</button>
<button
@click="toggleActiveLine"
:title="highlightActiveLine ? 'Disable line highlight' : 'Enable line highlight'"
class="dev-toggle"
:class="highlightActiveLine ? 'active' : 'inactive'"
>
</button>
</div>
<div v-if="isFullscreen" class="text-sm text-gray-300 ml-4"> <div v-if="isFullscreen" class="text-sm text-gray-300 ml-4">
<Icon name="material-symbols:info-outline" class="w-4 h-4 inline mr-1" /> <Icon name="material-symbols:info-outline" class="w-4 h-4 inline mr-1" />
Press <kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded">Esc</kbd> or Press <kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded">Esc</kbd> or
@ -408,9 +522,10 @@ watch(
<rs-button <rs-button
@click="formatCurrentCode" @click="formatCurrentCode"
class="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-sm rounded transition-colors" class="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-sm rounded transition-colors"
:title="getFormatButtonTitle()"
> >
<Icon name="vscode-icons:file-type-prettier" class="!w-5 !h-5 mr-2" /> <Icon :name="getFormatButtonIcon()" class="!w-5 !h-5 mr-2" />
Format Code (Shift + Alt + F) {{ getFormatButtonText() }}
</rs-button> </rs-button>
</div> </div>
</div> </div>
@ -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" class="footer flex justify-between items-center gap-2 p-2 bg-[#282C34] text-[#abb2bf] border-t border-gray-600"
> >
<div class="flex items-center gap-4 text-sm"> <div class="flex items-center gap-4 text-sm">
<span v-if="isFullscreen" class="text-gray-300"> <div class="text-gray-300 text-xs">
<Icon name="material-symbols:keyboard" class="w-4 h-4 inline mr-1" /> <Icon name="material-symbols:keyboard" class="w-4 h-4 inline mr-1" />
Shortcuts: Shortcuts:
<kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded mx-1">Ctrl+Shift+F</kbd> Format, <kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded mx-0.5">Ctrl+F</kbd> Search,
<kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded mx-1">F11</kbd> Toggle Fullscreen <kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded mx-0.5">Ctrl+H</kbd> Replace,
</span> <kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded mx-0.5">Shift+Alt+F</kbd> Format
<span v-if="isFullscreen">, <kbd class="px-1 py-0.5 text-xs bg-gray-700 rounded mx-0.5">F11</kbd> Exit Fullscreen</span>
</div> </div>
<div class="flex items-center gap-4"> </div>
<span class="">Lines: {{ numberComma(totalLines) }}</span> <div class="flex items-center gap-4 text-sm">
<span class="">Length: {{ numberComma(totalLength) }}</span> <span>Lines: {{ numberComma(totalLines) }}</span>
<span>Length: {{ numberComma(totalLength) }}</span>
<span class="text-gray-400">Ln {{ cursorPosition.line }}, Col {{ cursorPosition.column }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -536,6 +654,37 @@ button:focus {
outline: 2px solid #3b82f6; outline: 2px solid #3b82f6;
outline-offset: -2px; 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;
}
</style> </style>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>