feat(editor): implement view extension manager with builder pattern (#12193)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Refactor**
  - Streamlined and modularized the configuration of editor and theme extensions, introducing a chainable API for extension management.
  - Migrated extension setup to use new provider classes and centralized patch logic, improving maintainability and consistency.
  - Updated internal extension retrieval processes to use a more explicit, stepwise initialization sequence.
  - Removed legacy theme and editor config registration from common views and editor setups.
  - Removed direct patch registrations from editor views, consolidating them into extension providers.
  - Renamed classes and variables for clarity and consistency across the codebase.

- **New Features**
  - Added new extension providers for editor configuration, theme management, and edgeless block header customization, enabling more flexible and validated extension registration.
  - Introduced animated viewport focus and dynamic header rendering for edgeless notes and embedded synced documents.
  - Integrated reactive editor settings and toolbar configurations with workspace-aware base URL resolution.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saul-Mirone
2025-05-09 10:26:05 +00:00
parent 7b6e00d84a
commit 8704c98c7e
16 changed files with 521 additions and 313 deletions

View File

@@ -43,8 +43,10 @@ import type {
AffineAIPanelWidgetConfig, AffineAIPanelWidgetConfig,
} from '../widgets/ai-panel/type'; } from '../widgets/ai-panel/type';
export const getCustomPageEditorBlockSpecs: () => ExtensionType[] = () => [ export const getCustomPageEditorBlockSpecs: () => ExtensionType[] = () => {
...getViewManager().get('page'), const manager = getViewManager().config.init().value;
return [
...manager.get('page'),
{ {
setup: di => { setup: di => {
di.override( di.override(
@@ -53,7 +55,8 @@ export const getCustomPageEditorBlockSpecs: () => ExtensionType[] = () => [
); );
}, },
}, },
]; ];
};
const customHeadingStyles = css` const customHeadingStyles = css`
.custom-heading { .custom-heading {

View File

@@ -163,7 +163,13 @@ const usePreviewExtensions = () => {
const enableAI = useEnableAI(); const enableAI = useEnableAI();
const extensions = useMemo(() => { const extensions = useMemo(() => {
const manager = getViewManager(framework, enableAI); const manager = getViewManager()
.config.init()
.common(framework, enableAI)
.theme(framework)
.database(framework)
.linkedDoc(framework)
.paragraph(enableAI).value;
const specs = manager.get('preview-page'); const specs = manager.get('preview-page');
return [...specs, patchReferenceRenderer(reactToLit, referenceRenderer)]; return [...specs, patchReferenceRenderer(reactToLit, referenceRenderer)];
}, [reactToLit, referenceRenderer, framework, enableAI]); }, [reactToLit, referenceRenderer, framework, enableAI]);

View File

@@ -6,7 +6,6 @@ import {
LitEdgelessEditor, LitEdgelessEditor,
type PageEditor, type PageEditor,
} from '@affine/core/blocksuite/editors'; } from '@affine/core/blocksuite/editors';
import type { AffineEditorViewOptions } from '@affine/core/blocksuite/manager/editor-view';
import { getViewManager } from '@affine/core/blocksuite/manager/migrating-view'; import { getViewManager } from '@affine/core/blocksuite/manager/migrating-view';
import { useEnableAI } from '@affine/core/components/hooks/affine/use-enable-ai'; import { useEnableAI } from '@affine/core/components/hooks/affine/use-enable-ai';
import type { DocCustomPropertyInfo } from '@affine/core/modules/db'; import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
@@ -22,9 +21,8 @@ import { WorkspaceService } from '@affine/core/modules/workspace';
import track from '@affine/track'; import track from '@affine/track';
import type { DocTitle } from '@blocksuite/affine/fragments/doc-title'; import type { DocTitle } from '@blocksuite/affine/fragments/doc-title';
import type { DocMode } from '@blocksuite/affine/model'; import type { DocMode } from '@blocksuite/affine/model';
import type { ExtensionType, Store } from '@blocksuite/affine/store'; import type { Store } from '@blocksuite/affine/store';
import { import {
type FrameworkProvider,
useFramework, useFramework,
useLiveData, useLiveData,
useService, useService,
@@ -69,7 +67,7 @@ const usePatchSpecs = (mode: DocMode) => {
const enableAI = useEnableAI(); const enableAI = useEnableAI();
const insidePeekView = useInsidePeekView(); const isInPeekView = useInsidePeekView();
const enableTurboRenderer = useLiveData( const enableTurboRenderer = useLiveData(
featureFlagService.flags.enable_turbo_renderer.$ featureFlagService.flags.enable_turbo_renderer.$
@@ -81,33 +79,51 @@ const usePatchSpecs = (mode: DocMode) => {
) )
); );
const editorOptions: AffineEditorViewOptions = useMemo(() => { const patchedSpecs = useMemo(() => {
return { const manager = getViewManager()
.config.init()
.common(framework, enableAI)
.theme(framework)
.editorConfig(framework)
.editorView({
isCloud, isCloud,
isInPeekView: insidePeekView, isInPeekView,
enableTurboRenderer, enableTurboRenderer,
enablePDFEmbedPreview, enablePDFEmbedPreview,
framework, framework,
reactToLit,
reactToLit: reactToLit as AffineEditorViewOptions['reactToLit'],
confirmModal, confirmModal,
}; })
.edgelessBlockHeader({
framework,
isInPeekView,
reactToLit,
})
.database(framework)
.linkedDoc(framework)
.paragraph(enableAI).value;
if (BUILD_CONFIG.isMobileEdition) {
if (mode === 'page') {
return manager.get('mobile-page');
} else {
return manager.get('mobile-edgeless');
}
} else {
return manager.get(mode);
}
}, [ }, [
confirmModal, confirmModal,
enableAI,
enablePDFEmbedPreview, enablePDFEmbedPreview,
enableTurboRenderer, enableTurboRenderer,
framework, framework,
insidePeekView, isInPeekView,
isCloud, isCloud,
mode,
reactToLit, reactToLit,
]); ]);
const patchedSpecs = useMemo(() => {
return enableEditorExtension(framework, mode, enableAI, editorOptions);
}, [framework, mode, enableAI, editorOptions]);
return [ return [
patchedSpecs, patchedSpecs,
useMemo( useMemo(
@@ -302,20 +318,3 @@ export const BlocksuiteEdgelessEditor = forwardRef<
</div> </div>
); );
}); });
function enableEditorExtension(
framework: FrameworkProvider,
mode: 'edgeless' | 'page',
enableAI: boolean,
options: AffineEditorViewOptions
): ExtensionType[] {
const manager = getViewManager(framework, enableAI, options);
if (BUILD_CONFIG.isMobileEdition) {
if (mode === 'page') {
return manager.get('mobile-page');
}
return manager.get('mobile-edgeless');
}
return manager.get(mode);
}

View File

@@ -1,110 +1,42 @@
import type { ReactToLit } from '@affine/component'; import type { ElementOrFactory } from '@affine/component';
import { JournalService } from '@affine/core/modules/journal';
import { EmbedSyncedDocConfigExtension } from '@blocksuite/affine/blocks/embed-doc';
import { NoteConfigExtension } from '@blocksuite/affine/blocks/note';
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine/blocks/root';
import { Bound, Vec } from '@blocksuite/affine/global/gfx';
import { import {
DocModeProvider, patchForEdgelessNoteConfig,
EditPropsStore, patchForEmbedSyncedDocConfig,
} from '@blocksuite/affine/shared/services'; } from '@affine/core/blocksuite/extensions/edgeless-block-header/patch';
import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx'; import {
import type { FrameworkProvider } from '@toeverything/infra'; type ViewExtensionContext,
import { html } from 'lit'; ViewExtensionProvider,
} from '@blocksuite/affine/ext-loader';
import { FrameworkProvider } from '@toeverything/infra';
import type { TemplateResult } from 'lit';
import { z } from 'zod';
import { BlocksuiteEditorJournalDocTitle } from '../../block-suite-editor/journal-doc-title'; const optionsSchema = z.object({
import { EdgelessEmbedSyncedDocHeader } from './edgeless-embed-synced-doc-header'; isInPeekView: z.boolean(),
import { EdgelessNoteHeader } from './edgeless-note-header'; framework: z.instanceof(FrameworkProvider),
reactToLit: z
.function()
.args(z.custom<ElementOrFactory>(), z.boolean().optional())
.returns(z.custom<TemplateResult>()),
});
export function patchForEdgelessNoteConfig( export type EdgelessBlockHeaderViewOptions = z.infer<typeof optionsSchema>;
framework: FrameworkProvider,
reactToLit: ReactToLit, export class EdgelessBlockHeaderConfigViewExtension extends ViewExtensionProvider<EdgelessBlockHeaderViewOptions> {
insidePeekView: boolean override name = 'header-config-view';
) { override schema = optionsSchema;
return NoteConfigExtension({
edgelessNoteHeader: ({ note }) => override setup(
reactToLit(<EdgelessNoteHeader note={note} />), context: ViewExtensionContext,
pageBlockTitle: ({ note }) => { options?: EdgelessBlockHeaderViewOptions
const journalService = framework.get(JournalService); ) {
const isJournal = !!journalService.journalDate$(note.store.id).value; super.setup(context, options);
if (isJournal) { if (!options) return;
return reactToLit( const { framework, isInPeekView, reactToLit } = options;
<BlocksuiteEditorJournalDocTitle page={note.store} />
context.register(
patchForEdgelessNoteConfig(framework, reactToLit, isInPeekView)
); );
} else { context.register(patchForEmbedSyncedDocConfig(reactToLit));
return html`<doc-title .doc=${note.store}></doc-title>`;
} }
},
pageBlockViewportFitAnimation: insidePeekView
? undefined
: ({ std, note }) => {
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
// if there is a stored viewport, don't run the animation
// in other word, this doc has been opened before
if (storedViewport) return false;
if (!std.store.root) return false;
const rootView = std.view.getBlock(std.store.root.id);
if (!rootView) return false;
const gfx = std.get(GfxControllerIdentifier);
const primaryMode = std
.get(DocModeProvider)
.getPrimaryMode(std.store.id);
if (primaryMode !== 'page' || !note || note.props.edgeless.collapse) {
return false;
}
const leftPadding = parseInt(
window
.getComputedStyle(rootView)
.getPropertyValue('--affine-editor-side-padding')
.replace('px', '')
);
if (isNaN(leftPadding)) {
return false;
}
let editorWidth = parseInt(
window
.getComputedStyle(rootView)
.getPropertyValue('--affine-editor-width')
.replace('px', '')
);
if (isNaN(editorWidth)) {
return false;
}
const containerWidth = rootView.getBoundingClientRect().width;
const leftMargin =
containerWidth > editorWidth
? (containerWidth - editorWidth) / 2
: 0;
const pageTitleAnchor = gfx.viewport.toModelCoord(
leftPadding + leftMargin,
0
);
const noteBound = Bound.deserialize(note.xywh);
const edgelessTitleAnchor = Vec.add(noteBound.tl, [
EDGELESS_BLOCK_CHILD_PADDING,
12,
]);
const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor);
gfx.viewport.setCenter(center[0], center[1]);
gfx.viewport.smoothZoom(0.65, undefined, 15);
return true;
},
});
}
export function patchForEmbedSyncedDocConfig(reactToLit: ReactToLit) {
return EmbedSyncedDocConfigExtension({
edgelessHeader: ({ model, std }) =>
reactToLit(<EdgelessEmbedSyncedDocHeader model={model} std={std} />),
});
} }

View File

@@ -0,0 +1,110 @@
import type { ReactToLit } from '@affine/component';
import { JournalService } from '@affine/core/modules/journal';
import { EmbedSyncedDocConfigExtension } from '@blocksuite/affine/blocks/embed-doc';
import { NoteConfigExtension } from '@blocksuite/affine/blocks/note';
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine/blocks/root';
import { Bound, Vec } from '@blocksuite/affine/global/gfx';
import {
DocModeProvider,
EditPropsStore,
} from '@blocksuite/affine/shared/services';
import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx';
import type { FrameworkProvider } from '@toeverything/infra';
import { html } from 'lit';
import { BlocksuiteEditorJournalDocTitle } from '../../block-suite-editor/journal-doc-title';
import { EdgelessEmbedSyncedDocHeader } from './edgeless-embed-synced-doc-header';
import { EdgelessNoteHeader } from './edgeless-note-header';
export function patchForEdgelessNoteConfig(
framework: FrameworkProvider,
reactToLit: ReactToLit,
insidePeekView: boolean
) {
return NoteConfigExtension({
edgelessNoteHeader: ({ note }) =>
reactToLit(<EdgelessNoteHeader note={note} />),
pageBlockTitle: ({ note }) => {
const journalService = framework.get(JournalService);
const isJournal = !!journalService.journalDate$(note.store.id).value;
if (isJournal) {
return reactToLit(
<BlocksuiteEditorJournalDocTitle page={note.store} />
);
} else {
return html`<doc-title .doc=${note.store}></doc-title>`;
}
},
pageBlockViewportFitAnimation: insidePeekView
? undefined
: ({ std, note }) => {
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
// if there is a stored viewport, don't run the animation
// in other word, this doc has been opened before
if (storedViewport) return false;
if (!std.store.root) return false;
const rootView = std.view.getBlock(std.store.root.id);
if (!rootView) return false;
const gfx = std.get(GfxControllerIdentifier);
const primaryMode = std
.get(DocModeProvider)
.getPrimaryMode(std.store.id);
if (primaryMode !== 'page' || !note || note.props.edgeless.collapse) {
return false;
}
const leftPadding = parseInt(
window
.getComputedStyle(rootView)
.getPropertyValue('--affine-editor-side-padding')
.replace('px', '')
);
if (isNaN(leftPadding)) {
return false;
}
let editorWidth = parseInt(
window
.getComputedStyle(rootView)
.getPropertyValue('--affine-editor-width')
.replace('px', '')
);
if (isNaN(editorWidth)) {
return false;
}
const containerWidth = rootView.getBoundingClientRect().width;
const leftMargin =
containerWidth > editorWidth
? (containerWidth - editorWidth) / 2
: 0;
const pageTitleAnchor = gfx.viewport.toModelCoord(
leftPadding + leftMargin,
0
);
const noteBound = Bound.deserialize(note.xywh);
const edgelessTitleAnchor = Vec.add(noteBound.tl, [
EDGELESS_BLOCK_CHILD_PADDING,
12,
]);
const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor);
gfx.viewport.setCenter(center[0], center[1]);
gfx.viewport.smoothZoom(0.65, undefined, 15);
return true;
},
});
}
export function patchForEmbedSyncedDocConfig(reactToLit: ReactToLit) {
return EmbedSyncedDocConfigExtension({
edgelessHeader: ({ model, std }) =>
reactToLit(<EdgelessEmbedSyncedDocHeader model={model} std={std} />),
});
}

