Skip to content

refactor: extract formatter and template services#1205

Closed
chhoumann wants to merge 7 commits into
masterfrom
engine-flat/02-formatter-template-services
Closed

refactor: extract formatter and template services#1205
chhoumann wants to merge 7 commits into
masterfrom
engine-flat/02-formatter-template-services

Conversation

@chhoumann
Copy link
Copy Markdown
Owner

@chhoumann chhoumann commented May 15, 2026

Summary

  • Adds formatter evaluator boundaries so macro/template/inline JavaScript evaluation is explicit and injectable.
  • Extracts folder selection and template file operations into dedicated services.
  • Removes remaining direct capture dependencies on the old single-template path and covers preview/append behavior regressions.

Stack

  1. engine-flat/01-vault-frontmatter-services
  2. This PR
  3. engine-flat/03-template-choice
  4. engine-flat/04-capture-choice
  5. engine-flat/05-macro-cutover

Validation

  • bun run lint
  • bun run test
  • bun run build
  • Obsidian dev vault plugin reload and e2e validation

Summary by CodeRabbit

  • Refactoring

    • Improved template and formatter architecture for enhanced stability
    • Streamlined folder selection workflows
    • Better separation of concerns in template file handling and evaluation
  • Tests

    • Added comprehensive test coverage for evaluator boundaries
    • Added tests for template runtime safety to prevent unintended side effects

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

This PR refactors the formatter evaluation architecture to use dependency injection and factory patterns, extracting template and folder-selection logic into dedicated services while maintaining backward compatibility through optional evaluator parameters and default implementations.

Changes

Formatter Evaluation Architecture Refactoring

