buffer.go 11.6 KB
Newer Older
Patrick Devine's avatar
Patrick Devine committed
1
2
3
4
package readline

import (
	"fmt"
5
	"os"
Patrick Devine's avatar
Patrick Devine committed
6

Michael Yang's avatar
Michael Yang committed
7
	"github.com/emirpasic/gods/v2/lists/arraylist"
8
	"github.com/mattn/go-runewidth"
Patrick Devine's avatar
Patrick Devine committed
9
10
11
12
	"golang.org/x/term"
)

type Buffer struct {
13
14
	DisplayPos int
	Pos        int
Michael Yang's avatar
Michael Yang committed
15
	Buf        *arraylist.List[rune]
Michael Yang's avatar
lint  
Michael Yang committed
16
	// LineHasSpace is an arraylist of bools to keep track of whether a line has a space at the end
Michael Yang's avatar
Michael Yang committed
17
	LineHasSpace *arraylist.List[bool]
18
19
20
21
	Prompt       *Prompt
	LineWidth    int
	Width        int
	Height       int
Patrick Devine's avatar
Patrick Devine committed
22
23
24
}

func NewBuffer(prompt *Prompt) (*Buffer, error) {
25
	fd := int(os.Stdout.Fd())
Michael Yang's avatar
Michael Yang committed
26
27
28
	width, height := 80, 24
	if termWidth, termHeight, err := term.GetSize(fd); err == nil {
		width, height = termWidth, termHeight
Patrick Devine's avatar
Patrick Devine committed
29
30
	}

Michael Yang's avatar
Michael Yang committed
31
	lwidth := width - len(prompt.prompt())
Patrick Devine's avatar
Patrick Devine committed
32
33

	b := &Buffer{
34
35
		DisplayPos:   0,
		Pos:          0,
Michael Yang's avatar
Michael Yang committed
36
37
		Buf:          arraylist.New[rune](),
		LineHasSpace: arraylist.New[bool](),
38
39
40
41
		Prompt:       prompt,
		Width:        width,
		Height:       height,
		LineWidth:    lwidth,
Patrick Devine's avatar
Patrick Devine committed
42
43
44
45
46
	}

	return b, nil
}

47
48
func (b *Buffer) GetLineSpacing(line int) bool {
	hasSpace, _ := b.LineHasSpace.Get(line)
Michael Yang's avatar
Michael Yang committed
49
	return hasSpace
50
51
}

Patrick Devine's avatar
Patrick Devine committed
52
53
func (b *Buffer) MoveLeft() {
	if b.Pos > 0 {
Michael Yang's avatar
lint  
Michael Yang committed
54
		// asserts that we retrieve a rune
Michael Yang's avatar
Michael Yang committed
55
56
		if r, ok := b.Buf.Get(b.Pos - 1); ok {
			rLength := runewidth.RuneWidth(r)
57

Michael Yang's avatar
Michael Yang committed
58
59
60
61
			if b.DisplayPos%b.LineWidth == 0 {
				fmt.Print(CursorUp + CursorBOL + CursorRightN(b.Width))
				if rLength == 2 {
					fmt.Print(CursorLeft)
62
63
				}

Michael Yang's avatar
Michael Yang committed
64
65
66
67
68
69
70
71
				line := b.DisplayPos/b.LineWidth - 1
				hasSpace := b.GetLineSpacing(line)
				if hasSpace {
					b.DisplayPos -= 1
					fmt.Print(CursorLeft)
				}
			} else {
				fmt.Print(CursorLeftN(rLength))
72
			}
Michael Yang's avatar
Michael Yang committed
73
74
75

			b.Pos -= 1
			b.DisplayPos -= rLength
Patrick Devine's avatar
Patrick Devine committed
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
		}
	}
}

func (b *Buffer) MoveLeftWord() {
	if b.Pos > 0 {
		var foundNonspace bool
		for {
			v, _ := b.Buf.Get(b.Pos - 1)
			if v == ' ' {
				if foundNonspace {
					break
				}
			} else {
				foundNonspace = true
			}
			b.MoveLeft()

			if b.Pos == 0 {
				break
			}
		}
	}
}

