refactor: replace react-diff-view with @lobehub/ui CodeDiff (#12077)

* refactor: replace react-diff-view with @lobehub/ui CodeDiff

- Replace react-diff-view with @lobehub/ui's CodeDiff and PatchDiff components
- Use CodeDiff in Intervention for old/new content comparison
- Use PatchDiff in Render for unified diff patch display
- Remove react-diff-view dark theme CSS file
- Remove diff generation complexity, rely on built-in component styling
- Reduce code by ~93 lines

* 🔧 chore: add font-family style to previewText
This commit is contained in:
Innei
2026-02-03 19:19:41 +08:00
committed by arvinxx
parent 90c88da19d
commit 9ecca13388
5 changed files with 32 additions and 124 deletions

View File

@@ -322,7 +322,6 @@
"rc-util": "^5.44.4",
"react": "^19.2.3",
"react-confetti": "^6.4.0",
"react-diff-view": "^3.3.2",
"react-dom": "^19.2.3",
"react-fast-marquee": "^1.6.5",
"react-hotkeys-hook": "^5.2.3",

View File

@@ -1,19 +1,14 @@
import { type EditLocalFileParams } from '@lobechat/electron-client-ipc';
import { type BuiltinInterventionProps } from '@lobechat/types';
import { Flexbox, Icon, Skeleton, Text } from '@lobehub/ui';
import { createPatch } from 'diff';
import { CodeDiff, Flexbox, Icon, Skeleton, Text } from '@lobehub/ui';
import { ChevronRight } from 'lucide-react';
import { useTheme } from 'next-themes';
import path from 'path-browserify-esm';
import React, { memo, useMemo } from 'react';
import { Diff, Hunk, parseDiff } from 'react-diff-view';
import 'react-diff-view/style/index.css';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import { LocalFile, LocalFolder } from '@/features/LocalFile';
import { localFileService } from '@/services/electron/localFileService';
import '@/styles/react-diff-view.dark.css';
const EditLocalFile = memo<BuiltinInterventionProps<EditLocalFileParams>>(({ args }) => {
const { t } = useTranslation('tool');
@@ -29,32 +24,17 @@ const EditLocalFile = memo<BuiltinInterventionProps<EditLocalFileParams>>(({ arg
},
);
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === 'dark';
// Generate diff from full file content
const files = useMemo(() => {
if (!fileData?.content) return [];
// Generate new content by applying the replacement
const { oldContent, newContent } = useMemo(() => {
if (!fileData?.content) return { newContent: '', oldContent: '' };
try {
const oldContent = fileData.content;
const oldContent = fileData.content;
const newContent = args.replace_all
? oldContent.replaceAll(args.old_string, args.new_string)
: oldContent.replace(args.old_string, args.new_string);
// Generate new content by applying the replacement
const newContent = args.replace_all
? oldContent.replaceAll(args.old_string, args.new_string)
: oldContent.replace(args.old_string, args.new_string);
// Use createPatch to generate unified diff with full file content
const patch = createPatch(args.file_path, oldContent, newContent, '', '');
// Add git diff header for parseDiff compatibility
const diffText = `diff --git a${args.file_path} b${args.file_path}\n${patch}`;
return parseDiff(diffText);
} catch (error) {
console.error('Failed to generate diff:', error);
return [];
}
}, [fileData?.content, args.file_path, args.old_string, args.new_string, args.replace_all]);
return { newContent, oldContent };
}, [fileData?.content, args.old_string, args.new_string, args.replace_all]);
return (
<Flexbox gap={12}>
@@ -73,18 +53,16 @@ const EditLocalFile = memo<BuiltinInterventionProps<EditLocalFileParams>>(({ arg
? t('localFiles.editFile.replaceAll')
: t('localFiles.editFile.replaceFirst')}
</Text>
{files.map((file, index) => (
<div key={`${file.oldPath}-${index}`} style={{ fontSize: '12px' }}>
<Diff
data-theme={isDarkMode ? 'dark' : 'light'}
diffType={file.type}
hunks={file.hunks}
viewType="split"
>
{(hunks) => hunks.map((hunk) => <Hunk hunk={hunk} key={hunk.content} />)}
</Diff>
</div>
))}
{oldContent && (
<CodeDiff
fileName={args.file_path}
newContent={newContent}
oldContent={oldContent}
showHeader={false}
variant="borderless"
viewMode="split"
/>
)}
</Flexbox>
)}
</Flexbox>

View File

@@ -1,31 +1,11 @@
import { type EditLocalFileState } from '@lobechat/builtin-tool-local-system';
import { type EditLocalFileParams } from '@lobechat/electron-client-ipc';
import { type BuiltinRenderProps } from '@lobechat/types';
import { Alert, Flexbox, Skeleton } from '@lobehub/ui';
import { useTheme } from 'next-themes';
import React, { memo, useMemo } from 'react';
import { Diff, Hunk, parseDiff } from 'react-diff-view';
import 'react-diff-view/style/index.css';
import '@/styles/react-diff-view.dark.css';
import { Alert, Flexbox, PatchDiff, Skeleton } from '@lobehub/ui';
import React, { memo } from 'react';
const EditLocalFile = memo<BuiltinRenderProps<EditLocalFileParams, EditLocalFileState>>(
({ args, pluginState, pluginError }) => {
// Parse diff for react-diff-view
const files = useMemo(() => {
const diffText = pluginState?.diffText;
if (!diffText) return [];
try {
return parseDiff(diffText);
} catch (error) {
console.error('Failed to parse diff:', error);
return [];
}
}, [pluginState?.diffText]);
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === 'dark';
if (!args) return <Skeleton active />;
return (
@@ -37,22 +17,15 @@ const EditLocalFile = memo<BuiltinRenderProps<EditLocalFileParams, EditLocalFile
title="Edit Failed"
type="error"
/>
) : (
<Flexbox data-theme={isDarkMode ? 'dark' : 'light'} gap={12}>
{files.map((file, index) => (
<div key={`${file.oldPath}-${index}`} style={{ fontSize: '12px' }}>
<Diff
diffType={file.type}
gutterType="default"
hunks={file.hunks}
viewType="unified"
>
{(hunks) => hunks.map((hunk) => <Hunk hunk={hunk} key={hunk.content} />)}
</Diff>
</div>
))}
</Flexbox>
)}
) : pluginState?.diffText ? (
<PatchDiff
fileName={args.file_path}
patch={pluginState.diffText}
showHeader={false}
variant="borderless"
viewMode="unified"
/>
) : null}
</Flexbox>
);
},

View File

@@ -80,6 +80,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
previewText: css`
overflow: auto;
font-family: ${cssVar.fontFamilyCode};
font-size: 12px;
line-height: 1.6;
word-break: break-all;

View File

@@ -1,43 +0,0 @@
/* react-diff-view doesn't ship a dark theme.
* We override its CSS variables when the app is in dark mode.
*
* NOTE:
* - ThemeProvider (antd-style / @lobehub/ui) sets a `data-theme="dark"` attribute at runtime.
* - Keep this file purely as variable overrides to minimize drift with upstream styles.
*/
[data-theme='dark'] .diff {
/* base */
--diff-background-color: transparent;
--diff-text-color: #c9d1d9;
--diff-font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
/* selection */
--diff-selection-background-color: rgba(56, 139, 253, 0.35);
--diff-selection-text-color: var(--diff-text-color);
/* gutter */
--diff-gutter-insert-background-color: rgba(46, 160, 67, 0.22);
--diff-gutter-insert-text-color: var(--diff-text-color);
--diff-gutter-delete-background-color: rgba(248, 81, 73, 0.22);
--diff-gutter-delete-text-color: var(--diff-text-color);
--diff-gutter-selected-background-color: rgba(187, 128, 9, 0.28);
--diff-gutter-selected-text-color: var(--diff-text-color);
/* code */
--diff-code-insert-background-color: rgba(46, 160, 67, 0.14);
--diff-code-insert-text-color: var(--diff-text-color);
--diff-code-delete-background-color: rgba(248, 81, 73, 0.14);
--diff-code-delete-text-color: var(--diff-text-color);
--diff-code-insert-edit-background-color: rgba(46, 160, 67, 0.28);
--diff-code-insert-edit-text-color: var(--diff-text-color);
--diff-code-delete-edit-background-color: rgba(248, 81, 73, 0.28);
--diff-code-delete-edit-text-color: var(--diff-text-color);
--diff-code-selected-background-color: rgba(187, 128, 9, 0.28);
--diff-code-selected-text-color: var(--diff-text-color);
/* omit marker */
--diff-omit-gutter-line-color: rgba(248, 81, 73, 0.7);
}