View File

@@ -0,0 +1,29 @@
import {
createCustomToolbarExtension,
createToolbarMoreMenuConfig,
} from '@affine/core/blocksuite/extensions/editor-config/toolbar';
import { WorkspaceServerService } from '@affine/core/modules/cloud';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { ToolbarMoreMenuConfigExtension } from '@blocksuite/affine/components/toolbar';
import { EditorSettingExtension } from '@blocksuite/affine/shared/services';
import type { ExtensionType } from '@blocksuite/store';
import type { FrameworkProvider } from '@toeverything/infra';
export function getEditorConfigExtension(
framework: FrameworkProvider
): ExtensionType[] {
const editorSettingService = framework.get(EditorSettingService);
const workspaceServerService = framework.get(WorkspaceServerService);
const baseUrl = workspaceServerService.server?.baseUrl ?? location.origin;
return [
EditorSettingExtension({
// eslint-disable-next-line rxjs/finnish
setting$: editorSettingService.editorSetting.settingSignal,
set: (k, v) => editorSettingService.editorSetting.set(k, v),
}),
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
createCustomToolbarExtension(editorSettingService.editorSetting, baseUrl),
].flat();
}

View File

@@ -1,30 +1,34 @@
import { WorkspaceServerService } from '@affine/core/modules/cloud'; import { getEditorConfigExtension } from '@affine/core/blocksuite/extensions/editor-config/get-config';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { ToolbarMoreMenuConfigExtension } from '@blocksuite/affine/components/toolbar';
import { EditorSettingExtension } from '@blocksuite/affine/shared/services';
import type { ExtensionType } from '@blocksuite/affine/store';
import type { FrameworkProvider } from '@toeverything/infra';
import { import {
createCustomToolbarExtension, type ViewExtensionContext,
createToolbarMoreMenuConfig, ViewExtensionProvider,
} from './toolbar'; } from '@blocksuite/affine/ext-loader';
import { FrameworkProvider } from '@toeverything/infra';
import { z } from 'zod';
export function getEditorConfigExtension( const optionsSchema = z.object({
framework: FrameworkProvider framework: z.instanceof(FrameworkProvider).optional(),
): ExtensionType[] { });
const editorSettingService = framework.get(EditorSettingService);
const workspaceServerService = framework.get(WorkspaceServerService);
const baseUrl = workspaceServerService.server?.baseUrl ?? location.origin;
return [ type AffineEditorConfigViewOptions = z.infer<typeof optionsSchema>;
EditorSettingExtension({
// eslint-disable-next-line rxjs/finnish
setting$: editorSettingService.editorSetting.settingSignal,
set: (k, v) => editorSettingService.editorSetting.set(k, v),
}),
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
createCustomToolbarExtension(editorSettingService.editorSetting, baseUrl), export class AffineEditorConfigViewExtension extends ViewExtensionProvider<AffineEditorConfigViewOptions> {
].flat(); override name = 'affine-view-editor-config';
override schema = optionsSchema;
override setup(
context: ViewExtensionContext,
options?: AffineEditorConfigViewOptions
) {
super.setup(context, options);
const framework = options?.framework;
if (!framework) {
return;
}
if (context.scope === 'edgeless' || context.scope === 'page') {
context.register(getEditorConfigExtension(framework));
}
}
} }

