Merge 7ac13af7886591ff4b15effd5f43ff578ecc2728 into 492ed7d1a49c1102022e47d5bc5bf03b6a79921d

This commit is contained in:
Yongcheng Mu 2026-06-17 03:57:00 +00:00 committed by GitHub
commit e632274f97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 37 additions and 31 deletions

View File

@ -14,6 +14,7 @@ import (
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/atotto/clipboard"
"github.com/charmbracelet/x/ansi"
rw "github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
@ -745,35 +746,27 @@ func (m Model) placeholderView() string {
render = styles.Placeholder.Render
)
p := make([]rune, m.Width()+1)
copy(p, []rune(m.Placeholder))
m.virtualCursor.TextStyle = styles.Placeholder
m.virtualCursor.SetChar(string(p[:1]))
v += m.virtualCursor.View()
// If the entire placeholder is already set and no padding is needed, finish
if m.Width() < 1 && len(p) <= 1 {
return styles.Prompt.Render(m.Prompt) + v
graphemes := uniseg.NewGraphemes(m.Placeholder)
if !graphemes.Next() {
m.virtualCursor.TextStyle = styles.Placeholder
m.virtualCursor.SetChar(" ")
return styles.Prompt.Render(m.Prompt) + m.virtualCursor.View()
}
// If Width is set then size placeholder accordingly
if m.Width() > 0 {
// available width is width - len + cursor offset of 1
minWidth := lipgloss.Width(m.Placeholder)
availWidth := m.Width() - minWidth + 1
first := graphemes.Str()
rest := m.Placeholder[len(first):]
// if width < len, 'subtract'(add) number to len and dont add padding
if availWidth < 0 {
minWidth += availWidth
availWidth = 0
}
// append placeholder[len] - cursor, append padding
v += render(string(p[1:minWidth]))
v += render(strings.Repeat(" ", availWidth))
m.virtualCursor.TextStyle = styles.Placeholder
m.virtualCursor.SetChar(first)
v += m.virtualCursor.View()
if m.Width() > 0 {
remainingWidth := max(0, m.Width()-uniseg.StringWidth(first))
rest = ansi.Truncate(rest, remainingWidth, "…")
v += render(rest)
v += render(strings.Repeat(" ", max(0, remainingWidth-uniseg.StringWidth(rest))))
} else {
// if there is no width, the placeholder can be any length
v += render(string(p[1:]))
v += render(rest)
}
return styles.Prompt.Render(m.Prompt) + v

View File

@ -7,6 +7,7 @@ import (
"testing"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/x/ansi"
)
func Test_CurrentSuggestion(t *testing.T) {
@ -49,31 +50,43 @@ func Test_SlicingOutsideCap(t *testing.T) {
}
func TestChinesePlaceholder(t *testing.T) {
t.Skip("Skipping flaky test, the returned view seems incorrect. TODO: Needs investigation.")
textinput := New()
textinput.Placeholder = "输入消息..."
textinput.SetWidth(20)
got := textinput.View()
expected := "> 输入消息... "
got := ansi.Strip(textinput.View())
expected := "> 输入消息... "
if got != expected {
t.Fatalf("expected %q but got %q", expected, got)
}
}
func TestPlaceholderTruncate(t *testing.T) {
t.Skip("Skipping flaky test, the returned view seems incorrect. TODO: Needs investigation.")
textinput := New()
textinput.Placeholder = "A very long placeholder, or maybe not so much"
textinput.SetWidth(10)
got := textinput.View()
expected := "> A very …"
got := ansi.Strip(textinput.View())
expected := "> A very lo…"
if got != expected {
t.Fatalf("expected %q but got %q", expected, got)
}
}
func TestEmojiPlaceholder(t *testing.T) {
textinput := New()
textinput.Placeholder = "🤔scope"
textinput.SetWidth(20)
got := textinput.View()
if strings.ContainsRune(got, '\x00') {
t.Fatalf("expected placeholder view without null bytes, got %q", got)
}
if ansi.Strip(got) != "> 🤔scope " {
t.Fatalf("unexpected emoji placeholder rendering: %q", ansi.Strip(got))
}
}
func ExampleValidateFunc() {
creditCardNumber := New()
creditCardNumber.Placeholder = "4505 **** **** 1234"