From e4e3d8ef5955331e5eeb9eecbdae7e3462e55a76 Mon Sep 17 00:00:00 2001 From: donteatfriedrice Date: Tue, 8 Apr 2025 12:37:36 +0000 Subject: [PATCH] fix(editor): paste to code block should delete selected text (#11546) Close [BS-3064](https://linear.app/affine-design/issue/BS-3064/fix-bug-pasting-in-code-block-does-not-replace-text) --- .../shared/src/adapters/middlewares/paste.ts | 20 ++- .../blocksuite/clipboard/clipboard.spec.ts | 153 ++++++++++++++++-- 2 files changed, 152 insertions(+), 21 deletions(-) diff --git a/blocksuite/affine/shared/src/adapters/middlewares/paste.ts b/blocksuite/affine/shared/src/adapters/middlewares/paste.ts index 057ce81f5d..6c1617672e 100644 --- a/blocksuite/affine/shared/src/adapters/middlewares/paste.ts +++ b/blocksuite/affine/shared/src/adapters/middlewares/paste.ts @@ -124,6 +124,13 @@ class PasteTr { private readonly _mergeCode = () => { const deltas: DeltaOperation[] = [{ retain: this.pointState.point.index }]; + // if there is text selection, delete the text selected + if (this.pointState.text.length - this.pointState.point.index > 0) { + deltas.push({ + delete: this.pointState.text.length - this.pointState.point.index, + }); + } + // paste the text from the snapshot to code block this.snapshot.content.forEach((blockSnapshot, i) => { if (blockSnapshot.props.text) { const text = this._textFromSnapshot(blockSnapshot); @@ -133,6 +140,11 @@ class PasteTr { deltas.push(...text.delta); } }); + // paste the text after the text selection from the snapshot to code block + const { toDelta } = this._getDeltas(); + if (toDelta.length > 0) { + deltas.push(...toDelta); + } this.pointState.text.applyDelta(deltas); this.snapshot.content = []; }; @@ -490,13 +502,13 @@ class PasteTr { } merge() { - if (this.pointState.model.flavour === 'affine:code') { - this._mergeCode(); + if (this.firstSnapshot === this.lastSnapshot) { + this._mergeSingle(); return; } - if (this.firstSnapshot === this.lastSnapshot) { - this._mergeSingle(); + if (this.pointState.model.flavour === 'affine:code') { + this._mergeCode(); return; } diff --git a/tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts b/tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts index 1c262447e3..8df38a58fb 100644 --- a/tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts +++ b/tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts @@ -12,16 +12,20 @@ import { } from '@affine-test/kit/utils/keyboard'; import { openHomePage } from '@affine-test/kit/utils/load-page'; import { + addCodeBlock, clickNewPageButton, getBlockSuiteEditorTitle, type, waitForEditorLoad, } from '@affine-test/kit/utils/page-logic'; import { setSelection } from '@affine-test/kit/utils/selection'; +import type { CodeBlockComponent } from '@blocksuite/affine-block-code'; import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph'; +import type { BlockComponent } from '@blocksuite/std'; import { expect, type Page } from '@playwright/test'; const paragraphLocator = 'affine-note affine-paragraph'; +const codeBlockLocator = 'affine-note affine-code'; // Helper function to create paragraph blocks with text async function createParagraphBlocks(page: Page, texts: string[]) { @@ -31,32 +35,75 @@ async function createParagraphBlocks(page: Page, texts: string[]) { } } -// Helper function to verify paragraph text content -async function verifyParagraphContent( +// Helper function to verify block text content +async function verifyBlockContent( page: Page, + selector: string, index: number, expectedText: string ) { expect( await page - .locator(paragraphLocator) + .locator(selector) .nth(index) - .evaluate( - (block: ParagraphBlockComponent, expected: string) => - block.model.props.type === 'text' && - block.model.props.text.toString() === expected, - expectedText - ) + .evaluate((block: T, expected: string) => { + const model = block.model; + // Check if model has text property + if (!('text' in model)) return false; + const text = model.text; + // Check if text exists and has toString method + return text && text.toString() === expected; + }, expectedText) ).toBeTruthy(); } -// Helper function to get paragraph block ids -async function getParagraphIds(page: Page) { - const paragraph = page.locator(paragraphLocator); - const paragraphIds = await paragraph.evaluateAll( - (blocks: ParagraphBlockComponent[]) => blocks.map(block => block.model.id) +// Helper functions using the generic verifyBlockContent +async function verifyParagraphContent( + page: Page, + index: number, + expectedText: string +) { + await verifyBlockContent( + page, + paragraphLocator, + index, + expectedText ); - return { paragraphIds }; +} + +async function verifyCodeBlockContent( + page: Page, + index: number, + expectedText: string +) { + await verifyBlockContent( + page, + codeBlockLocator, + index, + expectedText + ); +} + +// Helper function to get block ids +async function getBlockIds( + page: Page, + selector: string +) { + const blocks = page.locator(selector); + const blockIds = await blocks.evaluateAll((blocks: T[]) => + blocks.map(block => block.model.id) + ); + return { blockIds }; +} + +// Helper functions using the generic getBlockIds +async function getParagraphIds(page: Page) { + return getBlockIds(page, paragraphLocator); +} + +// Helper functions using the generic getBlockIds +async function getCodeBlockIds(page: Page) { + return getBlockIds(page, codeBlockLocator); } test.beforeEach(async ({ page }) => { @@ -70,7 +117,7 @@ test.describe('paste in multiple blocks text selection', () => { const texts = ['hello world', 'hello world', 'hello world']; await createParagraphBlocks(page, texts); - const { paragraphIds } = await getParagraphIds(page); + const { blockIds: paragraphIds } = await getParagraphIds(page); /** * select text cross the 3 blocks: @@ -108,7 +155,7 @@ test.describe('paste in multiple blocks text selection', () => { * hello world * hello world */ - const { paragraphIds } = await getParagraphIds(page); + const { blockIds: paragraphIds } = await getParagraphIds(page); /** * select the first 2 blocks: @@ -205,3 +252,75 @@ test('paste surface-ref block to another doc as embed-linked-doc block', async ( ); await expect(embedLinkedDocBlockTitle).toHaveText('Clipboard Test'); }); + +test.describe('paste to code block', () => { + test('should replace the selected text when pasting plain text', async ({ + page, + }) => { + // press enter to focus on the first block of the editor + await pressEnter(page); + await addCodeBlock(page); + await type(page, 'hello world hello'); + const { blockIds: codeBlockIds } = await getCodeBlockIds(page); + await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11); + await pasteContent(page, { 'text/plain': 'test' }); + await verifyCodeBlockContent(page, 0, 'hello test hello'); + }); + + test('should replace the selected text when pasting single snapshot', async ({ + page, + }) => { + // Create initial test blocks + // add a paragraph block + await createParagraphBlocks(page, ['test']); + // add a code block + await pressEnter(page); + await addCodeBlock(page); + await type(page, 'hello world hello'); + + // select the paragraph content + const { blockIds: paragraphIds } = await getParagraphIds(page); + await setSelection(page, paragraphIds[0], 0, paragraphIds[0], 4); + // copy the paragraph content + await copyByKeyboard(page); + + // select 'world' in the code block + const { blockIds: codeBlockIds } = await getCodeBlockIds(page); + await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11); + + // paste to the code block + await pasteByKeyboard(page); + await page.waitForTimeout(100); + + await verifyCodeBlockContent(page, 0, 'hello test hello'); + }); + + test('should replace the selected text when pasting multiple snapshots', async ({ + page, + }) => { + // Create initial test blocks + // add three paragraph blocks + await createParagraphBlocks(page, ['test', 'test', 'test']); + + // add a code block + await pressEnter(page); + await addCodeBlock(page); + await type(page, 'hello world hello'); + + // select all paragraph content + const { blockIds: paragraphIds } = await getParagraphIds(page); + await setSelection(page, paragraphIds[0], 0, paragraphIds[2], 4); + // copy the paragraph content + await copyByKeyboard(page); + + // select 'world' in the code block + const { blockIds: codeBlockIds } = await getCodeBlockIds(page); + await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11); + + // paste to the code block + await pasteByKeyboard(page); + await page.waitForTimeout(100); + + await verifyCodeBlockContent(page, 0, 'hello test\ntest\ntest hello'); + }); +});