]> git.lizzy.rs Git - micro.git/blob - cmd/micro/buffer/cursor.go
More actions and window organization
[micro.git] / cmd / micro / buffer / cursor.go
1 package buffer
2
3 import (
4         "unicode/utf8"
5
6         runewidth "github.com/mattn/go-runewidth"
7         "github.com/zyedidia/clipboard"
8         "github.com/zyedidia/micro/cmd/micro/util"
9 )
10
11 // InBounds returns whether the given location is a valid character position in the given buffer
12 func InBounds(pos Loc, buf *Buffer) bool {
13         if pos.Y < 0 || pos.Y >= len(buf.lines) || pos.X < 0 || pos.X > utf8.RuneCount(buf.LineBytes(pos.Y)) {
14                 return false
15         }
16
17         return true
18 }
19
20 // The Cursor struct stores the location of the cursor in the buffer
21 // as well as the selection
22 type Cursor struct {
23         Buf *Buffer
24         Loc
25
26         // Last cursor x position
27         LastVisualX int
28
29         // The current selection as a range of character numbers (inclusive)
30         CurSelection [2]Loc
31         // The original selection as a range of character numbers
32         // This is used for line and word selection where it is necessary
33         // to know what the original selection was
34         OrigSelection [2]Loc
35
36         // Which cursor index is this (for multiple cursors)
37         Num int
38 }
39
40 // Goto puts the cursor at the given cursor's location and gives
41 // the current cursor its selection too
42 func (c *Cursor) Goto(b Cursor) {
43         c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
44         c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
45 }
46
47 // GotoLoc puts the cursor at the given cursor's location and gives
48 // the current cursor its selection too
49 func (c *Cursor) GotoLoc(l Loc) {
50         c.X, c.Y = l.X, l.Y
51         c.StoreVisualX()
52 }
53
54 // GetVisualX returns the x value of the cursor in visual spaces
55 func (c *Cursor) GetVisualX() int {
56         if c.X <= 0 {
57                 c.X = 0
58                 return 0
59         }
60
61         bytes := c.Buf.LineBytes(c.Y)
62         tabsize := int(c.Buf.Settings["tabsize"].(float64))
63         if c.X > utf8.RuneCount(bytes) {
64                 c.X = utf8.RuneCount(bytes) - 1
65         }
66
67         return util.StringWidth(bytes, c.X, tabsize)
68 }
69
70 // GetCharPosInLine gets the char position of a visual x y
71 // coordinate (this is necessary because tabs are 1 char but
72 // 4 visual spaces)
73 func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
74         tabsize := int(c.Buf.Settings["tabsize"].(float64))
75
76         // Scan rune by rune until we exceed the visual width that we are
77         // looking for. Then we can return the character position we have found
78         i := 0     // char pos
79         width := 0 // string visual width
80         for len(b) > 0 {
81                 r, size := utf8.DecodeRune(b)
82                 b = b[size:]
83
84                 switch r {
85                 case '\t':
86                         ts := tabsize - (width % tabsize)
87                         width += ts
88                 default:
89                         width += runewidth.RuneWidth(r)
90                 }
91
92                 if width >= visualPos {
93                         if width == visualPos {
94                                 i++
95                         }
96                         break
97                 }
98                 i++
99         }
100
101         return i
102 }
103
104 // Start moves the cursor to the start of the line it is on
105 func (c *Cursor) Start() {
106         c.X = 0
107         c.LastVisualX = c.GetVisualX()
108 }
109
110 // StartOfText moves the cursor to the first non-whitespace rune of
111 // the line it is on
112 func (c *Cursor) StartOfText() {
113         c.Start()
114         for util.IsWhitespace(c.RuneUnder(c.X)) {
115                 if c.X == utf8.RuneCount(c.Buf.LineBytes(c.Y)) {
116                         break
117                 }
118                 c.Right()
119         }
120 }
121
122 // End moves the cursor to the end of the line it is on
123 func (c *Cursor) End() {
124         c.X = utf8.RuneCount(c.Buf.LineBytes(c.Y))
125         c.LastVisualX = c.GetVisualX()
126 }
127
128 // CopySelection copies the user's selection to either "primary"
129 // or "clipboard"
130 func (c *Cursor) CopySelection(target string) {
131         if c.HasSelection() {
132                 if target != "primary" || c.Buf.Settings["useprimary"].(bool) {
133                         clipboard.WriteAll(string(c.GetSelection()), target)
134                 }
135         }
136 }
137
138 // ResetSelection resets the user's selection
139 func (c *Cursor) ResetSelection() {
140         c.CurSelection[0] = c.Buf.Start()
141         c.CurSelection[1] = c.Buf.Start()
142 }
143
144 // SetSelectionStart sets the start of the selection
145 func (c *Cursor) SetSelectionStart(pos Loc) {
146         c.CurSelection[0] = pos
147 }
148
149 // SetSelectionEnd sets the end of the selection
150 func (c *Cursor) SetSelectionEnd(pos Loc) {
151         c.CurSelection[1] = pos
152 }
153
154 // HasSelection returns whether or not the user has selected anything
155 func (c *Cursor) HasSelection() bool {
156         return c.CurSelection[0] != c.CurSelection[1]
157 }
158
159 // DeleteSelection deletes the currently selected text
160 func (c *Cursor) DeleteSelection() {
161         if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
162                 c.Buf.Remove(c.CurSelection[1], c.CurSelection[0])
163                 c.Loc = c.CurSelection[1]
164         } else if !c.HasSelection() {
165                 return
166         } else {
167                 c.Buf.Remove(c.CurSelection[0], c.CurSelection[1])
168                 c.Loc = c.CurSelection[0]
169         }
170 }
171
172 // Deselect closes the cursor's current selection
173 // Start indicates whether the cursor should be placed
174 // at the start or end of the selection
175 func (c *Cursor) Deselect(start bool) {
176         if c.HasSelection() {
177                 if start {
178                         c.Loc = c.CurSelection[0]
179                 } else {
180                         c.Loc = c.CurSelection[1]
181                 }
182                 c.ResetSelection()
183                 c.StoreVisualX()
184         }
185 }
186
187 // GetSelection returns the cursor's selection
188 func (c *Cursor) GetSelection() []byte {
189         if InBounds(c.CurSelection[0], c.Buf) && InBounds(c.CurSelection[1], c.Buf) {
190                 if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
191                         return c.Buf.Substr(c.CurSelection[1], c.CurSelection[0])
192                 }
193                 return c.Buf.Substr(c.CurSelection[0], c.CurSelection[1])
194         }
195         return []byte{}
196 }
197
198 // SelectLine selects the current line
199 func (c *Cursor) SelectLine() {
200         c.Start()
201         c.SetSelectionStart(c.Loc)
202         c.End()
203         if len(c.Buf.lines)-1 > c.Y {
204                 c.SetSelectionEnd(c.Loc.Move(1, c.Buf))
205         } else {
206                 c.SetSelectionEnd(c.Loc)
207         }
208
209         c.OrigSelection = c.CurSelection
210 }
211
212 // AddLineToSelection adds the current line to the selection
213 func (c *Cursor) AddLineToSelection() {
214         if c.Loc.LessThan(c.OrigSelection[0]) {
215                 c.Start()
216                 c.SetSelectionStart(c.Loc)
217                 c.SetSelectionEnd(c.OrigSelection[1])
218         }
219         if c.Loc.GreaterThan(c.OrigSelection[1]) {
220                 c.End()
221                 c.SetSelectionEnd(c.Loc.Move(1, c.Buf))
222                 c.SetSelectionStart(c.OrigSelection[0])
223         }
224
225         if c.Loc.LessThan(c.OrigSelection[1]) && c.Loc.GreaterThan(c.OrigSelection[0]) {
226                 c.CurSelection = c.OrigSelection
227         }
228 }
229
230 // UpN moves the cursor up N lines (if possible)
231 func (c *Cursor) UpN(amount int) {
232         proposedY := c.Y - amount
233         if proposedY < 0 {
234                 proposedY = 0
235         } else if proposedY >= len(c.Buf.lines) {
236                 proposedY = len(c.Buf.lines) - 1
237         }
238
239         bytes := c.Buf.LineBytes(proposedY)
240         c.X = c.GetCharPosInLine(bytes, c.LastVisualX)
241
242         if c.X > utf8.RuneCount(bytes) || (amount < 0 && proposedY == c.Y) {
243                 c.X = utf8.RuneCount(bytes)
244         }
245
246         c.Y = proposedY
247 }
248
249 // DownN moves the cursor down N lines (if possible)
250 func (c *Cursor) DownN(amount int) {
251         c.UpN(-amount)
252 }
253
254 // Up moves the cursor up one line (if possible)
255 func (c *Cursor) Up() {
256         c.UpN(1)
257 }
258
259 // Down moves the cursor down one line (if possible)
260 func (c *Cursor) Down() {
261         c.DownN(1)
262 }
263
264 // Left moves the cursor left one cell (if possible) or to
265 // the previous line if it is at the beginning
266 func (c *Cursor) Left() {
267         if c.Loc == c.Buf.Start() {
268                 return
269         }
270         if c.X > 0 {
271                 c.X--
272         } else {
273                 c.Up()
274                 c.End()
275         }
276         c.StoreVisualX()
277 }
278
279 // Right moves the cursor right one cell (if possible) or
280 // to the next line if it is at the end
281 func (c *Cursor) Right() {
282         if c.Loc == c.Buf.End() {
283                 return
284         }
285         if c.X < utf8.RuneCount(c.Buf.LineBytes(c.Y)) {
286                 c.X++
287         } else {
288                 c.Down()
289                 c.Start()
290         }
291         c.StoreVisualX()
292 }
293
294 // Relocate makes sure that the cursor is inside the bounds
295 // of the buffer If it isn't, it moves it to be within the
296 // buffer's lines
297 func (c *Cursor) Relocate() {
298         if c.Y < 0 {
299                 c.Y = 0
300         } else if c.Y >= len(c.Buf.lines) {
301                 c.Y = len(c.Buf.lines) - 1
302         }
303
304         if c.X < 0 {
305                 c.X = 0
306         } else if c.X > utf8.RuneCount(c.Buf.LineBytes(c.Y)) {
307                 c.X = utf8.RuneCount(c.Buf.LineBytes(c.Y))
308         }
309 }
310
311 // SelectWord selects the word the cursor is currently on
312 func (c *Cursor) SelectWord() {
313         if len(c.Buf.LineBytes(c.Y)) == 0 {
314                 return
315         }
316
317         if !util.IsWordChar(c.RuneUnder(c.X)) {
318                 c.SetSelectionStart(c.Loc)
319                 c.SetSelectionEnd(c.Loc.Move(1, c.Buf))
320                 c.OrigSelection = c.CurSelection
321                 return
322         }
323
324         forward, backward := c.X, c.X
325
326         for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
327                 backward--
328         }
329
330         c.SetSelectionStart(Loc{backward, c.Y})
331         c.OrigSelection[0] = c.CurSelection[0]
332
333         lineLen := utf8.RuneCount(c.Buf.LineBytes(c.Y)) - 1
334         for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
335                 forward++
336         }
337
338         c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.Buf))
339         c.OrigSelection[1] = c.CurSelection[1]
340         c.Loc = c.CurSelection[1]
341 }
342
343 // AddWordToSelection adds the word the cursor is currently on
344 // to the selection
345 func (c *Cursor) AddWordToSelection() {
346         if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
347                 c.CurSelection = c.OrigSelection
348                 return
349         }
350
351         if c.Loc.LessThan(c.OrigSelection[0]) {
352                 backward := c.X
353
354                 for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
355                         backward--
356                 }
357
358                 c.SetSelectionStart(Loc{backward, c.Y})
359                 c.SetSelectionEnd(c.OrigSelection[1])
360         }
361
362         if c.Loc.GreaterThan(c.OrigSelection[1]) {
363                 forward := c.X
364
365                 lineLen := utf8.RuneCount(c.Buf.LineBytes(c.Y)) - 1
366                 for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
367                         forward++
368                 }
369
370                 c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.Buf))
371                 c.SetSelectionStart(c.OrigSelection[0])
372         }
373
374         c.Loc = c.CurSelection[1]
375 }
376
377 // SelectTo selects from the current cursor location to the given
378 // location
379 func (c *Cursor) SelectTo(loc Loc) {
380         if loc.GreaterThan(c.OrigSelection[0]) {
381                 c.SetSelectionStart(c.OrigSelection[0])
382                 c.SetSelectionEnd(loc)
383         } else {
384                 c.SetSelectionStart(loc)
385                 c.SetSelectionEnd(c.OrigSelection[0])
386         }
387 }
388
389 // WordRight moves the cursor one word to the right
390 func (c *Cursor) WordRight() {
391         for util.IsWhitespace(c.RuneUnder(c.X)) {
392                 if c.X == utf8.RuneCount(c.Buf.LineBytes(c.Y)) {
393                         c.Right()
394                         return
395                 }
396                 c.Right()
397         }
398         c.Right()
399         for util.IsWordChar(c.RuneUnder(c.X)) {
400                 if c.X == utf8.RuneCount(c.Buf.LineBytes(c.Y)) {
401                         return
402                 }
403                 c.Right()
404         }
405 }
406
407 // WordLeft moves the cursor one word to the left
408 func (c *Cursor) WordLeft() {
409         c.Left()
410         for util.IsWhitespace(c.RuneUnder(c.X)) {
411                 if c.X == 0 {
412                         return
413                 }
414                 c.Left()
415         }
416         c.Left()
417         for util.IsWordChar(c.RuneUnder(c.X)) {
418                 if c.X == 0 {
419                         return
420                 }
421                 c.Left()
422         }
423         c.Right()
424 }
425
426 // RuneUnder returns the rune under the given x position
427 func (c *Cursor) RuneUnder(x int) rune {
428         line := c.Buf.LineBytes(c.Y)
429         if len(line) == 0 || x >= utf8.RuneCount(line) {
430                 return '\n'
431         } else if x < 0 {
432                 x = 0
433         }
434         i := 0
435         for len(line) > 0 {
436                 r, size := utf8.DecodeRune(line)
437                 line = line[size:]
438
439                 if i == x {
440                         return r
441                 }
442
443                 i++
444         }
445         return '\n'
446 }
447
448 func (c *Cursor) StoreVisualX() {
449         c.LastVisualX = c.GetVisualX()
450 }