import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type CSSProperties, } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import type { NotebookCell } from "@/bindings"; import { tx } from "@/platform/i18n"; import { useMarkdownPreviewFontSize } from "@/platform/languageSettings"; import { useProject } from "@/platform/project"; import { Button, Dialog, EmptyState, IconButton, Select, Spinner } from "@/ui"; import type { EditorInput } from "@/workbench/contributions"; import { CellEditor } from "./CellEditor"; import { CellOutputs, MarkdownCell } from "./OutputView"; import { notebookSession, type NotebookSession } from "./session"; import styles from "./Notebook.module.css"; export function NotebookEditor({ input }: { input: EditorInput }) { if (!input.path) return ; return ; } function NotebookDocument({ session }: { session: NotebookSession }) { const state = useSyncExternalStore(session.subscribe, session.getState); const root = useRef(null); const scroll = useRef(null); const [selected, select] = useState(session.selectedCell); const [reloadOpen, setReloadOpen] = useState(false); const cells = session.cells(); const active = cells.find((cell) => cell.id === selected) ?? cells.find((cell) => cell.cellType === "code"); const invoke = useCallback( (work: Promise) => { void work.catch((error) => session.report(error)); }, [session], ); const choose = (id: string) => { session.selectedCell = id; select(id); }; const virtualizer = useVirtualizer({ count: cells.length, getScrollElement: () => scroll.current, getItemKey: (index) => cells[index]!.id, estimateSize: () => 220, overscan: 3, initialOffset: session.scrollTop, }); useEffect(() => { invoke(session.load()); }, [session, invoke]); useLayoutEffect(() => { const element = root.current; if (!element) return; const history = (event: InputEvent) => { if (event.inputType !== "historyUndo" && event.inputType !== "historyRedo") return; event.preventDefault(); event.stopImmediatePropagation(); invoke(event.inputType === "historyUndo" ? session.undo() : session.redo()); }; element.addEventListener("beforeinput", history, true); return () => element.removeEventListener("beforeinput", history, true); }, [session, invoke]); // Markdown cells follow the Markdown preview size, code cells and outputs the editor font, not the UI font. const markdownFontSize = useMarkdownPreviewFontSize(); const editor = useProject((s) => s.settings?.editor as { fontSize?: number; lineHeight?: number } | undefined); const fontVars = { "--markdown-preview-font-size": `${markdownFontSize}px`, ...(editor?.fontSize ? { "--openidea-editor-font-size": `${editor.fontSize}px` } : {}), ...(editor?.lineHeight ? { "--openidea-editor-line-height": `${editor.lineHeight}px` } : {}), } as CSSProperties; const disabled = state.frozen || !state.snapshot; const historyDisabled = disabled || state.running; const options = state.environments.map((sdk) => ({ value: sdk.path, label: sdk.name, detail: sdk.path })); if (state.python && !options.some((option) => option.value === state.python)) options.unshift({ value: state.python, label: state.python, detail: state.python, }); return (
{ if (event.nativeEvent.isComposing || event.defaultPrevented) return; if (event.metaKey || event.ctrlKey) { const key = event.key.toLowerCase(); if (key === "s") { event.preventDefault(); event.stopPropagation(); invoke(session.save()); } if (key === "z" || key === "y") { event.preventDefault(); event.stopPropagation(); invoke(event.shiftKey || key === "y" ? session.redo() : session.undo()); } } if (event.key === "Enter" && (event.shiftKey || event.ctrlKey) && active?.cellType === "code") { event.preventDefault(); event.stopPropagation(); invoke(session.execute([active.id])); } }} >
invoke(session.interrupt())} /> invoke(session.restart())} /> session.edit({ type: "setCellType", cell: cell.id, cellType: value })} /> {cell.cellType === "code" && ( { void session.execute([cell.id]).catch((error) => session.report(error)); }} /> )} {cell.cellType === "markdown" && ( { if (editing) session.markdownEditing.delete(cell.id); else session.markdownEditing.add(cell.id); setEditing(!editing); }} /> )} session.edit({ type: "moveCell", cell: cell.id, before: cells[index - 1]!.id })} /> session.edit({ type: "moveCell", cell: cell.id, before: cells[index + 2]?.id ?? null })} /> session.edit({ type: "insertCell", after: cell.id, cellType: "code" })} /> session.edit({ type: "deleteCell", cell: cell.id })} />
{!known ? (

{tx("Raw or unknown cell; original content is preserved read-only.")}

{cell.source}
) : cell.cellType === "markdown" && !editing ? (
{ if (!frozen) { session.markdownEditing.add(cell.id); setEditing(true); } }} > {cell.source ? ( ) : ( )}
) : ( )} {cell.cellType === "code" && } ); }