Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/x/components/code-highlighter/CodeHighlighter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const CodeHighlighter = React.forwardRef<HTMLDivElement, CodeHighlighterProps>((
style = {},
highlightProps,
prismLightMode = true,
showLineNumbers = false,
wrapLongLines = false,
showCopyButton = true,
...restProps
} = props;

Expand Down Expand Up @@ -141,7 +144,7 @@ const CodeHighlighter = React.forwardRef<HTMLDivElement, CodeHighlighterProps>((
>
{lang}
</span>
<Actions.Copy text={children} />
{showCopyButton && <Actions.Copy text={children.replace(/\n$/, '')} />}
</div>
);
}
Expand All @@ -160,6 +163,8 @@ const CodeHighlighter = React.forwardRef<HTMLDivElement, CodeHighlighterProps>((
<Highlighter
language={lang}
wrapLines={true}
showLineNumbers={showLineNumbers}
wrapLongLines={wrapLongLines}
style={customOneLight}
codeTagProps={{ style: { background: 'transparent' } }}
{...highlightProps}
Expand Down
164 changes: 163 additions & 1 deletion packages/x/components/code-highlighter/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ describe('CodeHighlighter', () => {
const { container } = render(
<CodeHighlighter
lang="javascript"
highlightProps={{ showLineNumbers: true, startingLineNumber: 5 }}
highlightProps={{ showLineNumberss: true, startingLineNumber: 5 }}
>
{`console.log("test");`}
</CodeHighlighter>,
Expand Down Expand Up @@ -512,6 +512,168 @@ describe('CodeHighlighter', () => {
});
});

describe('showLineNumbers', () => {
it('should render line numbers when showLineNumbers is true', async () => {
const { container } = render(
<CodeHighlighter lang="javascript" showLineNumbers>
{`console.log("test");`}
</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('pre')).toBeInTheDocument();
});
// react-syntax-highlighter renders line numbers with class "react-syntax-highlighter-line-number"
expect(
container.querySelector('.react-syntax-highlighter-line-number'),
).toBeInTheDocument();
});

it('should not render line numbers by default', async () => {
const { container } = render(
<CodeHighlighter lang="javascript">{`console.log("test");`}</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('pre')).toBeInTheDocument();
});
expect(
container.querySelector('.react-syntax-highlighter-line-number'),
).not.toBeInTheDocument();
});

it('should not render line numbers when showLineNumbers is false', async () => {
const { container } = render(
<CodeHighlighter lang="javascript" showLineNumbers={false}>
{`console.log("test");`}
</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('pre')).toBeInTheDocument();
});
expect(
container.querySelector('.react-syntax-highlighter-line-number'),
).not.toBeInTheDocument();
});
});

describe('wrapLongLines', () => {
it('should apply wrapLongLines style when enabled', async () => {
const { container } = render(
<CodeHighlighter lang="javascript" wrapLongLines>
{`console.log("test");`}
</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('pre')).toBeInTheDocument();
});
// react-syntax-highlighter sets whiteSpace: 'pre-wrap' on the code element
const codeElement = container.querySelector('code');
expect(codeElement).toBeInTheDocument();
expect(codeElement?.style.whiteSpace).toBe('pre-wrap');
});

it('should not apply wrapLongLines style by default', async () => {
const { container } = render(
<CodeHighlighter lang="javascript">{`console.log("test");`}</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('pre')).toBeInTheDocument();
});
const codeElement = container.querySelector('code');
expect(codeElement).toBeInTheDocument();
expect(codeElement?.style.whiteSpace).not.toBe('pre-wrap');
});
});

describe('showCopyButton', () => {
it('should render copy button by default', async () => {
const { container } = render(
<CodeHighlighter lang="javascript">{`console.log("test");`}</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('.ant-codeHighlighter-header')).toBeInTheDocument();
});
// The default header should contain a copy button (Actions.Copy renders a button)
const buttons = container.querySelectorAll('.ant-codeHighlighter-header button');
expect(buttons.length).toBeGreaterThan(0);
});

