From 96fa098845e8973342c622bb1ec91212fb6f0ae6 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 20 Jun 2026 20:55:04 +0200 Subject: [PATCH] Anchor a drag-selection at the clicked line in the focused main view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- pkg/gui/context/main_context.go | 15 ++++++++++ pkg/gui/controllers/main_view_controller.go | 33 +++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/pkg/gui/context/main_context.go b/pkg/gui/context/main_context.go index 10c3c445d..d32b53a2d 100644 --- a/pkg/gui/context/main_context.go +++ b/pkg/gui/context/main_context.go @@ -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, diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 99e2f97e4..8d31bd882 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -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() {