func (b *Buffer) MoveRight() {
102
	if b.Pos < b.Buf.Size() {
Michael Yang's avatar
Michael Yang committed
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
		if r, ok := b.Buf.Get(b.Pos); ok {
			rLength := runewidth.RuneWidth(r)
			b.Pos += 1
			hasSpace := b.GetLineSpacing(b.DisplayPos / b.LineWidth)
			b.DisplayPos += rLength

			if b.DisplayPos%b.LineWidth == 0 {
				fmt.Print(CursorDown + CursorBOL + CursorRightN(len(b.Prompt.prompt())))
			} else if (b.DisplayPos-rLength)%b.LineWidth == b.LineWidth-1 && hasSpace {
				fmt.Print(CursorDown + CursorBOL + CursorRightN(len(b.Prompt.prompt())+rLength))
				b.DisplayPos += 1
			} else if b.LineHasSpace.Size() > 0 && b.DisplayPos%b.LineWidth == b.LineWidth-1 && hasSpace {
				fmt.Print(CursorDown + CursorBOL + CursorRightN(len(b.Prompt.prompt())))
				b.DisplayPos += 1
			} else {
				fmt.Print(CursorRightN(rLength))
119
			}
Patrick Devine's avatar
Patrick Devine committed
120
121
122
123
124
		}
	}
}

func (b *Buffer) MoveRightWord() {
125
	if b.Pos < b.Buf.Size() {
Patrick Devine's avatar
Patrick Devine committed
126
127
128
129
130
131
132
		for {
			b.MoveRight()
			v, _ := b.Buf.Get(b.Pos)
			if v == ' ' {
				break
			}

133
			if b.Pos == b.Buf.Size() {
Patrick Devine's avatar
Patrick Devine committed
134
135
136
137
138
139
140
141
				break
			}
		}
	}
}

func (b *Buffer) MoveToStart() {
	if b.Pos > 0 {
142
		currLine := b.DisplayPos / b.LineWidth
Patrick Devine's avatar
Patrick Devine committed
143
		if currLine > 0 {
Michael Yang's avatar
lint  
Michael Yang committed
144
			for range currLine {
145
				fmt.Print(CursorUp)
Patrick Devine's avatar
Patrick Devine committed
146
147
			}
		}
Michael Yang's avatar
lint  
Michael Yang committed
148
		fmt.Print(CursorBOL + CursorRightN(len(b.Prompt.prompt())))
Patrick Devine's avatar
Patrick Devine committed
149
		b.Pos = 0
150
		b.DisplayPos = 0
Patrick Devine's avatar
Patrick Devine committed
151
152
153
154
	}
}

func (b *Buffer) MoveToEnd() {
155
156
157
	if b.Pos < b.Buf.Size() {
		currLine := b.DisplayPos / b.LineWidth
		totalLines := b.DisplaySize() / b.LineWidth
Patrick Devine's avatar
Patrick Devine committed
158
		if currLine < totalLines {
Michael Yang's avatar
lint  
Michael Yang committed
159
			for range totalLines - currLine {
160
				fmt.Print(CursorDown)
Patrick Devine's avatar
Patrick Devine committed
161
			}
162
			remainder := b.DisplaySize() % b.LineWidth
Michael Yang's avatar
lint  
Michael Yang committed
163
			fmt.Print(CursorBOL + CursorRightN(len(b.Prompt.prompt())+remainder))
Patrick Devine's avatar
Patrick Devine committed
164
		} else {
Michael Yang's avatar
lint  
Michael Yang committed
165
			fmt.Print(CursorRightN(b.DisplaySize() - b.DisplayPos))
Patrick Devine's avatar
Patrick Devine committed
166
167
		}

168
169
		b.Pos = b.Buf.Size()
		b.DisplayPos = b.DisplaySize()
Patrick Devine's avatar
Patrick Devine committed
170
171
172
	}
}

