refactor: split files into {modal,normal,insert}.go

This commit is contained in:
Maas Lalani 2022-08-09 16:25:31 -04:00
parent 447ff2da6a
commit fb44abddf8
No known key found for this signature in database
GPG Key ID: 5A6ED5CBF1A0A000
4 changed files with 343 additions and 315 deletions

117
textarea/insert.go Normal file
View File

@ -0,0 +1,117 @@
package textarea
import (
tea "github.com/charmbracelet/bubbletea"
rw "github.com/mattn/go-runewidth"
)
func (m *Model) insertUpdate(msg tea.Msg) tea.Cmd {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
return m.SetMode(ModeNormal)
case "ctrl+k":
m.col = clamp(m.col, 0, len(m.value[m.row]))
if m.col >= len(m.value[m.row]) {
m.mergeLineBelow(m.row)
break
}
m.deleteAfterCursor()
case "ctrl+u":
m.col = clamp(m.col, 0, len(m.value[m.row]))
if m.col <= 0 {
m.mergeLineAbove(m.row)
break
}
m.deleteBeforeCursor()
case "backspace", "ctrl+h":
m.col = clamp(m.col, 0, len(m.value[m.row]))
if m.col <= 0 {
m.mergeLineAbove(m.row)
break
}
if len(m.value[m.row]) > 0 {
m.value[m.row] = append(m.value[m.row][:max(0, m.col-1)], m.value[m.row][m.col:]...)
if m.col > 0 {
m.SetCursor(m.col - 1)
}
}
case "delete", "ctrl+d":
if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {
m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][m.col+1:]...)
}
if m.col >= len(m.value[m.row]) {
m.mergeLineBelow(m.row)
break
}
case "alt+backspace", "ctrl+w":
if m.col <= 0 {
m.mergeLineAbove(m.row)
break
}
m.deleteWordLeft()
case "alt+delete", "alt+d":
m.col = clamp(m.col, 0, len(m.value[m.row]))
if m.col >= len(m.value[m.row]) {
m.mergeLineBelow(m.row)
break
}
m.deleteWordRight()
case "enter", "ctrl+m":
if len(m.value) >= maxHeight {
return nil
}
m.col = clamp(m.col, 0, len(m.value[m.row]))
m.splitLine(m.row, m.col)
case "end", "ctrl+e":
m.CursorEnd()
case "home", "ctrl+a":
m.CursorStart()
case "right", "ctrl+f":
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()
}
}
case "down", "ctrl+n":
m.CursorDown()
case "alt+right", "alt+f":
m.wordRight()
case "ctrl+v":
return Paste
case "left", "ctrl+b":
if m.col == 0 && m.row != 0 {
m.row--
m.CursorEnd()
break
}
if m.col > 0 {
m.SetCursor(m.col - 1)
}
case "up", "ctrl+p":
m.CursorUp()
case "alt+left", "alt+b":
m.wordLeft()
default:
if m.CharLimit > 0 && rw.StringWidth(m.Value()) >= m.CharLimit {
break
}
m.col = min(m.col, len(m.value[m.row]))
m.value[m.row] = append(m.value[m.row][:m.col], append(msg.Runes, m.value[m.row][m.col:]...)...)
m.SetCursor(m.col + len(msg.Runes))
}
case pasteMsg:
m.handlePaste(string(msg))
case pasteErrMsg:
m.Err = msg
}
return nil
}

75
textarea/modal.go Normal file
View File

