2024-09-21 02:34:32 -07:00
|
|
|
import { codeEditorTheme } from '@/components/CodeNodeEditor/theme';
|
|
|
|
import { editorKeymap } from '@/plugins/codemirror/keymap';
|
|
|
|
import { typescript } from '@/plugins/codemirror/lsp/typescript';
|
|
|
|
import { closeCursorInfoBox } from '@/plugins/codemirror/tooltips/InfoBoxTooltip';
|
|
|
|
import { closeCompletion, completionStatus } from '@codemirror/autocomplete';
|
|
|
|
import { history } from '@codemirror/commands';
|
|
|
|
import { javascript } from '@codemirror/lang-javascript';
|
|
|
|
import { json } from '@codemirror/lang-json';
|
|
|
|
import { python } from '@codemirror/lang-python';
|
|
|
|
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
|
2024-09-21 02:30:50 -07:00
|
|
|
import { highlightSelectionMatches } from '@codemirror/search';
|
2024-08-26 00:52:14 -07:00
|
|
|
import {
|
|
|
|
Compartment,
|
|
|
|
EditorSelection,
|
|
|
|
EditorState,
|
|
|
|
Prec,
|
|
|
|
type Extension,
|
|
|
|
type SelectionRange,
|
2024-09-21 02:34:32 -07:00
|
|
|
} from '@codemirror/state';
|
2024-08-26 00:52:14 -07:00
|
|
|
import {
|
2024-09-21 02:30:50 -07:00
|
|
|
drawSelection,
|
2024-08-26 00:52:14 -07:00
|
|
|
dropCursor,
|
|
|
|
EditorView,
|
|
|
|
highlightActiveLineGutter,
|
|
|
|
highlightSpecialChars,
|
|
|
|
keymap,
|
|
|
|
lineNumbers,
|
2024-09-21 02:34:32 -07:00
|
|
|
type ViewUpdate,
|
|
|
|
} from '@codemirror/view';
|
2024-09-21 02:30:50 -07:00
|
|
|
import { indentationMarkers } from '@replit/codemirror-indentation-markers';
|
2024-09-21 02:34:32 -07:00
|
|
|
import { html } from 'codemirror-lang-html-n8n';
|
|
|
|
import { debounce } from 'lodash-es';
|
2024-08-26 00:52:14 -07:00
|
|
|
import {
|
2024-09-21 02:30:50 -07:00
|
|
|
onBeforeUnmount,
|
|
|
|
onMounted,
|
|
|
|
ref,
|
|
|
|
toRef,
|
|
|
|
toValue,
|
|
|
|
watch,
|
|
|
|
type MaybeRefOrGetter,
|
|
|
|
type Ref,
|
2024-09-21 02:34:32 -07:00
|
|
|
} from 'vue';
|
2024-08-26 00:52:14 -07:00
|
|
|
|
2024-09-21 02:34:32 -07:00
|
|
|
export type CodeEditorLanguage = 'json' | 'html' | 'javaScript' | 'python';
|
2024-08-26 00:52:14 -07:00
|
|
|
|
|
|
|
export const useCodeEditor = ({
|
|
|
|
editorRef,
|
|
|
|
editorValue,
|
|
|
|
language,
|
2024-09-16 02:58:57 -07:00
|
|
|
placeholder,
|
2024-08-26 00:52:14 -07:00
|
|
|
extensions = [],
|
|
|
|
isReadOnly = false,
|
|
|
|
theme = {},
|
2024-09-16 02:58:57 -07:00
|
|
|
onChange = () => {},
|
2024-08-26 00:52:14 -07:00
|
|
|
}: {
|
|
|
|
editorRef: MaybeRefOrGetter<HTMLElement | undefined>;
|
|
|
|
language: MaybeRefOrGetter<CodeEditorLanguage>;
|
|
|
|
editorValue?: MaybeRefOrGetter<string>;
|
2024-09-16 02:58:57 -07:00
|
|
|
placeholder?: MaybeRefOrGetter<string>;
|
2024-08-26 00:52:14 -07:00
|
|
|
extensions?: MaybeRefOrGetter<Extension[]>;
|
|
|
|
isReadOnly?: MaybeRefOrGetter<boolean>;
|
2024-09-21 02:30:50 -07:00
|
|
|
theme?: MaybeRefOrGetter<{
|
|
|
|
maxHeight?: string;
|
|
|
|
minHeight?: string;
|
|
|
|
rows?: number;
|
|
|
|
}>;
|
2024-09-16 02:58:57 -07:00
|
|
|
onChange?: (viewUpdate: ViewUpdate) => void;
|
2024-08-26 00:52:14 -07:00
|
|
|
}) => {
|
|
|
|
const editor = ref<EditorView>();
|
|
|
|
const hasFocus = ref(false);
|
2024-09-16 02:58:57 -07:00
|
|
|
const hasChanges = ref(false);
|
2024-09-21 02:34:32 -07:00
|
|
|
const selection = ref<SelectionRange>(EditorSelection.cursor(0)) as Ref<SelectionRange>;
|
2024-08-26 00:52:14 -07:00
|
|
|
const customExtensions = ref<Compartment>(new Compartment());
|
|
|
|
const readOnlyExtensions = ref<Compartment>(new Compartment());
|
|
|
|
const telemetryExtensions = ref<Compartment>(new Compartment());
|
|
|
|
const languageExtensions = ref<Compartment>(new Compartment());
|
|
|
|
const themeExtensions = ref<Compartment>(new Compartment());
|
2024-09-21 02:34:32 -07:00
|
|
|
const autocompleteStatus = ref<'pending' | 'active' | null>(null);
|
2024-08-26 00:52:14 -07:00
|
|
|
const dragging = ref(false);
|
|
|
|
|
2024-09-21 02:30:50 -07:00
|
|
|
function getInitialLanguageExtensions(lang: CodeEditorLanguage): Extension[] {
|
2024-09-17 03:13:54 -07:00
|
|
|
switch (lang) {
|
2024-09-21 02:34:32 -07:00
|
|
|
case 'javaScript':
|
2024-09-21 02:30:50 -07:00
|
|
|
return [javascript()];
|
|
|
|
default:
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-21 02:34:32 -07:00
|
|
|
async function getFullLanguageExtensions(lang: CodeEditorLanguage): Promise<Extension[]> {
|
2024-09-21 02:30:50 -07:00
|
|
|
switch (lang) {
|
2024-09-21 02:34:32 -07:00
|
|
|
case 'javaScript':
|
|
|
|
return [await typescript(readEditorValue())];
|
|
|
|
case 'python':
|
2024-09-21 02:30:50 -07:00
|
|
|
return [python()];
|
2024-09-21 02:34:32 -07:00
|
|
|
case 'json':
|
2024-09-21 02:30:50 -07:00
|
|
|
return [json()];
|
2024-09-21 02:34:32 -07:00
|
|
|
case 'html':
|
2024-09-21 02:30:50 -07:00
|
|
|
return [html()];
|
2024-09-17 03:13:54 -07:00
|
|
|
}
|
|
|
|
}
|
2024-08-26 00:52:14 -07:00
|
|
|
|
|
|
|
function readEditorValue(): string {
|
2024-09-21 02:34:32 -07:00
|
|
|
return editor.value?.state.doc.toString() ?? '';
|
2024-08-26 00:52:14 -07:00
|
|
|
}
|
|
|
|
|
2024-09-16 02:58:57 -07:00
|
|
|
function updateSelection(update: ViewUpdate) {
|
2024-08-26 00:52:14 -07:00
|
|
|
const currentSelection = selection.value;
|
2024-09-16 02:58:57 -07:00
|
|
|
const newSelection = update.state.selection.ranges[0];
|
2024-08-26 00:52:14 -07:00
|
|
|
|
|
|
|
if (!currentSelection?.eq(newSelection)) {
|
|
|
|
selection.value = newSelection;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-17 03:13:54 -07:00
|
|
|
const emitChanges = debounce((update: ViewUpdate) => {
|
|
|
|
onChange(update);
|
|
|
|
}, 300);
|
|
|
|
|
2024-09-16 02:58:57 -07:00
|
|
|
function onEditorUpdate(update: ViewUpdate) {
|
|
|
|
autocompleteStatus.value = completionStatus(update.view.state);
|
|
|
|
updateSelection(update);
|
|
|
|
|
|
|
|
if (update.docChanged) {
|
|
|
|
hasChanges.value = true;
|
2024-09-17 03:13:54 -07:00
|
|
|
emitChanges(update);
|
2024-09-16 02:58:57 -07:00
|
|
|
}
|
2024-08-26 00:52:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function blur() {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.contentDOM.blur();
|
|
|
|
closeCompletion(editor.value);
|
|
|
|
closeCursorInfoBox(editor.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function blurOnClickOutside(event: MouseEvent) {
|
2024-09-21 02:34:32 -07:00
|
|
|
if (event.target && !dragging.value && !editor.value?.dom.contains(event.target as Node)) {
|
2024-08-26 00:52:14 -07:00
|
|
|
blur();
|
|
|
|
}
|
|
|
|
dragging.value = false;
|
|
|
|
}
|
|
|
|
|
2024-09-21 02:30:50 -07:00
|
|
|
async function setLanguageExtensions() {
|
|
|
|
if (!editor.value) return;
|
|
|
|
const initialExtensions = getInitialLanguageExtensions(toValue(language));
|
|
|
|
if (initialExtensions.length > 0) {
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: languageExtensions.value.reconfigure(initialExtensions),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: languageExtensions.value.reconfigure(
|
|
|
|
await getFullLanguageExtensions(toValue(language)),
|
|
|
|
),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function getReadOnlyExtensions() {
|
|
|
|
return [
|
|
|
|
EditorState.readOnly.of(toValue(isReadOnly)),
|
|
|
|
EditorView.editable.of(!toValue(isReadOnly)),
|
|
|
|
highlightSpecialChars(),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
function setReadOnlyExtensions() {
|
|
|
|
if (!editor.value) return;
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: readOnlyExtensions.value.reconfigure([getReadOnlyExtensions()]),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
watch(toRef(editorRef), async () => {
|
2024-08-26 00:52:14 -07:00
|
|
|
const parent = toValue(editorRef);
|
|
|
|
|
|
|
|
if (!parent) return;
|
|
|
|
|
2024-09-21 02:34:32 -07:00
|
|
|
const initialValue = toValue(editorValue) ? toValue(editorValue) : toValue(placeholder);
|
2024-08-26 00:52:14 -07:00
|
|
|
const state = EditorState.create({
|
2024-09-16 02:58:57 -07:00
|
|
|
doc: initialValue,
|
2024-08-26 00:52:14 -07:00
|
|
|
extensions: [
|
|
|
|
customExtensions.value.of(toValue(extensions)),
|
2024-09-21 02:30:50 -07:00
|
|
|
readOnlyExtensions.value.of(getReadOnlyExtensions()),
|
2024-08-26 00:52:14 -07:00
|
|
|
telemetryExtensions.value.of([]),
|
2024-09-21 02:34:32 -07:00
|
|
|
languageExtensions.value.of(getInitialLanguageExtensions(toValue(language))),
|
2024-09-21 02:30:50 -07:00
|
|
|
themeExtensions.value.of(codeEditorTheme(toValue(theme))),
|
2024-08-26 00:52:14 -07:00
|
|
|
EditorView.updateListener.of(onEditorUpdate),
|
|
|
|
EditorView.focusChangeEffect.of((_, newHasFocus) => {
|
|
|
|
hasFocus.value = newHasFocus;
|
|
|
|
selection.value = state.selection.ranges[0];
|
|
|
|
if (!newHasFocus) {
|
|
|
|
autocompleteStatus.value = null;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}),
|
2024-09-21 02:30:50 -07:00
|
|
|
EditorState.allowMultipleSelections.of(true),
|
2024-09-21 02:34:32 -07:00
|
|
|
EditorView.contentAttributes.of({ 'data-gramm': 'false' }), // disable grammarly
|
2024-08-26 00:52:14 -07:00
|
|
|
EditorView.domEventHandlers({
|
|
|
|
mousedown: () => {
|
|
|
|
dragging.value = true;
|
|
|
|
},
|
|
|
|
}),
|
2024-09-21 02:34:32 -07:00
|
|
|
highlightSelectionMatches({ highlightWordAroundCursor: true, minSelectionLength: 2 }),
|
2024-09-21 02:30:50 -07:00
|
|
|
lineNumbers(),
|
|
|
|
drawSelection(),
|
|
|
|
foldGutter({
|
|
|
|
markerDOM: (open) => {
|
2024-09-21 02:34:32 -07:00
|
|
|
const svgNS = 'http://www.w3.org/2000/svg';
|
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
wrapper.classList.add('cm-fold-marker');
|
|
|
|
const svgElement = document.createElementNS(svgNS, 'svg');
|
|
|
|
svgElement.setAttribute('viewBox', '0 0 10 10');
|
|
|
|
svgElement.setAttribute('width', '10');
|
|
|
|
svgElement.setAttribute('height', '10');
|
|
|
|
const pathElement = document.createElementNS(svgNS, 'path');
|
|
|
|
const d = open ? 'M1 3 L5 7 L9 3' : 'M3 1 L7 5 L3 9'; // Chevron paths
|
|
|
|
pathElement.setAttribute('d', d);
|
|
|
|
pathElement.setAttribute('fill', 'none');
|
|
|
|
pathElement.setAttribute('stroke', 'currentColor');
|
|
|
|
pathElement.setAttribute('stroke-width', '1.5');
|
|
|
|
pathElement.setAttribute('stroke-linecap', 'round');
|
2024-09-21 02:30:50 -07:00
|
|
|
svgElement.appendChild(pathElement);
|
|
|
|
wrapper.appendChild(svgElement);
|
|
|
|
return wrapper;
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
EditorView.lineWrapping,
|
2024-08-26 00:52:14 -07:00
|
|
|
history(),
|
|
|
|
dropCursor(),
|
|
|
|
indentOnInput(),
|
|
|
|
bracketMatching(),
|
|
|
|
highlightActiveLineGutter(),
|
2024-09-21 02:30:50 -07:00
|
|
|
indentationMarkers({
|
|
|
|
highlightActiveBlock: true,
|
|
|
|
markerType: 'fullScope',
|
|
|
|
colors: {
|
|
|
|
activeDark: 'var(--color-code-indentation-marker-active)',
|
|
|
|
activeLight: 'var(--color-code-indentation-marker-active)',
|
|
|
|
dark: 'var(--color-code-indentation-marker)',
|
|
|
|
light: 'var(--color-code-indentation-marker)',
|
|
|
|
},
|
|
|
|
}),
|
2024-09-21 02:34:32 -07:00
|
|
|
Prec.highest(keymap.of(editorKeymap)),
|
2024-08-26 00:52:14 -07:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.destroy();
|
|
|
|
}
|
2024-09-21 02:30:50 -07:00
|
|
|
editor.value = new EditorView({
|
|
|
|
parent,
|
|
|
|
state,
|
|
|
|
scrollTo: EditorView.scrollIntoView(0),
|
|
|
|
});
|
|
|
|
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: languageExtensions.value.reconfigure(
|
|
|
|
await getFullLanguageExtensions(toValue(language)),
|
|
|
|
),
|
|
|
|
});
|
2024-08-26 00:52:14 -07:00
|
|
|
});
|
|
|
|
|
2024-09-21 02:30:50 -07:00
|
|
|
watch(extensions, () => {
|
2024-08-26 00:52:14 -07:00
|
|
|
if (editor.value) {
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: customExtensions.value.reconfigure(toValue(extensions)),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-09-21 02:30:50 -07:00
|
|
|
watch(toRef(language), setLanguageExtensions);
|
2024-08-26 00:52:14 -07:00
|
|
|
|
2024-09-21 02:30:50 -07:00
|
|
|
watch(toRef(isReadOnly), setReadOnlyExtensions);
|
2024-08-26 00:52:14 -07:00
|
|
|
|
2024-09-21 02:30:50 -07:00
|
|
|
watch(toRef(theme), () => {
|
2024-08-26 00:52:14 -07:00
|
|
|
if (editor.value) {
|
|
|
|
editor.value.dispatch({
|
2024-09-21 02:34:32 -07:00
|
|
|
effects: themeExtensions.value.reconfigure(codeEditorTheme(toValue(theme))),
|
2024-08-26 00:52:14 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-09-16 02:58:57 -07:00
|
|
|
watch(toRef(editorValue), () => {
|
2024-08-26 00:52:14 -07:00
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
|
|
const newValue = toValue(editorValue);
|
|
|
|
const currentValue = readEditorValue();
|
|
|
|
if (newValue === undefined || newValue === currentValue) return;
|
|
|
|
|
|
|
|
editor.value.dispatch({
|
|
|
|
changes: { from: 0, to: currentValue.length, insert: newValue },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
onMounted(() => {
|
2024-09-21 02:34:32 -07:00
|
|
|
document.addEventListener('click', blurOnClickOutside);
|
2024-08-26 00:52:14 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
2024-09-21 02:34:32 -07:00
|
|
|
document.removeEventListener('click', blurOnClickOutside);
|
2024-08-26 00:52:14 -07:00
|
|
|
editor.value?.destroy();
|
|
|
|
});
|
|
|
|
|
2024-09-21 02:34:32 -07:00
|
|
|
function setCursorPosition(pos: number | 'end'): void {
|
|
|
|
const finalPos = pos === 'end' ? (editor.value?.state.doc.length ?? 0) : pos;
|
2024-09-21 02:30:50 -07:00
|
|
|
|
|
|
|
editor.value?.dispatch({ selection: { head: finalPos, anchor: finalPos } });
|
2024-08-26 00:52:14 -07:00
|
|
|
}
|
|
|
|
|
2024-09-21 02:34:32 -07:00
|
|
|
function select(anchor: number, head: number | 'end' = 'end'): void {
|
2024-08-26 00:52:14 -07:00
|
|
|
editor.value?.dispatch({
|
2024-09-21 02:30:50 -07:00
|
|
|
selection: {
|
|
|
|
anchor,
|
2024-09-21 02:34:32 -07:00
|
|
|
head: head === 'end' ? (editor.value?.state.doc.length ?? 0) : head,
|
2024-09-21 02:30:50 -07:00
|
|
|
},
|
2024-08-26 00:52:14 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-09-21 02:34:32 -07:00
|
|
|
function getLine(lineNumber: number | 'last' | 'first') {
|
2024-09-16 02:58:57 -07:00
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
|
|
const { doc } = editor.value.state;
|
|
|
|
switch (lineNumber) {
|
2024-09-21 02:34:32 -07:00
|
|
|
case 'first':
|
2024-09-16 02:58:57 -07:00
|
|
|
return doc.lineAt(0);
|
2024-09-21 02:34:32 -07:00
|
|
|
case 'last':
|
2024-09-16 02:58:57 -07:00
|
|
|
return doc.lineAt(doc.length - 1);
|
|
|
|
default:
|
|
|
|
return doc.line(lineNumber);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-21 02:34:32 -07:00
|
|
|
function selectLine(lineNumber: number | 'last' | 'first'): void {
|
2024-09-16 02:58:57 -07:00
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
|
|
const line = getLine(lineNumber);
|
|
|
|
|
|
|
|
if (!line) return;
|
|
|
|
|
2024-09-21 02:30:50 -07:00
|
|
|
editor.value.dispatch({
|
|
|
|
selection: EditorSelection.range(line.from, line.to),
|
|
|
|
});
|
2024-09-16 02:58:57 -07:00
|
|
|
}
|
|
|
|
|
2024-09-21 02:34:32 -07:00
|
|
|
function highlightLine(lineNumber: number | 'last' | 'first'): void {
|
2024-09-16 02:58:57 -07:00
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
|
|
const line = getLine(lineNumber);
|
|
|
|
|
|
|
|
if (!line) return;
|
|
|
|
|
|
|
|
editor.value.dispatch({ selection: EditorSelection.cursor(line.from) });
|
|
|
|
}
|
|
|
|
|
2024-09-21 02:34:32 -07:00
|
|
|
const selectAll = () => select(0, 'end');
|
2024-08-26 00:52:14 -07:00
|
|
|
|
|
|
|
function focus(): void {
|
|
|
|
if (hasFocus.value) return;
|
|
|
|
editor.value?.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
editor,
|
|
|
|
hasFocus,
|
2024-09-16 02:58:57 -07:00
|
|
|
hasChanges,
|
2024-08-26 00:52:14 -07:00
|
|
|
selection,
|
|
|
|
readEditorValue,
|
|
|
|
setCursorPosition,
|
|
|
|
select,
|
2024-09-16 02:58:57 -07:00
|
|
|
selectLine,
|
2024-08-26 00:52:14 -07:00
|
|
|
selectAll,
|
2024-09-16 02:58:57 -07:00
|
|
|
highlightLine,
|
2024-08-26 00:52:14 -07:00
|
|
|
focus,
|
|
|
|
blur,
|
|
|
|
};
|
|
|
|
};
|