173
174
func (b *Buffer) DisplaySize() int {
	sum := 0
Michael Yang's avatar
Michael Yang committed
175
	for i := range b.Buf.Size() {
Michael Yang's avatar
Michael Yang committed
176
177
		if r, ok := b.Buf.Get(i); ok {
			sum += runewidth.RuneWidth(r)
178
179
180
181
		}
	}

	return sum
Patrick Devine's avatar
Patrick Devine committed
182
183
184
185
}

func (b *Buffer) Add(r rune) {
	if b.Pos == b.Buf.Size() {
186
187
188
189
190
191
192
193
194
195
196
197
198
		b.AddChar(r, false)
	} else {
		b.AddChar(r, true)
	}
}

func (b *Buffer) AddChar(r rune, insert bool) {
	rLength := runewidth.RuneWidth(r)
	b.DisplayPos += rLength

	if b.Pos > 0 {
		if b.DisplayPos%b.LineWidth == 0 {
			fmt.Printf("%c", r)
Patrick Devine's avatar
Patrick Devine committed
199
			fmt.Printf("\n%s", b.Prompt.AltPrompt)
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222

			if insert {
				b.LineHasSpace.Set(b.DisplayPos/b.LineWidth-1, false)
			} else {
				b.LineHasSpace.Add(false)
			}

			// this case occurs when a double-width rune crosses the line boundary
		} else if b.DisplayPos%b.LineWidth < (b.DisplayPos-rLength)%b.LineWidth {
			if insert {
				fmt.Print(ClearToEOL)
			}
			fmt.Printf("\n%s", b.Prompt.AltPrompt)
			b.DisplayPos += 1
			fmt.Printf("%c", r)

			if insert {
				b.LineHasSpace.Set(b.DisplayPos/b.LineWidth-1, true)
			} else {
				b.LineHasSpace.Add(true)
			}
		} else {
			fmt.Printf("%c", r)
Patrick Devine's avatar
Patrick Devine committed
223
224
225
		}
	} else {
		fmt.Printf("%c", r)
226
227
228
	}

	if insert {
Patrick Devine's avatar
Patrick Devine committed
229
		b.Buf.Insert(b.Pos, r)
230
231
232
233
234
235
236
	} else {
		b.Buf.Add(r)
	}

	b.Pos += 1

	if insert {
Patrick Devine's avatar
Patrick Devine committed
237
238
239
240
		b.drawRemaining()
	}
}

241
242
243
244
245
246
247
248
func (b *Buffer) countRemainingLineWidth(place int) int {
	var sum int
	counter := -1
	var prevLen int

	for place <= b.LineWidth {
		counter += 1
		sum += prevLen
Michael Yang's avatar
Michael Yang committed
249
250
251
		if r, ok := b.Buf.Get(b.Pos + counter); ok {
			place += runewidth.RuneWidth(r)
			prevLen = len(string(r))
252
253
254
255
256
257
258
259
		} else {
			break
		}
	}

	return sum
}

