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