@@ -2,7 +2,13 @@ import { useTheme } from "@emotion/react";
22import Editor , { DiffEditor , loader } from "@monaco-editor/react" ;
33import type * as Monaco from "monaco-editor" ;
44import * as monaco from "monaco-editor" ;
5- import { type ComponentProps , type FC , useCallback } from "react" ;
5+ import {
6+ type ComponentProps ,
7+ type FC ,
8+ useCallback ,
9+ useEffect ,
10+ useRef ,
11+ } from "react" ;
612import { useCoderTheme } from "./coderTheme" ;
713
814loader . config ( { monaco } ) ;
@@ -38,40 +44,6 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
3844 const theme = useTheme ( ) ;
3945 const coderTheme = useCoderTheme ( ) ;
4046
41- // Auto-scroll to first diff when the diff editor mounts and diffs are computed.
42- const handleDiffEditorMount = useCallback (
43- (
44- editor : Monaco . editor . IStandaloneDiffEditor ,
45- monacoInstance : typeof Monaco ,
46- ) => {
47- // Call any existing onMount handler from editorProps.
48- editorProps ?. onMount ?.( editor , monacoInstance ) ;
49-
50- // Diffs may already be computed by the time onMount fires,
51- // so check immediately first. If not ready yet, fall back
52- // to waiting for the onDidUpdateDiff event.
53- const scrollToFirstDiff = ( ) => {
54- editor . goToDiff ( "next" ) ;
55- } ;
56-
57- const changes = editor . getLineChanges ( ) ;
58- if ( changes && changes . length > 0 ) {
59- scrollToFirstDiff ( ) ;
60- return ;
61- }
62-
63- const disposable = editor . onDidUpdateDiff ( ( ) => {
64- const updatedChanges = editor . getLineChanges ( ) ;
65- if ( ! updatedChanges || updatedChanges . length === 0 ) {
66- return ;
67- }
68- disposable . dispose ( ) ;
69- scrollToFirstDiff ( ) ;
70- } ) ;
71- } ,
72- [ editorProps ] ,
73- ) ;
74-
7547 const commonProps = {
7648 language,
7749 theme : coderTheme . name ,
@@ -99,20 +71,102 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
9971 } }
10072 >
10173 { hasDiff ? (
102- < DiffEditor
103- original = { compareWith }
104- modified = { value }
105- { ...commonProps }
106- // Let the editor handle model cleanup. Without this,
107- // @monaco -editor/react disposes models before the
108- // DiffEditorWidget and throws an error.
109- keepCurrentOriginalModel
110- keepCurrentModifiedModel
111- onMount = { handleDiffEditorMount }
112- />
74+ < DiffFile original = { compareWith } modified = { value } { ...commonProps } />
11375 ) : (
11476 < Editor value = { value } { ...commonProps } />
11577 ) }
11678 </ div >
11779 ) ;
11880} ;
81+
82+ type DiffFileProps = CommonEditorProps & {
83+ original : string ;
84+ modified : string ;
85+ } ;
86+
87+ // Renders the diff editor and owns its model cleanup. Scoping this to its own
88+ // component means the cleanup effect runs whenever the diff editor unmounts,
89+ // including when SyntaxHighlighter stays mounted but switches diff -> plain for
90+ // a file that stopped changing between versions.
91+ //
92+ // keepCurrent{Original,Modified}Model stops @monaco -editor/react from disposing
93+ // the models mid-teardown (which throws), so we dispose them ourselves after
94+ // React has torn the editor down. Without this the models accumulate unbounded
95+ // as users open template versions until the tab runs out of memory.
96+ const DiffFile : FC < DiffFileProps > = ( {
97+ original,
98+ modified,
99+ onMount,
100+ ...editorProps
101+ } ) => {
102+ const diffModelsRef = useRef < {
103+ original : Monaco . editor . ITextModel ;
104+ modified : Monaco . editor . ITextModel ;
105+ } | null > ( null ) ;
106+
107+ const handleMount = useCallback (
108+ (
109+ editor : Monaco . editor . IStandaloneDiffEditor ,
110+ monacoInstance : typeof Monaco ,
111+ ) => {
112+ onMount ?.( editor , monacoInstance ) ;
113+
114+ const diffModel = editor . getModel ( ) ;
115+ diffModelsRef . current = diffModel
116+ ? { original : diffModel . original , modified : diffModel . modified }
117+ : null ;
118+
119+ // Auto-scroll to the first diff. Diffs may already be computed by the
120+ // time onMount fires, so check immediately and otherwise wait for the
121+ // onDidUpdateDiff event.
122+ const scrollToFirstDiff = ( ) => {
123+ editor . goToDiff ( "next" ) ;
124+ } ;
125+
126+ const changes = editor . getLineChanges ( ) ;
127+ if ( changes && changes . length > 0 ) {
128+ scrollToFirstDiff ( ) ;
129+ return ;
130+ }
131+
132+ const disposable = editor . onDidUpdateDiff ( ( ) => {
133+ const updatedChanges = editor . getLineChanges ( ) ;
134+ if ( ! updatedChanges || updatedChanges . length === 0 ) {
135+ return ;
136+ }
137+ disposable . dispose ( ) ;
138+ scrollToFirstDiff ( ) ;
139+ } ) ;
140+ } ,
141+ [ onMount ] ,
142+ ) ;
143+
144+ useEffect ( ( ) => {
145+ return ( ) => {
146+ const models = diffModelsRef . current ;
147+ if ( ! models ) {
148+ return ;
149+ }
150+ diffModelsRef . current = null ;
151+ // Defer disposal until after React's commit finishes. @monaco-editor/
152+ // react disposes the diff widget in its own unmount cleanup; freeing
153+ // the models in the same synchronous teardown makes the widget throw
154+ // "TextModel got disposed before DiffEditorWidget model got reset".
155+ queueMicrotask ( ( ) => {
156+ models . original . dispose ( ) ;
157+ models . modified . dispose ( ) ;
158+ } ) ;
159+ } ;
160+ } , [ ] ) ;
161+
162+ return (
163+ < DiffEditor
164+ original = { original }
165+ modified = { modified }
166+ { ...editorProps }
167+ keepCurrentOriginalModel
168+ keepCurrentModifiedModel
169+ onMount = { handleMount }
170+ />
171+ ) ;
172+ } ;
0 commit comments