Skip to content

Commit

Permalink
feat(textarea) Add multiline placeholder
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mikelorant committed Feb 2, 2024
1 parent fc18779 commit fc81153
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 19 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/charmbracelet/bubbles
go 1.18

require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/harmonica v0.2.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
Expand Down
70 changes: 51 additions & 19 deletions textarea/textarea.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package textarea
import (
"crypto/sha256"
"fmt"
"strconv"
"strings"
"unicode"

Expand Down Expand Up @@ -1166,36 +1167,67 @@ func (m Model) getPromptString(displayLine int) (prompt string) {
func (m Model) placeholderView() string {
var (
s strings.Builder
p = rw.Truncate(m.Placeholder, m.width, "...")
p = m.Placeholder
style = m.style.Placeholder.Inline(true)
)

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

if m.ShowLineNumbers {
s.WriteString(m.style.CursorLine.Render(m.style.CursorLineNumber.Render((fmt.Sprintf(m.lineNumberFormat, 1)))))
}

m.Cursor.TextStyle = m.style.Placeholder
m.Cursor.SetChar(string(p[0]))
s.WriteString(m.style.CursorLine.Render(m.Cursor.View()))

// The rest of the placeholder text
s.WriteString(m.style.CursorLine.Render(style.Render(p[1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(p))))))
for i := 0; i < m.height; i++ {
lineStyle := m.style.Base
lineNumberStyle := m.style.LineNumber
if i == 0 {
lineStyle = m.style.CursorLine
lineNumberStyle = m.style.CursorLineNumber
}

// The rest of the new lines
for i := 1; i < m.height; i++ {
s.WriteRune('\n')
// render prompt
prompt := m.getPromptString(i)
prompt = m.style.Prompt.Render(prompt)
s.WriteString(prompt)
s.WriteString(lineStyle.Render(prompt))

// when show line numbers enabled:
// - render line number for only the cursor line
// - indent other placeholder lines
// this is consistent with vim with line numbers enabled
if m.ShowLineNumbers {
eob := m.style.EndOfBuffer.Render((fmt.Sprintf(m.lineNumberFormat, string(m.EndOfBufferCharacter))))
var ln string

switch {
case i == 0:
ln = strconv.Itoa(i + 1)
fallthrough
case len(plines) > i:
s.WriteString(lineStyle.Render(lineNumberStyle.Render(fmt.Sprintf(m.lineNumberFormat, ln))))
default:
}
}

switch {
// first line
case i == 0:
// first character of first line as cursor with character
m.Cursor.TextStyle = m.style.Placeholder
m.Cursor.SetChar(string(plines[0][0]))
s.WriteString(lineStyle.Render(m.Cursor.View()))

// the rest of the first line
s.WriteString(lineStyle.Render(style.Render(plines[0][1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[0]))))))
// remaining lines
case len(plines) > i:
// current line placeholder text
if len(plines) > i {
s.WriteString(lineStyle.Render(style.Render(plines[i] + strings.Repeat(" ", max(0, m.width-rw.StringWidth(plines[i]))))))
}
default:
// end of line buffer character
eob := m.style.EndOfBuffer.Render(string(m.EndOfBufferCharacter))
s.WriteString(eob)
}

// terminate with new line
s.WriteRune('\n')
}

m.viewport.SetContent(s.String())
Expand Down
217 changes: 217 additions & 0 deletions textarea/textarea_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package textarea
import (
"strings"
"testing"
"unicode"

"github.com/acarl005/stripansi"
tea "github.com/charmbracelet/bubbletea"
)

Expand Down Expand Up @@ -428,6 +430,208 @@ func TestRendersEndOfLineBuffer(t *testing.T) {
}
}

