/* Copyright 2026 Marimo. All rights reserved. */ import { EditorView } from "@codemirror/view"; import { type JSX, useEffect, useMemo, useState } from "react"; import useEvent from "react-use-event-hook"; import { z } from "zod"; import { useDebounceControlledState } from "@/hooks/useDebounce"; import { type Theme, useTheme } from "@/theme/useTheme"; import type { IPlugin, IPluginProps, Setter } from "../types"; import { LazyAnyLanguageCodeMirror } from "./code/LazyAnyLanguageCodeMirror"; import { Labeled } from "./common/labeled"; type T = string; interface Data { language: string; placeholder: string; theme?: Theme; label: string | null; disabled?: boolean; minHeight?: number; maxHeight?: number; showCopyButton?: boolean; debounce: boolean | number; } export class CodeEditorPlugin implements IPlugin { tagName = "marimo-code-editor"; validator = z.object({ initialValue: z.string(), language: z.string().default("python"), placeholder: z.string(), theme: z.enum(["light", "dark"]).optional(), label: z.string().nullable(), disabled: z.boolean().optional(), minHeight: z.number().optional(), maxHeight: z.number().optional(), showCopyButton: z.boolean().optional(), debounce: z.union([z.boolean(), z.number()]).default(false), }); render(props: IPluginProps): JSX.Element { return ( ); } } interface CodeEditorComponentProps extends Data { value: T; setValue: Setter; } const CodeEditorComponent = (props: CodeEditorComponentProps) => { const { theme } = useTheme(); const finalTheme = props.theme || theme; const minHeight = props.minHeight ? `${props.minHeight}px` : "70px"; const maxHeight = props.maxHeight ? `${props.maxHeight}px` : undefined; const [localValue, setLocalValue] = useState(props.value); const { onChange: setValueDebounced } = useDebounceControlledState({ initialValue: props.value, delay: Number.isFinite(props.debounce) ? (props.debounce as number) : 0, onChange: props.setValue, disabled: !Number.isFinite(props.debounce), }); const handleChange = useEvent((newValue: string) => { setLocalValue((_) => newValue); if (typeof props.debounce === "number") { setValueDebounced(newValue); } else if (!props.debounce) { props.setValue(newValue); } }); // This is to sync the value from Python whenever the cell is updated // as useState doesn't reinitialize on re-renders useEffect(() => { setLocalValue(props.value); }, [props.value]); const onBlur = useEvent(() => { props.setValue(localValue); }); const extensions = useMemo(() => { if (props.debounce === true) { return [EditorView.domEventHandlers({ blur: onBlur })]; } return []; }, [props.debounce, onBlur]); return ( ); };