@@ -4,240 +4,191 @@ import (
44 "bufio"
55 "fmt"
66 "os"
7- "regexp"
87 "strings"
98)
109
1110type xpathSegment struct {
1211 tag string
1312 attr string
1413 value string
15- isAttr bool // true for [@attr='val'], false for [tag='val']
14+ isAttr bool
1615}
1716
18- var segmentRegex = regexp .MustCompile (`^([a-zA-Z0-9_-]+)(?:\[(@?)([a-zA-Z0-9_()-]+)='([^']*)'\])?$` ) // This regex is generated by Gemini
19-
2017func parseEntityUniqueIdentifier (id string ) ([]xpathSegment , error ) {
2118 parts := strings .Split (id , "/" )
2219 var segments []xpathSegment
23- for _ , part := range parts {
24- if part == "" {
20+ for _ , p := range parts {
21+ seg := xpathSegment {}
22+ bracketIdx := strings .Index (p , "[" )
23+ if bracketIdx == - 1 {
24+ seg .tag = p
25+ segments = append (segments , seg )
2526 continue
2627 }
27- matches := segmentRegex .FindStringSubmatch (part )
28- if matches == nil {
29- return nil , fmt .Errorf ("invalid segment: %s" , part )
28+ seg .tag = p [:bracketIdx ]
29+ rest := p [bracketIdx + 1 : len (p )- 1 ] // remove matching ]
30+ equalsIdx := strings .Index (rest , "=" )
31+ if equalsIdx == - 1 {
32+ return nil , fmt .Errorf ("invalid segment: %s" , p )
3033 }
31- seg := xpathSegment { tag : matches [ 1 ]}
32- if len ( matches ) > 3 && matches [ 3 ] != "" {
33- seg .isAttr = matches [ 2 ] == "@"
34- seg .attr = matches [ 3 ]
35- seg .value = matches [ 4 ]
34+ seg . attr = rest [: equalsIdx ]
35+ seg . value = strings . Trim ( rest [ equalsIdx + 1 :], "'" )
36+ if strings . HasPrefix ( seg .attr , "@" ) {
37+ seg .attr = seg . attr [ 1 : ]
38+ seg .isAttr = true
3639 }
3740 segments = append (segments , seg )
3841 }
3942 return segments , nil
4043}
4144
42- // findElementLines searches for the element identified by id in the file at path and returns its lines.
43- func findElementLines (path string , id string ) ([]string , int , error ) {
44- segments , err := parseEntityUniqueIdentifier (id )
45- if err != nil {
46- return nil , 0 , err
47- }
48-
49- f , err := os .Open (path )
45+ // getCustomDiffLines reads a file and returns lines around the target element.
46+ // It returns the lines, the start index of the target in the returned lines,
47+ // the end index of the target, and an error.
48+ func getCustomDiffLines (path string , targetID string ) ([]string , int , int , error ) {
49+ file , err := os .Open (path )
5050 if err != nil {
51- return nil , 0 , err
51+ return nil , 0 , 0 , err
5252 }
53- defer f .Close ()
53+ defer file .Close ()
5454
55- scanner := bufio . NewScanner ( f )
56- var allLines [] string
55+ var lines [] string
56+ scanner := bufio . NewScanner ( file )
5757 for scanner .Scan () {
58- allLines = append (allLines , scanner .Text ()+ " \n " )
58+ lines = append (lines , scanner .Text ())
5959 }
6060 if err := scanner .Err (); err != nil {
61- return nil , 0 , err
61+ return nil , 0 , 0 , err
6262 }
6363
64- segIdx := 0
65- foundParent := false
66- parentStartLine := - 1
67- startLine := - 1
68-
69- for i := 0 ; i < len (allLines ); i ++ {
70- line := allLines [i ]
71- if segIdx >= len (segments ) {
72- break
73- }
74- seg := segments [segIdx ]
75-
76- // Boundary check: if we are looking for a child segment, and we see the parent close tag, we fail!
77- if segIdx > 0 {
78- parentTag := segments [segIdx - 1 ].tag
79- if strings .Contains (line , "</" + parentTag + ">" ) {
80- return nil , 0 , nil // Not found within parent boundary
81- }
82- }
83-
84- if seg .attr == "text()" {
85- // Text content filter
86- if strings .Contains (line , "<" + seg .tag ) && strings .Contains (line , seg .value ) {
87- segIdx ++
88- if segIdx >= len (segments ) {
89- startLine = i
64+ segments , err := parseEntityUniqueIdentifier (targetID )
65+ if err != nil {
66+ return nil , 0 , 0 , err
67+ }
68+
69+ currentLine := 0
70+ lastFoundLine := - 1
71+ foundLastSegment := false
72+
73+ for i , seg := range segments {
74+ found := false
75+ for j := currentLine ; j < len (lines ); j ++ {
76+ line := lines [j ]
77+
78+ // Guard against crossing into a sibling of the parent element
79+ if i > 0 {
80+ parentSeg := segments [i - 1 ]
81+ // If we see another instance of the parent tag, we've likely left its scope
82+ if strings .Contains (line , "<" + parentSeg .tag ) {
83+ break
9084 }
91- }
92- } else if seg .attr != "" && ! seg .isAttr {
93- // Child element filter
94- if ! foundParent {
95- if strings .Contains (line , "<" + seg .tag ) {
96- foundParent = true
97- parentStartLine = i
85+ // If we see the closing tag of the parent, we've definitely left its scope
86+ if strings .Contains (line , "</" + parentSeg .tag + ">" ) {
87+ break
9888 }
9989 }
100- if foundParent {
101- childRegex := fmt .Sprintf (`<%s(\s+[^>]*)?>%s</%s>` , regexp .QuoteMeta (seg .attr ), regexp .QuoteMeta (seg .value ), regexp .QuoteMeta (seg .attr ))
102- matched , _ := regexp .MatchString (childRegex , line )
103- if matched {
104- segIdx ++
105- foundParent = false
106- if segIdx >= len (segments ) {
107- startLine = parentStartLine
90+
91+ if strings .Contains (line , seg .tag ) {
92+ if seg .isAttr {
93+ attrPattern1 := fmt .Sprintf (`%s="%s"` , seg .attr , seg .value )
94+ attrPattern2 := fmt .Sprintf (`%s='%s'` , seg .attr , seg .value )
95+ if strings .Contains (line , attrPattern1 ) || strings .Contains (line , attrPattern2 ) {
96+ lastFoundLine = j
97+ currentLine = j + 1
98+ found = true
99+ if i == len (segments )- 1 {
100+ foundLastSegment = true
101+ }
102+ break
108103 }
104+ continue // Attributes must be on the same line as the tag
109105 }
110- }
111- } else {
112- // Normal or attr filter
113- if strings .Contains (line , "<" + seg .tag ) {
114- if seg .attr == "" {
115- segIdx ++
116- if segIdx >= len (segments ) {
117- startLine = i
106+
107+ if seg .value != "" {
108+ if strings .Contains (line , seg .value ) {
109+ lastFoundLine = j
110+ currentLine = j + 1 // Search next segment after this line
111+ found = true
112+ if i == len (segments )- 1 {
113+ foundLastSegment = true
114+ }
115+ break
118116 }
119- } else {
120- attrStr1 := fmt .Sprintf (`%s="%s"` , seg .attr , seg .value )
121- attrStr2 := fmt .Sprintf (`%s='%s'` , seg .attr , seg .value )
122- if strings .Contains (line , attrStr1 ) || strings .Contains (line , attrStr2 ) {
123- segIdx ++
124- if segIdx >= len (segments ) {
125- startLine = i
117+ // Search up to 5 lines ahead for value
118+ foundValue := false
119+ for k := 1 ; k <= 5 && j + k < len (lines ); k ++ {
120+ if strings .Contains (lines [j + k ], seg .value ) {
121+ lastFoundLine = j // Keep tag line as the found line for context
122+ currentLine = j + k + 1
123+ found = true
124+ foundValue = true
125+ if i == len (segments )- 1 {
126+ foundLastSegment = true
127+ }
128+ break
126129 }
127130 }
131+ if foundValue {
132+ break
133+ }
134+ }
135+ if seg .value == "" {
136+ lastFoundLine = j
137+ currentLine = j + 1
138+ found = true
139+ if i == len (segments )- 1 {
140+ foundLastSegment = true
141+ }
142+ break
128143 }
129144 }
130145 }
131- }
132-
133- if segIdx < len (segments ) || startLine == - 1 {
134- return nil , 0 , nil // Not found
135- }
136-
137- targetTag := segments [len (segments )- 1 ].tag
138-
139- isSelfClosing := false
140- endLine := startLine
141- for i := startLine ; i < len (allLines ); i ++ {
142- if strings .Contains (allLines [i ], "/>" ) {
143- isSelfClosing = true
144- endLine = i
145- break
146+ if found {
147+ // Check for self-closing tag if there are more segments to find
148+ if i < len (segments )- 1 {
149+ trimmedLine := strings .TrimSpace (lines [lastFoundLine ])
150+ if strings .HasSuffix (trimmedLine , "/>" ) {
151+ // Parent self-closes, cannot have children!
152+ found = false
153+ }
154+ }
146155 }
147- if strings .Contains (allLines [i ], ">" ) {
156+ if ! found {
157+ // If a middle segment is not found, we might fail to find the target.
148158 break
149159 }
150160 }
151161
152- if isSelfClosing {
153- return allLines [startLine : endLine + 1 ], startLine + 1 , nil
154- }
162+ var targetIdx int
163+ highlight := true
155164
156- closeTag := "</" + targetTag + ">"
157- for i := startLine ; i < len (allLines ); i ++ {
158- if strings .Contains (allLines [i ], closeTag ) {
159- endLine = i
160- break
161- }
165+ if foundLastSegment {
166+ targetIdx = lastFoundLine
167+ } else if lastFoundLine != - 1 {
168+ // Fallback to parent
169+ targetIdx = lastFoundLine
170+ highlight = false
171+ } else {
172+ // Element and all parents not found
173+ return nil , 0 , 0 , nil
162174 }
163175
164- return allLines [startLine : endLine + 1 ], startLine + 1 , nil
165- }
166-
167- // GenerateUnifiedDiff generates a block-based unified diff between two sets of lines using LCS.
168- func GenerateUnifiedDiff (lines1 , lines2 []string , start1 , start2 int ) (string , error ) {
169- m := len (lines1 )
170- n := len (lines2 )
171- c := make ([][]int , m + 1 )
172- for i := range c {
173- c [i ] = make ([]int , n + 1 )
176+ start := targetIdx - 8
177+ if start < 0 {
178+ start = 0
174179 }
175-
176- for i := 1 ; i <= m ; i ++ {
177- for j := 1 ; j <= n ; j ++ {
178- if lines1 [i - 1 ] == lines2 [j - 1 ] {
179- c [i ][j ] = c [i - 1 ][j - 1 ] + 1
180- } else {
181- c [i ][j ] = max (c [i - 1 ][j ], c [i ][j - 1 ])
182- }
183- }
180+ end := targetIdx + 8
181+ if end >= len (lines ) {
182+ end = len (lines ) - 1
184183 }
185184
186- type edit struct {
187- op rune // ' ', '-', '+'
188- line string
185+ resultLines := lines [start : end + 1 ]
186+ relStart := - 1
187+ relEnd := - 1
188+ if highlight {
189+ relStart = targetIdx - start
190+ relEnd = targetIdx - start
189191 }
190- var edits []edit
191192
192- var collectDiff func (i , j int )
193- collectDiff = func (i , j int ) {
194- if i > 0 && j > 0 && lines1 [i - 1 ] == lines2 [j - 1 ] {
195- collectDiff (i - 1 , j - 1 )
196- edits = append (edits , edit {op : ' ' , line : lines1 [i - 1 ]})
197- } else if j > 0 && (i == 0 || c [i ][j - 1 ] >= c [i - 1 ][j ]) {
198- collectDiff (i , j - 1 )
199- edits = append (edits , edit {op : '+' , line : lines2 [j - 1 ]})
200- } else if i > 0 && (j == 0 || c [i ][j - 1 ] < c [i - 1 ][j ]) {
201- collectDiff (i - 1 , j )
202- edits = append (edits , edit {op : '-' , line : lines1 [i - 1 ]})
203- }
204- }
205- collectDiff (m , n )
206-
207- var result strings.Builder
208- result .WriteString ("--- Ref\n " )
209- result .WriteString ("+++ Generated\n " )
210- result .WriteString (fmt .Sprintf ("@@ -%d,%d +%d,%d @@\n " , start1 , m , start2 , n ))
211-
212- idx := 0
213- for idx < len (edits ) {
214- if edits [idx ].op == ' ' {
215- result .WriteString (" " + edits [idx ].line )
216- idx ++
217- } else {
218- j := idx
219- for j < len (edits ) && edits [j ].op != ' ' {
220- j ++
221- }
222- for k := idx ; k < j ; k ++ {
223- if edits [k ].op == '-' {
224- result .WriteString ("-" + edits [k ].line )
225- }
226- }
227- for k := idx ; k < j ; k ++ {
228- if edits [k ].op == '+' {
229- result .WriteString ("+" + edits [k ].line )
230- }
231- }
232- idx = j
233- }
234- }
235- return result .String (), nil
236- }
237-
238- func max (a , b int ) int {
239- if a > b {
240- return a
241- }
242- return b
193+ return resultLines , relStart , relEnd , nil
243194}
0 commit comments