@@ -66,6 +66,13 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
6666
6767 private roots : vscode . Uri [ ] = [ ] ;
6868 private activeProjectRootUri : string | undefined ;
69+
70+ private logicalRoots : Map < string , string > = new Map ( ) ;
71+ private hasMultipleMods : Set < string > = new Set ( ) ;
72+
73+ public get workspaceRoots ( ) : vscode . Uri [ ] {
74+ return this . roots ;
75+ }
6976
7077 private sortRoots ( ) : void {
7178 this . roots . sort ( ( a , b ) =>
@@ -79,6 +86,17 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
7986 const stored = context . globalState . get < string [ ] > ( STORAGE_KEY , [ ] ) ;
8087 this . roots = stored . map ( ( fsPath ) => toSarcUri ( vscode . Uri . file ( fsPath ) ) ) ;
8188 this . activeProjectRootUri = context . globalState . get < string > ( 'totk-editor.activeProjectRoot' ) ;
89+
90+ const storedLogicalRoots = context . globalState . get < Record < string , string > > ( 'totk-editor.logicalRoots' , { } ) ;
91+ for ( const [ workspacePath , logicalPath ] of Object . entries ( storedLogicalRoots ) ) {
92+ this . logicalRoots . set ( workspacePath , logicalPath ) ;
93+ }
94+
95+ const storedMultipleMods = context . globalState . get < string [ ] > ( 'totk-editor.hasMultipleMods' , [ ] ) ;
96+ for ( const workspacePath of storedMultipleMods ) {
97+ this . hasMultipleMods . add ( workspacePath ) ;
98+ }
99+
82100 this . sortRoots ( ) ;
83101 }
84102
@@ -114,9 +132,9 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
114132
115133 const activeTkmmOption = getActiveTkmmOption ( this . context , projectRootPath ) ;
116134
117- return entries
135+ const children = await Promise . all ( entries
118136 . sort ( compareEntriesFoldersFirstKeepingArchivesMixed )
119- . map ( ( [ name , fileType ] ) => {
137+ . map ( async ( [ name , fileType ] ) => {
120138 const childUri = vscode . Uri . joinPath ( element . resourceUri , name ) ;
121139 const isDirectory = fileType === vscode . FileType . Directory || isArchiveFile ( name ) ;
122140
@@ -127,6 +145,29 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
127145 contextValue = 'tkmmOptionGroup' ;
128146 } else if ( element . contextValue === 'tkmmOptionGroup' && isDirectory ) {
129147 contextValue = 'tkmmOption' ;
148+ } else if ( isDirectory && this . hasMultipleMods . has ( projectRootPath ) && contextValue === 'archiveDir' ) {
149+ // Check if this directory is a valid mod folder (has romfs/exefs/.tkproj)
150+ let isModFolder = false ;
151+ try {
152+ const subEntries = await vscode . workspace . fs . readDirectory ( childUri ) ;
153+ for ( const [ subName , subType ] of subEntries ) {
154+ const lower = subName . toLowerCase ( ) ;
155+ if ( lower === 'romfs' || lower === 'exefs' || lower . endsWith ( '.tkproj' ) ) {
156+ isModFolder = true ;
157+ break ;
158+ }
159+ }
160+ } catch {
161+ // Ignore
162+ }
163+ if ( isModFolder ) {
164+ const currentLogicalRoot = this . logicalRoots . get ( projectRootPath ) ;
165+ if ( currentLogicalRoot && currentLogicalRoot === childUri . fsPath ) {
166+ contextValue = 'archiveProjectDirActive' ;
167+ } else {
168+ contextValue = 'archiveProjectDir' ;
169+ }
170+ }
130171 }
131172
132173 let isActiveOption = false ;
@@ -143,9 +184,10 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
143184 isDirectory
144185 ? vscode . TreeItemCollapsibleState . Collapsed
145186 : vscode . TreeItemCollapsibleState . None ,
146- { contextValue, isActive : isActiveOption } ,
187+ { contextValue, isActive : isActiveOption || contextValue === 'archiveProjectDirActive' } ,
147188 ) ;
148- } ) ;
189+ } ) ) ;
190+ return children ;
149191 } catch ( error ) {
150192 const message = error instanceof Error ? error . message : String ( error ) ;
151193 void vscode . window . showErrorMessage ( `TOTK Archives: ${ message } ` ) ;
@@ -185,7 +227,7 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
185227 }
186228 }
187229
188- addRoot ( fileUri : vscode . Uri ) : void {
230+ async addRoot ( fileUri : vscode . Uri ) : Promise < void > {
189231 const sarcUri = fileUri . scheme === 'sarc' ? fileUri : toSarcUri ( fileUri ) ;
190232 const key = sarcUri . fsPath ;
191233 if ( this . roots . some ( ( root ) => root . fsPath === key ) ) {
@@ -194,6 +236,37 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
194236 this . roots . push ( sarcUri ) ;
195237 this . sortRoots ( ) ;
196238 void this . persistRoots ( ) ;
239+
240+ const fileRootPath = sarcUri . fsPath ;
241+ const validMods = await findValidModFolders ( fileUri ) ;
242+ if ( validMods . length > 1 ) {
243+ this . hasMultipleMods . add ( fileRootPath ) ;
244+ const items = validMods . map ( uri => ( {
245+ label : path . basename ( uri . fsPath ) || uri . fsPath ,
246+ description : path . relative ( fileRootPath , uri . fsPath ) || 'Root' ,
247+ uri
248+ } ) ) ;
249+ const selection = await vscode . window . showQuickPick ( items , {
250+ title : `Multiple mods found in ${ path . basename ( fileRootPath ) } . Select the active project root.` ,
251+ ignoreFocusOut : true ,
252+ placeHolder : 'Select the active project root for this workspace'
253+ } ) ;
254+
255+ if ( selection ) {
256+ this . logicalRoots . set ( fileRootPath , selection . uri . fsPath ) ;
257+ } else {
258+ this . logicalRoots . set ( fileRootPath , validMods [ 0 ] ! . fsPath ) ;
259+ }
260+ } else if ( validMods . length === 1 ) {
261+ this . hasMultipleMods . delete ( fileRootPath ) ;
262+ this . logicalRoots . set ( fileRootPath , validMods [ 0 ] ! . fsPath ) ;
263+ } else {
264+ this . hasMultipleMods . delete ( fileRootPath ) ;
265+ this . logicalRoots . delete ( fileRootPath ) ;
266+ }
267+
268+ await this . persistLogicalRoots ( ) ;
269+
197270 this . onDidChangeTreeDataEmitter . fire ( undefined ) ;
198271 this . onDidChangeRootsEmitter . fire ( ) ;
199272 void this . ensureRomfsFolder ( fileUri ) ;
@@ -206,7 +279,10 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
206279 return ;
207280 }
208281 this . roots = next ;
282+ this . logicalRoots . delete ( key ) ;
283+ this . hasMultipleMods . delete ( key ) ;
209284 void this . persistRoots ( ) ;
285+ void this . persistLogicalRoots ( ) ;
210286 this . onDidChangeTreeDataEmitter . fire ( undefined ) ;
211287 this . onDidChangeRootsEmitter . fire ( ) ;
212288 }
@@ -216,24 +292,31 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
216292 }
217293
218294 getProjectRoots ( ) : { fsPath : string ; label : string } [ ] {
219- return this . roots . map ( ( root ) => ( {
220- fsPath : root . fsPath ,
221- label : path . basename ( root . fsPath ) ,
222- } ) ) ;
295+ return this . roots . map ( ( root ) => {
296+ const logicalPath = this . logicalRoots . get ( root . fsPath ) ;
297+ const activePath = logicalPath || root . fsPath ;
298+ return {
299+ fsPath : activePath ,
300+ label : path . basename ( activePath ) ,
301+ } ;
302+ } ) ;
223303 }
224304
225305 setActiveProject ( fsPath : string | undefined ) : void {
226- this . activeProjectRootUri = fsPath ;
227306 if ( fsPath ) {
228- void this . context . globalState . update ( 'totk-editor.activeProjectRoot' , fsPath ) ;
307+ const logicalPath = this . logicalRoots . get ( fsPath ) ;
308+ this . activeProjectRootUri = logicalPath || fsPath ;
309+ void this . context . globalState . update ( 'totk-editor.activeProjectRoot' , this . activeProjectRootUri ) ;
229310 } else {
311+ this . activeProjectRootUri = undefined ;
230312 void this . context . globalState . update ( 'totk-editor.activeProjectRoot' , undefined ) ;
231313 }
232314 this . onDidChangeTreeDataEmitter . fire ( undefined ) ;
233315 }
234316
235317 getActiveProject ( ) : string | undefined {
236- if ( this . activeProjectRootUri && ! this . roots . some ( ( r ) => r . fsPath === this . activeProjectRootUri ) ) {
318+ const currentProjectRoots = this . getProjectRoots ( ) . map ( r => r . fsPath ) ;
319+ if ( this . activeProjectRootUri && ! currentProjectRoots . includes ( this . activeProjectRootUri ) ) {
237320 this . setActiveProject ( undefined ) ;
238321 }
239322 return this . activeProjectRootUri ;
@@ -245,6 +328,21 @@ export class ArchiveTreeProvider implements vscode.TreeDataProvider<ArchiveTreeI
245328 this . roots . map ( ( root ) => root . fsPath ) ,
246329 ) ;
247330 }
331+
332+ private async persistLogicalRoots ( ) : Promise < void > {
333+ const mapping : Record < string , string > = { } ;
334+ for ( const [ k , v ] of this . logicalRoots . entries ( ) ) {
335+ mapping [ k ] = v ;
336+ }
337+ await this . context . globalState . update ( 'totk-editor.logicalRoots' , mapping ) ;
338+ await this . context . globalState . update ( 'totk-editor.hasMultipleMods' , Array . from ( this . hasMultipleMods ) ) ;
339+ }
340+
341+ public async setLogicalProjectRoot ( workspacePath : string , logicalPath : string ) : Promise < void > {
342+ this . logicalRoots . set ( workspacePath , logicalPath ) ;
343+ await this . persistLogicalRoots ( ) ;
344+ this . refresh ( ) ;
345+ }
248346}
249347
250348function compareEntriesFoldersFirstKeepingArchivesMixed (
@@ -286,7 +384,7 @@ export function registerArchiveTree(context: vscode.ExtensionContext): ArchiveTr
286384 } ) ,
287385 ) ;
288386
289- const addWorkspaceToArchives = ( ) : void => {
387+ const addWorkspaceToArchives = async ( ) : Promise < void > => {
290388 const folder = vscode . workspace . workspaceFolders ?. [ 0 ] ;
291389 if ( ! folder ) {
292390 void vscode . window . showWarningMessage (
@@ -300,7 +398,7 @@ export function registerArchiveTree(context: vscode.ExtensionContext): ArchiveTr
300398 ) ;
301399 return ;
302400 }
303- provider . addRoot ( folder . uri ) ;
401+ await provider . addRoot ( folder . uri ) ;
304402 void focusArchiveSidebar ( ) ;
305403 } ;
306404
@@ -314,7 +412,7 @@ export function registerArchiveTree(context: vscode.ExtensionContext): ArchiveTr
314412 } ) ;
315413 if ( uris && uris . length > 0 ) {
316414 for ( const uri of uris ) {
317- provider . addRoot ( uri ) ;
415+ await provider . addRoot ( uri ) ;
318416 }
319417 void focusArchiveSidebar ( ) ;
320418 }
@@ -347,6 +445,16 @@ export function registerArchiveTree(context: vscode.ExtensionContext): ArchiveTr
347445 }
348446 }
349447 ) ,
448+ vscode . commands . registerCommand (
449+ 'totk-editor.setLogicalProjectRoot' ,
450+ async ( item : ArchiveTreeItem | undefined ) => {
451+ if ( ! item ) return ;
452+ const workspaceRoot = provider . workspaceRoots . find ( r => item . resourceUri . fsPath . startsWith ( r . fsPath ) ) ;
453+ if ( workspaceRoot ) {
454+ await provider . setLogicalProjectRoot ( workspaceRoot . fsPath , item . resourceUri . fsPath ) ;
455+ }
456+ }
457+ ) ,
350458 vscode . commands . registerCommand (
351459 'totk-editor.createOptionGroup' ,
352460 async ( item : ArchiveTreeItem | undefined ) => {
@@ -437,7 +545,7 @@ export async function migrateSarcWorkspaceFolders(
437545 for ( let i = 0 ; i < folders . length ; i ++ ) {
438546 const folder = folders [ i ] ! ;
439547 if ( folder . uri . scheme === 'sarc' ) {
440- archiveTree . addRoot ( vscode . Uri . file ( folder . uri . fsPath ) ) ;
548+ await archiveTree . addRoot ( vscode . Uri . file ( folder . uri . fsPath ) ) ;
441549 toConvert . push ( {
442550 index : i ,
443551 uri : vscode . Uri . file ( folder . uri . fsPath ) ,
@@ -450,15 +558,59 @@ export async function migrateSarcWorkspaceFolders(
450558 return ;
451559 }
452560
453- for ( let i = toConvert . length - 1 ; i >= 0 ; i -- ) {
454- const entry = toConvert [ i ] ! ;
455- await vscode . workspace . updateWorkspaceFolders ( entry . index , 1 , {
456- uri : entry . uri ,
457- name : entry . name ,
458- } ) ;
561+ for ( let i = toConvert . length - 0 ; i >= 0 ; i -- ) {
562+ const entry = toConvert [ i ] ;
563+ if ( entry ) {
564+ await vscode . workspace . updateWorkspaceFolders ( entry . index , 1 , {
565+ uri : entry . uri ,
566+ name : entry . name ,
567+ } ) ;
568+ }
459569 }
460570
461571 void vscode . window . showInformationMessage (
462572 'TOTK Editor: Archive browsing moved to the **TOTK Archives** sidebar tab. Your workspace uses normal files again.' ,
463573 ) ;
464574}
575+
576+ async function findValidModFolders ( rootUri : vscode . Uri , maxDepth : number = 3 ) : Promise < vscode . Uri [ ] > {
577+ const validFolders : vscode . Uri [ ] = [ ] ;
578+
579+ async function scan ( currentUri : vscode . Uri , depth : number ) {
580+ if ( depth > maxDepth ) return ;
581+
582+ try {
583+ const entries = await vscode . workspace . fs . readDirectory ( currentUri ) ;
584+
585+ let isModFolder = false ;
586+ let hasSubDirs = false ;
587+ const subdirs : string [ ] = [ ] ;
588+
589+ for ( const [ name , type ] of entries ) {
590+ const lowerName = name . toLowerCase ( ) ;
591+ if ( lowerName === 'romfs' || lowerName === 'exefs' || lowerName . endsWith ( '.tkproj' ) ) {
592+ isModFolder = true ;
593+ }
594+ if ( type === vscode . FileType . Directory ) {
595+ hasSubDirs = true ;
596+ if ( lowerName !== 'romfs' && lowerName !== 'exefs' && lowerName !== 'options' ) {
597+ subdirs . push ( name ) ;
598+ }
599+ }
600+ }
601+
602+ if ( isModFolder ) {
603+ validFolders . push ( currentUri ) ;
604+ } else if ( hasSubDirs ) {
605+ for ( const subdir of subdirs ) {
606+ await scan ( vscode . Uri . joinPath ( currentUri , subdir ) , depth + 1 ) ;
607+ }
608+ }
609+ } catch {
610+ // Ignore permissions/read errors
611+ }
612+ }
613+
614+ await scan ( rootUri , 1 ) ;
615+ return validFolders ;
616+ }
0 commit comments