(null);
const primaryDisposableRef = useRef<{ dispose: () => void } | null>(null);
const mentionDisposableRef = useRef<{ dispose: () => void } | null>(null);
const primaryRegistrationRef = useRef<() => void>(() => undefined);
const mentionRegistrationRef = useRef<() => void>(() => undefined);
const disposePrimaryProvider = useCallback(() => {
primaryDisposableRef.current?.dispose();
primaryDisposableRef.current = null;
}, []);
const disposeMentionProvider = useCallback(() => {
mentionDisposableRef.current?.dispose();
mentionDisposableRef.current = null;
}, []);
const registerPrimaryProvider = useCallback(() => {
disposePrimaryProvider();
const monaco = monacoRef.current;
const model = modelRef.current;
const factory = resolveFactory(completionProvider, placeholders, executorType);
if (!monaco || !model || !factory) return;
primaryDisposableRef.current = monaco.languages.registerCompletionItemProvider(
language,
scopeCompletionProviderToModel(factory(monaco), model),
);
}, [completionProvider, disposePrimaryProvider, executorType, language, placeholders]);
const registerMentionProvider = useCallback(() => {
disposeMentionProvider();
const monaco = monacoRef.current;
const model = modelRef.current;
const factory = resolveMentionFactory(mentionPrompts);
if (!monaco || !model || !factory) return;
mentionDisposableRef.current = monaco.languages.registerCompletionItemProvider(
language,
scopeCompletionProviderToModel(factory(monaco), model),
);
}, [disposeMentionProvider, language, mentionPrompts]);
// Monaco calls onMount only once. Keep its callback pointed at the latest
// provider registrations so data that loads before Monaco finishes mounting
// is available on the first registration.
primaryRegistrationRef.current = registerPrimaryProvider;
mentionRegistrationRef.current = registerMentionProvider;
useEffect(() => {
registerPrimaryProvider();
return disposePrimaryProvider;
}, [disposePrimaryProvider, registerPrimaryProvider]);
useEffect(() => {
registerMentionProvider();
return disposeMentionProvider;
}, [disposeMentionProvider, registerMentionProvider]);
const handleMount: OnMount = useCallback((editor, monaco) => {
monacoRef.current = monaco;
modelRef.current = editor.getModel();
primaryRegistrationRef.current();
mentionRegistrationRef.current();
}, []);
return handleMount;
}
export function ScriptEditor({
value,
onChange,
language = "shell",
height = "300px",
placeholders,
executorType,
completionProvider,
mentionPrompts,
readOnly = false,
lineNumbers = "on",
ariaLabel,
testId,
}: ScriptEditorProps) {
const handleMount = useCompletionProviders({
language,
completionProvider,
placeholders,
executorType,
mentionPrompts,
});
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
monaco.editor.defineTheme("kandev-dark", KANDEV_MONACO_DARK);
}, []);
return (
onChange(v ?? "")}
beforeMount={handleBeforeMount}
onMount={handleMount}
theme="kandev-dark"
options={{
ariaLabel,
minimap: { enabled: false },
lineNumbers,
wordWrap: "on",
fontFamily: EDITOR_FONT_FAMILY,
fontSize: EDITOR_FONT_SIZE,
scrollBeyondLastLine: false,
readOnly,
bracketPairColorization: { enabled: true },
padding: { top: 8, bottom: 8 },
renderLineHighlight: "none",
overviewRulerLanes: 0,
fixedOverflowWidgets: true,
wordBasedSuggestions: "off",
scrollbar: {
vertical: "auto",
horizontal: "auto",
alwaysConsumeMouseWheel: false,
},
}}
/>
);
}
function resolveFactory(
custom: CompletionProviderFactory | undefined,
placeholders: ScriptPlaceholder[] | undefined,
executorType: string | undefined,
): CompletionProviderFactory | null {
if (custom) return custom;
if (placeholders && placeholders.length > 0) {
return (monaco) => createPlaceholderCompletionProvider(monaco, placeholders, executorType);
}
return null;
}
function resolveMentionFactory(
mentionPrompts: PromptReference[] | undefined,
): CompletionProviderFactory | null {
if (!mentionPrompts || mentionPrompts.length === 0) return null;
return (monaco) => createPromptMentionCompletionProvider(monaco, mentionPrompts);
}