Skip to content

Commit 97e8494

Browse files
committed
feat(textarea) Add multiline placeholder
Add the capability to show a multiline placeholder. Some refactoring was required to improve readability and improve logic. End of line buffer character was only shown when line numbers were displayed which requires some verification whether this is the intended outcome. This change resolves this issue.
1 parent fc18779 commit 97e8494

4 files changed

Lines changed: 265 additions & 20 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/charmbracelet/bubbles
33
go 1.18
44

55
require (
6+
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
67
github.com/atotto/clipboard v0.1.4
78
github.com/charmbracelet/bubbletea v0.25.0
89
github.com/charmbracelet/harmonica v0.2.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
2+
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
13
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
24
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
35
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=

textarea/textarea.go

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package textarea
33
import (
44
"crypto/sha256"
55
"fmt"
6+
"strconv"
67
"strings"
78
"unicode"
89

@@ -1166,36 +1167,60 @@ func (m Model) getPromptString(displayLine int) (prompt string) {
11661167
func (m Model) placeholderView() string {
11671168
var (
11681169
s strings.Builder
1169-
p = rw.Truncate(m.Placeholder, m.width, "...")
1170+
p = m.Placeholder
11701171
style = m.style.Placeholder.Inline(true)
11711172
)
11721173

1173-
prompt := m.getPromptString(0)
1174-
prompt = m.style.Prompt.Render(prompt)
1175-
s.WriteString(m.style.CursorLine.Render(prompt))
1174+
// split string by new lines
1175+
plines := strings.Split(strings.TrimSpace(p), "\n")
11761176

1177-
if m.ShowLineNumbers {
1178-
s.WriteString(m.style.CursorLine.Render(m.style.CursorLineNumber.Render((fmt.Sprintf(m.lineNumberFormat, 1)))))
1179-
}
1180-
1181-
m.Cursor.TextStyle = m.style.Placeholder
1182-
m.Cursor.SetChar(string(p[0]))
1183-
s.WriteString(m.style.CursorLine.Render(m.Cursor.View()))
1184-
1185-
// The rest of the placeholder text
1186-
s.WriteString(m.style.CursorLine.Render(style.Render(p[1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(p))))))
1187-
1188-
// The rest of the new lines
1189-
for i := 1; i < m.height; i++ {
1190-
s.WriteRune('\n')
1177+
for i := 0; i < m.height; i++ {
1178+
// render prompt
11911179
prompt := m.getPromptString(i)
11921180
prompt = m.style.Prompt.Render(prompt)
1193-
s.WriteString(prompt)
1181+
s.WriteString(m.style.CursorLine.Render(prompt))
11941182

1183+
// when show line numbers enabled:
1184+
// - render line number for only the cursor line
1185+
// - indent other placeholder lines
1186+
// this is consistent with vim with line numbers enabled
11951187
if m.ShowLineNumbers {
1196-
eob := m.style.EndOfBuffer.Render((fmt.Sprintf(m.lineNumberFormat, string(m.EndOfBufferCharacter))))
1188+
var ln string
1189+
1190+
switch {
1191+
case i == 0:
1192+
ln = strconv.Itoa(i + 1)
1193+
fallthrough
1194+
case len(plines) > i:
1195+
s.WriteString(m.style.CursorLine.Render(m.style.CursorLineNumber.Render(fmt.Sprintf(m.lineNumberFormat, ln))))
1196+
default:
1197+
}
1198+
}
1199+
1200+
switch {
1201+
// first line
1202+
case i == 0:
1203+
// first character of first line as cursor with character
1204+
m.Cursor.TextStyle = m.style.Placeholder
1205+
m.Cursor.SetChar(string(plines[0][0]))
1206+
s.WriteString(m.style.CursorLine.Render(m.Cursor.View()))
1207+
1208+
// the rest of the first line
1209+
s.WriteString(m.style.CursorLine.Render(style.Render(plines[0][1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[0]))))))
1210+
// remaining lines
1211+
case len(plines) > i:
1212+
// current line placeholder text
1213+
if len(plines) > i {
1214+
s.WriteString(m.style.CursorLine.Render(style.Render(plines[i] + strings.Repeat(" ", max(0, m.width-rw.StringWidth(plines[i]))))))
1215+
}
1216+
default:
1217+
// end of line buffer character
1218+
eob := m.style.EndOfBuffer.Render(string(m.EndOfBufferCharacter))
11971219
s.WriteString(eob)
11981220
}
1221+
1222+
// terminate with new line
1223+
s.WriteRune('\n')
11991224
}
12001225

12011226
m.viewport.SetContent(s.String())

textarea/textarea_test.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package textarea
33
import (
44
"strings"
55
"testing"
6+
"unicode"
67

78
tea "github.com/charmbracelet/bubbletea"
9+
"github.com/acarl005/stripansi"
810
)
911

1012
func TestNew(t *testing.T) {
@@ -428,6 +430,208 @@ func TestRendersEndOfLineBuffer(t *testing.T) {
428430
}
429431
}
430432

433+
func TestRendersPlaceholder(t *testing.T) {
434+
t.Parallel()
435+
436+
tests := []struct {
437+
name string
438+
lines []string
439+
showLineNumbers bool
440+
endOfBufferCharacter rune
441+
expected []string
442+
}{
443+
{
444+
name: "single line",
445+
lines: []string{
446+
"the first line",
447+
},
448+
expected: []string{
449+
"> the first line",
450+
"> ~",
451+
"> ~",
452+
"> ~",
453+
"> ~",
454+
"> ~",
455+
},
456+
},
457+
{
458+
name: "single line with show line numbers",
459+
lines: []string{
460+
"the first line",
461+
},
462+
showLineNumbers: true,
463+
expected: []string{
464+
"> 1 the first line",
465+
"> ~",
466+
"> ~",
467+
"> ~",
468+
"> ~",
469+
"> ~",
470+
},
471+
},
472+
{
473+
name: "single line with end of buffer character",
474+
lines: []string{
475+
"the first line",
476+
},
477+
endOfBufferCharacter: '*',
478+
expected: []string{
479+
"> the first line",
480+
"> *",
481+
"> *",
482+
"> *",
483+
"> *",
484+
"> *",
485+
},
486+
},
487+
{
488+
name: "single line with show line numbers and end of buffer character",
489+
lines: []string{
490+
"the first line",
491+
},
492+
showLineNumbers: true,
493+
endOfBufferCharacter: '*',
494+
expected: []string{
495+
"> 1 the first line",
496+
"> *",
497+
"> *",
498+
"> *",
499+
"> *",
500+
"> *",
501+
},
502+
},
503+
{
504+
name: "multiple lines",
505+
lines: []string{
506+
"the first line",
507+
"the second line",
508+
"the third line",
509+
},
510+
expected: []string{
511+
"> the first line",
512+
"> the second line",
513+
"> the third line",
514+
"> ~",
515+
"> ~",
516+
"> ~",
517+
},
518+
},
519+
{
520+
name: "multiple lines with show line numbers",
521+
lines: []string{
522+
"the first line",
523+
"the second line",
524+
"the third line",
525+
},
526+
showLineNumbers: true,
527+
expected: []string{
528+
"> 1 the first line",
529+
"> the second line",
530+
"> the third line",
531+
"> ~",
532+
"> ~",
533+
"> ~",
534+
},
535+
},
536+
{
537+
name: "multiple lines with end of buffer character",
538+
lines: []string{
539+
"the first line",
540+
"the second line",
541+
"the third line",
542+
},
543+
endOfBufferCharacter: '*',
544+
expected: []string{
545+
"> the first line",
546+
"> the second line",
547+
"> the third line",
548+
"> *",
549+
"> *",
550+
"> *",
551+
},
552+
},
553+
{
554+
name: "multiple lines with show line numbers and end of buffer character",
555+
lines: []string{
556+
"the first line",
557+
"the second line",
558+
"the third line",
559+
},
560+
showLineNumbers: true,
561+
endOfBufferCharacter: '*',
562+
expected: []string{
563+
"> 1 the first line",
564+
"> the second line",
565+
"> the third line",
566+
"> *",
567+
"> *",
568+
"> *",
569+
},
570+
},
571+
{
572+
name: "multiple lines (equal to default textarea height)",
573+
lines: []string{
574+
"the first line",
575+
"the second line",
576+
"the third line",
577+
"the forth line",
578+
"the fifth line",
579+
"the sixth line",
580+
},
581+
expected: []string{
582+
"> the first line",
583+
"> the second line",
584+
"> the third line",
585+
"> the forth line",
586+
"> the fifth line",
587+
"> the sixth line",
588+
},
589+
},
590+
{
591+
name: "multiple lines (greater than default textarea height)",
592+
lines: []string{
593+
"the first line",
594+
"the second line",
595+
"the third line",
596+
"the forth line",
597+
"the fifth line",
598+
"the sixth line",
599+
"the seventh line",
600+
},
601+
expected: []string{
602+
"> the first line",
603+
"> the second line",
604+
"> the third line",
605+
"> the forth line",
606+
"> the fifth line",
607+
"> the sixth line",
608+
},
609+
},
610+
}
611+
612+
for _, tt := range tests {
613+
tt := tt
614+
615+
t.Run(tt.name, func(t *testing.T) {
616+
t.Parallel()
617+
618+
textarea := newTextArea()
619+
textarea.Placeholder = strings.Join(tt.lines, "\n")
620+
textarea.ShowLineNumbers = tt.showLineNumbers
621+
if tt.endOfBufferCharacter != 0 {
622+
textarea.EndOfBufferCharacter = tt.endOfBufferCharacter
623+
}
624+
view := stripString(textarea.View())
625+
626+
expected := strings.Join(tt.expected, "\n")
627+
628+
if expected != view {
629+
t.Errorf("\nExpected:\n---\n%v\n---\n\nGot:\n---\n%v\n---\n\n", expected, view)
630+
}
631+
})
632+
}
633+
}
634+
431635
func newTextArea() Model {
432636
textarea := New()
433637

@@ -444,3 +648,16 @@ func newTextArea() Model {
444648
func keyPress(key rune) tea.Msg {
445649
return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{key}, Alt: false}
446650
}
651+
652+
func stripString(str string) string {
653+
s := stripansi.Strip(str)
654+
ss := strings.Split(s, "\n")
655+
656+
var lines []string
657+
for _, l := range ss {
658+
trim := strings.TrimRightFunc(l, unicode.IsSpace)
659+
lines = append(lines, trim)
660+
}
661+
662+
return strings.Join(lines, "\n")
663+
}

0 commit comments

Comments
 (0)