Patrick Devine's avatar
Patrick Devine committed
260
261
262
263
func (b *Buffer) drawRemaining() {
	var place int
	remainingText := b.StringN(b.Pos)
	if b.Pos > 0 {
264
		place = b.DisplayPos % b.LineWidth
Patrick Devine's avatar
Patrick Devine committed
265
	}
266
	fmt.Print(CursorHide)
Patrick Devine's avatar
Patrick Devine committed
267
268

	// render the rest of the current line
269
270
271
272
273
274
	currLineLength := b.countRemainingLineWidth(place)

	currLine := remainingText[:min(currLineLength, len(remainingText))]
	currLineSpace := runewidth.StringWidth(currLine)
	remLength := runewidth.StringWidth(remainingText)

Patrick Devine's avatar
Patrick Devine committed
275
	if len(currLine) > 0 {
Michael Yang's avatar
lint  
Michael Yang committed
276
		fmt.Print(ClearToEOL + currLine + CursorLeftN(currLineSpace))
Patrick Devine's avatar
Patrick Devine committed
277
	} else {
278
		fmt.Print(ClearToEOL)
Patrick Devine's avatar
Patrick Devine committed
279
280
	}

281
282
283
284
285
286
287
288
289
	if currLineSpace != b.LineWidth-place && currLineSpace != remLength {
		b.LineHasSpace.Set(b.DisplayPos/b.LineWidth, true)
	} else if currLineSpace != b.LineWidth-place {
		b.LineHasSpace.Remove(b.DisplayPos / b.LineWidth)
	} else {
		b.LineHasSpace.Set(b.DisplayPos/b.LineWidth, false)
	}

	if (b.DisplayPos+currLineSpace)%b.LineWidth == 0 && currLine == remainingText {
Michael Yang's avatar
lint  
Michael Yang committed
290
		fmt.Print(CursorRightN(currLineSpace))
291
		fmt.Printf("\n%s", b.Prompt.AltPrompt)
Michael Yang's avatar
lint  
Michael Yang committed
292
		fmt.Print(CursorUp + CursorBOL + CursorRightN(b.Width-currLineSpace))
293
294
	}

Patrick Devine's avatar
Patrick Devine committed
295
	// render the other lines
296
297
	if remLength > currLineSpace {
		remaining := (remainingText[len(currLine):])
Patrick Devine's avatar
Patrick Devine committed
298
		var totalLines int
299
300
301
302
303
		var displayLength int
		var lineLength int = currLineSpace

		for _, c := range remaining {
			if displayLength == 0 || (displayLength+runewidth.RuneWidth(c))%b.LineWidth < displayLength%b.LineWidth {
Patrick Devine's avatar
Patrick Devine committed
304
305
				fmt.Printf("\n%s", b.Prompt.AltPrompt)
				totalLines += 1
306
307
308
309
310
311
312
313
314
315

				if displayLength != 0 {
					if lineLength == b.LineWidth {
						b.LineHasSpace.Set(b.DisplayPos/b.LineWidth+totalLines-1, false)
					} else {
						b.LineHasSpace.Set(b.DisplayPos/b.LineWidth+totalLines-1, true)
					}
				}

				lineLength = 0
Patrick Devine's avatar
Patrick Devine committed
316
			}
317
318
319

			displayLength += runewidth.RuneWidth(c)
			lineLength += runewidth.RuneWidth(c)
Patrick Devine's avatar
Patrick Devine committed
320
321
			fmt.Printf("%c", c)
		}
Michael Yang's avatar
lint  
Michael Yang committed
322
		fmt.Print(ClearToEOL + CursorUpN(totalLines) + CursorBOL + CursorRightN(b.Width-currLineSpace))
323
324
325
326
327
328

		hasSpace := b.GetLineSpacing(b.DisplayPos / b.LineWidth)

		if hasSpace && b.DisplayPos%b.LineWidth != b.LineWidth-1 {
			fmt.Print(CursorLeft)
		}
Patrick Devine's avatar
Patrick Devine committed
329
330
	}

331
	fmt.Print(CursorShow)
Patrick Devine's avatar
Patrick Devine committed
332
333
334
335
}

