diff --git a/README.md b/README.md
index 0c3ead9..212762f 100644
--- a/README.md
+++ b/README.md
@@ -201,6 +201,7 @@ ui:
| `ui.colorFileNames` | bool | `true` | Color filenames by git status |
| `ui.showDiffStats` | bool | `true` | Show the amount of lines added / removed next to the file |
| `ui.sideBySide` | bool | `true` | Use side-by-side diff view (false for unified) |
+| `ui.wrapLines` | bool | `true` | Wrap long diff lines (false for unified view with horizontal scrolling) |
| `ui.startFoldersOpenDepth` | int | `-1` | Folder open depth on start (-1 = all, 0 = none) |
| `ui.theme` | string | `tokyo_night` | The ID of the theme to use |
@@ -233,6 +234,7 @@ ui:
| i | Cycle icon style |
| o | Open file in $EDITOR |
| s | Toggle side-by-side/unified view |
+| w | Toggle line wrap (off = scroll horizontally with ←/→) |
| Tab | Switch focus between the panes |
| T | Open the theme picker |
| q | Quit |
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 47a6f07..0ad2cd1 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -20,6 +20,7 @@ type UIConfig struct {
ColorFileNames bool `yaml:"colorFileNames"` // Color filenames by git status (default: true)
ShowDiffStats bool `yaml:"showDiffStats"` // Show the amount of lines added / removed next to the file
SideBySide bool `yaml:"sideBySide"` // Side-by-side diff view (default: true)
+ WrapLines bool `yaml:"wrapLines"` // Wrap long diff lines (default: true); when false, scroll the diff horizontally instead
StartFoldersOpenDepth int `yaml:"startFoldersOpenDepth"` // How many levels of folders to open on start (-1 = all, 0 = none)
Theme string `yaml:"theme"` // The theme to use (default: "tokyo_night")
}
@@ -46,6 +47,7 @@ func DefaultConfig() Config {
Icons: "nerd-fonts-status",
ColorFileNames: true,
SideBySide: true,
+ WrapLines: true,
ShowDiffStats: true,
StartFoldersOpenDepth: -1,
Theme: tint.TintTokyoNight.ID,
diff --git a/pkg/ui/keys.go b/pkg/ui/keys.go
index 742b1a9..9301887 100644
--- a/pkg/ui/keys.go
+++ b/pkg/ui/keys.go
@@ -29,6 +29,7 @@ type KeyMap struct {
SwitchPanel key.Binding
OpenInEditor key.Binding
ToggleDiffView key.Binding
+ ToggleWrapLines key.Binding
ToggleIconStyle key.Binding
ToggleHelp key.Binding
ToggleMessage key.Binding
@@ -141,6 +142,10 @@ var keys = &KeyMap{
key.WithKeys("s"),
key.WithHelp("s", "toggle side-by-side"),
),
+ ToggleWrapLines: key.NewBinding(
+ key.WithKeys("w"),
+ key.WithHelp("w", "toggle line wrap"),
+ ),
ToggleIconStyle: key.NewBinding(
key.WithKeys("i"),
key.WithHelp("i", "toggle icon style"),
@@ -186,6 +191,7 @@ func KeyGroups() [][]key.Binding {
keys.Copy,
keys.OpenInEditor,
keys.ToggleDiffView,
+ keys.ToggleWrapLines,
keys.ToggleIconStyle,
keys.ToggleSelection,
}, {
diff --git a/pkg/ui/panes/diffviewer/diffviewer.go b/pkg/ui/panes/diffviewer/diffviewer.go
index 69bd003..e4304a6 100644
--- a/pkg/ui/panes/diffviewer/diffviewer.go
+++ b/pkg/ui/panes/diffviewer/diffviewer.go
@@ -33,10 +33,13 @@ type cachedNode struct {
type nodeCache map[string]*cachedNode
-func cacheKey(path string, sideBySide bool) string {
+func cacheKey(path string, sideBySide bool, wrap bool) string {
key := path + ":" + common.Themes.Current().ID
if sideBySide {
- return key + ":sbs"
+ key += ":sbs"
+ }
+ if !wrap {
+ key += ":nowrap"
}
return key
}
@@ -91,8 +94,21 @@ type Model struct {
dir *cachedNode
cache nodeCache
sideBySide bool
+ wrapLines bool
preamble string
sb common.Scrollbar
+ // root additions/deletions are render-mode invariant and outlive cache
+ // clears, so the footer stats survive wrap/side-by-side toggles and theme
+ // changes while a file is open
+ rootAdditions int64
+ rootDeletions int64
+}
+
+// renderSideBySide reports whether the diff should render side-by-side.
+// delta truncates overflowing content in side-by-side mode, so it is only
+// usable when lines are wrapped.
+func (m Model) renderSideBySide() bool {
+ return m.sideBySide && m.wrapLines
}
// SetPreamble stores the preamble text (e.g. commit metadata from git show).
@@ -100,11 +116,12 @@ func (m *Model) SetPreamble(preamble string) {
m.preamble = preamble
}
-func New(sideBySide bool, styles *common.Styles) Model {
+func New(sideBySide bool, wrapLines bool, styles *common.Styles) Model {
vp := viewport.New(
0,
0,
viewport.WithKeyMap[diffLine](ViewportKeyMap),
+ viewport.WithWrapText[diffLine](wrapLines),
viewport.WithStyles[diffLine](
viewport.Styles{
SelectionPrefix: lipgloss.NewStyle().Foreground(styles.Tint.Blue).Render("▐"),
@@ -200,6 +217,7 @@ func New(sideBySide bool, styles *common.Styles) Model {
filterableviewport.WithHorizontalPad[diffLine](8),
),
sideBySide: sideBySide,
+ wrapLines: wrapLines,
cache: map[string]*cachedNode{},
}
}
@@ -208,6 +226,23 @@ func (m Model) Init() tea.Cmd {
return nil
}
+// currentCacheKey returns the cache key of the node and render mode currently
+// being displayed, or "" if none is set.
+func (m Model) currentCacheKey() string {
+ if m.file != nil {
+ file := m.file.files[0]
+ return cacheKey(
+ m.file.path,
+ m.renderSideBySide() && !file.IsNew && !file.IsDelete,
+ m.wrapLines,
+ )
+ }
+ if m.dir != nil {
+ return cacheKey(m.dir.path, m.renderSideBySide(), m.wrapLines)
+ }
+ return ""
+}
+
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
log.Debug("diffviewer", "msg", fmt.Sprintf("%T", msg))
cmds := make([]tea.Cmd, 0)
@@ -216,7 +251,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if _, ok := m.cache[msg.cacheKey]; ok {
m.cache[msg.cacheKey].diff = msg.lines
}
- m.fvp.SetObjects(msg.lines)
+ // a render finishing after we navigated away (other node, or toggled
+ // wrap/side-by-side) must not clobber what is on screen
+ if msg.cacheKey == m.currentCacheKey() {
+ m.fvp.SetObjects(msg.lines)
+ }
}
fvp, fvpCmd := m.fvp.Update(msg)
@@ -264,8 +303,8 @@ func (m Model) contentWidth() int {
func (m *Model) diff() tea.Cmd {
if m.file != nil {
file := m.file.files[0]
- sideBySide := m.sideBySide && !file.IsNew && !file.IsDelete
- key := cacheKey(m.file.path, sideBySide)
+ sideBySide := m.renderSideBySide() && !file.IsNew && !file.IsDelete
+ key := cacheKey(m.file.path, sideBySide, m.wrapLines)
if cached, ok := m.cache[key]; ok && len(cached.diff) != 0 {
m.file = cached
m.fvp.SetObjects(cached.diff)
@@ -281,7 +320,7 @@ func (m *Model) diff() tea.Cmd {
m.cache[key] = node
return m.diffFile(node, m.contentWidth())
} else if m.dir != nil {
- key := cacheKey(m.dir.path, m.sideBySide)
+ key := cacheKey(m.dir.path, m.renderSideBySide(), m.wrapLines)
if cached, ok := m.cache[key]; ok && len(cached.diff) != 0 {
m.dir = cached
m.fvp.SetObjects(cached.diff)
@@ -299,7 +338,7 @@ func (m *Model) diff() tea.Cmd {
if m.dir.path == "/" {
preamble = m.preamble
}
- return m.diffDir(node, m.contentWidth(), m.sideBySide, preamble)
+ return m.diffDir(node, m.contentWidth(), m.renderSideBySide(), preamble)
}
return nil
@@ -350,10 +389,11 @@ func (m Model) dirHeaderView() string {
func (m Model) SetFilePatch(file *gitdiff.File) (Model, tea.Cmd) {
m.dir = nil
+ m.resetPan()
fname := filenode.GetFileName(file)
- sideBySide := m.sideBySide && !file.IsNew && !file.IsDelete
- key := cacheKey(fname, sideBySide)
+ sideBySide := m.renderSideBySide() && !file.IsNew && !file.IsDelete
+ key := cacheKey(fname, sideBySide, m.wrapLines)
if cached, ok := m.cache[key]; ok {
m.file = cached
m.updateHeader()
@@ -378,8 +418,9 @@ func (m Model) SetFilePatch(file *gitdiff.File) (Model, tea.Cmd) {
func (m Model) SetDirPatch(dirPath string, files []*gitdiff.File) (Model, tea.Cmd) {
m.file = nil
+ m.resetPan()
- key := cacheKey(dirPath, m.sideBySide)
+ key := cacheKey(dirPath, m.renderSideBySide(), m.wrapLines)
if cached, ok := m.cache[key]; ok {
m.dir = cached
m.updateHeader()
@@ -400,12 +441,16 @@ func (m Model) SetDirPatch(dirPath string, files []*gitdiff.File) (Model, tea.Cm
deletions: deleted,
}
m.cache[key] = m.dir
+ if dirPath == "/" {
+ m.rootAdditions = added
+ m.rootDeletions = deleted
+ }
preamble := ""
if dirPath == "/" {
preamble = m.preamble
}
m.updateHeader()
- return m, m.diffDir(m.dir, m.contentWidth(), m.sideBySide, preamble)
+ return m, m.diffDir(m.dir, m.contentWidth(), m.renderSideBySide(), preamble)
}
func (m *Model) updateHeader() {
@@ -418,6 +463,21 @@ func (m *Model) SetSideBySide(sideBySide bool) tea.Cmd {
return m.diff()
}
+// SetWrapLines updates whether long lines wrap and re-renders. Wrapping is
+// required for side-by-side, so turning it off falls back to the unified view.
+func (m *Model) SetWrapLines(wrapLines bool) tea.Cmd {
+ m.wrapLines = wrapLines
+ m.fvp.SetWrapText(wrapLines)
+ return m.diff()
+}
+
+// panResetCols is large enough that a ScrollLeft by it always lands on column 0.
+const panResetCols = 1 << 30
+
+func (m *Model) resetPan() {
+ m.fvp.ScrollLeft(panResetCols)
+}
+
// ScrollUp scrolls the viewport up by the given number of lines.
func (m *Model) ScrollUp(lines int) {
m.fvp.ScrollUp(lines)
@@ -443,7 +503,7 @@ func (m *Model) ScrollLeft(cols int) {
m.fvp.ScrollLeft(cols)
}
-// ScrollRight scrolls the viewport one column away from column 0.
+// ScrollRight scrolls the viewport right by the given number of columns.
func (m *Model) ScrollRight(cols int) {
m.fvp.ScrollRight(cols)
}
@@ -454,8 +514,8 @@ func (m *Model) diffFile(node *cachedNode, width int) tea.Cmd {
}
file := node.files[0]
- sideBySide := m.sideBySide && !file.IsNew && !file.IsDelete
- key := cacheKey(node.path, sideBySide)
+ sideBySide := m.renderSideBySide() && !file.IsNew && !file.IsDelete
+ key := cacheKey(node.path, sideBySide, m.wrapLines)
return func() tea.Msg {
args := m.makeDeltaArgs(
sideBySide,
@@ -479,7 +539,7 @@ func (m *Model) diffDir(dir *cachedNode, width int, sideBySide bool, preamble st
if width == 0 || dir == nil {
return nil
}
- key := cacheKey(dir.path, sideBySide)
+ key := cacheKey(dir.path, sideBySide, m.wrapLines)
return func() tea.Msg {
args := m.makeDeltaArgs(sideBySide, width, deltaOpts{})
log.Info("executing delta", "cmd", fmt.Sprintf(`delta %s`, strings.Join(args, " ")))
@@ -549,11 +609,7 @@ func (m *Model) ClearCache() {
}
func (m *Model) RootDiffStats() (int64, int64) {
- if item, ok := m.cache[cacheKey("/", m.sideBySide)]; ok {
- return item.additions, item.deletions
- }
-
- return 0, 0
+ return m.rootAdditions, m.rootDeletions
}
func (m *Model) Searching() bool {
@@ -609,6 +665,12 @@ func (m *Model) makeDeltaArgs(sideBySide bool, width int, opts deltaOpts) []stri
minusLine := common.LipglossColorToHex(theme.BrightRed)
red := common.LipglossColorToHex(lipgloss.Darken(theme.Red, 0.8))
brightRed := common.LipglossColorToHex(lipgloss.Darken(theme.BrightRed, 0.5))
+ // zero means delta never wraps; with --max-line-length=0 the full line is
+ // emitted so the viewport can scroll it horizontally
+ wrapMaxLines := "unlimited"
+ if !m.wrapLines {
+ wrapMaxLines = "0"
+ }
args := []string{
"--paging=never",
"--line-numbers",
@@ -633,7 +695,7 @@ func (m *Model) makeDeltaArgs(sideBySide bool, width int, opts deltaOpts) []stri
fmt.Sprintf("--file-decoration-style='\"%s\" box %s'", selectionColor, selectionColor),
fmt.Sprintf("-w=%d", width),
"--max-line-length=0",
- "--wrap-max-lines=unlimited",
+ fmt.Sprintf("--wrap-max-lines=%s", wrapMaxLines),
fmt.Sprintf(
"--line-numbers-right-style='\"%s\" dim'",
common.LipglossColorToHex(m.Styles.Colors.FaintBlue()),
diff --git a/pkg/ui/panes/diffviewer/diffviewer_test.go b/pkg/ui/panes/diffviewer/diffviewer_test.go
index 4047473..f26edcf 100644
--- a/pkg/ui/panes/diffviewer/diffviewer_test.go
+++ b/pkg/ui/panes/diffviewer/diffviewer_test.go
@@ -1,16 +1,18 @@
package diffviewer
import (
+ "slices"
"strings"
"testing"
+ "github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/charmbracelet/x/ansi"
"github.com/dlvhdr/diffnav/pkg/ui/common"
)
func TestRenderPreamble_Empty(t *testing.T) {
common.RegisterSupportedTints()
- m := New(false, &common.Styles{
+ m := New(false, true, &common.Styles{
Tint: common.Themes.Current(),
Colors: common.Colors{},
})
@@ -24,7 +26,7 @@ func TestRenderPreamble_Empty(t *testing.T) {
func TestRenderPreamble_GitShow(t *testing.T) {
common.RegisterSupportedTints()
- m := New(false, &common.Styles{
+ m := New(false, true, &common.Styles{
Tint: common.Themes.Current(),
Colors: common.Colors{},
})
@@ -62,7 +64,7 @@ Date: Mon Jan 1 00:00:00 2026 +0000
Merge branch 'feature' into main`
common.RegisterSupportedTints()
- m := New(false, &common.Styles{
+ m := New(false, true, &common.Styles{
Tint: common.Themes.Current(),
Colors: common.Colors{},
})
@@ -78,3 +80,130 @@ Date: Mon Jan 1 00:00:00 2026 +0000
}
}
}
+
+func TestMakeDeltaArgs_WrapLines(t *testing.T) {
+ common.RegisterSupportedTints()
+ styles := common.MakeStyles()
+
+ wrapped := New(false, true, &styles)
+ if args := wrapped.makeDeltaArgs(false, 80, deltaOpts{}); !slices.Contains(
+ args,
+ "--wrap-max-lines=unlimited",
+ ) {
+ t.Errorf("expected wrapping on, got %v", args)
+ }
+
+ unwrapped := New(false, false, &styles)
+ args := unwrapped.makeDeltaArgs(false, 80, deltaOpts{})
+ if !slices.Contains(args, "--wrap-max-lines=0") {
+ t.Errorf("expected wrapping off, got %v", args)
+ }
+ // the full line must survive so the viewport can scroll it horizontally
+ if !slices.Contains(args, "--max-line-length=0") {
+ t.Errorf("expected no truncation, got %v", args)
+ }
+}
+
+func TestRenderSideBySide_RequiresWrap(t *testing.T) {
+ common.RegisterSupportedTints()
+ styles := common.MakeStyles()
+
+ if m := New(true, false, &styles); m.renderSideBySide() {
+ t.Error("side-by-side must not render when wrapping is off")
+ }
+ if m := New(true, true, &styles); !m.renderSideBySide() {
+ t.Error("expected side-by-side when wrapping is on")
+ }
+}
+
+func TestCacheKey_SeparatesWrapMode(t *testing.T) {
+ wrapOn := cacheKey("file.txt", false, true)
+ wrapOff := cacheKey("file.txt", false, false)
+ if wrapOn == wrapOff {
+ t.Errorf("expected distinct cache keys, got %q for both modes", wrapOn)
+ }
+}
+
+func TestRootDiffStats_SurvivesWrapToggle(t *testing.T) {
+ common.RegisterSupportedTints()
+ styles := common.MakeStyles()
+
+ files, _, err := gitdiff.Parse(strings.NewReader(
+ "diff --git a/test.txt b/test.txt\n" +
+ "index 1111111..2222222 100644\n" +
+ "--- a/test.txt\n" +
+ "+++ b/test.txt\n" +
+ "@@ -1,2 +1,2 @@\n" +
+ "-short\n" +
+ "-short2\n" +
+ "+long line\n" +
+ "+long line2\n",
+ ))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ m := New(true, true, &styles)
+ m, _ = m.SetDirPatch("/", files)
+ m, _ = m.SetFilePatch(files[0])
+
+ if additions, deletions := m.RootDiffStats(); additions != 2 || deletions != 2 {
+ t.Errorf("expected +2/-2 root stats, got +%d/-%d", additions, deletions)
+ }
+
+ _ = m.SetWrapLines(false)
+ if additions, deletions := m.RootDiffStats(); additions != 2 || deletions != 2 {
+ t.Errorf("expected stats to survive wrap toggle, got +%d/-%d", additions, deletions)
+ }
+}
+
+func TestSetWrapLines_TogglesViewportWrap(t *testing.T) {
+ common.RegisterSupportedTints()
+ styles := common.MakeStyles()
+
+ m := New(false, true, &styles)
+ _ = m.SetWrapLines(false)
+ if m.fvp.GetWrapText() {
+ t.Error("expected viewport wrapping off after SetWrapLines(false)")
+ }
+ _ = m.SetWrapLines(true)
+ if !m.fvp.GetWrapText() {
+ t.Error("expected viewport wrapping on after SetWrapLines(true)")
+ }
+}
+
+func TestUpdate_IgnoresStaleDiffContentMsg(t *testing.T) {
+ common.RegisterSupportedTints()
+ styles := common.MakeStyles()
+
+ files, _, err := gitdiff.Parse(strings.NewReader(
+ "diff --git a/test.txt b/test.txt\n" +
+ "index 1111111..2222222 100644\n" +
+ "--- a/test.txt\n" +
+ "+++ b/test.txt\n" +
+ "@@ -1 +1 @@\n" +
+ "-short\n" +
+ "+long line\n",
+ ))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ m := New(true, true, &styles)
+ m, _ = m.SetFilePatch(files[0])
+
+ m.fvp.SetWidth(40)
+ m.fvp.SetHeight(10)
+
+ fresh := stringToDiffLines("fresh content")
+ m, _ = m.Update(diffContentMsg{cacheKey: m.currentCacheKey(), lines: fresh})
+ if !strings.Contains(m.fvp.View(), "fresh content") {
+ t.Error("expected current-key render to be shown")
+ }
+
+ stale := stringToDiffLines("stale render that must be dropped")
+ m, _ = m.Update(diffContentMsg{cacheKey: "other:node", lines: stale})
+ if strings.Contains(m.fvp.View(), "stale render") {
+ t.Error("stale render must not replace the displayed content")
+ }
+}
diff --git a/pkg/ui/tui.go b/pkg/ui/tui.go
index 0930af1..3644d48 100644
--- a/pkg/ui/tui.go
+++ b/pkg/ui/tui.go
@@ -90,6 +90,7 @@ type mainModel struct {
draggingSidebar bool
iconStyle string
sideBySide bool
+ wrapLines bool
help help.Model
helpOpen bool
messageOpen bool
@@ -134,6 +135,7 @@ func New(opts ModelOpts, cfg config.Config) mainModel {
config: cfg,
iconStyle: cfg.UI.Icons,
sideBySide: cfg.UI.SideBySide,
+ wrapLines: cfg.UI.WrapLines,
watchEnabled: cfg.Watch.Enabled,
watchCmd: cfg.Watch.Cmd,
watchInterval: cfg.Watch.Interval,
@@ -142,7 +144,7 @@ func New(opts ModelOpts, cfg config.Config) mainModel {
}
m.fileTree = filetree.New(cfg, &m.styles)
m.fileTree.SetSize(cfg.UI.FileTreeWidth, 0)
- m.diffViewer = diffviewer.New(cfg.UI.SideBySide, &m.styles)
+ m.diffViewer = diffviewer.New(cfg.UI.SideBySide, cfg.UI.WrapLines, &m.styles)
m.themePicker = themepicker.New(&m.styles)
m.help = help.New()
m.help.SetKeys(KeyGroups())
@@ -361,8 +363,12 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cycleIconStyle()
case key.Matches(msg, keys.ToggleDiffView):
m.sideBySide = !m.sideBySide
- cmd = m.diffViewer.SetSideBySide(m.sideBySide)
- cmds = append(cmds, cmd)
+ cmds = append(cmds, m.diffViewer.SetSideBySide(m.sideBySide))
+ if m.sideBySide && !m.wrapLines {
+ // side-by-side needs wrapping; delta truncates overflow otherwise
+ m.wrapLines = true
+ cmds = append(cmds, m.diffViewer.SetWrapLines(true))
+ }
case key.Matches(msg, keys.SwitchPanel):
if m.isShowingFileTree {
if m.activePanel == FileTreePanel {
@@ -414,13 +420,14 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.diffViewer.ScrollTop()
}
case key.Matches(msg, keys.ScrollLeft):
- if m.activePanel != FileTreePanel {
- m.diffViewer.ScrollLeft(1)
- }
+ // the file tree binds h/l for expand/collapse, so the arrows are
+ // free to pan the diff from either panel
+ m.diffViewer.ScrollLeft(m.hScrollStep())
case key.Matches(msg, keys.ScrollRight):
- if m.activePanel != FileTreePanel {
- m.diffViewer.ScrollRight(1)
- }
+ m.diffViewer.ScrollRight(m.hScrollStep())
+ case key.Matches(msg, keys.ToggleWrapLines):
+ m.wrapLines = !m.wrapLines
+ cmds = append(cmds, m.diffViewer.SetWrapLines(m.wrapLines))
case key.Matches(msg, keys.Copy):
cmd = m.fileTree.CopyCurrNodePath()
if cmd != nil {
@@ -1110,6 +1117,11 @@ func (m mainModel) resultsView() string {
return sb.String()
}
+// hScrollStep is how many columns the diff view pans per horizontal scroll keypress.
+func (m mainModel) hScrollStep() int {
+ return max(1, (m.width-m.sidebarWidth())/4)
+}
+
func (m mainModel) sidebarWidth() int {
if m.searchingFiles {
return m.config.UI.SearchTreeWidth
@@ -1245,7 +1257,10 @@ func (m mainModel) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
}
// Handle scroll wheel first.
- if msg.Mouse().Button == tea.MouseWheelUp || msg.Mouse().Button == tea.MouseWheelDown {
+ if button := msg.Mouse().Button; button == tea.MouseWheelUp ||
+ button == tea.MouseWheelDown ||
+ button == tea.MouseWheelLeft ||
+ button == tea.MouseWheelRight {
return m.handleScroll(msg)
}
@@ -1376,16 +1391,18 @@ func (m mainModel) handleFileTreeClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
func (m mainModel) handleScroll(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
lines := scrollLines
+ button := msg.Mouse().Button
// Check if scrolling in sidebar (file tree or search results).
if zone.Get(zoneFileTree).InBounds(msg) || zone.Get(zoneSearchResults).InBounds(msg) {
- if msg.Mouse().Button == tea.MouseWheelUp {
+ switch button {
+ case tea.MouseWheelUp:
if m.searchingFiles {
m.resultsVp.ScrollUp(lines)
} else {
m.fileTree.ScrollUp(lines)
}
- } else {
+ case tea.MouseWheelDown:
if m.searchingFiles {
m.resultsVp.ScrollDown(lines)
} else {
@@ -1397,10 +1414,15 @@ func (m mainModel) handleScroll(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
// Check if scrolling in diff viewer.
if zone.Get(zoneDiffViewer).InBounds(msg) {
- if msg.Mouse().Button == tea.MouseWheelUp {
+ switch button {
+ case tea.MouseWheelUp:
m.diffViewer.ScrollUp(lines)
- } else {
+ case tea.MouseWheelDown:
m.diffViewer.ScrollDown(lines)
+ case tea.MouseWheelLeft:
+ m.diffViewer.ScrollLeft(lines)
+ case tea.MouseWheelRight:
+ m.diffViewer.ScrollRight(lines)
}
}
return m, nil