Repository: web-infra-dev/rsdoctor
Pull request #1028: feat(components): refactor CodeViewer and DiffViewer
Author: login: quanquan2100
Labels: name: change: feat
Created: 2025-04-20T09:37:24Z | Merged: 2025-04-23T04:12:40Z

## Summary

- Refactor CodeViewer and DiffViewer to new component
- Change some old usages to new components instead, and optimize UI
  - `/webpack/loaders/analysis`
  - `/bundle/size`
  - `/module/resolve`
- Add `@deprecated` to old comp. I will replace them later.

relate: https://github.com/web-infra-dev/rsdoctor/discussions/1027

## Editor change

- Editor configuration changes, disabling unnecessary features, including command shortcut behaviors, right-click menu behaviors, etc.
- The default theme is unified in light
- The editor style uses the monaco css variable, which can automatically adapt to the theme
- Optimize file type detection, support markdown, optimize data URL detection
- Add header bar and build-in editor capabilities, such as: side-by-side, word-wrap
- Performance optimization, add memo for non-primitive type properties to reduce unnecessary rendering

## New component

This feature offers developers out-of-the-box component/hook to display code, so developers don't need care about monaco-config detail any more.

### CodeViewer

**Example：**
```tsx
const code = "const a = 1;"
const filePath = "index.js"
const {showCode, codeDrawerComponent} = useCodeDrawer();
<>
  <!-- <CodeViewer>：show code -->
  <CodeViewer
    code={code}
    filePath={filePath}
  />
  <!-- useCodeDrawer：show code in drawer -->
  {codeDrawerComponent}
  <Button onClick={() => showCode({code, filePath})}>code</Button>
</>
```

### DiffViewer

**Example：**
```tsx
const original = "const a = 1;"
const modified = "var a = 1;"
const originalFilePath = "index.js"
const modifiedFilePath = "index.js"
const {showCodeDiff, codeDiffDrawerComponent} = useDiffDrawer();
<>
  <!-- <DiffViewer>：show code diff -->
  <DiffViewer
    original={original}
    modified={modified}
    originalFilePath={originalFilePath}
    modifiedFilePath={modifiedFilePath}
  />
  <!-- useDiffDrawer：show code diff in drawer -->
  {codeDrawerComponent}
  <Button onClick={() => showCodeDiff({original, modified, originalFilePath, modifiedFilePath})}>code</Button>
</>
```

## Preview


