mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-03 23:31:19 +08:00
6170a90785
Add a persistent "Show line numbers in code blocks" setting to Editor Settings that controls line-number visibility across all code blocks. Individual blocks can still override the global default via the per-block More menu toggle. ## Changes - **schema.ts** - add `codeBlockLineNumbers: z.boolean().default(true)` to `AffineEditorSettingSchema` - **code-block.ts** - read `codeBlockLineNumbers` from `EditorSettingProvider` reactively via a stable `signal(true)` updated by `effect()` in `connectedCallback`; expose `showLineNumbers` getter as single source of truth used by both `renderBlock()` and the toolbar - **config.ts** - toolbar line-number toggle reads `blockComponent.showLineNumbers` (resolved state) instead of `model.props.lineNumber ?? true` - **general.tsx** - add `DefaultCodeBlockLineNumberSettings` Switch row in editor general settings - **en.json + i18n.gen.ts** - add i18n strings for the new setting - **line-numbers.spec.ts** - add 7 e2e tests covering default visibility, global toggle on/off, per-block override in both directions, multi-block, newly created blocks, and persistence across reload ## Behaviour | State | Result | |---|---| | Global ON (default), no per-block override | Line numbers shown | | Global OFF, no per-block override | Line numbers hidden | | Global OFF, per-block explicitly ON | Line numbers shown | | Global ON, per-block explicitly OFF | Line numbers hidden | | Mobile (feature flag) | Always hidden regardless of settings | ## Notes - Existing per-block toggle behaviour is fully preserved and unchanged - Default is `true` so no regression for existing users - The blocksuite-side reads `codeBlockLineNumbers` via a type cast (`as Record<string, unknown>`) because the key lives in the AFFiNE-level `EditorSettingSchema`, not in blocksuite's own `GeneralSettingSchema` - this is an intentional architectural boundary Closes #14965 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a global setting to show or hide line numbers in code blocks by default. * Added localized title and description text for the new setting. * Preserved per-code-block overrides through the block’s More menu. * **Bug Fixes** * Line-number visibility now stays consistent across existing and newly created code blocks, including after reloads. * **Tests** * Added end-to-end coverage for defaults, overrides, persistence, and multiple code blocks. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
320 lines
9.3 KiB
TypeScript
320 lines
9.3 KiB
TypeScript
import {
|
|
CancelWrapIcon,
|
|
CaptionIcon,
|
|
CollapseCodeIcon,
|
|
CopyIcon,
|
|
DeleteIcon,
|
|
DuplicateIcon,
|
|
ExpandCodeIcon,
|
|
WrapIcon,
|
|
} from '@blocksuite/affine-components/icons';
|
|
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
|
|
import { CommentProviderIdentifier } from '@blocksuite/affine-shared/services';
|
|
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
|
|
import { noop, sleep } from '@blocksuite/global/utils';
|
|
import { CommentIcon, NumberedListIcon } from '@blocksuite/icons/lit';
|
|
import { BlockSelection } from '@blocksuite/std';
|
|
import { html } from 'lit';
|
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
|
|
import { CodeBlockConfigExtension } from '../code-block-config.js';
|
|
import type { CodeBlockToolbarContext } from './context.js';
|
|
import { duplicateCodeBlock } from './utils.js';
|
|
|
|
export const PRIMARY_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
|
|
{
|
|
type: 'primary',
|
|
items: [
|
|
{
|
|
type: 'change-lang',
|
|
generate: ({ blockComponent, setActive }) => {
|
|
const state = { active: false };
|
|
return {
|
|
action: noop,
|
|
render: () =>
|
|
html`<language-list-button
|
|
.blockComponent=${blockComponent}
|
|
.onActiveStatusChange=${async (active: boolean) => {
|
|
state.active = active;
|
|
if (!active) {
|
|
await sleep(1000);
|
|
if (state.active) return;
|
|
}
|
|
setActive(active);
|
|
}}
|
|
>
|
|
</language-list-button>`,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
type: 'preview',
|
|
generate: ({ blockComponent }) => {
|
|
return {
|
|
action: noop,
|
|
render: () => html`
|
|
<preview-button .blockComponent=${blockComponent}>
|
|
</preview-button>
|
|
`,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
type: 'copy-code',
|
|
label: 'Copy code',
|
|
icon: CopyIcon,
|
|
generate: ({ blockComponent }) => {
|
|
return {
|
|
action: () => {
|
|
blockComponent.copyCode();
|
|
},
|
|
render: item => html`
|
|
<editor-icon-button
|
|
class="code-toolbar-button copy-code"
|
|
aria-label=${ifDefined(item.label)}
|
|
.tooltip=${item.label}
|
|
.tooltipOffset=${4}
|
|
.iconSize=${'16px'}
|
|
.iconContainerPadding=${4}
|
|
@click=${(e: MouseEvent) => {
|
|
e.stopPropagation();
|
|
item.action();
|
|
}}
|
|
>
|
|
${item.icon}
|
|
</editor-icon-button>
|
|
`,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
type: 'collapse',
|
|
when: ({ doc }) => !doc.readonly,
|
|
generate: ({ blockComponent }) => {
|
|
return {
|
|
action: () => {
|
|
blockComponent.setCollapsed(!blockComponent.collapsed$.value);
|
|
},
|
|
render: item => {
|
|
const collapsed = blockComponent.collapsed$.value;
|
|
const icon = collapsed ? ExpandCodeIcon : CollapseCodeIcon;
|
|
const label = collapsed ? 'Expand code' : 'Collapse code';
|
|
return html`
|
|
<editor-icon-button
|
|
class="code-toolbar-button collapse"
|
|
aria-label=${label}
|
|
.tooltip=${label}
|
|
.tooltipOffset=${4}
|
|
.iconSize=${'16px'}
|
|
.iconContainerPadding=${4}
|
|
@click=${(e: MouseEvent) => {
|
|
e.stopPropagation();
|
|
item.action();
|
|
}}
|
|
>
|
|
${icon}
|
|
</editor-icon-button>
|
|
`;
|
|
},
|
|
};
|
|
},
|
|
},
|
|
{
|
|
type: 'caption',
|
|
label: 'Caption',
|
|
icon: CaptionIcon,
|
|
when: ({ doc }) => !doc.readonly,
|
|
generate: ({ blockComponent }) => {
|
|
return {
|
|
action: () => {
|
|
blockComponent.captionEditor?.show();
|
|
},
|
|
render: item => html`
|
|
<editor-icon-button
|
|
class="code-toolbar-button caption"
|
|
aria-label=${ifDefined(item.label)}
|
|
.tooltip=${item.label}
|
|
.tooltipOffset=${4}
|
|
.iconSize=${'16px'}
|
|
.iconContainerPadding=${4}
|
|
@click=${(e: MouseEvent) => {
|
|
e.stopPropagation();
|
|
item.action();
|
|
}}
|
|
>
|
|
${item.icon}
|
|
</editor-icon-button>
|
|
`,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
type: 'comment',
|
|
label: 'Comment',
|
|
tooltip: 'Comment',
|
|
icon: CommentIcon({
|
|
width: '20',
|
|
height: '20',
|
|
}),
|
|
when: ({ std }) => !!std.getOptional(CommentProviderIdentifier),
|
|
generate: ({ blockComponent }) => {
|
|
return {
|
|
action: () => {
|
|
const commentProvider = blockComponent.std.getOptional(
|
|
CommentProviderIdentifier
|
|
);
|
|
if (!commentProvider) return;
|
|
|
|
commentProvider.addComment([
|
|
new BlockSelection({
|
|
blockId: blockComponent.model.id,
|
|
}),
|
|
]);
|
|
},
|
|
render: item =>
|
|
html`<editor-icon-button
|
|
class="code-toolbar-button comment"
|
|
aria-label=${ifDefined(item.label)}
|
|
.tooltip=${item.label}
|
|
.tooltipOffset=${4}
|
|
.iconSize=${'16px'}
|
|
.iconContainerPadding=${4}
|
|
@click=${(e: MouseEvent) => {
|
|
e.stopPropagation();
|
|
item.action();
|
|
}}
|
|
>
|
|
${item.icon}
|
|
</editor-icon-button>`,
|
|
};
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
export const toggleGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
|
type: 'toggle',
|
|
items: [
|
|
{
|
|
type: 'wrap',
|
|
generate: ({ blockComponent }) => {
|
|
return {
|
|
action: () => {},
|
|
render: () => {
|
|
const wrapped = blockComponent.model.props.wrap;
|
|
const label = wrapped ? 'Cancel wrap' : 'Wrap';
|
|
const icon = wrapped ? CancelWrapIcon : WrapIcon;
|
|
return html`
|
|
<editor-menu-action
|
|
@click=${() => {
|
|
const currentWrap = blockComponent.model.props.wrap;
|
|
blockComponent.setWrap(!currentWrap);
|
|
}}
|
|
aria-label=${label}
|
|
>
|
|
${icon}
|
|
<span class="label">${label}</span>
|
|
<toggle-switch
|
|
style="margin-left: auto;"
|
|
.on="${wrapped}"
|
|
></toggle-switch>
|
|
</editor-menu-action>
|
|
`;
|
|
},
|
|
};
|
|
},
|
|
},
|
|
{
|
|
type: 'line-number',
|
|
when: ({ std }) =>
|
|
std.getOptional(CodeBlockConfigExtension.identifier)?.showLineNumbers ??
|
|
true,
|
|
generate: ({ blockComponent }) => {
|
|
return {
|
|
action: () => {},
|
|
render: () => {
|
|
const lineNumber = blockComponent.showLineNumbers;
|
|
const label = lineNumber ? 'Cancel line number' : 'Line number';
|
|
return html`
|
|
<editor-menu-action
|
|
@click=${() => {
|
|
blockComponent.store.updateBlock(blockComponent.model, {
|
|
lineNumber: !blockComponent.showLineNumbers,
|
|
});
|
|
}}
|
|
aria-label=${label}
|
|
>
|
|
${NumberedListIcon()}
|
|
<span class="label">${label}</span>
|
|
<toggle-switch
|
|
style="margin-left: auto;"
|
|
.on="${lineNumber}"
|
|
></toggle-switch>
|
|
</editor-menu-action>
|
|
`;
|
|
},
|
|
};
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
// Clipboard Group
|
|
export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
|
type: 'clipboard',
|
|
items: [
|
|
{
|
|
type: 'duplicate',
|
|
label: 'Duplicate',
|
|
icon: DuplicateIcon,
|
|
when: ({ doc }) => !doc.readonly,
|
|
action: ({ host, blockComponent, close }) => {
|
|
const codeId = duplicateCodeBlock(blockComponent.model);
|
|
|
|
host.updateComplete
|
|
.then(() => {
|
|
host.selection.setGroup('note', [
|
|
host.selection.create(BlockSelection, {
|
|
blockId: codeId,
|
|
}),
|
|
]);
|
|
|
|
if (isInsidePageEditor(host)) {
|
|
const duplicateElement = host.view.getBlock(codeId);
|
|
if (duplicateElement) {
|
|
duplicateElement.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
}
|
|
})
|
|
.catch(console.error);
|
|
|
|
close();
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
// Delete Group
|
|
export const deleteGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
|
type: 'delete',
|
|
items: [
|
|
{
|
|
type: 'delete',
|
|
label: 'Delete',
|
|
icon: DeleteIcon,
|
|
when: ({ doc }) => !doc.readonly,
|
|
action: ({ doc, blockComponent, close }) => {
|
|
doc.deleteBlock(blockComponent.model);
|
|
close();
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
export const MORE_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
|
|
toggleGroup,
|
|
clipboardGroup,
|
|
deleteGroup,
|
|
];
|