Anchor a drag-selection at the clicked line in the focused main view

Click-and-drag in the focused main view turns a hunk selection into a range,
but it was anchored at the change block's far end — where selecting a hunk
leaves the range anchor — rather than the clicked line. And on a context
line, where the click leaves no range anchor at all, dragging just moved the
single selected line instead of opening a range. Remember the line each
mouse-down lands on (the click can show a whole hunk, so it can't be read
back from the view) and, as the drag proceeds, anchor the range there while
the cursor end follows the mouse as gocui already moves it. Works for a click
that focuses the view and for one while it's already focused, on change and
context lines alike.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-06-20 20:55:04 +02:00
parent 79ce7f3382
commit 96fa098845
2 changed files with 48 additions and 0 deletions

View file

@ -10,6 +10,10 @@ type MainContext struct {
*SearchTrait
diffSelect DiffSelectState
// dragAnchorViewLine is the view line a mouse-down landed on, remembered so a drag
// that follows can anchor its range there. The click can show a whole hunk (a range
// with a different anchor), so the clicked line can't be read back from the view.
dragAnchorViewLine int
}
var _ types.ISearchableContext = (*MainContext)(nil)
@ -48,6 +52,17 @@ func (self *MainContext) DiffSelectState() *DiffSelectState {
return &self.diffSelect
}
// SetDragAnchorViewLine records the view line a mouse-down landed on, so a drag that
// follows can anchor its range there (see dragAnchorViewLine).
func (self *MainContext) SetDragAnchorViewLine(viewLine int) {
self.dragAnchorViewLine = viewLine
}
// DragAnchorViewLine returns the view line the last mouse-down landed on.
func (self *MainContext) DragAnchorViewLine() int {
return self.dragAnchorViewLine
}
func NewMainContext(
view *gocui.View,
windowName string,

View file

@ -182,6 +182,14 @@ func (self *MainViewController) GetMouseKeybindings(opts types.KeybindingsOpts)
Handler: self.onClickInOtherViewOfMainViewPair,
FocusedView: self.otherContext.GetViewName(),
},
{
// Dragging after a click extends a range selection from the clicked line.
ViewName: self.context.GetViewName(),
Key: gocui.MouseLeft,
Modifier: gocui.ModMotion,
Handler: self.onDragInFocusedView,
FocusedView: self.context.GetViewName(),
},
{
// Alt- or shift-click anywhere on a diff line opens it in the editor,
// without focusing the view or creating a selection. Two modifiers
@ -296,6 +304,9 @@ func placeOrHideInitialDiffSelection(c *ControllerCommon, mainContext *context.M
return
}
if clickedViewLine >= 0 {
// Remember where the click landed so a drag that follows anchors its range there,
// even when the click selects a whole hunk (whose anchor is the block's far end).
mainContext.SetDragAnchorViewLine(clickedViewLine)
if c.UserConfig().Gui.UseHunkModeInStagingView && c.Helpers().Staging.IsChangeLine(view, clickedViewLine) {
mainContext.DiffSelectState().Mode = context.DiffSelectModeHunk
selectDiffHunk(c, mainContext, clickedViewLine)
@ -998,6 +1009,9 @@ func (self *MainViewController) selectClickedDiffLine(opts gocui.ViewMouseBindin
return nil
}
view := self.context.GetView()
// Remember where the click landed so a drag that follows anchors its range there,
// even when this click selects a whole hunk (whose anchor is the block's far end).
self.context.SetDragAnchorViewLine(opts.Y)
if self.sel().Mode == context.DiffSelectModeHunk && self.c.Helpers().Staging.IsChangeLine(view, opts.Y) {
self.selectHunkAround(opts.Y)
} else {
@ -1010,6 +1024,25 @@ func (self *MainViewController) selectClickedDiffLine(opts gocui.ViewMouseBindin
return nil
}
// onDragInFocusedView extends a range selection as the mouse is dragged after a click,
// anchored at the line the click landed on (DragAnchorViewLine) — not wherever the click
// left the selection, since a click can select a whole hunk whose far end would otherwise
// become the anchor. Dragging turns hunk mode off: you get a plain range from the clicked
// line to the line under the cursor, which gocui has already moved here. A no-op when the
// view holds no selectable diff.
func (self *MainViewController) onDragInFocusedView(gocui.ViewMouseBindingOpts) error {
view := self.context.GetView()
if !self.isDiffView() || !view.Highlight {
return nil
}
sel := self.sel()
sel.Mode = context.DiffSelectModeRange
sel.RangeIsSticky = false
sel.UserEnabledHunkMode = false
view.SetRangeSelectStart(self.context.DragAnchorViewLine())
return nil
}
func (self *MainViewController) openSearch() error {
if manager := self.c.GetViewBufferManagerForView(self.context.GetView()); manager != nil {
manager.ReadToEnd(func() {