@@ -46,6 +46,10 @@ type JumpTarget = {
4646 line : number ;
4747 column ?: number ;
4848} ;
49+ type BufferEntry = {
50+ content : string ;
51+ isDirty : boolean ;
52+ } ;
4953
5054const toFilePath = ( filePath : string ) => {
5155 if ( ! filePath . startsWith ( "file:" ) ) return filePath ;
@@ -160,10 +164,12 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
160164 const lspClientRef = useRef < MonacoLanguageClient | null > ( null ) ;
161165 const lspStartingRef = useRef ( false ) ;
162166 const resizerRef = useRef < HTMLDivElement > ( null ) ;
163- const { registerEditor } = useEditor ( ) ;
167+ const { registerEditor, setCurrentFile : setCurrentFileContext } = useEditor ( ) ;
164168 const loadIdRef = useRef ( 0 ) ;
165169 const pendingJumpRef = useRef < JumpTarget | null > ( null ) ;
166170 const currentFileRef = useRef < string > ( currentFile ) ;
171+ const bufferCacheRef = useRef < Map < string , BufferEntry > > ( new Map ( ) ) ;
172+ const isApplyingRef = useRef ( false ) ;
167173 const openFileRef = useRef <
168174 ( ( filePath : string , line ?: number , column ?: number ) => Promise < void > ) | null
169175 > ( null ) ;
@@ -175,6 +181,39 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
175181 currentFileRef . current = currentFile ;
176182 } , [ currentFile ] ) ;
177183
184+ useEffect ( ( ) => {
185+ const handleBeforeUnload = ( event : BeforeUnloadEvent ) => {
186+ const hasDirty = Array . from ( bufferCacheRef . current . values ( ) ) . some ( ( entry ) => entry . isDirty ) ;
187+ if ( ! hasDirty ) return ;
188+ event . preventDefault ( ) ;
189+ event . returnValue = "" ;
190+ } ;
191+ window . addEventListener ( "beforeunload" , handleBeforeUnload ) ;
192+ return ( ) => {
193+ window . removeEventListener ( "beforeunload" , handleBeforeUnload ) ;
194+ } ;
195+ } , [ ] ) ;
196+
197+ const notifyUnsavedChanges = useCallback ( ( ) => {
198+ if ( ! window . editorAPI ?. setUnsavedChanges ) return ;
199+ const hasDirty = Array . from ( bufferCacheRef . current . values ( ) ) . some ( ( entry ) => entry . isDirty ) ;
200+ window . editorAPI . setUnsavedChanges ( hasDirty ) ;
201+ } , [ ] ) ;
202+
203+ const applyBuffer = useCallback ( ( fileUri : string , content : string , dirty : boolean ) => {
204+ isApplyingRef . current = true ;
205+ setCode ( content ) ;
206+ setCurrentFile ( fileUri ) ;
207+ setIsDirty ( dirty ) ;
208+ currentFileRef . current = fileUri ;
209+ bufferCacheRef . current . set ( fileUri , { content, isDirty : dirty } ) ;
210+ notifyUnsavedChanges ( ) ;
211+ setCurrentFileContext ( fileUri ) ;
212+ queueMicrotask ( ( ) => {
213+ isApplyingRef . current = false ;
214+ } ) ;
215+ } , [ notifyUnsavedChanges , setCurrentFileContext ] ) ;
216+
178217 const readFile = useCallback ( async ( filePath : string ) => {
179218 const rawPath = toFilePath ( filePath ) ;
180219 if ( window . editorAPI ?. readFileOptional ) {
@@ -217,19 +256,21 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
217256 const { content, path } = payload ;
218257 if ( loadId !== loadIdRef . current ) return ;
219258 const fileUri = toFileUri ( path ) ;
220- setCode ( content ) ;
221- setCurrentFile ( fileUri ) ;
222- setIsDirty ( false ) ;
259+ const cached = bufferCacheRef . current . get ( fileUri ) ;
260+ const shouldUseCache = cached ?. isDirty === true ;
261+ const nextContent = shouldUseCache && cached ? cached . content : content ;
262+ const nextDirty = shouldUseCache ;
263+ applyBuffer ( fileUri , nextContent , nextDirty ) ;
223264 const editor = editorRef . current ;
224265 const monacoApi = monacoRef . current ;
225266 if ( editor && monacoApi ) {
226267 const uri = monacoApi . Uri . parse ( fileUri ) ;
227268 let model = monacoApi . editor . getModel ( uri ) ;
228269 const languageId = getLanguageId ( fileUri ) ;
229270 if ( ! model ) {
230- model = monacoApi . editor . createModel ( content , languageId , uri ) ;
231- } else if ( model . getValue ( ) !== content ) {
232- model . setValue ( content ) ;
271+ model = monacoApi . editor . createModel ( nextContent , languageId , uri ) ;
272+ } else if ( model . getValue ( ) !== nextContent ) {
273+ model . setValue ( nextContent ) ;
233274 }
234275 if ( editor . getModel ( ) ?. uri . toString ( ) !== uri . toString ( ) ) {
235276 editor . setModel ( model ) ;
@@ -244,7 +285,7 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
244285 setIsLoading ( false ) ;
245286 }
246287 }
247- } , [ readFile ] ) ;
288+ } , [ applyBuffer , readFile ] ) ;
248289
249290 const revealLine = useCallback ( ( line : number ) => {
250291 const editor = editorRef . current ;
@@ -488,24 +529,32 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
488529 const textModel = model ?. textEditorModel ?? null ;
489530 const target = textModel ?. uri ?. toString ( ) ;
490531 if ( target ) {
532+ const cached = bufferCacheRef . current . get ( target ) ;
533+ const cachedDirty = cached ?. isDirty === true ;
534+ const cachedContent = cached ?. content ;
491535 const selection = extractSelection ( options ) ;
492536 if ( ! selection && options ) {
493537 logNavigationIssue ( "Missing selection for editor navigation" , options ) ;
494538 }
495539 const editor = editorRef . current ;
496540 if ( textModel && editor ) {
497- editor . setModel ( textModel as unknown as MonacoEditor . editor . ITextModel ) ;
498- const content = textModel . getValue ( ) ;
499- setCode ( content ) ;
500- setCurrentFile ( target ) ;
501- setIsDirty ( false ) ;
502- currentFileRef . current = target ;
541+ if ( cachedDirty && cachedContent != null ) {
542+ applyBuffer ( target , cachedContent , true ) ;
543+ editor . setModel ( textModel as unknown as MonacoEditor . editor . ITextModel ) ;
544+ if ( textModel . getValue ( ) !== cachedContent ) {
545+ textModel . setValue ( cachedContent ) ;
546+ }
547+ } else {
548+ const content = textModel . getValue ( ) ;
549+ applyBuffer ( target , content , false ) ;
550+ editor . setModel ( textModel as unknown as MonacoEditor . editor . ITextModel ) ;
551+ }
503552 if ( selection ) {
504553 revealPosition ( selection . line , selection . column ) ;
505554 }
506555 return editor as unknown as MonacoEditor . editor . ICodeEditor ;
507556 }
508- const content = textModel ?. getValue ( ) ;
557+ const content = cachedDirty && cachedContent != null ? cachedContent : textModel ?. getValue ( ) ;
509558 if ( content != null ) {
510559 await openFileWithContentRef . current ?.( target , content , selection ?. line , selection ?. column ) ;
511560 } else {
@@ -546,7 +595,7 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
546595 throw error ;
547596 } ) ;
548597 return vscodeInitRef . current ;
549- } , [ ensureFileSystemProvider ] ) ;
598+ } , [ applyBuffer , ensureFileSystemProvider , revealPosition ] ) ;
550599
551600 useEffect ( ( ) => {
552601 let cancelled = false ;
@@ -770,6 +819,10 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
770819 }
771820 }
772821 setIsDirty ( false ) ;
822+ if ( currentFileRef . current ) {
823+ bufferCacheRef . current . set ( currentFileRef . current , { content : code , isDirty : false } ) ;
824+ }
825+ notifyUnsavedChanges ( ) ;
773826
774827 // Trigger hot reload
775828 if ( import . meta. hot ) {
@@ -920,8 +973,14 @@ export const CodeEditor = ({ width = 400, onWidthChange }: CodeEditorProps) => {
920973 language = { languageId }
921974 value = { code }
922975 onChange = { ( value ) => {
923- setCode ( value || "" ) ;
976+ if ( isApplyingRef . current ) return ;
977+ const nextValue = value || "" ;
978+ setCode ( nextValue ) ;
924979 setIsDirty ( true ) ;
980+ if ( currentFileRef . current ) {
981+ bufferCacheRef . current . set ( currentFileRef . current , { content : nextValue , isDirty : true } ) ;
982+ }
983+ notifyUnsavedChanges ( ) ;
925984 } }
926985 beforeMount = { configureMonaco }
927986 onMount = { handleEditorDidMount }
0 commit comments