@ -0,0 +1,75 @@
package textarea
import (
"github.com/charmbracelet/bubbles/cursor"
tea "github.com/charmbracelet/bubbletea"
)
// Mode is the possible modes of the textarea for modal editing.
type Mode string
const (
// ModeNormal is the normal mode for navigating around text.
ModeNormal Mode = "normal"
// ModeInsert is the insert mode for inserting text.
ModeInsert Mode = "insert"
)
// SetMode sets the mode of the textarea.
func (m *Model) SetMode(mode Mode) tea.Cmd {
switch mode {
case ModeInsert:
m.mode = ModeInsert
return m.Cursor.SetCursorMode(cursor.CursorBlink)
case ModeNormal:
m.mode = ModeNormal
return m.Cursor.SetCursorMode(cursor.CursorStatic)
}
return nil
}
// Action is the type of action that will be performed when the user completes
// a key combination.
type Action int
const (
// ActionMove moves the cursor.
ActionMove Action = iota
// ActionSeek seeks the cursor to the desired character.
// Used in conjunction with f/F/t/T.
ActionSeek
// ActionReplace replaces text.
ActionReplace
// ActionDelete deletes text.
ActionDelete
// ActionYank yanks text.
ActionYank
)
// Position is a (row, column) pair representing a position of the cursor or
// any character.
type Position struct {
Row int
Col int
}
// Range is a range of characters in the text area.
type Range struct {
Start Position
End Position
}
// NormalCommand is a helper for keeping track of the various relevant information
// when performing vim motions in the textarea.
type NormalCommand struct {
// Buffer is the buffer of keys that have been press for the current
// command.
Buffer string
// Count is the number of times to replay the action. This is usually
// optional and defaults to 1.
Count int
// Action is the action to be performed.
Action Action
// Range is the range of characters to perform the action on.
Range Range
}

150
textarea/normal.go Normal file
View File

@ -0,0 +1,150 @@
package textarea
import (
"strconv"
tea "github.com/charmbracelet/bubbletea"
)
func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
m.command = &NormalCommand{}
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
v := m.command.Buffer + msg.String()
count, err := strconv.Atoi(v)
if err != nil {
count, _ = strconv.Atoi(msg.String())
m.command = &NormalCommand{Buffer: msg.String(), Count: count}
} else {
m.command = &NormalCommand{Buffer: v, Count: count}
}
case "G":
var row int
if m.command.Count > 0 {
row = m.command.Count - 1
} else {
row = len(m.value) - 1
}
m.row = clamp(row, 0, len(m.value)-1)
return nil
case "g":
if m.command.Buffer == "g" {
m.command = &NormalCommand{}
m.row = clamp(m.command.Count-1, 0, len(m.value)-1)
} else {
m.command = &NormalCommand{Buffer: "g"}
}
return nil
case "d":
if m.command.Action == ActionDelete {
for i := 0; i < max(m.command.Count, 1); i++ {
m.CursorStart()
m.deleteAfterCursor()
m.mergeLineBelow(m.row)
}
m.command = &NormalCommand{}
break
}
m.command.Action = ActionDelete
case "y":
m.command.Action = ActionYank
case "r":
m.command.Action = ActionReplace
case "i":
return m.SetMode(ModeInsert)
case "I":
m.CursorStart()
return m.SetMode(ModeInsert)
case "a":
m.SetCursor(m.col + 1)
return m.SetMode(ModeInsert)
case "A":
m.CursorEnd()
return m.SetMode(ModeInsert)
case "^":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row, 0},
}
case "$":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row, len(m.value[m.row]) - 1},
}
case "e", "E":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: m.findWordEnd(m.command.Count, msg.String() == "E"),
}
case "w", "W":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: m.findWordStart(m.command.Count, msg.String() == "W"),
}
case "b", "B":
direction := -1
m.command.Range = Range{
Start: Position{m.row, m.col},
End: m.findWordStart(direction*m.command.Count, msg.String() == "B"),
}
case "h":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row, m.col - max(m.command.Count, 1)},
}
case "j":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row + max(m.command.Count, 1), m.col},
}
case "k":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row - max(m.command.Count, 1), m.col},
}
case "l":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row, m.col + max(m.command.Count, 1)},
}
case "J":
m.CursorEnd()
m.mergeLineBelow(m.row)
return nil
case "p":
cmd = Paste
}
switch msg.String() {
case "i", "I", "a", "A", "e", "w", "W", "b", "B", "h", "j", "k", "l", "p", "$", "^":
switch m.command.Action {
case ActionDelete:
m.deleteRange(m.command.Range)
case ActionMove:
rowDelta := m.command.Range.End.Row - m.command.Range.Start.Row
if rowDelta > 0 {
for i := 0; i < rowDelta; i++ {
m.CursorDown()
}
} else if rowDelta < 0 {
for i := 0; i < -rowDelta; i++ {
m.CursorUp()
}
} else {
m.SetCursor(m.command.Range.End.Col)
}
}
m.command = &NormalCommand{}
}
case pasteMsg:
m.handlePaste(string(msg))
}
return cmd
}