Layer / File(s) Summary
Evaluator contracts and context types
src/formatters/formatterEvaluators.ts
New types define FormatterEvaluatorContext, async evaluator interfaces for macros/templates/inline-JS, and composite evaluator containers (CompleteFormatterEvaluators, FormatDisplayFormatterEvaluators).
IChoiceExecutor runtime evaluation hooks
src/IChoiceExecutor.ts, src/choiceExecutor.ts
IChoiceExecutor gains optional evaluateMacroToken and evaluateInlineJavaScriptToken hooks; ChoiceExecutor implements them by instantiating SingleMacroEngine and SingleInlineScriptEngine with formatter context variables.
FormatterFactory and factory pattern wiring
src/services/FormatterFactory.ts, src/services/FormatterFactory.test.ts
Centralized factory wires formatters with evaluators: creates CompleteFormatterEvaluators delegating to IChoiceExecutor hooks and TemplateFileService; creates FormatDisplayFormatterEvaluators for preview evaluation.
CompleteFormatter evaluator injection
src/formatters/completeFormatter.ts, src/formatters/completeFormatter.evaluator-boundaries.test.ts
CompleteFormatter receives optional CompleteFormatterEvaluators via constructor; routes macro/template/inline-JS evaluation through injected evaluators instead of direct engine instantiation; includes boundary tests verifying evaluator order, shared state, and error propagation.
TemplateFileService and TemplateEvaluator
src/services/TemplateFileService.ts, src/services/TemplateEvaluator.test.ts, src/services/TemplateFileService.test.ts
New service layer: TemplateEvaluator formats template content and collects property variables; TemplateFileService wraps vault operations (create/overwrite/append) with evaluator integration, Templater plugin coordination, frontmatter post-processing, and consistent error handling; comprehensive test coverage for path normalization, file creation, appending with Templater parsing.
FolderSelectionService extraction
src/services/FolderSelectionService.ts
Extracts folder-selection logic from TemplateEngine: prompts user to select/create folders, validates path segments and reserved names, enforces allowed-root constraints, creates missing folders, and handles cancellation as MacroAbortError.
TemplateEngine delegation to services
src/engine/TemplateEngine.ts
Refactors TemplateEngine to delegate folder-selection and template operations to FolderSelectionService and TemplateFileService; removes prior inline prompting, validation, and file-handling code; uses FormatterFactory for formatter construction.
CaptureChoiceEngine and MacroChoiceEngine factory wiring
src/engine/CaptureChoiceEngine.ts, src/engine/MacroChoiceEngine.ts, src/engine/SingleInlineScriptEngine.ts
CaptureChoiceEngine uses FormatterFactory and TemplateFileService for template-based creation; MacroChoiceEngine uses FormatterFactory in AI assistant and OpenFile paths; SingleInlineScriptEngine constructor parameter variables type widened from Map<string, string> to Map<string, unknown>.
FormatDisplayFormatter preview evaluators
src/formatters/formatDisplayFormatter.ts, src/formatters/formatDisplayFormatter.evaluator-boundaries.test.ts
FormatDisplayFormatter gains optional evaluators parameter and new setPreviewVariables method; getTemplateContent delegates to injected template evaluator or uses default vault-read implementation; test suite verifies preview non-execution of macros/scripts, fallback behavior, and variable resolution.
Integration points and API updates
src/quickAddApi.ts, src/preflight/runOnePagePreflight.ts, src/preflight/collectChoiceRequirements.ts
quickAddApi uses FormatterFactory for file formatting; runOnePagePreflight uses FormatterFactory and setPreviewVariables for live preview with isolated variable state; collectChoiceRequirements uses TemplateFileService for path normalization.
Test updates and new test suites
src/formatters/captureChoiceFormatter-*.test.ts, src/preflight/*.test.ts
Removes old engine mocks from existing tests; adds FormatterFactory-based formatter construction helpers; adds runtime-safety tests verifying preflight does not execute template side effects; adds live preview isolation tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • chhoumann/quickadd#1204: Parallel refactoring of TemplateEngine.ts with different service abstractions for vault and frontmatter operations.
  • chhoumann/quickadd#1048: Adds label-aware syntax ({{MACRO:...|Label}}) and threads label context into formatter evaluation, complementary to the FormatterEvaluatorContext introduced here.

Suggested labels

released

Poem

🐰 The Evaluator's Refrain

Factories now wire the formatters with care,
Evaluators injected, clean patterns everywhere.
Templates in services, folders delegate too—
Hooks in the executor, context flows through.
One factory orchestrates the entire choir's dance! 🎭✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: extract formatter and template services' directly and clearly describes the main change: extracting formatter evaluation boundaries and template/folder services into dedicated abstractions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch engine-flat/02-formatter-template-services

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 15, 2026

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 59ad913
Status: ✅  Deploy successful!
Preview URL: https://11ac5e34.quickadd.pages.dev
Branch Preview URL: https://engine-flat-02-formatter-tem.quickadd.pages.dev

View logs

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c338da32b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +79 to +81
const { content } = await new TemplateEvaluator(
formatter,
).evaluateTemplateContent(templateContent, templatePath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve caller title when evaluating TEMPLATE tokens

Using TemplateEvaluator.evaluateTemplateContent(..., templatePath) here makes nested {{TEMPLATE:...}} expansion call setTitle() with the template file name, and because the nested formatter shares context.variables, that title value leaks back into the parent formatting run. In flows where no title was pre-set (for example API format() input that includes {{TEMPLATE:...}} followed by {{TITLE}} or {{VALUE:title}}), the output now incorrectly uses the template basename instead of leaving title unset/derived from the actual target file context.

Useful? React with 👍 / 👎.

Base automatically changed from engine-flat/01-vault-frontmatter-services to master May 16, 2026 15:32
@chhoumann chhoumann force-pushed the engine-flat/02-formatter-template-services branch from c338da3 to 59ad913 Compare May 16, 2026 15:35
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/preflight/runOnePagePreflight.selection.test.ts (1)

187-190: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset modalComputePreview in this beforeEach to avoid stale callback leakage.

Line 187 setup does not clear modalComputePreview, so Line 239 test can accidentally reuse a callback from a previous test and mask regressions.

Suggested fix
 describe("runOnePagePreflight template extension handling", () => {
 	beforeEach(() => {
 		modalOpenMock.mockClear();
 		modalResult = {};
+		modalComputePreview = undefined;
 	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/preflight/runOnePagePreflight.selection.test.ts` around lines 187 - 190,
The beforeEach currently only clears modalOpenMock and resets modalResult but
fails to reset modalComputePreview, which can leak callbacks across tests;
update the beforeEach in runOnePagePreflight.selection.test.ts to also clear or
reset the modalComputePreview mock/variable (the same place
modalOpenMock.mockClear() is called) so each test gets a fresh
modalComputePreview and cannot reuse a previous callback.
🧹 Nitpick comments (1)
src/formatters/formatDisplayFormatter.ts (1)

159-161: 💤 Low value

Prefer explicit TFile cast over never.

The duck-type check above confirms the object is a TFile, but casting to never obscures intent. Use as TFile for clarity.

Suggested fix
-				return await vault.cachedRead(file as never);
+				return await vault.cachedRead(file as TFile);

You'll also need to import TFile from obsidian at the top of the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/formatters/formatDisplayFormatter.ts` around lines 159 - 161, The cast to
never in the cachedRead call hides intent—change the cast to the explicit type
confirmed by the duck-check (use file as TFile when calling vault.cachedRead
inside the try block in formatDisplayFormatter) and add an import for TFile from
'obsidian' at the top of the file so the type is available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/engine/TemplateEngine.ts`:
- Around line 89-98: The change pre-reads template content via
getTemplateContent and then calls
createFileWithTemplateContent/overwriteFileWithTemplateContent/appendToFileWithTemplateContent,
which bypasses TemplateFileService's internal try/catch + reportError behavior;
instead, call the template-path variants on templateFileService
(templateService.createFileWithTemplate, overwriteFileWithTemplate,
appendToFileWithTemplate) passing the templatePath and a new
TemplateEvaluator(this.formatter) so errors are handled consistently; update
createFileWithTemplate, overwriteFileWithTemplate, and appendToFileWithTemplate
methods to stop using getTemplateContent and use the corresponding templatePath
methods on templateFileService.

In `@src/services/FolderSelectionService.ts`:
- Around line 107-112: The current shouldPromptForFolder implementation hides
the prompt when allowCreate is true and there is exactly one suggested folder;
update shouldPromptForFolder to keep the prompt reachable by treating one or
zero items the same when creation is allowed. In practice, change the condition
in shouldPromptForFolder (in FolderSelectionService) so it returns true if
context.items.length > 1 OR (context.allowCreate && context.items.length <= 1),
ensuring the prompt appears for the single-suggestion case when allowCreate is
enabled.
- Around line 159-163: The current existence check in ensureFolderExists uses
this.app.vault.adapter.exists(resolved) which returns true for files and
folders; update ensureFolderExists to call
this.app.vault.getAbstractFileByPath(resolved), test whether the returned
abstract file is an instance of TFolder (not TFile), and set exists only when
the abstract file is non-null and instanceof TFolder; replace the
canonical/adapter.exists logic with this new check so callers never receive a
file path when a folder is expected.

---

Outside diff comments:
In `@src/preflight/runOnePagePreflight.selection.test.ts`:
- Around line 187-190: The beforeEach currently only clears modalOpenMock and
resets modalResult but fails to reset modalComputePreview, which can leak
callbacks across tests; update the beforeEach in
runOnePagePreflight.selection.test.ts to also clear or reset the
modalComputePreview mock/variable (the same place modalOpenMock.mockClear() is
called) so each test gets a fresh modalComputePreview and cannot reuse a
previous callback.

---

Nitpick comments:
In `@src/formatters/formatDisplayFormatter.ts`:
- Around line 159-161: The cast to never in the cachedRead call hides
intent—change the cast to the explicit type confirmed by the duck-check (use
file as TFile when calling vault.cachedRead inside the try block in
formatDisplayFormatter) and add an import for TFile from 'obsidian' at the top
of the file so the type is available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8cb08f4-246f-4fdd-a4bf-7b6616307230

📥 Commits

Reviewing files that changed from the base of the PR and between 4961c0d and 59ad913.

📒 Files selected for processing (25)
  • src/IChoiceExecutor.ts
  • src/choiceExecutor.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/engine/MacroChoiceEngine.ts
  • src/engine/SingleInlineScriptEngine.ts
  • src/engine/TemplateEngine.ts
  • src/formatters/captureChoiceFormatter-frontmatter.test.ts
  • src/formatters/captureChoiceFormatter-selection.test.ts
  • src/formatters/captureChoiceFormatter-write-position.test.ts
  • src/formatters/completeFormatter.evaluator-boundaries.test.ts
  • src/formatters/completeFormatter.ts
  • src/formatters/formatDisplayFormatter.evaluator-boundaries.test.ts
  • src/formatters/formatDisplayFormatter.ts
  • src/formatters/formatterEvaluators.ts
  • src/preflight/collectChoiceRequirements.runtime-safety.test.ts
  • src/preflight/collectChoiceRequirements.ts
  • src/preflight/runOnePagePreflight.selection.test.ts
  • src/preflight/runOnePagePreflight.ts
  • src/quickAddApi.ts
  • src/services/FolderSelectionService.ts
  • src/services/FormatterFactory.test.ts
  • src/services/FormatterFactory.ts
  • src/services/TemplateEvaluator.test.ts
  • src/services/TemplateFileService.test.ts
  • src/services/TemplateFileService.ts
💤 Files with no reviewable changes (2)
  • src/formatters/captureChoiceFormatter-write-position.test.ts
  • src/formatters/captureChoiceFormatter-frontmatter.test.ts

Comment on lines 89 to +98
protected async createFileWithTemplate(
filePath: string,
templatePath: string
) {
try {
const templateContent: string = await this.getTemplateContent(
templatePath
);

// Extract filename without extension from the full path.
const fileBasename = basenameWithoutMdOrCanvas(filePath);
this.formatter.setTitle(fileBasename);

const formattedTemplateContent: string =
await this.formatter.withTemplatePropertyCollection(() =>
this.formatter.formatFileContent(templateContent),
);

// Get template variables before creating the file
const templateVars = this.formatter.getAndClearTemplatePropertyVars();

log.logMessage(`TemplateEngine.createFileWithTemplate: Collected ${templateVars.size} template property variables for ${filePath}`);
if (templateVars.size > 0) {
log.logMessage(`Variables: ${Array.from(templateVars.keys()).join(', ')}`);
}

const suppressTemplaterOnCreate = filePath
.toLowerCase()
.endsWith(".md");
const createdFile: TFile = await this.vaultFileService.createFileWithInput(
filePath,
formattedTemplateContent,
{ suppressTemplaterOnCreate },
);

// Post-process front matter for template property types BEFORE Templater
if (this.frontmatterPropertyService.shouldPostProcessFrontMatter(createdFile, templateVars)) {
await this.frontmatterPropertyService.postProcessFrontMatter(createdFile, templateVars);
}

// Process Templater commands for template choices
await overwriteTemplaterOnce(this.app, createdFile);

return createdFile;
} catch (err) {
if (isMacroAbortError(err)) {
throw err;
}
reportError(err, `Could not create file with template at ${filePath}`);
return null;
}
const templateContent = await this.getTemplateContent(templatePath);
return await this.templateFileService.createFileWithTemplateContent(
filePath,
templateContent,
new TemplateEvaluator(this.formatter),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Delegate to the templatePath service methods instead of pre-reading the template.

Line 93, Line 111, and Line 124 fetch template content up front and then call the *WithTemplateContent variants. That bypasses the try/catch + reportError path already built into TemplateFileService.createFileWithTemplate(...), overwriteFileWithTemplate(...), and appendToFileWithTemplate(...), so missing/unreadable templates now escape from the engine instead of returning null consistently.

Suggested fix
 protected async createFileWithTemplate(
 	filePath: string,
 	templatePath: string
 ) {
-	const templateContent = await this.getTemplateContent(templatePath);
-	return await this.templateFileService.createFileWithTemplateContent(
+	return await this.templateFileService.createFileWithTemplate(
 		filePath,
-		templateContent,
+		templatePath,
 		new TemplateEvaluator(this.formatter),
 	);
 }
@@
 protected async overwriteFileWithTemplate(
 	file: TFile,
 	templatePath: string
 ) {
-	const templateContent = await this.getTemplateContent(templatePath);
-	return await this.templateFileService.overwriteFileWithTemplateContent(
+	return await this.templateFileService.overwriteFileWithTemplate(
 		file,
-		templateContent,
+		templatePath,
 		new TemplateEvaluator(this.formatter),
 	);
 }
@@
 protected async appendToFileWithTemplate(
 	file: TFile,
 	templatePath: string,
 	section: "top" | "bottom"
 ) {
-	const templateContent = await this.getTemplateContent(templatePath);
-	return await this.templateFileService.appendToFileWithTemplateContent(
+	return await this.templateFileService.appendToFileWithTemplate(
 		file,
-		templateContent,
+		templatePath,
 		section,
 		new TemplateEvaluator(this.formatter),
 	);
 }

Also applies to: 107-116, 119-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/TemplateEngine.ts` around lines 89 - 98, The change pre-reads
template content via getTemplateContent and then calls
createFileWithTemplateContent/overwriteFileWithTemplateContent/appendToFileWithTemplateContent,
which bypasses TemplateFileService's internal try/catch + reportError behavior;
instead, call the template-path variants on templateFileService
(templateService.createFileWithTemplate, overwriteFileWithTemplate,
appendToFileWithTemplate) passing the templatePath and a new
TemplateEvaluator(this.formatter) so errors are handled consistently; update
createFileWithTemplate, overwriteFileWithTemplate, and appendToFileWithTemplate
methods to stop using getTemplateContent and use the corresponding templatePath
methods on templateFileService.

Comment on lines +107 to +112
private shouldPromptForFolder(context: FolderSelectionContext): boolean {
return (
context.items.length > 1 ||
(context.allowCreate && context.items.length === 0)
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the prompt reachable when folder creation is enabled.

With allowCreate === true and exactly one suggested folder, this returns false and auto-selects the existing folder. That makes the “type to create one” path unreachable in the common single-suggestion case.

Suggested fix
 private shouldPromptForFolder(context: FolderSelectionContext): boolean {
-	return (
-		context.items.length > 1 ||
-		(context.allowCreate && context.items.length === 0)
-	);
+	return context.allowCreate || context.items.length > 1;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private shouldPromptForFolder(context: FolderSelectionContext): boolean {
return (
context.items.length > 1 ||
(context.allowCreate && context.items.length === 0)
);
}
private shouldPromptForFolder(context: FolderSelectionContext): boolean {
return context.allowCreate || context.items.length > 1;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/FolderSelectionService.ts` around lines 107 - 112, The current
shouldPromptForFolder implementation hides the prompt when allowCreate is true
and there is exactly one suggested folder; update shouldPromptForFolder to keep
the prompt reachable by treating one or zero items the same when creation is
allowed. In practice, change the condition in shouldPromptForFolder (in
FolderSelectionService) so it returns true if context.items.length > 1 OR
(context.allowCreate && context.items.length <= 1), ensuring the prompt appears
for the single-suggestion case when allowCreate is enabled.

Comment on lines +159 to +163
const exists = isEmpty
? false
: canonical !== undefined ||
(await this.app.vault.adapter.exists(resolved));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Obsidian plugin API 1.11.4, does app.vault.adapter.exists(path)return true for both files and folders, and what API should plugin code use to distinguishTFolderfromTFile for a path?

💡 Result:

In the Obsidian plugin API, the app.vault.adapter.exists(path) method returns true if the specified path exists in the vault, regardless of whether it is a file or a folder [1][2][3]. It is a general-purpose check for the existence of an entry at that location [1][2]. To distinguish between a TFile and a TFolder for a given path, you should use the app.vault.getAbstractFileByPath(path) method [4][5]. This method returns an instance of TAbstractFile (or null if the path does not exist), which you can then check using the instanceof operator [4][5]: const abstractFile = app.vault.getAbstractFileByPath(path); if (abstractFile instanceof TFile) { // The path is a file } else if (abstractFile instanceof TFolder) { // The path is a folder } Using app.vault.getAbstractFileByPath is the recommended approach for this purpose, as it provides access to the Obsidian-managed file system objects directly [1][3][4]. While DataAdapter methods like exists are available for low-level file system operations, the Vault API is preferred for interacting with files and folders within the Obsidian vault [3].

Citations:


🏁 Script executed:

cd src/services && wc -l FolderSelectionService.ts

Repository: chhoumann/quickadd

Length of output: 92


🏁 Script executed:

cd src/services && sed -n '145,175p' FolderSelectionService.ts

Repository: chhoumann/quickadd

Length of output: 760


🏁 Script executed:

cd src && rg "FolderSelection" --type ts -A 3 -B 1 | head -80

Repository: chhoumann/quickadd

Length of output: 4595


🏁 Script executed:

cd src && rg "resolveSelection" --type ts -A 5

Repository: chhoumann/quickadd

Length of output: 1357


🏁 Script executed:

cd src/services && sed -n '115,135p' FolderSelectionService.ts

Repository: chhoumann/quickadd

Length of output: 502


🏁 Script executed:

cd src/services && sed -n '176,210p' FolderSelectionService.ts

Repository: chhoumann/quickadd

Length of output: 853


🏁 Script executed:

cd src/services && sed -n '78,105p' FolderSelectionService.ts

Repository: chhoumann/quickadd

Length of output: 686


🏁 Script executed:

cd src && rg "selectFolder" --type ts -A 3 -B 1 | head -60

Repository: chhoumann/quickadd

Length of output: 44


🏁 Script executed:

cd src && rg "selectFolder\(" --type ts -A 5 -B 2

Repository: chhoumann/quickadd

Length of output: 44


🏁 Script executed:

cd src/services && sed -n '60,77p' FolderSelectionService.ts

Repository: chhoumann/quickadd

Length of output: 593


🏁 Script executed:

cd src && rg "selectFolder" --type ts | head -20

Repository: chhoumann/quickadd

Length of output: 44


🏁 Script executed:

cd src && grep -r "\.selectFolder\(" --include="*.ts" -A 3

Repository: chhoumann/quickadd

Length of output: 86


🏁 Script executed:

cd src && rg "selectFolder" --type ts

Repository: chhoumann/quickadd

Length of output: 44


🏁 Script executed:

cd src/services && sed -n '1,60p' FolderSelectionService.ts

Repository: chhoumann/quickadd

Length of output: 1710


🏁 Script executed:

cd src && rg "getOrCreateFolder" --type ts -B 2 -A 5 | head -80

Repository: chhoumann/quickadd

Length of output: 2565


🏁 Script executed:

cd src && rg "getOrCreateFolder" --type ts | grep -v "services/FolderSelectionService.ts" | grep "engine"

Repository: chhoumann/quickadd

Length of output: 563


🏁 Script executed:

cd src/engine && rg "getOrCreateFolder" -A 10 | head -40

Repository: chhoumann/quickadd

Length of output: 1873


Use getAbstractFileByPath() with instanceof to distinguish files from folders.

The adapter.exists(resolved) method returns true for both files and folders, so if a user provides a path to an existing file, ensureFolderExists() will skip folder creation and return the file path. Use app.vault.getAbstractFileByPath(path) instead to check whether the path points to a TFile or TFolder:

const abstractFile = app.vault.getAbstractFileByPath(resolved);
const isFolder = abstractFile instanceof TFolder;
const exists = abstractFile !== null && isFolder;

This prevents non-folder paths from being returned to callers expecting a valid folder path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/FolderSelectionService.ts` around lines 159 - 163, The current
existence check in ensureFolderExists uses
this.app.vault.adapter.exists(resolved) which returns true for files and
folders; update ensureFolderExists to call
this.app.vault.getAbstractFileByPath(resolved), test whether the returned
abstract file is an instance of TFolder (not TFile), and set exists only when
the abstract file is non-null and instanceof TFolder; replace the
canonical/adapter.exists logic with this new check so callers never receive a
file path when a folder is expected.

@chhoumann
Copy link
Copy Markdown
Owner Author

Closing this stack after deciding not to continue with this refactor direction.

@chhoumann chhoumann closed this May 16, 2026
@chhoumann chhoumann deleted the engine-flat/02-formatter-template-services branch May 16, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant