From e7998356be50aecb2ef252035b0c02999ca7905f Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 22 Jul 2026 09:49:50 +0200 Subject: [PATCH 1/2] Create a range selection by dragging with the mouse in list views Dragging with the left button held now extends the selection from the pressed line, exactly like moving with shift+up/down does. We use the non-sticky flavor so that the range collapses on the next plain cursor movement, again matching the keyboard behavior. The binding is only registered for contexts that support range selection in the first place; dragging in other lists continues to do nothing. --- pkg/gui/controllers/list_controller.go | 22 +++++++++++- pkg/integration/tests/ui/range_select.go | 43 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/pkg/gui/controllers/list_controller.go b/pkg/gui/controllers/list_controller.go index b2d45679b..828a9d7db 100644 --- a/pkg/gui/controllers/list_controller.go +++ b/pkg/gui/controllers/list_controller.go @@ -257,6 +257,15 @@ func (self *ListController) HandleClick(opts gocui.ViewMouseBindingOpts) error { return nil } +func (self *ListController) HandleDrag(opts gocui.ViewMouseBindingOpts) error { + list := self.context.GetList() + newSelectedLineIdx := self.context.ViewIndexToModelIndex(opts.Y) + list.ExpandNonStickyRange(newSelectedLineIdx - list.GetSelectedLineIdx()) + + self.context.HandleFocus(types.OnFocusOpts{}) + return nil +} + func (self *ListController) pushContextIfNotFocused() error { if !self.isFocused() { self.c.Context().Push(self.context, types.OnFocusOpts{}) @@ -295,7 +304,7 @@ func (self *ListController) GetKeybindings(opts types.KeybindingsOpts) []*types. } func (self *ListController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { - return []*gocui.ViewMouseBinding{ + bindings := []*gocui.ViewMouseBinding{ { ViewName: self.context.GetViewName(), Key: gocui.MouseWheelUp, @@ -312,4 +321,15 @@ func (self *ListController) GetMouseKeybindings(opts types.KeybindingsOpts) []*g Handler: func(gocui.ViewMouseBindingOpts) error { return self.HandleScrollDown() }, }, } + + if self.context.RangeSelectEnabled() { + bindings = append(bindings, &gocui.ViewMouseBinding{ + ViewName: self.context.GetViewName(), + Key: gocui.MouseLeft, + Modifier: gocui.ModMotion, + Handler: self.HandleDrag, + }) + } + + return bindings } diff --git a/pkg/integration/tests/ui/range_select.go b/pkg/integration/tests/ui/range_select.go index b021ea65d..4c5d8420a 100644 --- a/pkg/integration/tests/ui/range_select.go +++ b/pkg/integration/tests/ui/range_select.go @@ -33,6 +33,7 @@ var RangeSelect = NewIntegrationTest(NewIntegrationTestArgs{ Skip: false, SetupConfig: func(config *config.AppConfig) { config.GetUserConfig().Gui.UseHunkModeInStagingView = false + config.GetUserConfig().Gui.ExpandFocusedSidePanel = true }, SetupRepo: func(shell *Shell) { // We're testing the commits view as our representative list context, @@ -51,6 +52,7 @@ var RangeSelect = NewIntegrationTest(NewIntegrationTestArgs{ } shell.CreateFileAndAdd("file1", "staged\n") shell.UpdateFile("file1", fileContent) + shell.NewBranch("branch1").NewBranch("branch2") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { assertRangeSelectBehaviour := func(v *ViewDriver, focusOtherView func(), lineIdxOfFirstItem int) { @@ -179,5 +181,46 @@ var RangeSelect = NewIntegrationTest(NewIntegrationTestArgs{ PressEnter() assertRangeSelectBehaviour(t.Views().Staging().IsFocused(), func() { t.Views().Staging().PressTab() }, 6) + + t.Views().Branches().Focus() + t.Views().Branches(). + SelectedLines( + Contains("branch2"), + ) + t.Views().Commits(). + ClickAndHold(1, 3). + MouseMoveToView(t.Views().Branches(), 1, 2). + SelectedLines( + Contains("line 1"), + Contains("line 2"), + Contains("line 3"), + Contains("line 4"), + ). + Tap(func() { + t.Views().Branches().SelectedLines( + Contains("branch2"), + ) + }). + MouseRelease() + + t.Views().Branches().Focus() + t.Views().Commits(). + ClickAndHold(1, 0). + SelectedLines( + Contains("line 1"), + ). + RepeatMouseMove(). + SelectedLines( + Contains("line 1"), + ). + MouseMove(1, 3). + SelectedLines( + Contains("line 1"), + Contains("line 2"), + Contains("line 3"), + Contains("line 4"), + ). + MouseRelease(). + Click(1, 0) }, }) From 14434015c739c843b5257116bc63d36531092bd2 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 24 Jul 2026 08:18:30 +0200 Subject: [PATCH 2/2] Keep range drags moving at panel edges Give the list views the same edge autoscroll during drag selection that the staging view already has; the new mouse-release binding stops the autoscroll when the drag ends. --- pkg/gui/controllers/list_controller.go | 68 ++++++++++++++++--- .../tests/ui/range_select_with_autoscroll.go | 9 +++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/pkg/gui/controllers/list_controller.go b/pkg/gui/controllers/list_controller.go index 828a9d7db..8136c6aa9 100644 --- a/pkg/gui/controllers/list_controller.go +++ b/pkg/gui/controllers/list_controller.go @@ -2,6 +2,7 @@ package controllers import ( "github.com/jesseduffield/lazygit/pkg/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -16,18 +17,27 @@ func NewListControllerFactory(c *ControllerCommon) *ListControllerFactory { } func (self *ListControllerFactory) Create(context types.IListContext) *ListController { - return &ListController{ + controller := &ListController{ baseController: baseController{}, c: self.c, context: context, } + controller.dragAutoscroller = helpers.NewDragAutoscroller( + self.c.HelperCommon, + context, + func(int) bool { return context.GetList().IsSelectingRange() }, + controller.handleDragAutoscroll, + ) + return controller } type ListController struct { baseController c *ControllerCommon - context types.IListContext + context types.IListContext + dragAutoscroller *helpers.DragAutoscroller + draggingWithMouse bool } func (self *ListController) Context() types.Context { @@ -258,14 +268,47 @@ func (self *ListController) HandleClick(opts gocui.ViewMouseBindingOpts) error { } func (self *ListController) HandleDrag(opts gocui.ViewMouseBindingOpts) error { + self.draggingWithMouse = true + self.selectRangeThroughViewIndex(opts.Y) + originY, _ := self.context.GetViewTrait().ViewPortYBounds() + self.dragAutoscroller.Update(opts.Y - originY) + return nil +} + +func (self *ListController) selectRangeThroughViewIndex(viewIndex int) { list := self.context.GetList() - newSelectedLineIdx := self.context.ViewIndexToModelIndex(opts.Y) + newSelectedLineIdx := self.context.ViewIndexToModelIndex(viewIndex) list.ExpandNonStickyRange(newSelectedLineIdx - list.GetSelectedLineIdx()) self.context.HandleFocus(types.OnFocusOpts{}) +} + +func (self *ListController) handleDragAutoscroll(viewIndex int) bool { + if !self.context.GetList().IsSelectingRange() { + return false + } + + self.context.SetNeedRerenderVisibleLines() + self.selectRangeThroughViewIndex(viewIndex) + return true +} + +func (self *ListController) handleDragRelease() error { + self.draggingWithMouse = false + self.dragAutoscroller.Cancel() return nil } +func (self *ListController) GetOnFocusLost() func(types.OnFocusLostOpts) { + return func(types.OnFocusLostOpts) { + self.dragAutoscroller.Cancel() + if self.draggingWithMouse { + self.draggingWithMouse = false + self.c.GocuiGui().CancelMouseCapture() + } + } +} + func (self *ListController) pushContextIfNotFocused() error { if !self.isFocused() { self.c.Context().Push(self.context, types.OnFocusOpts{}) @@ -323,12 +366,19 @@ func (self *ListController) GetMouseKeybindings(opts types.KeybindingsOpts) []*g } if self.context.RangeSelectEnabled() { - bindings = append(bindings, &gocui.ViewMouseBinding{ - ViewName: self.context.GetViewName(), - Key: gocui.MouseLeft, - Modifier: gocui.ModMotion, - Handler: self.HandleDrag, - }) + bindings = append(bindings, + &gocui.ViewMouseBinding{ + ViewName: self.context.GetViewName(), + Key: gocui.MouseLeft, + Modifier: gocui.ModMotion, + Handler: self.HandleDrag, + }, + &gocui.ViewMouseBinding{ + ViewName: self.context.GetViewName(), + Key: gocui.MouseRelease, + Handler: func(gocui.ViewMouseBindingOpts) error { return self.handleDragRelease() }, + }, + ) } return bindings diff --git a/pkg/integration/tests/ui/range_select_with_autoscroll.go b/pkg/integration/tests/ui/range_select_with_autoscroll.go index 83d2bcba3..94a4fb4a5 100644 --- a/pkg/integration/tests/ui/range_select_with_autoscroll.go +++ b/pkg/integration/tests/ui/range_select_with_autoscroll.go @@ -17,6 +17,7 @@ var RangeSelectWithAutoscroll = NewIntegrationTest(NewIntegrationTestArgs{ config.GetUserConfig().Gui.UseHunkModeInStagingView = false }, SetupRepo: func(shell *Shell) { + shell.CreateNCommits(40) fileContent := "base\n" shell.CreateFileAndAdd("file1", fileContent) for i := 1; i <= 40; i++ { @@ -25,6 +26,14 @@ var RangeSelectWithAutoscroll = NewIntegrationTest(NewIntegrationTestArgs{ shell.UpdateFile("file1", fileContent) }, Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches().Focus() + t.Views().Commits(). + ClickAndHold(1, 0). + MouseMoveToBottom(1). + OriginYAtLeast(3). + SelectedLineIdxAtLeast(3). + MouseRelease() + t.Views().Files(). Focus(). PressEnter()