Skip to content

Commit 39fc2d0

Browse files
committed
feat(editor): rune-wrap text
1 parent be83024 commit 39fc2d0

File tree

3 files changed

+81
-8
lines changed

3 files changed

+81
-8
lines changed

editor/editor.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ func Edit(title, initial string, save bool) (string, error) {
8787
ta := textarea.New()
8888
ta.CharLimit = editorCharLimit
8989
ta.Cursor.SetMode(cursor.CursorStatic)
90-
ta.ShowLineNumbers = false
9190
ta.SetValue(initial)
91+
ta.SetWrapMode(textarea.RuneWrap)
92+
ta.ShowLineNumbers = false
9293
ta.Focus()
9394

9495
// Remove cursor line highlighting.

textarea/textarea.go

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ type (
4040
pasteErrMsg struct{ error }
4141
)
4242

43+
// WrapMode describes how to line-wrap text.
44+
type WrapMode int
45+
46+
// Available wrap modes.
47+
const (
48+
WordWrap WrapMode = iota
49+
RuneWrap
50+
)
51+
52+
// String returns the wrap mode in a human-readable format. This method is
53+
// provisional and for informational purposes only.
54+
func (w WrapMode) String() string {
55+
return [...]string{
56+
"word",
57+
"rune",
58+
}[w]
59+
}
60+
4361
// KeyMap is the key bindings for different actions within the textarea.
4462
type KeyMap struct {
4563
CharacterBackward key.Binding
@@ -174,13 +192,14 @@ func (s Style) computedText() lipgloss.Style {
174192
// line is the input to the text wrapping function. This is stored in a struct
175193
// so that it can be hashed and memoized.
176194
type line struct {
177-
runes []rune
178-
width int
195+
runes []rune
196+
width int
197+
wrapMode WrapMode
179198
}
180199

181200
// Hash returns a hash of the line.
182201
func (w line) Hash() string {
183-
v := fmt.Sprintf("%s:%d", string(w.runes), w.width)
202+
v := fmt.Sprintf("%s:%d:%d", string(w.runes), w.width, w.wrapMode)
184203
return fmt.Sprintf("%x", sha256.Sum256([]byte(v)))
185204
}
186205

@@ -238,6 +257,9 @@ type Model struct {
238257
// there's no limit.
239258
MaxWidth int
240259

260+
// wrapMode determines line-wrapping behavior.
261+
wrapMode WrapMode
262+
241263
// If promptFunc is set, it replaces Prompt as a generator for
242264
// prompt strings at the beginning of each line.
243265
promptFunc func(line int) string
@@ -300,6 +322,7 @@ func New() Model {
300322
ShowLineNumbers: true,
301323
Cursor: cur,
302324
KeyMap: DefaultKeyMap,
325+
wrapMode: WordWrap,
303326

304327
value: make([][]rune, minHeight, maxLines),
305328
focus: false,
@@ -954,6 +977,14 @@ func (m *Model) SetHeight(h int) {
954977
}
955978
}
956979

980+
// SetWrapMode sets the model's wrap mode.
981+
func (m *Model) SetWrapMode(mode WrapMode) {
982+
if mode < WordWrap || mode > RuneWrap {
983+
return
984+
}
985+
m.wrapMode = mode
986+
}
987+
957988
// Update is the Bubble Tea update loop.
958989
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
959990
if !m.focus {
@@ -1304,11 +1335,11 @@ func Blink() tea.Msg {
13041335
}
13051336

13061337
func (m Model) memoizedWrap(runes []rune, width int) [][]rune {
1307-
input := line{runes: runes, width: width}
1338+
input := line{runes: runes, width: width, wrapMode: m.wrapMode}
13081339
if v, ok := m.cache.Get(input); ok {
13091340
return v
13101341
}
1311-
v := wrap(runes, width)
1342+
v := wrap(runes, width, m.wrapMode)
13121343
m.cache.Set(input, v)
13131344
return v
13141345
}
@@ -1395,7 +1426,23 @@ func Paste() tea.Msg {
13951426
return pasteMsg(str)
13961427
}
13971428

1398-
func wrap(runes []rune, width int) [][]rune {
1429+
func wrap(runes []rune, width int, mode WrapMode) [][]rune {
1430+
if mode == RuneWrap {
1431+
wrapped := ansi.Hardwrap(string(runes), width, true)
1432+
wrappedSplit := strings.Split(wrapped, "\n")
1433+
1434+
lines := [][]rune{}
1435+
for i, s := range wrappedSplit {
1436+
if i == len(wrappedSplit)-1 {
1437+
s += " "
1438+
}
1439+
1440+
lines = append(lines, []rune(s))
1441+
}
1442+
1443+
return lines
1444+
}
1445+
13991446
var (
14001447
lines = [][]rune{{}}
14011448
word = []rune{}

textarea/textarea_test.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ func TestView(t *testing.T) {
606606
},
607607
},
608608
{
609-
name: "softwrap",
609+
name: "softwrap-word",
610610
modelFunc: func(m Model) Model {
611611
m.ShowLineNumbers = false
612612
m.Prompt = ""
@@ -630,6 +630,31 @@ func TestView(t *testing.T) {
630630
cursorCol: 3,
631631
},
632632
},
633+
{
634+
name: "softwrap-rune",
635+
modelFunc: func(m Model) Model {
636+
m.ShowLineNumbers = false
637+
m.Prompt = ""
638+
m.SetWidth(5)
639+
m.SetWrapMode(RuneWrap)
640+
641+
input := "foobarbaz"
642+
m = sendString(m, input)
643+
644+
return m
645+
},
646+
want: want{
647+
view: heredoc.Doc(`
648+
fooba
649+
rbaz
650+
651+
652+
653+
`),
654+
cursorRow: 1,
655+
cursorCol: 4,
656+
},
657+
},
633658
{
634659
name: "single line character limit",
635660
modelFunc: func(m Model) Model {

0 commit comments

Comments
 (0)