Skip to content

Commit e651360

Browse files
authored
feat: add scrollbar to diff viewer (#112)
## Summary - Adds a vertical scrollbar to the diff panel when content overflows - Extracts reusable `RenderScrollbar` in `pkg/ui/common` for use by other panels
1 parent 19503d8 commit e651360

2 files changed

Lines changed: 61 additions & 6 deletions

File tree

pkg/ui/common/scrollbar.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package common
2+
3+
import (
4+
"strings"
5+
6+
"charm.land/lipgloss/v2"
7+
)
8+
9+
// RenderScrollbar renders a vertical scrollbar track with a thumb indicator.
10+
// It takes the viewport height, total number of content lines, and the current
11+
// scroll offset (YOffset). Returns an empty string if all content fits.
12+
func RenderScrollbar(viewHeight, totalLines, yOffset int) string {
13+
if totalLines <= viewHeight {
14+
return ""
15+
}
16+
17+
trackHeight := viewHeight
18+
thumbSize := max(1, trackHeight*viewHeight/totalLines)
19+
20+
scrollableLines := totalLines - viewHeight
21+
thumbPos := 0
22+
if scrollableLines > 0 {
23+
thumbPos = yOffset * (trackHeight - thumbSize) / scrollableLines
24+
if yOffset > 0 && thumbPos == 0 {
25+
thumbPos = 1
26+
}
27+
}
28+
29+
track := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
30+
thumb := lipgloss.NewStyle().Foreground(lipgloss.Blue)
31+
32+
var sb strings.Builder
33+
for i := 0; i < trackHeight; i++ {
34+
if i > 0 {
35+
sb.WriteByte('\n')
36+
}
37+
if i >= thumbPos && i < thumbPos+thumbSize {
38+
sb.WriteString(thumb.Render("┃"))
39+
} else {
40+
sb.WriteString(track.Render("│"))
41+
}
42+
}
43+
return sb.String()
44+
}

pkg/ui/panes/diffviewer/diffviewer.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,30 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
9797
return m, tea.Batch(cmds...)
9898
}
9999

100+
const scrollbarWidth = 3 // 1 space + 1 scrollbar character + 1 padding
101+
100102
func (m Model) View() string {
101-
return lipgloss.JoinVertical(lipgloss.Left, m.headerView(), m.vp.View())
103+
vpView := m.vp.View()
104+
scrollbar := common.RenderScrollbar(m.vp.Height(), m.vp.TotalLineCount(), m.vp.YOffset())
105+
if scrollbar != "" {
106+
vpView = lipgloss.JoinHorizontal(lipgloss.Top, vpView, " ", scrollbar)
107+
}
108+
return lipgloss.JoinVertical(lipgloss.Left, m.headerView(), vpView)
102109
}
103110

104111
func (m *Model) SetSize(width, height int) tea.Cmd {
105112
m.Width = width
106113
m.Height = height
107-
m.vp.SetWidth(m.Width)
114+
m.vp.SetWidth(m.contentWidth())
108115
m.vp.SetHeight(m.Height - dirHeaderHeight)
109116
m.cache = make(nodeCache)
110117
return m.diff()
111118
}
112119

120+
func (m Model) contentWidth() int {
121+
return m.Width - scrollbarWidth
122+
}
123+
113124
func (m *Model) diff() tea.Cmd {
114125
if m.file != nil {
115126
key := cacheKey(m.file.path, m.sideBySide)
@@ -126,7 +137,7 @@ func (m *Model) diff() tea.Cmd {
126137
}
127138
m.file = node
128139
m.cache[key] = node
129-
return diffFile(node, m.Width, m.sideBySide)
140+
return diffFile(node, m.contentWidth(), m.sideBySide)
130141
} else if m.dir != nil {
131142
key := cacheKey(m.dir.path, m.sideBySide)
132143
if cached, ok := m.cache[key]; ok && cached.diff != "" {
@@ -146,7 +157,7 @@ func (m *Model) diff() tea.Cmd {
146157
if m.dir.path == "/" {
147158
preamble = m.preamble
148159
}
149-
return diffDir(node, m.Width, m.sideBySide, preamble)
160+
return diffDir(node, m.contentWidth(), m.sideBySide, preamble)
150161
}
151162

152163
return nil
@@ -217,7 +228,7 @@ func (m Model) SetFilePatch(file *gitdiff.File) (Model, tea.Cmd) {
217228
}
218229
m.cache[key] = m.file
219230

220-
return m, diffFile(m.file, m.Width, m.sideBySide)
231+
return m, diffFile(m.file, m.contentWidth(), m.sideBySide)
221232
}
222233

223234
func (m Model) SetDirPatch(dirPath string, files []*gitdiff.File) (Model, tea.Cmd) {
@@ -247,7 +258,7 @@ func (m Model) SetDirPatch(dirPath string, files []*gitdiff.File) (Model, tea.Cm
247258
if dirPath == "/" {
248259
preamble = m.preamble
249260
}
250-
return m, diffDir(m.dir, m.Width, m.sideBySide, preamble)
261+
return m, diffDir(m.dir, m.contentWidth(), m.sideBySide, preamble)
251262
}
252263

253264
func (m *Model) GoToTop() {

0 commit comments

Comments
 (0)