Create a range selection in list views by dragging with the mouse (#5856)

Like in the staging view, drag with the mouse in a list view to create a
non-sticky range selection (including auto-scrolling as the mouse
reaches the view edge).
This commit is contained in:
Stefan Haller
2026-07-31 08:37:13 +02:00
committed by GitHub
3 changed files with 125 additions and 3 deletions
+73 -3
View File
@@ -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 {
@@ -257,6 +267,48 @@ func (self *ListController) HandleClick(opts gocui.ViewMouseBindingOpts) error {
return nil
}
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(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{})
@@ -295,7 +347,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 +364,22 @@ 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,
},
&gocui.ViewMouseBinding{
ViewName: self.context.GetViewName(),
Key: gocui.MouseRelease,
Handler: func(gocui.ViewMouseBindingOpts) error { return self.handleDragRelease() },
},
)
}
return bindings
}
+43
View File
@@ -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)
},
})
@@ -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()