diff --git a/textarea/textarea.go b/textarea/textarea.go index a830efd6..1e99437c 100644 --- a/textarea/textarea.go +++ b/textarea/textarea.go @@ -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))))) - } - - 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.Placeholder + lineNumberStyle := m.style.LineNumber + if len(plines) > i { + 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 { + 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()) diff --git a/textarea/textarea_test.go b/textarea/textarea_test.go index 770e997e..6aab299b 100644 --- a/textarea/textarea_test.go +++ b/textarea/textarea_test.go @@ -326,6 +326,8 @@ func TestVerticalNavigationShouldRememberPositionWhileTraversing(t *testing.T) { } func TestView(t *testing.T) { + t.Parallel() + type want struct { view string cursorRow int @@ -1264,11 +1266,407 @@ 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 + > + > + > + > + > + `), + }, + }, + { + 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 + > > `), }, @@ -1276,7 +1674,11 @@ func TestView(t *testing.T) { } for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + textarea := newTextArea() if tt.modelFunc != nil {