Skip to content

Commit fc81153

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 fc81153

4 files changed

Lines changed: 271 additions & 19 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: 51 additions & 19 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,67 @@ 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))))))
1177+
for i := 0; i < m.height; i++ {
1178+
lineStyle := m.style.Base
1179+
lineNumberStyle := m.style.LineNumber
1180+
if i == 0 {
1181+
lineStyle = m.style.CursorLine
1182+
lineNumberStyle = m.style.CursorLineNumber
1183+
}
11871184

1188-
// The rest of the new lines
1189-
for i := 1; i < m.height; i++ {
1190-
s.WriteRune('\n')
1185+
// render prompt
11911186
prompt := m.getPromptString(i)
11921187
prompt = m.style.Prompt.Render(prompt)
1193-
s.WriteString(prompt)
1188+
s.WriteString(lineStyle.Render(prompt))
11941189

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

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

textarea/textarea_test.go

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

8+
"github.com/acarl005/stripansi"
79
tea "github.com/charmbracelet/bubbletea"
810
)
911

@@ -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)