diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2024-01-07 19:44:19 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2024-01-19 10:47:21 +1100 |
commit | 24a4302c528e3a11b30855c006669de1adf9e1d4 (patch) | |
tree | 4b8053168746c2978c2b47968bae1576d3718bcf /pkg/gui/context | |
parent | e887a2eb3c0ece1d2be78b869ff936a0703a3940 (diff) |
Add range selection ability on list contexts
This adds range select ability in two ways:
1) Sticky: like what we already have with the staging view i.e. press v then use arrow keys
2) Non-sticky: where you just use shift+up/down to expand the range
The state machine works like this:
(no range, press 'v') -> sticky range
(no range, press arrow) -> no range
(no range, press shift+arrow) -> nonsticky range
(sticky range, press 'v') -> no range
(sticky range, press arrow) -> sticky range
(sticky range, press shift+arrow) -> nonsticky range
(nonsticky range, press 'v') -> no range
(nonsticky range, press arrow) -> no range
(nonsticky range, press shift+arrow) -> nonsticky range
Diffstat (limited to 'pkg/gui/context')
-rw-r--r-- | pkg/gui/context/list_context_trait.go | 10 | ||||
-rw-r--r-- | pkg/gui/context/traits/list_cursor.go | 103 | ||||
-rw-r--r-- | pkg/gui/context/view_trait.go | 8 |
3 files changed, 110 insertions, 11 deletions
diff --git a/pkg/gui/context/list_context_trait.go b/pkg/gui/context/list_context_trait.go index ca3b3254f..c4f0e2549 100644 --- a/pkg/gui/context/list_context_trait.go +++ b/pkg/gui/context/list_context_trait.go @@ -32,6 +32,14 @@ func (self *ListContextTrait) FocusLine() { self.GetViewTrait().FocusPoint( self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx())) + selectRangeIndex, isSelectingRange := self.list.GetRangeStartIdx() + if isSelectingRange { + selectRangeIndex = self.ModelIndexToViewIndex(selectRangeIndex) + self.GetViewTrait().SetRangeSelectStart(selectRangeIndex) + } else { + self.GetViewTrait().CancelRangeSelect() + } + // If FocusPoint() caused the view to scroll (because the selected line // was out of view before), we need to rerender the view port again. // This can happen when pressing , or . to scroll by pages, or < or > to @@ -84,7 +92,7 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error // OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view func (self *ListContextTrait) HandleRender() error { - self.list.RefreshSelectedIdx() + self.list.ClampSelection() content := self.renderLines(-1, -1) self.GetViewTrait().SetContent(content) self.c.Render() diff --git a/pkg/gui/context/traits/list_cursor.go b/pkg/gui/context/traits/list_cursor.go index 9e86d5139..647f2a36b 100644 --- a/pkg/gui/context/traits/list_cursor.go +++ b/pkg/gui/context/traits/list_cursor.go @@ -9,13 +9,34 @@ type HasLength interface { Len() int } +type RangeSelectMode int + +const ( + // None means we are not selecting a range + RangeSelectModeNone RangeSelectMode = iota + // Sticky range select is started by pressing 'v', then the range is expanded + // when you move up or down. It is cancelled by pressing 'v' again. + RangeSelectModeSticky + // Nonsticky range select is started by pressing shift+arrow and cancelled + // when pressing up/down without shift, or by pressing 'v' + RangeSelectModeNonSticky +) + type ListCursor struct { - selectedIdx int - list HasLength + selectedIdx int + rangeSelectMode RangeSelectMode + // value is ignored when rangeSelectMode is RangeSelectModeNone + rangeStartIdx int + list HasLength } func NewListCursor(list HasLength) *ListCursor { - return &ListCursor{selectedIdx: 0, list: list} + return &ListCursor{ + selectedIdx: 0, + rangeStartIdx: 0, + rangeSelectMode: RangeSelectModeNone, + list: list, + } } var _ types.IListCursor = (*ListCursor)(nil) @@ -25,24 +46,86 @@ func (self *ListCursor) GetSelectedLineIdx() int { } func (self *ListCursor) SetSelectedLineIdx(value int) { + self.selectedIdx = self.clampValue(value) +} + +func (self *ListCursor) clampValue(value int) int { clampedValue := -1 if self.list.Len() > 0 { clampedValue = utils.Clamp(value, 0, self.list.Len()-1) } - self.selectedIdx = clampedValue + return clampedValue +} + +// Moves the cursor up or down by the given amount. +// If we are in non-sticky range select mode, this will cancel the range select +func (self *ListCursor) MoveSelectedLine(change int) { + if self.rangeSelectMode == RangeSelectModeNonSticky { + self.CancelRangeSelect() + } + + self.SetSelectedLineIdx(self.selectedIdx + change) } -// moves the cursor up or down by the given amount -func (self *ListCursor) MoveSelectedLine(delta int) { - self.SetSelectedLineIdx(self.selectedIdx + delta) +// Moves the cursor up or down by the given amount, and also moves the range start +// index by the same amount +func (self *ListCursor) MoveSelection(delta int) { + self.selectedIdx = self.clampValue(self.selectedIdx + delta) + if self.IsSelectingRange() { + self.rangeStartIdx = self.clampValue(self.rangeStartIdx + delta) + } } -// to be called when the model might have shrunk so that our selection is not not out of bounds -func (self *ListCursor) RefreshSelectedIdx() { - self.SetSelectedLineIdx(self.selectedIdx) +// To be called when the model might have shrunk so that our selection is not out of bounds +func (self *ListCursor) ClampSelection() { + self.selectedIdx = self.clampValue(self.selectedIdx) + self.rangeStartIdx = self.clampValue(self.rangeStartIdx) } func (self *ListCursor) Len() int { return self.list.Len() } + +func (self *ListCursor) GetRangeStartIdx() (int, bool) { + if self.IsSelectingRange() { + return self.rangeStartIdx, true + } + + return 0, false +} + +func (self *ListCursor) CancelRangeSelect() { + self.rangeSelectMode = RangeSelectModeNone +} + +func (self *ListCursor) IsSelectingRange() bool { + return self.rangeSelectMode != RangeSelectModeNone +} + +func (self *ListCursor) GetSelectionRange() (int, int) { + if self.IsSelectingRange() { + return utils.MinMax(self.selectedIdx, self.rangeStartIdx) + } + + return self.selectedIdx, self.selectedIdx +} + +func (self *ListCursor) ToggleStickyRange() { + if self.IsSelectingRange() { + self.CancelRangeSelect() + } else { + self.rangeStartIdx = self.selectedIdx + self.rangeSelectMode = RangeSelectModeSticky + } +} + +func (self *ListCursor) ExpandNonStickyRange(change int) { + if !self.IsSelectingRange() { + self.rangeStartIdx = self.selectedIdx + } + + self.rangeSelectMode = RangeSelectModeNonSticky + + self.SetSelectedLineIdx(self.selectedIdx + change) +} diff --git a/pkg/gui/context/view_trait.go b/pkg/gui/context/view_trait.go index bf8a49e43..1179a8b14 100644 --- a/pkg/gui/context/view_trait.go +++ b/pkg/gui/context/view_trait.go @@ -21,6 +21,14 @@ func (self *ViewTrait) FocusPoint(yIdx int) { self.view.FocusPoint(self.view.OriginX(), yIdx) } +func (self *ViewTrait) SetRangeSelectStart(yIdx int) { + self.view.SetRangeSelectStart(yIdx) +} + +func (self *ViewTrait) CancelRangeSelect() { + self.view.CancelRangeSelect() +} + func (self *ViewTrait) SetViewPortContent(content string) { _, y := self.view.Origin() self.view.OverwriteLines(y, content) |