@@ -26,6 +26,27 @@ type FolderChoiceOptions = {
2626 topItems ?: Array < { path : string ; label : string } > ;
2727} ;
2828
29+ type FolderSelectionContext = {
30+ items : string [ ] ;
31+ displayItems : string [ ] ;
32+ normalizedItems : string [ ] ;
33+ canonicalByNormalized : Map < string , string > ;
34+ displayByNormalized : Map < string , string > ;
35+ existingSet : Set < string > ;
36+ allowCreate : boolean ;
37+ allowedRoots : string [ ] ;
38+ placeholder ?: string ;
39+ } ;
40+
41+ type FolderSelection = {
42+ raw : string ;
43+ normalized : string ;
44+ resolved : string ;
45+ exists : boolean ;
46+ isAllowed : boolean ;
47+ isEmpty : boolean ;
48+ } ;
49+
2950function isMacroAbortError ( error : unknown ) : error is MacroAbortError {
3051 return (
3152 error instanceof MacroAbortError ||
@@ -59,10 +80,42 @@ export abstract class TemplateEngine extends QuickAddEngine {
5980 folders : string [ ] ,
6081 options : FolderChoiceOptions = { } ,
6182 ) : Promise < string > {
83+ const context = this . buildFolderSelectionContext ( folders , options ) ;
84+
85+ if ( ! this . shouldPromptForFolder ( context ) ) {
86+ return await this . handleSingleSelection ( context ) ;
87+ }
88+
89+ while ( true ) {
90+ const raw = await this . promptForFolder ( context ) ;
91+ const selection = await this . resolveSelection ( raw , context ) ;
92+
93+ if ( selection . isEmpty ) {
94+ if ( ! selection . isAllowed ) {
95+ this . showFolderNotAllowedNotice ( context . allowedRoots ) ;
96+ continue ;
97+ }
98+ return "" ;
99+ }
100+
101+ if ( ! selection . isAllowed ) {
102+ this . showFolderNotAllowedNotice ( context . allowedRoots ) ;
103+ continue ;
104+ }
105+
106+ await this . ensureFolderExists ( selection ) ;
107+ return selection . resolved ;
108+ }
109+ }
110+
111+ private buildFolderSelectionContext (
112+ folders : string [ ] ,
113+ options : FolderChoiceOptions ,
114+ ) : FolderSelectionContext {
62115 const allowCreate = options . allowCreate ?? false ;
63- const normalizedAllowedRoots = options . allowedRoots ?. map ( ( root ) =>
64- this . normalizeFolderPath ( root ) ,
65- ) ;
116+ const allowedRoots =
117+ options . allowedRoots ?. map ( ( root ) => this . normalizeFolderPath ( root ) ) ?? [ ] ;
118+
66119 const {
67120 items,
68121 displayItems,
@@ -72,108 +125,113 @@ export abstract class TemplateEngine extends QuickAddEngine {
72125 } = this . buildFolderSuggestions (
73126 folders ,
74127 options . topItems ?? [ ] ,
75- normalizedAllowedRoots ,
128+ allowedRoots . length > 0 ? allowedRoots : undefined ,
76129 ) ;
77130
78- const existingSet = new Set ( normalizedItems ) ;
79- let folderPath = "" ;
80-
81- if ( items . length > 1 || ( allowCreate && items . length === 0 ) ) {
82- while ( true ) {
83- try {
84- if ( allowCreate ) {
85- folderPath = await InputSuggester . Suggest (
86- this . app ,
87- displayItems ,
88- items ,
89- {
90- placeholder :
91- options . placeholder ??
92- "Choose a folder or type to create one" ,
93- renderItem : ( item , el ) => {
94- this . renderFolderSuggestion (
95- item ,
96- el ,
97- existingSet ,
98- displayByNormalized ,
99- ) ;
100- } ,
101- } ,
102- ) ;
103- } else {
104- folderPath = await GenericSuggester . Suggest (
105- this . app ,
106- displayItems ,
107- items ,
108- options . placeholder ,
109- ) ;
110- }
111- if ( ! folderPath ) throw new Error ( "No folder selected." ) ;
112- } catch ( error ) {
113- // Always abort on cancelled input
114- if ( isCancellationError ( error ) ) {
115- throw new MacroAbortError ( "Input cancelled by user" ) ;
116- }
117- throw error ;
118- }
119-
120- const normalized = this . normalizeFolderPath ( folderPath ) ;
121- if ( ! normalized ) {
122- if (
123- normalizedAllowedRoots &&
124- normalizedAllowedRoots . length > 0 &&
125- ! this . isPathAllowed ( "" , normalizedAllowedRoots )
126- ) {
127- this . showFolderNotAllowedNotice ( normalizedAllowedRoots ) ;
128- continue ;
129- }
130- return "" ;
131- }
132-
133- const canonical = canonicalByNormalized . get ( normalized ) ;
134- const resolved = canonical ?? normalized ;
131+ return {
132+ items,
133+ displayItems,
134+ normalizedItems,
135+ canonicalByNormalized,
136+ displayByNormalized,
137+ existingSet : new Set ( normalizedItems ) ,
138+ allowCreate,
139+ allowedRoots,
140+ placeholder : options . placeholder ,
141+ } ;
142+ }
135143
136- if ( ! allowCreate ) {
137- if ( resolved ) await this . createFolder ( resolved ) ;
138- return resolved ;
139- }
144+ private shouldPromptForFolder ( context : FolderSelectionContext ) : boolean {
145+ return (
146+ context . items . length > 1 ||
147+ ( context . allowCreate && context . items . length === 0 )
148+ ) ;
149+ }
140150
141- const exists =
142- canonical !== undefined ||
143- ( await this . app . vault . adapter . exists ( resolved ) ) ;
144- if ( exists ) return resolved ;
145-
146- if (
147- normalizedAllowedRoots &&
148- normalizedAllowedRoots . length > 0 &&
149- ! this . isPathAllowed ( resolved , normalizedAllowedRoots )
150- ) {
151- this . showFolderNotAllowedNotice ( normalizedAllowedRoots ) ;
152- continue ;
153- }
151+ private async promptForFolder ( context : FolderSelectionContext ) : Promise < string > {
152+ try {
153+ if ( context . allowCreate ) {
154+ return await InputSuggester . Suggest (
155+ this . app ,
156+ context . displayItems ,
157+ context . items ,
158+ {
159+ placeholder :
160+ context . placeholder ?? "Choose a folder or type to create one" ,
161+ renderItem : ( item , el ) => {
162+ this . renderFolderSuggestion (
163+ item ,
164+ el ,
165+ context . existingSet ,
166+ context . displayByNormalized ,
167+ ) ;
168+ } ,
169+ } ,
170+ ) ;
171+ }
154172
155- await this . createFolder ( resolved ) ;
156- return resolved ;
173+ return await GenericSuggester . Suggest (
174+ this . app ,
175+ context . displayItems ,
176+ context . items ,
177+ context . placeholder ,
178+ ) ;
179+ } catch ( error ) {
180+ if ( isCancellationError ( error ) ) {
181+ throw new MacroAbortError ( "Input cancelled by user" ) ;
157182 }
183+ throw error ;
158184 }
185+ }
159186
160- folderPath = items [ 0 ] ?? "" ;
187+ private async resolveSelection (
188+ raw : string ,
189+ context : FolderSelectionContext ,
190+ ) : Promise < FolderSelection > {
191+ const normalized = this . normalizeFolderPath ( raw ) ;
192+ const isEmpty = normalized . length === 0 ;
193+ const canonical = context . canonicalByNormalized . get ( normalized ) ;
194+ const resolved = canonical ?? normalized ;
195+
196+ const exists = isEmpty
197+ ? false
198+ : canonical !== undefined ||
199+ ( await this . app . vault . adapter . exists ( resolved ) ) ;
161200
162- const normalized = this . normalizeFolderPath ( folderPath ) ;
163- if ( ! normalized ) return "" ;
201+ const isAllowed =
202+ context . allowedRoots . length === 0
203+ ? true
204+ : this . isPathAllowed ( isEmpty ? "" : resolved , context . allowedRoots ) ;
164205
165- if ( allowCreate ) {
166- const canonical = canonicalByNormalized . get ( normalized ) ;
167- const resolved = canonical ?? normalized ;
168- const exists =
169- canonical !== undefined ||
170- ( await this . app . vault . adapter . exists ( resolved ) ) ;
171- if ( ! exists ) await this . createFolder ( resolved ) ;
172- return resolved ;
206+ return {
207+ raw,
208+ normalized,
209+ resolved,
210+ exists,
211+ isAllowed,
212+ isEmpty,
213+ } ;
214+ }
215+
216+ private async ensureFolderExists ( selection : FolderSelection ) : Promise < void > {
217+ if ( selection . isEmpty || selection . exists ) return ;
218+ await this . createFolder ( selection . resolved ) ;
219+ }
220+
221+ private async handleSingleSelection (
222+ context : FolderSelectionContext ,
223+ ) : Promise < string > {
224+ const raw = context . items [ 0 ] ?? "" ;
225+ const selection = await this . resolveSelection ( raw , context ) ;
226+
227+ if ( selection . isEmpty ) return "" ;
228+ if ( ! selection . isAllowed ) {
229+ this . showFolderNotAllowedNotice ( context . allowedRoots ) ;
230+ return "" ;
173231 }
174232
175- await this . createFolder ( normalized ) ;
176- return normalized ;
233+ await this . ensureFolderExists ( selection ) ;
234+ return selection . resolved ;
177235 }
178236
179237 private normalizeFolderPath ( path : string ) : string {
0 commit comments