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 { PowerShellHighlighter } from '@/components/scripts/PowerShellHighlighter'
|
||||
import { useCallback } from 'react'
|
||||
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 {
|
||||
value: string
|
||||
@@ -8,50 +10,43 @@ interface Props {
|
||||
}
|
||||
|
||||
export function ScriptBodyEditor({ value, onChange, disabled }: Props) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (textareaRef.current && overlayRef.current) {
|
||||
overlayRef.current.scrollTop = textareaRef.current.scrollTop
|
||||
overlayRef.current.scrollLeft = textareaRef.current.scrollLeft
|
||||
}
|
||||
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
|
||||
// Register our dark theme if not already defined
|
||||
monaco.editor.defineTheme(THEME_ID, resolutionFlowTheme)
|
||||
}, [])
|
||||
|
||||
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 (
|
||||
<div className="relative rounded-xl border border-border overflow-hidden">
|
||||
{/* Highlighted overlay (read-only visual layer) — scroll synced to textarea */}
|
||||
<div ref={overlayRef} className="absolute inset-0 pointer-events-none overflow-hidden p-4">
|
||||
<PowerShellHighlighter script={value || ' '} className="font-label text-sm leading-[21px] whitespace-pre m-0 p-0 border-0" />
|
||||
</div>
|
||||
|
||||
{/* Editable textarea (transparent text, visible caret) */}
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
<div className="rounded-xl border border-border overflow-hidden">
|
||||
<Editor
|
||||
height="300px"
|
||||
language="powershell"
|
||||
theme={THEME_ID}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onScroll={handleScroll}
|
||||
onKeyDown={handleTab}
|
||||
disabled={disabled}
|
||||
wrap="off"
|
||||
spellCheck={false}
|
||||
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"
|
||||
onChange={v => onChange(v ?? '')}
|
||||
beforeMount={handleBeforeMount}
|
||||
loading={
|
||||
<div className="flex h-[300px] items-center justify-center bg-card">
|
||||
<Spinner size="sm" className="h-6 w-6 border-t-foreground" />
|
||||
</div>
|
||||
}
|
||||
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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user