2024-08-26 00:52:14 -07:00
|
|
|
import {
|
2024-09-21 02:30:50 -07:00
|
|
|
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";
|
|
|
|
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:30:50 -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:30:50 -07:00
|
|
|
type ViewUpdate
|
|
|
|
} from "@codemirror/view";
|
|
|
|
import { indentationMarkers } from '@replit/codemirror-indentation-markers';
|
|
|
|
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,
|
|
|
|
} from "vue";
|
|
|
|
|
|
|
|
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:30:50 -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:30:50 -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:30:50 -07:00
|
|
|
case "javaScript":
|
|
|
|
return [javascript()];
|
|
|
|
default:
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getFullLanguageExtensions(
|
|
|
|
lang: CodeEditorLanguage,
|
|
|
|
): Promise<Extension[]> {
|
|
|
|
switch (lang) {
|
|
|
|
case "javaScript":
|
|
|
|
return [
|
|
|
|
await typescript(readEditorValue()),
|
|
|
|
];
|
|
|
|
case "python":
|
|
|
|
return [python()];
|
|
|
|
case "json":
|
|
|
|
return [json()];
|
|
|
|
case "html":
|
|
|
|
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:30:50 -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:30:50 -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:30:50 -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:30:50 -07:00
|
|
|
languageExtensions.value.of(
|
|
|
|
getInitialLanguageExtensions(toValue(language)),
|
|
|
|
),
|
|
|
|
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),
|
|
|
|
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:30:50 -07:00
|
|
|
highlightSelectionMatches({highlightWordAroundCursor:true, minSelectionLength: 2 }),
|
|
|
|
lineNumbers(),
|
|
|
|
drawSelection(),
|
|
|
|
foldGutter({
|
|
|
|
markerDOM: (open) => {
|
|
|
|
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");
|
|
|
|
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-08-26 00:52:14 -07:00
|
|
|
Prec.highest(
|
2024-09-21 02:30:50 -07:00
|
|
|
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:30:50 -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:30:50 -07:00
|
|
|
document.addEventListener("click", blurOnClickOutside);
|
2024-08-26 00:52:14 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
2024-09-21 02:30:50 -07:00
|
|
|
document.removeEventListener("click", blurOnClickOutside);
|
2024-08-26 00:52:14 -07:00
|
|
|
editor.value?.destroy();
|
|
|
|
});
|
|
|
|
|
2024-09-21 02:30:50 -07:00
|
|
|
function setCursorPosition(pos: number | "end"): void {
|
|
|
|
const finalPos =
|
|
|
|
pos === "end" ? (editor.value?.state.doc.length ?? 0) : pos;
|
|
|
|
|
|
|
|
editor.value?.dispatch({ selection: { head: finalPos, anchor: finalPos } });
|
2024-08-26 00:52:14 -07:00
|
|
|
}
|
|
|
|
|
2024-09-21 02:30:50 -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,
|
|
|
|
head: head === "end" ? (editor.value?.state.doc.length ?? 0) : head,
|
|
|
|
},
|
2024-08-26 00:52:14 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-09-21 02:30:50 -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:30:50 -07:00
|
|
|
case "first":
|
2024-09-16 02:58:57 -07:00
|
|
|
return doc.lineAt(0);
|
2024-09-21 02:30:50 -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:30:50 -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:30:50 -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:30:50 -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,
|
|
|
|
};
|
|
|
|
};
|