Skip to content

Markdown viewer: Cmd+C copies markdown source, not the rendered text #7454

Closed

Description

@jpacareu-ctrl

Summary

  • In the rendered markdown file view, Cmd+C writes markdown source into the clipboard's text/plain flavor, so a copied inline code span pastes with its backticks
  • A copied fenced block pastes with the whole fence, language tag included
  • Any target that reads only text/plain shows the syntax: Chrome's find bar or a WhatsApp text input
  • The editable view has no clean escape. It skips the in-app context menu, and the native right-click Copy runs the same serializer

Steps to reproduce

  1. Open a .md file in the desktop app, in the rendered markdown view.
  2. Select an inline code span, e.g. a function name the source wrote between backticks.
  3. Press Cmd+C.
  4. Paste into Chrome's find bar (Cmd+F) or a WhatsApp text input.

Expected
text/plain holds the visible text, the way window.getSelection().toString() returns it. The markdown belongs in text/html, which ProseMirror already sets.

Actual
text/plain holds markdown source. The find bar receives the identifier wrapped in backticks, so the search matches nothing. A fenced block behaves the same way: the paste keeps the opening fence with its language tag, and the closing fence.

Investigation

  • The behavior is deliberate. TableClipboardMarkdown (priority 1000) installs a clipboardTextSerializer at apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/createMarkdownExtensions.ts:174.
  • serializeSelectionForClipboard (serializeMarkdownTable.ts:196) routes a CellSelection and any selection inside a table to sliceToPlainText, and everything else to the markdown serializer. Its comment calls that last branch "preserves copy-as-markdown".
  • The table carve-out added for Issue with Copying Preview-State Table Strings in Markdown #5698 already treats markdown-in-text/plain as the wrong result inside a table. The same reasoning holds outside one.
  • Editable mode offers no way out. TipTapMarkdownRenderer.tsx returns early, before SelectionContextMenu, when editable is true, so the right-click Copy that Issue with Copying Preview-State Table Strings in Markdown #5698's triage pointed to is simply absent. FileViewerContent.tsx passes editable unconditionally. Right-click then reaches the native Electron menu from apps/desktop/src/main/lib/edit-context-menu.ts, whose copy role fires the same DOM copy event and the same serializer.
  • The one clean path left is a code block's hover Copy button. It covers fenced blocks and not inline code.
  • No setting turns this off. The app defines exactly one clipboardTextSerializer, it reads no store or option, and the only markdown preference is the page style ("default" | "tufte").
  • Options, in my order of preference: (a) put the rendered text in text/plain and leave the markdown in text/html; (b) add a setting, next to the terminal's "copy on select"; (c) add a "Copy as Markdown" action and keep Cmd+C plain.

Environment
Superset 1.28.0, desktop app on macOS. Code paths read from main, which matches the 1.28.0 bundle.

Activity

  1. github-actions commented on Sep 11, 2026

    @github-actions
    Contributor

    Summary

    The report is accurate and the behavior is reproducible from the code: in the rendered markdown file view, the clipboard's text/plain flavor is deliberately filled with markdown source for any selection that isn't inside a table, so an inline code span pastes as `foo` and a fenced block pastes with its opening fence, language tag, and closing fence. text/html is unaffected (ProseMirror writes its own serialized DOM), so rich-text targets look right and only plain-text consumers — Chrome's find bar, a chat input, a terminal — see the syntax. The report's claim that editable mode has no escape hatch also checks out: the in-app context menu is gated behind !editable, and the file viewer passes editable unconditionally.

    Affected code

    The serializer that produces markdown in text/plain:

    • apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/createMarkdownExtensions.ts:165-196 — the TableClipboardMarkdown extension, priority: 1000, installs a clipboardTextSerializer ProseMirror prop. The priority is intentional (comment at :159-164) so it is consulted ahead of tiptap-markdown's own serializer.
    • apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/serializeMarkdownTable.ts:196-213 — serializeSelectionForClipboard. Three branches; the last one, :212, is the bug surface:
      return serializeMarkdown(slice.content);
      Its doc comment at :194 describes this as "preserves copy-as-markdown", confirming the report's read that this is deliberate rather than accidental.
    • apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/createMarkdownExtensions.ts:287-292 — Markdown.configure({ transformCopiedText: true }). This is a second, redundant markdown-to-text/plain path (tiptap-markdown installs its own clipboardTextSerializer when this flag is set, at extension priority 50). It matters for the fix: deleting or bypassing TableClipboardMarkdown alone would not fix anything, it would just hand the job back to tiptap-markdown and also regress the table carve-out.

    The missing escape hatch in editable mode:

    • apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/TipTapMarkdownRenderer.tsx:238-246 — returns content bare when editable, and only wraps in SelectionContextMenu otherwise.
    • apps/desktop/src/renderer/components/MarkdownRenderer/components/SelectionContextMenu/SelectionContextMenu.tsx:56-64 — the Copy item this skips is exactly the wanted behavior: it copies window.getSelection().toString(), bypassing the ProseMirror serializer entirely.
    • .../FileViewerPane/components/FileViewerContent/FileViewerContent.tsx:455-462 — editable is passed with no condition, so the rendered .md view always takes the branch without the context menu.
    • apps/desktop/src/main/lib/edit-context-menu.ts:42-51 — the native fallback right-click menu uses { role: "copy" }, which is the Chromium copy path and therefore re-enters the same clipboardTextSerializer. Nothing there can produce plain text.

    One useful confirmation signal: the v2 pane makes the same prop conditional — .../FilePane/registry/views/MarkdownPreviewView/MarkdownPreviewView.tsx:31 sets editable = content.length <= MAX_EDITABLE_LENGTH. A markdown file above that threshold renders read-only, regains SelectionContextMenu, and its right-click Copy should yield clean text while Cmd+C on the same selection still yields markdown. That asymmetry is a cheap manual check of the diagnosis.

    Root cause

    Not a regression or an edge case — serializeMarkdownTable.ts:212 is the designed behavior, and the table carve-out added alongside it (:201-210) is a narrower version of the same complaint: markdown in text/plain was judged wrong inside a table but left in place everywhere else. The report's observation that the reasoning generalizes is fair; whether to generalize it is a product call, not a code fix waiting to happen.

    I ruled out a configuration cause. clipboardTextSerializer appears exactly once in the repo (createMarkdownExtensions.ts:174; the only other hit is the transformCopiedText flag at :291 and its twin in MarkdownEditor.tsx:333), the plugin reads no store or option, and the only persisted markdown preference is the page style — apps/desktop/src/renderer/stores/markdown-preferences/store.ts:4-9 holds "default" | "tufte" and nothing else. So there is no setting a user could have flipped, and none to flip back.

    Reproduction

    Manual, matching the report: open a .md file in the rendered view, select an inline code span, Cmd+C, paste into Cmd+F. Expect the identifier, get backticks.

    For a maintainer working without a build, serializeMarkdownTable.test.ts:181-295 already exercises serializeSelectionForClipboard directly with a real schema, and a failing case drops in next to "keeps markdown for a selection outside any table" (:257-266 — note that test currently asserts the buggy behavior and would need to be rewritten by any fix):

    it("copies an inline code span without backticks", () => {
      // schema must include Code; serializer needs a `code` mark entry
      const doc = schema.nodes.doc.create(null, [
        schema.nodes.paragraph.create(null, [
          schema.text("call "),
          schema.text("formatData", [schema.marks.code.create()]),
        ]),
      ]);
      const selection = TextSelection.create(doc, /* over "formatData" */ from, to);
      const result = serializeSelectionForClipboard(
        selection,
        selection.content(),
        serializeMarkdown,
      );
    
      expect(result).toBe("formatData");
    });

    A fenced-block equivalent (a codeBlock node with language: "ts") asserting expect(result).not.toContain("```") covers the second half of the report.

    Fix direction

    Option (a) is the smallest change and is well-contained: make the final branch of serializeSelectionForClipboard (serializeMarkdownTable.ts:212) return sliceToPlainText(slice), keeping the whole-table CellSelection branch as the single markdown exception, and set transformCopiedText: false at createMarkdownExtensions.ts:290 so the redundant tiptap-markdown serializer can't quietly reinstate the old behavior later. transformPastedText should stay true — in-app paste prefers text/html (ProseMirror tags it with data-pm-slice), so round-tripping a copy inside the app does not depend on the text flavor.

    Two things worth deciding explicitly rather than inheriting:

    • What "plain" means. sliceToPlainText (:178-180) joins blocks with "\n"; ProseMirror's built-in fallback uses "\n\n"; window.getSelection().toString() (what SelectionContextMenu copies) differs from both. The issue asks for the getSelection() result, and matching it would make Cmd+C and right-click Copy agree — which seems like the right target, but it is not what a one-line change to sliceToPlainText gives you.
    • The real cost. Copying from the rendered view into a plain-text target that wants markdown — the raw editor pane, a terminal, an agent prompt box — silently loses the syntax. That's the argument for the report's option (c), a separate "Copy as Markdown" action. Note that (c) is currently unreachable in the file viewer for the same reason the plain Copy is: the context menu isn't mounted.

    Either way, the editable-mode gap at TipTapMarkdownRenderer.tsx:238-240 is worth closing on its own. One trap if you do: edit-context-menu.ts:10-13 documents that Radix's preventDefault on contextmenu suppresses the native Electron menu, so mounting SelectionContextMenu over an editable surface would take away Cut, Paste, and spellcheck suggestions unless those items are added to the Radix menu first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions