From fd717af3db84802ca82121b5ac7d8c5877bfa962 Mon Sep 17 00:00:00 2001 From: L-Sun Date: Tue, 16 Sep 2025 16:47:43 +0800 Subject: [PATCH 1/2] fix(core): update and fix oxlint error (#13591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### PR Dependency Tree * **PR #13591** 👈 * **PR #13590** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit - Bug Fixes - Improved drag-and-drop stability: draggables, drop targets, and monitors now respond when option sources or external data change. - Improved async actions and permission checks to always use the latest callbacks and error handlers. - Chores - Lint/Prettier configs updated to ignore the Git directory. - Upgraded oxlint dev dependency. - Tests - Updated several end-to-end tests for more reliable text selection, focus handling, and timing. --- .prettierignore | 1 + oxlint.json | 1 + package.json | 2 +- .../component/src/ui/dnd/draggable.ts | 2 +- .../component/src/ui/dnd/drop-target.ts | 2 +- .../frontend/component/src/ui/dnd/monitor.tsx | 2 +- .../core/src/components/guard/use-guard.tsx | 6 +- .../components/hooks/affine-async-hooks.ts | 2 +- tests/affine-cloud/e2e/comments.spec.ts | 35 +--- tests/blocksuite/e2e/link.spec.ts | 1 + yarn.lock | 149 +++++------------- 11 files changed, 61 insertions(+), 142 deletions(-) diff --git a/.prettierignore b/.prettierignore index 5cbdefea1b..38a1ef6845 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,7 @@ **/node_modules .yarn .github/helm +.git .vscode .yarnrc.yml .docker diff --git a/oxlint.json b/oxlint.json index 261fff18d0..2145806bc1 100644 --- a/oxlint.json +++ b/oxlint.json @@ -9,6 +9,7 @@ "**/node_modules", ".yarn", ".github/helm", + ".git", ".vscode", ".yarnrc.yml", ".docker", diff --git a/package.json b/package.json index ddb3f0c8be..34fa8995cb 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "husky": "^9.1.7", "lint-staged": "^16.0.0", "msw": "^2.6.8", - "oxlint": "^1.11.1", + "oxlint": "^1.15.0", "prettier": "^3.4.2", "semver": "^7.6.3", "serve": "^14.2.4", diff --git a/packages/frontend/component/src/ui/dnd/draggable.ts b/packages/frontend/component/src/ui/dnd/draggable.ts index 1bb1022c5c..354f60afcd 100644 --- a/packages/frontend/component/src/ui/dnd/draggable.ts +++ b/packages/frontend/component/src/ui/dnd/draggable.ts @@ -84,7 +84,7 @@ export const useDraggable = ( : undefined, }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [...deps, context.toExternalData]); + }, [...deps, getOptions, context.toExternalData]); useEffect(() => { if ( diff --git a/packages/frontend/component/src/ui/dnd/drop-target.ts b/packages/frontend/component/src/ui/dnd/drop-target.ts index 66831f8e1d..0a4e3d5b47 100644 --- a/packages/frontend/component/src/ui/dnd/drop-target.ts +++ b/packages/frontend/component/src/ui/dnd/drop-target.ts @@ -207,7 +207,7 @@ export const useDropTarget = ( : undefined, }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [...deps, dropTargetContext.fromExternalData]); + }, [...deps, getOptions, dropTargetContext.fromExternalData]); const getDropTargetOptions = useCallback(() => { const wrappedCanDrop = dropTargetGet(options.canDrop, options); diff --git a/packages/frontend/component/src/ui/dnd/monitor.tsx b/packages/frontend/component/src/ui/dnd/monitor.tsx index 852708a454..1a0d618ff0 100644 --- a/packages/frontend/component/src/ui/dnd/monitor.tsx +++ b/packages/frontend/component/src/ui/dnd/monitor.tsx @@ -95,7 +95,7 @@ export const useDndMonitor = ( : undefined, }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [...deps, getOptions]); + }, [...deps, getOptions, dropTargetContext.fromExternalData]); const monitorOptions = useMemo(() => { return { diff --git a/packages/frontend/core/src/components/guard/use-guard.tsx b/packages/frontend/core/src/components/guard/use-guard.tsx index 9f0e779b0f..ff67c339b2 100644 --- a/packages/frontend/core/src/components/guard/use-guard.tsx +++ b/packages/frontend/core/src/components/guard/use-guard.tsx @@ -14,12 +14,16 @@ export const useGuard = < ) => { const guardService = useService(GuardService); useEffect(() => { + // oxlint-disable-next-line exhaustive-deps guardService.revalidateCan(action, ...args); // eslint-disable-next-line react-hooks/exhaustive-deps }, [action, guardService, ...args]); const livedata$ = useMemo( - () => guardService.can$(action, ...args), + () => { + // oxlint-disable-next-line exhaustive-deps + return guardService.can$(action, ...args); + }, // eslint-disable-next-line react-hooks/exhaustive-deps [action, guardService, ...args] ); diff --git a/packages/frontend/core/src/components/hooks/affine-async-hooks.ts b/packages/frontend/core/src/components/hooks/affine-async-hooks.ts index c1d9679fcf..ae1daeb540 100644 --- a/packages/frontend/core/src/components/hooks/affine-async-hooks.ts +++ b/packages/frontend/core/src/components/hooks/affine-async-hooks.ts @@ -24,6 +24,6 @@ export function useAsyncCallback( (...args: any) => { callback(...args).catch(e => handleAsyncError(e)); }, - [...deps] // eslint-disable-line react-hooks/exhaustive-deps + [callback, handleAsyncError, ...deps] // eslint-disable-line react-hooks/exhaustive-deps ); } diff --git a/tests/affine-cloud/e2e/comments.spec.ts b/tests/affine-cloud/e2e/comments.spec.ts index fab743d251..82ae0177e8 100644 --- a/tests/affine-cloud/e2e/comments.spec.ts +++ b/tests/affine-cloud/e2e/comments.spec.ts @@ -59,27 +59,9 @@ test.describe('comments', () => { { delay: 50 } ); - // Select some text using triple-click and then refine selection - // Triple-click to select the entire paragraph - await page.locator('affine-paragraph').first().click({ clickCount: 3 }); - - // Wait for selection - await page.waitForTimeout(100); - - // Now we have the whole paragraph selected, let's use mouse to select just "some text" - const paragraph = page.locator('affine-paragraph').first(); - const bbox = await paragraph.boundingBox(); - if (!bbox) throw new Error('Paragraph not found'); - - // Click and drag to select "some text" portion - // Start roughly where "some" begins (estimated position) - await page.mouse.move(bbox.x + bbox.width * 0.45, bbox.y + bbox.height / 2); - await page.mouse.down(); - await page.mouse.move(bbox.x + bbox.width * 0.58, bbox.y + bbox.height / 2); - await page.mouse.up(); - - // Wait a bit for selection to stabilize - await page.waitForTimeout(200); + for (let i = 0; i < 11; i++) { + await page.keyboard.press('Shift+ArrowLeft'); + } // Wait for the toolbar to appear after text selection const toolbar = page.locator('editor-toolbar'); @@ -97,11 +79,14 @@ test.describe('comments', () => { await page.waitForTimeout(300); // Wait for sidebar animation // Find the comment editor - const commentEditor = page.locator('.comment-editor-viewport'); + const commentEditor = page.locator( + '.comment-editor-viewport .page-editor-container' + ); await expect(commentEditor).toBeVisible(); // Enter comment text await commentEditor.click(); + await commentEditor.focus(); await page.keyboard.type('This is my first comment on this text', { delay: 50, }); @@ -125,11 +110,7 @@ test.describe('comments', () => { // The preview should show the selected text that was commented on // Target specifically the sidebar tab content to avoid conflicts with editor content const sidebarTab = page.getByTestId('sidebar-tab-content-comment'); - await expect( - sidebarTab.locator( - 'text=This is a test paragraph with some text that we will comment on.' - ) - ).toBeVisible(); + await expect(sidebarTab.locator('text=comment on.')).toBeVisible(); // This text should appear in the sidebar as the preview of what was commented on diff --git a/tests/blocksuite/e2e/link.spec.ts b/tests/blocksuite/e2e/link.spec.ts index cc113d8571..348344d9ee 100644 --- a/tests/blocksuite/e2e/link.spec.ts +++ b/tests/blocksuite/e2e/link.spec.ts @@ -281,6 +281,7 @@ test('link bar should not be appear when the range is collapsed', async ({ await expect(linkPopoverLocator).toBeVisible(); await focusRichText(page); // click to cancel the link popover + await waitNextFrame(page); await focusRichTextEnd(page); await pressShiftEnter(page); await waitNextFrame(page); diff --git a/yarn.lock b/yarn.lock index cdb16bf92f..b85ea49dfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -803,7 +803,7 @@ __metadata: husky: "npm:^9.1.7" lint-staged: "npm:^16.0.0" msw: "npm:^2.6.8" - oxlint: "npm:^1.11.1" + oxlint: "npm:^1.15.0" prettier: "npm:^3.4.2" semver: "npm:^7.6.3" serve: "npm:^14.2.4" @@ -10698,100 +10698,58 @@ __metadata: languageName: node linkType: hard -"@oxlint-tsgolint/darwin-arm64@npm:0.0.1": - version: 0.0.1 - resolution: "@oxlint-tsgolint/darwin-arm64@npm:0.0.1" - conditions: os=darwin - languageName: node - linkType: hard - -"@oxlint-tsgolint/darwin-x64@npm:0.0.1": - version: 0.0.1 - resolution: "@oxlint-tsgolint/darwin-x64@npm:0.0.1" - conditions: os=darwin - languageName: node - linkType: hard - -"@oxlint-tsgolint/linux-arm64@npm:0.0.1": - version: 0.0.1 - resolution: "@oxlint-tsgolint/linux-arm64@npm:0.0.1" - conditions: os=linux - languageName: node - linkType: hard - -"@oxlint-tsgolint/linux-x64@npm:0.0.1": - version: 0.0.1 - resolution: "@oxlint-tsgolint/linux-x64@npm:0.0.1" - conditions: os=linux - languageName: node - linkType: hard - -"@oxlint-tsgolint/win32-arm64@npm:0.0.1": - version: 0.0.1 - resolution: "@oxlint-tsgolint/win32-arm64@npm:0.0.1" - conditions: os=win32 - languageName: node - linkType: hard - -"@oxlint-tsgolint/win32-x64@npm:0.0.1": - version: 0.0.1 - resolution: "@oxlint-tsgolint/win32-x64@npm:0.0.1" - conditions: os=win32 - languageName: node - linkType: hard - -"@oxlint/darwin-arm64@npm:1.11.1": - version: 1.11.1 - resolution: "@oxlint/darwin-arm64@npm:1.11.1" +"@oxlint/darwin-arm64@npm:1.15.0": + version: 1.15.0 + resolution: "@oxlint/darwin-arm64@npm:1.15.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxlint/darwin-x64@npm:1.11.1": - version: 1.11.1 - resolution: "@oxlint/darwin-x64@npm:1.11.1" +"@oxlint/darwin-x64@npm:1.15.0": + version: 1.15.0 + resolution: "@oxlint/darwin-x64@npm:1.15.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxlint/linux-arm64-gnu@npm:1.11.1": - version: 1.11.1 - resolution: "@oxlint/linux-arm64-gnu@npm:1.11.1" +"@oxlint/linux-arm64-gnu@npm:1.15.0": + version: 1.15.0 + resolution: "@oxlint/linux-arm64-gnu@npm:1.15.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxlint/linux-arm64-musl@npm:1.11.1": - version: 1.11.1 - resolution: "@oxlint/linux-arm64-musl@npm:1.11.1" +"@oxlint/linux-arm64-musl@npm:1.15.0": + version: 1.15.0 + resolution: "@oxlint/linux-arm64-musl@npm:1.15.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxlint/linux-x64-gnu@npm:1.11.1": - version: 1.11.1 - resolution: "@oxlint/linux-x64-gnu@npm:1.11.1" +"@oxlint/linux-x64-gnu@npm:1.15.0": + version: 1.15.0 + resolution: "@oxlint/linux-x64-gnu@npm:1.15.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxlint/linux-x64-musl@npm:1.11.1": - version: 1.11.1 - resolution: "@oxlint/linux-x64-musl@npm:1.11.1" +"@oxlint/linux-x64-musl@npm:1.15.0": + version: 1.15.0 + resolution: "@oxlint/linux-x64-musl@npm:1.15.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxlint/win32-arm64@npm:1.11.1": - version: 1.11.1 - resolution: "@oxlint/win32-arm64@npm:1.11.1" +"@oxlint/win32-arm64@npm:1.15.0": + version: 1.15.0 + resolution: "@oxlint/win32-arm64@npm:1.15.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxlint/win32-x64@npm:1.11.1": - version: 1.11.1 - resolution: "@oxlint/win32-x64@npm:1.11.1" +"@oxlint/win32-x64@npm:1.15.0": + version: 1.15.0 + resolution: "@oxlint/win32-x64@npm:1.15.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -28685,48 +28643,20 @@ __metadata: languageName: node linkType: hard -"oxlint-tsgolint@npm:>=0.0.1": - version: 0.0.1 - resolution: "oxlint-tsgolint@npm:0.0.1" +"oxlint@npm:^1.15.0": + version: 1.15.0 + resolution: "oxlint@npm:1.15.0" dependencies: - "@oxlint-tsgolint/darwin-arm64": "npm:0.0.1" - "@oxlint-tsgolint/darwin-x64": "npm:0.0.1" - "@oxlint-tsgolint/linux-arm64": "npm:0.0.1" - "@oxlint-tsgolint/linux-x64": "npm:0.0.1" - "@oxlint-tsgolint/win32-arm64": "npm:0.0.1" - "@oxlint-tsgolint/win32-x64": "npm:0.0.1" - dependenciesMeta: - "@oxlint-tsgolint/darwin-arm64": - optional: true - "@oxlint-tsgolint/darwin-x64": - optional: true - "@oxlint-tsgolint/linux-arm64": - optional: true - "@oxlint-tsgolint/linux-x64": - optional: true - "@oxlint-tsgolint/win32-arm64": - optional: true - "@oxlint-tsgolint/win32-x64": - optional: true - bin: - tsgolint: bin/tsgolint.js - checksum: 10/2cadb04d5597f425564ed080ca608edb1014aebc85a7a9336a49285c2cb4289379f3eb614694666c8802618a28f619ab2f37dd1ac86cba33a309bc69d8ff47f1 - languageName: node - linkType: hard - -"oxlint@npm:^1.11.1": - version: 1.11.1 - resolution: "oxlint@npm:1.11.1" - dependencies: - "@oxlint/darwin-arm64": "npm:1.11.1" - "@oxlint/darwin-x64": "npm:1.11.1" - "@oxlint/linux-arm64-gnu": "npm:1.11.1" - "@oxlint/linux-arm64-musl": "npm:1.11.1" - "@oxlint/linux-x64-gnu": "npm:1.11.1" - "@oxlint/linux-x64-musl": "npm:1.11.1" - "@oxlint/win32-arm64": "npm:1.11.1" - "@oxlint/win32-x64": "npm:1.11.1" - oxlint-tsgolint: "npm:>=0.0.1" + "@oxlint/darwin-arm64": "npm:1.15.0" + "@oxlint/darwin-x64": "npm:1.15.0" + "@oxlint/linux-arm64-gnu": "npm:1.15.0" + "@oxlint/linux-arm64-musl": "npm:1.15.0" + "@oxlint/linux-x64-gnu": "npm:1.15.0" + "@oxlint/linux-x64-musl": "npm:1.15.0" + "@oxlint/win32-arm64": "npm:1.15.0" + "@oxlint/win32-x64": "npm:1.15.0" + peerDependencies: + oxlint-tsgolint: ">=0.2.0" dependenciesMeta: "@oxlint/darwin-arm64": optional: true @@ -28744,12 +28674,13 @@ __metadata: optional: true "@oxlint/win32-x64": optional: true + peerDependenciesMeta: oxlint-tsgolint: optional: true bin: oxc_language_server: bin/oxc_language_server oxlint: bin/oxlint - checksum: 10/bdf6cb7f6d74b1d6c63ddfdc9597f5394857b1bbee2fb5ab6b86bae9bb58e3ca707ce345f488ae6087ffd909122b615c6020843f7669f6dade14e0396c107a9c + checksum: 10/1ee632ad359b3e63a3a5fccadfcab23ac4b0881b06f2e6c29431db56377858571592005459f247b2eef822d1da4c9d68afdf23965afe6ec6a5fe092f60239fa8 languageName: node linkType: hard From 34a3c83d84ffab790f4ccc31246d285f89b48fd1 Mon Sep 17 00:00:00 2001 From: L-Sun Date: Tue, 16 Sep 2025 17:02:54 +0800 Subject: [PATCH 2/2] fix(editor): prevent SwiftKey IME double input (#13590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close [BS-3610](https://linear.app/affine-design/issue/BS-3610/bug-每次按空格会出现重复单词-,特定输入法,比如swiftkey) #### PR Dependency Tree * **PR #13591** * **PR #13590** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit - Bug Fixes - Android: More reliable Backspace/delete handling, preventing missed inputs and double-deletions. - Android: Cursor/selection is correctly restored after merging a paragraph with the previous block. - Android: Smoother IME composition input; captures correct composition range. - Deletion across lines and around embeds/empty lines is more consistent. - Chores - Internal event handling updated to improve Android compatibility and stability (no user-facing changes). #### PR Dependency Tree * **PR #13591** * **PR #13590** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) --- .../paragraph/src/utils/merge-with-prev.ts | 24 ++++++- .../std/src/event/control/keyboard.ts | 35 ++++++--- blocksuite/framework/std/src/event/keymap.ts | 22 ++++++ .../std/src/inline/services/event.ts | 71 ++++++++++--------- 4 files changed, 109 insertions(+), 43 deletions(-) diff --git a/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts b/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts index b96de60532..d9d856ec65 100644 --- a/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts +++ b/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts @@ -24,7 +24,7 @@ import { getPrevContentBlock, matchModels, } from '@blocksuite/affine-shared/utils'; -import { IS_MOBILE } from '@blocksuite/global/env'; +import { IS_ANDROID, IS_MOBILE } from '@blocksuite/global/env'; import { BlockSelection, type EditorHost } from '@blocksuite/std'; import type { BlockModel, Text } from '@blocksuite/store'; @@ -79,6 +79,28 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) { index: lengthBeforeJoin, length: 0, }).catch(console.error); + + // due to some IME like Microsoft Swift IME on Android will reset range after join text, + // for example: + // + // $ZERO_WIDTH_FOR_EMPTY_LINE <--- p1 + // |aaa <--- p2 + // + // after pressing backspace, during beforeinput event, the native range is (p1, 1) -> (p2, 0) + // and after browser and IME handle the event, the native range is (p1, 1) -> (p1, 1) + // + // a|aa <--- p1 + // + // so we need to set range again after join text. + if (IS_ANDROID) { + setTimeout(() => { + asyncSetInlineRange(editorHost.std, prevBlock, { + index: lengthBeforeJoin, + length: 0, + }).catch(console.error); + }); + } + return true; } diff --git a/blocksuite/framework/std/src/event/control/keyboard.ts b/blocksuite/framework/std/src/event/control/keyboard.ts index 9ecab2faad..84a04a6f01 100644 --- a/blocksuite/framework/std/src/event/control/keyboard.ts +++ b/blocksuite/framework/std/src/event/control/keyboard.ts @@ -1,4 +1,5 @@ -import { IS_MAC } from '@blocksuite/global/env'; +import { DisposableGroup } from '@blocksuite/global/disposable'; +import { IS_ANDROID, IS_MAC } from '@blocksuite/global/env'; import { type UIEventHandler, @@ -6,7 +7,7 @@ import { UIEventStateContext, } from '../base.js'; import type { EventOptions, UIEventDispatcher } from '../dispatcher.js'; -import { bindKeymap } from '../keymap.js'; +import { androidBindKeymapPatch, bindKeymap } from '../keymap.js'; import { KeyboardEventState } from '../state/index.js'; import { EventScopeSourceType, EventSourceState } from '../state/source.js'; @@ -87,15 +88,29 @@ export class KeyboardControl { } bindHotkey(keymap: Record, options?: EventOptions) { - return this._dispatcher.add( - 'keyDown', - ctx => { - if (this.composition) return false; - const binding = bindKeymap(keymap); - return binding(ctx); - }, - options + const disposables = new DisposableGroup(); + if (IS_ANDROID) { + disposables.add( + this._dispatcher.add('beforeInput', ctx => { + if (this.composition) return false; + const binding = androidBindKeymapPatch(keymap); + return binding(ctx); + }) + ); + } + + disposables.add( + this._dispatcher.add( + 'keyDown', + ctx => { + if (this.composition) return false; + const binding = bindKeymap(keymap); + return binding(ctx); + }, + options + ) ); + return () => disposables.dispose(); } listen() { diff --git a/blocksuite/framework/std/src/event/keymap.ts b/blocksuite/framework/std/src/event/keymap.ts index 30f78b0d62..4f682e7483 100644 --- a/blocksuite/framework/std/src/event/keymap.ts +++ b/blocksuite/framework/std/src/event/keymap.ts @@ -103,3 +103,25 @@ export function bindKeymap( return false; }; } + +// In some IME of Android like, the keypress event dose not contain +// the information about what key is pressed. See +// https://stackoverflow.com/a/68188679 +// https://stackoverflow.com/a/66724830 +export function androidBindKeymapPatch( + bindings: Record +): UIEventHandler { + return ctx => { + const event = ctx.get('defaultState').event; + if (!(event instanceof InputEvent)) return; + + if ( + event.inputType === 'deleteContentBackward' && + 'Backspace' in bindings + ) { + return bindings['Backspace'](ctx); + } + + return false; + }; +} diff --git a/blocksuite/framework/std/src/inline/services/event.ts b/blocksuite/framework/std/src/inline/services/event.ts index b48b07134e..ee81a7a083 100644 --- a/blocksuite/framework/std/src/inline/services/event.ts +++ b/blocksuite/framework/std/src/inline/services/event.ts @@ -1,3 +1,4 @@ +import { IS_ANDROID } from '@blocksuite/global/env'; import type { BaseTextAttributes } from '@blocksuite/store'; import type { InlineEditor } from '../inline-editor.js'; @@ -41,11 +42,10 @@ export class EventService { } }; - private readonly _onBeforeInput = (event: InputEvent) => { + private readonly _onBeforeInput = async (event: InputEvent) => { const range = this.editor.rangeService.getNativeRange(); if ( this.editor.isReadonly || - this._isComposing || !range || !this._isRangeCompletelyInRoot(range) ) @@ -54,33 +54,29 @@ export class EventService { let inlineRange = this.editor.toInlineRange(range); if (!inlineRange) return; + if (this._isComposing) { + if (IS_ANDROID && event.inputType === 'insertCompositionText') { + this._compositionInlineRange = inlineRange; + } + return; + } + let ifHandleTargetRange = true; - if (event.inputType.startsWith('delete')) { - if ( - isInEmbedGap(range.commonAncestorContainer) && - inlineRange.length === 0 && - inlineRange.index > 0 - ) { - inlineRange = { - index: inlineRange.index - 1, - length: 1, - }; - ifHandleTargetRange = false; - } else if ( - isInEmptyLine(range.commonAncestorContainer) && - inlineRange.length === 0 && - inlineRange.index > 0 - // eslint-disable-next-line sonarjs/no-duplicated-branches - ) { - // do not use target range when deleting across lines + if ( + event.inputType.startsWith('delete') && + (isInEmbedGap(range.commonAncestorContainer) || // https://github.com/toeverything/blocksuite/issues/5381 - inlineRange = { - index: inlineRange.index - 1, - length: 1, - }; - ifHandleTargetRange = false; - } + isInEmptyLine(range.commonAncestorContainer)) && + inlineRange.length === 0 && + inlineRange.index > 0 + ) { + // do not use target range when deleting across lines + inlineRange = { + index: inlineRange.index - 1, + length: 1, + }; + ifHandleTargetRange = false; } if (ifHandleTargetRange) { @@ -97,11 +93,24 @@ export class EventService { } } } - if (!inlineRange) return; event.preventDefault(); + if (IS_ANDROID) { + this.editor.rerenderWholeEditor(); + await this.editor.waitForUpdate(); + if ( + event.inputType === 'deleteContentBackward' && + !(inlineRange.index === 0 && inlineRange.length === 0) + ) { + // when press backspace at offset 1, double characters will be removed. + // because we mock backspace key event `androidBindKeymapPatch` in blocksuite/framework/std/src/event/keymap.ts + // so we need to stop the event propagation to prevent the double characters removal. + event.stopPropagation(); + } + } + const ctx: BeforeinputHookCtx = { inlineEditor: this.editor, raw: event, @@ -346,11 +355,9 @@ export class EventService { return; } - this.editor.disposables.addFromEvent( - eventSource, - 'beforeinput', - this._onBeforeInput - ); + this.editor.disposables.addFromEvent(eventSource, 'beforeinput', e => { + this._onBeforeInput(e).catch(console.error); + }); this.editor.disposables.addFromEvent( eventSource, 'compositionstart',