Skip to content

Commit 76d050d

Browse files
authored
Merge pull request #6 from kaishuu0123/feat/unified-diff-viewer
Implement Unified Diff viewer dialog and generator logic
2 parents 0e0d920 + c43799b commit 76d050d

10 files changed

Lines changed: 577 additions & 24 deletions

File tree

example_texts/freebsd_sbin_route/left_82641e1.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ inet_makenetandmask(u_long net, struct sockaddr_in *sin, u_long bits)
837837
j <<= 8;
838838
}
839839
/* i holds the first non zero bit */
840-
bits = 32 - (i*8);
840+
bits = 32 - (i*8);
841841
}
842842
if (bits != 0)
843843
mask = 0xffffffff << (32 - bits);
@@ -1089,7 +1089,7 @@ prefixlen(const char *str)
10891089
int max;
10901090
char *p;
10911091

1092-
rtm_addrs |= RTA_NETMASK;
1092+
rtm_addrs |= RTA_NETMASK;
10931093
switch (af) {
10941094
#ifdef INET6
10951095
case AF_INET6:
@@ -1110,7 +1110,7 @@ prefixlen(const char *str)
11101110
fprintf(stderr, "%s: bad value\n", str);
11111111
exit(1);
11121112
}
1113-
1113+
11141114
q = len >> 3;
11151115
r = len & 7;
11161116
so_mask.sa.sa_family = af;
@@ -1689,4 +1689,4 @@ atalk_ntoa(struct at_addr at)
16891689

16901690
(void) snprintf(buf, sizeof(buf), "%u.%u", ntohs(at.s_net), at.s_node);
16911691
return(buf);
1692-
}
1692+
}

example_texts/freebsd_sbin_route/right_158f319.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2022,4 +2022,4 @@ sockaddr(char *addr, struct sockaddr *sa, size_t size)
20222022
break;
20232023
} while (cp < cplim);
20242024
sa->sa_len = cp - (char *)sa;
2025-
}
2025+
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@electron-toolkit/preload": "^3.0.2",
2727
"@electron-toolkit/utils": "^4.0.0",
2828
"@monaco-editor/react": "^4.7.0",
29+
"@radix-ui/react-dialog": "^1.1.15",
2930
"@radix-ui/react-icons": "^1.3.1",
3031
"@radix-ui/react-select": "^2.1.2",
3132
"@radix-ui/react-separator": "^1.1.0",
@@ -35,6 +36,8 @@
3536
"clsx": "^2.1.1",
3637
"electron-store": "^10.0.0",
3738
"electron-updater": "^6.1.7",
39+
"highlight.js": "^11.11.1",
40+
"lucide-react": "^0.563.0",
3841
"monaco-editor": "^0.52.0",
3942
"tailwind-merge": "^2.5.4",
4043
"tailwindcss-animate": "^1.0.7"

src/renderer/src/App.tsx

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ import {
1010
} from './components/ui/select'
1111
import { Button } from './components/ui/button'
1212
import { Separator } from './components/ui/separator'
13+
import { UnifiedDiffDialog } from './components/UnifiedDiffDialog'
1314
import { cn } from './lib/utils'
1415

1516
type ThemeNameType = 'light' | 'dark'
1617

1718
type ThemeColorSetType = {
1819
name: ThemeNameType
19-
textColor: string
20-
backgroundColor: string
2120
monacoThemeName: 'vs' | 'vs-dark'
2221
}
2322

@@ -28,15 +27,11 @@ const getThemeColorByThemeName = (themeName: ThemeNameType): ThemeColorSetType =
2827
case 'light':
2928
return {
3029
name: 'light',
31-
textColor: '#000000',
32-
backgroundColor: '#f9fafa',
3330
monacoThemeName: 'vs'
3431
}
3532
case 'dark':
3633
return {
3734
name: 'dark',
38-
textColor: '#d4d4d4',
39-
backgroundColor: '#1f1f1f',
4035
monacoThemeName: 'vs-dark'
4136
}
4237
}
@@ -51,9 +46,11 @@ function App(): JSX.Element {
5146
const [selectedTheme, setSelectedTheme] = useState<ThemeColorSetType>(
5247
getThemeColorByThemeName('light')
5348
)
49+
const [unifiedDiffDialogOpen, setUnifiedDiffDialogOpen] = useState(false)
5450

5551
const options: monaco.editor.IDiffEditorOptions = {
5652
fontSize: 14,
53+
fontLigatures: false,
5754
renderSideBySide: true,
5855
originalEditable: true,
5956
automaticLayout: true,
@@ -132,7 +129,14 @@ function App(): JSX.Element {
132129
</Select>
133130
</div>
134131

135-
<div className="flex space-x-2">
132+
<div className="flex space-x-2 items-center">
133+
<Button className="h-8 rounded-sm" onClick={() => setUnifiedDiffDialogOpen(true)}>
134+
<span className="flex items-center space-x-2">
135+
<i className="codicon codicon-file-code"></i>
136+
<span>Show Unified Diff</span>
137+
</span>
138+
</Button>
139+
<Separator orientation="vertical" className="h-8" />
136140
<Button
137141
size="icon"
138142
className="w-8 h-8 rounded-sm"
@@ -164,25 +168,30 @@ function App(): JSX.Element {
164168
/>
165169
</div>
166170

167-
<div id="footer" className="flex items-center justify-end space-x-3">
168-
<div>
169-
<span className="text-xs text-muted-foreground">Text Diff View</span>
170-
</div>
171-
<Separator orientation="vertical" />
171+
<div id="footer" className="flex justify-end space-x-3 items-center">
172172
<div>
173173
<a
174-
href="https://github.com/kaishuu0123/text-diff-view"
175-
className="text-xs text-muted-foreground align-middle"
174+
href="https://github.com"
175+
className="text-xs text-muted-foreground"
176+
target="_blank"
177+
rel="noreferrer"
176178
>
177-
<span className="codicon codicon-github"></span> kaishuu0123/text-diff-view
179+
Text Diff View
178180
</a>
179181
</div>
180-
<Separator orientation="vertical" />
182+
<Separator orientation="vertical" className="h-full" />
181183
<div>
182184
<span className="text-xs text-muted-foreground">Powered by Monaco Editor.</span>
183185
</div>
184186
</div>
185187
</div>
188+
189+
<UnifiedDiffDialog
190+
open={unifiedDiffDialogOpen}
191+
onOpenChange={setUnifiedDiffDialogOpen}
192+
diffEditor={currentEditor.current}
193+
themeName={selectedTheme.name}
194+
/>
186195
</div>
187196
)
188197
}

src/renderer/src/assets/main.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,4 @@
7979
@apply bg-background text-foreground;
8080
font-feature-settings: "rlig" 1, "calt" 1;
8181
}
82-
}
82+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { useState, useEffect, useMemo } from 'react'
2+
import * as monaco from 'monaco-editor'
3+
import hljs from 'highlight.js/lib/core'
4+
import diff from 'highlight.js/lib/languages/diff'
5+
import 'highlight.js/styles/atom-one-dark.css'
6+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'
7+
import { Button } from './ui/button'
8+
import { generateUnifiedDiff } from '@renderer/lib/unifiedDiffGenerator'
9+
import { cn } from '../lib/utils'
10+
11+
hljs.registerLanguage('diff', diff)
12+
13+
interface UnifiedDiffDialogProps {
14+
open: boolean
15+
onOpenChange: (open: boolean) => void
16+
diffEditor: monaco.editor.IStandaloneDiffEditor | null
17+
themeName: 'light' | 'dark'
18+
}
19+
20+
export function UnifiedDiffDialog({
21+
open,
22+
onOpenChange,
23+
diffEditor,
24+
themeName
25+
}: UnifiedDiffDialogProps): JSX.Element {
26+
const [copied, setCopied] = useState(false)
27+
const [unifiedDiffContent, setUnifiedDiffContent] = useState('')
28+
const [hasError, setHasError] = useState(false)
29+
30+
useEffect(() => {
31+
if (open && diffEditor) {
32+
try {
33+
const unifiedDiff = generateUnifiedDiff(diffEditor, 'before')
34+
35+
const isError = unifiedDiff.startsWith('[ERROR]') || unifiedDiff.startsWith('[INFO]')
36+
37+
setUnifiedDiffContent(unifiedDiff)
38+
setHasError(isError)
39+
console.log('Unified diff generated successfully') // ログメッセージを変更
40+
} catch (error) {
41+
console.error('Error in UnifiedDiffDialog:', error)
42+
const errorMsg = `[ERROR] ${error instanceof Error ? error.message : String(error)}`
43+
setUnifiedDiffContent(errorMsg)
44+
setHasError(true)
45+
}
46+
}
47+
}, [open, diffEditor])
48+
49+
// highlight.jsでdiffをハイライト
50+
const highlightedHtml = useMemo(() => {
51+
if (!unifiedDiffContent || hasError) return unifiedDiffContent
52+
try {
53+
return hljs.highlight(unifiedDiffContent, { language: 'diff' }).value
54+
} catch (error) {
55+
console.error('Highlight error:', error)
56+
return unifiedDiffContent
57+
}
58+
}, [unifiedDiffContent, hasError])
59+
60+
const handleOpenChange = (isOpen: boolean): void => {
61+
onOpenChange(isOpen)
62+
if (!isOpen) {
63+
setCopied(false)
64+
setHasError(false)
65+
}
66+
}
67+
68+
const copyToClipboard = async (): Promise<void> => {
69+
try {
70+
await navigator.clipboard.writeText(unifiedDiffContent)
71+
setCopied(true)
72+
setTimeout(() => setCopied(false), 2000)
73+
} catch (err) {
74+
console.error('Failed to copy:', err)
75+
}
76+
}
77+
78+
return (
79+
<Dialog open={open} onOpenChange={handleOpenChange}>
80+
{/* ダイアログの高さをコンテンツ依存にする */}
81+
<DialogContent
82+
className={cn(
83+
'max-w-[calc(100vw-4rem)] w-full max-h-[90vh] flex flex-col p-6',
84+
themeName === 'dark' && 'dark'
85+
)}
86+
>
87+
<DialogHeader className="pb-4">
88+
<DialogTitle className="text-foreground">Unified Diff</DialogTitle>
89+
</DialogHeader>
90+
91+
<div className="relative border rounded-md max-h-[70vh] min-h-0 overflow-y-auto">
92+
{!hasError && (
93+
<Button
94+
size="sm"
95+
onClick={copyToClipboard}
96+
disabled={!unifiedDiffContent || hasError}
97+
className="absolute top-3 right-3 z-50"
98+
>
99+
{copied ? (
100+
<>
101+
<i className="codicon codicon-check mr-2"></i>
102+
Copied!
103+
</>
104+
) : (
105+
<>
106+
<i className="codicon codicon-copy mr-2"></i>
107+
Copy
108+
</>
109+
)}
110+
</Button>
111+
)}
112+
113+
<pre className="text-xs font-mono">
114+
<code className="hljs" dangerouslySetInnerHTML={{ __html: highlightedHtml }} />
115+
</pre>
116+
</div>
117+
</DialogContent>
118+
</Dialog>
119+
)
120+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as React from 'react'
2+
import * as DialogPrimitive from '@radix-ui/react-dialog'
3+
import { X } from 'lucide-react'
4+
5+
import { cn } from '@renderer/lib/utils'
6+
7+
const Dialog = DialogPrimitive.Root
8+
9+
const DialogTrigger = DialogPrimitive.Trigger
10+
11+
const DialogPortal = DialogPrimitive.Portal
12+
13+
const DialogClose = DialogPrimitive.Close
14+
15+
const DialogOverlay = React.forwardRef<
16+
React.ElementRef<typeof DialogPrimitive.Overlay>,
17+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
18+
>(({ className, ...props }, ref) => (
19+
<DialogPrimitive.Overlay
20+
ref={ref}
21+
className={cn(
22+
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
23+
className
24+
)}
25+
{...props}
26+
/>
27+
))
28+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29+
30+
const DialogContent = React.forwardRef<
31+
React.ElementRef<typeof DialogPrimitive.Content>,
32+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
33+
>(({ className, children, ...props }, ref) => (
34+
<DialogPortal>
35+
<DialogOverlay />
36+
<DialogPrimitive.Content
37+
ref={ref}
38+
className={cn(
39+
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-4xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg',
40+
className
41+
)}
42+
{...props}
43+
>
44+
{children}
45+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground text-muted-foreground">
46+
<X className="h-4 w-4" />
47+
<span className="sr-only">Close</span>
48+
</DialogPrimitive.Close>
49+
</DialogPrimitive.Content>
50+
</DialogPortal>
51+
))
52+
DialogContent.displayName = DialogPrimitive.Content.displayName
53+
54+
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
55+
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
56+
)
57+
DialogHeader.displayName = 'DialogHeader'
58+
59+
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
60+
<div
61+
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
62+
{...props}
63+
/>
64+
)
65+
DialogFooter.displayName = 'DialogFooter'
66+
67+
const DialogTitle = React.forwardRef<
68+
React.ElementRef<typeof DialogPrimitive.Title>,
69+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
70+
>(({ className, ...props }, ref) => (
71+
<DialogPrimitive.Title
72+
ref={ref}
73+
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
74+
{...props}
75+
/>
76+
))
77+
DialogTitle.displayName = DialogPrimitive.Title.displayName
78+
79+
const DialogDescription = React.forwardRef<
80+
React.ElementRef<typeof DialogPrimitive.Description>,
81+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
82+
>(({ className, ...props }, ref) => (
83+
<DialogPrimitive.Description
84+
ref={ref}
85+
className={cn('text-sm text-muted-foreground', className)}
86+
{...props}
87+
/>
88+
))
89+
DialogDescription.displayName = DialogPrimitive.Description.displayName
90+
91+
export {
92+
Dialog,
93+
DialogPortal,
94+
DialogOverlay,
95+
DialogClose,
96+
DialogTrigger,
97+
DialogContent,
98+
DialogHeader,
99+
DialogFooter,
100+
DialogTitle,
101+
DialogDescription
102+
}

0 commit comments

Comments
 (0)