on main help sign in
feat(diffview): prevent infinite vertical scroll by default
a723e4aAndrey Nering
internal/exp/diffview/Taskfile.yaml+20
@@ -100,3 +100,23 @@ tasks:
100 - for: sources
100 - for: sources
101 cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
101 cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
102 silent: true
102 silent: true
103+
104+ test:print:yoffset:unified:infinite:
105+ desc: Print golden files for debugging
106+ method: none
107+ sources:
108+ - ./testdata/TestDiffViewYOffsetInfinite/Unified/*.golden
109+ cmds:
110+ - for: sources
111+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
112+ silent: true
113+
114+ test:print:yoffset:split:infinite:
115+ desc: Print golden files for debugging
116+ method: none
117+ sources:
118+ - ./testdata/TestDiffViewYOffsetInfinite/Split/*.golden
119+ cmds:
120+ - for: sources
121+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
122+ silent: true
internal/exp/diffview/diffview.go+53−12
@@ -33,18 +33,19 @@ const (
33
33
34// DiffView represents a view for displaying differences between two files.
34// DiffView represents a view for displaying differences between two files.
35type DiffView struct {
35type DiffView struct {
36− layout layout
36+ layout layout
37− before file
37+ before file
38− after file
38+ after file
39− contextLines int
39+ contextLines int
40− lineNumbers bool
40+ lineNumbers bool
41− height int
41+ height int
42− width int
42+ width int
43− xOffset int
43+ xOffset int
44− yOffset int
44+ yOffset int
45+ infiniteYScroll bool
45− style Style
46+ style Style
46− tabWidth int
47+ tabWidth int
47− chromaStyle *chroma.Style
48+ chromaStyle *chroma.Style
48
49
49 isComputed bool
50 isComputed bool
50 err error
51 err error
@@ -53,6 +54,7 @@ type DiffView struct {
53
54
54 splitHunks []splitHunk
55 splitHunks []splitHunk
55
56
57+ totalLines int
56 codeWidth int
58 codeWidth int
57 fullCodeWidth int // with leading symbols
59 fullCodeWidth int // with leading symbols
58 extraColOnAfter bool // add extra column on after panel
60 extraColOnAfter bool // add extra column on after panel
@@ -138,6 +140,12 @@ func (dv *DiffView) YOffset(yOffset int) *DiffView {
138 return dv
140 return dv
139}
141}
140
142
143+// InfiniteYScroll allows the YOffset to scroll beyond the last line.
144+func (dv *DiffView) InfiniteYScroll(infiniteYScroll bool) *DiffView {
145+ dv.infiniteYScroll = infiniteYScroll
146+ return dv
147+}
148+
141// TabWidth sets the tab width. Only relevant for code that contains tabs, like
149// TabWidth sets the tab width. Only relevant for code that contains tabs, like
142// Go code.
150// Go code.
143func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
151func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
@@ -161,6 +169,8 @@ func (dv *DiffView) String() string {
161 dv.convertDiffToSplit()
169 dv.convertDiffToSplit()
162 dv.adjustStyles()
170 dv.adjustStyles()
163 dv.detectNumDigits()
171 dv.detectNumDigits()
172+ dv.detectTotalLines()
173+ dv.preventInfiniteYScroll()
164
174
165 if dv.width <= 0 {
175 if dv.width <= 0 {
166 dv.detectCodeWidth()
176 dv.detectCodeWidth()
@@ -251,6 +261,37 @@ func (dv *DiffView) detectNumDigits() {
251 }
261 }
252}
262}
253
263
264+func (dv *DiffView) detectTotalLines() {
265+ dv.totalLines = 0
266+
267+ switch dv.layout {
268+ case layoutUnified:
269+ for _, h := range dv.unified.Hunks {
270+ dv.totalLines += 1 + len(h.Lines)
271+ }
272+ case layoutSplit:
273+ for _, h := range dv.splitHunks {
274+ dv.totalLines += 1 + len(h.lines)
275+ }
276+ }
277+}
278+
279+func (dv *DiffView) preventInfiniteYScroll() {
280+ if dv.infiniteYScroll {
281+ return
282+ }
283+
284+ // clamp yOffset to prevent scrolling beyond the last line
285+ if dv.height > 0 {
286+ maxYOffset := max(0, dv.totalLines-dv.height)
287+ dv.yOffset = min(dv.yOffset, maxYOffset)
288+ } else {
289+ // if no height limit, ensure yOffset doesn't exceed total lines
290+ dv.yOffset = min(dv.yOffset, max(0, dv.totalLines-1))
291+ }
292+ dv.yOffset = max(0, dv.yOffset) // ensure yOffset is not negative
293+}
294+
254// detectCodeWidth calculates the maximum width of code lines in the diff view.
295// detectCodeWidth calculates the maximum width of code lines in the diff view.
255func (dv *DiffView) detectCodeWidth() {
296func (dv *DiffView) detectCodeWidth() {
256 switch dv.layout {
297 switch dv.layout {
internal/exp/diffview/diffview_test.go+27
@@ -283,6 +283,33 @@ func TestDiffViewYOffset(t *testing.T) {
283 }
283 }
284}
284}
285
285
286+func TestDiffViewYOffsetInfinite(t *testing.T) {
287+ for layoutName, layoutFunc := range LayoutFuncs {
288+ t.Run(layoutName, func(t *testing.T) {
289+ for yOffset := range 17 {
290+ t.Run(fmt.Sprintf("YOffsetOf%02d", yOffset), func(t *testing.T) {
291+ t.Parallel()
292+
293+ dv := diffview.New().
294+ Before("main.go", TestMultipleHunksBefore).
295+ After("main.go", TestMultipleHunksAfter).
296+ Style(diffview.DefaultLightStyle()).
297+ ChromaStyle(styles.Get("catppuccin-latte")).
298+ Height(5).
299+ YOffset(yOffset).
300+ InfiniteYScroll(true)
301+ dv = layoutFunc(dv)
302+
303+ output := dv.String()
304+ golden.RequireEqual(t, []byte(output))
305+
306+ assertHeight(t, 5, output)
307+ })
308+ }
309+ })
310+ }
311+}
312+
286func assertLineWidth(t *testing.T, expected int, output string) {
313func assertLineWidth(t *testing.T, expected int, output string) {
287 var lineWidth int
314 var lineWidth int
288 for line := range strings.SplitSeq(output, "\n") {
315 for line := range strings.SplitSeq(output, "\n") {
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf11.golden+2−2
@@ -1,5 +1,5 @@
1+ 10    11   
1 11  func getContent() string {  12  func getContent() string { 
2 11  func getContent() string {  12  func getContent() string { 
2 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3      14 +  return content 
4      14 +  return content 
4− 13  }  15  } 
5+ 13  }  15  } 
5−          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf12.golden+3−3
@@ -1,5 +1,5 @@
1+ 10    11   
2+ 11  func getContent() string {  12  func getContent() string { 
1 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
2      14 +  return content 
4      14 +  return content 
3− 13  }  15  } 
5+ 13  }  15  } 
4−          
5−          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf13.golden+4−4
@@ -1,5 +1,5 @@
1+ 10    11   
2+ 11  func getContent() string {  12  func getContent() string { 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
1      14 +  return content 
4      14 +  return content 
2− 13  }  15  } 
5+ 13  }  15  } 
3−          
4−          
5−          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf14.golden+5−5
@@ -1,5 +1,5 @@
1+ 10    11   
2+ 11  func getContent() string {  12  func getContent() string { 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4+      14 +  return content 
1− 13  }  15  } 
5+ 13  }  15  } 
2−          
3−          
4−          
5−          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf15.golden+5−5
@@ -1,5 +1,5 @@
1−          
1+ 10    11   
2−          
2+ 11  func getContent() string {  12  func getContent() string { 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3−          
4+      14 +  return content 
4−          
5+ 13  }  15  } 
5−          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf16.golden+5−5
@@ -1,5 +1,5 @@
1−          
1+ 10    11   
2−          
2+ 11  func getContent() string {  12  func getContent() string { 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3−          
4+      14 +  return content 
4−          
5+ 13  }  15  } 
5−          
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf12.golden+2−2
@@ -1,5 +1,5 @@
1+ 11  12  func getContent() string { 
1 12    -  return "Hello, world!" 
2 12    -  return "Hello, world!" 
2    13 +  content := strings.ToUpper("Hello, World!") 
3    13 +  content := strings.ToUpper("Hello, World!") 
3    14 +  return content 
4    14 +  return content 
4− 13  15  } 
5+ 13  15  } 
5−        
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf13.golden+3−3
@@ -1,5 +1,5 @@
1+ 11  12  func getContent() string { 
2+ 12    -  return "Hello, world!" 
1    13 +  content := strings.ToUpper("Hello, World!") 
3    13 +  content := strings.ToUpper("Hello, World!") 
2    14 +  return content 
4    14 +  return content 
3− 13  15  } 
5+ 13  15  } 
4−        
5−        
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf14.golden+4−4
@@ -1,5 +1,5 @@
1+ 11  12  func getContent() string { 
2+ 12    -  return "Hello, world!" 
3+    13 +  content := strings.ToUpper("Hello, World!") 
1    14 +  return content 
4    14 +  return content 
2− 13  15  } 
5+ 13  15  } 
3−        
4−        
5−        
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf15.golden+5−5
@@ -1,5 +1,5 @@
1+ 11  12  func getContent() string { 
2+ 12    -  return "Hello, world!" 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4+    14 +  return content 
1− 13  15  } 
5+ 13  15  } 
2−        
3−        
4−        
5−        
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf16.golden+5−5
@@ -1,5 +1,5 @@
1−        
1+ 11  12  func getContent() string { 
2−        
2+ 12    -  return "Hello, world!" 
3−        
3+    13 +  content := strings.ToUpper("Hello, World!") 
4−        
4+    14 +  return content 
5−        
5+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf00.goldennew file+5
@@ -0,0 +1,5 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf01.goldennew file+5
@@ -0,0 +1,5 @@
1+  2     2   
2+  3  import (   3  import ( 
3+  4   "fmt"   4   "fmt" 
4+       5 +  "strings" 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf02.goldennew file+5
@@ -0,0 +1,5 @@
1+  3  import (   3  import ( 
2+  4   "fmt"   4   "fmt" 
3+       5 +  "strings" 
4+  5  )   6  ) 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf03.goldennew file+5
@@ -0,0 +1,5 @@
1+  4   "fmt"   4   "fmt" 
2+       5 +  "strings" 
3+  5  )   6  ) 
4+  6     7   
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf04.goldennew file+5
@@ -0,0 +1,5 @@
1+       5 +  "strings" 
2+  5  )   6  ) 
3+  6     7   
4+  7  func main() {   8  func main() { 
5+  …  @@ -9,5 +10,6 @@    …   
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf05.goldennew file+5
@@ -0,0 +1,5 @@
1+  5  )   6  ) 
2+  6     7   
3+  7  func main() {   8  func main() { 
4+  …  @@ -9,5 +10,6 @@    …   
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf06.goldennew file+5
@@ -0,0 +1,5 @@
1+  6     7   
2+  7  func main() {   8  func main() { 
3+  …  @@ -9,5 +10,6 @@    …   
4+  9  }  10  } 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf07.goldennew file+5
@@ -0,0 +1,5 @@
1+  7  func main() {   8  func main() { 
2+  …  @@ -9,5 +10,6 @@    …   
3+  9  }  10  } 
4+ 10    11   
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf08.goldennew file+5
@@ -0,0 +1,5 @@
1+  …  @@ -9,5 +10,6 @@    …   
2+  9  }  10  } 
3+ 10    11   
4+ 11  func getContent() string {  12  func getContent() string { 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf09.goldennew file+5
@@ -0,0 +1,5 @@
1+  9  }  10  } 
2+ 10    11   
3+ 11  func getContent() string {  12  func getContent() string { 
4+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf10.goldennew file+5
@@ -0,0 +1,5 @@
1+ 10    11   
2+ 11  func getContent() string {  12  func getContent() string { 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4+      14 +  return content 
5+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf11.goldennew file+5
@@ -0,0 +1,5 @@
1+ 11  func getContent() string {  12  func getContent() string { 
2+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+      14 +  return content 
4+ 13  }  15  } 
5+          
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf12.goldennew file+5
@@ -0,0 +1,5 @@
1+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
2+      14 +  return content 
3+ 13  }  15  } 
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf13.goldennew file+5
@@ -0,0 +1,5 @@
1+      14 +  return content 
2+ 13  }  15  } 
3+          
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf14.goldennew file+5
@@ -0,0 +1,5 @@
1+ 13  }  15  } 
2+          
3+          
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf15.goldennew file+5
@@ -0,0 +1,5 @@
1+          
2+          
3+          
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf16.goldennew file+5
@@ -0,0 +1,5 @@
1+          
2+          
3+          
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf00.goldennew file+5
@@ -0,0 +1,5 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf01.goldennew file+5
@@ -0,0 +1,5 @@
1+  2   2   
2+  3   3  import ( 
3+  4   4   "fmt" 
4+     5 +  "strings" 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf02.goldennew file+5
@@ -0,0 +1,5 @@
1+  3   3  import ( 
2+  4   4   "fmt" 
3+     5 +  "strings" 
4+  5   6  ) 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf03.goldennew file+5
@@ -0,0 +1,5 @@
1+  4   4   "fmt" 
2+     5 +  "strings" 
3+  5   6  ) 
4+  6   7   
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf04.goldennew file+5
@@ -0,0 +1,5 @@
1+     5 +  "strings" 
2+  5   6  ) 
3+  6   7   
4+  7   8  func main() { 
5+  …   …  @@ -9,5 +10,6 @@  
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf05.goldennew file+5
@@ -0,0 +1,5 @@
1+  5   6  ) 
2+  6   7   
3+  7   8  func main() { 
4+  …   …  @@ -9,5 +10,6 @@  
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf06.goldennew file+5
@@ -0,0 +1,5 @@
1+  6   7   
2+  7   8  func main() { 
3+  …   …  @@ -9,5 +10,6 @@  
4+  9  10  } 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf07.goldennew file+5
@@ -0,0 +1,5 @@
1+  7   8  func main() { 
2+  …   …  @@ -9,5 +10,6 @@  
3+  9  10  } 
4+ 10  11   
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf08.goldennew file+5
@@ -0,0 +1,5 @@
1+  …   …  @@ -9,5 +10,6 @@  
2+  9  10  } 
3+ 10  11   
4+ 11  12  func getContent() string { 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf09.goldennew file+5
@@ -0,0 +1,5 @@
1+  9  10  } 
2+ 10  11   
3+ 11  12  func getContent() string { 
4+ 12    -  return "Hello, world!" 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf10.goldennew file+5
@@ -0,0 +1,5 @@
1+ 10  11   
2+ 11  12  func getContent() string { 
3+ 12    -  return "Hello, world!" 
4+    13 +  content := strings.ToUpper("Hello, World!") 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf11.goldennew file+5
@@ -0,0 +1,5 @@
1+ 11  12  func getContent() string { 
2+ 12    -  return "Hello, world!" 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4+    14 +  return content 
5+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf12.goldennew file+5
@@ -0,0 +1,5 @@
1+ 12    -  return "Hello, world!" 
2+    13 +  content := strings.ToUpper("Hello, World!") 
3+    14 +  return content 
4+ 13  15  } 
5+        
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf13.goldennew file+5
@@ -0,0 +1,5 @@
1+    13 +  content := strings.ToUpper("Hello, World!") 
2+    14 +  return content 
3+ 13  15  } 
4+        
5+        
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf14.goldennew file+5
@@ -0,0 +1,5 @@
1+    14 +  return content 
2+ 13  15  } 
3+        
4+        
5+        
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf15.goldennew file+5
@@ -0,0 +1,5 @@
1+ 13  15  } 
2+        
3+        
4+        
5+        
internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf16.goldennew file+5
@@ -0,0 +1,5 @@
1+        
2+        
3+        
4+        
5+