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.
This commit is contained in:
Kodai Nakamura 2026-06-22 02:20:28 +09:00
parent 73014eb012
commit 6585b7393a
5 changed files with 156 additions and 2 deletions

View file

@ -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(...)`.

View file

@ -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",
)
})
})

View file

@ -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",
)
})
})

View file

@ -84,6 +84,30 @@ export const formatMarkdownBody = ({
return updatedBody
}
export const formatMarkdownSelection = ({
body,
filePath,
settings,
baseDir,
candidateIndex,
linkGenerator,
}: Omit<FormattingRunOptions, "content" | "frontmatter" | "urlTitleMap"> & { 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

View file

@ -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,