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,17 +43,20 @@ import type {
AffineAIPanelWidgetConfig,
} from '../widgets/ai-panel/type';
export const getCustomPageEditorBlockSpecs: () => ExtensionType[] = () => [
...getViewManager().get('page'),
{
setup: di => {
di.override(
BlockViewIdentifier('affine:page'),
() => literal`affine-page-root`
);
export const getCustomPageEditorBlockSpecs: () => ExtensionType[] = () => {
const manager = getViewManager().config.init().value;
return [
...manager.get('page'),
{
setup: di => {
di.override(
BlockViewIdentifier('affine:page'),
() => literal`affine-page-root`
);
},
},
},
];
];
};
const customHeadingStyles = css`
.custom-heading {

View File

@@ -163,7 +163,13 @@ const usePreviewExtensions = () => {
const enableAI = useEnableAI();
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');
return [...specs, patchReferenceRenderer(reactToLit, referenceRenderer)];
}, [reactToLit, referenceRenderer, framework, enableAI]);

View File

@@ -6,7 +6,6 @@ import {
LitEdgelessEditor,
type PageEditor,
} 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 { useEnableAI } from '@affine/core/components/hooks/affine/use-enable-ai';
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 type { DocTitle } from '@blocksuite/affine/fragments/doc-title';
import type { DocMode } from '@blocksuite/affine/model';
import type { ExtensionType, Store } from '@blocksuite/affine/store';
import type { Store } from '@blocksuite/affine/store';
import {
type FrameworkProvider,
useFramework,
useLiveData,
useService,
@@ -69,7 +67,7 @@ const usePatchSpecs = (mode: DocMode) => {
const enableAI = useEnableAI();
const insidePeekView = useInsidePeekView();
const isInPeekView = useInsidePeekView();
const enableTurboRenderer = useLiveData(
featureFlagService.flags.enable_turbo_renderer.$
@@ -81,33 +79,51 @@ const usePatchSpecs = (mode: DocMode) => {
)
);
const editorOptions: AffineEditorViewOptions = useMemo(() => {
return {
isCloud,
isInPeekView: insidePeekView,
const patchedSpecs = useMemo(() => {
const manager = getViewManager()
.config.init()
.common(framework, enableAI)
.theme(framework)
.editorConfig(framework)
.editorView({
isCloud,
isInPeekView,
enableTurboRenderer,
enablePDFEmbedPreview,
framework,
reactToLit,
confirmModal,
})
.edgelessBlockHeader({
framework,
isInPeekView,
reactToLit,
})
.database(framework)
.linkedDoc(framework)
.paragraph(enableAI).value;
enableTurboRenderer,
enablePDFEmbedPreview,
framework,
reactToLit: reactToLit as AffineEditorViewOptions['reactToLit'],
confirmModal,
};
if (BUILD_CONFIG.isMobileEdition) {
if (mode === 'page') {
return manager.get('mobile-page');
} else {
return manager.get('mobile-edgeless');
}
} else {
return manager.get(mode);
}
}, [
confirmModal,
enableAI,
enablePDFEmbedPreview,
enableTurboRenderer,
framework,
insidePeekView,
isInPeekView,
isCloud,
mode,
reactToLit,
]);
const patchedSpecs = useMemo(() => {
return enableEditorExtension(framework, mode, enableAI, editorOptions);
}, [framework, mode, enableAI, editorOptions]);
return [
patchedSpecs,
useMemo(
@@ -302,20 +318,3 @@ export const BlocksuiteEdgelessEditor = forwardRef<
</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 { 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 type { ElementOrFactory } from '@affine/component';
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';
patchForEdgelessNoteConfig,
patchForEmbedSyncedDocConfig,
} from '@affine/core/blocksuite/extensions/edgeless-block-header/patch';
import {
type ViewExtensionContext,
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';
import { EdgelessEmbedSyncedDocHeader } from './edgeless-embed-synced-doc-header';
import { EdgelessNoteHeader } from './edgeless-note-header';
const optionsSchema = z.object({
isInPeekView: z.boolean(),
framework: z.instanceof(FrameworkProvider),
reactToLit: z
.function()
.args(z.custom<ElementOrFactory>(), z.boolean().optional())
.returns(z.custom<TemplateResult>()),
});
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;
export type EdgelessBlockHeaderViewOptions = z.infer<typeof optionsSchema>;
if (!std.store.root) return false;
const rootView = std.view.getBlock(std.store.root.id);
if (!rootView) return false;
export class EdgelessBlockHeaderConfigViewExtension extends ViewExtensionProvider<EdgelessBlockHeaderViewOptions> {
override name = 'header-config-view';
override schema = optionsSchema;
const gfx = std.get(GfxControllerIdentifier);
const primaryMode = std
.get(DocModeProvider)
.getPrimaryMode(std.store.id);
override setup(
context: ViewExtensionContext,
options?: EdgelessBlockHeaderViewOptions
) {
super.setup(context, options);
if (!options) return;
const { framework, isInPeekView, reactToLit } = options;
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} />),
});
context.register(
patchForEdgelessNoteConfig(framework, reactToLit, isInPeekView)
);
context.register(patchForEmbedSyncedDocConfig(reactToLit));
}
}

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 { 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 { getEditorConfigExtension } from '@affine/core/blocksuite/extensions/editor-config/get-config';
import {
createCustomToolbarExtension,
createToolbarMoreMenuConfig,
} from './toolbar';
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine/ext-loader';
import { FrameworkProvider } from '@toeverything/infra';
import { z } from 'zod';
export function getEditorConfigExtension(
framework: FrameworkProvider
): ExtensionType[] {
const editorSettingService = framework.get(EditorSettingService);
const workspaceServerService = framework.get(WorkspaceServerService);
const baseUrl = workspaceServerService.server?.baseUrl ?? location.origin;
const optionsSchema = z.object({
framework: z.instanceof(FrameworkProvider).optional(),
});
return [
EditorSettingExtension({
// eslint-disable-next-line rxjs/finnish
setting$: editorSettingService.editorSetting.settingSignal,
set: (k, v) => editorSettingService.editorSetting.set(k, v),
}),
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
type AffineEditorConfigViewOptions = z.infer<typeof optionsSchema>;
createCustomToolbarExtension(editorSettingService.editorSetting, baseUrl),
].flat();
export class AffineEditorConfigViewExtension extends ViewExtensionProvider<AffineEditorConfigViewOptions> {
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,
type Signal,
} from '@blocksuite/affine/shared/utils';
import {
type BlockStdScope,
LifeCycleWatcher,
StdIdentifier,
} from '@blocksuite/affine/std';
import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/std';
import { type FrameworkProvider } from '@toeverything/infra';
import type { Observable } from 'rxjs';
import { combineLatest, map } from 'rxjs';
@@ -99,58 +95,3 @@ export function getThemeExtension(
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,
} from '../extensions/code-block-preview/html-preview';
export class CodeBlockPreviewExtensionProvider extends ViewExtensionProvider {
export class CodeBlockPreviewViewExtension extends ViewExtensionProvider {
override name = 'code-block-preview';
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 { edgelessCopilotWidget } from '@affine/core/blocksuite/ai/widgets/edgeless-copilot';
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 { getFontConfigExtension } from '@affine/core/blocksuite/extensions/font-config';
import { patchPeekViewService } from '@affine/core/blocksuite/extensions/peek-view-service';
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 {
type ViewExtensionContext,
@@ -42,17 +37,6 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
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(
context: ViewExtensionContext,
framework: FrameworkProvider
@@ -97,7 +81,7 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
context: ViewExtensionContext,
options?: z.infer<typeof optionsSchema>
) {
super.setup(context);
super.setup(context, options);
const { framework, enableAI } = options || {};
if (framework) {
context.register(patchPeekViewService(framework.get(PeekViewService)));
@@ -106,9 +90,7 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
buildDocDisplayMetaExtension(framework),
]);
context.register(getTelemetryExtension());
this._setupTheme(context, framework);
if (context.scope === 'edgeless' || context.scope === 'page') {
context.register(getEditorConfigExtension(framework));
context.register(patchFileSizeLimitExtension(framework));
}
if (enableAI) {

View File

@@ -6,10 +6,6 @@ import {
import { patchDatabaseBlockConfigService } from '@affine/core/blocksuite/extensions/database-block-config-service';
import { patchDocModeService } from '@affine/core/blocksuite/extensions/doc-mode-service';
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 { patchForClipboardInElectron } from '@affine/core/blocksuite/extensions/electron-clipboard';
import { patchNotificationService } from '@affine/core/blocksuite/extensions/notification-service';
@@ -116,13 +112,12 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
context: ViewExtensionContext,
options?: AffineEditorViewOptions
) {
super.setup(context);
super.setup(context, options);
if (!options) {
return;
}
const {
isCloud,
isInPeekView,
enableTurboRenderer,
enablePDFEmbedPreview,
@@ -155,8 +150,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
context.register(patchDocUrlExtensions(framework));
context.register(patchQuickSearchService(framework));
context.register([
patchForEmbedSyncedDocConfig(reactToLit),
patchForEdgelessNoteConfig(framework, reactToLit, isInPeekView),
patchDatabaseBlockConfigService(),
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 { 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 {
AffineEditorViewExtension,
@@ -12,54 +18,142 @@ import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view';
import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view';
import type { FrameworkProvider } from '@toeverything/infra';
import { CodeBlockPreviewExtensionProvider } from './code-block-preview';
import { CodeBlockPreviewViewExtension } from './code-block-preview';
const manager = new ViewExtensionManager([
...getInternalViewExtensions(),
AffineCommonViewExtension,
AffineEditorViewExtension,
CodeBlockPreviewExtensionProvider,
]);
export function getViewManager(
framework?: FrameworkProvider,
enableAI?: boolean,
options?: AffineEditorViewOptions
) {
manager.configure(AffineCommonViewExtension, {
framework,
enableAI,
});
manager.configure(AffineEditorViewExtension, options);
if (framework) {
manager.configure(
DatabaseViewExtension,
createDatabaseOptionsConfig(framework)
);
manager.configure(
LinkedDocViewExtension,
createLinkedWidgetConfig(framework)
);
class ViewProvider {
static instance: ViewProvider | null = null;
static getInstance() {
if (!ViewProvider.instance) {
ViewProvider.instance = new ViewProvider();
}
return ViewProvider.instance;
}
if (enableAI) {
manager.configure(ParagraphViewExtension, {
getPlaceholder: model => {
const placeholders = {
text: "Type '/' for commands, 'space' for AI",
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
h4: 'Heading 4',
h5: 'Heading 5',
h6: 'Heading 6',
quote: '',
};
return placeholders[model.props.type] ?? '';
},
private readonly _manager: ViewExtensionManager;
constructor() {
this._manager = new ViewExtensionManager([
...getInternalViewExtensions(),
AffineThemeViewExtension,
AffineCommonViewExtension,
AffineEditorViewExtension,
AffineEditorConfigViewExtension,
CodeBlockPreviewViewExtension,
EdgelessBlockHeaderConfigViewExtension,
]);
}
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,
enableAI?: boolean
) => {
this._manager.configure(AffineCommonViewExtension, {
framework,
enableAI,
});
}
return manager;
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) {
this._manager.configure(
DatabaseViewExtension,
createDatabaseOptionsConfig(framework)
);
}
return this.config;
};
private readonly _configureLinkedDoc = (framework?: FrameworkProvider) => {
if (framework) {
this._manager.configure(
LinkedDocViewExtension,
createLinkedWidgetConfig(framework)
);
}
return this.config;
};
private readonly _configureParagraph = (enableAI?: boolean) => {
if (enableAI) {
this._manager.configure(ParagraphViewExtension, {
getPlaceholder: model => {
const placeholders = {
text: "Type '/' for commands, 'space' for AI",
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
h4: 'Heading 4',
h5: 'Heading 5',
h6: 'Heading 6',
quote: '',
};
return placeholders[model.props.type] ?? '';
},
});
}
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 extensions = useMemo(() => {
const manager = getViewManager(framework, false);
const manager = getViewManager()
.config.init()
.common(framework)
.theme(framework)
.database(framework)
.linkedDoc(framework).value;
return manager
.get('preview-edgeless')
.concat([ViewportElementExtension('.ref-viewport')]);

View File

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