import type { editor as MonacoEditor } from 'monaco-editor'; type HeightStyleProperty = 'height' | 'minHeight'; interface ResizeObserverLike { observe(target: Element): void; disconnect(): void; } type ResizeObserverConstructor = new (callback: () => void) => ResizeObserverLike; type WindowWithResizeObserver = Window & typeof globalThis & { ResizeObserver?: ResizeObserverConstructor }; interface AutoHeightLayoutOptions { minHeight?: number; heightStyleProperty?: HeightStyleProperty; } type AutoHeightEditor = Pick; /** * Keeps an editor sized to its content without enabling Monaco's automaticLayout. * * `automaticLayout` observes the editor container itself. Writing the content * height from `onDidContentSizeChange` while that observer is active can make * Monaco re-enter layout. This controller has one layout owner, coalesces bursts * of content-size events, and only reacts to actual width changes. */ export function installAutoHeightLayout( editor: AutoHeightEditor, container: HTMLElement, { minHeight = 0, heightStyleProperty = 'height' }: AutoHeightLayoutOptions = {}, ): () => void { const targetWindow = container.ownerDocument?.defaultView as WindowWithResizeObserver | null; let cancelPending: (() => void) | undefined; let disposed = false; let lastObservedWidth = container.clientWidth; let lastLayoutWidth = -1; let lastLayoutHeight = -1; const syncLayout = () => { cancelPending = undefined; if (disposed) return; const width = container.clientWidth; if (width === 0) return; const height = Math.max(editor.getContentHeight(), minHeight); const heightValue = `${height}px`; if (container.style.width !== '100%') { container.style.width = '100%'; } if (container.style[heightStyleProperty] !== heightValue) { container.style[heightStyleProperty] = heightValue; } lastObservedWidth = width; if (width !== lastLayoutWidth || height !== lastLayoutHeight) { lastLayoutWidth = width; lastLayoutHeight = height; editor.layout({ width, height }); } }; const scheduleLayout = () => { if (disposed || cancelPending) return; if (targetWindow?.requestAnimationFrame && targetWindow.cancelAnimationFrame) { const animationFrame = targetWindow.requestAnimationFrame(syncLayout); cancelPending = () => targetWindow.cancelAnimationFrame(animationFrame); return; } const timeout = setTimeout(syncLayout, 0); cancelPending = () => clearTimeout(timeout); }; const contentSizeListener = editor.onDidContentSizeChange(scheduleLayout); const ResizeObserverCtor = targetWindow?.ResizeObserver; const resizeObserver = ResizeObserverCtor === undefined ? undefined : new ResizeObserverCtor(() => { if (container.clientWidth !== lastObservedWidth) { scheduleLayout(); } }); // Height is owned by content sizing. Only width changes need a Monaco relayout. const handleWindowResize = () => scheduleLayout(); resizeObserver?.observe(container); if (!resizeObserver) { targetWindow?.addEventListener('resize', handleWindowResize); } syncLayout(); const dispose = () => { if (disposed) return; disposed = true; contentSizeListener.dispose(); resizeObserver?.disconnect(); targetWindow?.removeEventListener('resize', handleWindowResize); cancelPending?.(); cancelPending = undefined; }; editor.onDidDispose(dispose); return dispose; }