From fb44abddf8e3f232f63ad854b9f4226435de1335 Mon Sep 17 00:00:00 2001 From: Maas Lalani Date: Tue, 9 Aug 2022 16:25:31 -0400 Subject: [PATCH] refactor: split files into {modal,normal,insert}.go --- textarea/insert.go | 117 ++++++++++++++++ textarea/modal.go | 75 ++++++++++ textarea/normal.go | 150 ++++++++++++++++++++ textarea/textarea.go | 316 +------------------------------------------ 4 files changed, 343 insertions(+), 315 deletions(-) create mode 100644 textarea/insert.go create mode 100644 textarea/modal.go create mode 100644 textarea/normal.go diff --git a/textarea/insert.go b/textarea/insert.go new file mode 100644 index 0000000..2337ab4 --- /dev/null +++ b/textarea/insert.go @@ -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 +} diff --git a/textarea/modal.go b/textarea/modal.go new file mode 100644 index 0000000..8edf8c4 --- /dev/null +++ b/textarea/modal.go @@ -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 +} diff --git a/textarea/normal.go b/textarea/normal.go new file mode 100644 index 0000000..a9fec1f --- /dev/null +++ b/textarea/normal.go @@ -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 +} diff --git a/textarea/textarea.go b/textarea/textarea.go index 12f31a4..5667f58 100644 --- a/textarea/textarea.go +++ b/textarea/textarea.go @@ -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.