mirror of
https://github.com/Maks1mS/bubbles.git
synced 2024-12-23 22:32:59 +03:00
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:
parent
649f78e1fd
commit
1c26128786
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user