From 6585b7393a11ef308e1ba8f52ddbf6cf14e2a0d2 Mon Sep 17 00:00:00 2001 From: Kodai Nakamura Date: Mon, 22 Jun 2026 02:20:28 +0900 Subject: [PATCH] fix(formatting): restore selection-only link replacement Why: - The selection command regressed by inheriting document-level URL formatting from the shared body formatter. - That changed user-visible output for selected text containing GitHub, Jira, or Linear URLs. What: - Added a selection-only formatter that projects link replacement settings and calls `replaceLinks(...)` directly. - Switched `mofifyLinksSelection()` to the new helper so document formatting stays unchanged. - Added regression coverage for selection-only behavior and documented the fix in the task report. --- .superpowers/sdd/task-2-report.md | 46 ++++++++++++++++++++ src/__tests__/formatting-run.test.ts | 32 ++++++++++++++ src/__tests__/main-link-generator.test.ts | 52 +++++++++++++++++++++++ src/formatting-run.ts | 24 +++++++++++ src/main.ts | 4 +- 5 files changed, 156 insertions(+), 2 deletions(-) diff --git a/.superpowers/sdd/task-2-report.md b/.superpowers/sdd/task-2-report.md index 6fbca3a..22b14f2 100644 --- a/.superpowers/sdd/task-2-report.md +++ b/.superpowers/sdd/task-2-report.md @@ -83,3 +83,49 @@ Results: ## Concerns None. + +## Fix: Selection Formatting Regression + +Selection formatting was accidentally routed through `formatMarkdownBody(...)`, which let URL-formatting behavior leak into the selection command. + +### RED + +Command: + +```bash +npx vitest run src/__tests__/main-link-generator.test.ts +``` + +Result: + +- Failed as expected +- Failure showed `mofifyLinksSelection()` was formatting a GitHub URL instead of leaving it unchanged: + `[[notes/TypeScript|TypeScript]] [[github/openai/openai/issues/1]] [🔗](https://github.com/openai/openai/issues/1)` + +### GREEN + +Changes: + +- Added `formatMarkdownSelection(...)` in `src/formatting-run.ts` +- Updated `mofifyLinksSelection()` in `src/main.ts` to call the selection-only helper +- Added a unit guard in `src/__tests__/formatting-run.test.ts` + +### Verification + +Commands: + +```bash +npx vitest run src/__tests__/formatting-run.test.ts src/__tests__/main-link-generator.test.ts +npm run tsc +npm run lint +``` + +Results: + +- `npx vitest run src/__tests__/formatting-run.test.ts src/__tests__/main-link-generator.test.ts`: passed `2` files, `7` tests +- `npm run tsc`: exit `0` +- `npm run lint`: exit `0` + +### Concerns + +- Selection formatting now has its own helper; any future selection-specific behavior should be added there, not to `formatMarkdownBody(...)`. diff --git a/src/__tests__/formatting-run.test.ts b/src/__tests__/formatting-run.test.ts index 323f1a6..21d87c2 100644 --- a/src/__tests__/formatting-run.test.ts +++ b/src/__tests__/formatting-run.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest" import { formatMarkdownBody, formatMarkdownDocument, + formatMarkdownSelection, toReplaceLinksSettings, } from "../formatting-run" import { buildCandidateTrieForTest } from "../replace-links/__tests__/test-helpers" @@ -111,3 +112,34 @@ describe("formatMarkdownBody", () => { expect(result).toBe("[[notes/TypeScript|TypeScript]]") }) }) + +describe("formatMarkdownSelection", () => { + it("keeps selection formatting to link replacement only", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "notes/TypeScript" }], + settings: { + scoped: false, + baseDir: undefined, + ignoreCase: true, + }, + }) + + const result = formatMarkdownSelection({ + body: "TypeScript https://github.com/openai/openai/issues/1", + filePath: "current-file.md", + settings: { + ...DEFAULT_SETTINGS, + formatGitHubURLs: true, + formatJiraURLs: true, + formatLinearURLs: true, + replaceUrlWithTitle: true, + ignoreCase: true, + }, + candidateIndex: { candidateMap, trie }, + }) + + expect(result).toBe( + "[[notes/TypeScript|TypeScript]] https://github.com/openai/openai/issues/1", + ) + }) +}) diff --git a/src/__tests__/main-link-generator.test.ts b/src/__tests__/main-link-generator.test.ts index 807d9b6..2408145 100644 --- a/src/__tests__/main-link-generator.test.ts +++ b/src/__tests__/main-link-generator.test.ts @@ -76,4 +76,56 @@ describe("AutomaticLinkerPlugin link generator", () => { expect(result).toBe("| [[notes/foo\\|bar]] | x |\n| --- | --- |\n") }) + + it("keeps selection formatting to link replacement only", async () => { + const { default: AutomaticLinkerPlugin } = await import("../main") + const settings = { + scoped: false, + baseDir: undefined, + ignoreCase: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "notes/TypeScript" }], + settings, + }) + const replaceSelection = vi.fn() + const app = { + workspace: { + getActiveFile: vi.fn(() => ({ path: "current-file.md" })), + activeEditor: { + editor: { + getSelection: vi.fn(() => "TypeScript https://github.com/openai/openai/issues/1"), + replaceSelection, + }, + }, + }, + vault: { + getConfig: vi.fn(() => "pages"), + getAbstractFileByPath: vi.fn((path: string) => { + if (path === "notes/TypeScript.md") return { path: "notes/TypeScript.md" } + return null + }), + }, + fileManager: { + generateMarkdownLink: vi.fn(() => "[[notes/TypeScript|TypeScript]]"), + }, + } + const plugin = new AutomaticLinkerPlugin(app as never, {} as never) + plugin.settings = { + ...DEFAULT_SETTINGS, + formatGitHubURLs: true, + formatJiraURLs: true, + formatLinearURLs: true, + replaceUrlWithTitle: true, + respectNewFileFolderPath: true, + } + ;(plugin as unknown as { trie: typeof trie }).trie = trie + ;(plugin as unknown as { candidateMap: typeof candidateMap }).candidateMap = candidateMap + + await plugin.mofifyLinksSelection() + + expect(replaceSelection).toHaveBeenCalledWith( + "[[notes/TypeScript|TypeScript]] https://github.com/openai/openai/issues/1", + ) + }) }) diff --git a/src/formatting-run.ts b/src/formatting-run.ts index e174277..a3e9320 100644 --- a/src/formatting-run.ts +++ b/src/formatting-run.ts @@ -84,6 +84,30 @@ export const formatMarkdownBody = ({ return updatedBody } +export const formatMarkdownSelection = ({ + body, + filePath, + settings, + baseDir, + candidateIndex, + linkGenerator, +}: Omit & { body: string }): string => { + if (!candidateIndex) { + return body + } + + return replaceLinks({ + body, + linkResolverContext: { + filePath: filePath.replace(/\.md$/, ""), + trie: candidateIndex.trie, + candidateMap: candidateIndex.candidateMap, + }, + settings: toReplaceLinksSettings(settings, baseDir), + linkGenerator, + }) +} + const inferContentStart = (content: string): number => { const frontmatter = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/) return frontmatter?.[0].length ?? 0 diff --git a/src/main.ts b/src/main.ts index c26e6dc..5e649cb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,8 +12,8 @@ import { } from "obsidian" import { excludeLinks } from "./exclude-links" import { - formatMarkdownBody, formatMarkdownDocument, + formatMarkdownSelection, toReplaceLinksSettings, } from "./formatting-run" import { @@ -233,7 +233,7 @@ export default class AutomaticLinkerPlugin extends Plugin { const linkGenerator = this.createLinkGenerator(activeFile.path) const baseDir = this.settings.respectNewFileFolderPath ? this.app.vault.getConfig("newFileFolderPath") : undefined - const updatedText = formatMarkdownBody({ + const updatedText = formatMarkdownSelection({ body: selectedText, filePath: activeFile.path, settings: this.settings,