View File

@@ -0,0 +1,37 @@
import { getPreviewThemeExtension } from '@affine/core/blocksuite/extensions/theme/preview-theme';
import { getThemeExtension } from '@affine/core/blocksuite/extensions/theme/theme';
import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine/ext-loader';
import { FrameworkProvider } from '@toeverything/infra';
import { z } from 'zod';
const optionsSchema = z.object({
framework: z.instanceof(FrameworkProvider).optional(),
});
type AffineThemeViewOptions = z.infer<typeof optionsSchema>;
export class AffineThemeViewExtension extends ViewExtensionProvider<AffineThemeViewOptions> {
override name = 'affine-view-theme';
override schema = optionsSchema;
override setup(
context: ViewExtensionContext,
options?: AffineThemeViewOptions
) {
super.setup(context, options);
const framework = options?.framework;
if (!framework) {
return;
}
if (this.isPreview(context.scope)) {
context.register(getPreviewThemeExtension(framework));
} else {
context.register(getThemeExtension(framework));
}
}
}

View File

@@ -0,0 +1,73 @@
import { AppThemeService } from '@affine/core/modules/theme';
import { ColorScheme } from '@blocksuite/affine/model';
import {
type ThemeExtension,
ThemeExtensionIdentifier,
} from '@blocksuite/affine/shared/services';
import {
createSignalFromObservable,
type Signal,
} from '@blocksuite/affine/shared/utils';
import {
type BlockStdScope,
LifeCycleWatcher,
StdIdentifier,
} from '@blocksuite/affine/std';
import type { Container } from '@blocksuite/global/di';
import type { FrameworkProvider } from '@toeverything/infra';
import type { Observable } from 'rxjs';
export function getPreviewThemeExtension(framework: FrameworkProvider) {
class AffinePagePreviewThemeExtension
extends LifeCycleWatcher
implements ThemeExtension
{
static override readonly key = 'affine-page-preview-theme';
readonly theme: Signal<ColorScheme>;
readonly disposables: (() => void)[] = [];
static override setup(di: Container) {
super.setup(di);
di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [
StdIdentifier,
]);
}
constructor(std: BlockStdScope) {
super(std);
const theme$: Observable<ColorScheme> = framework
.get(AppThemeService)
.appTheme.theme$.map(theme => {
return theme === ColorScheme.Dark
? ColorScheme.Dark
: ColorScheme.Light;
});
const { signal, cleanup } = createSignalFromObservable<ColorScheme>(
theme$,
ColorScheme.Light
);
this.theme = signal;
this.disposables.push(cleanup);
}
getAppTheme() {
return this.theme;
}
getEdgelessTheme() {
return this.theme;
}
override unmounted() {
this.dispose();
}
dispose() {
this.disposables.forEach(dispose => dispose());
}
}
return AffinePagePreviewThemeExtension;
}