View File

@ -2,7 +2,6 @@ package textarea
import (
"fmt"
"strconv"
"strings"
"unicode"
@ -24,16 +23,6 @@ const (
maxWidth = 500
)
// Mode is the possible modes of the textarea for modal editing.
type Mode string
const (
// ModeNormal is the normal mode for navigating around text.
ModeNormal Mode = "normal"
// ModeInsert is the insert mode for inserting text.
ModeInsert Mode = "insert"
)
// Internal messages for clipboard operations.
type pasteMsg string
type pasteErrMsg struct{ error }
@ -63,52 +52,6 @@ type LineInfo struct {
CharOffset int
}
// Action is the type of action that will be performed when the user completes
// a key combination.
type Action int
const (
// ActionMove moves the cursor.
ActionMove Action = iota
// ActionSeek seeks the cursor to the desired character.
// Used in conjunction with f/F/t/T.
ActionSeek
// ActionReplace replaces text.
ActionReplace
// ActionDelete deletes text.
ActionDelete
// ActionYank yanks text.
ActionYank
)
// Position is a (row, column) pair representing a position of the cursor or
// any character.
type Position struct {
Row int
Col int
}
// Range is a range of characters in the text area.
type Range struct {
Start Position
End Position
}
// NormalCommand is a helper for keeping track of the various relevant information
// when performing vim motions in the textarea.
type NormalCommand struct {
// Buffer is the buffer of keys that have been press for the current
// command.
Buffer string
// Count is the number of times to replay the action. This is usually
// optional and defaults to 1.
Count int
// Action is the action to be performed.
Action Action
// Range is the range of characters to perform the action on.
Range Range
}
// Style that will be applied to the text area.
//
// Style can be applied to focused and unfocused states to change the styles
@ -790,264 +733,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
// SetMode sets the mode of the textarea.
func (m *Model) SetMode(mode Mode) tea.Cmd {
switch mode {
case ModeInsert:
m.mode = ModeInsert
return m.Cursor.SetCursorMode(cursor.CursorBlink)
case ModeNormal:
m.mode = ModeNormal
return m.Cursor.SetCursorMode(cursor.CursorStatic)
}
return nil
}
func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
m.command = &NormalCommand{}
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
v := m.command.Buffer + msg.String()
count, err := strconv.Atoi(v)
if err != nil {
count, _ = strconv.Atoi(msg.String())
m.command = &NormalCommand{Buffer: msg.String(), Count: count}
} else {
m.command = &NormalCommand{Buffer: v, Count: count}
}
case "G":
var row int
if m.command.Count > 0 {
row = m.command.Count - 1
} else {
row = len(m.value) - 1
}
m.row = clamp(row, 0, len(m.value)-1)
return nil
case "g":
if m.command.Buffer == "g" {
m.command = &NormalCommand{}
m.row = clamp(m.command.Count-1, 0, len(m.value)-1)
} else {
m.command = &NormalCommand{Buffer: "g"}
}
return nil
case "d":
if m.command.Action == ActionDelete {
for i := 0; i < m.command.Count; i++ {
m.CursorStart()
m.deleteAfterCursor()
m.mergeLineBelow(m.row)
}
m.command = &NormalCommand{}
break
}
m.command.Action = ActionDelete
case "y":
m.command.Action = ActionYank
case "r":
m.command.Action = ActionReplace
case "i":
return m.SetMode(ModeInsert)
case "I":
m.CursorStart()
return m.SetMode(ModeInsert)
case "a":
m.SetCursor(m.col + 1)
return m.SetMode(ModeInsert)
case "A":
m.CursorEnd()
return m.SetMode(ModeInsert)
case "^":
m.CursorStart()
return nil
case "$":
m.CursorEnd()
return nil
case "e", "E":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: m.findWordEnd(m.command.Count, msg.String() == "E"),
}
case "w", "W":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: m.findWordStart(m.command.Count, msg.String() == "W"),
}
case "b", "B":
direction := -1
m.command.Range = Range{
Start: Position{m.row, m.col},
End: m.findWordStart(direction*m.command.Count, msg.String() == "B"),
}
case "h":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row, m.col - max(m.command.Count, 1)},
}
case "j":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row + max(m.command.Count, 1), m.col},
}
case "k":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row - max(m.command.Count, 1), m.col},
}
case "l":
m.command.Range = Range{
Start: Position{m.row, m.col},
End: Position{m.row, m.col + max(m.command.Count, 1)},
}
case "J":
m.CursorEnd()
m.mergeLineBelow(m.row)
return nil
case "p":
cmd = Paste
}
switch msg.String() {
case "i", "I", "a", "A", "e", "w", "W", "b", "B", "h", "j", "k", "l", "p":
if m.command.Action == ActionMove {
rowDelta := m.command.Range.End.Row - m.command.Range.Start.Row
if rowDelta > 0 {
for i := 0; i < rowDelta; i++ {
m.CursorDown()
}
} else if rowDelta < 0 {
for i := 0; i < -rowDelta; i++ {
m.CursorUp()
}
} else {
m.SetCursor(m.command.Range.End.Col)
}
}
m.command = &NormalCommand{}
}
case pasteMsg:
m.handlePaste(string(msg))
}
return cmd
}
func (m *Model) insertUpdate(msg tea.Msg) tea.Cmd {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
return m.SetMode(ModeNormal)
case "ctrl+k":
m.col = clamp(m.col, 0, len(m.value[m.row]))
if m.col >= len(m.value[m.row]) {
m.mergeLineBelow(m.row)
break
}
m.deleteAfterCursor()
case "ctrl+u":
m.col = clamp(m.col, 0, len(m.value[m.row]))
if m.col <= 0 {
m.mergeLineAbove(m.row)
break
}
m.deleteBeforeCursor()
case "backspace", "ctrl+h":
m.col = clamp(m.col, 0, len(m.value[m.row]))
if m.col <= 0 {
m.mergeLineAbove(m.row)
break
}
if len(m.value[m.row]) > 0 {
m.value[m.row] = append(m.value[m.row][:max(0, m.col-1)], m.value[m.row][m.col:]...)
if m.col > 0 {
m.SetCursor(m.col - 1)
}
}
case "delete", "ctrl+d":
if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {
m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][m.col+1:]...)
}
if m.col >= len(m.value[m.row]) {
m.mergeLineBelow(m.row)
break
}
case "alt+backspace", "ctrl+w":
if m.col <= 0 {
m.mergeLineAbove(m.row)
break
}
m.deleteWordLeft()
case "alt+delete", "alt+d":
m.col = clamp(m.col, 0, len(m.value[m.row]))
if m.col >= len(m.value[m.row]) {
m.mergeLineBelow(m.row)
break
}
m.deleteWordRight()
case "enter", "ctrl+m":
if len(m.value) >= maxHeight {
return nil
}
m.col = clamp(m.col, 0, len(m.value[m.row]))
m.splitLine(m.row, m.col)
case "end", "ctrl+e":
m.CursorEnd()
case "home", "ctrl+a":
m.CursorStart()
case "right", "ctrl+f":
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()
}
}
case "down", "ctrl+n":
m.CursorDown()
case "alt+right", "alt+f":
m.wordRight()
case "ctrl+v":
return Paste
case "left", "ctrl+b":
if m.col == 0 && m.row != 0 {
m.row--
m.CursorEnd()
break
}
if m.col > 0 {
m.SetCursor(m.col - 1)
}
case "up", "ctrl+p":
m.CursorUp()
case "alt+left", "alt+b":
m.wordLeft()
default:
if m.CharLimit > 0 && rw.StringWidth(m.Value()) >= m.CharLimit {
break
}
m.col = min(m.col, len(m.value[m.row]))
m.value[m.row] = append(m.value[m.row][:m.col], append(msg.Runes, m.value[m.row][m.col:]...)...)
m.SetCursor(m.col + len(msg.Runes))
}
case pasteMsg:
m.handlePaste(string(msg))
case pasteErrMsg:
m.Err = msg
}
return nil
func (m *Model) deleteRange(r Range) {
}
// View renders the text area in its current state.