it('should not render copy button when showCopyButton is false', async () => {
const { container } = render(
<CodeHighlighter lang="javascript" showCopyButton={false}>
{`console.log("test");`}
</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('.ant-codeHighlighter-header')).toBeInTheDocument();
});
// Header should still show the language name
expect(container.querySelector('.ant-codeHighlighter-header')?.textContent).toContain(
'javascript',
);
// But no copy button (no buttons in header)
const buttons = container.querySelectorAll('.ant-codeHighlighter-header button');
expect(buttons.length).toBe(0);
});

it('should render copy button when showCopyButton is true', async () => {
const { container } = render(
<CodeHighlighter lang="javascript" showCopyButton>
{`console.log("test");`}
</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('.ant-codeHighlighter-header')).toBeInTheDocument();
});
const buttons = container.querySelectorAll('.ant-codeHighlighter-header button');
expect(buttons.length).toBeGreaterThan(0);
});

it('should not affect custom header', async () => {
const { container } = render(
<CodeHighlighter
lang="javascript"
showCopyButton={false}
header={<div className="myCustomHeader">custom</div>}
>
{`console.log("test");`}
</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('.myCustomHeader')).toBeInTheDocument();
});
// Custom header should be rendered as-is
expect(container.querySelector('.myCustomHeader')?.textContent).toContain('custom');
});
});

describe('combined props', () => {
it('should work with showLineNumbers, wrapLongLines, and showCopyButton together', async () => {
const { container } = render(
<CodeHighlighter
lang="javascript"
showLineNumbers
wrapLongLines
showCopyButton={false}
>
{`console.log("test");`}
</CodeHighlighter>,
);
await waitFor(() => {
expect(container.querySelector('pre')).toBeInTheDocument();
});
// Line numbers should be present
expect(
container.querySelector('.react-syntax-highlighter-line-number'),
).toBeInTheDocument();
// Wrap should be applied
const codeElement = container.querySelector('code');
expect(codeElement?.style.whiteSpace).toBe('pre-wrap');
// No copy button
const buttons = container.querySelectorAll('.ant-codeHighlighter-header button');
expect(buttons.length).toBe(0);
});
});

