2024-08-26 00:52:14 -07:00
|
|
|
import {
|
|
|
|
onBeforeUnmount,
|
|
|
|
onMounted,
|
|
|
|
ref,
|
|
|
|
toRef,
|
|
|
|
toValue,
|
|
|
|
watch,
|
|
|
|
watchEffect,
|
|
|
|
type MaybeRefOrGetter,
|
|
|
|
type Ref,
|
|
|
|
} from 'vue';
|
|
|
|
import { closeCursorInfoBox } from '@/plugins/codemirror/tooltips/InfoBoxTooltip';
|
|
|
|
import { closeCompletion, completionStatus } from '@codemirror/autocomplete';
|
|
|
|
import {
|
|
|
|
Compartment,
|
|
|
|
EditorSelection,
|
|
|
|
EditorState,
|
|
|
|
Prec,
|
|
|
|
type Extension,
|
|
|
|
type SelectionRange,
|
|
|
|
} from '@codemirror/state';
|
|
|
|
import {
|
|
|
|
dropCursor,
|
|
|
|
EditorView,
|
|
|
|
highlightActiveLine,
|
|
|
|
highlightActiveLineGutter,
|
|
|
|
highlightSpecialChars,
|
|
|
|
keymap,
|
|
|
|
lineNumbers,
|
|
|
|
type ViewUpdate,
|
|
|
|
} from '@codemirror/view';
|
|
|
|
import { javascript } from '@codemirror/lang-javascript';
|
|
|
|
import { python } from '@codemirror/lang-python';
|
|
|
|
import { json } from '@codemirror/lang-json';
|
|
|
|
import { html } from 'codemirror-lang-html-n8n';
|
|
|
|
import { lintGutter } from '@codemirror/lint';
|
|
|
|
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
|
|
|
|
import { history, toggleComment, deleteCharBackward } from '@codemirror/commands';
|
|
|
|
import {
|
|
|
|
autocompleteKeyMap,
|
|
|
|
enterKeyMap,
|
|
|
|
historyKeyMap,
|
|
|
|
tabKeyMap,
|
|
|
|
} from '../plugins/codemirror/keymap';
|
|
|
|
import {
|
|
|
|
codeEditorSyntaxHighlighting,
|
|
|
|
codeEditorTheme,
|
|
|
|
htmlEditorHighlighting,
|
|
|
|
} from '../components/CodeNodeEditor/theme';
|
|
|
|
|
|
|
|
export type CodeEditorLanguage = 'json' | 'html' | 'javaScript' | 'python';
|
|
|
|
|
|
|
|
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 = () => {},
|
|
|
|
onViewUpdate = () => {},
|
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>;
|
|
|
|
theme?: MaybeRefOrGetter<{ maxHeight?: string; minHeight?: string; rows?: number }>;
|
2024-09-16 02:58:57 -07:00
|
|
|
onChange?: (viewUpdate: ViewUpdate) => void;
|
|
|
|
onViewUpdate?: (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-08-26 00:52:14 -07:00
|
|
|
const selection = ref<SelectionRange>(EditorSelection.cursor(0)) as Ref<SelectionRange>;
|
|
|
|
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());
|
|
|
|
const autocompleteStatus = ref<'pending' | 'active' | null>(null);
|
|
|
|
const dragging = ref(false);
|
|
|
|
|
|
|
|
const EXTENSIONS_BY_LANGUAGE: Record<CodeEditorLanguage, Extension[]> = {
|
|
|
|
javaScript: [javascript(), codeEditorSyntaxHighlighting],
|
|
|
|
python: [python(), codeEditorSyntaxHighlighting],
|
|
|
|
json: [json(), codeEditorSyntaxHighlighting],
|
|
|
|
html: [html(), htmlEditorHighlighting],
|
|
|
|
};
|
|
|
|
|
|
|
|
function readEditorValue(): string {
|
|
|
|
return editor.value?.state.doc.toString() ?? '';
|
|
|
|
}
|
|
|
|
|
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-16 02:58:57 -07:00
|
|
|
function onEditorUpdate(update: ViewUpdate) {
|
|
|
|
autocompleteStatus.value = completionStatus(update.view.state);
|
|
|
|
updateSelection(update);
|
|
|
|
onViewUpdate(update);
|
|
|
|
|
|
|
|
if (update.docChanged) {
|
|
|
|
hasChanges.value = true;
|
|
|
|
onChange(update);
|
|
|
|
}
|
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) {
|
|
|
|
if (event.target && !dragging.value && !editor.value?.dom.contains(event.target as Node)) {
|
|
|
|
blur();
|
|
|
|
}
|
|
|
|
dragging.value = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
watch(toRef(editorRef), () => {
|
|
|
|
const parent = toValue(editorRef);
|
|
|
|
|
|
|
|
if (!parent) return;
|
|
|
|
|
2024-09-16 02:58:57 -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-16 02:58:57 -07:00
|
|
|
readOnlyExtensions.value.of([]),
|
2024-08-26 00:52:14 -07:00
|
|
|
telemetryExtensions.value.of([]),
|
|
|
|
languageExtensions.value.of([]),
|
|
|
|
themeExtensions.value.of([]),
|
|
|
|
EditorView.updateListener.of(onEditorUpdate),
|
|
|
|
EditorView.focusChangeEffect.of((_, newHasFocus) => {
|
|
|
|
hasFocus.value = newHasFocus;
|
|
|
|
selection.value = state.selection.ranges[0];
|
|
|
|
if (!newHasFocus) {
|
|
|
|
autocompleteStatus.value = null;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}),
|
|
|
|
EditorView.contentAttributes.of({ 'data-gramm': 'false' }), // disable grammarly
|
|
|
|
EditorView.domEventHandlers({
|
|
|
|
mousedown: () => {
|
|
|
|
dragging.value = true;
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
history(),
|
|
|
|
lintGutter(),
|
|
|
|
foldGutter(),
|
|
|
|
dropCursor(),
|
|
|
|
indentOnInput(),
|
|
|
|
bracketMatching(),
|
|
|
|
highlightActiveLine(),
|
|
|
|
highlightActiveLineGutter(),
|
|
|
|
Prec.highest(
|
|
|
|
keymap.of([
|
|
|
|
...tabKeyMap(),
|
|
|
|
...enterKeyMap,
|
|
|
|
...autocompleteKeyMap,
|
|
|
|
...historyKeyMap,
|
|
|
|
{ key: 'Mod-/', run: toggleComment },
|
|
|
|
{ key: 'Backspace', run: deleteCharBackward, shift: deleteCharBackward },
|
|
|
|
]),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.destroy();
|
|
|
|
}
|
|
|
|
editor.value = new EditorView({ parent, state, scrollTo: EditorView.scrollIntoView(0) });
|
|
|
|
});
|
|
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: customExtensions.value.reconfigure(toValue(extensions)),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: languageExtensions.value.reconfigure(
|
|
|
|
toValue(EXTENSIONS_BY_LANGUAGE[toValue(language)]),
|
|
|
|
),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: readOnlyExtensions.value.reconfigure([
|
|
|
|
EditorState.readOnly.of(toValue(isReadOnly)),
|
2024-09-16 02:58:57 -07:00
|
|
|
EditorView.editable.of(!toValue(isReadOnly)),
|
2024-08-26 00:52:14 -07:00
|
|
|
lineNumbers(),
|
|
|
|
EditorView.lineWrapping,
|
|
|
|
highlightSpecialChars(),
|
|
|
|
]),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: themeExtensions.value.reconfigure(codeEditorTheme(toValue(theme))),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
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(() => {
|
|
|
|
document.addEventListener('click', blurOnClickOutside);
|
|
|
|
});
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
document.removeEventListener('click', blurOnClickOutside);
|
|
|
|
editor.value?.destroy();
|
|
|
|
});
|
|
|
|
|
|
|
|
function setCursorPosition(pos: number | 'end'): void {
|
|
|
|
if (pos === 'end') {
|
|
|
|
pos = editor.value?.state.doc.length ?? 0;
|
|
|
|
}
|
|
|
|
editor.value?.dispatch({ selection: { head: pos, anchor: pos } });
|
|
|
|
}
|
|
|
|
|
|
|
|
function select(anchor: number, head: number | 'end' = 'end'): void {
|
|
|
|
editor.value?.dispatch({
|
|
|
|
selection: { anchor, head: head === 'end' ? editor.value?.state.doc.length ?? 0 : head },
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-09-16 02:58:57 -07:00
|
|
|
function getLine(lineNumber: number | 'last' | 'first') {
|
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
|
|
const { doc } = editor.value.state;
|
|
|
|
switch (lineNumber) {
|
|
|
|
case 'first':
|
|
|
|
return doc.lineAt(0);
|
|
|
|
case 'last':
|
|
|
|
return doc.lineAt(doc.length - 1);
|
|
|
|
default:
|
|
|
|
return doc.line(lineNumber);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function selectLine(lineNumber: number | 'last' | 'first'): void {
|
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
|
|
const line = getLine(lineNumber);
|
|
|
|
|
|
|
|
if (!line) return;
|
|
|
|
|
|
|
|
editor.value.dispatch({ selection: EditorSelection.range(line.from, line.to) });
|
|
|
|
}
|
|
|
|
|
|
|
|
function highlightLine(lineNumber: number | 'last' | 'first'): void {
|
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
|
|
const line = getLine(lineNumber);
|
|
|
|
|
|
|
|
if (!line) return;
|
|
|
|
|
|
|
|
editor.value.dispatch({ selection: EditorSelection.cursor(line.from) });
|
|
|
|
}
|
|
|
|
|
2024-08-26 00:52:14 -07:00
|
|
|
const selectAll = () => select(0, 'end');
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
};
|