|      NEW     |   OLD   |
| --------------------------------------------------------------------------- | --- |
| `/module/resolve`<br>![CleanShot 2025-04-20 at 17 19 55@2x](https://github.com/user-attachments/assets/17627d1b-17b6-4814-9cc3-46d742df68ad)               |  ![CleanShot 2025-04-20 at 17 23 44@2x](https://github.com/user-attachments/assets/59ba3e91-a496-41ba-a2a2-e39259f20b7b)   |
| `/bundle/size`<br>![CleanShot 2025-04-20 at 17 12 38@2x](https://github.com/user-attachments/assets/7f21a62b-10ac-496a-bbcb-fdd78c5c74d1)              |  ![CleanShot 2025-04-20 at 17 29 06@2x](https://github.com/user-attachments/assets/947ba1db-f9b3-4f54-9000-f1545d1b2c80)  |
| `/bundle/size`<br>![CleanShot 2025-04-20 at 17 11 31@2x](https://github.com/user-attachments/assets/1994489d-d5b7-4b91-93af-6fd5eef40a3c)      |   ![CleanShot 2025-04-20 at 17 28 11@2x](https://github.com/user-attachments/assets/cd2b7f6d-dda7-4b3c-abe8-453099c847d7)  |
| `/webpack/loaders/analysis`<br>![CleanShot 2025-04-20 at 17 13 22@2x](https://github.com/user-attachments/assets/77c01e3f-1f4d-418e-9d8f-d25e02fa0a92)  |  ![CleanShot 2025-04-20 at 17 25 06@2x](https://github.com/user-attachments/assets/6ea082ca-3203-47ff-872e-6fa9a9cdeb76)
   |




Diff:
diff --git a/packages/components/package.json b/packages/components/package.json
index 33b607002..6d4dd920f 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -47,8 +47,8 @@
   },
   "devDependencies": {
     "@ant-design/icons": "5.6.1",
-    "@rsbuild/plugin-check-syntax": "1.3.0",
     "@monaco-editor/react": "4.7.0",
+    "@rsbuild/plugin-check-syntax": "1.3.0",
     "@rsdoctor/graph": "workspace:*",
     "@rsdoctor/types": "workspace:*",
     "@rsdoctor/utils": "workspace:*",
@@ -61,6 +61,7 @@
     "ansi-to-react": "6.1.6",
     "antd": "5.19.1",
     "axios": "^1.8.4",
+    "clsx": "^2.1.1",
     "dayjs": "1.11.13",
     "echarts": "^5.6.0",
     "echarts-for-react": "^3.0.3",
diff --git a/packages/components/src/components/CodeViewer/config.ts b/packages/components/src/components/CodeViewer/config.ts
index 2eecd6952..335abf6b6 100644
--- a/packages/components/src/components/CodeViewer/config.ts
+++ b/packages/components/src/components/CodeViewer/config.ts
@@ -1,17 +1,19 @@
 import type { editor } from 'monaco-editor';
 
-export const DefaultEditorConfig: editor.IStandaloneEditorConstructionOptions = {
-  readOnly: true,
-  domReadOnly: true,
-  fontSize: 12,
-  renderValidationDecorations: 'off',
-  hideCursorInOverviewRuler: true,
-  smoothScrolling: true,
-  wordWrap: 'bounded',
-  colorDecorators: true,
-  codeLens: false,
-  cursorWidth: 0,
-  minimap: {
-    enabled: false,
-  },
-};
+/** @deprecated if you need get monacoConfig, please use defineMonacoOptions() in /base/CodeViewer/utils.ts */
+export const DefaultEditorConfig: editor.IStandaloneEditorConstructionOptions =
+  {
+    readOnly: true,
+    domReadOnly: true,
+    fontSize: 12,
+    renderValidationDecorations: 'off',
+    hideCursorInOverviewRuler: true,
+    smoothScrolling: true,
+    wordWrap: 'bounded',
+    colorDecorators: true,
+    codeLens: false,
+    cursorWidth: 0,
+    minimap: {
+      enabled: false,
+    },
+  };
diff --git a/packages/components/src/components/CodeViewer/diff.tsx b/packages/components/src/components/CodeViewer/diff.tsx
index 08763f109..c3b5a117c 100644
--- a/packages/components/src/components/CodeViewer/diff.tsx
+++ b/packages/components/src/components/CodeViewer/diff.tsx
@@ -12,6 +12,7 @@ interface DiffViewerProps {
   editorProps?: DiffEditorProps;
 }
 
+/** @deprecated please use base/DiffViewer */
 export const DiffViewer: React.FC<DiffViewerProps> = ({
   before,
   after,
diff --git a/packages/components/src/components/CodeViewer/highlight.tsx b/packages/components/src/components/CodeViewer/highlight.tsx
index eccf1093b..c6007c3f5 100644
--- a/packages/components/src/components/CodeViewer/highlight.tsx
+++ b/packages/components/src/components/CodeViewer/highlight.tsx
@@ -1,15 +1,15 @@
 /* eslint-disable financial/no-float-calculation */
-import React, { useRef } from 'react';
-import { Space, Card, Tooltip } from 'antd';
-import { SDK } from '@rsdoctor/types';
 import { InfoCircleOutlined } from '@ant-design/icons';
 import Editor, { OnMount } from '@monaco-editor/react';
+import { SDK } from '@rsdoctor/types';
+import { Card, Space, Tooltip } from 'antd';
 import type { editor, Range as RangeClass } from 'monaco-editor';
+import React, { useRef } from 'react';
 import {
-  getOriginalLanguage,
   beautifyModulePath,
-  getSelectionRange,
+  getOriginalLanguage,
   getRevealPositionForViewer,
+  getSelectionRange,
 } from '../../utils';
 import { CodeOpener } from '../Opener';
 
@@ -20,6 +20,8 @@ interface FileHightLightViewerProps {
   moduleCode: SDK.ModuleSource;
 }
 
+///REVIEW - if can delete
+/** @deprecated please use CodeViewer in /base/CodeViewer */
 export const FileHightLightViewer: React.FC<FileHightLightViewerProps> = ({
   dependency,
   module,
diff --git a/packages/components/src/components/CodeViewer/viewer.tsx b/packages/components/src/components/CodeViewer/viewer.tsx
index b60390d5c..2574bdb3a 100644
--- a/packages/components/src/components/CodeViewer/viewer.tsx
+++ b/packages/components/src/components/CodeViewer/viewer.tsx
@@ -1,12 +1,12 @@
-import React from 'react';
-import { SDK } from '@rsdoctor/types';
 import Editor, { OnMount } from '@monaco-editor/react';
+import { SDK } from '@rsdoctor/types';
+import { Empty } from 'antd';
 import { Lodash } from '@rsdoctor/utils/common';
 import type { editor } from 'monaco-editor';
+import React from 'react';
 import { getOriginalLanguage, getSelectionRange } from '../../utils';
-import { DefaultEditorConfig } from './config';
 import { TextDrawer } from '../TextDrawer';
-import { Empty } from 'antd';
+import { DefaultEditorConfig } from './config';
 
 interface CodeViewerProps {
   path: string;
@@ -17,6 +17,7 @@ interface CodeViewerProps {
   emptyReason?: string;
 }
 
+/** @deprecated please use CodeViewer in /base/CodeViewer */
 export const CodeViewer: React.FC<CodeViewerProps> = ({
   content,
   ranges,
@@ -58,6 +59,8 @@ export const CodeViewer: React.FC<CodeViewerProps> = ({
   );
 };
 
+///REVIEW - no use, if can delete
+/** @deprecated please use hook useCodeDrawer instead */
 export const CodeViewerWithDrawer: React.FC<CodeViewerProps> = (props) => {
   return (
     <TextDrawer
diff --git a/packages/components/src/components/Loader/Analysis/files.tsx b/packages/components/src/components/Loader/Analysis/files.tsx
index 961004284..dde223a10 100644
--- a/packages/components/src/components/Loader/Analysis/files.tsx
+++ b/packages/components/src/components/Loader/Analysis/files.tsx
@@ -15,7 +15,7 @@ import {
   Typography,
 } from 'antd';
 import { get } from 'lodash-es';
-import React, { useCallback, useMemo, useRef, useState } from 'react';
+import React, { useMemo, useState } from 'react';
 import { drawerWidth, Size } from '../../../constants';
 import {
   createFileStructures,
@@ -23,7 +23,6 @@ import {
   filterLoader,
   formatCosts,
   mapFileKey,
-  useMonacoEditor,
 } from '../../../utils';
 import { FileTree } from '../../FileTree';
 import { Keyword } from '../../Keyword';
@@ -32,7 +31,6 @@ import { LoaderExecutions } from '../executions';
 import styles from './style.module.scss';
 
 const ADDITION_LOADER_NUMBER = 3;
-// const { innerWidth } = window;
 
 export const LoaderFiles: React.FC<{
   filetree: SDK.ServerAPI.InferResponseType<SDK.ServerAPI.API.GetLoaderFileTree>;
@@ -47,13 +45,6 @@ export const LoaderFiles: React.FC<{
   const [loaderIndex, setLoaderIndex] = useState(0);
   const [selectedNode, setSelectedNode] = useState<DataNode | null>(null);
 
-  const monacoRef = useRef<HTMLElement>(null);
-  const { initMonaco } = useMonacoEditor();
-
-  const init = useCallback(async () => {
-    await initMonaco(monacoRef);
-  }, []);
-
   const maxHeight = 800;
 
   const filteredFiles = useMemo(
@@ -258,7 +249,6 @@ export const LoaderFiles: React.FC<{
             treeData={files}
             key={`${props.loaders.join('|')}_${props.filename}`}
             onSelect={(_e, info) => {
-              init();
               if (!info.node.isLeaf) {
                 setSelectedNode(info.node);
               }
diff --git a/packages/components/src/components/Loader/executions.tsx b/packages/components/src/components/Loader/executions.tsx
index f3ea9227a..86a136c6a 100644
--- a/packages/components/src/components/Loader/executions.tsx
+++ b/packages/components/src/components/Loader/executions.tsx
@@ -1,8 +1,6 @@
 import { ClockCircleTwoTone } from '@ant-design/icons';
-import Editor from '@monaco-editor/react';
 import { SDK } from '@rsdoctor/types';
 import {
-  Checkbox,
   Col,
   Divider,
   Empty,
@@ -17,19 +15,14 @@ import {
 } from 'antd';
 import dayjs from 'dayjs';
 import { PropsWithChildren, useCallback, useState } from 'react';
-
 import InputIcon from 'src/common/svg/loader/input.svg';
 import OutputIcon from 'src/common/svg/loader/output.svg';
 import StepIcon from 'src/common/svg/loader/step.svg';
 import { Size } from '../../constants';
-import {
-  beautifyPath,
-  formatCosts,
-  getModifiedLanguage,
-  useTheme,
-} from '../../utils';
+import { beautifyPath, formatCosts, useTheme } from '../../utils';
+import { CodeViewer } from '../base/CodeViewer';
+import { DiffViewer } from '../base/DiffViewer';
 import { Card } from '../Card';
-import { DiffViewer } from '../CodeViewer';
 import { CodeOpener } from '../Opener';
 import { Title } from '../Title';
 import styles from './Analysis/style.module.scss';
@@ -130,7 +123,6 @@ export const LoaderExecutions = ({
   const leftSpan = 5;
   const hasError = loader.errors && loader.errors.length;
   const [activeKey, setActiveKey] = useState('loaderDetails');
-  const [isSideBySide, setIsSideBySide] = useState(true);
   const onChange = useCallback((key: string) => {
     setActiveKey(key);
   }, []);
@@ -241,17 +233,9 @@ export const LoaderExecutions = ({
                         />
                       </div>
                       <div style={{ height: '90%' }}>
-                        <Editor
-                          theme="vs"
-                          options={{
-                            readOnly: true,
-                            domReadOnly: true,
-                            fontSize: 14,
-                            minimap: { enabled: false },
-                            lineNumbers: 'off',
-                          }}
-                          value={loader.errors[0].message}
-                          language="javascript"
+                        <CodeViewer
+                          code={loader.errors[0].message}
+                          lang="javascript"
                         />
                       </div>
                     </Col>
@@ -262,38 +246,45 @@ export const LoaderExecutions = ({
                           display: 'flex',
                           alignItems: 'center',
                           padding: Size.BasePadding,
-                          borderTop: `1px solid ${isLight ? '#f0f0f0' : 'rgba(253, 253, 253, 0.12)'}`,
                           borderBottom: `1px solid ${isLight ? '#f0f0f0' : 'rgba(253, 253, 253, 0.12)'}`,
                         }}
                       >
                         <Title
-                          text={`the result of [${loader.loader}] ${loader.isPitch ? 'pitch' : ''}`}
+                          text={
+                            <>
+                              {`the result of [${loader.loader}] ${loader.isPitch ? 'pitch' : ''}`}
+                              {!loader.isPitch && (
+                                <span style={{ fontWeight: 400 }}>
+                                  (
+                                  <InputIcon
+                                    style={{
+                                      verticalAlign: 'middle',
+                                      margin: '0 2px',
+                                    }}
+                                  />
+                                  Input ⟷
+                                  <OutputIcon
+                                    style={{
+                                      verticalAlign: 'middle',
+                                      position: 'relative',
+                                      top: -2,
+                                    }}
+                                  />
+                                  Output)
+                                </span>
+                              )}
+                            </>
+                          }
                         />
                         <div style={{ flex: 1 }} />
-                        <Checkbox
-                          title="side-by-side"
-                          checked={isSideBySide}
-                          onChange={(evt) => {
-                            setIsSideBySide(evt.target.checked);
-                          }}
-                        >
-                          side-by-side
-                        </Checkbox>
                       </div>
                       {loader.isPitch ? (
                         loader.result ? (
                           <div style={{ height: '90%' }}>
-                            <Editor
-                              theme="vs"
-                              options={{
-                                readOnly: true,
-                                domReadOnly: true,
-                                fontSize: 14,
-                                formatOnType: true,
-                                formatOnPaste: true,
-                              }}
-                              value={loader.result}
-                              language={getModifiedLanguage(resource.path)}
+                            <CodeViewer
+                              isEmbed
+                              code={loader.result}
+                              filePath={resource.path}
                             />
                           </div>
                         ) : (
@@ -305,37 +296,7 @@ export const LoaderExecutions = ({
                         )
                       ) : (
                         <div style={{ minHeight: '700px' }}>
-                          {isSideBySide && (
-                            <Row>
-                              <Col
-                                span={12}
-                                style={{
-                                  padding: `${Size.BasePadding / 2}px ${Size.BasePadding}px`,
-                                }}
-                              >
-                                <Space align="center" className={styles.space}>
-                                  <InputIcon />
-                                  <Typography.Text strong>
-                                    Input
-                                  </Typography.Text>
-                                </Space>
-                              </Col>
-                              <Col
-                                span={12}
-                                style={{
-                                  padding: `${Size.BasePadding / 2}px ${Size.BasePadding}px`,
-                                }}
-                              >
-                                <Space align="center" className={styles.space}>
-                                  <OutputIcon />
-                                  <Typography.Text strong>
-                                    Output
-                                  </Typography.Text>
-                                </Space>
-                              </Col>
-                            </Row>
-                          )}
-                          <div style={{ height: '40rem' }}>
+                          <div style={{ height: '40rem', overflow: 'hidden' }}>
                             {!loader.result && !before ? (
                               <Empty
                                 description={
@@ -344,15 +305,11 @@ export const LoaderExecutions = ({
                               />
                             ) : (
                               <DiffViewer
-                                filepath={resource.path}
-                                before={before}
-                                after={loader.result || ''}
-                                editorProps={{
-                                  options: {
-                                    renderSideBySideInlineBreakpoint: 1,
-                                    renderSideBySide: isSideBySide,
-                                  },
-                                }}
+                                isEmbed
+                                original={before}
+                                modified={loader.result || ''}
+                                originalFilePath={resource.path}
+                                modifiedFilePath={resource.path}
                               />
                             )}
                           </div>
diff --git a/packages/components/src/components/Resolver/analysis.tsx b/packages/components/src/components/Resolver/analysis.tsx
index 9fbee8402..6f78f146c 100644
--- a/packages/components/src/components/Resolver/analysis.tsx
+++ b/packages/components/src/components/Resolver/analysis.tsx
@@ -1,32 +1,46 @@
 /* eslint-disable react/no-unescaped-entities */
-import React, { useMemo, useState } from 'react';
-import { get, map } from 'lodash-es';
-import { Button, Card, Col, Row, Space, Table, Typography } from 'antd';
-import { CloseCircleOutlined, FileSearchOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
-import { Resolver } from '@rsdoctor/utils/common';
+import {
+  CloseCircleOutlined,
+  FileSearchOutlined,
+  MenuFoldOutlined,
+  MenuUnfoldOutlined,
+} from '@ant-design/icons';
 import { SDK } from '@rsdoctor/types';
+import { Resolver } from '@rsdoctor/utils/common';
+import { Button, Card, Col, Row, Space, Table, Typography } from 'antd';
+import { get, map } from 'lodash-es';
+import React, { useMemo, useState } from 'react';
 import { Size } from '../../constants';
 import { createFileStructures, formatCosts, mapFileKey } from '../../utils';
 import { FileTree } from '../FileTree';
 import { KeywordInput } from '../Form/keyword';
-import { DiffViewer } from '../CodeViewer';
 import { ServerAPIProvider, withServerAPI } from '../Manifest';
+import { DiffViewer } from '../base/DiffViewer';
 
 const height = 735;
 
-const ResolverDetailsPanel: React.FC<SDK.ServerAPI.InferResponseType<SDK.ServerAPI.API.GetResolverFileDetails>> = ({
-  filepath,
-  before,
-  after,
-  resolvers,
-}) => {
+const ResolverDetailsPanel: React.FC<
+  SDK.ServerAPI.InferResponseType<SDK.ServerAPI.API.GetResolverFileDetails>
+> = ({ filepath, before, after, resolvers }) => {
   const [collapsed, setCollapsed] = useState(false);
 
   return (
     <React.Fragment>
       <Col flex={1}>
-        <Card title="Resolve Diff Viewer">
-          <DiffViewer filepath={filepath} before={before} after={after} editorProps={{ height }} />
+        <Card
+          title="Resolve Diff Viewer"
+          styles={{ body: { padding: 0, overflow: 'hidden' } }}
+        >
+          <DiffViewer
+            style={{
+              height: height + 50,
+            }}
+            isEmbed
+            originalFilePath={filepath}
+            modifiedFilePath={filepath}
+            original={before}
+            modified={after}
+          />
         </Card>
       </Col>
       <Col span={collapsed ? 2 : 7}>
@@ -43,7 +57,12 @@ const ResolverDetailsPanel: React.FC<SDK.ServerAPI.InferResponseType<SDK.ServerA
         >
           {collapsed ? null : (
             <Table
-              style={{ width: '100%', height, overflowY: 'scroll', wordBreak: 'break-all' }}
+              style={{
+                width: '100%',
+                height,
+                overflowY: 'scroll',
+                wordBreak: 'break-all',
+              }}
               size="small"
               pagination={false}
               bordered
@@ -63,14 +82,21 @@ const ResolverDetailsPanel: React.FC<SDK.ServerAPI.InferResponseType<SDK.ServerA
                 {
                   title: 'Duration',
                   width: 80,
-                  render: (_v, r) => <Typography.Text strong>{formatCosts(r.costs)}</Typography.Text>,
+                  render: (_v, r) => (
+                    <Typography.Text strong>
+                      {formatCosts(r.costs)}
+                    </Typography.Text>
+                  ),
                   sorter: (a, b) => a.costs - b.costs,
                   sortDirections: ['descend', 'ascend'],
                 },
                 {
                   title: 'Resolve Result',
                   render: (_v, r) => {
-                    if (Resolver.isResolveSuccessData(r)) return <Typography.Text copyable>{r.result}</Typography.Text>;
+                    if (Resolver.isResolveSuccessData(r))
+                      return (
+                        <Typography.Text copyable>{r.result}</Typography.Text>
+                      );
                     return <CloseCircleOutlined style={{ color: '#f50' }} />;
                   },
                 },
@@ -101,10 +127,13 @@ export const ResolverFiles: React.FC<{
   }, [resolver]);
 
   const filterPaths = useMemo(() => {
-    return paths.filter((e) => (props.filename ? e.indexOf(props.filename) > -1 : true));
+    return paths.filter((e) =>
+      props.filename ? e.indexOf(props.filename) > -1 : true,
+    );
   }, [props.filename, paths]);
 
-  const allNodeModules = filterPaths.length && filterPaths.every((e) => e.indexOf(n) > -1);
+  const allNodeModules =
+    filterPaths.length && filterPaths.every((e) => e.indexOf(n) > -1);
 
   const files = useMemo(() => {
     return createFileStructures({
@@ -129,10 +158,16 @@ export const ResolverFiles: React.FC<{
               allNodeModules
                 ? undefined
                 : (node) => {
-                    const resourcePath: string = get(node, inlinedResourcePathKey)!;
+                    const resourcePath: string = get(
+                      node,
+                      inlinedResourcePathKey,
+                    )!;
                     const isNodeModules = resourcePath.indexOf(n) > -1;
 
-                    if (filterPaths.length && filterPaths.every((e) => e.indexOf(n) > -1)) {
+                    if (
+                      filterPaths.length &&
+                      filterPaths.every((e) => e.indexOf(n) > -1)
+                    ) {
                       return true;
                     }
 
@@ -144,7 +179,10 @@ export const ResolverFiles: React.FC<{
         </Card>
       </Col>
       {filepath && (
-        <ServerAPIProvider api={SDK.ServerAPI.API.GetResolverFileDetails} body={{ filepath }}>
+        <ServerAPIProvider
+          api={SDK.ServerAPI.API.GetResolverFileDetails}
+          body={{ filepath }}
+        >
           {(resolvers) => <ResolverDetailsPanel {...resolvers} />}
         </ServerAPIProvider>
       )}
@@ -160,7 +198,9 @@ export const ResolverAnalysisBase: React.FC<{
 
   return (
     <div style={{ width: '100%' }}>
-      <Space style={{ marginTop: Size.BasePadding, marginBottom: Size.BasePadding }}>
+      <Space
+        style={{ marginTop: Size.BasePadding, marginBottom: Size.BasePadding }}
+      >
         <KeywordInput
           icon={<FileSearchOutlined />}
           label="Filename"
@@ -169,7 +209,9 @@ export const ResolverAnalysisBase: React.FC<{
         />
       </Space>
       <ServerAPIProvider api={SDK.ServerAPI.API.GetResolverFileTree}>
-        {(resolver) => <ResolverFiles filename={filename} resolver={resolver} cwd={cwd} />}
+        {(resolver) => (
+          <ResolverFiles filename={filename} resolver={resolver} cwd={cwd} />
+        )}
       </ServerAPIProvider>
     </div>
   );
diff --git a/packages/components/src/components/base/CodeViewer/index.module.scss b/packages/components/src/components/base/CodeViewer/index.module.scss
new file mode 100644
index 000000000..366bb060b
--- /dev/null
+++ b/packages/components/src/components/base/CodeViewer/index.module.scss
@@ -0,0 +1,44 @@
+.code-viewer {
+  height: 70vh;
+  border-radius: 8px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background-color: var(--vscode-editor-background);
+  box-shadow:
+    0 1px 3px 0 rgb(0 0 0 / 0.1),
+    0 1px 2px -1px rgb(0 0 0 / 0.1);
+
+  &.embed {
+    box-shadow: none;
+    border-radius: 0;
+    overflow: auto;
+  }
+
+  .header {
+    background-color: var(--vscode-textBlockQuote-background);
+    color: var(--vscode-foreground);
+    padding: 8px 16px;
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
+    flex-grow: 0;
+
+    .text {
+      color: var(--vscode-foreground);
+    }
+  }
+
+  .content {
+    flex: 1;
+  }
+
+  :global {
+    .file-inline-decoration {
+      cursor: pointer;
+      text-decoration: underline;
+      background: rgb(77, 112, 151);
+      background: var(--vscode-editor-selectionHighlightBackground);
+    }
+  }
+}
diff --git a/packages/components/src/components/base/CodeViewer/index.tsx b/packages/components/src/components/base/CodeViewer/index.tsx
new file mode 100644
index 000000000..38c42601f
--- /dev/null
+++ b/packages/components/src/components/base/CodeViewer/index.tsx
@@ -0,0 +1,101 @@
+import { Editor, Monaco, OnMount } from '@monaco-editor/react';
+import clsx from 'clsx';
+import { isNumber } from 'lodash-es';
+import { editor } from 'monaco-editor';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { CodeViewerProps } from './interface';
+import {
+  defineMonacoOptions,
+  editorShowRange,
+  getFileName,
+  getFilePathFormat,
+} from './utils';
+
+import { Checkbox } from 'antd';
+import styles from './index.module.scss';
+
+export function CodeViewer({
+  className,
+  style,
+  code = '',
+  lang,
+  filePath = '',
+  defaultLine,
+  ranges,
+  isEmbed = false,
+  headerVisible = true,
+  isLightTheme = true,
+}: CodeViewerProps) {
+  const editor = useRef<editor.IStandaloneCodeEditor>();
+  const monaco = useRef<Monaco>();
+  const language = useMemo(
+    () => lang || getFilePathFormat(filePath) || 'plaintext',
+    [lang, filePath],
+  );
+  const [isWordWrap, setIsWordWrap] = useState(true);
+  const options = useMemo(
+    () => defineMonacoOptions({ wordWrap: isWordWrap ? 'on' : 'off' }),
+    [isWordWrap],
+  );
+  const onEditorMount = useCallback<OnMount>((editorInstance, monacoVal) => {
+    editor.current = editorInstance;
+    monaco.current = monacoVal;
+
+    editorShowRange(editorInstance, monacoVal, ranges);
+    if (isNumber(defaultLine)) {
+      setTimeout(() => {
+        editorInstance.revealLine(defaultLine);
+      });
+    }
+  }, []);
+
+  const theme = isLightTheme ? 'vs-light' : 'vs-dark';
+
+  useEffect(
+    () => () => {
+      editor.current?.setModel(null);
+    },
+    [],
+  );
+
+  return (
+    <div
+      className={clsx(
+        'monaco-component',
+        'code-viewer',
+        styles['code-viewer'],
+        isEmbed && styles['embed'],
+        className,
+      )}
+      style={style}
+    >
+      {headerVisible && Boolean(filePath) && (
+        <div className={styles['header']}>
+          <div>{getFileName(filePath)}</div>
+          <div style={{ flex: 1 }} />
+          <div>
+            <Checkbox
+              className={styles['text']}
+              title="side-by-side"
+              checked={isWordWrap}
+              onChange={(evt) => {
+                setIsWordWrap(evt.target.checked);
+              }}
+            >
+              word-wrap
+            </Checkbox>
+          </div>
+        </div>
+      )}
+      <div className={clsx(styles['content'], 'editor-wrap')}>
+        <Editor
+          theme={theme}
+          language={language}
+          value={code}
+          options={options}
+          onMount={onEditorMount}
+        />
+      </div>
+    </div>
+  );
+}
diff --git a/packages/components/src/components/base/CodeViewer/interface.ts b/packages/components/src/components/base/CodeViewer/interface.ts
new file mode 100644
index 000000000..423acf588
--- /dev/null
+++ b/packages/components/src/components/base/CodeViewer/interface.ts
@@ -0,0 +1,46 @@
+import { SDK } from '@rsdoctor/types';
+import { CSSProperties } from 'react';
+
+export interface CodeViewerProps {
+  className?: string;
+  style?: CSSProperties;
+  /**
+   * Whether to display in embedded mode.
+   * Embedded mode has no border or border-radius
+   * @default false
+   */
+  isEmbed?: boolean;
+  /**
+   * Whether to use light theme configuration
+   * @default true
+   */
+  isLightTheme?: boolean;
+  /**
+   * Code content
+   */
+  code?: string;
+  /**
+   * Specify language format, highest priority. If not specified,
+   * it will be inferred from filePath
+   */
+  lang?: string;
+  /**
+   * File path
+   */
+  filePath?: string;
+  /**
+   * Default line to display in the editor
+   */
+  defaultLine?: number;
+  /**
+   * Editor text highlighting configuration. When configured,
+   * it will automatically position to the first highlighted location.
+   * Positioning priority is lower than defaultLine
+   */
+  ranges?: SDK.SourceRange[];
+  /**
+   * Whether the top bar is displayed
+   * @default true
+   */
+  headerVisible?: boolean;
+}
diff --git a/packages/components/src/components/base/CodeViewer/useCodeDrawer.tsx b/packages/components/src/components/base/CodeViewer/useCodeDrawer.tsx
new file mode 100644
index 000000000..e381f163d
--- /dev/null
+++ b/packages/components/src/components/base/CodeViewer/useCodeDrawer.tsx
@@ -0,0 +1,61 @@
+import { SDK } from '@rsdoctor/types';
+import { Drawer, Empty } from 'antd';
+import { useState } from 'react';
+import { CodeViewer } from '.';
+import { CodeViewerProps } from './interface';
+
+const defaultEmptyReason = 'No Code';
+
+/**
+ * Use drawer to display code content
+ * @param emptyReason Text to display when there is no code content
+ */
+export function useCodeDrawer(emptyReason: string) {
+  const [code, setCode] = useState('');
+  const [lang, setLang] = useState('');
+  const [filePath, setFilePath] = useState('');
+  const [visible, setVisible] = useState(false);
+  const [defaultLine, setDefaultLine] = useState<number>();
+  const [ranges, setRanges] = useState<SDK.SourceRange[]>();
+
+  const codeDrawerComponent = (
+    <Drawer
+      maskClosable
+      zIndex={999}
+      width={'70%'}
+      destroyOnClose
+      title={`Code of "${filePath}"`}
+      open={visible}
+      onClose={() => setVisible(false)}
+    >
+      {code ? (
+        <CodeViewer
+          code={code}
+          lang={lang}
+          filePath={filePath}
+          defaultLine={defaultLine}
+          ranges={ranges}
+        />
+      ) : (
+        <Empty description={emptyReason || defaultEmptyReason} />
+      )}
+    </Drawer>
+  );
+
+  return {
+    showCode(
+      codeConfig: Pick<
+        CodeViewerProps,
+        'code' | 'filePath' | 'ranges' | 'lang' | 'defaultLine'
+      >,
+    ) {
+      setCode(codeConfig.code || '');
+      setLang(codeConfig.lang || '');
+      setFilePath(codeConfig.filePath || '');
+      setDefaultLine(codeConfig.defaultLine);
+      setRanges(codeConfig.ranges);
+      setVisible(true);
+    },
+    codeDrawerComponent,
+  };
+}
diff --git a/packages/components/src/components/base/CodeViewer/utils.ts b/packages/components/src/components/base/CodeViewer/utils.ts
new file mode 100644
index 000000000..d3a473c03
--- /dev/null
+++ b/packages/components/src/components/base/CodeViewer/utils.ts
@@ -0,0 +1,238 @@
+import { Monaco } from '@monaco-editor/react';
+import { SDK } from '@rsdoctor/types';
+import { editor } from 'monaco-editor';
+import { extname } from 'path';
+import { getSelectionRange } from 'src/utils/monaco';
+
+/**
+ * 判断 data 协议文件格式 `data:[<mediatype>][;base64],<data>`
+ * 完整协议: https://www.rfc-editor.org/rfc/rfc2397.html
+ * @param {string} content 文件内容
+ * @returns {string} 文件格式类型
+ */
+function getDataProtocolFormat(content: string): string {
+  // 如果内容为空返回空
+  if (!content) {
+    return '';
+  }
+
+  try {
+    const prefix = 'data:';
+    // 检查是否以 data: 开头
+    if (!content.startsWith(prefix)) {
+      return '';
+    }
+
+    const { mimeType } = parseDataUrl(content);
+
+    // 根据 MIME 类型返回对应的格式
+    switch (mimeType) {
+      case 'text/javascript':
+        return 'javascript';
+      case 'text/html':
+        return 'html';
+      case 'text/css':
+        return 'css';
+      case 'text/plain':
+        return 'text';
+      case 'application/json':
+        return 'json';
+      // 可以根据需要添加更多类型
+      default:
+        return '';
+    }
+  } catch (error) {
+    console.error('解析 data 协议文件格式失败:', error);
+    return '';
+  }
+}
+
+/**
+ * 解析 Data URL 并提取 MIME 类型
+ * @param {string} dataUrl - 要解析的 Data URL 字符串
+ * @returns {Object} 包含解析结果的对象
+ */
+function parseDataUrl(dataUrl: string) {
+  // 检查是否是 Data URL
+  if (!dataUrl.startsWith('data:')) {
+    throw new Error('输入不是有效的 Data URL');
+  }
+
+  // 提取内容部分 (去掉 "data:" 前缀)
+  const content = dataUrl.substring(5);
+
+  // 查找分隔数据和元数据的逗号
+  const commaIndex = content.indexOf(',');
+
+  if (commaIndex === -1) {
+    throw new Error('无效的 Data URL 格式: 缺少数据分隔符');
+  }
+
+  // 提取元数据部分
+  const metadataPart = content.substring(0, commaIndex);
+
+  // 提取数据部分
+  const dataPart = content.substring(commaIndex + 1);
+
+  // 解析元数据
+  let mimeType = '';
+  let charset = '';
+  let isBase64 = false;
+
+  // 分割元数据部分
+  const metadataSegments = metadataPart.split(';');
+
+  // 第一部分通常是 MIME 类型
+  if (metadataSegments.length > 0) {
+    mimeType = metadataSegments[0] || 'text/plain'; // 默认为 text/plain
+  }
+
+  // 检查其他元数据参数
+  for (let i = 1; i < metadataSegments.length; i++) {
+    const segment = metadataSegments[i];
+
+    if (segment === 'base64') {
+      isBase64 = true;
+    } else if (segment.startsWith('charset=')) {
+      charset = segment.substring(8);
+    }
+  }
+
+  // 返回解析结果
+  return {
+    mimeType,
+    charset,
+    isBase64,
+    data: dataPart,
+  };
+}
+
+export function getFilePathFormat(filePath: string): string {
+  if (!filePath) return '';
+
+  const i = getDataProtocolFormat(filePath);
+
+  if (i) {
+    return i;
+  }
+
+  try {
+    const ext = extname(filePath).slice(1);
+    switch (ext) {
+      case 'js':
+      case 'cjs':
+      case 'mjs':
+      case 'jsx':
+        return 'javascript';
+      case 'ts':
+      case 'tsx':
+        return 'typescript';
+      case 'md':
+        return 'markdown';
+      default:
+        return ext;
+    }
+  } catch (e) {
+    return '';
+  }
+}
+
+/**
+ * 默认编辑器样式, 适合预览代码
+ */
+export const DefaultEditorConfig: editor.IStandaloneEditorConstructionOptions =
+  {
+    // 设置编辑器为只读模式
+    readOnly: true,
+    // 设置 DOM 元素为只读模式
+    domReadOnly: true,
+    // 设置字体大小为 14px
+    fontSize: 14,
+    // 设置制表符宽度为 2 个空格
+    tabSize: 2,
+    // 设置行高为 24px
+    lineHeight: 24,
+    // 关闭验证装饰器的渲染(如错误、警告标记等)
+    renderValidationDecorations: 'off',
+    // 在概览标尺中隐藏光标
+    hideCursorInOverviewRuler: true,
+    // 启用平滑滚动
+    smoothScrolling: true,
+    // 设置文本换行方式
+    wordWrap: 'on',
+    // 启用颜色装饰器(如显示颜色预览)
+    colorDecorators: true,
+    // 禁用代码镜头功能(如显示引用次数等)
+    codeLens: false,
+    // 设置光标宽度为 0(隐藏光标)
+    cursorWidth: 0,
+    // 禁用右侧的小地图预览
+    minimap: {
+      enabled: false,
+    },
+
+    /** 禁用编辑器内置的右键菜单 */
+    contextmenu: false,
+    /** 禁用定义跳转功能 */
+    gotoLocation: {
+      multiple: 'goto',
+      multipleDefinitions: 'goto',
+      multipleTypeDefinitions: 'goto',
+      multipleDeclarations: 'goto',
+      multipleImplementations: 'goto',
+      multipleReferences: 'goto',
+    },
+    /** 禁用悬停提示功能 */
+    hover: { enabled: false },
+    // 禁用链接功能 - 会禁用 Command/Ctrl + 点击
+    links: false,
+  };
+
+/**
+ * get monaco options, it'll combine default config
+ * @param options
+ * @returns target options
+ */
+export function defineMonacoOptions(
+  options?: editor.IStandaloneEditorConstructionOptions,
+): editor.IStandaloneEditorConstructionOptions {
+  return {
+    ...structuredClone(DefaultEditorConfig),
+    ...options,
+  };
+}
+
+export function getFileName(filePath: string): string {
+  // 如果内容为空返回空
+  if (!filePath) {
+    return '';
+  }
+
+  const prefix = 'data:';
+  // 检查是否以 data: 开头
+  if (filePath.startsWith(prefix)) {
+    return 'data';
+  }
+
+  return filePath.split('/').at(-1) || '';
+}
+
+export function editorShowRange(
+  editor: editor.IStandaloneCodeEditor,
+  monaco: Monaco,
+  ranges?: SDK.SourceRange[],
+) {
+  if (!ranges || ranges.length === 0) return;
+
+  const decorations = ranges.map((range) => ({
+    range: getSelectionRange(range, monaco.Range),
+    options: {
+      inlineClassName: 'file-inline-decoration',
+    },
+  }));
+  editor.deltaDecorations([], decorations);
+
+  setTimeout(() => {
+    editor.revealLine(decorations[0].range.startLineNumber);
+  });
+}
diff --git a/packages/components/src/components/base/DiffViewer/index.module.scss b/packages/components/src/components/base/DiffViewer/index.module.scss
new file mode 100644
index 000000000..4fb5b718b
--- /dev/null
+++ b/packages/components/src/components/base/DiffViewer/index.module.scss
@@ -0,0 +1,35 @@
+.diff-viewer {
+  height: 70vh;
+  border-radius: 8px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background-color: var(--vscode-editor-background);
+  box-shadow:
+    0 1px 3px 0 rgb(0 0 0 / 0.1),
+    0 1px 2px -1px rgb(0 0 0 / 0.1);
+
+  &.embed {
+    box-shadow: none;
+    border-radius: 0;
+    overflow: auto;
+  }
+
+  .header {
+    background-color: var(--vscode-textBlockQuote-background);
+    color: var(--vscode-foreground);
+    padding: 8px 16px;
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
+    flex-grow: 0;
+
+    .text {
+      color: var(--vscode-foreground);
+    }
+  }
+
+  .content {
+    flex: 1;
+  }
+}
diff --git a/packages/components/src/components/base/DiffViewer/index.tsx b/packages/components/src/components/base/DiffViewer/index.tsx
new file mode 100644
index 000000000..d7c5c6456
--- /dev/null
+++ b/packages/components/src/components/base/DiffViewer/index.tsx
@@ -0,0 +1,85 @@
+import { DiffEditor, MonacoDiffEditor } from '@monaco-editor/react';
+import { Checkbox } from 'antd';
+import clsx from 'clsx';
+import { useCallback, useMemo, useRef, useState } from 'react';
+import { getFileName, getFilePathFormat } from '../CodeViewer/utils';
+import styles from './index.module.scss';
+import { DiffViewerProps } from './interface';
+import { defineMonacoDiffOptions } from './utils';
+
+export function DiffViewer({
+  className,
+  style,
+  original = '',
+  modified = '',
+  originalFilePath = '',
+  modifiedFilePath = '',
+  originalLang,
+  modifiedLang,
+  isEmbed = false,
+  isLightTheme = true,
+  headerVisible = true,
+}: DiffViewerProps) {
+  const [isSideBySide, setIsSideBySide] = useState(true);
+  const editor = useRef<MonacoDiffEditor>();
+  const originalLanguage = useMemo(
+    () => originalLang || getFilePathFormat(originalFilePath) || 'plaintext',
+    [originalLang, originalFilePath],
+  );
+  const modifiedLanguage = useMemo(
+    () => modifiedLang || getFilePathFormat(modifiedFilePath) || 'plaintext',
+    [modifiedLang, modifiedFilePath],
+  );
+  const options = useMemo(
+    () => defineMonacoDiffOptions({ renderSideBySide: isSideBySide }),
+    [isSideBySide],
+  );
+  const onEditorMount = useCallback((editorInstance: MonacoDiffEditor) => {
+    editor.current = editorInstance;
+  }, []);
+  const theme = isLightTheme ? 'vs-light' : 'vs-dark';
+
+  return (
+    <div
+      className={clsx(
+        'monaco-component',
+        'diff-viewer',
+        styles['diff-viewer'],
+        isEmbed && styles['embed'],
+        className,
+      )}
+      style={style}
+    >
+      {headerVisible && (
+        <div className={styles['header']}>
+          <div>{getFileName(originalFilePath)}</div>
+          <div style={{ flex: 1 }} />
+          <div>
+            <Checkbox
+              className={styles['text']}
+              title="side-by-side"
+              checked={isSideBySide}
+              onChange={(evt) => {
+                setIsSideBySide(evt.target.checked);
+              }}
+            >
+              side-by-side
+            </Checkbox>
+          </div>
+        </div>
+      )}
+      <div className={clsx(styles['content'], 'editor-wrap')}>
+        <DiffEditor
+          theme={theme}
+          originalLanguage={originalLanguage}
+          modifiedLanguage={modifiedLanguage}
+          original={original}
+          modified={modified}
+          width="100%"
+          options={options}
+          onMount={onEditorMount}
+        />
+      </div>
+    </div>
+  );
+}
diff --git a/packages/components/src/components/base/DiffViewer/interface.ts b/packages/components/src/components/base/DiffViewer/interface.ts
new file mode 100644
index 000000000..d3858754a
--- /dev/null
+++ b/packages/components/src/components/base/DiffViewer/interface.ts
@@ -0,0 +1,48 @@
+import { CSSProperties } from 'react';
+
+export interface DiffViewerProps {
+  className?: string;
+  style?: CSSProperties;
+  /**
+   * Original file, displayed on the left
+   */
+  original: string;
+  /**
+   * Modified file, displayed on the right
+   */
+  modified: string;
+  /**
+   * Original file language, highest priority.
+   * If not configured, it will be inferred from filePath
+   */
+  originalLang?: string;
+  /**
+   * Modified file language, highest priority.
+   * If not configured, it will be inferred from filePath
+   */
+  modifiedLang?: string;
+  /**
+   * Original file path
+   */
+  originalFilePath?: string;
+  /**
+   * Modified file path
+   */
+  modifiedFilePath?: string;
+  /**
+   * Whether to display in embedded mode.
+   * Embedded mode has no border or border-radius
+   * @default false
+   */
+  isEmbed?: boolean;
+  /**
+   * Whether to use light theme
+   * @default true
+   */
+  isLightTheme?: boolean;
+  /**
+   * Whether the top bar is displayed
+   * @default true
+   */
+  headerVisible?: boolean;
+}
diff --git a/packages/components/src/components/base/DiffViewer/useDiffDrawer.tsx b/packages/components/src/components/base/DiffViewer/useDiffDrawer.tsx
new file mode 100644
index 000000000..e2e35dccb
--- /dev/null
+++ b/packages/components/src/components/base/DiffViewer/useDiffDrawer.tsx
@@ -0,0 +1,68 @@
+import { Drawer, Empty } from 'antd';
+import { useState } from 'react';
+import { DiffViewer } from '.';
+import { DiffViewerProps } from './interface';
+
+const defaultEmptyReason = 'No diff code';
+
+/**
+ * Use drawer to display code diff content
+ * @param emptyReason Text to display when there is no code content
+ */
+export function useDiffDrawer(emptyReason?: string) {
+  const [original, setOriginal] = useState('');
+  const [modified, setModified] = useState('');
+  const [originalFilePath, setOriginalFilePath] = useState('');
+  const [modifiedFilePath, setModifiedFilePath] = useState('');
+  const [originalLang, setOriginalLang] = useState('');
+  const [modifiedLang, setModifiedLang] = useState('');
+  const [visible, setVisible] = useState(false);
+
+  const codeDiffDrawerComponent = (
+    <Drawer
+      maskClosable
+      zIndex={999}
+      width={'70%'}
+      destroyOnClose
+      title={`Code Diff`}
+      open={visible}
+      onClose={() => setVisible(false)}
+    >
+      {original || modified ? (
+        <DiffViewer
+          original={original}
+          modified={modified}
+          originalLang={originalLang}
+          modifiedLang={modifiedLang}
+          originalFilePath={originalFilePath}
+          modifiedFilePath={modifiedFilePath}
+        />
+      ) : (
+        <Empty description={emptyReason || defaultEmptyReason} />
+      )}
+    </Drawer>
+  );
+
+  return {
+    showCodeDiff(
+      diffConfig: Pick<
+        DiffViewerProps,
+        | 'original'
+        | 'modified'
+        | 'originalFilePath'
+        | 'modifiedFilePath'
+        | 'originalLang'
+        | 'modifiedLang'
+      >,
+    ) {
+      setOriginal(diffConfig.original);
+      setModified(diffConfig.modified);
+      setOriginalLang(diffConfig.originalLang || '');
+      setModifiedLang(diffConfig.modifiedLang || '');
+      setOriginalFilePath(diffConfig.originalFilePath || '');
+      setModifiedFilePath(diffConfig.modifiedFilePath || '');
+      setVisible(true);
+    },
+    codeDiffDrawerComponent,
+  };
+}
diff --git a/packages/components/src/components/base/DiffViewer/utils.ts b/packages/components/src/components/base/DiffViewer/utils.ts
new file mode 100644
index 000000000..330bf395d
--- /dev/null
+++ b/packages/components/src/components/base/DiffViewer/utils.ts
@@ -0,0 +1,58 @@
+import { editor } from 'monaco-editor';
+import { DefaultEditorConfig } from '../CodeViewer/utils';
+const OptimizedDiffEditorConfig: editor.IDiffEditorConstructionOptions = {
+  ...structuredClone(DefaultEditorConfig),
+  readOnly: true,
+  originalEditable: false,
+  // Enable side-by-side view mode, displaying comparison content left and right
+  renderSideBySide: true,
+  // Set the automatic switching breakpoint for side-by-side display to 1,
+  // disabling auto-switching
+  renderSideBySideInlineBreakpoint: 1,
+  renderIndicators: true,
+  definitionLinkOpensInPeek: false,
+  // Set DOM elements to read-only, enhancing security
+  domReadOnly: true,
+  // Disable the code minimap on the right to improve performance
+  minimap: { enabled: false },
+  // Enable word wrap to display long lines of text completely in the view
+  diffWordWrap: 'on',
+
+  // Performance optimization items
+  // Disable code folding to reduce unnecessary calculations
+  folding: false,
+  // Disable line number margin hover effects to improve rendering performance
+  lineNumbersMinChars: 3,
+  // Reduce rendering of external decorations to improve performance
+  renderLineHighlight: 'none',
+  // Disable bracket matching highlighting to reduce computation
+  matchBrackets: 'never',
+
+  // Display effect optimizations
+  // Enable advanced configuration for the difference algorithm to improve accuracy of difference display
+  diffAlgorithm: 'advanced',
+  // Ignore whitespace changes to focus on content differences
+  ignoreTrimWhitespace: true,
+  // Enable connecting lines between different code blocks to improve readability
+  renderOverviewRuler: true,
+  // Highlight the current line to enhance readability
+  renderLineHighlightOnlyWhenFocus: true,
+  // Enable smooth scrolling effect
+  smoothScrolling: true,
+  // Set appropriate letter spacing to improve readability
+  letterSpacing: 0.5,
+};
+
+/**
+ * get monaco options, it'll combine default config
+ * @param options
+ * @returns target options
+ */
+export function defineMonacoDiffOptions(
+  options?: editor.IDiffEditorConstructionOptions,
+): editor.IDiffEditorConstructionOptions {
+  return {
+    ...structuredClone(OptimizedDiffEditorConfig),
+    ...options,
+  };
+}
diff --git a/packages/components/src/pages/BundleSize/components/asset.tsx b/packages/components/src/pages/BundleSize/components/asset.tsx
index 131d0c007..61b2739ca 100644
--- a/packages/components/src/pages/BundleSize/components/asset.tsx
+++ b/packages/components/src/pages/BundleSize/components/asset.tsx
@@ -4,7 +4,6 @@ import {
   DeploymentUnitOutlined,
   InfoCircleOutlined,
 } from '@ant-design/icons';
-import Editor from '@monaco-editor/react';
 import { SDK } from '@rsdoctor/types';
 import {
   Button,
@@ -17,30 +16,30 @@ import {
   Space,
   Tag,
   Tooltip,
+  Tree,
   Typography,
 } from 'antd';
-import { omitBy, sumBy } from 'lodash-es';
 import { DataNode as AntdDataNode } from 'antd/es/tree';
+import { omitBy, sumBy } from 'lodash-es';
 import { dirname, relative } from 'path';
 import React, { useEffect, useMemo, useState } from 'react';
-import { ServerAPIProvider } from '../../../components/Manifest';
-import { ModuleAnalyzeComponent } from '../../ModuleAnalyze';
+import { CodeViewer } from 'src/components/base/CodeViewer';
 import { Badge as Bdg } from '../../../components/Badge';
 import { KeywordInput } from '../../../components/Form/keyword';
 import { Keyword } from '../../../components/Keyword';
+import { ServerAPIProvider } from '../../../components/Manifest';
 import { TextDrawer } from '../../../components/TextDrawer';
 import { Size } from '../../../constants';
 import {
   DataNode,
   createFileStructures,
   formatSize,
-  getOriginalLanguage,
   isJsDataUrl,
   useI18n,
 } from '../../../utils';
+import { ModuleAnalyzeComponent } from '../../ModuleAnalyze';
 import { ModuleGraphListContext } from '../config';
 import styles from './index.module.scss';
-import { Tree } from 'antd';
 
 const { DirectoryTree } = Tree;
 
@@ -58,7 +57,7 @@ const tagStyle = {
 const EmptyCodeItem = () => (
   <Empty
     description={`Do not have the module code.
-  (1) If you use the brief mode, there will not have any codes to show. 
+  (1) If you use the brief mode, there will not have any codes to show.
   (2) If you use lite mode, there will not have source codes.`}
   />
 );
@@ -172,30 +171,21 @@ export const ModuleCodeViewer: React.FC<{ data: SDK.ModuleData }> = ({
                       <a href="#">Explain</a>
                     </Popover>
                   }
+                  styles={{ body: { padding: 0, overflow: 'hidden' } }}
                 >
                   {source['parsedSource'] ||
                   source['source'] ||
                   source['transformed'] ? (
-                    <Editor
-                      theme="vs-dark"
-                      language={getOriginalLanguage(path)}
-                      height={window.innerHeight / 1.5}
-                      value={
+                    <CodeViewer
+                      isEmbed
+                      code={
                         tab
                           ? source[tab as keyof SDK.ModuleSource]
                           : source['parsedSource']
                             ? source['parsedSource']
                             : source['source']
                       }
-                      options={{
-                        readOnly: true,
-                        domReadOnly: true,
-                        fontSize: 14,
-                        wordWrap: 'bounded',
-                        minimap: {
-                          enabled: false,
-                        },
-                      }}
+                      filePath={path}
                     />
                   ) : (
                     <EmptyCodeItem />
diff --git a/packages/components/src/pages/BundleSize/components/index.tsx b/packages/components/src/pages/BundleSize/components/index.tsx
index f3c134b97..fcc1b7b9d 100644
--- a/packages/components/src/pages/BundleSize/components/index.tsx
+++ b/packages/components/src/pages/BundleSize/components/index.tsx
@@ -1,4 +1,5 @@
 import {
+  CodeOutlined,
   CodepenCircleOutlined,
   DeploymentUnitOutlined,
   InfoCircleOutlined,
@@ -20,21 +21,20 @@ import {
 } from 'antd';
 import { debounce, includes, sumBy } from 'lodash-es';
 import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { SearchModal } from './search-modal';
-import { ServerAPIProvider, withServerAPI } from '../../../components/Manifest';
+import { useCodeDrawer } from 'src/components/base/CodeViewer/useCodeDrawer';
 import { Badge as Bdg } from '../../../components/Badge';
 import { FileTree } from '../../../components/FileTree';
 import { KeywordInput } from '../../../components/Form/keyword';
 import { Keyword } from '../../../components/Keyword';
+import { ServerAPIProvider, withServerAPI } from '../../../components/Manifest';
 import { Size } from '../../../constants';
 import { createFileStructures, formatSize, useI18n } from '../../../utils';
-import { BundleCards } from './cards';
-import { CodeViewerWithDrawer } from '../../../components/CodeViewer';
-
+import { GraphType } from '../constants';
 import { AssetDetail } from './asset';
-import './index.sass';
+import { BundleCards } from './cards';
 import styles from './index.module.scss';
-import { GraphType } from '../constants';
+import './index.sass';
+import { SearchModal } from './search-modal';
 
 const { Option } = Select;
 
@@ -174,6 +174,9 @@ export const WebpackModulesOverallBase: React.FC<
   const [inputChunkUnit, setChunkUnit] = useState('');
   const [assetPath, setAssetPath] = useState<string | null>(null);
   const [graphType, setGraphType] = useState('tree' as GraphType);
+  const { showCode, codeDrawerComponent } = useCodeDrawer(
+    'Do not have the codes of assets. If you use the lite or brief mode, there will have codes.',
+  );
 
   const { t } = useI18n();
 
@@ -282,11 +285,9 @@ export const WebpackModulesOverallBase: React.FC<
                   initial
                 </Typography.Text>
               ) : null}
-              <CodeViewerWithDrawer
-                path={path}
-                content={content!}
-                editorConfig={{ readOnly: false, domReadOnly: false }}
-                emptyReason="Do not have the codes of assets. If you use the lite or brief mode, there will have codes."
+              <CodeOutlined
+                style={{ fontSize: 14, padding: 0 }}
+                onClick={() => showCode({ code: content!, filePath: path })}
               />
             </Space>
           </div>
@@ -554,6 +555,7 @@ export const WebpackModulesOverallBase: React.FC<
           </Space>
         </Card>
       </div>
+      {codeDrawerComponent}
     </>
   );
 };
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4199b857d..4db499da9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -610,6 +610,9 @@ importers:
       axios:
         specifier: ^1.8.4
         version: 1.8.4
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
       dayjs:
         specifier: 1.11.13
         version: 1.11.13
@@ -4942,6 +4945,10 @@ packages:
     resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
     engines: {node: '>=0.8'}
 
+  clsx@2.1.1:
+    resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+    engines: {node: '>=6'}
+
   collapse-white-space@2.1.0:
     resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
 
@@ -15347,6 +15354,8 @@ snapshots:
 
   clone@1.0.4: {}
 
+  clsx@2.1.1: {}
+
   collapse-white-space@2.1.0: {}
 
   color-convert@1.9.3:


Reviews:
user: login: yifancong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T03:41:22Z
user: login: quanquan2100
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T03:43:29Z
user: login: yifancong
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T04:05:55Z

Review comments:
diff_hunk: @@ -0,0 +1,62 @@
+import { editor } from 'monaco-editor';
+import { DefaultEditorConfig } from '../CodeViewer/utils';
+
+const OptimizedDiffEditorConfig: editor.IDiffEditorConstructionOptions = {
+  ...structuredClone(DefaultEditorConfig),
+  /** 设置编辑器为只读模式，禁止用户修改内容 */
path: packages/components/src/components/base/DiffViewer/utils.ts
user: login: yifancong
body: Maybe is better to use English annotations.
created_at: 2025-04-23T03:40:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+import { Drawer, Empty } from 'antd';
+import { useState } from 'react';
+import { DiffViewer } from '.';
+import { DiffViewerProps } from './interface';
+
+const defaultEmptyReason = 'No diff code';
+
+/**
path: packages/components/src/components/base/DiffViewer/useDiffDrawer.tsx
user: login: yifancong
body: Maybe is better to use English annotations.
created_at: 2025-04-23T03:41:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+import { CSSProperties } from 'react';
+
+export interface DiffViewerProps {
+  className?: string;
+  style?: CSSProperties;
+  /**
path: packages/components/src/components/base/DiffViewer/interface.ts
user: login: yifancong
body: Maybe is better to use English annotations.
created_at: 2025-04-23T03:41:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+import { Drawer, Empty } from 'antd';
+import { useState } from 'react';
+import { DiffViewer } from '.';
+import { DiffViewerProps } from './interface';
+
+const defaultEmptyReason = 'No diff code';
+
+/**
path: packages/components/src/components/base/DiffViewer/useDiffDrawer.tsx
user: login: quanquan2100
body: ok
created_at: 2025-04-23T03:43:29Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-04-20T09:37:43Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *rsdoctor* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | d9d4bb6c35558f089408b182119e8a3f3c7570bb |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/rsdoctor/deploys/68086624230b4500083ec488 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1028--rsdoctor.netlify.app](https://deploy-preview-1028--rsdoctor.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEwMjgtLXJzZG9jdG9yLm5ldGxpZnkuYXBwIn0.k64Eila4xnfRleNtG8C-wEhCrSfbOaVpziGwAoxnrTg)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [rsdoctor Preview](https://deploy-preview-1028--rsdoctor.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/rsdoctor/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: quanquan2100
created_at: 2025-04-20T13:16:52Z
body: ![CleanShot 2025-04-20 at 21 12 41@2x](https://github.com/user-attachments/assets/5c09ecf9-2984-480d-84f3-7e430361023b)

This test case is unrelated to the content of this feature.  And it‘s a random case that I run it at my computer.
author_association: CONTRIBUTOR
user: login: yifancong
created_at: 2025-04-21T02:48:51Z
body: > ![CleanShot 2025-04-20 at 21 12 41@2x](https://private-user-images.githubusercontent.com/9942452/435468178-5c09ecf9-2984-480d-84f3-7e430361023b.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDUyMDM2NzEsIm5iZiI6MTc0NTIwMzM3MSwicGF0aCI6Ii85OTQyNDUyLzQzNTQ2ODE3OC01YzA5ZWNmOS0yOTg0LTQ4MGQtODRmMy03ZTQzMDM2MTAyM2IucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDQyMSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA0MjFUMDI0MjUxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZGFmNjcyNDlhMjA0YzFkOGFlZWUzYTQ4YmY4YWY4Zjk4ZDJhZTZjYTQ4OWVkZjRkNjUxM2U3NzQ5YzI5MzQ0NyZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.ogbGQZSxAszkKJlaLPruHDzrV7O0oTobGRT25z97Gbw)
> 
> This test case is unrelated to the content of this feature. And it‘s a random case that I run it at my computer.

yes, Let me take a look at this test question.
author_association: CONTRIBUTOR
user: login: yifancong
created_at: 2025-04-22T08:58:39Z
body: Thanks for your contributions. And I see that you have been submitting the code. If you want to merge this mr, you can request me.
author_association: CONTRIBUTOR
user: login: quanquan2100
created_at: 2025-04-22T09:46:56Z
body: > Thanks for your contributions. And I see that you have been submitting the code. If you want to merge this mr, you can request me.

Yes, I want to merge this first. And then I'll continue to change other. @easy1090 
author_association: CONTRIBUTOR