@@ -4,6 +4,7 @@ import { useToolState } from '@/components/providers/ToolStateProvider';
44import { Button } from '@/components/ui/button' ;
55import { CodePanel } from '@/components/ui/code-panel' ;
66import { DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuTrigger } from '@/components/ui/dropdown-menu' ;
7+ import { LoadFileButton } from '@/components/ui/load-file-button' ;
78import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from '@/components/ui/select' ;
89import { Switch } from '@/components/ui/switch' ;
910import { DEFAULT_DIFF_OPTIONS , DIFF_CHECKER_OPTIONS , DIFF_EXAMPLES } from '@/config/diff-checker-config' ;
@@ -38,6 +39,18 @@ interface InlineDecoration {
3839 type : 'added' | 'removed' ;
3940}
4041
42+ const EXTENSION_LANGUAGE_MAP : Record < string , string > = {
43+ js : 'javascript' , jsx : 'javascript' , mjs : 'javascript' , cjs : 'javascript' ,
44+ ts : 'typescript' , tsx : 'typescript' ,
45+ json : 'json' , html : 'html' , htm : 'html' ,
46+ css : 'css' , scss : 'css' , less : 'css' ,
47+ py : 'python' , java : 'java' , cs : 'csharp' ,
48+ cpp : 'cpp' , cc : 'cpp' , go : 'go' , rs : 'rust' ,
49+ sql : 'sql' , xml : 'xml' , svg : 'xml' ,
50+ yaml : 'yaml' , yml : 'yaml' , md : 'markdown' ,
51+ sh : 'shell' , bash : 'shell' , zsh : 'shell' ,
52+ } ;
53+
4154export function DiffChecker ( { className, instanceId } : DiffCheckerProps ) {
4255 const { toolState, updateToolState } = useToolState ( 'diff-checker' , instanceId ) ;
4356
@@ -57,6 +70,10 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
5770 const originalDecorationsRef = useRef < string [ ] > ( [ ] ) ;
5871 const modifiedDecorationsRef = useRef < string [ ] > ( [ ] ) ;
5972
73+ // View zone IDs for alignment padding
74+ const originalViewZoneIdsRef = useRef < string [ ] > ( [ ] ) ;
75+ const modifiedViewZoneIdsRef = useRef < string [ ] > ( [ ] ) ;
76+
6077 // Scroll sync refs
6178 const isSyncingScrollRef = useRef < boolean > ( false ) ;
6279 const originalScrollDisposableRef = useRef < any > ( null ) ;
@@ -98,6 +115,11 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
98115 }
99116 } , [ toolState , isHydrated ] ) ;
100117
118+ interface ViewZoneData {
119+ afterLineNumber : number ;
120+ heightInLines : number ;
121+ }
122+
101123 // Calculate diff and apply decorations with character-level highlighting
102124 const calculateDiffAndDecorate = useCallback ( ( ) => {
103125 const lineChanges : Change [ ] = diffLines ( originalText , modifiedText , {
@@ -110,6 +132,8 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
110132 const modifiedLineDecorations : any [ ] = [ ] ;
111133 const originalInlineDecorations : InlineDecoration [ ] = [ ] ;
112134 const modifiedInlineDecorations : InlineDecoration [ ] = [ ] ;
135+ const originalViewZones : ViewZoneData [ ] = [ ] ;
136+ const modifiedViewZones : ViewZoneData [ ] = [ ] ;
113137
114138 let originalLine = 1 ;
115139 let modifiedLine = 1 ;
@@ -120,11 +144,7 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
120144 const lineCount = change . count || 0 ;
121145
122146 if ( change . added ) {
123- // Check if previous was removed (paired change for inline diff)
124- const prevChange = i > 0 ? lineChanges [ i - 1 ] : null ;
125- if ( prevChange && prevChange . removed ) {
126- // Already handled in removed case
127- }
147+ // Standalone addition (not preceded by a removal that already handled it)
128148 additions += lineCount ;
129149 const lines = ( change . value || '' ) . split ( '\n' ) . filter ( ( _ , idx , arr ) => idx < arr . length - 1 || arr [ idx ] !== '' ) ;
130150 lines . forEach ( ( _ , idx ) => {
@@ -133,6 +153,8 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
133153 type : 'added' ,
134154 } ) ;
135155 } ) ;
156+ // Pad original side so unchanged lines below stay aligned
157+ originalViewZones . push ( { afterLineNumber : originalLine - 1 , heightInLines : lineCount } ) ;
136158 modifiedLine += lineCount ;
137159 } else if ( change . removed ) {
138160 // Check if next is added (paired change for inline diff)
@@ -201,17 +223,33 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
201223 } ) ;
202224 }
203225
204- additions += nextChange . count || 0 ;
205- modifiedLine += nextChange . count || 0 ;
226+ const addedCount = nextChange . count || 0 ;
227+ additions += addedCount ;
228+
229+ // Add alignment padding on the shorter side
230+ if ( removedLines . length > addedLines . length ) {
231+ modifiedViewZones . push ( {
232+ afterLineNumber : modifiedLine + addedLines . length - 1 ,
233+ heightInLines : removedLines . length - addedLines . length ,
234+ } ) ;
235+ } else if ( addedLines . length > removedLines . length ) {
236+ originalViewZones . push ( {
237+ afterLineNumber : originalLine + removedLines . length - 1 ,
238+ heightInLines : addedLines . length - removedLines . length ,
239+ } ) ;
240+ }
241+
242+ modifiedLine += addedCount ;
206243 i ++ ; // Skip the next (added) change since we handled it
207244 } else {
208- // Unpaired removal
245+ // Isolated removal — pad modified side
209246 removedLines . forEach ( ( _ , idx ) => {
210247 originalLineDecorations . push ( {
211248 lineNumber : originalLine + idx ,
212249 type : 'removed' ,
213250 } ) ;
214251 } ) ;
252+ modifiedViewZones . push ( { afterLineNumber : modifiedLine - 1 , heightInLines : lineCount } ) ;
215253 }
216254 originalLine += lineCount ;
217255 } else {
@@ -256,6 +294,22 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
256294 originalDecorationsRef . current ,
257295 decorations
258296 ) ;
297+
298+ // Apply alignment view zones
299+ editor . changeViewZones ( ( accessor : any ) => {
300+ originalViewZoneIdsRef . current . forEach ( ( id ) => accessor . removeZone ( id ) ) ;
301+ originalViewZoneIdsRef . current = [ ] ;
302+ originalViewZones . forEach ( ( zone ) => {
303+ const domNode = document . createElement ( 'div' ) ;
304+ domNode . className = 'diff-placeholder-zone' ;
305+ const id = accessor . addZone ( {
306+ afterLineNumber : zone . afterLineNumber ,
307+ heightInLines : zone . heightInLines ,
308+ domNode,
309+ } ) ;
310+ originalViewZoneIdsRef . current . push ( id ) ;
311+ } ) ;
312+ } ) ;
259313 }
260314
261315 if ( modifiedEditorRef . current ) {
@@ -282,6 +336,22 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
282336 modifiedDecorationsRef . current ,
283337 decorations
284338 ) ;
339+
340+ // Apply alignment view zones
341+ editor . changeViewZones ( ( accessor : any ) => {
342+ modifiedViewZoneIdsRef . current . forEach ( ( id ) => accessor . removeZone ( id ) ) ;
343+ modifiedViewZoneIdsRef . current = [ ] ;
344+ modifiedViewZones . forEach ( ( zone ) => {
345+ const domNode = document . createElement ( 'div' ) ;
346+ domNode . className = 'diff-placeholder-zone' ;
347+ const id = accessor . addZone ( {
348+ afterLineNumber : zone . afterLineNumber ,
349+ heightInLines : zone . heightInLines ,
350+ domNode,
351+ } ) ;
352+ modifiedViewZoneIdsRef . current . push ( id ) ;
353+ } ) ;
354+ } ) ;
285355 }
286356 } , [ originalText , modifiedText , options . ignoreWhitespace ] ) ;
287357
@@ -461,6 +531,15 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
461531 background-color: rgba(34, 197, 94, 0.4) !important;
462532 border-radius: 2px;
463533 }
534+ .diff-placeholder-zone {
535+ background: repeating-linear-gradient(
536+ 45deg,
537+ rgba(128, 128, 128, 0.08) 0px,
538+ rgba(128, 128, 128, 0.08) 4px,
539+ transparent 4px,
540+ transparent 8px
541+ );
542+ }
464543 ` } </ style >
465544
466545 { /* Header Section */ }
@@ -539,22 +618,32 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
539618 showClearButton = { true }
540619 showCopyButton = { true }
541620 headerActions = {
542- < DropdownMenu >
543- < DropdownMenuTrigger asChild >
544- < Button variant = "outline" size = "sm" className = "h-8 px-3 text-xs" >
545- Load Example
546- < ChevronDownIcon className = "h-3 w-3 ml-1" />
547- </ Button >
548- </ DropdownMenuTrigger >
549- < DropdownMenuContent align = "end" >
550- < DropdownMenuItem onClick = { ( ) => handleLoadExample ( 'original' , 'javascript' ) } >
551- Load JavaScript Example
552- </ DropdownMenuItem >
553- < DropdownMenuItem onClick = { ( ) => handleLoadExample ( 'original' , 'python' ) } >
554- Load Python Example
555- </ DropdownMenuItem >
556- </ DropdownMenuContent >
557- </ DropdownMenu >
621+ < >
622+ < LoadFileButton
623+ onFileLoad = { ( content , file ) => {
624+ setOriginalText ( content ) ;
625+ const ext = file . name . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ?? '' ;
626+ const lang = EXTENSION_LANGUAGE_MAP [ ext ] ;
627+ if ( lang ) setOptions ( ( prev ) => ( { ...prev , language : lang } ) ) ;
628+ } }
629+ />
630+ < DropdownMenu >
631+ < DropdownMenuTrigger asChild >
632+ < Button variant = "outline" size = "sm" className = "h-8 px-3 text-xs" >
633+ Load Example
634+ < ChevronDownIcon className = "h-3 w-3 ml-1" />
635+ </ Button >
636+ </ DropdownMenuTrigger >
637+ < DropdownMenuContent align = "end" >
638+ < DropdownMenuItem onClick = { ( ) => handleLoadExample ( 'original' , 'javascript' ) } >
639+ Load JavaScript Example
640+ </ DropdownMenuItem >
641+ < DropdownMenuItem onClick = { ( ) => handleLoadExample ( 'original' , 'python' ) } >
642+ Load Python Example
643+ </ DropdownMenuItem >
644+ </ DropdownMenuContent >
645+ </ DropdownMenu >
646+ </ >
558647 }
559648 footerLeftContent = {
560649 < >
@@ -578,22 +667,32 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
578667 showClearButton = { true }
579668 showCopyButton = { true }
580669 headerActions = {
581- < DropdownMenu >
582- < DropdownMenuTrigger asChild >
583- < Button variant = "outline" size = "sm" className = "h-8 px-3 text-xs" >
584- Load Example
585- < ChevronDownIcon className = "h-3 w-3 ml-1" />
586- </ Button >
587- </ DropdownMenuTrigger >
588- < DropdownMenuContent align = "end" >
589- < DropdownMenuItem onClick = { ( ) => handleLoadExample ( 'modified' , 'javascript' ) } >
590- Load JavaScript Example
591- </ DropdownMenuItem >
592- < DropdownMenuItem onClick = { ( ) => handleLoadExample ( 'modified' , 'python' ) } >
593- Load Python Example
594- </ DropdownMenuItem >
595- </ DropdownMenuContent >
596- </ DropdownMenu >
670+ < >
671+ < LoadFileButton
672+ onFileLoad = { ( content , file ) => {
673+ setModifiedText ( content ) ;
674+ const ext = file . name . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ?? '' ;
675+ const lang = EXTENSION_LANGUAGE_MAP [ ext ] ;
676+ if ( lang ) setOptions ( ( prev ) => ( { ...prev , language : lang } ) ) ;
677+ } }
678+ />
679+ < DropdownMenu >
680+ < DropdownMenuTrigger asChild >
681+ < Button variant = "outline" size = "sm" className = "h-8 px-3 text-xs" >
682+ Load Example
683+ < ChevronDownIcon className = "h-3 w-3 ml-1" />
684+ </ Button >
685+ </ DropdownMenuTrigger >
686+ < DropdownMenuContent align = "end" >
687+ < DropdownMenuItem onClick = { ( ) => handleLoadExample ( 'modified' , 'javascript' ) } >
688+ Load JavaScript Example
689+ </ DropdownMenuItem >
690+ < DropdownMenuItem onClick = { ( ) => handleLoadExample ( 'modified' , 'python' ) } >
691+ Load Python Example
692+ </ DropdownMenuItem >
693+ </ DropdownMenuContent >
694+ </ DropdownMenu >
695+ </ >
597696 }
598697 footerLeftContent = {
599698 < >
0 commit comments