From 8ea39d543819aedbdbfe1bbfe6a177733e79016e Mon Sep 17 00:00:00 2001 From: yoyoyohamapi <8338436+yoyoyohamapi@users.noreply.github.com> Date: Wed, 7 May 2025 07:46:13 +0000 Subject: [PATCH] fix(core): cannot input space at the beginning of a blank paragraph (#12166) ### TL:DR fix: cannot input space at the beginning of a blank paragraph > CLOSE BS-3427 ## Summary by CodeRabbit - **New Features** - Improved space key handling in the editor: pressing space on an empty AI input now hides the AI panel and inserts a space character back into the editor. - **Bug Fixes** - Prevented the AI panel from processing empty input when space is pressed, ensuring smoother user experience. - **Tests** - Added an end-to-end test verifying that pressing space on an empty AI input hides the AI panel and inserts a space. - **Refactor** - Streamlined event handling logic for space key detection in the editor. - **Chores** - Enhanced editor content retrieval to optionally preserve whitespace while removing invisible characters. --- .../ai/entries/space/setup-space.ts | 46 ++++++++++++++++--- .../e2e/basic/guidance.spec.ts | 16 +++++++ .../e2e/utils/editor-utils.ts | 10 +++- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/packages/frontend/core/src/blocksuite/ai/entries/space/setup-space.ts b/packages/frontend/core/src/blocksuite/ai/entries/space/setup-space.ts index e80a93214f..b069b0d590 100644 --- a/packages/frontend/core/src/blocksuite/ai/entries/space/setup-space.ts +++ b/packages/frontend/core/src/blocksuite/ai/entries/space/setup-space.ts @@ -1,9 +1,34 @@ -import { TextSelection } from '@blocksuite/affine/std'; +import type { RichText } from '@blocksuite/affine/rich-text'; +import { type EditorHost, TextSelection } from '@blocksuite/affine/std'; import { handleInlineAskAIAction } from '../../actions/doc-handler'; import { AIProvider } from '../../provider'; import type { AffineAIPanelWidget } from '../../widgets/ai-panel/ai-panel'; +function isSpaceEvent(event: KeyboardEvent) { + return event.key === ' ' && event.which === 32 && !event.isComposing; +} + +function insertSpace(host: EditorHost) { + const textSelection = host.selection.find(TextSelection); + if (!textSelection || !textSelection.isCollapsed()) return; + + const blockComponent = host.view.getBlock(textSelection.from.blockId); + if (!blockComponent) return; + + const richText = blockComponent.querySelector('rich-text') as RichText | null; + if (!richText) return; + + const inlineEditor = richText.inlineEditor; + inlineEditor?.insertText( + { + index: textSelection.from.index, + length: 0, + }, + ' ' + ); +} + export function setupSpaceAIEntry(panel: AffineAIPanelWidget) { // Background: The keydown event triggered by a space may originate from: // 1. Normal space insertion @@ -18,12 +43,19 @@ export function setupSpaceAIEntry(panel: AffineAIPanelWidget) { const host = panel.host; const keyboardState = ctx.get('keyboardState'); const event = keyboardState.raw; - if ( - AIProvider.actions.chat && - event.key === ' ' && - event.which === 32 && - !event.isComposing - ) { + if (AIProvider.actions.chat && isSpaceEvent(event)) { + // If the AI panel is in the input state and the input content is empty, + // insert a space back into the editor. + if (panel.state === 'input') { + const input = panel.shadowRoot?.querySelector('ai-panel-input'); + if (input?.textarea.value.trim() === '') { + event.preventDefault(); + insertSpace(host); + panel.hide(); + return; + } + } + const selection = host.selection.find(TextSelection); if (selection && selection.isCollapsed() && selection.from.index === 0) { const block = host.view.getBlock(selection.blockId); diff --git a/tests/affine-cloud-copilot/e2e/basic/guidance.spec.ts b/tests/affine-cloud-copilot/e2e/basic/guidance.spec.ts index 6bbd41bb3d..676550aec0 100644 --- a/tests/affine-cloud-copilot/e2e/basic/guidance.spec.ts +++ b/tests/affine-cloud-copilot/e2e/basic/guidance.spec.ts @@ -35,4 +35,20 @@ test.describe('AIBasic/Guidance', () => { await page.keyboard.press('Enter'); await expect(page.locator('affine-ai-panel-widget')).not.toBeVisible(); }); + + test('should hide AI panel and insert space back to editor when space is pressed on empty input', async ({ + page, + utils, + }) => { + await utils.editor.focusToEditor(page); + await page.keyboard.press('Space'); + await expect(page.locator('affine-ai-panel-widget')).toBeVisible(); + + await page.keyboard.press('Space'); + await expect(page.locator('affine-ai-panel-widget')).not.toBeVisible(); + await expect(async () => { + const content = await utils.editor.getEditorContent(page, false); + expect(content).toBe(' '); + }).toPass({ timeout: 5000 }); + }); }); diff --git a/tests/affine-cloud-copilot/e2e/utils/editor-utils.ts b/tests/affine-cloud-copilot/e2e/utils/editor-utils.ts index 04017b6b0b..7d07b3e8cc 100644 --- a/tests/affine-cloud-copilot/e2e/utils/editor-utils.ts +++ b/tests/affine-cloud-copilot/e2e/utils/editor-utils.ts @@ -21,14 +21,20 @@ export class EditorUtils { await page.keyboard.press('Enter'); } - public static async getEditorContent(page: Page) { + public static async getEditorContent(page: Page, trim = true) { let content = ''; let retry = 3; while (!content && retry > 0) { const lines = await page.$$('page-editor .inline-editor'); const contents = await Promise.all(lines.map(el => el.innerText())); content = contents - .map(c => c.replace(/[\u200B-\u200D\uFEFF]/g, '').trim()) + .map(c => { + const invisibleFiltered = c.replace(/[\u200B-\u200D\uFEFF]/g, ''); + if (trim) { + return invisibleFiltered.trim(); + } + return invisibleFiltered; + }) .filter(c => !!c) .join('\n'); if (!content) {