feat: delete/change ranges

This commit is contained in:
Maas Lalani 2022-08-10 13:30:13 -04:00
parent fb44abddf8
commit fdfe776a07
No known key found for this signature in database
GPG Key ID: 5A6ED5CBF1A0A000
3 changed files with 174 additions and 62 deletions

View File

@ -44,6 +44,8 @@ const (
ActionDelete ActionDelete
// ActionYank yanks text. // ActionYank yanks text.
ActionYank ActionYank
// ActionChange deletes text and enters insert mode.
ActionChange
) )
// Position is a (row, column) pair representing a position of the cursor or // Position is a (row, column) pair representing a position of the cursor or

View File

@ -2,12 +2,15 @@ package textarea
import ( import (
"strconv" "strconv"
"strings"
"unicode"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
var cmd tea.Cmd var cmd tea.Cmd
var execute bool
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
@ -15,13 +18,24 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
case "esc": case "esc":
m.command = &NormalCommand{} m.command = &NormalCommand{}
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
if m.command.Count == 0 && msg.String() == "0" {
m.command.Range = Range{
Start: Position{Row: m.row, Col: m.col},
End: Position{Row: m.row, Col: 0},
}
execute = true
break
}
v := m.command.Buffer + msg.String() v := m.command.Buffer + msg.String()
count, err := strconv.Atoi(v) count, err := strconv.Atoi(v)
if err != nil { if err != nil {
count, _ = strconv.Atoi(msg.String()) count, _ = strconv.Atoi(msg.String())
m.command = &NormalCommand{Buffer: msg.String(), Count: count} m.command.Buffer = msg.String()
m.command.Count = count
} else { } else {
m.command = &NormalCommand{Buffer: v, Count: count} m.command.Buffer = v
m.command.Count = count
} }
case "G": case "G":
var row int var row int
@ -40,15 +54,38 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
m.command = &NormalCommand{Buffer: "g"} m.command = &NormalCommand{Buffer: "g"}
} }
return nil return nil
case "x":
m.command.Action = ActionDelete
m.command.Range = Range{
Start: Position{Row: m.row, Col: m.col},
End: Position{Row: m.row, Col: m.col + max(m.command.Count, 1)},
}
case "X":
m.command.Action = ActionDelete
m.command.Range = Range{
Start: Position{Row: m.row, Col: m.col},
End: Position{Row: m.row, Col: m.col - max(m.command.Count, 1)},
}
case "c":
if m.command.Action == ActionChange {
m.CursorStart()
m.deleteAfterCursor()
m.command = &NormalCommand{}
return m.SetMode(ModeInsert)
}
m.command.Action = ActionChange
case "d": case "d":
if m.command.Action == ActionDelete { if m.command.Action == ActionDelete {
for i := 0; i < max(m.command.Count, 1); i++ { for i := 0; i < max(m.command.Count, 1); i++ {
m.CursorStart() m.value[m.row] = []rune{}
m.deleteAfterCursor() if m.row < len(m.value)-1 {
m.mergeLineBelow(m.row) m.mergeLineBelow(m.row)
} else {
m.mergeLineAbove(m.row)
}
} }
m.command = &NormalCommand{} m.command = &NormalCommand{}
break return nil
} }
m.command.Action = ActionDelete m.command.Action = ActionDelete
case "y": case "y":
@ -56,16 +93,29 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
case "r": case "r":
m.command.Action = ActionReplace m.command.Action = ActionReplace
case "i": case "i":
return m.SetMode(ModeInsert) m.command.Range = Range{
Start: Position{Row: m.row, Col: m.col},
End: Position{Row: m.row, Col: m.col},
}
cmd = m.SetMode(ModeInsert)
case "I": case "I":
m.CursorStart() m.command.Range = Range{
return m.SetMode(ModeInsert) Start: Position{Row: m.row, Col: m.col},
End: Position{Row: m.row, Col: 0},
}
cmd = m.SetMode(ModeInsert)
case "a": case "a":
m.SetCursor(m.col + 1) m.command.Range = Range{
return m.SetMode(ModeInsert) Start: Position{Row: m.row, Col: m.col},
End: Position{Row: m.row, Col: m.col + 1},
}
cmd = m.SetMode(ModeInsert)
case "A": case "A":
m.CursorEnd() m.command.Range = Range{
return m.SetMode(ModeInsert) Start: Position{Row: m.row, Col: m.col},
End: Position{Row: m.row, Col: len(m.value[m.row]) + 1},
}
cmd = m.SetMode(ModeInsert)
case "^": case "^":
m.command.Range = Range{ m.command.Range = Range{
Start: Position{m.row, m.col}, Start: Position{m.row, m.col},
@ -74,23 +124,22 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
case "$": case "$":
m.command.Range = Range{ m.command.Range = Range{
Start: Position{m.row, m.col}, Start: Position{m.row, m.col},
End: Position{m.row, len(m.value[m.row]) - 1}, End: Position{m.row, len(m.value[m.row])},
} }
case "e", "E": case "e", "E":
m.command.Range = Range{ m.command.Range = Range{
Start: Position{m.row, m.col}, Start: Position{m.row, m.col},
End: m.findWordEnd(m.command.Count, msg.String() == "E"), End: m.findWordRight(max(m.command.Count, 1), msg.String() == "E"),
} }
case "w", "W": case "w", "W":
m.command.Range = Range{ m.command.Range = Range{
Start: Position{m.row, m.col}, Start: Position{m.row, m.col},
End: m.findWordStart(m.command.Count, msg.String() == "W"), End: m.findWordRight(max(m.command.Count, 1), msg.String() == "W"),
} }
case "b", "B": case "b", "B":
direction := -1
m.command.Range = Range{ m.command.Range = Range{
Start: Position{m.row, m.col}, Start: Position{m.row, m.col},
End: m.findWordStart(direction*m.command.Count, msg.String() == "B"), End: m.findWordLeft(max(m.command.Count, 1), msg.String() == "B"),
} }
case "h": case "h":
m.command.Range = Range{ m.command.Range = Range{
@ -100,18 +149,26 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
case "j": case "j":
m.command.Range = Range{ m.command.Range = Range{
Start: Position{m.row, m.col}, Start: Position{m.row, m.col},
End: Position{m.row + max(m.command.Count, 1), m.col}, End: Position{min(m.row+max(m.command.Count, 1), len(m.value)-1), m.col},
} }
case "k": case "k":
m.command.Range = Range{ m.command.Range = Range{
Start: Position{m.row, m.col}, Start: Position{m.row, m.col},
End: Position{m.row - max(m.command.Count, 1), m.col}, End: Position{max(m.row-max(m.command.Count, 1), 0), m.col},
} }
case "l": case "l":
m.command.Range = Range{ m.command.Range = Range{
Start: Position{m.row, m.col}, Start: Position{m.row, m.col},
End: Position{m.row, m.col + max(m.command.Count, 1)}, End: Position{m.row, m.col + max(m.command.Count, 1)},
} }
case "C":
m.deleteAfterCursor()
m.command = &NormalCommand{}
return m.SetMode(ModeInsert)
case "D":
m.deleteAfterCursor()
m.command = &NormalCommand{}
return nil
case "J": case "J":
m.CursorEnd() m.CursorEnd()
m.mergeLineBelow(m.row) m.mergeLineBelow(m.row)
@ -120,24 +177,16 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
cmd = Paste cmd = Paste
} }
switch msg.String() { if strings.ContainsAny(msg.String(), "iIaAewWbBhjklp$^xX") || execute {
case "i", "I", "a", "A", "e", "w", "W", "b", "B", "h", "j", "k", "l", "p", "$", "^":
switch m.command.Action { switch m.command.Action {
case ActionDelete: case ActionDelete:
m.deleteRange(m.command.Range) m.deleteRange(m.command.Range)
case ActionChange:
m.deleteRange(m.command.Range)
cmd = m.SetMode(ModeInsert)
case ActionMove: case ActionMove:
rowDelta := m.command.Range.End.Row - m.command.Range.Start.Row m.row = clamp(m.command.Range.End.Row, 0, len(m.value)-1)
if rowDelta > 0 { m.col = clamp(m.command.Range.End.Col, 0, len(m.value[m.row]))
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{} m.command = &NormalCommand{}
} }
@ -148,3 +197,92 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd {
return cmd return cmd
} }
// findWordRight locates the end of the current word or the end of the next word
// if already at the end of the current word. It takes whether or not to break
// words on spaces or any non-alpha-numeric character as an argument.
func (m *Model) findWordRight(count int, _ bool) Position {
row, col := m.row, m.col
for count > 0 {
if col < len(m.value[row])-1 && unicode.IsSpace(m.value[row][col+1]) {
col++
} else if col >= len(m.value[row])-1 && row < len(m.value)-1 {
row++
col = 0
}
for col < len(m.value[row])-1 {
if !unicode.IsSpace(m.value[row][col+1]) {
col++
} else {
count--
break
}
}
if row >= len(m.value)-1 && col >= len(m.value[row])-1 {
break
}
}
return Position{Row: row, Col: col}
}
// findWordLeft locates the start of the next word. It takes whether or not to
// break words on spaces or any non-alpha-numeric character as an argument.
func (m *Model) findWordLeft(count int, onlySpaces bool) Position {
_ = onlySpaces
row, col := m.row, m.col
for count > 0 {
if col > 0 && unicode.IsSpace(m.value[row][col-1]) {
col--
} else if col <= 0 && row > 0 {
row--
col = len(m.value[row]) - 1
}
for col > 0 {
if !unicode.IsSpace(m.value[row][col-1]) {
col--
} else {
count--
break
}
}
if row <= 0 && col <= 0 {
break
}
}
return Position{Row: row, Col: col}
}
func (m *Model) deleteRange(r Range) {
if r.Start.Row == r.End.Row && r.Start.Col == r.End.Col {
return
}
minCol, maxCol := min(r.Start.Col, r.End.Col), max(r.Start.Col, r.End.Col)
minCol = clamp(minCol, 0, len(m.value[r.Start.Row]))
maxCol = clamp(maxCol, 0, len(m.value[r.Start.Row]))
if r.Start.Row == r.End.Row {
m.value[r.Start.Row] = append(m.value[r.Start.Row][:minCol], m.value[r.Start.Row][maxCol:]...)
m.SetCursor(minCol)
return
}
minRow, maxRow := min(r.Start.Row, r.End.Row), max(r.Start.Row, r.End.Row)
for i := max(minRow, 0); i <= min(maxRow, len(m.value)-1); i++ {
m.value[i] = []rune{}
}
m.value = append(m.value[:minRow], m.value[maxRow:]...)
m.row = clamp(0, minRow, len(m.value)-1)
}

View File

@ -572,31 +572,6 @@ func (m *Model) wordRight() {
} }
} }
// findWordEnd locates the end of the current word or the end of the next word
// if already at the end of the current word. It takes whether or not to break
// words on spaces or any non-alpha-numeric character as an argument.
func (m *Model) findWordEnd(count int, onlySpaces bool) Position {
_ = count
_ = onlySpaces
row, col := m.row, m.col
for col < len(m.value[row]) {
if !unicode.IsSpace(m.value[row][col]) {
col++
} else {
break
}
}
return Position{Row: row, Col: col}
}
// findWordStart locates the start of the next word. It takes whether or not to
// break words on spaces or any non-alpha-numeric character as an argument.
func (m *Model) findWordStart(count int, onlySpaces bool) Position {
return m.findWordEnd(count, onlySpaces)
}
// LineInfo returns the number of characters from the start of the // LineInfo returns the number of characters from the start of the
// (soft-wrapped) line and the (soft-wrapped) line width. // (soft-wrapped) line and the (soft-wrapped) line width.
func (m Model) LineInfo() LineInfo { func (m Model) LineInfo() LineInfo {
@ -733,9 +708,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
func (m *Model) deleteRange(r Range) {
}
// View renders the text area in its current state. // View renders the text area in its current state.
func (m Model) View() string { func (m Model) View() string {
if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" { if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {