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)
This commit is contained in:
donteatfriedrice
2025-04-08 12:37:36 +00:00
parent 1081d6281f
commit e4e3d8ef59
2 changed files with 152 additions and 21 deletions

View File

@@ -124,6 +124,13 @@ class PasteTr {
private readonly _mergeCode = () => { private readonly _mergeCode = () => {
const deltas: DeltaOperation[] = [{ retain: this.pointState.point.index }]; 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) => { this.snapshot.content.forEach((blockSnapshot, i) => {
if (blockSnapshot.props.text) { if (blockSnapshot.props.text) {
const text = this._textFromSnapshot(blockSnapshot); const text = this._textFromSnapshot(blockSnapshot);
@@ -133,6 +140,11 @@ class PasteTr {
deltas.push(...text.delta); 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.pointState.text.applyDelta(deltas);
this.snapshot.content = []; this.snapshot.content = [];
}; };
@@ -490,13 +502,13 @@ class PasteTr {
} }
merge() { merge() {
if (this.pointState.model.flavour === 'affine:code') { if (this.firstSnapshot === this.lastSnapshot) {
this._mergeCode(); this._mergeSingle();
return; return;
} }
if (this.firstSnapshot === this.lastSnapshot) { if (this.pointState.model.flavour === 'affine:code') {
this._mergeSingle(); this._mergeCode();
return; return;
} }

View File

@@ -12,16 +12,20 @@ import {
} from '@affine-test/kit/utils/keyboard'; } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page'; import { openHomePage } from '@affine-test/kit/utils/load-page';
import { import {
addCodeBlock,
clickNewPageButton, clickNewPageButton,
getBlockSuiteEditorTitle, getBlockSuiteEditorTitle,
type, type,
waitForEditorLoad, waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic'; } from '@affine-test/kit/utils/page-logic';
import { setSelection } from '@affine-test/kit/utils/selection'; 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 { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
import type { BlockComponent } from '@blocksuite/std';
import { expect, type Page } from '@playwright/test'; import { expect, type Page } from '@playwright/test';
const paragraphLocator = 'affine-note affine-paragraph'; const paragraphLocator = 'affine-note affine-paragraph';
const codeBlockLocator = 'affine-note affine-code';
// Helper function to create paragraph blocks with text // Helper function to create paragraph blocks with text
async function createParagraphBlocks(page: Page, texts: string[]) { 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 // Helper function to verify block text content
async function verifyParagraphContent( async function verifyBlockContent<T extends BlockComponent>(
page: Page, page: Page,
selector: string,
index: number, index: number,
expectedText: string expectedText: string
) { ) {
expect( expect(
await page await page
.locator(paragraphLocator) .locator(selector)
.nth(index) .nth(index)
.evaluate( .evaluate((block: T, expected: string) => {
(block: ParagraphBlockComponent, expected: string) => const model = block.model;
block.model.props.type === 'text' && // Check if model has text property
block.model.props.text.toString() === expected, if (!('text' in model)) return false;
expectedText const text = model.text;
) // Check if text exists and has toString method
return text && text.toString() === expected;
}, expectedText)
).toBeTruthy(); ).toBeTruthy();
} }
// Helper function to get paragraph block ids // Helper functions using the generic verifyBlockContent
async function getParagraphIds(page: Page) { async function verifyParagraphContent(
const paragraph = page.locator(paragraphLocator); page: Page,
const paragraphIds = await paragraph.evaluateAll( index: number,
(blocks: ParagraphBlockComponent[]) => blocks.map(block => block.model.id) expectedText: string
) {
await verifyBlockContent<ParagraphBlockComponent>(
page,
paragraphLocator,
index,
expectedText
); );
return { paragraphIds }; }
async function verifyCodeBlockContent(
page: Page,
index: number,
expectedText: string
) {
await verifyBlockContent<CodeBlockComponent>(
page,
codeBlockLocator,
index,
expectedText
);
}
// Helper function to get block ids
async function getBlockIds<T extends BlockComponent>(
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<ParagraphBlockComponent>(page, paragraphLocator);
}
// Helper functions using the generic getBlockIds
async function getCodeBlockIds(page: Page) {
return getBlockIds<CodeBlockComponent>(page, codeBlockLocator);
} }
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@@ -70,7 +117,7 @@ test.describe('paste in multiple blocks text selection', () => {
const texts = ['hello world', 'hello world', 'hello world']; const texts = ['hello world', 'hello world', 'hello world'];
await createParagraphBlocks(page, texts); await createParagraphBlocks(page, texts);
const { paragraphIds } = await getParagraphIds(page); const { blockIds: paragraphIds } = await getParagraphIds(page);
/** /**
* select text cross the 3 blocks: * select text cross the 3 blocks:
@@ -108,7 +155,7 @@ test.describe('paste in multiple blocks text selection', () => {
* hello world * hello world
* hello world * hello world
*/ */
const { paragraphIds } = await getParagraphIds(page); const { blockIds: paragraphIds } = await getParagraphIds(page);
/** /**
* select the first 2 blocks: * 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'); 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');
});
});