textarea: add uppercase/lowercase/capitalize word right (#210)

* Extract char navigation in separate function.

* Make wordLeft/wordRight find words on the prev/next line

... if there's no word on the current line to find any more.

* Support uppercase/lowercase/capitalize commands.

* Add transpose
This commit is contained in:
kena 2022-08-19 17:17:36 +02:00 committed by GitHub
parent 649f78e1fd
commit 1c26128786
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -46,6 +46,12 @@ type KeyMap struct {
Paste key.Binding Paste key.Binding
WordBackward key.Binding WordBackward key.Binding
WordForward key.Binding WordForward key.Binding
UppercaseWordForward key.Binding
LowercaseWordForward key.Binding
CapitalizeWordForward key.Binding
TransposeCharacterBackward key.Binding
} }
// DefaultKeyMap is the default set of key bindings for navigating and acting // DefaultKeyMap is the default set of key bindings for navigating and acting
@ -67,6 +73,12 @@ var DefaultKeyMap = KeyMap{
LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")), LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")),
LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")), LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")),
Paste: key.NewBinding(key.WithKeys("ctrl+v")), Paste: key.NewBinding(key.WithKeys("ctrl+v")),
CapitalizeWordForward: key.NewBinding(key.WithKeys("alt+c")),
LowercaseWordForward: key.NewBinding(key.WithKeys("alt+l")),
UppercaseWordForward: key.NewBinding(key.WithKeys("alt+u")),
TransposeCharacterBackward: key.NewBinding(key.WithKeys("ctrl+t")),
} }
// LineInfo is a helper for keeping track of line information regarding // LineInfo is a helper for keeping track of line information regarding
@ -473,6 +485,24 @@ func (m *Model) deleteAfterCursor() {
m.SetCursor(len(m.value[m.row])) m.SetCursor(len(m.value[m.row]))
} }
// transposeLeft exchanges the runes at the cursor and immediately
// before. No-op if the cursor is at the beginning of the line. If
// the cursor is not at the end of the line yet, moves the cursor to
// the right.
func (m *Model) transposeLeft() {
if m.col == 0 || len(m.value[m.row]) < 2 {
return
}
if m.col >= len(m.value[m.row]) {
m.SetCursor(m.col - 1)
}
m.value[m.row][m.col-1], m.value[m.row][m.col] =
m.value[m.row][m.col], m.value[m.row][m.col-1]
if m.col < len(m.value[m.row]) {
m.SetCursor(m.col + 1)
}
}
// deleteWordLeft deletes the word left to the cursor. Returns whether or not // deleteWordLeft deletes the word left to the cursor. Returns whether or not
// the cursor blink should be reset. // the cursor blink should be reset.
func (m *Model) deleteWordLeft() { func (m *Model) deleteWordLeft() {
@ -547,31 +577,50 @@ func (m *Model) deleteWordRight() {
m.SetCursor(oldCol) m.SetCursor(oldCol)
} }
// characterRight moves the cursor one character to the right.
func (m *Model) characterRight() {
if m.col < len(m.value[m.row]) {
m.SetCursor(m.col + 1)
} else {
if m.row < len(m.value)-1 {
m.row++
m.CursorStart()
}
}
}
// characterLeft moves the cursor one character to the left.
// If insideLine is set, the cursor is moved to the last
// character in the previous line, instead of one past that.
func (m *Model) characterLeft(insideLine bool) {
if m.col == 0 && m.row != 0 {
m.row--
m.CursorEnd()
if !insideLine {
return
}
}
if m.col > 0 {
m.SetCursor(m.col - 1)
}
}
// wordLeft moves the cursor one word to the left. Returns whether or not the // wordLeft moves the cursor one word to the left. Returns whether or not the
// cursor blink should be reset. If input is masked, move input to the start // cursor blink should be reset. If input is masked, move input to the start
// so as not to reveal word breaks in the masked input. // so as not to reveal word breaks in the masked input.
func (m *Model) wordLeft() { func (m *Model) wordLeft() {
if m.col == 0 || len(m.value[m.row]) == 0 { for {
return m.characterLeft(true /* insideLine */)
} if m.col < len(m.value[m.row]) && !unicode.IsSpace(m.value[m.row][m.col]) {
i := m.col - 1
for i >= 0 {
if unicode.IsSpace(m.value[m.row][min(i, len(m.value[m.row])-1)]) {
m.SetCursor(m.col - 1)
i--
} else {
break break
} }
} }
for i >= 0 { for m.col > 0 {
if !unicode.IsSpace(m.value[m.row][min(i, len(m.value[m.row])-1)]) { if unicode.IsSpace(m.value[m.row][m.col-1]) {
m.SetCursor(m.col - 1)
i--
} else {
break break
} }
m.SetCursor(m.col - 1)
} }
} }
@ -579,28 +628,54 @@ func (m *Model) wordLeft() {
// cursor blink should be reset. If the input is masked, move input to the end // cursor blink should be reset. If the input is masked, move input to the end
// so as not to reveal word breaks in the masked input. // so as not to reveal word breaks in the masked input.
func (m *Model) wordRight() { func (m *Model) wordRight() {
if m.col >= len(m.value[m.row]) || len(m.value[m.row]) == 0 { m.doWordRight(func(int, int) { /* nothing */ })
return }
}
i := m.col func (m *Model) doWordRight(fn func(charIdx int, pos int)) {
for i < len(m.value[m.row]) { // Skip spaces forward.
if unicode.IsSpace(m.value[m.row][i]) { for {
m.SetCursor(m.col + 1) if m.col < len(m.value[m.row]) && !unicode.IsSpace(m.value[m.row][m.col]) {
i++
} else {
break break
} }
} if m.row == len(m.value)-1 && m.col == len(m.value[m.row]) {
// End of text.
for i < len(m.value[m.row]) {
if !unicode.IsSpace(m.value[m.row][i]) {
m.SetCursor(m.col + 1)
i++
} else {
break break
} }
m.characterRight()
} }
charIdx := 0
for m.col < len(m.value[m.row]) {
if unicode.IsSpace(m.value[m.row][m.col]) {
break
}
fn(charIdx, m.col)
m.SetCursor(m.col + 1)
charIdx++
}
}
// uppercaseRight changes the word to the right to uppercase.
func (m *Model) uppercaseRight() {
m.doWordRight(func(_ int, i int) {
m.value[m.row][i] = unicode.ToUpper(m.value[m.row][i])
})
}
// lowercaseRight changes the word to the right to lowercase.
func (m *Model) lowercaseRight() {
m.doWordRight(func(_ int, i int) {
m.value[m.row][i] = unicode.ToLower(m.value[m.row][i])
})
}
// capitalizeRight changes the word to the right to title case.
func (m *Model) capitalizeRight() {
m.doWordRight(func(charIdx int, i int) {
if charIdx == 0 {
m.value[m.row][i] = unicode.ToTitle(m.value[m.row][i])
}
})
} }
// LineInfo returns the number of characters from the start of the // LineInfo returns the number of characters from the start of the
@ -774,14 +849,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
case key.Matches(msg, m.KeyMap.LineStart): case key.Matches(msg, m.KeyMap.LineStart):
m.CursorStart() m.CursorStart()
case key.Matches(msg, m.KeyMap.CharacterForward): case key.Matches(msg, m.KeyMap.CharacterForward):
if m.col < len(m.value[m.row]) { m.characterRight()
m.SetCursor(m.col + 1)
} else {
if m.row < len(m.value)-1 {
m.row++
m.CursorStart()
}
}
case key.Matches(msg, m.KeyMap.LineNext): case key.Matches(msg, m.KeyMap.LineNext):
m.CursorDown() m.CursorDown()
case key.Matches(msg, m.KeyMap.WordForward): case key.Matches(msg, m.KeyMap.WordForward):
@ -789,18 +857,21 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
case key.Matches(msg, m.KeyMap.Paste): case key.Matches(msg, m.KeyMap.Paste):
return m, Paste return m, Paste
case key.Matches(msg, m.KeyMap.CharacterBackward): case key.Matches(msg, m.KeyMap.CharacterBackward):
if m.col == 0 && m.row != 0 { m.characterLeft(false /* insideLine */)
m.row--
m.CursorEnd()
break
}
if m.col > 0 {
m.SetCursor(m.col - 1)
}
case key.Matches(msg, m.KeyMap.LinePrevious): case key.Matches(msg, m.KeyMap.LinePrevious):
m.CursorUp() m.CursorUp()
case key.Matches(msg, m.KeyMap.WordBackward): case key.Matches(msg, m.KeyMap.WordBackward):
m.wordLeft() m.wordLeft()
case key.Matches(msg, m.KeyMap.LowercaseWordForward):
m.lowercaseRight()
case key.Matches(msg, m.KeyMap.UppercaseWordForward):
m.uppercaseRight()
case key.Matches(msg, m.KeyMap.CapitalizeWordForward):
m.capitalizeRight()
case key.Matches(msg, m.KeyMap.TransposeCharacterBackward):
m.transposeLeft()
default: default:
if m.CharLimit > 0 && rw.StringWidth(m.Value()) >= m.CharLimit { if m.CharLimit > 0 && rw.StringWidth(m.Value()) >= m.CharLimit {
break break