Merge pasted list items into the list item under the cursor in the rich text editor

What does this MR do and why?

The rich text editor pastes markdown by rendering it through the API and inserting the result as closed block nodes. Only a lone paragraph was special-cased to inline content. Copying text out of a list item puts the whole list on the clipboard (* text), so pasting it into another list item nested a new list inside that item instead of inserting the text at the cursor. Blockquotes and headings pasted into the same kind of block recreated the block the same way.

When the pasted content's outer blocks match the blocks around the cursor (list flavours count as one kind, and so do their items), open the pasted slice through them and insert it with ProseMirror's replaceRange, the way a native paste does, so the content merges into the current block. The end of the slice stays open as deep as it goes so the text after the cursor continues in the last pasted block. Everything else keeps the existing insertion: a list pasted into a paragraph still becomes a list block, a heading pasted into a paragraph still becomes a heading, and table cells and code blocks are untouched.

Resolves #512151 (closed)

References

  • Reporter's GIF and the agreed expectation: content copied from a block-level element and pasted into a similar block-level element should reuse the existing block (note by pedroms, note by himkp).
  • A new end-to-end example in the rich text editor copy/paste shared examples drives the real Chrome clipboard: copying the word one out of item one and pasting it at the end of item two yields * item two followed by a nested * one at the merge base and * item twoone with this change. A second example pastes the same word copied from a plain paragraph and passes on both, isolating the list-context payload. All 10 examples of the shared group pass locally. Pre-approval pipelines select RSpec predictively and do not map this shared example to the spec that includes it, so it first runs in CI at tier-2 or tier-3, or with pipeline:run-all-rspec.
  • New jest examples in the extension spec: 17 fail at the merge base and pass with this change (list item content pasted at the end, start and middle of an item and into an empty item; ordered and task list items both ways; several items becoming siblings; a nested target; sub-items kept; marks kept; text after the cursor continuing in the last pasted block; a selection replaced; typing while the render is pending; blockquote into blockquote; heading into heading). Four more lock the unchanged behaviour: a list into an empty paragraph, a list block inside a paragraph, a list block inside a blockquote paragraph, a heading block inside a paragraph. The extension spec passes 74/74 and the content editor and markdown trees 1574/1574 under Vue 2 and Vue 3.
  • The single-paragraph special case comes from Improve paste markdown behavior in Content Editor (!115574); this change generalizes it to lists, blockquotes and headings and keeps its behaviour for paragraphs. The same closed insertion produced URL pasted from clipboard are inserted as new lines (#378079) for paragraphs.
  • Stock ProseMirror pastes non-editor HTML as a maximally open slice (Slice.maxOpen in prosemirror-view's parseFromClipboard) and fits it with replaceRange. This change uses the same primitives but opens only through matching blocks: opening everything would merge a pasted table's cells into the surrounding table (measured against the "pastes clipboard table HTML as a nested table inside the cell" example) and flatten headings and blockquotes pasted into paragraphs.

Screenshots or screen recordings

Scenario Before After
Fix: word copied from a list item, pasted at the end of another item 01_fix_item_end 01_fix_item_end
Fix: pasted in the middle of an item 02_fix_item_middle 02_fix_item_middle
Fix: pasted into a new empty item (the case in the issue's recording) 03_fix_new_empty_item 03_fix_new_empty_item
Fix: ordered-list item text pasted into a bullet item 04_fix_ordered_into_bullet 04_fix_ordered_into_bullet
Fix: two items pasted into an item (siblings, not a sub-list) 05_fix_two_items 05_fix_two_items
Fix: quoted text pasted into a blockquote 06_fix_blockquote 06_fix_blockquote
Fix: heading text pasted into a heading 07_fix_heading 07_fix_heading
Edge: item with a sub-item pasted mid-item (text after the cursor continues in the last pasted block) 08_edge_nested_payload_mid_item 08_edge_nested_payload_mid_item
Edge: pasted into a nested item 09_edge_nested_target 09_edge_nested_target
Edge: the target item has sub-items (they stay) 10_edge_item_with_children 10_edge_item_with_children
Edge: pasted over a selection inside an item 11_edge_selection_replaced 11_edge_selection_replaced
Unchanged: list item text pasted into an empty paragraph (becomes a list) 12_same_list_into_empty_paragraph 12_same_list_into_empty_paragraph
Unchanged: list item text pasted mid-paragraph (list block inserted) 13_same_list_into_paragraph_middle 13_same_list_into_paragraph_middle
Unchanged: heading pasted at the end of a paragraph (heading block) 14_same_heading_into_paragraph 14_same_heading_into_paragraph
Unchanged: plain text pasted into a list item (inline) 15_same_plain_text_into_item 15_same_plain_text_into_item
Unchanged: list item text pasted into a table cell (list block) 16_same_table_cell 16_same_table_cell

Every row is staged the same way in real headless Chrome with the real clipboard: the document is typed into the rich text editor, the source text is selected with the keyboard and copied, the cursor is placed in the target and the content pasted. The five Unchanged rows are byte-identical between before and after.

Recordings

Story Before After
The case in the issue: a word copied from a list item, pasted at the end of another item item_end_before item_end_after
An item with a sub-item pasted in the middle of another item nested_payload_before nested_payload_after
Unchanged: a word copied from a list item, pasted into an empty paragraph empty_paragraph_before empty_paragraph_after

Three beats per side: the source text selected and copied, the cursor placed in the target, the paste settled. Frames 1 and 2 are byte-identical between the sides; only the paste beat differs, and in the unchanged story none does.

How to set up and validate locally

  1. Open any rich text editor surface, for example the Description field on the new issue page, and switch to rich text editing if in plain text mode.
  2. Type a bullet list with two items, for example item one and item two.
  3. Select the word one in the first item and copy it.
  4. Click at the end of item two and paste.
  5. Before the fix: a nested bullet one appears under item two. After the fix: the item reads item twoone.
  6. Repeat with the cursor at the start and in the middle of item two, with an ordered list as the source, and with a task list as the target: the text lands at the cursor every time.
  7. Regression checks: paste the same clipboard into an empty paragraph (still becomes a list), into the middle of a paragraph (still inserts a list block, as before), and paste plain text into a list item (still inline).

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Paul W

Merge request reports

Loading
Loading