@@ -6,12 +6,96 @@ import styles from "./Editor.module.css";
66import { highlightedLinesAtom , highlighterAtom , loadingLanguageAtom , diffModeAtom } from "../store" ;
77import { useAtom , useAtomValue , useSetAtom } from "jotai" ;
88import { themeDarkModeAtom , themeAtom } from "../store/themes" ;
9+ import { diffWordsWithSpace } from "diff" ;
910
1011type PropTypes = {
1112 selectedLanguage : Language | null ;
1213 code : string ;
1314} ;
1415
16+ interface InlineRange {
17+ start : number ;
18+ end : number ;
19+ type : "add" | "remove" ;
20+ }
21+
22+ function applyInlineDiffs ( lineNode : any , ranges : InlineRange [ ] ) {
23+ if ( ! ranges || ranges . length === 0 ) return ;
24+
25+ const textNodes : { parent : any ; textNode : any ; text : string ; offset : number } [ ] = [ ] ;
26+ let currentOffset = 0 ;
27+
28+ function walk ( node : any ) {
29+ if ( node . type === "text" ) {
30+ textNodes . push ( { parent : null , textNode : node , text : node . value , offset : currentOffset } ) ;
31+ currentOffset += node . value . length ;
32+ } else if ( node . children ) {
33+ for ( const child of node . children ) {
34+ const startLen = textNodes . length ;
35+ walk ( child ) ;
36+ const endLen = textNodes . length ;
37+ for ( let i = startLen ; i < endLen ; i ++ ) {
38+ if ( ! textNodes [ i ] . parent ) textNodes [ i ] . parent = node ;
39+ }
40+ }
41+ }
42+ }
43+ walk ( lineNode ) ;
44+
45+ const tokenToNewChildren = new Map < any , any [ ] > ( ) ;
46+
47+ for ( const item of textNodes ) {
48+ const { parent, textNode, text, offset } = item ;
49+ const tokenStart = offset ;
50+ const tokenEnd = offset + text . length ;
51+
52+ const intersectingRanges = ranges . filter ( ( r ) => r . start < tokenEnd && r . end > tokenStart ) ;
53+
54+ if ( intersectingRanges . length === 0 ) {
55+ if ( ! tokenToNewChildren . has ( parent ) ) {
56+ tokenToNewChildren . set ( parent , [ ] ) ;
57+ }
58+ tokenToNewChildren . get ( parent ) ! . push ( textNode ) ;
59+ continue ;
60+ }
61+
62+ let currentIdx = 0 ;
63+ const newChildren = [ ] ;
64+
65+ for ( const r of intersectingRanges ) {
66+ const rStartInText = Math . max ( 0 , r . start - tokenStart ) ;
67+ if ( rStartInText > currentIdx ) {
68+ newChildren . push ( { type : "text" , value : text . substring ( currentIdx , rStartInText ) } ) ;
69+ }
70+
71+ const rEndInText = Math . min ( text . length , r . end - tokenStart ) ;
72+ if ( rEndInText > rStartInText ) {
73+ const className = r . type === "add" ? "diff-word-add" : "diff-word-remove" ;
74+ newChildren . push ( {
75+ type : "element" ,
76+ tagName : "span" ,
77+ properties : { className : [ className ] } ,
78+ children : [ { type : "text" , value : text . substring ( Math . max ( currentIdx , rStartInText ) , rEndInText ) } ] ,
79+ } ) ;
80+ currentIdx = rEndInText ;
81+ }
82+ }
83+
84+ if ( currentIdx < text . length ) {
85+ newChildren . push ( { type : "text" , value : text . substring ( currentIdx ) } ) ;
86+ }
87+
88+ if ( ! tokenToNewChildren . has ( parent ) ) {
89+ tokenToNewChildren . set ( parent , [ ] ) ;
90+ }
91+ tokenToNewChildren . get ( parent ) ! . push ( ...newChildren ) ;
92+ }
93+
94+ tokenToNewChildren . forEach ( ( newChildren , parent ) => {
95+ parent . children = newChildren ;
96+ } ) ;
97+ }
98+
1599const HighlightedCode : React . FC < PropTypes > = ( { selectedLanguage, code } ) => {
16100 const [ highlightedHtml , setHighlightedHtml ] = useState ( "" ) ;
17101 const highlighter = useAtomValue ( highlighterAtom ) ;
@@ -42,6 +126,49 @@ const HighlightedCode: React.FC<PropTypes> = ({ selectedLanguage, code }) => {
42126 lang = "tsx" ;
43127 }
44128
129+ const lineDiffs : Record < number , InlineRange [ ] > = { } ;
130+ const lines = code . split ( "\n" ) ;
131+
132+ if ( diffMode ) {
133+ for ( let i = 0 ; i < lines . length ; i ++ ) {
134+ if ( lines [ i ] . startsWith ( "-" ) && i + 1 < lines . length && lines [ i + 1 ] . startsWith ( "+" ) ) {
135+ const removedCode = lines [ i ] . substring ( 1 ) ;
136+ const addedCode = lines [ i + 1 ] . substring ( 1 ) ;
137+ const changes = diffWordsWithSpace ( removedCode , addedCode ) ;
138+
139+ let removedOffset = 1 ;
140+ let addedOffset = 1 ;
141+
142+ const removedRanges : InlineRange [ ] = [ ] ;
143+ const addedRanges : InlineRange [ ] = [ ] ;
144+
145+ for ( const part of changes ) {
146+ if ( part . added ) {
147+ addedRanges . push ( {
148+ start : addedOffset ,
149+ end : addedOffset + part . value . length ,
150+ type : "add" ,
151+ } ) ;
152+ addedOffset += part . value . length ;
153+ } else if ( part . removed ) {
154+ removedRanges . push ( {
155+ start : removedOffset ,
156+ end : removedOffset + part . value . length ,
157+ type : "remove" ,
158+ } ) ;
159+ removedOffset += part . value . length ;
160+ } else {
161+ addedOffset += part . value . length ;
162+ removedOffset += part . value . length ;
163+ }
164+ }
165+
166+ lineDiffs [ i + 1 ] = removedRanges ;
167+ lineDiffs [ i + 2 ] = addedRanges ;
168+ }
169+ }
170+ }
171+
45172 return highlighter . codeToHtml ( code , {
46173 lang : lang ,
47174 theme : themeName ,
@@ -53,12 +180,16 @@ const HighlightedCode: React.FC<PropTypes> = ({ selectedLanguage, code }) => {
53180
54181 // Handle diff mode
55182 if ( diffMode ) {
56- const lineContent = code . split ( "\n" ) [ line - 1 ] ;
183+ const lineContent = lines [ line - 1 ] ;
57184 if ( lineContent ?. startsWith ( "+" ) ) {
58185 this . addClassToHast ( node , "diff-add" ) ;
59186 } else if ( lineContent ?. startsWith ( "-" ) ) {
60187 this . addClassToHast ( node , "diff-remove" ) ;
61188 }
189+
190+ if ( lineDiffs [ line ] ) {
191+ applyInlineDiffs ( node , lineDiffs [ line ] ) ;
192+ }
62193 }
63194 } ,
64195 } ,
0 commit comments