@@ -28,6 +28,9 @@ export class ProjectProvider implements vscode.TreeDataProvider<LibertyProject>
2828
2929 // Map of buildFilePath -> LibertyProject
3030 private projects : Map < string , LibertyProject > = new Map ( ) ;
31+
32+ // Root projects for hierarchical tree view
33+ private rootProjects : LibertyProject [ ] = [ ] ;
3134
3235 private _context : vscode . ExtensionContext ;
3336
@@ -253,7 +256,104 @@ export class ProjectProvider implements vscode.TreeDataProvider<LibertyProject>
253256 }
254257
255258 public getTreeItem ( element : LibertyProject ) : vscode . TreeItem {
256- return element ;
259+ // Set collapsible state based on whether project has children
260+ const collapsibleState = ( element . isAggregator && element . children . length > 0 )
261+ ? vscode . TreeItemCollapsibleState . Collapsed
262+ : vscode . TreeItemCollapsibleState . None ;
263+
264+ // Update the element's collapsible state by creating a new TreeItem
265+ const treeItem = new vscode . TreeItem ( element . label , collapsibleState ) ;
266+ treeItem . tooltip = element . tooltip ;
267+ treeItem . iconPath = element . iconPath ;
268+ treeItem . contextValue = element . contextValue ;
269+ treeItem . command = element . command ;
270+
271+ return treeItem ;
272+ }
273+
274+ /**
275+ * Check if a project has any Liberty-enabled descendants
276+ * @param project The project to check
277+ * @returns true if project or any descendant is Liberty-enabled
278+ */
279+ private hasLibertyDescendants ( project : LibertyProject ) : boolean {
280+ if ( project . isLibertyEnabled ) {
281+ return true ;
282+ }
283+ return project . children . some ( child => this . hasLibertyDescendants ( child ) ) ;
284+ }
285+
286+ /**
287+ * Find all Liberty-enabled descendants of a project
288+ * @param project The project to search
289+ * @returns Array of Liberty-enabled descendant projects
290+ */
291+ public findLibertyDescendants ( project : LibertyProject ) : LibertyProject [ ] {
292+ const descendants : LibertyProject [ ] = [ ] ;
293+
294+ // Only search children - don't include the project itself
295+ // This ensures aggregators aren't included in their own descendant list
296+ for ( const child of project . children ) {
297+ // Add the child if it's Liberty-enabled
298+ if ( child . isLibertyEnabled ) {
299+ descendants . push ( child ) ;
300+ }
301+ // Recursively search the child's descendants
302+ descendants . push ( ...this . findLibertyDescendants ( child ) ) ;
303+ }
304+
305+ return descendants ;
306+ }
307+
308+ /**
309+ * Execute a command on a project, with delegation to children if it's an aggregator
310+ * @param project The project to execute the command on
311+ * @param commandName The command name for display purposes
312+ * @returns The target project to execute on, or undefined if cancelled
313+ */
314+ public async resolveCommandTarget ( project : LibertyProject , commandName : string ) : Promise < LibertyProject | undefined > {
315+ // Check if project is an aggregator FIRST (before checking if Liberty-enabled)
316+ // An aggregator with children should delegate to children, not execute directly
317+ if ( project . isAggregator && project . children . length > 0 ) {
318+ const libertyChildren = this . findLibertyDescendants ( project ) ;
319+
320+ if ( libertyChildren . length === 0 ) {
321+ vscode . window . showWarningMessage (
322+ localize ( "no.liberty.modules.found" , `No Liberty-enabled modules found in ${ project . label } ` )
323+ ) ;
324+ return undefined ;
325+ }
326+
327+ if ( libertyChildren . length === 1 ) {
328+ // Only one Liberty child, execute automatically
329+ return libertyChildren [ 0 ] ;
330+ }
331+
332+ // Multiple children - show notification with buttons
333+ const buttonLabels = libertyChildren . map ( c => c . label ) ;
334+ const selected = await vscode . window . showInformationMessage (
335+ localize ( "select.module.for.command" , commandName ) ,
336+ ...buttonLabels
337+ ) ;
338+
339+ if ( ! selected ) {
340+ return undefined ;
341+ }
342+
343+ // Find the project that matches the selected button
344+ return libertyChildren . find ( c => c . label === selected ) ;
345+ }
346+
347+ // If we get here, check if it's a Liberty-enabled leaf project (not an aggregator)
348+ if ( project . isLibertyEnabled ) {
349+ return project ;
350+ }
351+
352+ // Not Liberty-enabled and not an aggregator
353+ vscode . window . showWarningMessage (
354+ localize ( "project.not.liberty.enabled" , `${ project . label } is not Liberty-enabled` )
355+ ) ;
356+ return undefined ;
257357 }
258358
259359 // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -264,12 +364,17 @@ export class ProjectProvider implements vscode.TreeDataProvider<LibertyProject>
264364 }
265365 // if element is null, vscode is asking for the root node
266366 if ( element === undefined ) {
267- // projects is a map of buildFilePath -> LibertyProjects
268- // Need to return an array of just the LibertyProject
367+ // Return root projects for hierarchical view
368+ // If no hierarchy is built, fall back to flat list
369+ if ( this . rootProjects . length > 0 ) {
370+ return this . rootProjects ;
371+ }
269372 return [ ... this . projects . values ( ) ] ;
270373 }
271- // else it is asking for a child node
272- return [ ] ;
374+ // Return children that are Liberty-enabled OR have Liberty descendants
375+ return element . children . filter ( child =>
376+ child . isLibertyEnabled || this . hasLibertyDescendants ( child )
377+ ) ;
273378 }
274379
275380
@@ -401,6 +506,63 @@ export class ProjectProvider implements vscode.TreeDataProvider<LibertyProject>
401506 }
402507 return added ;
403508 }
509+ /**
510+ * Build hierarchical relationships between projects based on metadata
511+ * @param projectsMap Map of all projects
512+ */
513+ private async buildHierarchy ( projectsMap : Map < string , LibertyProject > ) : Promise < void > {
514+ const mavenProjectsByArtifactId = new Map < string , LibertyProject > ( ) ;
515+ const gradleProjectsByName = new Map < string , LibertyProject > ( ) ;
516+
517+ // First pass: populate metadata for all projects
518+ for ( const [ buildFilePath , project ] of projectsMap . entries ( ) ) {
519+ try {
520+ if ( buildFilePath . endsWith ( "pom.xml" ) ) {
521+ const xmlString = await fse . readFile ( buildFilePath , "utf8" ) ;
522+ const metadata = await mavenUtil . extractMavenMetadata ( buildFilePath , xmlString ) ;
523+ project . artifactId = metadata . artifactId ;
524+ project . parentArtifactId = metadata . parentArtifactId ;
525+ project . isAggregator = metadata . isAggregator ;
526+ project . isLibertyEnabled = metadata . isLibertyEnabled ;
527+ mavenProjectsByArtifactId . set ( metadata . artifactId , project ) ;
528+ } else if ( buildFilePath . endsWith ( "build.gradle" ) ) {
529+ const metadata = await gradleUtil . extractGradleMetadata ( buildFilePath ) ;
530+ project . artifactId = metadata . projectName ;
531+ project . parentArtifactId = metadata . parentProjectName ;
532+ project . isAggregator = metadata . isAggregator ;
533+ project . isLibertyEnabled = metadata . isLibertyEnabled ;
534+ gradleProjectsByName . set ( metadata . projectName , project ) ;
535+ }
536+ } catch ( error ) {
537+ console . error ( `Error extracting metadata for ${ buildFilePath } :` , error ) ;
538+ // Set defaults if metadata extraction fails
539+ project . isLibertyEnabled = true ; // Assume Liberty-enabled if in the map
540+ }
541+ }
542+
543+ // Second pass: build parent-child relationships
544+ for ( const project of projectsMap . values ( ) ) {
545+ if ( project . parentArtifactId ) {
546+ // Look up parent in the appropriate map based on build tool
547+ const parent = project . path . endsWith ( "pom.xml" )
548+ ? mavenProjectsByArtifactId . get ( project . parentArtifactId )
549+ : gradleProjectsByName . get ( project . parentArtifactId ) ;
550+
551+ if ( parent ) {
552+ project . parent = parent ;
553+ if ( ! parent . children . includes ( project ) ) {
554+ parent . children . push ( project ) ;
555+ }
556+ }
557+ }
558+ }
559+
560+ // Third pass: identify root projects (no parent or parent not in workspace)
561+ this . rootProjects = Array . from ( projectsMap . values ( ) )
562+ . filter ( p => ! p . parent )
563+ . filter ( p => p . isLibertyEnabled || this . hasLibertyDescendants ( p ) ) ;
564+ }
565+
404566
405567 private async updateProjects ( ) : Promise < void > {
406568 // find all build files in the open workspace and find all the ones that are valid for dev-mode
@@ -425,12 +587,14 @@ export class ProjectProvider implements vscode.TreeDataProvider<LibertyProject>
425587 }
426588
427589 for ( const gradleBuild of validGradleBuilds ) {
590+ console . log ( `[DEBUG] Processing Gradle build: ${ gradleBuild . getBuildFilePath ( ) } , type: ${ gradleBuild . getProjectType ( ) } ` ) ;
428591 // if a LibertyProject for this build.gradle has already been created
429592 // we want to re-use it
430593 if ( ! await this . addExistingProjectToNewProjectsMap ( gradleBuild . getBuildFilePath ( ) , gradleBuild . getProjectType ( ) ,
431594 newProjectsMap ) ) {
432595 const project = await createProject ( this . _context , gradleBuild . getBuildFilePath ( ) , gradleBuild . getProjectType ( ) ) ;
433596 newProjectsMap . set ( gradleBuild . getBuildFilePath ( ) , project ) ;
597+ console . log ( `[DEBUG] Added Gradle project to map: ${ gradleBuild . getBuildFilePath ( ) } ` ) ;
434598 }
435599 }
436600
@@ -471,11 +635,32 @@ export class ProjectProvider implements vscode.TreeDataProvider<LibertyProject>
471635 }
472636 }
473637 }
638+ console . log ( `[DEBUG] Total projects in map: ${ newProjectsMap . size } ` ) ;
639+ for ( const [ path , proj ] of newProjectsMap . entries ( ) ) {
640+ console . log ( `[DEBUG] Project: ${ proj . label } at ${ path } ` ) ;
641+ }
642+
474643 this . projects = newProjectsMap ;
644+
645+ // Build hierarchical relationships between projects
646+ await this . buildHierarchy ( newProjectsMap ) ;
647+
648+ console . log ( `[DEBUG] Root projects after hierarchy: ${ this . rootProjects . length } ` ) ;
649+ for ( const root of this . rootProjects ) {
650+ console . log ( `[DEBUG] Root: ${ root . label } , isAggregator: ${ root . isAggregator } , isLibertyEnabled: ${ root . isLibertyEnabled } , children: ${ root . children . length } ` ) ;
651+ }
475652 }
476653}
477654
478655export class LibertyProject extends vscode . TreeItem {
656+ // New fields for multi-module hierarchy support
657+ public parent ?: LibertyProject ;
658+ public children : LibertyProject [ ] = [ ] ;
659+ public isAggregator : boolean = false ;
660+ public isLibertyEnabled : boolean = false ;
661+ public artifactId : string = "" ;
662+ public parentArtifactId ?: string ;
663+
479664 constructor (
480665 private _context : vscode . ExtensionContext ,
481666 public label : string ,
@@ -491,6 +676,7 @@ export class LibertyProject extends vscode.TreeItem {
491676 ) {
492677 super ( label , collapsibleState ) ;
493678 this . tooltip = this . path ;
679+ this . children = [ ] ;
494680 }
495681
496682 private EXPLORER_ICON = this . setExplorerIcon ( ) ;
0 commit comments