feat(editor): add line number display option for code block (#12305)

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

- **New Features**
  - Added a toggle in the code block toolbar to show or hide line numbers for individual code blocks.
  - The display of line numbers now respects both global and per-block settings, allowing more flexible control.
- **Style**
  - Updated styles to hide line numbers when disabled via the new toggle option.
- **Tests**
  - Added end-to-end tests to verify toggling line numbers visibility and undo/redo behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Flrande
2025-05-15 10:59:38 +00:00
parent 3a2fe0bf91
commit 147fa9a6b1
6 changed files with 89 additions and 12 deletions

View File

@@ -388,8 +388,10 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
override renderBlock(): TemplateResult<1> { override renderBlock(): TemplateResult<1> {
const showLineNumbers = const showLineNumbers =
this.std.getOptional(CodeBlockConfigExtension.identifier) (this.std.getOptional(CodeBlockConfigExtension.identifier)
?.showLineNumbers ?? true; ?.showLineNumbers ??
true) &&
(this.model.props.lineNumber ?? true);
const preview = !!this.model.props.preview; const preview = !!this.model.props.preview;
const previewContext = this.std.getOptional( const previewContext = this.std.getOptional(
@@ -403,6 +405,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
'affine-code-block-container': true, 'affine-code-block-container': true,
mobile: IS_MOBILE, mobile: IS_MOBILE,
wrap: this.model.props.wrap, wrap: this.model.props.wrap,
'disable-line-numbers': !showLineNumbers,
})} })}
> >
<rich-text <rich-text
@@ -420,16 +423,14 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
.enableUndoRedo=${false} .enableUndoRedo=${false}
.wrapText=${this.model.props.wrap} .wrapText=${this.model.props.wrap}
.verticalScrollContainerGetter=${() => getViewportElement(this.host)} .verticalScrollContainerGetter=${() => getViewportElement(this.host)}
.vLineRenderer=${showLineNumbers .vLineRenderer=${(vLine: VLine) => {
? (vLine: VLine) => { return html`
return html` <span contenteditable="false" class="line-number"
<span contenteditable="false" class="line-number" >${vLine.index + 1}</span
>${vLine.index + 1}</span >
> ${vLine.renderVElements()}
${vLine.renderVElements()} `;
`; }}
}
: undefined}
> >
</rich-text> </rich-text>
<div <div

View File

@@ -9,10 +9,12 @@ import {
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar'; import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils'; import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
import { noop, sleep } from '@blocksuite/global/utils'; import { noop, sleep } from '@blocksuite/global/utils';
import { NumberedListIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std'; import { BlockSelection } from '@blocksuite/std';
import { html } from 'lit'; import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { CodeBlockConfigExtension } from '../code-block-config.js';
import type { CodeBlockToolbarContext } from './context.js'; import type { CodeBlockToolbarContext } from './context.js';
import { duplicateCodeBlock } from './utils.js'; import { duplicateCodeBlock } from './utils.js';
@@ -148,6 +150,40 @@ export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
}; };
}, },
}, },
{
type: 'line-number',
when: ({ std }) =>
std.getOptional(CodeBlockConfigExtension.identifier)?.showLineNumbers ??
true,
generate: ({ blockComponent, close }) => {
return {
action: () => {},
render: () => {
const lineNumber = blockComponent.model.props.lineNumber ?? true;
const label = lineNumber ? 'Cancel line number' : 'Line number';
return html`
<editor-menu-action
@click=${() => {
blockComponent.store.updateBlock(blockComponent.model, {
lineNumber: !lineNumber,
});
close();
}}
aria-label=${label}
>
${NumberedListIcon()}
<span class="label">${label}</span>
<toggle-switch
style="margin-left: auto;"
.on="${lineNumber}"
></toggle-switch>
</editor-menu-action>
`;
},
};
},
},
{ {
type: 'duplicate', type: 'duplicate',
label: 'Duplicate', label: 'Duplicate',

View File

@@ -50,6 +50,10 @@ export const codeBlockStyles = css`
user-select: none; user-select: none;
} }
.affine-code-block-container.disable-line-numbers .line-number {
display: none;
}
affine-code .affine-code-block-preview { affine-code .affine-code-block-preview {
padding: 12px; padding: 12px;
} }

View File

@@ -13,6 +13,7 @@ type CodeBlockProps = {
wrap: boolean; wrap: boolean;
caption: string; caption: string;
preview?: boolean; preview?: boolean;
lineNumber?: boolean;
} & BlockMeta; } & BlockMeta;
export const CodeBlockSchema = defineBlockSchema({ export const CodeBlockSchema = defineBlockSchema({
@@ -24,6 +25,7 @@ export const CodeBlockSchema = defineBlockSchema({
wrap: false, wrap: false,
caption: '', caption: '',
preview: undefined, preview: undefined,
lineNumber: undefined,
'meta:createdAt': undefined, 'meta:createdAt': undefined,
'meta:createdBy': undefined, 'meta:createdBy': undefined,
'meta:updatedAt': undefined, 'meta:updatedAt': undefined,

View File

@@ -321,6 +321,34 @@ test('undo code block wrap can work', async ({ page }, testInfo) => {
); );
}); });
test('toggle code block line number can work', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyCodeBlockState(page);
await focusRichText(page);
const lineNumber = page.locator('affine-code .line-number');
await expect(lineNumber).toBeVisible();
const codeBlockController = getCodeBlock(page);
await codeBlockController.codeBlock.hover();
await (await codeBlockController.openMore()).cancelLineNumberButton.click();
await expect(lineNumber).toBeHidden();
await undoByKeyboard(page);
await expect(lineNumber).toBeVisible();
await redoByKeyboard(page);
await expect(lineNumber).toBeHidden();
await codeBlockController.codeBlock.hover();
await (await codeBlockController.openMore()).lineNumberButton.click();
await expect(lineNumber).toBeVisible();
});
test('code block toolbar widget can appear and disappear during mousemove', async ({ test('code block toolbar widget can appear and disappear during mousemove', async ({
page, page,
}) => { }) => {

View File

@@ -34,6 +34,10 @@ export function getCodeBlock(page: Page) {
const cancelWrapButton = menu.getByRole('button', { name: 'Cancel wrap' }); const cancelWrapButton = menu.getByRole('button', { name: 'Cancel wrap' });
const duplicateButton = menu.getByRole('button', { name: 'Duplicate' }); const duplicateButton = menu.getByRole('button', { name: 'Duplicate' });
const deleteButton = menu.getByRole('button', { name: 'Delete' }); const deleteButton = menu.getByRole('button', { name: 'Delete' });
const lineNumberButton = menu.getByRole('button', { name: 'Line number' });
const cancelLineNumberButton = menu.getByRole('button', {
name: 'Cancel line number',
});
return { return {
menu, menu,
@@ -41,6 +45,8 @@ export function getCodeBlock(page: Page) {
cancelWrapButton, cancelWrapButton,
duplicateButton, duplicateButton,
deleteButton, deleteButton,
lineNumberButton,
cancelLineNumberButton,
}; };
}; };