]> git.lizzy.rs Git - micro.git/blob - cmd/micro/cursor.go
Merge
[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 // Goto puts the cursor at the given cursor's location and gives the current cursor its selection too
67 func (c *Cursor) Goto(b Cursor) {
68         c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
69         c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
70 }
71
72 // SetLoc sets the location of the cursor in terms of character number
73 // and not x, y location
74 // It's just a simple wrapper of FromCharPos
75 func (c *Cursor) SetLoc(loc int) {
76         c.X, c.Y = FromCharPos(loc, c.buf)
77         c.LastVisualX = c.GetVisualX()
78 }
79
80 // Loc gets the cursor location in terms of character number instead
81 // of x, y location
82 // It's just a simple wrapper of ToCharPos
83 func (c *Cursor) Loc() int {
84         return ToCharPos(c.X, c.Y, c.buf)
85 }
86
87 // ResetSelection resets the user's selection
88 func (c *Cursor) ResetSelection() {
89         c.CurSelection[0] = 0
90         c.CurSelection[1] = 0
91 }
92
93 // HasSelection returns whether or not the user has selected anything
94 func (c *Cursor) HasSelection() bool {
95         return c.CurSelection[0] != c.CurSelection[1]
96 }
97
98 // DeleteSelection deletes the currently selected text
99 func (c *Cursor) DeleteSelection() {
100         if c.CurSelection[0] > c.CurSelection[1] {
101                 c.buf.Remove(c.CurSelection[1], c.CurSelection[0])
102                 c.SetLoc(c.CurSelection[1])
103         } else if c.GetSelection() == "" {
104                 return
105         } else {
106                 c.buf.Remove(c.CurSelection[0], c.CurSelection[1])
107                 c.SetLoc(c.CurSelection[0])
108         }
109 }
110
111 // GetSelection returns the cursor's selection
112 func (c *Cursor) GetSelection() string {
113         if c.CurSelection[0] > c.CurSelection[1] {
114                 return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
115         }
116         return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
117 }
118
119 // SelectLine selects the current line
120 func (c *Cursor) SelectLine() {
121         c.Start()
122         c.CurSelection[0] = c.Loc()
123         c.End()
124         if c.buf.NumLines-1 > c.Y {
125                 c.CurSelection[1] = c.Loc() + 1
126         } else {
127                 c.CurSelection[1] = c.Loc()
128         }
129
130         c.OrigSelection = c.CurSelection
131 }
132
133 // AddLineToSelection adds the current line to the selection
134 func (c *Cursor) AddLineToSelection() {
135         loc := c.Loc()
136
137         if loc < c.OrigSelection[0] {
138                 c.Start()
139                 c.CurSelection[0] = c.Loc()
140                 c.CurSelection[1] = c.OrigSelection[1]
141         }
142         if loc > c.OrigSelection[1] {
143                 c.End()
144                 c.CurSelection[1] = c.Loc()
145                 c.CurSelection[0] = c.OrigSelection[0]
146         }
147
148         if loc < c.OrigSelection[1] && loc > c.OrigSelection[0] {
149                 c.CurSelection = c.OrigSelection
150         }
151 }
152
153 // SelectWord selects the word the cursor is currently on
154 func (c *Cursor) SelectWord() {
155         if len(c.buf.Lines[c.Y]) == 0 {
156                 return
157         }
158
159         if !IsWordChar(string(c.RuneUnder(c.X))) {
160                 loc := c.Loc()
161                 c.CurSelection[0] = loc
162                 c.CurSelection[1] = loc + 1
163                 c.OrigSelection = c.CurSelection
164                 return
165         }
166
167         forward, backward := c.X, c.X
168
169         for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
170                 backward--
171         }
172
173         c.CurSelection[0] = ToCharPos(backward, c.Y, c.buf)
174         c.OrigSelection[0] = c.CurSelection[0]
175
176         for forward < Count(c.buf.Lines[c.Y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
177                 forward++
178         }
179
180         c.CurSelection[1] = ToCharPos(forward, c.Y, c.buf) + 1
181         c.OrigSelection[1] = c.CurSelection[1]
182         c.SetLoc(c.CurSelection[1])
183 }
184
185 // AddWordToSelection adds the word the cursor is currently on to the selection
186 func (c *Cursor) AddWordToSelection() {
187         loc := c.Loc()
188
189         if loc > c.OrigSelection[0] && loc < c.OrigSelection[1] {
190                 c.CurSelection = c.OrigSelection
191                 return
192         }
193
194         if loc < c.OrigSelection[0] {
195                 backward := c.X
196
197                 for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
198                         backward--
199                 }
200
201                 c.CurSelection[0] = ToCharPos(backward, c.Y, c.buf)
202                 c.CurSelection[1] = c.OrigSelection[1]
203         }
204
205         if loc > c.OrigSelection[1] {
206                 forward := c.X
207
208                 for forward < Count(c.buf.Lines[c.Y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
209                         forward++
210                 }
211
212                 c.CurSelection[1] = ToCharPos(forward, c.Y, c.buf) + 1
213                 c.CurSelection[0] = c.OrigSelection[0]
214         }
215
216         c.SetLoc(c.CurSelection[1])
217 }
218
219 // SelectTo selects from the current cursor location to the given location
220 func (c *Cursor) SelectTo(loc int) {
221         if loc > c.OrigSelection[0] {
222                 c.CurSelection[0] = c.OrigSelection[0]
223                 c.CurSelection[1] = loc
224         } else {
225                 c.CurSelection[0] = loc
226                 c.CurSelection[1] = c.OrigSelection[0]
227         }
228 }
229
230 // WordRight moves the cursor one word to the right
231 func (c *Cursor) WordRight() {
232         c.Right()
233         for IsWhitespace(c.RuneUnder(c.X)) {
234                 if c.X == Count(c.buf.Lines[c.Y]) {
235                         return
236                 }
237                 c.Right()
238         }
239         for !IsWhitespace(c.RuneUnder(c.X)) {
240                 if c.X == Count(c.buf.Lines[c.Y]) {
241                         return
242                 }
243                 c.Right()
244         }
245 }
246
247 // WordLeft moves the cursor one word to the left
248 func (c *Cursor) WordLeft() {
249         c.Left()
250         for IsWhitespace(c.RuneUnder(c.X)) {
251                 if c.X == 0 {
252                         return
253                 }
254                 c.Left()
255         }
256         for !IsWhitespace(c.RuneUnder(c.X)) {
257                 if c.X == 0 {
258                         return
259                 }
260                 c.Left()
261         }
262         c.Right()
263 }
264
265 // RuneUnder returns the rune under the given x position
266 func (c *Cursor) RuneUnder(x int) rune {
267         line := []rune(c.buf.Lines[c.Y])
268         if len(line) == 0 {
269                 return '\n'
270         }
271         if x >= len(line) {
272                 return '\n'
273         } else if x < 0 {
274                 x = 0
275         }
276         return line[x]
277 }
278
279 // UpN moves the cursor up N lines (if possible)
280 func (c *Cursor) UpN(amount int) {
281         proposedY := c.Y - amount
282         if proposedY < 0 {
283                 proposedY = 0
284         } else if proposedY >= c.buf.NumLines {
285                 proposedY = c.buf.NumLines - 1
286         }
287         if proposedY == c.Y {
288                 return
289         }
290
291         c.Y = proposedY
292         runes := []rune(c.buf.Lines[c.Y])
293         c.X = c.GetCharPosInLine(c.Y, c.LastVisualX)
294         if c.X > len(runes) {
295                 c.X = len(runes)
296         }
297 }
298
299 // DownN moves the cursor down N lines (if possible)
300 func (c *Cursor) DownN(amount int) {
301         c.UpN(-amount)
302 }
303
304 // Up moves the cursor up one line (if possible)
305 func (c *Cursor) Up() {
306         c.UpN(1)
307 }
308
309 // Down moves the cursor down one line (if possible)
310 func (c *Cursor) Down() {
311         c.DownN(1)
312 }
313
314 // Left moves the cursor left one cell (if possible) or to the last line if it is at the beginning
315 func (c *Cursor) Left() {
316         if c.Loc() == 0 {
317                 return
318         }
319         if c.X > 0 {
320                 c.X--
321         } else {
322                 c.Up()
323                 c.End()
324         }
325         c.LastVisualX = c.GetVisualX()
326 }
327
328 // Right moves the cursor right one cell (if possible) or to the next line if it is at the end
329 func (c *Cursor) Right() {
330         if c.Loc() == c.buf.Len() {
331                 return
332         }
333         // TermMessage(Count(c.buf.Lines[c.Y]))
334         if c.X < Count(c.buf.Lines[c.Y]) {
335                 c.X++
336         } else {
337                 c.Down()
338                 c.Start()
339         }
340         c.LastVisualX = c.GetVisualX()
341 }
342
343 // End moves the cursor to the end of the line it is on
344 func (c *Cursor) End() {
345         c.X = Count(c.buf.Lines[c.Y])
346         c.LastVisualX = c.GetVisualX()
347 }
348
349 // Start moves the cursor to the start of the line it is on
350 func (c *Cursor) Start() {
351         c.X = 0
352         c.LastVisualX = c.GetVisualX()
353 }
354
355 // GetCharPosInLine gets the char position of a visual x y coordinate (this is necessary because tabs are 1 char but 4 visual spaces)
356 func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int {
357         // Get the tab size
358         tabSize := int(settings["tabsize"].(float64))
359         // This is the visual line -- every \t replaced with the correct number of spaces
360         visualLine := strings.Replace(c.buf.Lines[lineNum], "\t", "\t"+Spaces(tabSize-1), -1)
361         if visualPos > Count(visualLine) {
362                 visualPos = Count(visualLine)
363         }
364         numTabs := NumOccurences(visualLine[:visualPos], '\t')
365         if visualPos >= (tabSize-1)*numTabs {
366                 return visualPos - (tabSize-1)*numTabs
367         }
368         return visualPos / tabSize
369 }
370
371 // GetVisualX returns the x value of the cursor in visual spaces
372 func (c *Cursor) GetVisualX() int {
373         runes := []rune(c.buf.Lines[c.Y])
374         tabSize := int(settings["tabsize"].(float64))
375         return c.X + NumOccurences(string(runes[:c.X]), '\t')*(tabSize-1)
376 }
377
378 // Relocate makes sure that the cursor is inside the bounds of the buffer
379 // If it isn't, it moves it to be within the buffer's lines
380 func (c *Cursor) Relocate() {
381         if c.Y < 0 {
382                 c.Y = 0
383         } else if c.Y >= c.buf.NumLines {
384                 c.Y = c.buf.NumLines - 1
385         }
386
387         if c.X < 0 {
388                 c.X = 0
389         } else if c.X > Count(c.buf.Lines[c.Y]) {
390                 c.X = Count(c.buf.Lines[c.Y])
391         }
392 }