View File

@@ -10,11 +10,7 @@ import {
createSignalFromObservable, createSignalFromObservable,
type Signal, type Signal,
} from '@blocksuite/affine/shared/utils'; } from '@blocksuite/affine/shared/utils';
import { import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/std';
type BlockStdScope,
LifeCycleWatcher,
StdIdentifier,
} from '@blocksuite/affine/std';
import { type FrameworkProvider } from '@toeverything/infra'; import { type FrameworkProvider } from '@toeverything/infra';
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import { combineLatest, map } from 'rxjs'; import { combineLatest, map } from 'rxjs';
@@ -99,58 +95,3 @@ export function getThemeExtension(
return AffineThemeExtension; return AffineThemeExtension;
} }
export function getPreviewThemeExtension(framework: FrameworkProvider) {
class AffinePagePreviewThemeExtension
extends LifeCycleWatcher
implements ThemeExtension
{
static override readonly key = 'affine-page-preview-theme';
readonly theme: Signal<ColorScheme>;
readonly disposables: (() => void)[] = [];
static override setup(di: Container) {
super.setup(di);
di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [
StdIdentifier,
]);
}
constructor(std: BlockStdScope) {
super(std);
const theme$: Observable<ColorScheme> = framework
.get(AppThemeService)
.appTheme.theme$.map(theme => {
return theme === ColorScheme.Dark
? ColorScheme.Dark
: ColorScheme.Light;
});
const { signal, cleanup } = createSignalFromObservable<ColorScheme>(
theme$,
ColorScheme.Light
);
this.theme = signal;
this.disposables.push(cleanup);
}
getAppTheme() {
return this.theme;
}
getEdgelessTheme() {
return this.theme;
}
override unmounted() {
this.dispose();
}
dispose() {
this.disposables.forEach(dispose => dispose());
}
}
return AffinePagePreviewThemeExtension;
}

