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:
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user