From 126ab18967ca395a21cb3d98ce4224d75087ce7e Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Thu, 9 Jan 2025 11:49:23 +0000 Subject: [PATCH] feat(editor): selection as store extension (#9605) --- .../block-database/src/database-spec.ts | 2 - .../src/components/page-image-block.ts | 3 +- .../affine/block-image/src/image-spec.ts | 2 - .../affine/block-list/src/list-block.ts | 3 +- .../affine/block-note/src/note-service.ts | 3 +- .../src/surface-ref-block.ts | 3 +- .../affine/block-surface/src/surface-spec.ts | 2 - .../src/core/common/selection-schema.ts | 2 +- .../affine/shared/src/selection/hightlight.ts | 2 +- .../affine/shared/src/selection/image.ts | 2 +- .../affine/widget-drag-handle/src/utils.ts | 8 +- .../src/doc/doc-remote-selection.ts | 3 +- blocksuite/blocks/src/_specs/common.ts | 22 ++- .../root-block/widgets/ai-panel/ai-panel.ts | 2 +- .../widgets/format-bar/format-bar.ts | 2 +- .../block-std/src/extension/index.ts | 1 - .../block-std/src/extension/selection.ts | 14 -- .../framework/block-std/src/identifier.ts | 4 - .../src/range/inline-range-provider.ts | 27 +-- .../block-std/src/range/range-binding.ts | 7 +- .../block-std/src/scope/block-std-scope.ts | 15 +- .../block-std/src/selection/index.ts | 2 - .../block-std/src/selection/variants/block.ts | 4 +- .../src/selection/variants/cursor.ts | 4 +- .../src/selection/variants/surface.ts | 4 +- .../block-std/src/selection/variants/text.ts | 4 +- .../block-std/src/view/element/lit-host.ts | 9 +- .../framework/store/src/extension/index.ts | 1 + .../src/extension}/selection/base.ts | 2 +- .../src/extension/selection/identifier.ts | 17 ++ .../store/src/extension/selection/index.ts | 4 + .../selection/selection-extension.ts} | 155 ++++++------------ .../store/src/extension/selection/types.ts | 9 + .../framework/store/src/model/store/store.ts | 24 ++- .../presets/src/__tests__/utils/setup.ts | 4 +- .../tests-legacy/edgeless/note/note.spec.ts | 1 - .../tests-legacy/edgeless/paste-block.spec.ts | 1 + .../ai/chat-panel/chat-panel-messages.ts | 3 +- tests/affine-local/e2e/links.spec.ts | 3 + 39 files changed, 176 insertions(+), 204 deletions(-) delete mode 100644 blocksuite/framework/block-std/src/extension/selection.ts rename blocksuite/framework/{block-std/src => store/src/extension}/selection/base.ts (94%) create mode 100644 blocksuite/framework/store/src/extension/selection/identifier.ts create mode 100644 blocksuite/framework/store/src/extension/selection/index.ts rename blocksuite/framework/{block-std/src/selection/manager.ts => store/src/extension/selection/selection-extension.ts} (62%) create mode 100644 blocksuite/framework/store/src/extension/selection/types.ts diff --git a/blocksuite/affine/block-database/src/database-spec.ts b/blocksuite/affine/block-database/src/database-spec.ts index 86edb47ce6..c86ba5e1ed 100644 --- a/blocksuite/affine/block-database/src/database-spec.ts +++ b/blocksuite/affine/block-database/src/database-spec.ts @@ -3,7 +3,6 @@ import { CommandExtension, FlavourExtension, } from '@blocksuite/block-std'; -import { DatabaseSelectionExtension } from '@blocksuite/data-view'; import type { ExtensionType } from '@blocksuite/store'; import { literal } from 'lit/static-html.js'; @@ -16,6 +15,5 @@ export const DatabaseBlockSpec: ExtensionType[] = [ DatabaseBlockService, CommandExtension(commands), BlockViewExtension('affine:database', literal`affine-database`), - DatabaseSelectionExtension, DatabaseBlockAdapterExtensions, ].flat(); diff --git a/blocksuite/affine/block-image/src/components/page-image-block.ts b/blocksuite/affine/block-image/src/components/page-image-block.ts index 8961bfd41f..dc7b21b1bc 100644 --- a/blocksuite/affine/block-image/src/components/page-image-block.ts +++ b/blocksuite/affine/block-image/src/components/page-image-block.ts @@ -1,11 +1,12 @@ import { ImageSelection } from '@blocksuite/affine-shared/selection'; -import type { BaseSelection, UIEventStateContext } from '@blocksuite/block-std'; +import type { UIEventStateContext } from '@blocksuite/block-std'; import { BlockSelection, ShadowlessElement, TextSelection, } from '@blocksuite/block-std'; import { WithDisposable } from '@blocksuite/global/utils'; +import type { BaseSelection } from '@blocksuite/store'; import { css, html, type PropertyValues } from 'lit'; import { property, query, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; diff --git a/blocksuite/affine/block-image/src/image-spec.ts b/blocksuite/affine/block-image/src/image-spec.ts index 92068953a3..a1fbe453bf 100644 --- a/blocksuite/affine/block-image/src/image-spec.ts +++ b/blocksuite/affine/block-image/src/image-spec.ts @@ -1,4 +1,3 @@ -import { ImageSelectionExtension } from '@blocksuite/affine-shared/selection'; import { BlockViewExtension, CommandExtension, @@ -29,6 +28,5 @@ export const ImageBlockSpec: ExtensionType[] = [ imageToolbar: literal`affine-image-toolbar-widget`, }), ImageDropOption, - ImageSelectionExtension, ImageBlockAdapterExtensions, ].flat(); diff --git a/blocksuite/affine/block-list/src/list-block.ts b/blocksuite/affine/block-list/src/list-block.ts index 06396e97c6..b6b5b36723 100644 --- a/blocksuite/affine/block-list/src/list-block.ts +++ b/blocksuite/affine/block-list/src/list-block.ts @@ -14,13 +14,14 @@ import { } from '@blocksuite/affine-shared/consts'; import { DocModeProvider } from '@blocksuite/affine-shared/services'; import { getViewportElement } from '@blocksuite/affine-shared/utils'; -import type { BaseSelection, BlockComponent } from '@blocksuite/block-std'; +import type { BlockComponent } from '@blocksuite/block-std'; import { BlockSelection, getInlineRangeProvider, TextSelection, } from '@blocksuite/block-std'; import type { InlineRangeProvider } from '@blocksuite/inline'; +import type { BaseSelection } from '@blocksuite/store'; import { effect } from '@preact/signals-core'; import { html, nothing, type TemplateResult } from 'lit'; import { query, state } from 'lit/decorators.js'; diff --git a/blocksuite/affine/block-note/src/note-service.ts b/blocksuite/affine/block-note/src/note-service.ts index c17882c5e0..3c9d9913e4 100644 --- a/blocksuite/affine/block-note/src/note-service.ts +++ b/blocksuite/affine/block-note/src/note-service.ts @@ -2,7 +2,6 @@ import { textConversionConfigs } from '@blocksuite/affine-components/rich-text'; import { NoteBlockSchema } from '@blocksuite/affine-model'; import { matchFlavours } from '@blocksuite/affine-shared/utils'; import { - type BaseSelection, type BlockComponent, BlockSelection, BlockService, @@ -11,7 +10,7 @@ import { type UIEventHandler, type UIEventStateContext, } from '@blocksuite/block-std'; -import type { BlockModel } from '@blocksuite/store'; +import type { BaseSelection, BlockModel } from '@blocksuite/store'; import { moveBlockConfigs } from './move-block'; import { quickActionConfig } from './quick-action'; diff --git a/blocksuite/affine/block-surface-ref/src/surface-ref-block.ts b/blocksuite/affine/block-surface-ref/src/surface-ref-block.ts index 8e12194916..e3099aad33 100644 --- a/blocksuite/affine/block-surface-ref/src/surface-ref-block.ts +++ b/blocksuite/affine/block-surface-ref/src/surface-ref-block.ts @@ -26,7 +26,6 @@ import { SpecProvider, } from '@blocksuite/affine-shared/utils'; import { - type BaseSelection, BlockComponent, BlockSelection, BlockServiceWatcher, @@ -47,7 +46,7 @@ import { DisposableGroup, type SerializedXYWH, } from '@blocksuite/global/utils'; -import { type Store } from '@blocksuite/store'; +import type { BaseSelection, Store } from '@blocksuite/store'; import { css, html, nothing, type TemplateResult } from 'lit'; import { query, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; diff --git a/blocksuite/affine/block-surface/src/surface-spec.ts b/blocksuite/affine/block-surface/src/surface-spec.ts index 38e519cab4..acd639eda0 100644 --- a/blocksuite/affine/block-surface/src/surface-spec.ts +++ b/blocksuite/affine/block-surface/src/surface-spec.ts @@ -1,4 +1,3 @@ -import { HighlightSelectionExtension } from '@blocksuite/affine-shared/selection'; import { BlockViewExtension, CommandExtension, @@ -23,7 +22,6 @@ const CommonSurfaceBlockSpec: ExtensionType[] = [ FlavourExtension('affine:surface'), SurfaceBlockService, CommandExtension(commands), - HighlightSelectionExtension, MindMapView, EdgelessCRUDExtension, EdgelessLegacySlotExtension, diff --git a/blocksuite/affine/data-view/src/core/common/selection-schema.ts b/blocksuite/affine/data-view/src/core/common/selection-schema.ts index db6ee5bd56..76e0b90393 100644 --- a/blocksuite/affine/data-view/src/core/common/selection-schema.ts +++ b/blocksuite/affine/data-view/src/core/common/selection-schema.ts @@ -1,4 +1,4 @@ -import { BaseSelection, SelectionExtension } from '@blocksuite/block-std'; +import { BaseSelection, SelectionExtension } from '@blocksuite/store'; import { z } from 'zod'; import type { DataViewSelection, GetDataViewSelection } from '../types.js'; diff --git a/blocksuite/affine/shared/src/selection/hightlight.ts b/blocksuite/affine/shared/src/selection/hightlight.ts index a4577c7276..6af6fb016d 100644 --- a/blocksuite/affine/shared/src/selection/hightlight.ts +++ b/blocksuite/affine/shared/src/selection/hightlight.ts @@ -2,7 +2,7 @@ import { type ReferenceParams, ReferenceParamsSchema, } from '@blocksuite/affine-model'; -import { BaseSelection, SelectionExtension } from '@blocksuite/block-std'; +import { BaseSelection, SelectionExtension } from '@blocksuite/store'; export class HighlightSelection extends BaseSelection { static override group = 'scene'; diff --git a/blocksuite/affine/shared/src/selection/image.ts b/blocksuite/affine/shared/src/selection/image.ts index 098c696f44..f2b15a9a9c 100644 --- a/blocksuite/affine/shared/src/selection/image.ts +++ b/blocksuite/affine/shared/src/selection/image.ts @@ -1,4 +1,4 @@ -import { BaseSelection, SelectionExtension } from '@blocksuite/block-std'; +import { BaseSelection, SelectionExtension } from '@blocksuite/store'; import z from 'zod'; const ImageSelectionSchema = z.object({ diff --git a/blocksuite/affine/widget-drag-handle/src/utils.ts b/blocksuite/affine/widget-drag-handle/src/utils.ts index 16988a00ac..20a27f7db1 100644 --- a/blocksuite/affine/widget-drag-handle/src/utils.ts +++ b/blocksuite/affine/widget-drag-handle/src/utils.ts @@ -9,13 +9,9 @@ import { getClosestBlockComponentByPoint, matchFlavours, } from '@blocksuite/affine-shared/utils'; -import type { - BaseSelection, - BlockComponent, - EditorHost, -} from '@blocksuite/block-std'; +import type { BlockComponent, EditorHost } from '@blocksuite/block-std'; import { Point, Rect } from '@blocksuite/global/utils'; -import type { BlockModel } from '@blocksuite/store'; +import type { BaseSelection, BlockModel } from '@blocksuite/store'; import { DRAG_HANDLE_CONTAINER_HEIGHT, diff --git a/blocksuite/affine/widget-remote-selection/src/doc/doc-remote-selection.ts b/blocksuite/affine/widget-remote-selection/src/doc/doc-remote-selection.ts index e6a8e0597f..0df138b957 100644 --- a/blocksuite/affine/widget-remote-selection/src/doc/doc-remote-selection.ts +++ b/blocksuite/affine/widget-remote-selection/src/doc/doc-remote-selection.ts @@ -1,12 +1,11 @@ import { matchFlavours } from '@blocksuite/affine-shared/utils'; import { - type BaseSelection, BlockSelection, TextSelection, WidgetComponent, } from '@blocksuite/block-std'; import { throttle } from '@blocksuite/global/utils'; -import type { UserInfo } from '@blocksuite/store'; +import type { BaseSelection, UserInfo } from '@blocksuite/store'; import { computed, effect } from '@preact/signals-core'; import { css, html, nothing, type PropertyValues } from 'lit'; import { state } from 'lit/decorators.js'; diff --git a/blocksuite/blocks/src/_specs/common.ts b/blocksuite/blocks/src/_specs/common.ts index a814060a4a..24e556f486 100644 --- a/blocksuite/blocks/src/_specs/common.ts +++ b/blocksuite/blocks/src/_specs/common.ts @@ -27,6 +27,10 @@ import { RefNodeSlotsExtension, RichTextExtensions, } from '@blocksuite/affine-components/rich-text'; +import { + HighlightSelectionExtension, + ImageSelectionExtension, +} from '@blocksuite/affine-shared/selection'; import { DefaultOpenDocExtension, DocDisplayMetaService, @@ -34,6 +38,13 @@ import { FeatureFlagService, FontLoaderService, } from '@blocksuite/affine-shared/services'; +import { + BlockSelectionExtension, + CursorSelectionExtension, + SurfaceSelectionExtension, + TextSelectionExtension, +} from '@blocksuite/block-std'; +import { DatabaseSelectionExtension } from '@blocksuite/data-view'; import type { ExtensionType } from '@blocksuite/store'; import { AdapterFactoryExtensions } from '../_common/adapters/extension.js'; @@ -77,4 +88,13 @@ export const EdgelessFirstPartyBlockSpecs: ExtensionType[] = [ FontLoaderService, ].flat(); -export const StoreExtensions: ExtensionType[] = [FeatureFlagService]; +export const StoreExtensions: ExtensionType[] = [ + FeatureFlagService, + BlockSelectionExtension, + TextSelectionExtension, + SurfaceSelectionExtension, + CursorSelectionExtension, + HighlightSelectionExtension, + ImageSelectionExtension, + DatabaseSelectionExtension, +]; diff --git a/blocksuite/blocks/src/root-block/widgets/ai-panel/ai-panel.ts b/blocksuite/blocks/src/root-block/widgets/ai-panel/ai-panel.ts index 028fa6be31..257ace28ad 100644 --- a/blocksuite/blocks/src/root-block/widgets/ai-panel/ai-panel.ts +++ b/blocksuite/blocks/src/root-block/widgets/ai-panel/ai-panel.ts @@ -7,9 +7,9 @@ import { getPageRootByElement, stopPropagation, } from '@blocksuite/affine-shared/utils'; -import type { BaseSelection } from '@blocksuite/block-std'; import { WidgetComponent } from '@blocksuite/block-std'; import { assertExists } from '@blocksuite/global/utils'; +import type { BaseSelection } from '@blocksuite/store'; import { autoPlacement, autoUpdate, diff --git a/blocksuite/blocks/src/root-block/widgets/format-bar/format-bar.ts b/blocksuite/blocks/src/root-block/widgets/format-bar/format-bar.ts index 4d373903e9..8d455afa09 100644 --- a/blocksuite/blocks/src/root-block/widgets/format-bar/format-bar.ts +++ b/blocksuite/blocks/src/root-block/widgets/format-bar/format-bar.ts @@ -9,7 +9,6 @@ import { import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { matchFlavours } from '@blocksuite/affine-shared/utils'; import { - type BaseSelection, type BlockComponent, BlockSelection, CursorSelection, @@ -22,6 +21,7 @@ import { DisposableGroup, nextTick, } from '@blocksuite/global/utils'; +import type { BaseSelection } from '@blocksuite/store'; import { autoUpdate, computePosition, diff --git a/blocksuite/framework/block-std/src/extension/index.ts b/blocksuite/framework/block-std/src/extension/index.ts index 8171ad221c..20f94f8308 100644 --- a/blocksuite/framework/block-std/src/extension/index.ts +++ b/blocksuite/framework/block-std/src/extension/index.ts @@ -4,7 +4,6 @@ export * from './config.js'; export * from './flavour.js'; export * from './keymap.js'; export * from './lifecycle-watcher.js'; -export * from './selection.js'; export * from './service.js'; export * from './service-watcher.js'; export * from './widget-view-map.js'; diff --git a/blocksuite/framework/block-std/src/extension/selection.ts b/blocksuite/framework/block-std/src/extension/selection.ts deleted file mode 100644 index 33162c92b6..0000000000 --- a/blocksuite/framework/block-std/src/extension/selection.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ExtensionType } from '@blocksuite/store'; - -import { SelectionIdentifier } from '../identifier.js'; -import type { SelectionConstructor } from '../selection/index.js'; - -export function SelectionExtension( - selectionCtor: SelectionConstructor -): ExtensionType { - return { - setup: di => { - di.addImpl(SelectionIdentifier(selectionCtor.type), () => selectionCtor); - }, - }; -} diff --git a/blocksuite/framework/block-std/src/identifier.ts b/blocksuite/framework/block-std/src/identifier.ts index 4adf0e4d2d..571d509c38 100644 --- a/blocksuite/framework/block-std/src/identifier.ts +++ b/blocksuite/framework/block-std/src/identifier.ts @@ -4,7 +4,6 @@ import type { Command } from './command/index.js'; import type { EventOptions, UIEventHandler } from './event/index.js'; import type { BlockService, LifeCycleWatcher } from './extension/index.js'; import type { BlockStdScope } from './scope/index.js'; -import type { SelectionConstructor } from './selection/index.js'; import type { BlockViewType, WidgetViewMapType } from './spec/type.js'; export const BlockServiceIdentifier = @@ -33,6 +32,3 @@ export const KeymapIdentifier = createIdentifier<{ getter: (std: BlockStdScope) => Record; options?: EventOptions; }>('Keymap'); - -export const SelectionIdentifier = - createIdentifier('Selection'); diff --git a/blocksuite/framework/block-std/src/range/inline-range-provider.ts b/blocksuite/framework/block-std/src/range/inline-range-provider.ts index 90280f9dae..5466d0fa96 100644 --- a/blocksuite/framework/block-std/src/range/inline-range-provider.ts +++ b/blocksuite/framework/block-std/src/range/inline-range-provider.ts @@ -84,18 +84,21 @@ export const getInlineRangeProvider: ( } }; const inlineRange$: InlineRangeProvider['inlineRange$'] = signal(null); - selectionManager.slots.changed.on(selections => { - const textSelection = selections.find(s => s.type === 'text') as - | TextSelection - | undefined; - const range = rangeManager.value; - if (!range || !textSelection) { - inlineRange$.value = null; - return; - } - const inlineRange = calculateInlineRange(range, textSelection); - inlineRange$.value = inlineRange; - }); + + editorHost.disposables.add( + selectionManager.slots.changed.on(selections => { + const textSelection = selections.find(s => s.type === 'text') as + | TextSelection + | undefined; + const range = rangeManager.value; + if (!range || !textSelection) { + inlineRange$.value = null; + return; + } + const inlineRange = calculateInlineRange(range, textSelection); + inlineRange$.value = inlineRange; + }) + ); return { setInlineRange, diff --git a/blocksuite/framework/block-std/src/range/range-binding.ts b/blocksuite/framework/block-std/src/range/range-binding.ts index 8095d778c2..b3eee6ff2a 100644 --- a/blocksuite/framework/block-std/src/range/range-binding.ts +++ b/blocksuite/framework/block-std/src/range/range-binding.ts @@ -1,7 +1,7 @@ import { throttle } from '@blocksuite/global/utils'; -import type { BlockModel } from '@blocksuite/store'; +import type { BaseSelection, BlockModel } from '@blocksuite/store'; -import { type BaseSelection, TextSelection } from '../selection/index.js'; +import { TextSelection } from '../selection/index.js'; import type { BlockComponent } from '../view/element/block-component.js'; import { BLOCK_ID_ATTR } from '../view/index.js'; import { RANGE_SYNC_EXCLUDE_ATTR } from './consts.js'; @@ -247,6 +247,9 @@ export class RangeBinding { }; private readonly _onStdSelectionChanged = (selections: BaseSelection[]) => { + const closestHost = document.activeElement?.closest('editor-host'); + if (closestHost && closestHost !== this.host) return; + const text = selections.find((selection): selection is TextSelection => selection.is(TextSelection) diff --git a/blocksuite/framework/block-std/src/scope/block-std-scope.ts b/blocksuite/framework/block-std/src/scope/block-std-scope.ts index 2019f13e21..ba220fb617 100644 --- a/blocksuite/framework/block-std/src/scope/block-std-scope.ts +++ b/blocksuite/framework/block-std/src/scope/block-std-scope.ts @@ -5,6 +5,7 @@ import { Job, type JobMiddleware, type Store, + StoreSelectionExtension, } from '@blocksuite/store'; import { Clipboard } from '../clipboard/index.js'; @@ -23,13 +24,6 @@ import { StdIdentifier, } from '../identifier.js'; import { RangeManager } from '../range/index.js'; -import { - BlockSelectionExtension, - CursorSelectionExtension, - SelectionManager, - SurfaceSelectionExtension, - TextSelectionExtension, -} from '../selection/index.js'; import { ServiceManager } from '../service/index.js'; import { EditorHost } from '../view/element/index.js'; import { ViewStore } from '../view/view-store.js'; @@ -43,15 +37,10 @@ const internalExtensions = [ ServiceManager, CommandManager, UIEventDispatcher, - SelectionManager, RangeManager, ViewStore, Clipboard, GfxController, - BlockSelectionExtension, - TextSelectionExtension, - SurfaceSelectionExtension, - CursorSelectionExtension, GfxSelectionManager, SurfaceMiddlewareExtension, ViewManager, @@ -107,7 +96,7 @@ export class BlockStdScope { } get selection() { - return this.get(SelectionManager); + return this.get(StoreSelectionExtension); } get view() { diff --git a/blocksuite/framework/block-std/src/selection/index.ts b/blocksuite/framework/block-std/src/selection/index.ts index cdb6841fb7..8ac7bfc5bc 100644 --- a/blocksuite/framework/block-std/src/selection/index.ts +++ b/blocksuite/framework/block-std/src/selection/index.ts @@ -1,3 +1 @@ -export * from './base.js'; -export * from './manager.js'; export * from './variants/index.js'; diff --git a/blocksuite/framework/block-std/src/selection/variants/block.ts b/blocksuite/framework/block-std/src/selection/variants/block.ts index dbd75a28ec..765ec2a319 100644 --- a/blocksuite/framework/block-std/src/selection/variants/block.ts +++ b/blocksuite/framework/block-std/src/selection/variants/block.ts @@ -1,8 +1,6 @@ +import { BaseSelection, SelectionExtension } from '@blocksuite/store'; import z from 'zod'; -import { SelectionExtension } from '../../extension/selection.js'; -import { BaseSelection } from '../base.js'; - const BlockSelectionSchema = z.object({ blockId: z.string(), }); diff --git a/blocksuite/framework/block-std/src/selection/variants/cursor.ts b/blocksuite/framework/block-std/src/selection/variants/cursor.ts index a6ede90552..9c7d56559e 100644 --- a/blocksuite/framework/block-std/src/selection/variants/cursor.ts +++ b/blocksuite/framework/block-std/src/selection/variants/cursor.ts @@ -1,8 +1,6 @@ +import { BaseSelection, SelectionExtension } from '@blocksuite/store'; import z from 'zod'; -import { SelectionExtension } from '../../extension/selection.js'; -import { BaseSelection } from '../base.js'; - const CursorSelectionSchema = z.object({ x: z.number(), y: z.number(), diff --git a/blocksuite/framework/block-std/src/selection/variants/surface.ts b/blocksuite/framework/block-std/src/selection/variants/surface.ts index 1fe7092e81..60d9c8e074 100644 --- a/blocksuite/framework/block-std/src/selection/variants/surface.ts +++ b/blocksuite/framework/block-std/src/selection/variants/surface.ts @@ -1,8 +1,6 @@ +import { BaseSelection, SelectionExtension } from '@blocksuite/store'; import z from 'zod'; -import { SelectionExtension } from '../../extension/selection.js'; -import { BaseSelection } from '../base.js'; - const SurfaceSelectionSchema = z.object({ blockId: z.string(), elements: z.array(z.string()), diff --git a/blocksuite/framework/block-std/src/selection/variants/text.ts b/blocksuite/framework/block-std/src/selection/variants/text.ts index 29217ead00..e88c1fd3ae 100644 --- a/blocksuite/framework/block-std/src/selection/variants/text.ts +++ b/blocksuite/framework/block-std/src/selection/variants/text.ts @@ -1,8 +1,6 @@ +import { BaseSelection, SelectionExtension } from '@blocksuite/store'; import z from 'zod'; -import { SelectionExtension } from '../../extension/selection.js'; -import { BaseSelection } from '../base.js'; - export type TextRangePoint = { blockId: string; index: number; diff --git a/blocksuite/framework/block-std/src/view/element/lit-host.ts b/blocksuite/framework/block-std/src/view/element/lit-host.ts index fbe660e006..bd90704f9e 100644 --- a/blocksuite/framework/block-std/src/view/element/lit-host.ts +++ b/blocksuite/framework/block-std/src/view/element/lit-host.ts @@ -4,7 +4,11 @@ import { handleError, } from '@blocksuite/global/exceptions'; import { SignalWatcher, Slot, WithDisposable } from '@blocksuite/global/utils'; -import { type BlockModel, Store } from '@blocksuite/store'; +import { + type BlockModel, + Store, + type StoreSelectionExtension, +} from '@blocksuite/store'; import { createContext, provide } from '@lit/context'; import { css, LitElement, nothing, type TemplateResult } from 'lit'; import { property } from 'lit/decorators.js'; @@ -16,7 +20,6 @@ import type { UIEventDispatcher } from '../../event/index.js'; import { WidgetViewMapIdentifier } from '../../identifier.js'; import type { RangeManager } from '../../range/index.js'; import type { BlockStdScope } from '../../scope/block-std-scope.js'; -import type { SelectionManager } from '../../selection/index.js'; import { PropTypes, requiredProperties } from '../decorators/index.js'; import type { ViewStore } from '../view-store.js'; import { BLOCK_ID_ATTR, WIDGET_ID_ATTR } from './consts.js'; @@ -114,7 +117,7 @@ export class EditorHost extends SignalWatcher( return this.std.range; } - get selection(): SelectionManager { + get selection(): StoreSelectionExtension { return this.std.selection; } diff --git a/blocksuite/framework/store/src/extension/index.ts b/blocksuite/framework/store/src/extension/index.ts index 8b7ae56bdb..554a044e13 100644 --- a/blocksuite/framework/store/src/extension/index.ts +++ b/blocksuite/framework/store/src/extension/index.ts @@ -1,2 +1,3 @@ export * from './extension'; +export * from './selection'; export * from './store-extension'; diff --git a/blocksuite/framework/block-std/src/selection/base.ts b/blocksuite/framework/store/src/extension/selection/base.ts similarity index 94% rename from blocksuite/framework/block-std/src/selection/base.ts rename to blocksuite/framework/store/src/extension/selection/base.ts index b2433ccbbd..52a8f84e5c 100644 --- a/blocksuite/framework/block-std/src/selection/base.ts +++ b/blocksuite/framework/store/src/extension/selection/base.ts @@ -1,6 +1,6 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; -import type { SelectionConstructor } from './manager'; +import type { SelectionConstructor } from './types'; export type BaseSelectionOptions = { blockId: string; diff --git a/blocksuite/framework/store/src/extension/selection/identifier.ts b/blocksuite/framework/store/src/extension/selection/identifier.ts new file mode 100644 index 0000000000..24f9fdff38 --- /dev/null +++ b/blocksuite/framework/store/src/extension/selection/identifier.ts @@ -0,0 +1,17 @@ +import { createIdentifier } from '@blocksuite/global/di'; + +import type { ExtensionType } from '../extension'; +import type { SelectionConstructor } from './types'; + +export const SelectionIdentifier = + createIdentifier('Selection'); + +export function SelectionExtension( + selectionCtor: SelectionConstructor +): ExtensionType { + return { + setup: di => { + di.addImpl(SelectionIdentifier(selectionCtor.type), () => selectionCtor); + }, + }; +} diff --git a/blocksuite/framework/store/src/extension/selection/index.ts b/blocksuite/framework/store/src/extension/selection/index.ts new file mode 100644 index 0000000000..aabfea3569 --- /dev/null +++ b/blocksuite/framework/store/src/extension/selection/index.ts @@ -0,0 +1,4 @@ +export * from './base'; +export * from './identifier'; +export * from './selection-extension'; +export * from './types'; diff --git a/blocksuite/framework/block-std/src/selection/manager.ts b/blocksuite/framework/store/src/extension/selection/selection-extension.ts similarity index 62% rename from blocksuite/framework/block-std/src/selection/manager.ts rename to blocksuite/framework/store/src/extension/selection/selection-extension.ts index 9145f87453..258141c1f6 100644 --- a/blocksuite/framework/block-std/src/selection/manager.ts +++ b/blocksuite/framework/store/src/extension/selection/selection-extension.ts @@ -1,28 +1,27 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; -import { DisposableGroup, Slot } from '@blocksuite/global/utils'; -import { nanoid, type StackItem } from '@blocksuite/store'; +import { Slot } from '@blocksuite/global/utils'; import { computed, signal } from '@preact/signals-core'; -import { LifeCycleWatcher } from '../extension/index.js'; -import { SelectionIdentifier } from '../identifier.js'; -import type { BlockStdScope } from '../scope/index.js'; -import type { BaseSelection } from './base.js'; +import type { Store } from '../../model'; +import { nanoid } from '../../utils/id-generator'; +import type { StackItem } from '../../yjs'; +import { StoreExtension } from '../store-extension'; +import type { BaseSelection } from './base'; +import { SelectionIdentifier } from './identifier'; +import type { SelectionConstructor } from './types'; -export interface SelectionConstructor { - type: string; - group: string; +export class StoreSelectionExtension extends StoreExtension { + static override readonly key = 'selection'; - new (...args: any[]): T; - fromJSON(json: Record): T; -} - -export class SelectionManager extends LifeCycleWatcher { - static override readonly key = 'selectionManager'; - - private readonly _id: string; + private readonly _id = `${this.store.id}:${nanoid()}`; + private _selectionConstructors: Record = {}; + private readonly _selections = signal([]); + private readonly _remoteSelections = signal>( + new Map() + ); private readonly _itemAdded = (event: { stackItem: StackItem }) => { - event.stackItem.meta.set('selection-state', this.value); + event.stackItem.meta.set('selection-state', this._selections.value); }; private readonly _itemPopped = (event: { stackItem: StackItem }) => { @@ -43,50 +42,30 @@ export class SelectionManager extends LifeCycleWatcher { return ctor.fromJSON(json); }; - private readonly _remoteSelections = signal>( - new Map() - ); - - private _selectionConstructors: Record = {}; - - private readonly _selections = signal([]); - - disposables = new DisposableGroup(); - slots = { changed: new Slot(), remoteChanged: new Slot>(), }; - private get _store() { - return this.std.workspace.awarenessStore; - } + constructor(store: Store) { + super(store); - get id() { - return this._id; - } + this.store.provider.getAll(SelectionIdentifier).forEach(ctor => { + [ctor].flat().forEach(ctor => { + this._selectionConstructors[ctor.type] = ctor; + }); + }); - get remoteSelections() { - return this._remoteSelections.value; - } - - get value() { - return this._selections.value; - } - - constructor(std: BlockStdScope) { - super(std); - this._id = `${this.std.store.id}:${nanoid()}`; - this._setupDefaultSelections(); - this._store.awareness.on( + this.store.awarenessStore.awareness.on( 'change', (change: { updated: number[]; added: number[]; removed: number[] }) => { const all = change.updated.concat(change.added).concat(change.removed); - const localClientID = this._store.awareness.clientID; + const localClientID = this.store.awarenessStore.awareness.clientID; const exceptLocal = all.filter(id => id !== localClientID); const hasLocal = all.includes(localClientID); if (hasLocal) { - const localSelectionJson = this._store.getLocalSelection(this.id); + const localSelectionJson = + this.store.awarenessStore.getLocalSelection(this._id); const localSelection = localSelectionJson.map(json => { return this._jsonToSelection(json); }); @@ -96,11 +75,11 @@ export class SelectionManager extends LifeCycleWatcher { // Only consider remote selections from other clients if (exceptLocal.length > 0) { const map = new Map(); - this._store.getStates().forEach((state, id) => { - if (id === this._store.awareness.clientID) return; + this.store.awarenessStore.getStates().forEach((state, id) => { + if (id === this.store.awarenessStore.awareness.clientID) return; // selection id starts with the same block collection id from others clients would be considered as remote selections const selection = Object.entries(state.selectionV2) - .filter(([key]) => key.startsWith(this.std.store.id)) + .filter(([key]) => key.startsWith(this.store.id)) .flatMap(([_, selection]) => selection); const selections = selection @@ -122,15 +101,21 @@ export class SelectionManager extends LifeCycleWatcher { map.set(id, selections); }); this._remoteSelections.value = map; + this.slots.remoteChanged.emit(map); } } ); + + this.store.history.on('stack-item-added', this._itemAdded); + this.store.history.on('stack-item-popped', this._itemPopped); } - private _setupDefaultSelections() { - this.std.provider.getAll(SelectionIdentifier).forEach(ctor => { - this.register(ctor); - }); + get value() { + return this._selections.value; + } + + get remoteSelections() { + return this._remoteSelections.value; } clear(types?: string[]) { @@ -151,9 +136,8 @@ export class SelectionManager extends LifeCycleWatcher { return new Type(...args) as InstanceType; } - dispose() { - Object.values(this.slots).forEach(slot => slot.dispose()); - this.disposables.dispose(); + getGroup(group: string) { + return this.value.filter(s => s.group === group); } filter(type: T) { @@ -176,43 +160,9 @@ export class SelectionManager extends LifeCycleWatcher { ); } - fromJSON(json: Record[]) { - const selections = json.map(json => { - return this._jsonToSelection(json); - }); - return this.set(selections); - } - - getGroup(group: string) { - return this.value.filter(s => s.group === group); - } - - override mounted() { - if (this.disposables.disposed) { - this.disposables = new DisposableGroup(); - } - this.std.store.history.on('stack-item-added', this._itemAdded); - this.std.store.history.on('stack-item-popped', this._itemPopped); - this.disposables.add( - this._store.slots.update.on(({ id }) => { - if (id === this._store.awareness.clientID) { - return; - } - this.slots.remoteChanged.emit(this.remoteSelections); - }) - ); - } - - register(ctor: SelectionConstructor | SelectionConstructor[]) { - [ctor].flat().forEach(ctor => { - this._selectionConstructors[ctor.type] = ctor; - }); - return this; - } - set(selections: BaseSelection[]) { - this._store.setLocalSelection( - this.id, + this.store.awarenessStore.setLocalSelection( + this._id, selections.map(s => s.toJSON()) ); this.slots.changed.emit(selections); @@ -223,16 +173,15 @@ export class SelectionManager extends LifeCycleWatcher { this.set([...current, ...selections]); } - override unmounted() { - this.std.store.history.off('stack-item-added', this._itemAdded); - this.std.store.history.off('stack-item-popped', this._itemPopped); - this.slots.changed.dispose(); - this.disposables.dispose(); - this.clear(); - } - update(fn: (currentSelections: BaseSelection[]) => BaseSelection[]) { const selections = fn(this.value); this.set(selections); } + + fromJSON(json: Record[]) { + const selections = json.map(json => { + return this._jsonToSelection(json); + }); + return this.set(selections); + } } diff --git a/blocksuite/framework/store/src/extension/selection/types.ts b/blocksuite/framework/store/src/extension/selection/types.ts new file mode 100644 index 0000000000..eea8b6c544 --- /dev/null +++ b/blocksuite/framework/store/src/extension/selection/types.ts @@ -0,0 +1,9 @@ +import type { BaseSelection } from './base'; + +export interface SelectionConstructor { + type: string; + group: string; + + new (...args: any[]): T; + fromJSON(json: Record): T; +} diff --git a/blocksuite/framework/store/src/model/store/store.ts b/blocksuite/framework/store/src/model/store/store.ts index 2f028aba92..cc10aed1ae 100644 --- a/blocksuite/framework/store/src/model/store/store.ts +++ b/blocksuite/framework/store/src/model/store/store.ts @@ -4,6 +4,7 @@ import { type Disposable, Slot } from '@blocksuite/global/utils'; import { signal } from '@preact/signals-core'; import type { ExtensionType } from '../../extension/extension.js'; +import { StoreSelectionExtension } from '../../extension/index.js'; import type { Schema } from '../../schema/index.js'; import { Block, @@ -27,29 +28,33 @@ export type StoreOptions = { extensions?: ExtensionType[]; }; +const internalExtensions = [StoreSelectionExtension]; + export class Store { + readonly userExtensions: ExtensionType[]; + private readonly _provider: ServiceProvider; private readonly _runQuery = (block: Block) => { runQuery(this._query, block); }; - protected readonly _doc: Doc; + private readonly _doc: Doc; - protected readonly _blocks = signal>({}); + private readonly _blocks = signal>({}); - protected readonly _crud: DocCRUD; + private readonly _crud: DocCRUD; - protected readonly _disposeBlockUpdated: Disposable; + private readonly _disposeBlockUpdated: Disposable; - protected readonly _query: Query = { + private readonly _query: Query = { match: [], mode: 'loose', }; - protected _readonly = signal(false); + private readonly _readonly = signal(false); - protected readonly _schema: Schema; + private readonly _schema: Schema; readonly slots: Doc['slots'] & { /** This is always triggered after `doc.load` is called. */ @@ -295,7 +300,12 @@ export class Store { const container = new Container(); container.addImpl(StoreIdentifier, () => this); + internalExtensions.forEach(ext => { + ext.setup(container); + }); + const userExtensions = extensions ?? []; + this.userExtensions = userExtensions; userExtensions.forEach(extension => { extension.setup(container); }); diff --git a/blocksuite/presets/src/__tests__/utils/setup.ts b/blocksuite/presets/src/__tests__/utils/setup.ts index bee73f382f..9c2066ecec 100644 --- a/blocksuite/presets/src/__tests__/utils/setup.ts +++ b/blocksuite/presets/src/__tests__/utils/setup.ts @@ -9,8 +9,8 @@ effects(); import { CommunityCanvasTextFonts, type DocMode, - FeatureFlagService, FontConfigExtension, + StoreExtensions, } from '@blocksuite/blocks'; import { AffineSchemas } from '@blocksuite/blocks/schemas'; import { assertExists } from '@blocksuite/global/utils'; @@ -85,7 +85,7 @@ async function createEditor(collection: TestWorkspace, mode: DocMode = 'page') { export async function setupEditor(mode: DocMode = 'page') { const collection = new TestWorkspace(createCollectionOptions()); - collection.storeExtensions = [FeatureFlagService]; + collection.storeExtensions = StoreExtensions; collection.meta.initialize(); window.collection = collection; diff --git a/blocksuite/tests-legacy/edgeless/note/note.spec.ts b/blocksuite/tests-legacy/edgeless/note/note.spec.ts index d1c092ba0a..f8b8de7c8d 100644 --- a/blocksuite/tests-legacy/edgeless/note/note.spec.ts +++ b/blocksuite/tests-legacy/edgeless/note/note.spec.ts @@ -329,7 +329,6 @@ test('cursor for active and inactive state', async ({ page }) => { await switchEditorMode(page); - await assertTextSelection(page); await page.mouse.click(CENTER_X, CENTER_Y); await waitNextFrame(page); await assertTextSelection(page); diff --git a/blocksuite/tests-legacy/edgeless/paste-block.spec.ts b/blocksuite/tests-legacy/edgeless/paste-block.spec.ts index 1813e62f97..69b7cd5ce3 100644 --- a/blocksuite/tests-legacy/edgeless/paste-block.spec.ts +++ b/blocksuite/tests-legacy/edgeless/paste-block.spec.ts @@ -43,6 +43,7 @@ test.describe('pasting blocks', () => { await focusRichText(page); await initContent(page); await switchEditorMode(page); + await click(page, { x: 0, y: 0 }); const box = await getNoteBoundBoxInEdgeless(page, noteId); await click(page, { x: box.x + 10, diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts index b2b578f098..543c7e4def 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/chat-panel-messages.ts @@ -1,4 +1,4 @@ -import type { BaseSelection, EditorHost } from '@blocksuite/affine/block-std'; +import type { EditorHost } from '@blocksuite/affine/block-std'; import { ShadowlessElement } from '@blocksuite/affine/block-std'; import { type AIError, @@ -9,6 +9,7 @@ import { UnauthorizedError, } from '@blocksuite/affine/blocks'; import { WithDisposable } from '@blocksuite/affine/global/utils'; +import type { BaseSelection } from '@blocksuite/affine/store'; import { css, html, nothing } from 'lit'; import { property, query, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts index b8b28e9443..949f1e5a76 100644 --- a/tests/affine-local/e2e/links.spec.ts +++ b/tests/affine-local/e2e/links.spec.ts @@ -62,6 +62,7 @@ test('not allowed to switch to embed view when linking to the same document', as await expect(peekViewModel.locator('page-editor')).toBeVisible(); await page.keyboard.press('Escape'); await expect(peekViewModel).not.toBeVisible(); + await page.click('body'); await cardLink.click(); await cardToolbar.getByLabel('Switch view').click(); @@ -103,6 +104,7 @@ test('not allowed to switch to embed view when linking to block', async ({ await page.keyboard.press('Escape'); await expect(peekViewModel).not.toBeVisible(); + await page.click('body'); await cardLink.click(); await cardToolbar.getByLabel('More').click(); @@ -131,6 +133,7 @@ test('not allowed to switch to embed view when linking to block', async ({ await page.keyboard.press('Escape'); await expect(peekViewModel).not.toBeVisible(); + await page.click('body'); await otherCardLink.click(); await cardToolbar.getByLabel('Switch view').click();