View File

@@ -8,7 +8,7 @@ import {
effects as htmlPreviewEffects, effects as htmlPreviewEffects,
} from '../extensions/code-block-preview/html-preview'; } from '../extensions/code-block-preview/html-preview';
export class CodeBlockPreviewExtensionProvider extends ViewExtensionProvider { export class CodeBlockPreviewViewExtension extends ViewExtensionProvider {
override name = 'code-block-preview'; override name = 'code-block-preview';
override effect() { override effect() {

View File

@@ -11,15 +11,10 @@ import { CopilotTool } from '@affine/core/blocksuite/ai/tool/copilot-tool';
import { aiPanelWidget } from '@affine/core/blocksuite/ai/widgets/ai-panel/ai-panel'; import { aiPanelWidget } from '@affine/core/blocksuite/ai/widgets/ai-panel/ai-panel';
import { edgelessCopilotWidget } from '@affine/core/blocksuite/ai/widgets/edgeless-copilot'; import { edgelessCopilotWidget } from '@affine/core/blocksuite/ai/widgets/edgeless-copilot';
import { buildDocDisplayMetaExtension } from '@affine/core/blocksuite/extensions/display-meta'; import { buildDocDisplayMetaExtension } from '@affine/core/blocksuite/extensions/display-meta';
import { getEditorConfigExtension } from '@affine/core/blocksuite/extensions/editor-config';
import { patchFileSizeLimitExtension } from '@affine/core/blocksuite/extensions/file-size-limit'; import { patchFileSizeLimitExtension } from '@affine/core/blocksuite/extensions/file-size-limit';
import { getFontConfigExtension } from '@affine/core/blocksuite/extensions/font-config'; import { getFontConfigExtension } from '@affine/core/blocksuite/extensions/font-config';
import { patchPeekViewService } from '@affine/core/blocksuite/extensions/peek-view-service'; import { patchPeekViewService } from '@affine/core/blocksuite/extensions/peek-view-service';
import { getTelemetryExtension } from '@affine/core/blocksuite/extensions/telemetry'; import { getTelemetryExtension } from '@affine/core/blocksuite/extensions/telemetry';
import {
getPreviewThemeExtension,
getThemeExtension,
} from '@affine/core/blocksuite/extensions/theme';
import { PeekViewService } from '@affine/core/modules/peek-view'; import { PeekViewService } from '@affine/core/modules/peek-view';
import { import {
type ViewExtensionContext, type ViewExtensionContext,
@@ -42,17 +37,6 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
override schema = optionsSchema; override schema = optionsSchema;
private _setupTheme(
context: ViewExtensionContext,
framework: FrameworkProvider
) {
if (this.isPreview(context.scope)) {
context.register(getPreviewThemeExtension(framework));
} else {
context.register(getThemeExtension(framework));
}
}
private _setupAI( private _setupAI(
context: ViewExtensionContext, context: ViewExtensionContext,
framework: FrameworkProvider framework: FrameworkProvider
@@ -97,7 +81,7 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
context: ViewExtensionContext, context: ViewExtensionContext,
options?: z.infer<typeof optionsSchema> options?: z.infer<typeof optionsSchema>
) { ) {
super.setup(context); super.setup(context, options);
const { framework, enableAI } = options || {}; const { framework, enableAI } = options || {};
if (framework) { if (framework) {
context.register(patchPeekViewService(framework.get(PeekViewService))); context.register(patchPeekViewService(framework.get(PeekViewService)));
@@ -106,9 +90,7 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
buildDocDisplayMetaExtension(framework), buildDocDisplayMetaExtension(framework),
]); ]);
context.register(getTelemetryExtension()); context.register(getTelemetryExtension());
this._setupTheme(context, framework);
if (context.scope === 'edgeless' || context.scope === 'page') { if (context.scope === 'edgeless' || context.scope === 'page') {
context.register(getEditorConfigExtension(framework));
context.register(patchFileSizeLimitExtension(framework)); context.register(patchFileSizeLimitExtension(framework));
} }
if (enableAI) { if (enableAI) {

View File

@@ -6,10 +6,6 @@ import {
import { patchDatabaseBlockConfigService } from '@affine/core/blocksuite/extensions/database-block-config-service'; import { patchDatabaseBlockConfigService } from '@affine/core/blocksuite/extensions/database-block-config-service';
import { patchDocModeService } from '@affine/core/blocksuite/extensions/doc-mode-service'; import { patchDocModeService } from '@affine/core/blocksuite/extensions/doc-mode-service';
import { patchDocUrlExtensions } from '@affine/core/blocksuite/extensions/doc-url'; import { patchDocUrlExtensions } from '@affine/core/blocksuite/extensions/doc-url';
import {
patchForEdgelessNoteConfig,
patchForEmbedSyncedDocConfig,
} from '@affine/core/blocksuite/extensions/edgeless-block-header';
import { EdgelessClipboardAIChatConfig } from '@affine/core/blocksuite/extensions/edgeless-clipboard'; import { EdgelessClipboardAIChatConfig } from '@affine/core/blocksuite/extensions/edgeless-clipboard';
import { patchForClipboardInElectron } from '@affine/core/blocksuite/extensions/electron-clipboard'; import { patchForClipboardInElectron } from '@affine/core/blocksuite/extensions/electron-clipboard';
import { patchNotificationService } from '@affine/core/blocksuite/extensions/notification-service'; import { patchNotificationService } from '@affine/core/blocksuite/extensions/notification-service';
@@ -116,13 +112,12 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
context: ViewExtensionContext, context: ViewExtensionContext,
options?: AffineEditorViewOptions options?: AffineEditorViewOptions
) { ) {
super.setup(context); super.setup(context, options);
if (!options) { if (!options) {
return; return;
} }
const { const {
isCloud, isCloud,
isInPeekView,
enableTurboRenderer, enableTurboRenderer,
enablePDFEmbedPreview, enablePDFEmbedPreview,
@@ -155,8 +150,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
context.register(patchDocUrlExtensions(framework)); context.register(patchDocUrlExtensions(framework));
context.register(patchQuickSearchService(framework)); context.register(patchQuickSearchService(framework));
context.register([ context.register([
patchForEmbedSyncedDocConfig(reactToLit),
patchForEdgelessNoteConfig(framework, reactToLit, isInPeekView),
patchDatabaseBlockConfigService(), patchDatabaseBlockConfigService(),
patchForAudioEmbedView(reactToLit), patchForAudioEmbedView(reactToLit),
]); ]);

View File

@@ -1,5 +1,11 @@
import {
EdgelessBlockHeaderConfigViewExtension,
type EdgelessBlockHeaderViewOptions,
} from '@affine/core/blocksuite/extensions/edgeless-block-header';
import { AffineEditorConfigViewExtension } from '@affine/core/blocksuite/extensions/editor-config';
import { createDatabaseOptionsConfig } from '@affine/core/blocksuite/extensions/editor-config/database'; import { createDatabaseOptionsConfig } from '@affine/core/blocksuite/extensions/editor-config/database';
import { createLinkedWidgetConfig } from '@affine/core/blocksuite/extensions/editor-config/linked'; import { createLinkedWidgetConfig } from '@affine/core/blocksuite/extensions/editor-config/linked';
import { AffineThemeViewExtension } from '@affine/core/blocksuite/extensions/theme';
import { AffineCommonViewExtension } from '@affine/core/blocksuite/manager/common-view'; import { AffineCommonViewExtension } from '@affine/core/blocksuite/manager/common-view';
import { import {
AffineEditorViewExtension, AffineEditorViewExtension,
@@ -12,40 +18,123 @@ import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view';
import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view'; import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view';
import type { FrameworkProvider } from '@toeverything/infra'; import type { FrameworkProvider } from '@toeverything/infra';
import { CodeBlockPreviewExtensionProvider } from './code-block-preview'; import { CodeBlockPreviewViewExtension } from './code-block-preview';
const manager = new ViewExtensionManager([ class ViewProvider {
static instance: ViewProvider | null = null;
static getInstance() {
if (!ViewProvider.instance) {
ViewProvider.instance = new ViewProvider();
}
return ViewProvider.instance;
}
private readonly _manager: ViewExtensionManager;
constructor() {
this._manager = new ViewExtensionManager([
...getInternalViewExtensions(), ...getInternalViewExtensions(),
AffineThemeViewExtension,
AffineCommonViewExtension, AffineCommonViewExtension,
AffineEditorViewExtension, AffineEditorViewExtension,
CodeBlockPreviewExtensionProvider, AffineEditorConfigViewExtension,
]); CodeBlockPreviewViewExtension,
EdgelessBlockHeaderConfigViewExtension,
]);
}
export function getViewManager( get value() {
return this._manager;
}
get config() {
return {
init: this._initDefaultConfig,
common: this._configureCommon,
editorView: this._configureEditorView,
theme: this._configureTheme,
editorConfig: this._configureEditorConfig,
edgelessBlockHeader: this._configureEdgelessBlockHeader,
database: this._configureDatabase,
linkedDoc: this._configureLinkedDoc,
paragraph: this._configureParagraph,
value: this._manager,
};
}
private readonly _initDefaultConfig = () => {
this.config
.common()
.theme()
.editorView()
.editorConfig()
.edgelessBlockHeader()
.database()
.linkedDoc()
.paragraph();
return this.config;
};
private readonly _configureCommon = (
framework?: FrameworkProvider, framework?: FrameworkProvider,
enableAI?: boolean, enableAI?: boolean
options?: AffineEditorViewOptions ) => {
) { this._manager.configure(AffineCommonViewExtension, {
manager.configure(AffineCommonViewExtension, {
framework, framework,
enableAI, enableAI,
}); });
manager.configure(AffineEditorViewExtension, options); return this.config;
};
private readonly _configureEditorView = (
options?: AffineEditorViewOptions
) => {
this._manager.configure(AffineEditorViewExtension, options);
return this.config;
};
private readonly _configureTheme = (framework?: FrameworkProvider) => {
this._manager.configure(AffineThemeViewExtension, { framework });
return this.config;
};
private readonly _configureEditorConfig = (framework?: FrameworkProvider) => {
this._manager.configure(AffineEditorConfigViewExtension, { framework });
return this.config;
};
private readonly _configureEdgelessBlockHeader = (
options?: EdgelessBlockHeaderViewOptions
) => {
this._manager.configure(EdgelessBlockHeaderConfigViewExtension, options);
return this.config;
};
private readonly _configureDatabase = (framework?: FrameworkProvider) => {
if (framework) { if (framework) {
manager.configure( this._manager.configure(
DatabaseViewExtension, DatabaseViewExtension,
createDatabaseOptionsConfig(framework) createDatabaseOptionsConfig(framework)
); );
manager.configure( }
return this.config;
};
private readonly _configureLinkedDoc = (framework?: FrameworkProvider) => {
if (framework) {
this._manager.configure(
LinkedDocViewExtension, LinkedDocViewExtension,
createLinkedWidgetConfig(framework) createLinkedWidgetConfig(framework)
); );
} }
return this.config;
};
private readonly _configureParagraph = (enableAI?: boolean) => {
if (enableAI) { if (enableAI) {
manager.configure(ParagraphViewExtension, { this._manager.configure(ParagraphViewExtension, {
getPlaceholder: model => { getPlaceholder: model => {
const placeholders = { const placeholders = {
text: "Type '/' for commands, 'space' for AI", text: "Type '/' for commands, 'space' for AI",
@@ -61,5 +150,10 @@ export function getViewManager(
}, },
}); });
} }
return manager; return this.config;
};
}
export function getViewManager() {
return ViewProvider.getInstance();
} }

View File

@@ -56,7 +56,12 @@ export const EdgelessSnapshot = (props: Props) => {
const { editorSetting } = framework.get(EditorSettingService); const { editorSetting } = framework.get(EditorSettingService);
const extensions = useMemo(() => { const extensions = useMemo(() => {
const manager = getViewManager(framework, false); const manager = getViewManager()
.config.init()
.common(framework)
.theme(framework)
.database(framework)
.linkedDoc(framework).value;
return manager return manager
.get('preview-edgeless') .get('preview-edgeless')
.concat([ViewportElementExtension('.ref-viewport')]); .concat([ViewportElementExtension('.ref-viewport')]);

View File

@@ -10,7 +10,7 @@ export function createBlockStdScope(doc: Store) {
logger.debug('createBlockStdScope', doc.id); logger.debug('createBlockStdScope', doc.id);
const std = new BlockStdScope({ const std = new BlockStdScope({
store: doc, store: doc,
extensions: getViewManager().get('page'), extensions: getViewManager().config.init().value.get('page'),
}); });
return std; return std;
} }