@@ -42,7 +42,7 @@ import {
4242} from '@src/lib/settings/settingsUtils'
4343
4444import { err , reportRejection } from '@src/lib/trap'
45- import { deferredCallback } from '@src/lib/utils'
45+ import { deferredCallback , uuidv4 } from '@src/lib/utils'
4646import { ConnectionManager } from '@src/network/connectionManager'
4747import { EngineDebugger } from '@src/lib/debugger'
4848import type {
@@ -131,6 +131,7 @@ import type { FileEntry, Project } from '@src/lib/project'
131131import { getStringAfterLastSeparator } from '@src/lib/paths'
132132import type { SettingsActorType } from '@src/machines/settingsMachine'
133133import type { CommandBarActorType } from '@src/machines/commandBarMachine'
134+ import { isCodeTheSame } from '@src/lib/codeEditor'
134135import { getResolvedTheme } from '@src/lib/theme'
135136
136137interface ExecuteArgs {
@@ -155,6 +156,7 @@ interface SystemDeps {
155156 wasmInstancePromise : Promise < ModuleType >
156157 settings : SettingsActorType
157158 commandBar : CommandBarActorType
159+ projectPath : string
158160}
159161
160162export enum KclManagerEvents {
@@ -234,42 +236,47 @@ export class ZDSProject {
234236 if ( newPath === null ) {
235237 return
236238 }
237- const foundPathSignal = this . findEditorPathSignal ( newPath )
239+ const foundPathSignal = this . findEditor ( newPath )
238240 if ( ! foundPathSignal ) {
239241 return
240242 }
241- const found = this . editors . get ( foundPathSignal )
243+ const found = foundPathSignal [ 1 ]
242244 if ( found ) {
243245 // TODO: Reconfigure the editor to be an executing one
244246 }
245- this . #executingPath. value = foundPathSignal
247+ this . #executingPath. value = foundPathSignal [ 0 ]
246248 }
247- findEditorPathSignal ( path : string ) {
248- return this . editors . keys ( ) . find ( ( p ) => p . value === path )
249+ findEditor ( path : string ) {
250+ return this . editors . entries ( ) . find ( ( [ p ] ) => p . value === path )
249251 }
250252
251253 // Saving some keystrokes
252- private get = this . editors . get . bind ( this . editors )
253254 private set = this . editors . set . bind ( this . editors )
254255
255256 // TODO: Remove providedEditor, replace with options about if the editor is the executing one
256257 // once the app can handle not having a KclManager.
257258 openEditor ( path : string , providedEditor ?: KclManager ) {
258- const foundPathSignal = this . findEditorPathSignal ( path )
259- const found = foundPathSignal ? this . get ( foundPathSignal ) : undefined
259+ const foundEditor = this . findEditor ( path )
260+ const found = foundEditor ?. [ 1 ]
260261 if ( found ) {
261262 console . warn ( `Attempted to overwrite editor with path "${ path } "` )
262263 return found
263264 }
264265
265266 const newEditor =
266267 providedEditor ??
267- new KclManager ( {
268+ new KclManager ( path , {
268269 wasmInstancePromise : this . app . wasmPromise ,
269270 commandBar : this . app . commands . actor ,
270271 settings : this . app . settings . actor ,
272+ get projectPath ( ) {
273+ return this . path
274+ } ,
271275 } )
272276
277+ if ( providedEditor ) {
278+ providedEditor . path = path
279+ }
273280 // Initialize the editor theme
274281 // Subsequent changes are listened for within app.onSettingsUpdate()
275282 // TODO: Disassemble onSettingsUpdate, subscribe to changes from subsystems
@@ -285,15 +292,19 @@ export class ZDSProject {
285292 }
286293
287294 closeEditor ( path : string ) {
288- const foundPathSignal = this . findEditorPathSignal ( path )
295+ const foundPathSignal = this . findEditor ( path )
289296 if ( ! foundPathSignal ) {
290297 console . warn ( `Attempted to close nonexistent editor with path "${ path } "` )
291298 return
292299 }
293- this . editors . delete ( foundPathSignal )
300+ foundPathSignal [ 1 ] . close ( )
301+ this . editors . delete ( foundPathSignal [ 0 ] )
294302 }
295303
296304 closeAllEditors ( ) {
305+ for ( const editor of this . editors . values ( ) ) {
306+ editor . close ( )
307+ }
297308 this . editors . clear ( )
298309 }
299310}
@@ -428,6 +439,44 @@ export class KclManager extends EventTarget {
428439
429440 // INTERNAL BOOKKEEPING STATE
430441
442+ private fileWatcherKey = uuidv4 ( )
443+ /**
444+ * Watching the file system for updates and reacting to them.
445+ * TODO: We don't watch for deletions here, should we?
446+ */
447+ private onFileWatchEvent = ( _eventType : string , path : string ) => {
448+ // TODO: We can remove this once we make it impossible to have
449+ // a KclManager without a ZDSProject.
450+ if ( path !== this . path || ! this . systemDeps . projectPath ) {
451+ return
452+ }
453+ // Your current file is changed, read it from disk and write it into the code manager and execute the AST,
454+ // unless the change was initiated by us (the currently running instance).
455+ window . electron
456+ ?. readFile ( path , {
457+ encoding : 'utf-8' ,
458+ } )
459+ . then ( ( code ) => {
460+ const isInSketchMode =
461+ this . modelingState ?. matches ( 'Sketch' ) ||
462+ this . modelingState ?. matches ( 'sketchSolveMode' )
463+
464+ if ( ! isCodeTheSame ( code , this . code ) ) {
465+ // Nothing written out yet by ourselves, or it's not the same as the current file content
466+ // -> this must be an external change -> re-execute.
467+ this . updateCodeEditor ( code , {
468+ shouldExecute : ! isInSketchMode ,
469+ shouldResetCamera : ! isInSketchMode ,
470+ // We explicitly do not write to the file here since we are loading from
471+ // the file system and not the editor.
472+ shouldWriteToDisk : false ,
473+ } )
474+
475+ toast ( 'Reloading file from disk' , { icon : '📁' } )
476+ }
477+ } )
478+ . catch ( reportRejection )
479+ }
431480 private _wasmInitFailed = signal < boolean | undefined > ( undefined )
432481 private _astParseFailed = false
433482 private _switchedFiles = false
@@ -450,11 +499,11 @@ export class KclManager extends EventTarget {
450499 undefined
451500 public writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
452501 public mlEphantManagerMachineBulkManipulatingFileSystem = false
453- // The last code written by the app, used to compare against external changes to the current file
454- public lastWrite : {
455- code : string // last code written by ZDS
456- time : number // Unix epoch time in milliseconds
457- } | null = null
502+ /**
503+ Indicator Promise that is pending while a live write is happening.
504+ If this value isn't `null`, don't watch for file system writes it was probably us!
505+ */
506+ public writingPromise = signal < Promise < unknown > | null > ( null )
458507 public isBufferMode = false
459508 sceneInfraBaseUnitMultiplierSetter : ( unit : BaseUnit ) => void = ( ) => { }
460509 /** Values merged in from former EditorManager and CodeManager classes */
@@ -751,7 +800,7 @@ export class KclManager extends EventTarget {
751800 console . error ( 'Error when updating Rust state after user edit:' , error )
752801 }
753802 } ,
754- 300
803+ 1000
755804 )
756805
757806 private createEditorExtensions ( ) {
@@ -826,6 +875,18 @@ export class KclManager extends EventTarget {
826875 this . _wasmInitFailed . value = true
827876 reportRejection ( e )
828877 } )
878+
879+ // Register a file watcher for this file.
880+ window . electron ?. watchFileOn (
881+ path ,
882+ this . fileWatcherKey ,
883+ this . onFileWatchEvent
884+ )
885+ }
886+
887+ /** Clean up listeners, watchers, etc */
888+ public close ( ) {
889+ window . electron ?. watchFileOff ( this . path , this . fileWatcherKey )
829890 }
830891
831892 clearAst ( ) {
@@ -1854,7 +1915,6 @@ export class KclManager extends EventTarget {
18541915 updateCurrentFilePath ( path : string ) {
18551916 if ( this . _currentFilePath !== path ) {
18561917 this . _currentFilePath = path
1857- this . lastWrite = null
18581918 }
18591919 }
18601920 get currentFileName ( ) {
@@ -1954,17 +2014,14 @@ export class KclManager extends EventTarget {
19542014 // writes.
19552015 clearTimeout ( this . timeoutWriter )
19562016 return new Promise ( ( resolve , reject ) => {
1957- this . lastWrite = {
1958- code : newCode ?? '' ,
1959- time : Date . now ( ) ,
1960- }
19612017 this . timeoutWriter = setTimeout ( ( ) => {
19622018 if ( ! path ) {
19632019 return reject ( new Error ( 'currentFilePath not set' ) )
19642020 }
19652021 // Wait one event loop to give a chance for params to be set
19662022 // Save the file to disk
19672023 this . writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
2024+ window . electron ?. watchFileOff ( this . path , this . fileWatcherKey )
19682025 fsZds
19692026 . writeFile ( path , new TextEncoder ( ) . encode ( newCode ) )
19702027 . then ( resolve )
0 commit comments