func TestRendersPlaceholder(t *testing.T) {
t.Parallel()

tests := []struct {
name string
lines []string
showLineNumbers bool
endOfBufferCharacter rune
expected []string
}{
{
name: "single line",
lines: []string{
"the first line",
},
expected: []string{
"> the first line",
"> ~",
"> ~",
"> ~",
"> ~",
"> ~",
},
},
{
name: "single line with show line numbers",
lines: []string{
"the first line",
},
showLineNumbers: true,
expected: []string{
"> 1 the first line",
"> ~",
"> ~",
"> ~",
"> ~",
"> ~",
},
},
{
name: "single line with end of buffer character",
lines: []string{
"the first line",
},
endOfBufferCharacter: '*',
expected: []string{
"> the first line",
"> *",
"> *",
"> *",
"> *",
"> *",
},
},
{
name: "single line with show line numbers and end of buffer character",
lines: []string{
"the first line",
},
showLineNumbers: true,
endOfBufferCharacter: '*',
expected: []string{
"> 1 the first line",
"> *",
"> *",
"> *",
"> *",
"> *",
},
},
{
name: "multiple lines",
lines: []string{
"the first line",
"the second line",
"the third line",
},
expected: []string{
"> the first line",
"> the second line",
"> the third line",
"> ~",
"> ~",
"> ~",
},
},
{
name: "multiple lines with show line numbers",
lines: []string{
"the first line",
"the second line",
"the third line",
},
showLineNumbers: true,
expected: []string{
"> 1 the first line",
"> the second line",
"> the third line",
"> ~",
"> ~",
"> ~",
},
},
{
name: "multiple lines with end of buffer character",
lines: []string{
"the first line",
"the second line",
"the third line",
},
endOfBufferCharacter: '*',
expected: []string{
"> the first line",
"> the second line",
"> the third line",
"> *",
"> *",
"> *",
},
},
{
name: "multiple lines with show line numbers and end of buffer character",
lines: []string{
"the first line",
"the second line",
"the third line",
},
showLineNumbers: true,
endOfBufferCharacter: '*',
expected: []string{
"> 1 the first line",
"> the second line",
"> the third line",
"> *",
"> *",
"> *",
},
},
{
name: "multiple lines (equal to default textarea height)",
lines: []string{
"the first line",
"the second line",
"the third line",
"the forth line",
"the fifth line",
"the sixth line",
},
expected: []string{
"> the first line",
"> the second line",
"> the third line",
"> the forth line",
"> the fifth line",
"> the sixth line",
},
},
{
name: "multiple lines (greater than default textarea height)",
lines: []string{
"the first line",
"the second line",
"the third line",
"the forth line",
"the fifth line",
"the sixth line",
"the seventh line",
},
expected: []string{
"> the first line",
"> the second line",
"> the third line",
"> the forth line",
"> the fifth line",
"> the sixth line",
},
},
}

for _, tt := range tests {
tt := tt

t.Run(tt.name, func(t *testing.T) {
t.Parallel()

textarea := newTextArea()
textarea.Placeholder = strings.Join(tt.lines, "\n")
textarea.ShowLineNumbers = tt.showLineNumbers
if tt.endOfBufferCharacter != 0 {
textarea.EndOfBufferCharacter = tt.endOfBufferCharacter
}
view := stripString(textarea.View())

expected := strings.Join(tt.expected, "\n")

if expected != view {
t.Errorf("\nExpected:\n---\n%v\n---\n\nGot:\n---\n%v\n---\n\n", expected, view)
}
})
}
}

func newTextArea() Model {
textarea := New()

Expand All @@ -444,3 +648,16 @@ func newTextArea() Model {
func keyPress(key rune) tea.Msg {
return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{key}, Alt: false}
}

func stripString(str string) string {
s := stripansi.Strip(str)
ss := strings.Split(s, "\n")

var lines []string
for _, l := range ss {
trim := strings.TrimRightFunc(l, unicode.IsSpace)
lines = append(lines, trim)
}

return strings.Join(lines, "\n")
}

0 comments on commit fc81153

Please sign in to comment.