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