From 6be4d6ec5624b26c09c0fe9fa04ceb37334dda0d Mon Sep 17 00:00:00 2001 From: Keath Milligan Date: Tue, 10 Feb 2026 21:30:10 -0600 Subject: [PATCH] fix: skip reformatting when pasting in codeblock (#19) - Add isCursorInCodeBlock() helper that detects fenced code blocks by scanning from line 0 to cursor, toggling on backtick/tilde fences - Add early-return guard in onPaste() to bypass reformatting when cursor is inside a code block - Archive openspec change proposal and update paste-interception spec with new requirement --- .../proposal.md | 12 ++++++ .../specs/paste-interception/spec.md | 39 +++++++++++++++++++ .../tasks.md | 11 ++++++ openspec/specs/paste-interception/spec.md | 39 +++++++++++++++++++ src/main.ts | 32 +++++++++++++++ 5 files changed, 133 insertions(+) create mode 100644 openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/proposal.md create mode 100644 openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/specs/paste-interception/spec.md create mode 100644 openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/tasks.md diff --git a/openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/proposal.md b/openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/proposal.md new file mode 100644 index 0000000..f3f2698 --- /dev/null +++ b/openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/proposal.md @@ -0,0 +1,12 @@ +# Change: Skip reformatting when pasting into a code block + +## Why +When a user pastes content into a fenced code block (`` ``` ``), the plugin reformats the pasted text (HTML-to-Markdown conversion, heading cascade, regex replacements, etc.). This destroys the raw content the user intended to paste as code. The plugin should detect code block context and let Obsidian's default paste proceed. (GitHub issue #14) + +## What Changes +- Add a code block detection function that scans backward from the cursor to determine if the cursor is inside a fenced code block +- Add an early-return guard in `onPaste()` that skips reformatting when the cursor is inside a code block + +## Impact +- Affected specs: `paste-interception` +- Affected code: `src/main.ts` (`onPaste` method, new helper function) diff --git a/openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/specs/paste-interception/spec.md b/openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/specs/paste-interception/spec.md new file mode 100644 index 0000000..cf5de00 --- /dev/null +++ b/openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/specs/paste-interception/spec.md @@ -0,0 +1,39 @@ +## ADDED Requirements +### Requirement: Code block paste bypass +The plugin SHALL NOT reformat pasted content when the cursor is inside a fenced code block. When a fenced code block is detected, the plugin SHALL allow Obsidian's default paste behavior to proceed unmodified. + +A fenced code block is detected by scanning backward from the cursor line to find an opening fence (a line starting with `` ``` `` or `~~~`, optionally followed by a language identifier) that has no corresponding closing fence before the cursor position. + +#### Scenario: Paste inside an open fenced code block +- **WHEN** `pasteOverride` is enabled +- **AND** the cursor is positioned inside a fenced code block (between an opening `` ``` `` and before a closing `` ``` ``) +- **AND** the user pastes content +- **THEN** the plugin SHALL skip all transformations +- **AND** `event.preventDefault()` SHALL NOT be called +- **AND** Obsidian's default paste behavior SHALL proceed + +#### Scenario: Paste outside a code block +- **WHEN** `pasteOverride` is enabled +- **AND** the cursor is NOT inside a fenced code block +- **AND** the user pastes content +- **THEN** the plugin SHALL process the paste as normal (applying configured transformations) + +#### Scenario: Paste after a closed code block +- **WHEN** `pasteOverride` is enabled +- **AND** the document contains a fenced code block that has both opening and closing fences +- **AND** the cursor is positioned after the closing fence +- **THEN** the plugin SHALL process the paste as normal (the code block is closed, cursor is not inside it) + +#### Scenario: Fenced code block with language specifier +- **WHEN** `pasteOverride` is enabled +- **AND** the cursor is inside a fenced code block that has a language specifier (e.g., `` ```javascript ``) +- **AND** the user pastes content +- **THEN** the plugin SHALL skip all transformations +- **AND** Obsidian's default paste behavior SHALL proceed + +#### Scenario: Tilde-fenced code block +- **WHEN** `pasteOverride` is enabled +- **AND** the cursor is inside a tilde-fenced code block (opened with `~~~`) +- **AND** the user pastes content +- **THEN** the plugin SHALL skip all transformations +- **AND** Obsidian's default paste behavior SHALL proceed diff --git a/openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/tasks.md b/openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/tasks.md new file mode 100644 index 0000000..0dc000a --- /dev/null +++ b/openspec/changes/archive/2026-02-11-fix-codeblock-paste-bypass/tasks.md @@ -0,0 +1,11 @@ +## 1. Implementation +- [x] 1.1 Add `isCursorInCodeBlock(editor)` helper to `src/main.ts` that scans lines backward from the cursor to detect unclosed fenced code blocks (triple-backtick or triple-tilde) +- [x] 1.2 Add early-return guard in `onPaste()` that calls `isCursorInCodeBlock()` and skips reformatting when inside a code block +- [x] 1.3 Add debug log message when paste is skipped due to code block context + +## 2. Verification +- [x] 2.1 Build with `npm run build` to confirm no compilation errors +- [ ] 2.2 Manual test: paste HTML content into a fenced code block and verify raw text is pasted (no reformatting) +- [ ] 2.3 Manual test: paste HTML content outside a code block and verify reformatting still works +- [ ] 2.4 Manual test: paste into a code block with a language specifier (e.g., ```js) and verify raw text is pasted +- [ ] 2.5 Manual test: paste into an indented code block (4 spaces) -- note: this change only targets fenced blocks diff --git a/openspec/specs/paste-interception/spec.md b/openspec/specs/paste-interception/spec.md index 59d4daa..85c24fa 100644 --- a/openspec/specs/paste-interception/spec.md +++ b/openspec/specs/paste-interception/spec.md @@ -35,3 +35,42 @@ The heading cascade logic in `transformMarkdown` SHALL accumulate the `appliedTr - **AND** pasted content contains multiple headings where at least one is changed - **THEN** the `appliedTransformations` flag SHALL remain `true` after processing all headings +### Requirement: Code block paste bypass +The plugin SHALL NOT reformat pasted content when the cursor is inside a fenced code block. When a fenced code block is detected, the plugin SHALL allow Obsidian's default paste behavior to proceed unmodified. + +A fenced code block is detected by scanning backward from the cursor line to find an opening fence (a line starting with `` ``` `` or `~~~`, optionally followed by a language identifier) that has no corresponding closing fence before the cursor position. + +#### Scenario: Paste inside an open fenced code block +- **WHEN** `pasteOverride` is enabled +- **AND** the cursor is positioned inside a fenced code block (between an opening `` ``` `` and before a closing `` ``` ``) +- **AND** the user pastes content +- **THEN** the plugin SHALL skip all transformations +- **AND** `event.preventDefault()` SHALL NOT be called +- **AND** Obsidian's default paste behavior SHALL proceed + +#### Scenario: Paste outside a code block +- **WHEN** `pasteOverride` is enabled +- **AND** the cursor is NOT inside a fenced code block +- **AND** the user pastes content +- **THEN** the plugin SHALL process the paste as normal (applying configured transformations) + +#### Scenario: Paste after a closed code block +- **WHEN** `pasteOverride` is enabled +- **AND** the document contains a fenced code block that has both opening and closing fences +- **AND** the cursor is positioned after the closing fence +- **THEN** the plugin SHALL process the paste as normal (the code block is closed, cursor is not inside it) + +#### Scenario: Fenced code block with language specifier +- **WHEN** `pasteOverride` is enabled +- **AND** the cursor is inside a fenced code block that has a language specifier (e.g., `` ```javascript ``) +- **AND** the user pastes content +- **THEN** the plugin SHALL skip all transformations +- **AND** Obsidian's default paste behavior SHALL proceed + +#### Scenario: Tilde-fenced code block +- **WHEN** `pasteOverride` is enabled +- **AND** the cursor is inside a tilde-fenced code block (opened with `~~~`) +- **AND** the user pastes content +- **THEN** the plugin SHALL skip all transformations +- **AND** Obsidian's default paste behavior SHALL proceed + diff --git a/src/main.ts b/src/main.ts index 9d0e69f..766554b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -207,6 +207,31 @@ export default class PasteReformatter extends Plugin { } } + /** + * Determines whether the cursor is currently inside a fenced code block. + * Scans backward from the cursor line, counting opening and closing fences + * (triple-backtick or triple-tilde lines). If there is an unclosed opening + * fence before the cursor, the cursor is inside a code block. + * @param editor The editor instance + * @returns true if the cursor is inside a fenced code block + */ + isCursorInCodeBlock(editor: any): boolean { + const cursor = editor.getCursor(); + const currentLine = cursor.line; + let insideCodeBlock = false; + + for (let line = 0; line <= currentLine; line++) { + const lineText = editor.getLine(line); + // Match opening/closing fences: ``` or ~~~ at the start of a line, + // optionally followed by a language identifier (only on opening fences) + if (/^(`{3,}|~{3,})/.test(lineText)) { + insideCodeBlock = !insideCodeBlock; + } + } + + return insideCodeBlock; + } + onPaste(event: ClipboardEvent) { if (this.settings.pasteOverride) { // Check if there's content in the clipboard @@ -214,6 +239,13 @@ export default class PasteReformatter extends Plugin { if (!clipboardData) { return; } + + // Skip reformatting when the cursor is inside a fenced code block + const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); + if (activeView?.editor && this.isCursorInCodeBlock(activeView.editor)) { + console.debug("Paste Reformatter plugin skipping paste: cursor is inside a code block"); + return; + } // Process the clipboard data if (this.doPaste(clipboardData)) {