summaryrefslogtreecommitdiffstats
path: root/pkg/gui/context/traits/list_cursor.go
blob: 368485c057286079a3a55b3ad7fc39a61c8722a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package traits

import (
	"github.com/jesseduffield/lazygit/pkg/gui/types"
	"github.com/jesseduffield/lazygit/pkg/utils"
)

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
	rangeSelectMode RangeSelectMode
	// value is ignored when rangeSelectMode is RangeSelectModeNone
	rangeStartIdx int
	list          HasLength
}

func NewListCursor(list HasLength) *ListCursor {
	return &ListCursor{
		selectedIdx:     0,
		rangeStartIdx:   0,
		rangeSelectMode: RangeSelectModeNone,
		list:            list,
	}
}

var _ types.IListCursor = (*ListCursor)(nil)

func (self *ListCursor) GetSelectedLineIdx() int {
	return self.selectedIdx
}

// Sets the selected line index. Note, you probably don't want to use this directly,
// because it doesn't affect the range select mode or range start index. You should only
// use this for navigation situations where e.g. the user wants to jump to the top of
// a list while in range select mode so that the selection ends up being between
// the top of the list and the previous selection
func (self *ListCursor) SetSelectedLineIdx(value int) {
	self.selectedIdx = self.clampValue(value)
}

// Sets the selected index and cancels the range. You almost always want to use
// this instead of SetSelectedLineIdx. For example, if you want to jump the cursor
// to the top of a list after checking out a branch, you should use this method,
// or you may end up with a large range selection from the previous cursor position
// to the top of the list.
func (self *ListCursor) SetSelection(value int) {
	self.selectedIdx = self.clampValue(value)
	self.CancelRangeSelect()
}

func (self *ListCursor) clampValue(value int) int {
	clampedValue := -1
	if self.list.Len() > 0 {
		clampedValue = utils.Clamp(value, 0, self.list.Len()-1)
	}

	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, 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 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
}

// Returns true if we are in range select mode. Note that we may be in range select
// mode and still only selecting a single item. See AreMultipleItemsSelected below.
func (self *ListCursor) IsSelectingRange() bool {
	return self.rangeSelectMode != RangeSelectModeNone
}

// Returns true if we are in range select mode and selecting multiple items
func (self *ListCursor) AreMultipleItemsSelected() bool {
	startIdx, endIdx := self.GetSelectionRange()
	return startIdx != endIdx
}

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)
}