@@ -33,7 +33,6 @@ import ToolboxSettingsModal from './reactComponents/ToolboxSettings';
3333import * as Tabs from './reactComponents/Tabs' ;
3434import { TabType } from './types/TabType' ;
3535
36- import { createGeneratorContext , GeneratorContext } from './editor/generator_context' ;
3736import * as editor from './editor/editor' ;
3837import { extendedPythonGenerator } from './editor/extended_python_generator' ;
3938
@@ -161,6 +160,7 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
161160 const [ messageApi , contextHolder ] = Antd . message . useMessage ( ) ;
162161 const [ generatedCode , setGeneratedCode ] = React . useState < string > ( '' ) ;
163162 const [ toolboxSettingsModalIsOpen , setToolboxSettingsModalIsOpen ] = React . useState ( false ) ;
163+ const [ modulePathToContentText , setModulePathToContentText ] = React . useState < { [ modulePath : string ] : string } > ( { } ) ;
164164 const [ tabItems , setTabItems ] = React . useState < Tabs . TabItem [ ] > ( [ ] ) ;
165165 const [ activeTab , setActiveTab ] = React . useState ( '' ) ;
166166 const [ shownPythonToolboxCategories , setShownPythonToolboxCategories ] = React . useState < Set < string > > ( new Set ( ) ) ;
@@ -171,9 +171,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
171171 const [ languageInitialized , setLanguageInitialized ] = React . useState ( false ) ;
172172 const [ themeInitialized , setThemeInitialized ] = React . useState ( false ) ;
173173
174- const blocksEditor = React . useRef < editor . Editor | null > ( null ) ;
175- const generatorContext = React . useRef < GeneratorContext | null > ( null ) ;
176- const blocklyComponent = React . useRef < BlocklyComponentType | null > ( null ) ;
174+ /** modulePaths controls how BlocklyComponents are created. */
175+ const modulePaths = React . useRef < string [ ] > ( [ ] ) ;
176+ const modulePathToBlocklyComponent = React . useRef < { [ modulePath : string ] : BlocklyComponentType } > ( { } ) ;
177+ const modulePathToEditor = React . useRef < { [ modulePath : string ] : editor . Editor } > ( { } ) ;
177178
178179 /** Initialize language from UserSettings when app first starts. */
179180 React . useEffect ( ( ) => {
@@ -207,7 +208,7 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
207208 // Save current blocks before language change
208209 if ( currentModule && areBlocksModified ( ) ) {
209210 try {
210- await saveBlocks ( ) ;
211+ await saveModule ( ) ;
211212 } catch ( e ) {
212213 console . error ( 'Failed to save blocks before language change:' , e ) ;
213214 }
@@ -222,9 +223,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
222223 }
223224 }
224225
225- // Update toolbox after language change
226- if ( blocksEditor . current ) {
227- blocksEditor . current . updateToolbox ( shownPythonToolboxCategories ) ;
226+ // Update toolbox in all editors after language change.
227+ for ( const modulePath in modulePathToEditor . current ) {
228+ const editor = modulePathToEditor . current [ modulePath ] ;
229+ editor . updateToolbox ( shownPythonToolboxCategories ) ;
228230 }
229231 } ;
230232
@@ -298,19 +300,32 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
298300 return ;
299301 }
300302
303+ // Check whether this blockly workspace is for the current module.
304+ if ( ! currentModule ||
305+ ! ( currentModule . modulePath in modulePathToBlocklyComponent . current ) ) {
306+ return ;
307+ }
308+ const blocklyComponent = modulePathToBlocklyComponent . current [ currentModule . modulePath ] ;
309+ if ( event . workspaceId != blocklyComponent . getBlocklyWorkspace ( ) . id ) {
310+ return ;
311+ }
312+
301313 setTriggerPythonRegeneration ( Date . now ( ) ) ;
302314 } ;
303315
304316 /** Saves blocks to storage with success/error messaging. */
305- const saveBlocks = async ( ) : Promise < boolean > => {
317+ const saveModule = async ( ) : Promise < boolean > => {
306318 return new Promise ( async ( resolve , reject ) => {
307- if ( ! blocksEditor . current ) {
319+ if ( ! currentModule ||
320+ ! ( currentModule . modulePath in modulePathToEditor . current ) ) {
308321 reject ( new Error ( 'Blocks editor not initialized' ) ) ;
309322 return ;
310323 }
324+ const editor = modulePathToEditor . current [ currentModule . modulePath ] ;
311325
312326 try {
313- await blocksEditor . current . saveBlocks ( ) ;
327+ const moduleContentText = await editor . saveModule ( ) ;
328+ modulePathToContentText [ currentModule . modulePath ] = moduleContentText ;
314329 messageApi . open ( {
315330 type : 'success' ,
316331 content : SAVE_SUCCESS_MESSAGE ,
@@ -339,13 +354,18 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
339354
340355 /** Checks if blocks have been modified. */
341356 const areBlocksModified = ( ) : boolean => {
342- return blocksEditor . current ? blocksEditor . current . isModified ( ) : false ;
357+ if ( currentModule &&
358+ currentModule . modulePath in modulePathToEditor . current ) {
359+ const editor = modulePathToEditor . current [ currentModule . modulePath ] ;
360+ return editor . isModified ( ) ;
361+ }
362+ return false ;
343363 } ;
344364
345365 /** Changes current module with automatic saving if modified. */
346366 const changeModule = async ( module : storageModule . Module | null ) : Promise < void > => {
347367 if ( currentModule && areBlocksModified ( ) ) {
348- await saveBlocks ( ) ;
368+ await saveModule ( ) ;
349369 }
350370 setCurrentModule ( module ) ;
351371 } ;
@@ -391,11 +411,13 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
391411 } ;
392412
393413 /** Handles toolbox update requests from blocks */
394- const handleToolboxUpdateRequest = React . useCallback ( ( ) => {
395- if ( blocksEditor . current && currentModule ) {
396- blocksEditor . current . updateToolbox ( shownPythonToolboxCategories ) ;
414+ const handleToolboxUpdateRequest = React . useCallback ( ( e : Event ) => {
415+ const workspaceId = ( e as CustomEvent ) . detail . workspaceId ;
416+ const correspondingEditor = editor . Editor . getEditorForBlocklyWorkspaceId ( workspaceId ) ;
417+ if ( correspondingEditor ) {
418+ correspondingEditor . updateToolbox ( shownPythonToolboxCategories ) ;
397419 }
398- } , [ currentModule , shownPythonToolboxCategories , i18n . language ] ) ;
420+ } , [ shownPythonToolboxCategories , i18n . language ] ) ;
399421
400422 // Add event listener for toolbox updates
401423 React . useEffect ( ( ) => {
@@ -417,85 +439,153 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
417439
418440 // Update generator context and load module blocks when current module changes
419441 React . useEffect ( ( ) => {
420- if ( generatorContext . current ) {
421- generatorContext . current . setModule ( currentModule ) ;
422- }
423- if ( blocksEditor . current ) {
424- blocksEditor . current . loadModuleBlocks ( currentModule , project ) ;
442+ if ( currentModule ) {
443+ if ( modulePaths . current . includes ( currentModule . modulePath ) ) {
444+ activateEditor ( ) ;
445+ } else {
446+ // Add the module path to modulePaths to create a new BlocklyComponent.
447+ modulePaths . current . push ( currentModule . modulePath ) ;
448+ }
425449 }
426450 } , [ currentModule ] ) ;
427451
428- const setupWorkspace = ( newWorkspace : Blockly . WorkspaceSvg ) => {
429- if ( ! blocklyComponent . current || ! storage ) {
452+ const activateEditor = ( ) => {
453+ if ( ! project || ! currentModule ) {
454+ return ;
455+ }
456+ for ( const modulePath in modulePathToBlocklyComponent . current ) {
457+ const blocklyComponent = modulePathToBlocklyComponent . current [ modulePath ] ;
458+ const active = ( modulePath === currentModule . modulePath ) ;
459+ const workspaceIsVisible = blocklyComponent . getBlocklyWorkspace ( ) ! . isVisible ( ) ;
460+ if ( active != workspaceIsVisible ) {
461+ blocklyComponent . setActive ( active ) ;
462+ }
463+ }
464+ if ( currentModule . modulePath in modulePathToEditor . current ) {
465+ const editor = modulePathToEditor . current [ currentModule . modulePath ] ;
466+ editor . makeCurrent ( project , modulePathToContentText ) ;
467+ }
468+ } ;
469+
470+ const setupBlocklyComponent = ( modulePath : string , newBlocklyComponent : BlocklyComponentType ) => {
471+ modulePathToBlocklyComponent . current [ modulePath ] = newBlocklyComponent ;
472+ if ( currentModule ) {
473+ newBlocklyComponent . setActive ( modulePath === currentModule . modulePath ) ;
474+ }
475+ } ;
476+
477+ const setupWorkspace = ( modulePath : string , newWorkspace : Blockly . WorkspaceSvg ) => {
478+ if ( ! project || ! storage ) {
479+ return ;
480+ }
481+ const module = storageProject . findModuleByModulePath ( project , modulePath ) ;
482+ if ( ! module ) {
483+ console . error ( "setupWorkspace called for unknown module path " + modulePath ) ;
430484 return ;
431485 }
432- // Recreate workspace when Blockly component is ready
486+
433487 ChangeFramework . setup ( newWorkspace ) ;
434488 newWorkspace . addChangeListener ( mutatorOpenListener ) ;
435489 newWorkspace . addChangeListener ( handleBlocksChanged ) ;
436490
437491 registerToolboxButton ( newWorkspace , messageApi ) ;
438492
439- generatorContext . current = createGeneratorContext ( ) ;
440-
441- if ( currentModule ) {
442- generatorContext . current . setModule ( currentModule ) ;
493+ const oldEditor = modulePathToEditor . current [ modulePath ] ;
494+ if ( oldEditor ) {
495+ oldEditor . abandon ( ) ;
443496 }
444497
445- if ( blocksEditor . current ) {
446- blocksEditor . current . abandon ( ) ;
447- }
448- blocksEditor . current = new editor . Editor ( newWorkspace , generatorContext . current , storage ) ;
449- blocksEditor . current . makeCurrent ( ) ;
498+ const newEditor = new editor . Editor (
499+ newWorkspace , module , project , storage , modulePathToContentText ) ;
500+ modulePathToEditor . current [ modulePath ] = newEditor ;
501+ newEditor . loadModuleBlocks ( ) ;
502+ newEditor . updateToolbox ( shownPythonToolboxCategories ) ;
450503
451- // Set the current module in the editor after creating it
452- if ( currentModule ) {
453- blocksEditor . current . loadModuleBlocks ( currentModule , project ) ;
504+ if ( currentModule && currentModule . modulePath === modulePath ) {
505+ activateEditor ( ) ;
454506 }
455-
456- blocksEditor . current . updateToolbox ( shownPythonToolboxCategories ) ;
457507 } ;
458508
459- // Initialize Blockly workspace and editor when component and storage are ready
509+ // Generate code when module or regeneration trigger changes
460510 React . useEffect ( ( ) => {
461- if ( ! blocklyComponent . current || ! storage ) {
462- return ;
463- }
464-
465- const blocklyWorkspace = blocklyComponent . current . getBlocklyWorkspace ( ) ;
466- if ( blocklyWorkspace ) {
467- setupWorkspace ( blocklyWorkspace ) ;
511+ let generatedCode = '' ;
512+ if ( currentModule ) {
513+ if ( currentModule . modulePath in modulePathToBlocklyComponent . current ) {
514+ const blocklyComponent = modulePathToBlocklyComponent . current [ currentModule . modulePath ] ;
515+ generatedCode = extendedPythonGenerator . mrcWorkspaceToCode (
516+ blocklyComponent . getBlocklyWorkspace ( ) , currentModule ) ;
517+ }
468518 }
469- } , [ blocklyComponent , storage ] ) ;
519+ setGeneratedCode ( generatedCode ) ;
520+ } , [ currentModule , project , triggerPythonRegeneration ] ) ;
470521
471- // Generate code when module or regeneration trigger changes
522+ // Update toolbox when categories change
472523 React . useEffect ( ( ) => {
473- if ( currentModule && blocklyComponent . current && generatorContext . current ) {
474- const blocklyWorkspace = blocklyComponent . current . getBlocklyWorkspace ( ) ;
475- setGeneratedCode ( extendedPythonGenerator . mrcWorkspaceToCode (
476- blocklyWorkspace ,
477- generatorContext . current
478- ) ) ;
479- } else {
480- setGeneratedCode ( '' ) ;
524+ if ( currentModule ) {
525+ if ( currentModule . modulePath in modulePathToEditor . current ) {
526+ const editor = modulePathToEditor . current [ currentModule . modulePath ] ;
527+ editor . updateToolbox ( shownPythonToolboxCategories ) ;
528+ }
481529 }
482- } , [ currentModule , project , triggerPythonRegeneration , blocklyComponent ] ) ;
530+ } , [ shownPythonToolboxCategories ] ) ;
483531
484- // Update toolbox when module or categories change
532+ // Fetch modules when project changes.
485533 React . useEffect ( ( ) => {
486- if ( blocksEditor . current ) {
487- blocksEditor . current . updateToolbox ( shownPythonToolboxCategories ) ;
534+ if ( project && storage ) {
535+ const fetchModules = async ( ) => {
536+ const promises : { [ modulePath : string ] : Promise < string > } = { } ; // value is promise of module content.
537+ promises [ project . robot . modulePath ] = storage . fetchFileContentText ( project . robot . modulePath ) ;
538+ project . mechanisms . forEach ( mechanism => {
539+ promises [ mechanism . modulePath ] = storage . fetchFileContentText ( mechanism . modulePath ) ;
540+ } ) ;
541+ project . opModes . forEach ( opmode => {
542+ promises [ opmode . modulePath ] = storage . fetchFileContentText ( opmode . modulePath ) ;
543+ } ) ;
544+ const updatedModulePathToContentText : { [ modulePath : string ] : string } = { } ; // value is module content text
545+ await Promise . all (
546+ Object . entries ( promises ) . map ( async ( [ modulePath , promise ] ) => {
547+ updatedModulePathToContentText [ modulePath ] = await promise ;
548+ } )
549+ ) ;
550+ const oldModulePathToContentText = modulePathToContentText ;
551+ setModulePathToContentText ( updatedModulePathToContentText ) ;
552+
553+ // Remove any deleted modules from modulePaths, modulePathToBlocklyComponent, and
554+ // modulePathToEditor. Update currentModule if the current module was deleted.
555+ for ( const modulePath in oldModulePathToContentText ) {
556+ if ( modulePath in updatedModulePathToContentText ) {
557+ continue ;
558+ }
559+ if ( currentModule && currentModule . modulePath === modulePath ) {
560+ setCurrentModule ( project . robot ) ;
561+ setActiveTab ( project . robot . modulePath ) ;
562+ }
563+ const indexToRemove : number = modulePaths . current . indexOf ( modulePath ) ;
564+ if ( indexToRemove !== - 1 ) {
565+ modulePaths . current . splice ( indexToRemove , 1 ) ;
566+ }
567+ if ( modulePath in modulePathToBlocklyComponent . current ) {
568+ delete modulePathToBlocklyComponent . current [ modulePath ] ;
569+ }
570+ if ( modulePath in modulePathToEditor . current ) {
571+ const editor = modulePathToEditor . current [ modulePath ] ;
572+ editor . abandon ( ) ;
573+ delete modulePathToEditor . current [ modulePath ] ;
574+ }
575+ }
576+ } ;
577+ fetchModules ( ) ;
488578 }
489- } , [ currentModule , shownPythonToolboxCategories ] ) ;
579+ } , [ project ] ) ;
490580
491- // Update tab items when project changes
581+ // Update tab items when fetching modules is done.
492582 React . useEffect ( ( ) => {
493583 if ( project ) {
494584 const tabs = createTabItemsFromProject ( project ) ;
495585 setTabItems ( tabs ) ;
496586 setActiveTab ( project . robot . modulePath ) ;
497587 }
498- } , [ project ] ) ;
588+ } , [ modulePathToContentText ] ) ;
499589
500590 const { Sider, Content } = Antd . Layout ;
501591
@@ -546,11 +636,15 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
546636 />
547637 < Antd . Layout >
548638 < Content >
549- < BlocklyComponent
550- theme = { theme }
551- onWorkspaceRecreated = { setupWorkspace }
552- ref = { blocklyComponent }
553- />
639+ { modulePaths . current . map ( ( modulePath ) => (
640+ < BlocklyComponent
641+ key = { modulePath }
642+ modulePath = { modulePath }
643+ onBlocklyComponentCreated = { setupBlocklyComponent }
644+ theme = { theme }
645+ onWorkspaceCreated = { setupWorkspace }
646+ />
647+ ) ) }
554648 </ Content >
555649 < Sider
556650 collapsible
0 commit comments