describe('displayName', () => {
it('should have displayName in development', () => {
const originalEnv = process.env.NODE_ENV;
Expand Down
42 changes: 42 additions & 0 deletions packages/x/components/code-highlighter/demo/configurations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { CodeHighlighter } from '@ant-design/x';
import React from 'react';

const App: React.FC = () => {
const code = `function fibonacci(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}

const result = fibonacci(10);
console.log(result); // 55`;

return (
<div>
<h3 style={{ marginBottom: 8 }}>Show Line Number</h3>
<CodeHighlighter lang="javascript" showLineNumbers>
{code}
</CodeHighlighter>

<h3 style={{ margin: '8px 0' }}>Wrap Long Lines</h3>
<CodeHighlighter lang="javascript" wrapLongLines>
{`const config = { apiKey: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", endpoint: "https://api.example.com/v1/chat/completions", model: "gpt-4" };`}
</CodeHighlighter>

<h3 style={{ margin: '8px 0' }}>Hide Copy Button</h3>
<CodeHighlighter lang="javascript" showCopyButton={false}>
{code}
</CodeHighlighter>

<h3 style={{ margin: '8px 0' }}>All Combined</h3>
<CodeHighlighter lang="javascript" showLineNumbers wrapLongLines showCopyButton={false}>
{code}
</CodeHighlighter>
</div>
);
};

export default App;
4 changes: 4 additions & 0 deletions packages/x/components/code-highlighter/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The CodeHighlighter component is used in scenarios where you need to display cod
<!-- prettier-ignore -->
<code src="./demo/basic.tsx">Basic</code>
<code src="./demo/custom-header.tsx">Custom Header</code>
<code src="./demo/configurations.tsx">Configurations</code>
<code src="./demo/with-xmarkdown.tsx">With XMarkdown</code>

## API
Expand All @@ -39,6 +40,9 @@ For common properties, refer to: [Common Properties](/docs/react/common-props).
| classNames | Style class names | `string` | - |
| highlightProps | Code highlighting configuration | [`highlightProps`](https://github.com/react-syntax-highlighter/react-syntax-highlighter?tab=readme-ov-file#props) | - |
| prismLightMode | Whether to use Prism light mode to automatically load language support based on lang prop for smaller bundle size | `boolean` | `true` |
| showLineNumbers | Whether to show line numbers | `boolean` | `false` |
| wrapLongLines | Whether to wrap long lines | `boolean` | `false` |
| showCopyButton | Whether to show copy button (only effective with default header) | `boolean` | `true` |

### CodeHighlighterRef

Expand Down
4 changes: 4 additions & 0 deletions packages/x/components/code-highlighter/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ CodeHighlighter 组件用于需要展示带有语法高亮的代码片段的场
<!-- prettier-ignore -->
<code src="./demo/basic.tsx">基本</code>
<code src="./demo/custom-header.tsx">自定义 Header</code>
<code src="./demo/configurations.tsx">配置项</code>
<code src="./demo/with-xmarkdown.tsx">配合 XMarkdown</code>

## API
Expand All @@ -38,6 +39,9 @@ CodeHighlighter 组件用于需要展示带有语法高亮的代码片段的场
| header | 头部内容,为 `false` 时不显示头部 | `React.ReactNode \| (() => React.ReactNode \| false) \| false` | - |
| highlightProps | 代码高亮配置,透传给 react-syntax-highlighter | [`SyntaxHighlighterProps`](https://github.com/react-syntax-highlighter/react-syntax-highlighter?tab=readme-ov-file#props) | - |
| prismLightMode | 是否使用 Prism 轻量模式,根据 `lang` 自动按需加载语言支持以减少打包体积 | `boolean` | `true` |
| showLineNumbers | 是否显示行号 | `boolean` | `false` |
| wrapLongLines | 是否自动换行(长行换行) | `boolean` | `false` |
| showCopyButton | 是否显示复制按钮(仅在默认 header 下生效) | `boolean` | `true` |

### CodeHighlighterRef

Expand Down
18 changes: 18 additions & 0 deletions packages/x/components/code-highlighter/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,22 @@ export interface CodeHighlighterProps
* ```
*/
prismLightMode?: boolean;
/**
* @desc 是否显示行号
* @descEN Whether to show line numbers
* @default false
*/
showLineNumbers?: boolean;
/**
* @desc 是否自动换行(长行换行)
* @descEN Whether to wrap long lines
* @default false
*/
wrapLongLines?: boolean;
/**
* @desc 是否显示复制按钮(仅在默认 header 下生效)
* @descEN Whether to show copy button (only effective with default header)
* @default true
*/
showCopyButton?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,62 @@ exports[`renders components/suggestion/demo/block.tsx correctly 1`] = `
</div>
`;

exports[`renders components/suggestion/demo/scroll.tsx correctly 1`] = `
<div
class="ant-suggestion ant-suggestion-content css-var-_R_0_"
>
<div
class="ant-sender css-var-_R_0_ ant-sender-main"
>
<div
class="ant-sender-content"
>
<textarea
class="ant-input ant-input-borderless css-var-_R_0_ ant-input-css-var ant-sender-input"
placeholder="输入 / 获取建议"
/>
<div
class="ant-sender-actions-list"
>
<div
class="ant-sender-actions-list-presets ant-flex css-var-_R_0_"
>
<button
class="ant-btn css-var-_R_0_ ant-btn-circle ant-btn-primary ant-btn-color-primary ant-btn-variant-solid ant-btn-icon-only ant-sender-actions-btn ant-sender-actions-btn-disabled"
disabled=""
type="button"
>
<span
class="ant-btn-icon"
>
<span
aria-label="arrow-up"
class="anticon anticon-arrow-up"
role="img"
>
<svg
aria-hidden="true"
data-icon="arrow-up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M868 545.5L536.1 163a31.96 31.96 0 00-48.3 0L156 545.5a7.97 7.97 0 006 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2z"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;

exports[`renders components/suggestion/demo/trigger.tsx correctly 1`] = `
<div
class="ant-suggestion ant-suggestion-content css-var-_R_0_"
Expand Down
Loading