replace monaco with codemirror

This commit is contained in:
2026-03-06 23:22:48 +01:00
parent 49a6e5b4a3
commit 5feb2ea228
4 changed files with 341 additions and 243 deletions

View File

@@ -1,5 +1,6 @@
import React, { memo, useCallback, useContext, useEffect, useRef, useState } from 'react'
import Editor, { loader } from '@monaco-editor/react'
import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import CodeMirror from '@uiw/react-codemirror'
import { yaml } from '@codemirror/lang-yaml'
import FlowContext from '../../lib/flowContext'
import {
BaseNode,
@@ -13,9 +14,7 @@ import {
Menubar,
MenubarContent,
MenubarItem,
MenubarLabel,
MenubarMenu,
MenubarSeparator,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
@@ -23,9 +22,6 @@ import {
} from '../ui/menubar'
import { InputHandle, OutputHandle } from './NodeHandles'
// Ensure Monaco can load its web workers under Vite by pointing to a CDN
loader.config({ paths: { vs: 'https://unpkg.com/monaco-editor@latest/min/vs' } })
type Props = {
id: string
data: any
@@ -38,8 +34,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
const setNodes = ctx?.setNodes
const storedYaml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.yaml ?? ''
const editorRef = useRef<any>(null)
const monacoRef = useRef<any>(null)
const editorRef = useRef<unknown>(null)
const incomingEdges = ctx?.edges?.filter((e: any) => e.target === id) ?? []
const incomingIds = incomingEdges.map((e: any) => e.source).sort()
@@ -56,166 +51,69 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
}, [])
const onChange = useCallback(
(val?: string) => {
const v = val ?? ''
setValue(v)
// eslint-disable-next-line no-console
console.debug('ConfigNode:onChange', id, v.substring(0, 60))
(val: string) => {
setValue(val)
if (setNodes) {
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, yaml: v } } : n)))
setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, yaml: val } } : n)))
}
},
[id, setNodes]
)
const handleEditorMount = useCallback((editor: any, monaco: any) => {
editorRef.current = editor
monacoRef.current = monaco
// Prevent React Flow and other global handlers from interfering with typing
try {
editor.onKeyDown((e: any) => {
if (e?.browserEvent) {
e.browserEvent.stopPropagation()
const insertAt = useCallback(
(insertText: string, mode: 'prepend' | 'append' | 'cursor') => {
const ref = editorRef.current as { view: { state: { doc: { length: number; toString(): string }; selection: { main: { from: number } } }; dispatch: (arg: { changes: { from: number; to: number; insert: string } }) => void } } | null
if (ref?.view) {
const view = ref.view
const doc = view.state.doc
const len = doc.length
let from: number
if (mode === 'prepend') {
from = 0
} else if (mode === 'append') {
from = len
} else {
const main = view.state.selection.main
from = main.from
}
})
editor.onMouseDown((e: any) => {
if (e?.event) {
e.event.stopPropagation()
}
})
} catch {
// ignore if Monaco internals change
}
}, [])
view.dispatch({ changes: { from, to: from, insert: insertText } })
const newVal = view.state.doc.toString()
onChange(newVal)
return
}
if (mode === 'prepend') {
onChange(insertText + value)
} else {
onChange(value + insertText)
}
},
[onChange, value]
)
const insertIncludeFromNode = useCallback(
(sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
const ref = `${sourceNode.id}.yaml`
const includeText = `!include ${ref}\n`
const editor = editorRef.current
const monaco = monacoRef.current
try {
if (editor && monaco) {
const model = editor.getModel()
const lineCount = model.getLineCount()
const selection = editor.getSelection()
let range
if (mode === 'prepend') {
range = new monaco.Range(1, 1, 1, 1)
} else if (mode === 'append') {
range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1)
} else if (selection) {
range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.startLineNumber, selection.startColumn)
} else {
range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1)
}
editor.executeEdits('insert-include', [{ range, text: includeText, forceMoveMarkers: true }])
const newVal = editor.getModel().getValue()
onChange(newVal)
return
}
if (mode === 'prepend') {
onChange(includeText + value)
} else {
onChange(value + '\n' + includeText)
}
} catch (err) {
// eslint-disable-next-line no-console
console.error('Insert include failed', err)
}
insertAt(`!include ${ref}\n`, mode)
},
[onChange, value]
[insertAt]
)
const insertVariableReference = useCallback(
(sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
const insertText = `\${${sourceNode.id}}`
const editor = editorRef.current
const monaco = monacoRef.current
try {
if (editor && monaco) {
const model = editor.getModel()
const lineCount = model.getLineCount()
const selection = editor.getSelection()
let range
if (mode === 'prepend') {
range = new monaco.Range(1, 1, 1, 1)
} else if (mode === 'append') {
range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1)
} else if (selection) {
range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.startLineNumber, selection.startColumn)
} else {
range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1)
}
editor.executeEdits('insert-var', [{ range, text: insertText, forceMoveMarkers: true }])
const newVal = editor.getModel().getValue()
onChange(newVal)
return
}
if (mode === 'prepend') {
onChange(insertText + value)
} else {
onChange(value + insertText)
}
} catch (err) {
// eslint-disable-next-line no-console
console.error('Insert variable reference failed', err)
}
insertAt(`\${${sourceNode.id}}`, mode)
},
[onChange, value]
[insertAt]
)
const insertFunctionCall = useCallback(
(sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => {
const insertText = `\${${sourceNode.id}()}` // user can add variable ids inside: ${funcId(var1, var2)}
const editor = editorRef.current
const monaco = monacoRef.current
try {
if (editor && monaco) {
const model = editor.getModel()
const lineCount = model.getLineCount()
const selection = editor.getSelection()
let range
if (mode === 'prepend') {
range = new monaco.Range(1, 1, 1, 1)
} else if (mode === 'append') {
range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1)
} else if (selection) {
range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.startLineNumber, selection.startColumn)
} else {
range = new monaco.Range(lineCount + 1, 1, lineCount + 1, 1)
}
editor.executeEdits('insert-fn', [{ range, text: insertText, forceMoveMarkers: true }])
const newVal = editor.getModel().getValue()
onChange(newVal)
return
}
if (mode === 'prepend') {
onChange(insertText + value)
} else {
onChange(value + insertText)
}
} catch (err) {
// eslint-disable-next-line no-console
console.error('Insert function call failed', err)
}
insertAt(`\${${sourceNode.id}()}`, mode)
},
[onChange, value]
[insertAt]
)
const extensions = useMemo(() => [yaml()], [])
return (
<BaseNode className="w-80">
<BaseNodeHeader className="border-b">
@@ -295,24 +193,16 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
</div>
)}
<div style={{ height: 180 }} className="w-full nodrag nopan">
<Editor
height="100%"
defaultLanguage="yaml"
<div style={{ height: 180 }} className="w-full nodrag nopan overflow-hidden rounded border border-input">
<CodeMirror
// @ts-expect-error ref is { view, state, editor }; package ref type not in our node_modules
ref={editorRef}
value={value}
theme="vs-light"
onMount={handleEditorMount}
height="180px"
extensions={extensions}
onChange={onChange}
options={{
minimap: { enabled: false },
fontSize: 12,
lineHeight: 16,
glyphMargin: false,
renderLineHighlight: 'none',
lineDecorationsWidth: 2,
padding: { top: 0, bottom: 4 },
lineNumbers: 'on',
}}
basicSetup={{ lineNumbers: true, foldGutter: false }}
className="text-sm [&_.cm-editor]:outline-none [&_.cm-editor]:cursor-text [&_.cm-gutters]:border-0"
/>
</div>
</BaseNodeContent>