func (b *Buffer) Remove() {
	if b.Buf.Size() > 0 && b.Pos > 0 {
Michael Yang's avatar
Michael Yang committed
336
337
338
		if r, ok := b.Buf.Get(b.Pos - 1); ok {
			rLength := runewidth.RuneWidth(r)
			hasSpace := b.GetLineSpacing(b.DisplayPos/b.LineWidth - 1)
339

Michael Yang's avatar
Michael Yang committed
340
341
342
343
			if b.DisplayPos%b.LineWidth == 0 {
				// if the user backspaces over the word boundary, do this magic to clear the line
				// and move to the end of the previous line
				fmt.Print(CursorBOL + ClearToEOL + CursorUp + CursorBOL + CursorRightN(b.Width))
344

Michael Yang's avatar
Michael Yang committed
345
346
347
				if b.DisplaySize()%b.LineWidth < (b.DisplaySize()-rLength)%b.LineWidth {
					b.LineHasSpace.Remove(b.DisplayPos/b.LineWidth - 1)
				}
348

Michael Yang's avatar
Michael Yang committed
349
				if hasSpace {
350
					b.DisplayPos -= 1
Michael Yang's avatar
Michael Yang committed
351
352
353
354
355
					fmt.Print(CursorLeft)
				}

				if rLength == 2 {
					fmt.Print(CursorLeft + "  " + CursorLeftN(2))
356
				} else {
Michael Yang's avatar
Michael Yang committed
357
					fmt.Print(" " + CursorLeft)
358
				}
Michael Yang's avatar
Michael Yang committed
359
360
			} else if (b.DisplayPos-rLength)%b.LineWidth == 0 && hasSpace {
				fmt.Print(CursorBOL + ClearToEOL + CursorUp + CursorBOL + CursorRightN(b.Width))
361

Michael Yang's avatar
Michael Yang committed
362
363
364
365
366
367
368
369
				if b.Pos == b.Buf.Size() {
					b.LineHasSpace.Remove(b.DisplayPos/b.LineWidth - 1)
				}
				b.DisplayPos -= 1
			} else {
				fmt.Print(CursorLeftN(rLength))
				for range rLength {
					fmt.Print(" ")
370
				}
Michael Yang's avatar
Michael Yang committed
371
372
				fmt.Print(CursorLeftN(rLength))
			}
Patrick Devine's avatar
Patrick Devine committed
373

Michael Yang's avatar
Michael Yang committed
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
			var eraseExtraLine bool
			if (b.DisplaySize()-1)%b.LineWidth == 0 || (rLength == 2 && ((b.DisplaySize()-2)%b.LineWidth == 0)) || b.DisplaySize()%b.LineWidth == 0 {
				eraseExtraLine = true
			}

			b.Pos -= 1
			b.DisplayPos -= rLength
			b.Buf.Remove(b.Pos)

			if b.Pos < b.Buf.Size() {
				b.drawRemaining()
				// this erases a line which is left over when backspacing in the middle of a line and there
				// are trailing characters which go over the line width boundary
				if eraseExtraLine {
					remainingLines := (b.DisplaySize() - b.DisplayPos) / b.LineWidth
					fmt.Print(CursorDownN(remainingLines+1) + CursorBOL + ClearToEOL)
					place := b.DisplayPos % b.LineWidth
					fmt.Print(CursorUpN(remainingLines+1) + CursorRightN(place+len(b.Prompt.prompt())))
392
				}
Patrick Devine's avatar
Patrick Devine committed
393
394
395
396
397
398
			}
		}
	}
}

func (b *Buffer) Delete() {
399
	if b.Buf.Size() > 0 && b.Pos < b.Buf.Size() {
Patrick Devine's avatar
Patrick Devine committed
400
401
		b.Buf.Remove(b.Pos)
		b.drawRemaining()
402
403
404
		if b.DisplaySize()%b.LineWidth == 0 {
			if b.DisplayPos != b.DisplaySize() {
				remainingLines := (b.DisplaySize() - b.DisplayPos) / b.LineWidth
Michael Yang's avatar
lint  
Michael Yang committed
405
				fmt.Print(CursorDownN(remainingLines) + CursorBOL + ClearToEOL)
406
				place := b.DisplayPos % b.LineWidth
Michael Yang's avatar
lint  
Michael Yang committed
407
				fmt.Print(CursorUpN(remainingLines) + CursorRightN(place+len(b.Prompt.prompt())))
Patrick Devine's avatar
Patrick Devine committed
408
409
410
411
412
413
414
415
416
417
418
419
420
421
			}
		}
	}
}

func (b *Buffer) DeleteBefore() {
	if b.Pos > 0 {
		for cnt := b.Pos - 1; cnt >= 0; cnt-- {
			b.Remove()
		}
	}
}

func (b *Buffer) DeleteRemaining() {
422
423
	if b.DisplaySize() > 0 && b.Pos < b.DisplaySize() {
		charsToDel := b.Buf.Size() - b.Pos
Michael Yang's avatar
lint  
Michael Yang committed
424
		for range charsToDel {
Patrick Devine's avatar
Patrick Devine committed
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
			b.Delete()
		}
	}
}

