@@ -541,6 +541,320 @@ export function LandscapeEditor({ landscape, onLandscapeUpdated, onError }: Land
541541 }
542542 } ;
543543
544+ const movePackage = (
545+ sourceAppIdx : number ,
546+ sourcePackageName : string ,
547+ targetAppIdx : number ,
548+ targetPackageName : string | null
549+ ) => {
550+ const updated = [ ...localLandscape ] ;
551+ const sourceApp = updated [ sourceAppIdx ] ;
552+ const targetApp = updated [ targetAppIdx ] ;
553+
554+ if ( ! sourceApp || ! targetApp ) {
555+ onError ( 'Invalid app index' ) ;
556+ return ;
557+ }
558+
559+ let sourcePackage : CleanedPackage | null = null ;
560+ let sourceParent : CleanedPackage | null = null ;
561+ let sourceParentArray : CleanedPackage [ ] | null = null ;
562+
563+ // Find source package and its parent
564+ const findSource = ( pkg : CleanedPackage , parent : CleanedPackage | null , parentArray : CleanedPackage [ ] ) : boolean => {
565+ if ( pkg . name === sourcePackageName ) {
566+ sourcePackage = pkg ;
567+ sourceParent = parent ;
568+ sourceParentArray = parentArray ;
569+ return true ;
570+ }
571+ for ( let i = 0 ; i < pkg . subpackages . length ; i ++ ) {
572+ if ( findSource ( pkg . subpackages [ i ] , pkg , pkg . subpackages ) ) {
573+ return true ;
574+ }
575+ }
576+ return false ;
577+ } ;
578+
579+ for ( let i = 0 ; i < sourceApp . rootPackages . length ; i ++ ) {
580+ if ( findSource ( sourceApp . rootPackages [ i ] , null , sourceApp . rootPackages ) ) {
581+ break ;
582+ }
583+ }
584+
585+ if ( ! sourcePackage || ! sourceParentArray ) {
586+ onError ( `Source package "${ sourcePackageName } " not found` ) ;
587+ return ;
588+ }
589+
590+ // Store references after null check for proper type narrowing
591+ const packageToMove = sourcePackage ;
592+ const parentArray : CleanedPackage [ ] = sourceParentArray ;
593+
594+ // Prevent moving package into itself or its descendants (only if same app)
595+ if ( sourceAppIdx === targetAppIdx && targetPackageName ) {
596+ const targetPkg = findPackage ( targetApp , targetPackageName ) ;
597+ if ( targetPkg ) {
598+ // Check if targetPkg is the source package itself
599+ if ( targetPkg === packageToMove ) {
600+ onError ( 'Cannot move package into itself' ) ;
601+ return ;
602+ }
603+ // Check if targetPkg is a descendant (subpackage) of the source package
604+ // We need to traverse the source package's subpackages to see if targetPkg is found
605+ const isDescendantOfSource = ( pkg : CleanedPackage ) : boolean => {
606+ if ( pkg === targetPkg ) return true ;
607+ return pkg . subpackages . some ( isDescendantOfSource ) ;
608+ } ;
609+ if ( isDescendantOfSource ( packageToMove ) ) {
610+ onError ( 'Cannot move package into its own subpackage' ) ;
611+ return ;
612+ }
613+ }
614+ }
615+
616+ // Remove from source
617+ const sourceIndex = parentArray . findIndex ( ( p : CleanedPackage ) => p . name === sourcePackageName ) ;
618+ if ( sourceIndex === - 1 ) {
619+ onError ( `Source package "${ sourcePackageName } " not found in parent` ) ;
620+ return ;
621+ }
622+ parentArray . splice ( sourceIndex , 1 ) ;
623+
624+ // Add to target
625+ if ( targetPackageName === null ) {
626+ // Move to root of target app
627+ targetApp . rootPackages . push ( packageToMove ) ;
628+ } else {
629+ const targetPkg = findPackage ( targetApp , targetPackageName ) ;
630+ if ( ! targetPkg ) {
631+ onError ( `Target package "${ targetPackageName } " not found` ) ;
632+ return ;
633+ }
634+ targetPkg . subpackages . push ( packageToMove ) ;
635+ }
636+
637+ // Update the tree structure for source app
638+ const updateSourceTree = ( p : CleanedPackage ) : CleanedPackage => {
639+ if ( sourceParent && p . name === sourceParent . name ) {
640+ return { ...p , subpackages : [ ...p . subpackages ] } ;
641+ }
642+ return { ...p , subpackages : p . subpackages . map ( updateSourceTree ) } ;
643+ } ;
644+
645+ updated [ sourceAppIdx ] = {
646+ ...sourceApp ,
647+ rootPackages : sourceApp . rootPackages . map ( updateSourceTree ) ,
648+ } ;
649+
650+ // Update the tree structure for target app
651+ if ( targetPackageName ) {
652+ const updateTargetTree = ( p : CleanedPackage ) : CleanedPackage => {
653+ if ( p . name === targetPackageName ) {
654+ return { ...p , subpackages : [ ...p . subpackages ] } ;
655+ }
656+ return { ...p , subpackages : p . subpackages . map ( updateTargetTree ) } ;
657+ } ;
658+ updated [ targetAppIdx ] = {
659+ ...targetApp ,
660+ rootPackages : targetApp . rootPackages . map ( updateTargetTree ) ,
661+ } ;
662+ }
663+
664+ updateLocalLandscape ( updated ) ;
665+ } ;
666+
667+ const moveClass = ( sourceAppIdx : number , className : string , targetAppIdx : number , targetPackageName : string ) => {
668+ const updated = [ ...localLandscape ] ;
669+ const sourceApp = updated [ sourceAppIdx ] ;
670+ const targetApp = updated [ targetAppIdx ] ;
671+
672+ if ( ! sourceApp || ! targetApp ) {
673+ onError ( 'Invalid app index' ) ;
674+ return ;
675+ }
676+
677+ const sourceClass = findClass ( sourceApp , className ) ;
678+ if ( ! sourceClass ) {
679+ onError ( `Class "${ className } " not found` ) ;
680+ return ;
681+ }
682+
683+ const targetPkg = findPackage ( targetApp , targetPackageName ) ;
684+ if ( ! targetPkg ) {
685+ onError ( `Target package "${ targetPackageName } " not found` ) ;
686+ return ;
687+ }
688+
689+ // Update parentAppName if moving to different app
690+ const updatedClass =
691+ sourceAppIdx !== targetAppIdx ? { ...sourceClass , parentAppName : targetApp . name } : sourceClass ;
692+
693+ // Remove from source package and add to target package
694+ const removeClass = ( p : CleanedPackage ) : CleanedPackage => ( {
695+ ...p ,
696+ classes : p . classes . filter ( ( c ) => c . identifier !== className ) ,
697+ subpackages : p . subpackages . map ( removeClass ) ,
698+ } ) ;
699+
700+ // Update target app tree structure - add class to target package
701+ const updateTargetTree = ( p : CleanedPackage ) : CleanedPackage => {
702+ if ( p . name === targetPackageName ) {
703+ return { ...p , classes : [ ...p . classes , updatedClass ] } ;
704+ }
705+ return { ...p , subpackages : p . subpackages . map ( updateTargetTree ) } ;
706+ } ;
707+
708+ if ( sourceAppIdx === targetAppIdx ) {
709+ // Same app: combine both operations
710+ const combinedUpdate = ( p : CleanedPackage ) : CleanedPackage => {
711+ // First remove the class
712+ let updated = {
713+ ...p ,
714+ classes : p . classes . filter ( ( c ) => c . identifier !== className ) ,
715+ subpackages : p . subpackages . map ( combinedUpdate ) ,
716+ } ;
717+ // Then add to target if this is the target package
718+ if ( p . name === targetPackageName ) {
719+ updated = { ...updated , classes : [ ...updated . classes , updatedClass ] } ;
720+ }
721+ return updated ;
722+ } ;
723+
724+ updated [ sourceAppIdx ] = {
725+ ...sourceApp ,
726+ rootPackages : sourceApp . rootPackages . map ( combinedUpdate ) ,
727+ classes : sourceApp . classes . filter ( ( c ) => c . identifier !== className ) ,
728+ } ;
729+ } else {
730+ // Different apps: update separately
731+ updated [ sourceAppIdx ] = {
732+ ...sourceApp ,
733+ rootPackages : sourceApp . rootPackages . map ( removeClass ) ,
734+ classes : sourceApp . classes . filter ( ( c ) => c . identifier !== className ) ,
735+ } ;
736+
737+ updated [ targetAppIdx ] = {
738+ ...targetApp ,
739+ rootPackages : targetApp . rootPackages . map ( updateTargetTree ) ,
740+ classes : [ ...targetApp . classes , updatedClass ] ,
741+ } ;
742+ }
743+
744+ updateLocalLandscape ( updated ) ;
745+ } ;
746+
747+ const moveMethod = (
748+ sourceAppIdx : number ,
749+ className : string ,
750+ methodName : string ,
751+ targetAppIdx : number ,
752+ targetClassName : string
753+ ) => {
754+ const updated = [ ...localLandscape ] ;
755+ const sourceApp = updated [ sourceAppIdx ] ;
756+ const targetApp = updated [ targetAppIdx ] ;
757+
758+ if ( ! sourceApp || ! targetApp ) {
759+ onError ( 'Invalid app index' ) ;
760+ return ;
761+ }
762+
763+ const sourceClass = findClass ( sourceApp , className ) ;
764+ const targetClass = findClass ( targetApp , targetClassName ) ;
765+
766+ if ( ! sourceClass ) {
767+ onError ( `Source class "${ className } " not found` ) ;
768+ return ;
769+ }
770+ if ( ! targetClass ) {
771+ onError ( `Target class "${ targetClassName } " not found` ) ;
772+ return ;
773+ }
774+
775+ const method = sourceClass . methods . find ( ( m ) => m . identifier === methodName ) ;
776+ if ( ! method ) {
777+ onError ( `Method "${ methodName } " not found in class "${ className } "` ) ;
778+ return ;
779+ }
780+
781+ // Remove from source class
782+ const removeMethod = ( p : CleanedPackage ) : CleanedPackage => ( {
783+ ...p ,
784+ classes : p . classes . map ( ( c ) =>
785+ c . identifier === className
786+ ? {
787+ ...c ,
788+ methods : c . methods . filter ( ( m ) => m . identifier !== methodName ) ,
789+ }
790+ : c
791+ ) ,
792+ subpackages : p . subpackages . map ( removeMethod ) ,
793+ } ) ;
794+
795+ // Add to target class
796+ const addMethod = ( p : CleanedPackage ) : CleanedPackage => ( {
797+ ...p ,
798+ classes : p . classes . map ( ( c ) =>
799+ c . identifier === targetClassName
800+ ? {
801+ ...c ,
802+ methods : [ ...c . methods , method ] ,
803+ }
804+ : c
805+ ) ,
806+ subpackages : p . subpackages . map ( addMethod ) ,
807+ } ) ;
808+
809+ if ( sourceAppIdx === targetAppIdx ) {
810+ // Same app: combine both operations
811+ updated [ sourceAppIdx ] = {
812+ ...sourceApp ,
813+ rootPackages : sourceApp . rootPackages . map ( ( pkg ) => {
814+ const afterRemove = removeMethod ( pkg ) ;
815+ return addMethod ( afterRemove ) ;
816+ } ) ,
817+ classes : sourceApp . classes . map ( ( c ) => {
818+ if ( c . identifier === className ) {
819+ return { ...c , methods : c . methods . filter ( ( m ) => m . identifier !== methodName ) } ;
820+ }
821+ if ( c . identifier === targetClassName ) {
822+ return { ...c , methods : [ ...c . methods , method ] } ;
823+ }
824+ return c ;
825+ } ) ,
826+ methods : sourceApp . methods . map ( ( m ) => ( m . identifier === methodName ? method : m ) ) ,
827+ } ;
828+ } else {
829+ // Different apps: update separately
830+ updated [ sourceAppIdx ] = {
831+ ...sourceApp ,
832+ rootPackages : sourceApp . rootPackages . map ( removeMethod ) ,
833+ classes : sourceApp . classes . map ( ( c ) => {
834+ if ( c . identifier === className ) {
835+ return { ...c , methods : c . methods . filter ( ( m ) => m . identifier !== methodName ) } ;
836+ }
837+ return c ;
838+ } ) ,
839+ methods : sourceApp . methods . filter ( ( m ) => m . identifier !== methodName ) ,
840+ } ;
841+
842+ updated [ targetAppIdx ] = {
843+ ...targetApp ,
844+ rootPackages : targetApp . rootPackages . map ( addMethod ) ,
845+ classes : targetApp . classes . map ( ( c ) => {
846+ if ( c . identifier === targetClassName ) {
847+ return { ...c , methods : [ ...c . methods , method ] } ;
848+ }
849+ return c ;
850+ } ) ,
851+ methods : [ ...targetApp . methods , method ] ,
852+ } ;
853+ }
854+
855+ updateLocalLandscape ( updated ) ;
856+ } ;
857+
544858 const addApp = ( ) => {
545859 const appName = prompt ( 'Enter app name:' , 'newapp' ) ;
546860 if ( appName && appName . trim ( ) !== '' ) {
@@ -595,6 +909,9 @@ export function LandscapeEditor({ landscape, onLandscapeUpdated, onError }: Land
595909 deletePackage,
596910 deleteClass,
597911 deleteMethod,
912+ movePackage,
913+ moveClass,
914+ moveMethod,
598915 } ;
599916
600917 return (
0 commit comments