Skip to content

Commit c6befcf

Browse files
committed
refactor: clarify folder chooser flow
1 parent f6343e5 commit c6befcf

File tree

1 file changed

+151
-93
lines changed

1 file changed

+151
-93
lines changed

src/engine/TemplateEngine.ts

Lines changed: 151 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
2950
function 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

Comments
 (0)