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:
parent
4b23da5239
commit
8d6184fd8b
@ -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>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4 text-sm">
|
||||||
<span class="">Lines: {{ numberComma(totalLines) }}</span>
|
<span>Lines: {{ numberComma(totalLines) }}</span>
|
||||||
<span class="">Length: {{ numberComma(totalLength) }}</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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user