]> git.lizzy.rs Git - micro.git/blob - cmd/micro/cursor.go
ec5acbcc89cfec48658b9c83a0485a8934b61c56
[micro.git] / cmd / micro / 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         return FromCharPosStart(0, 0, 0, loc, buf)
10 }
11
12 // FromCharPosStart converts from a character position to an x, y position, starting at the specified character location
13 func FromCharPosStart(startLoc, startX, startY, loc int, buf *Buffer) (int, int) {
14         charNum := startLoc
15         x, y := startX, startY
16
17         lineLen := Count(buf.Lines[y]) + 1
18         for charNum+lineLen <= loc {
19                 charNum += lineLen
20                 y++
21                 if y >= buf.NumLines {
22                         return 0, 0
23                 }
24                 lineLen = Count(buf.Lines[y]) + 1
25         }
26         x = loc - charNum
27
28         return x, y
29 }
30
31 // ToCharPos converts from an x, y position to a character position
32 func ToCharPos(x, y int, buf *Buffer) int {
33         loc := 0
34         for i := 0; i < y; i++ {
35                 // + 1 for the newline
36                 loc += Count(buf.Lines[i]) + 1
37         }
38         loc += x
39         return loc
40 }
41
42 // The Cursor struct stores the location of the cursor in the view
43 // The complicated part about the cursor is storing its location.
44 // The cursor must be displayed at an x, y location, but since the buffer
45 // uses a rope to store text, to insert text we must have an index. It
46 // is also simpler to use character indicies for other tasks such as
47 // selection.
48 type Cursor struct {
49         buf *Buffer
50
51         // The cursor display location
52         X int
53         Y int
54
55         // Last cursor x position
56         LastVisualX int
57
58         // The current selection as a range of character numbers (inclusive)
59         CurSelection [2]int
60         // The original selection as a range of character numbers
61         // This is used for line and word selection where it is necessary
62         // to know what the original selection was
63         OrigSelection [2]int
64 }
65
66 // Clamp makes sure that the cursor is in the bounds of the buffer
67 // It cannot be less than 0 or greater than the buffer length
68 func (c *Cursor) Clamp() {
69         loc := c.Loc()
70         if loc < 0 {
71                 c.SetLoc(0)
72         } else if loc > c.buf.Len() {
73                 c.SetLoc(c.buf.Len())
74         }
75 }
76
77 // SetLoc sets the location of the cursor in terms of character number
78 // and not x, y location
79 // It's just a simple wrapper of FromCharPos
80 func (c *Cursor) SetLoc(loc int) {
81         c.X, c.Y = FromCharPos(loc, c.buf)
82         c.LastVisualX = c.GetVisualX()
83 }
84
85 // Loc gets the cursor location in terms of character number instead
86 // of x, y location
87 // It's just a simple wrapper of ToCharPos
88 func (c *Cursor) Loc() int {
89         return ToCharPos(c.X, c.Y, c.buf)
90 }
91
92 // ResetSelection resets the user's selection
93 func (c *Cursor) ResetSelection() {
94         c.CurSelection[0] = 0
95         c.CurSelection[1] = 0
96 }
97
98 // HasSelection returns whether or not the user has selected anything
99 func (c *Cursor) HasSelection() bool {
100         return c.CurSelection[0] != c.CurSelection[1]
101 }
102
103 // DeleteSelection deletes the currently selected text
104 func (c *Cursor) DeleteSelection() {
105         if c.CurSelection[0] > c.CurSelection[1] {
106                 c.buf.Remove(c.CurSelection[1], c.CurSelection[0])
107                 c.SetLoc(c.CurSelection[1])
108         } else if c.GetSelection() == "" {
109                 return
110         } else {
111                 c.buf.Remove(c.CurSelection[0], c.CurSelection[1])
112                 c.SetLoc(c.CurSelection[0])
113         }
114 }
115
116 // GetSelection returns the cursor's selection
117 func (c *Cursor) GetSelection() string {
118         if c.CurSelection[0] > c.CurSelection[1] {
119                 return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
120         }
121         return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
122 }
123
124 // SelectLine selects the current line
125 func (c *Cursor) SelectLine() {
126         c.Start()
127         c.CurSelection[0] = c.Loc()
128         c.End()
129         if c.buf.NumLines-1 > c.Y {
130                 c.CurSelection[1] = c.Loc() + 1
131         } else {
132                 c.CurSelection[1] = c.Loc()
133         }
134
135         c.OrigSelection = c.CurSelection
136 }
137
138 // AddLineToSelection adds the current line to the selection
139 func (c *Cursor) AddLineToSelection() {
140         loc := c.Loc()
141
142         if loc < c.OrigSelection[0] {
143                 c.Start()
144                 c.CurSelection[0] = c.Loc()
145                 c.CurSelection[1] = c.OrigSelection[1]
146         }
147         if loc > c.OrigSelection[1] {
148                 c.End()
149                 c.CurSelection[1] = c.Loc()
150                 c.CurSelection[0] = c.OrigSelection[0]
151         }
152
153         if loc < c.OrigSelection[1] && loc > c.OrigSelection[0] {
154                 c.CurSelection = c.OrigSelection
155         }
156 }
157
158 // SelectWord selects the word the cursor is currently on
159 func (c *Cursor) SelectWord() {
160         if len(c.buf.Lines[c.Y]) == 0 {
161                 return
162         }
163
164         if !IsWordChar(string(c.RuneUnder(c.X))) {
165                 loc := c.Loc()
166                 c.CurSelection[0] = loc
167                 c.CurSelection[1] = loc + 1
168                 c.OrigSelection = c.CurSelection
169                 return
170         }
171
172         forward, backward := c.X, c.X
173
174         for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
175                 backward--
176         }
177
178         c.CurSelection[0] = ToCharPos(backward, c.Y, c.buf)
179         c.OrigSelection[0] = c.CurSelection[0]
180
181         for forward < Count(c.buf.Lines[c.Y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
182                 forward++
183         }
184
185         c.CurSelection[1] = ToCharPos(forward, c.Y, c.buf) + 1
186         c.OrigSelection[1] = c.CurSelection[1]
187         c.SetLoc(c.CurSelection[1])
188 }
189
190 // AddWordToSelection adds the word the cursor is currently on to the selection
191 func (c *Cursor) AddWordToSelection() {
192         loc := c.Loc()
193
194         if loc > c.OrigSelection[0] && loc < c.OrigSelection[1] {
195                 c.CurSelection = c.OrigSelection
196                 return
197         }
198
199         if loc < c.OrigSelection[0] {
200                 backward := c.X
201
202                 for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
203                         backward--
204                 }
205
206                 c.CurSelection[0] = ToCharPos(backward, c.Y, c.buf)
207                 c.CurSelection[1] = c.OrigSelection[1]
208         }
209
210         if loc > c.OrigSelection[1] {
211                 forward := c.X
212
213                 for forward < Count(c.buf.Lines[c.Y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
214                         forward++
215                 }
216
217                 c.CurSelection[1] = ToCharPos(forward, c.Y, c.buf) + 1
218                 c.CurSelection[0] = c.OrigSelection[0]
219         }
220
221         c.SetLoc(c.CurSelection[1])
222 }
223
224 // SelectTo selects from the current cursor location to the given location
225 func (c *Cursor) SelectTo(loc int) {
226         if loc > c.OrigSelection[0] {
227                 c.CurSelection[0] = c.OrigSelection[0]
228                 c.CurSelection[1] = loc
229         } else {
230                 c.CurSelection[0] = loc
231                 c.CurSelection[1] = c.OrigSelection[0]
232         }
233 }
234
235 // WordRight moves the cursor one word to the right
236 func (c *Cursor) WordRight() {
237         c.Right()
238         for IsWhitespace(c.RuneUnder(c.X)) {
239                 if c.X == Count(c.buf.Lines[c.Y]) {
240                         return
241                 }
242                 c.Right()
243         }
244         for !IsWhitespace(c.RuneUnder(c.X)) {
245                 if c.X == Count(c.buf.Lines[c.Y]) {
246                         return
247                 }
248                 c.Right()
249         }
250 }
251
252 // WordLeft moves the cursor one word to the left
253 func (c *Cursor) WordLeft() {
254         c.Left()
255         for IsWhitespace(c.RuneUnder(c.X)) {
256                 if c.X == 0 {
257                         return
258                 }
259                 c.Left()
260         }
261         for !IsWhitespace(c.RuneUnder(c.X)) {
262                 if c.X == 0 {
263                         return
264                 }
265                 c.Left()
266         }
267         c.Right()
268 }
269
270 // RuneUnder returns the rune under the given x position
271 func (c *Cursor) RuneUnder(x int) rune {
272         line := []rune(c.buf.Lines[c.Y])
273         if len(line) == 0 {
274                 return '\n'
275         }
276         if x >= len(line) {
277                 return '\n'
278         } else if x < 0 {
279                 x = 0
280         }
281         return line[x]
282 }
283
284 // Up moves the cursor up one line (if possible)
285 func (c *Cursor) Up() {
286         if c.Y > 0 {
287                 c.Y--
288
289                 runes := []rune(c.buf.Lines[c.Y])
290                 c.X = c.GetCharPosInLine(c.Y, c.LastVisualX)
291                 if c.X > len(runes) {
292                         c.X = len(runes)
293                 }
294         }
295 }
296
297 // Down moves the cursor down one line (if possible)
298 func (c *Cursor) Down() {
299         if c.Y < c.buf.NumLines-1 {
300                 c.Y++
301
302                 runes := []rune(c.buf.Lines[c.Y])
303                 c.X = c.GetCharPosInLine(c.Y, c.LastVisualX)
304                 if c.X > len(runes) {
305                         c.X = len(runes)
306                 }
307         }
308 }
309
310 // Left moves the cursor left one cell (if possible) or to the last line if it is at the beginning
311 func (c *Cursor) Left() {
312         if c.Loc() == 0 {
313                 return
314         }
315         if c.X > 0 {
316                 c.X--
317         } else {
318                 c.Up()
319                 c.End()
320         }
321         c.LastVisualX = c.GetVisualX()
322 }
323
324 // Right moves the cursor right one cell (if possible) or to the next line if it is at the end
325 func (c *Cursor) Right() {
326         if c.Loc() == c.buf.Len() {
327                 return
328         }
329         if c.X < Count(c.buf.Lines[c.Y]) {
330                 c.X++
331         } else {
332                 c.Down()
333                 c.Start()
334         }
335         c.LastVisualX = c.GetVisualX()
336 }
337
338 // End moves the cursor to the end of the line it is on
339 func (c *Cursor) End() {
340         c.X = Count(c.buf.Lines[c.Y])
341         c.LastVisualX = c.GetVisualX()
342 }
343
344 // Start moves the cursor to the start of the line it is on
345 func (c *Cursor) Start() {
346         c.X = 0
347         c.LastVisualX = c.GetVisualX()
348 }
349
350 // GetCharPosInLine gets the char position of a visual x y coordinate (this is necessary because tabs are 1 char but 4 visual spaces)
351 func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int {
352         // Get the tab size
353         tabSize := int(settings["tabsize"].(float64))
354         // This is the visual line -- every \t replaced with the correct number of spaces
355         visualLine := strings.Replace(c.buf.Lines[lineNum], "\t", "\t"+Spaces(tabSize-1), -1)
356         if visualPos > Count(visualLine) {
357                 visualPos = Count(visualLine)
358         }
359         numTabs := NumOccurences(visualLine[:visualPos], '\t')
360         if visualPos >= (tabSize-1)*numTabs {
361                 return visualPos - (tabSize-1)*numTabs
362         }
363         return visualPos / tabSize
364 }
365
366 // GetVisualX returns the x value of the cursor in visual spaces
367 func (c *Cursor) GetVisualX() int {
368         runes := []rune(c.buf.Lines[c.Y])
369         tabSize := int(settings["tabsize"].(float64))
370         return c.X + NumOccurences(string(runes[:c.X]), '\t')*(tabSize-1)
371 }
372
373 // Relocate makes sure that the cursor is inside the bounds of the buffer
374 // If it isn't, it moves it to be within the buffer's lines
375 func (c *Cursor) Relocate() {
376         if c.Y < 0 {
377                 c.Y = 0
378         } else if c.Y >= c.buf.NumLines {
379                 c.Y = c.buf.NumLines - 1
380         }
381
382         if c.X < 0 {
383                 c.X = 0
384         } else if c.X > Count(c.buf.Lines[c.Y]) {
385                 c.X = Count(c.buf.Lines[c.Y])
386         }
387 }