mirror of
https://github.com/Maks1mS/bubbles.git
synced 2025-01-12 06:51:05 +03:00
feat: delete/change ranges
This commit is contained in:
parent
fb44abddf8
commit
fdfe776a07
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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 != "" {
|
||||||
|
Loading…
Reference in New Issue
Block a user