]> git.lizzy.rs Git - micro.git/blob - src/cursor.go
Fix half page down bug
[micro.git] / src / cursor.go
1 package main
2
3 import (
4         "strings"
5 )
6
7 // FromCharPos converts from a character position to an x, y position
8 func FromCharPos(loc int, buf *Buffer) (int, int) {
9         charNum := 0
10         x, y := 0, 0
11
12         for charNum+Count(buf.lines[y])+1 <= loc {
13                 charNum += Count(buf.lines[y]) + 1
14                 y++
15         }
16         x = loc - charNum
17
18         return x, y
19 }
20
21 // ToCharPos converts from an x, y position to a character position
22 func ToCharPos(x, y int, buf *Buffer) int {
23         loc := 0
24         for i := 0; i < y; i++ {
25                 // + 1 for the newline
26                 loc += Count(buf.lines[i]) + 1
27         }
28         loc += x
29         return loc
30 }
31
32 // The Cursor struct stores the location of the cursor in the view
33 // The complicated part about the cursor is storing its location.
34 // The cursor must be displayed at an x, y location, but since the buffer
35 // uses a rope to store text, to insert text we must have an index. It
36 // is also simpler to use character indicies for other tasks such as
37 // selection.
38 type Cursor struct {
39         v *View
40
41         // The cursor display location
42         x int
43         y int
44
45         // Last cursor x position
46         lastVisualX int
47
48         // The current selection as a range of character numbers (inclusive)
49         curSelection [2]int
50         // The original selection as a range of character numbers
51         // This is used for line and word selection where it is necessary
52         // to know what the original selection was
53         origSelection [2]int
54 }
55
56 // SetLoc sets the location of the cursor in terms of character number
57 // and not x, y location
58 // It's just a simple wrapper of FromCharPos
59 func (c *Cursor) SetLoc(loc int) {
60         c.x, c.y = FromCharPos(loc, c.v.buf)
61 }
62
63 // Loc gets the cursor location in terms of character number instead
64 // of x, y location
65 // It's just a simple wrapper of ToCharPos
66 func (c *Cursor) Loc() int {
67         return ToCharPos(c.x, c.y, c.v.buf)
68 }
69
70 // ResetSelection resets the user's selection
71 func (c *Cursor) ResetSelection() {
72         c.curSelection[0] = 0
73         c.curSelection[1] = 0
74 }
75
76 // HasSelection returns whether or not the user has selected anything
77 func (c *Cursor) HasSelection() bool {
78         return c.curSelection[1] != c.curSelection[0]
79 }
80
81 // DeleteSelection deletes the currently selected text
82 func (c *Cursor) DeleteSelection() {
83         if c.curSelection[0] > c.curSelection[1] {
84                 c.v.eh.Remove(c.curSelection[1], c.curSelection[0]+1)
85                 c.SetLoc(c.curSelection[1])
86         } else {
87                 c.v.eh.Remove(c.curSelection[0], c.curSelection[1]+1)
88                 c.SetLoc(c.curSelection[0])
89         }
90 }
91
92 // GetSelection returns the cursor's selection
93 func (c *Cursor) GetSelection() string {
94         if c.curSelection[0] > c.curSelection[1] {
95                 return string([]rune(c.v.buf.text)[c.curSelection[1] : c.curSelection[0]+1])
96         }
97         return string([]rune(c.v.buf.text)[c.curSelection[0] : c.curSelection[1]+1])
98 }
99
100 // SelectLine selects the current line
101 func (c *Cursor) SelectLine() {
102         c.Start()
103         c.curSelection[0] = c.Loc()
104         c.End()
105         c.curSelection[1] = c.Loc()
106
107         c.origSelection = c.curSelection
108 }
109
110 // AddLineToSelection adds the current line to the selection
111 func (c *Cursor) AddLineToSelection() {
112         loc := c.Loc()
113
114         if loc < c.origSelection[0] {
115                 c.Start()
116                 c.curSelection[0] = c.Loc()
117                 c.curSelection[1] = c.origSelection[1]
118         }
119         if loc > c.origSelection[1] {
120                 c.End()
121                 c.curSelection[1] = c.Loc()
122                 c.curSelection[0] = c.origSelection[0]
123         }
124
125         if loc < c.origSelection[1] && loc > c.origSelection[0] {
126                 c.curSelection = c.origSelection
127         }
128 }
129
130 // SelectWord selects the word the cursor is currently on
131 func (c *Cursor) SelectWord() {
132         if len(c.v.buf.lines[c.y]) == 0 {
133                 return
134         }
135
136         if !IsWordChar(string(c.RuneUnder(c.x))) {
137                 loc := c.Loc()
138                 c.curSelection[0] = loc
139                 c.curSelection[1] = loc
140                 c.origSelection = c.curSelection
141                 return
142         }
143
144         forward, backward := c.x, c.x
145
146         for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
147                 backward--
148         }
149
150         c.curSelection[0] = ToCharPos(backward, c.y, c.v.buf)
151         c.origSelection[0] = c.curSelection[0]
152
153         for forward < Count(c.v.buf.lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
154                 forward++
155         }
156
157         c.curSelection[1] = ToCharPos(forward, c.y, c.v.buf)
158         c.origSelection[1] = c.curSelection[1]
159 }
160
161 // AddWordToSelection adds the word the cursor is currently on to the selection
162 func (c *Cursor) AddWordToSelection() {
163         loc := c.Loc()
164
165         if loc > c.origSelection[0] && loc < c.origSelection[1] {
166                 c.curSelection = c.origSelection
167                 return
168         }
169
170         if loc < c.origSelection[0] {
171                 backward := c.x
172
173                 for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
174                         backward--
175                 }
176
177                 c.curSelection[0] = ToCharPos(backward, c.y, c.v.buf)
178                 c.curSelection[1] = c.origSelection[1]
179         }
180
181         if loc > c.origSelection[1] {
182                 forward := c.x
183
184                 for forward < Count(c.v.buf.lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
185                         forward++
186                 }
187
188                 c.curSelection[1] = ToCharPos(forward, c.y, c.v.buf)
189                 c.curSelection[0] = c.origSelection[0]
190         }
191 }
192
193 // RuneUnder returns the rune under the given x position
194 func (c *Cursor) RuneUnder(x int) rune {
195         line := []rune(c.v.buf.lines[c.y])
196         if x >= len(line) {
197                 x = len(line) - 1
198         } else if x < 0 {
199                 x = 0
200         }
201         return line[x]
202 }
203
204 // Up moves the cursor up one line (if possible)
205 func (c *Cursor) Up() {
206         if c.y > 0 {
207                 c.y--
208
209                 runes := []rune(c.v.buf.lines[c.y])
210                 c.x = c.GetCharPosInLine(c.y, c.lastVisualX)
211                 if c.x > len(runes) {
212                         c.x = len(runes)
213                 }
214         }
215 }
216
217 // Down moves the cursor down one line (if possible)
218 func (c *Cursor) Down() {
219         if c.y < len(c.v.buf.lines)-1 {
220                 c.y++
221
222                 runes := []rune(c.v.buf.lines[c.y])
223                 c.x = c.GetCharPosInLine(c.y, c.lastVisualX)
224                 if c.x > len(runes) {
225                         c.x = len(runes)
226                 }
227         }
228 }
229
230 // Left moves the cursor left one cell (if possible) or to the last line if it is at the beginning
231 func (c *Cursor) Left() {
232         if c.Loc() == 0 {
233                 return
234         }
235         if c.x > 0 {
236                 c.x--
237         } else {
238                 c.Up()
239                 c.End()
240         }
241         c.lastVisualX = c.GetVisualX()
242 }
243
244 // Right moves the cursor right one cell (if possible) or to the next line if it is at the end
245 func (c *Cursor) Right() {
246         if c.Loc() == c.v.buf.Len() {
247                 return
248         }
249         if c.x < Count(c.v.buf.lines[c.y]) {
250                 c.x++
251         } else {
252                 c.Down()
253                 c.Start()
254         }
255         c.lastVisualX = c.GetVisualX()
256 }
257
258 // End moves the cursor to the end of the line it is on
259 func (c *Cursor) End() {
260         c.x = Count(c.v.buf.lines[c.y])
261         c.lastVisualX = c.GetVisualX()
262 }
263
264 // Start moves the cursor to the start of the line it is on
265 func (c *Cursor) Start() {
266         c.x = 0
267         c.lastVisualX = c.GetVisualX()
268 }
269
270 // GetCharPosInLine gets the char position of a visual x y coordinate (this is necessary because tabs are 1 char but 4 visual spaces)
271 func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int {
272         // Get the tab size
273         tabSize := settings.TabSize
274         // This is the visual line -- every \t replaced with the correct number of spaces
275         visualLine := strings.Replace(c.v.buf.lines[lineNum], "\t", "\t"+Spaces(tabSize-1), -1)
276         if visualPos > Count(visualLine) {
277                 visualPos = Count(visualLine)
278         }
279         numTabs := NumOccurences(visualLine[:visualPos], '\t')
280         if visualPos >= (tabSize-1)*numTabs {
281                 return visualPos - (tabSize-1)*numTabs
282         }
283         return visualPos / tabSize
284 }
285
286 // GetVisualX returns the x value of the cursor in visual spaces
287 func (c *Cursor) GetVisualX() int {
288         runes := []rune(c.v.buf.lines[c.y])
289         tabSize := settings.TabSize
290         return c.x + NumOccurences(string(runes[:c.x]), '\t')*(tabSize-1)
291 }
292
293 // Display draws the cursor to the screen at the correct position
294 func (c *Cursor) Display() {
295         // Don't draw the cursor if it is out of the viewport or if it has a selection
296         if (c.y-c.v.topline < 0 || c.y-c.v.topline > c.v.height-1) || c.HasSelection() {
297                 screen.HideCursor()
298         } else {
299                 screen.ShowCursor(c.GetVisualX()+c.v.lineNumOffset-c.v.leftCol, c.y-c.v.topline)
300         }
301 }