mirror of
https://github.com/Maks1mS/bubbles.git
synced 2025-03-13 12:33:43 +03:00
feat: ranges and motions
This commit is contained in:
parent
57a01e62e3
commit
447ff2da6a
@ -2,6 +2,7 @@ package textarea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@ -62,6 +63,52 @@ 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
|
||||
@ -132,6 +179,9 @@ type Model struct {
|
||||
// mode is the current mode of the textarea.
|
||||
mode Mode
|
||||
|
||||
// command is the normal command of the textarea.
|
||||
command *NormalCommand
|
||||
|
||||
// Last character offset, used to maintain state when the cursor is moved
|
||||
// vertically such that we can maintain the same navigating position.
|
||||
lastCharOffset int
|
||||
@ -168,6 +218,7 @@ func New() Model {
|
||||
col: 0,
|
||||
row: 0,
|
||||
lineNumberFormat: "%2v ",
|
||||
command: &NormalCommand{},
|
||||
|
||||
viewport: &vp,
|
||||
}
|
||||
@ -269,6 +320,11 @@ func (m Model) Line() int {
|
||||
return m.row
|
||||
}
|
||||
|
||||
// NormalCommand returns the normal command.
|
||||
func (m *Model) NormalCommand() *NormalCommand {
|
||||
return m.command
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down by one line.
|
||||
// Returns whether or not the cursor blink should be reset.
|
||||
func (m *Model) CursorDown() {
|
||||
@ -573,6 +629,31 @@ 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
|
||||
// (soft-wrapped) line and the (soft-wrapped) line width.
|
||||
func (m Model) LineInfo() LineInfo {
|
||||
@ -723,44 +804,139 @@ func (m *Model) SetMode(mode Mode) tea.Cmd {
|
||||
}
|
||||
|
||||
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 "e":
|
||||
m.wordRight()
|
||||
case "w":
|
||||
m.wordRight()
|
||||
case "W":
|
||||
m.wordRight()
|
||||
case "b":
|
||||
m.wordLeft()
|
||||
case "B":
|
||||
m.wordLeft()
|
||||
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.SetCursor(m.col - 1)
|
||||
m.command.Range = Range{
|
||||
Start: Position{m.row, m.col},
|
||||
End: Position{m.row, m.col - max(m.command.Count, 1)},
|
||||
}
|
||||
case "j":
|
||||
m.CursorDown()
|
||||
m.command.Range = Range{
|
||||
Start: Position{m.row, m.col},
|
||||
End: Position{m.row + max(m.command.Count, 1), m.col},
|
||||
}
|
||||
case "k":
|
||||
m.CursorUp()
|
||||
m.command.Range = Range{
|
||||
Start: Position{m.row, m.col},
|
||||
End: Position{m.row - max(m.command.Count, 1), m.col},
|
||||
}
|
||||
case "l":
|
||||
m.SetCursor(m.col + 1)
|
||||
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":
|
||||
return Paste
|
||||
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 nil
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (m *Model) insertUpdate(msg tea.Msg) tea.Cmd {
|
||||
|
Loading…
Reference in New Issue
Block a user