mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-06 05:06:16 +00:00
feat(textarea) Add multiline placeholder (#302)
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.
This commit is contained in:
+55
-18
@@ -3,6 +3,7 @@ package textarea
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
rw "github.com/mattn/go-runewidth"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
@@ -1170,36 +1172,71 @@ func (m Model) getPromptString(displayLine int) (prompt string) {
|
||||
func (m Model) placeholderView() string {
|
||||
var (
|
||||
s strings.Builder
|
||||
p = rw.Truncate(rw.Truncate(m.Placeholder, m.width, "..."), 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))
|
||||
// word wrap lines
|
||||
pwordwrap := ansi.Wordwrap(p, m.width, "")
|
||||
// wrap lines (handles lines that could not be word wrapped)
|
||||
pwrap := ansi.Hardwrap(pwordwrap, m.width, true)
|
||||
// split string by new lines
|
||||
plines := strings.Split(strings.TrimSpace(pwrap), "\n")
|
||||
|
||||
if m.ShowLineNumbers {
|
||||
s.WriteString(m.style.CursorLine.Render(m.style.CursorLineNumber.Render((fmt.Sprintf(m.lineNumberFormat, 1)))))
|
||||
}
|
||||
for i := 0; i < m.height; i++ {
|
||||
lineStyle := m.style.Placeholder
|
||||
lineNumberStyle := m.style.LineNumber
|
||||
if len(plines) > i {
|
||||
lineStyle = m.style.CursorLine
|
||||
lineNumberStyle = m.style.CursorLineNumber
|
||||
}
|
||||
|
||||
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))))))
|
||||
|
||||
// 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 {
|
||||
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-uniseg.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())
|
||||
|
||||
+403
-1
@@ -326,6 +326,8 @@ func TestVerticalNavigationShouldRememberPositionWhileTraversing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type want struct {
|
||||
view string
|
||||
cursorRow int
|
||||
@@ -1264,7 +1266,64 @@ func TestView(t *testing.T) {
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 .
|
||||
> 1 H
|
||||
> e
|
||||
> l
|
||||
> l
|
||||
> o
|
||||
> ,
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line"
|
||||
m.ShowLineNumbers = false
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> placeholder the first line
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line\nplaceholder the second line\nplaceholder the third line"
|
||||
m.ShowLineNumbers = false
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> placeholder the first line
|
||||
> placeholder the second line
|
||||
> placeholder the third line
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line with line numbers",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line"
|
||||
m.ShowLineNumbers = true
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 placeholder the first line
|
||||
>
|
||||
>
|
||||
>
|
||||
@@ -1273,10 +1332,353 @@ func TestView(t *testing.T) {
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines with line numbers",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line\nplaceholder the second line\nplaceholder the third line"
|
||||
m.ShowLineNumbers = true
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 placeholder the first line
|
||||
> placeholder the second line
|
||||
> placeholder the third line
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line with end of buffer character",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line"
|
||||
m.ShowLineNumbers = false
|
||||
m.EndOfBufferCharacter = '*'
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> placeholder the first line
|
||||
> *
|
||||
> *
|
||||
> *
|
||||
> *
|
||||
> *
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines with with end of buffer character",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line\nplaceholder the second line\nplaceholder the third line"
|
||||
m.ShowLineNumbers = false
|
||||
m.EndOfBufferCharacter = '*'
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> placeholder the first line
|
||||
> placeholder the second line
|
||||
> placeholder the third line
|
||||
> *
|
||||
> *
|
||||
> *
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line with line numbers and end of buffer character",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line"
|
||||
m.ShowLineNumbers = true
|
||||
m.EndOfBufferCharacter = '*'
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 placeholder the first line
|
||||
> *
|
||||
> *
|
||||
> *
|
||||
> *
|
||||
> *
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines with line numbers and end of buffer character",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line\nplaceholder the second line\nplaceholder the third line"
|
||||
m.ShowLineNumbers = true
|
||||
m.EndOfBufferCharacter = '*'
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 placeholder the first line
|
||||
> placeholder the second line
|
||||
> placeholder the third line
|
||||
> *
|
||||
> *
|
||||
> *
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line that is longer than max width",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line that is longer than the max width"
|
||||
m.SetWidth(40)
|
||||
m.ShowLineNumbers = false
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> placeholder the first line that is
|
||||
> longer than the max width
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines that are longer than max width",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line that is longer than the max width\nplaceholder the second line that is longer than the max width"
|
||||
m.ShowLineNumbers = false
|
||||
m.SetWidth(40)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> placeholder the first line that is
|
||||
> longer than the max width
|
||||
> placeholder the second line that is
|
||||
> longer than the max width
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line that is longer than max width with line numbers",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line that is longer than the max width"
|
||||
m.ShowLineNumbers = true
|
||||
m.SetWidth(40)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 placeholder the first line that is
|
||||
> longer than the max width
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines that are longer than max width with line numbers",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "placeholder the first line that is longer than the max width\nplaceholder the second line that is longer than the max width"
|
||||
m.ShowLineNumbers = true
|
||||
m.SetWidth(40)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 placeholder the first line that is
|
||||
> longer than the max width
|
||||
> placeholder the second line that
|
||||
> is longer than the max width
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line that is longer than max width at limit",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "123456789012345678"
|
||||
m.ShowLineNumbers = false
|
||||
m.SetWidth(20)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 123456789012345678
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line that is longer than max width at limit plus one",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "1234567890123456789"
|
||||
m.ShowLineNumbers = false
|
||||
m.SetWidth(20)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 123456789012345678
|
||||
> 9
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line that is longer than max width with line numbers at limit",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "12345678901234"
|
||||
m.ShowLineNumbers = true
|
||||
m.SetWidth(20)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 12345678901234
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder single line that is longer than max width with line numbers at limit plus one",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "123456789012345"
|
||||
m.ShowLineNumbers = true
|
||||
m.SetWidth(20)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 12345678901234
|
||||
> 5
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines that are longer than max width at limit",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "123456789012345678\n123456789012345678"
|
||||
m.ShowLineNumbers = false
|
||||
m.SetWidth(20)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 123456789012345678
|
||||
> 123456789012345678
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines that are longer than max width at limit plus one",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "1234567890123456789\n1234567890123456789"
|
||||
m.ShowLineNumbers = false
|
||||
m.SetWidth(20)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 123456789012345678
|
||||
> 9
|
||||
> 123456789012345678
|
||||
> 9
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines that are longer than max width with line numbers at limit",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "12345678901234\n12345678901234"
|
||||
m.ShowLineNumbers = true
|
||||
m.SetWidth(20)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 12345678901234
|
||||
> 12345678901234
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "placeholder multiple lines that are longer than max width with line numbers at limit plus one",
|
||||
modelFunc: func(m Model) Model {
|
||||
m.Placeholder = "123456789012345\n123456789012345"
|
||||
m.ShowLineNumbers = true
|
||||
m.SetWidth(20)
|
||||
|
||||
return m
|
||||
},
|
||||
want: want{
|
||||
view: heredoc.Doc(`
|
||||
> 1 12345678901234
|
||||
> 5
|
||||
> 12345678901234
|
||||
> 5
|
||||
>
|
||||
>
|
||||
`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
textarea := newTextArea()
|
||||
|
||||
if tt.modelFunc != nil {
|
||||
|
||||
Reference in New Issue
Block a user