|
1 | | -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react' |
| 1 | +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type MutableRefObject } from 'react' |
2 | 2 | import ReactDOM from 'react-dom' |
3 | | -import Editor, { DiffEditor, useMonaco } from '@monaco-editor/react' |
| 3 | +import Editor, { useMonaco } from '@monaco-editor/react' |
| 4 | +import type { editor as MonacoEditor } from 'monaco-editor' |
4 | 5 | import * as YAML from 'js-yaml' |
5 | 6 | import { |
6 | 7 | Box, |
@@ -1708,58 +1709,96 @@ function SafeDiffEditor({ |
1708 | 1709 | modified: string |
1709 | 1710 | }) { |
1710 | 1711 | const monaco = useMonaco() |
1711 | | - const monacoRef = useRef(monaco) |
1712 | | - const modelPathsRef = useRef<Set<string>>(new Set()) |
| 1712 | + const containerRef = useRef<HTMLDivElement | null>(null) |
| 1713 | + const editorRef = useRef<MonacoEditor.IStandaloneDiffEditor | null>(null) |
| 1714 | + const originalModelRef = useRef<MonacoEditor.ITextModel | null>(null) |
| 1715 | + const modifiedModelRef = useRef<MonacoEditor.ITextModel | null>(null) |
1713 | 1716 | const safeModelKey = encodeURIComponent(modelKey) |
1714 | 1717 | const originalModelPath = `inmemory://umodel-diff/${safeModelKey}.original.json` |
1715 | 1718 | const modifiedModelPath = `inmemory://umodel-diff/${safeModelKey}.modified.json` |
1716 | 1719 |
|
1717 | 1720 | useEffect(() => { |
1718 | | - monacoRef.current = monaco |
| 1721 | + if (!monaco || !containerRef.current) return undefined |
| 1722 | + |
| 1723 | + const diffEditor = monaco.editor.createDiffEditor(containerRef.current, { |
| 1724 | + automaticLayout: true, |
| 1725 | + fontFamily: 'SF Mono, Fira Code, Consolas, monospace', |
| 1726 | + fontSize: 12, |
| 1727 | + minimap: { enabled: false }, |
| 1728 | + originalEditable: false, |
| 1729 | + readOnly: true, |
| 1730 | + renderMarginRevertIcon: false, |
| 1731 | + renderSideBySide: true, |
| 1732 | + scrollBeyondLastLine: false, |
| 1733 | + wordWrap: 'on', |
| 1734 | + }) |
| 1735 | + editorRef.current = diffEditor |
| 1736 | + |
| 1737 | + return () => { |
| 1738 | + editorRef.current = null |
| 1739 | + diffEditor.setModel(null) |
| 1740 | + diffEditor.dispose() |
| 1741 | + |
| 1742 | + const originalModel = originalModelRef.current |
| 1743 | + const modifiedModel = modifiedModelRef.current |
| 1744 | + originalModelRef.current = null |
| 1745 | + modifiedModelRef.current = null |
| 1746 | + scheduleDiffModelDispose(originalModel, originalModelRef, modifiedModelRef) |
| 1747 | + scheduleDiffModelDispose(modifiedModel, originalModelRef, modifiedModelRef) |
| 1748 | + } |
1719 | 1749 | }, [monaco]) |
1720 | 1750 |
|
1721 | 1751 | useEffect(() => { |
1722 | | - modelPathsRef.current.add(originalModelPath) |
1723 | | - modelPathsRef.current.add(modifiedModelPath) |
1724 | | - }, [modifiedModelPath, originalModelPath]) |
| 1752 | + if (!monaco || !editorRef.current) return |
1725 | 1753 |
|
1726 | | - useEffect(() => { |
1727 | | - return () => { |
1728 | | - const monacoInstance = monacoRef.current |
1729 | | - const modelPaths = Array.from(modelPathsRef.current) |
1730 | | - window.setTimeout(() => { |
1731 | | - for (const path of modelPaths) { |
1732 | | - const model = monacoInstance?.editor.getModel(monacoInstance.Uri.parse(path)) |
1733 | | - model?.dispose() |
1734 | | - } |
1735 | | - }, 0) |
| 1754 | + const originalUri = monaco.Uri.parse(originalModelPath) |
| 1755 | + const modifiedUri = monaco.Uri.parse(modifiedModelPath) |
| 1756 | + const originalModel = upsertMonacoModel(monaco, originalUri, original) |
| 1757 | + const modifiedModel = upsertMonacoModel(monaco, modifiedUri, modified) |
| 1758 | + const previousOriginal = originalModelRef.current |
| 1759 | + const previousModified = modifiedModelRef.current |
| 1760 | + |
| 1761 | + editorRef.current.setModel({ original: originalModel, modified: modifiedModel }) |
| 1762 | + originalModelRef.current = originalModel |
| 1763 | + modifiedModelRef.current = modifiedModel |
| 1764 | + |
| 1765 | + if (previousOriginal !== originalModel) { |
| 1766 | + scheduleDiffModelDispose(previousOriginal, originalModelRef, modifiedModelRef) |
1736 | 1767 | } |
1737 | | - }, []) |
| 1768 | + if (previousModified !== modifiedModel) { |
| 1769 | + scheduleDiffModelDispose(previousModified, originalModelRef, modifiedModelRef) |
| 1770 | + } |
| 1771 | + }, [modified, modifiedModelPath, monaco, original, originalModelPath]) |
1738 | 1772 |
|
1739 | | - return ( |
1740 | | - <DiffEditor |
1741 | | - original={original} |
1742 | | - modified={modified} |
1743 | | - language="json" |
1744 | | - originalModelPath={originalModelPath} |
1745 | | - modifiedModelPath={modifiedModelPath} |
1746 | | - keepCurrentOriginalModel |
1747 | | - keepCurrentModifiedModel |
1748 | | - theme="vs" |
1749 | | - options={{ |
1750 | | - automaticLayout: true, |
1751 | | - fontFamily: 'SF Mono, Fira Code, Consolas, monospace', |
1752 | | - fontSize: 12, |
1753 | | - minimap: { enabled: false }, |
1754 | | - originalEditable: false, |
1755 | | - readOnly: true, |
1756 | | - renderMarginRevertIcon: false, |
1757 | | - renderSideBySide: true, |
1758 | | - scrollBeyondLastLine: false, |
1759 | | - wordWrap: 'on', |
1760 | | - }} |
1761 | | - /> |
1762 | | - ) |
| 1773 | + return <div className="ume-diff-editor-host" ref={containerRef} /> |
| 1774 | +} |
| 1775 | + |
| 1776 | +function upsertMonacoModel( |
| 1777 | + monaco: NonNullable<ReturnType<typeof useMonaco>>, |
| 1778 | + uri: ReturnType<NonNullable<ReturnType<typeof useMonaco>>['Uri']['parse']>, |
| 1779 | + value: string, |
| 1780 | +) { |
| 1781 | + const model = monaco.editor.getModel(uri) |
| 1782 | + if (model) { |
| 1783 | + if (model.getValue() !== value) model.setValue(value) |
| 1784 | + return model |
| 1785 | + } |
| 1786 | + return monaco.editor.createModel(value, 'json', uri) |
| 1787 | +} |
| 1788 | + |
| 1789 | +function scheduleDiffModelDispose( |
| 1790 | + model: MonacoEditor.ITextModel | null, |
| 1791 | + originalModelRef: MutableRefObject<MonacoEditor.ITextModel | null>, |
| 1792 | + modifiedModelRef: MutableRefObject<MonacoEditor.ITextModel | null>, |
| 1793 | +) { |
| 1794 | + if (!model) return |
| 1795 | + |
| 1796 | + window.setTimeout(() => { |
| 1797 | + if (model.isDisposed()) return |
| 1798 | + if (originalModelRef.current === model || modifiedModelRef.current === model) return |
| 1799 | + if (model.isAttachedToEditor()) return |
| 1800 | + model.dispose() |
| 1801 | + }, 1_000) |
1763 | 1802 | } |
1764 | 1803 |
|
1765 | 1804 | function rowToElement(row: Record<string, unknown>): UModelElement { |
|
0 commit comments