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 { 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(
}"
/>
</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">
<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
@ -408,9 +522,10 @@ watch(
<rs-button
@click="formatCurrentCode"
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" />
Format Code (Shift + Alt + F)
<Icon :name="getFormatButtonIcon()" class="!w-5 !h-5 mr-2" />
{{ getFormatButtonText() }}
</rs-button>
</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"
>
<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" />
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-1">F11</kbd> Toggle Fullscreen
</span>
<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-0.5">Ctrl+H</kbd> Replace,
<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 class="flex items-center gap-4">
<span class="">Lines: {{ numberComma(totalLines) }}</span>
<span class="">Length: {{ numberComma(totalLength) }}</span>
</div>
<div class="flex items-center gap-4 text-sm">
<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>
@ -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;
}
</style>
<style lang="scss" scoped></style>