func (b *Buffer) DeleteWord() {
	if b.Buf.Size() > 0 && b.Pos > 0 {
		var foundNonspace bool
		for {
			v, _ := b.Buf.Get(b.Pos - 1)
			if v == ' ' {
				if !foundNonspace {
					b.Remove()
				} else {
					break
				}
			} else {
				foundNonspace = true
				b.Remove()
			}

			if b.Pos == 0 {
				break
			}
		}
	}
}

func (b *Buffer) ClearScreen() {
Michael Yang's avatar
lint  
Michael Yang committed
454
	fmt.Print(ClearScreen + CursorReset + b.Prompt.prompt())
Patrick Devine's avatar
Patrick Devine committed
455
	if b.IsEmpty() {
Michael Yang's avatar
Michael Yang committed
456
		ph := b.Prompt.placeholder()
Michael Yang's avatar
lint  
Michael Yang committed
457
		fmt.Print(ColorGrey + ph + CursorLeftN(len(ph)) + ColorDefault)
Patrick Devine's avatar
Patrick Devine committed
458
	} else {
459
460
		currPos := b.DisplayPos
		currIndex := b.Pos
Patrick Devine's avatar
Patrick Devine committed
461
		b.Pos = 0
462
		b.DisplayPos = 0
Patrick Devine's avatar
Patrick Devine committed
463
		b.drawRemaining()
Michael Yang's avatar
lint  
Michael Yang committed
464
		fmt.Print(CursorReset + CursorRightN(len(b.Prompt.prompt())))
Patrick Devine's avatar
Patrick Devine committed
465
466
467
		if currPos > 0 {
			targetLine := currPos / b.LineWidth
			if targetLine > 0 {
Michael Yang's avatar
lint  
Michael Yang committed
468
				for range targetLine {
469
					fmt.Print(CursorDown)
Patrick Devine's avatar
Patrick Devine committed
470
471
472
473
				}
			}
			remainder := currPos % b.LineWidth
			if remainder > 0 {
Michael Yang's avatar
lint  
Michael Yang committed
474
				fmt.Print(CursorRightN(remainder))
Patrick Devine's avatar
Patrick Devine committed
475
476
			}
			if currPos%b.LineWidth == 0 {
Michael Yang's avatar
lint  
Michael Yang committed
477
				fmt.Print(CursorBOL + b.Prompt.AltPrompt)
Patrick Devine's avatar
Patrick Devine committed
478
479
			}
		}
480
481
		b.Pos = currIndex
		b.DisplayPos = currPos
Patrick Devine's avatar
Patrick Devine committed
482
483
484
485
486
487
488
489
	}
}

func (b *Buffer) IsEmpty() bool {
	return b.Buf.Empty()
}

func (b *Buffer) Replace(r []rune) {
490
	b.DisplayPos = 0
Patrick Devine's avatar
Patrick Devine committed
491
	b.Pos = 0
492
493
	lineNums := b.DisplaySize() / b.LineWidth

Patrick Devine's avatar
Patrick Devine committed
494
	b.Buf.Clear()
495

Michael Yang's avatar
lint  
Michael Yang committed
496
	fmt.Print(CursorBOL + ClearToEOL)
497

Michael Yang's avatar
Michael Yang committed
498
	for range lineNums {
499
500
501
		fmt.Print(CursorUp + CursorBOL + ClearToEOL)
	}

Michael Yang's avatar
lint  
Michael Yang committed
502
	fmt.Print(CursorBOL + b.Prompt.prompt())
503

Patrick Devine's avatar
Patrick Devine committed
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
	for _, c := range r {
		b.Add(c)
	}
}

func (b *Buffer) String() string {
	return b.StringN(0)
}

func (b *Buffer) StringN(n int) string {
	return b.StringNM(n, 0)
}

func (b *Buffer) StringNM(n, m int) string {
	var s string
	if m == 0 {
520
		m = b.Buf.Size()
Patrick Devine's avatar
Patrick Devine committed
521
522
523
	}
	for cnt := n; cnt < m; cnt++ {
		c, _ := b.Buf.Get(cnt)
Michael Yang's avatar
Michael Yang committed
524
		s += string(c)
Patrick Devine's avatar
Patrick Devine committed
525
526
527
	}
	return s
}