Skip to content

[lexical-markdown] Bug Fix: Copy only selected code to Markdown - #9676

Open
clean99 wants to merge 2 commits into
meta:mainfrom
clean99:contrib/lexical-selected-code-markdown
Open

clean99 wants to merge 2 commits into
meta:mainfrom
clean99:contrib/lexical-selected-code-markdown

Conversation

@clean99

@clean99 clean99 commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Description

Copy as Markdown includes an entire code block even when only part of its text is selected. Make the CODE transformer slice the selected raw text before adding the fence, preserving the language and whitespace. Whole-document exports stay unchanged.

Changeset

.changeset/markdown-selected-code.md

Test plan

Before

pnpm run test-unit packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts --maxWorkers=1 -t 'code block partial selection'

Test Files  1 failed (1)
Tests  1 failed | 659 skipped (660)

The regression copies unselected lines and text outside both selection endpoints.

After

pnpm run test-unit packages/lexical-markdown --maxWorkers=2

Test Files  9 passed (9)
Tests  925 passed (925)

The test also checks that export leaves editor state unchanged and that importing the result preserves the selected code. Full unit suite, ci-check, build, build-www, build-prod and build-types passed. Browser suites were not run; this change is covered through the headless public API.

## Description

Copy as Markdown includes an entire code block even when only part of its text is selected. Make the CODE transformer slice the selected raw text before adding the fence, preserving the language and whitespace. Whole-document exports stay unchanged.

## Changeset

`.changeset/markdown-selected-code.md`

## Test plan

### Before

`pnpm run test-unit packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts --maxWorkers=1 -t 'code block partial selection'`

```text
Test Files  1 failed (1)
Tests  1 failed | 659 skipped (660)
```

The regression copies unselected lines and text outside both selection endpoints.

### After

`pnpm run test-unit packages/lexical-markdown --maxWorkers=2`

```text
Test Files  9 passed (9)
Tests  925 passed (925)
```

The test also checks that export leaves editor state unchanged and that importing the result preserves the selected code. Full unit suite, `ci-check`, `build`, `build-www`, `build-prod` and `build-types` passed. Browser suites were not run; this change is covered through the headless public API.
@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lexical Ready Ready Preview Nov 1, 2026 2:23am UTC
lexical-playground Ready Ready Preview Nov 1, 2026 2:23am UTC

Request Review

@clean99
clean99 requested a review from etrepum as a code owner October 31, 2026 17:51
@meta-code-review

meta-code-review Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 1, 2026, 2:34 AM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 31, 2026
@pkg-pr-new

pkg-pr-new Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9676

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9676

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9676

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9676

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9676

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9676

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9676

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9676

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9676

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9676

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9676

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9676

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9676

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9676

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9676

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9676

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9676

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9676

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9676

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9676

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9676

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9676

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9676

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9676

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9676

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9676

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9676

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9676

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9676

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9676

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9676

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9676

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9676

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9676

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9676

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9676

commit: 402c381

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks good — thanks for the fix! Copy as Markdown now copies only the selected part of a code block. The CODE transformer honours the selection argument that MultilineElementTransformer.export already documents ("only selected content should be included"). Whole-document export is unchanged, and the fence length is chosen from the sliced text, so a slice that contains backticks still gets a fence that closes it. The changeset follows .changeset/README.md, including the Verify: paragraph.

A few optional, non-blocking suggestions:

  1. Cover the NodeSelection branch (packages/lexical-markdown/src/MarkdownTransformers.ts:867-869). No test exercises it: without it, a NodeSelection of a code block would export an empty fence (```js\n```), and the current suite would still pass. CONTRIBUTING.md asks for tests for new code ("If you've added code that should be tested, add tests"). A NodeSelection case, plus a backward or paragraph-into-code selection, next to the new test in LexicalMarkdown.test.ts would pin both paths.
  2. Possible follow-up, outside this PR: @lexical/mdast has its own code handler (packages/lexical-mdast/src/handlers.ts:534) that still uses node.getTextContent(). Its $convertSelectionToMarkdownString therefore still copies the whole block. A matching slice there would make the two exporters agree.

FYI only: a selection that merely touches the edge of a code block (from the end of its last line into the next paragraph) now copies an empty fence. That matches how headings (# ) and quotes (> ) already behave in the selection exporter, so I don't think it needs to change here.

Comment on lines +867 to +869
if ($isNodeSelection(selection) && node.isSelected(selection)) {
return node.getTextContent();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: no test exercises this NodeSelection branch. Without it, a NodeSelection of a code block would export an empty fence and the current suite would still pass. A NodeSelection case next to the new test in LexicalMarkdown.test.ts would pin it (CONTRIBUTING.md: "If you've added code that should be tested, add tests").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a NodeSelection case in 402c381 that expects the whole two-line code block, with its language and whitespace, and not the paragraphs around it. It fails with an empty fence when the NodeSelection branch is removed.

## Description

The code block NodeSelection branch in the CODE transformer had no test: with it removed, a NodeSelection of a code block exports an empty fence and the suite still passed. Add a case beside the partial-selection test that selects a two-line code block between unselected paragraphs and expects the whole fenced block, including language and whitespace. No production code changes.

## Test plan

### Before

With only the NodeSelection early return removed:

`node_modules/.bin/vitest --project unit --no-watch packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts --maxWorkers=1 -t 'code block node selection'`

```text
AssertionError: expected '```js\n```' to be '```js\nconst value = "*";\n\treturn v…' // Object.is equality
 Test Files  1 failed (1)
      Tests  1 failed | 660 skipped (661)
```

### After

`node_modules/.bin/vitest --project unit --project scripts-unit --no-watch packages/lexical-markdown --maxWorkers=2`

```text
 Test Files  9 passed (9)
      Tests  926 passed (926)
```

`pnpm run ci-check`, `pnpm run build` and the full unit suite passed. Browser suites were not run; the case uses the headless public API.

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks good — thanks for the fix, and for adding the NodeSelection case. Copy as Markdown now copies only the selected part of a code block: CODE.export honours the selection argument that MultilineElementTransformer.export already documents ("When provided, only selected content should be included"). Whole-document export is unchanged, and the fence length is chosen from the sliced text, so a slice that contains three backticks still gets a fence that closes it (CommonMark 0.31.2 §4.5). The new NodeSelection test pins the branch at packages/lexical-markdown/src/MarkdownTransformers.ts:867-869, and the changeset follows .changeset/README.md, including the Verify: paragraph.

Optional, non-blocking suggestions:

  1. Pin a backward or paragraph-into-code selection (packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts, next to the two new tests). Both endpoints of the partial-selection test sit inside the code block and the selection runs forward. A selection that starts in the paragraph above the block, or one made backward, exports correctly today, but no test covers it. CONTRIBUTING.md asks: "If you've added code that should be tested, add tests."
  2. Possible follow-up, outside this PR: @lexical/mdast's $exportCode (packages/lexical-mdast/src/handlers.ts:527-535) still uses node.getTextContent(), so that package's $convertSelectionToMarkdownString still copies the whole block.

FYI only, no change needed: a selection that only touches the edge of a code block (from the end of its last line into the next paragraph) copies an empty fence, the same way headings (# ) and quotes (> ) already behave in this exporter.

@potatowagon potatowagon added the extended-tests Run extended e2e tests on a PR label Nov 3, 2026

This branch was successfully deployed

2 active deployments
Preview – lexical — 402c3812 Deployed Nov 1, 2026 by vercel[bot]
Preview – lexical-playground — 402c3812 Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants