refactor: replace dual-layer textarea with Monaco Editor for script body
The transparent-textarea-over-highlighted-overlay approach had persistent cursor alignment issues. Replace with Monaco Editor (already used in tree editor Code Mode) using built-in PowerShell language support and our existing dark theme. Gives proper syntax highlighting, cursor positioning, line numbers, and editing for free. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import { useRef, useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { PowerShellHighlighter } from '@/components/scripts/PowerShellHighlighter'
|
import Editor, { type BeforeMount } from '@monaco-editor/react'
|
||||||
|
import { resolutionFlowTheme, THEME_ID } from '@/components/tree-editor/code-mode/resolutionFlowTheme'
|
||||||
|
import { Spinner } from '@/components/common/Spinner'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string
|
value: string
|
||||||
@@ -8,50 +10,43 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ScriptBodyEditor({ value, onChange, disabled }: Props) {
|
export function ScriptBodyEditor({ value, onChange, disabled }: Props) {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
|
||||||
const overlayRef = useRef<HTMLDivElement>(null)
|
// Register our dark theme if not already defined
|
||||||
|
monaco.editor.defineTheme(THEME_ID, resolutionFlowTheme)
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
if (textareaRef.current && overlayRef.current) {
|
|
||||||
overlayRef.current.scrollTop = textareaRef.current.scrollTop
|
|
||||||
overlayRef.current.scrollLeft = textareaRef.current.scrollLeft
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleTab = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
e.preventDefault()
|
|
||||||
const ta = e.currentTarget
|
|
||||||
const start = ta.selectionStart
|
|
||||||
const end = ta.selectionEnd
|
|
||||||
const newValue = value.substring(0, start) + ' ' + value.substring(end)
|
|
||||||
onChange(newValue)
|
|
||||||
// Restore cursor position after React re-render
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
ta.selectionStart = ta.selectionEnd = start + 4
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [value, onChange])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative rounded-xl border border-border overflow-hidden">
|
<div className="rounded-xl border border-border overflow-hidden">
|
||||||
{/* Highlighted overlay (read-only visual layer) — scroll synced to textarea */}
|
<Editor
|
||||||
<div ref={overlayRef} className="absolute inset-0 pointer-events-none overflow-hidden p-4">
|
height="300px"
|
||||||
<PowerShellHighlighter script={value || ' '} className="font-label text-sm leading-[21px] whitespace-pre m-0 p-0 border-0" />
|
language="powershell"
|
||||||
</div>
|
theme={THEME_ID}
|
||||||
|
|
||||||
{/* Editable textarea (transparent text, visible caret) */}
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.value)}
|
onChange={v => onChange(v ?? '')}
|
||||||
onScroll={handleScroll}
|
beforeMount={handleBeforeMount}
|
||||||
onKeyDown={handleTab}
|
loading={
|
||||||
disabled={disabled}
|
<div className="flex h-[300px] items-center justify-center bg-card">
|
||||||
wrap="off"
|
<Spinner size="sm" className="h-6 w-6 border-t-foreground" />
|
||||||
spellCheck={false}
|
</div>
|
||||||
className="relative z-10 w-full min-h-[300px] resize-y font-label text-sm leading-[21px] bg-transparent text-transparent caret-foreground p-4 whitespace-pre overflow-auto focus:outline-none focus:ring-1 focus:ring-[rgba(6,182,212,0.2)] disabled:cursor-not-allowed disabled:opacity-50"
|
}
|
||||||
placeholder="# Enter your PowerShell script here… # Use {{ param_name }} for parameter placeholders"
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
lineNumbers: 'on',
|
||||||
|
wordWrap: 'on',
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
renderLineHighlight: 'line',
|
||||||
|
tabSize: 4,
|
||||||
|
insertSpaces: true,
|
||||||
|
automaticLayout: true,
|
||||||
|
readOnly: disabled,
|
||||||
|
padding: { top: 12, bottom: 12 },
|
||||||
|
scrollbar: {
|
||||||
|
verticalScrollbarSize: 8,
|
||||||
|
horizontalScrollbarSize: 8,
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user