270 lines
4.9 KiB
Go
270 lines
4.9 KiB
Go
package editor
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
type cursor struct {
|
|
x int
|
|
y int
|
|
}
|
|
|
|
type Model struct {
|
|
lines []string
|
|
cursor cursor
|
|
anchor cursor // starting point for visual modes
|
|
mode action.Mode
|
|
win_h int
|
|
win_w int
|
|
command string
|
|
input *input.Handler
|
|
|
|
// Insert repetition
|
|
insertCount int
|
|
insertKeys []string
|
|
insertAction action.Action
|
|
|
|
// Settings
|
|
gutterSize int
|
|
tabSize int
|
|
}
|
|
|
|
func NewModel(lines []string, pos action.Position) Model {
|
|
return Model{
|
|
lines: lines,
|
|
cursor: cursor{
|
|
x: pos.Col,
|
|
y: pos.Line,
|
|
},
|
|
gutterSize: 5,
|
|
tabSize: 2,
|
|
mode: action.NormalMode,
|
|
command: "",
|
|
input: input.NewHandler(),
|
|
}
|
|
}
|
|
|
|
func (m Model) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
// Implement action.Model interface
|
|
|
|
func (m *Model) Lines() []string {
|
|
return m.lines
|
|
}
|
|
|
|
func (m *Model) Line(idx int) string {
|
|
if idx < 0 || idx >= len(m.lines) {
|
|
return ""
|
|
}
|
|
return m.lines[idx]
|
|
}
|
|
|
|
func (m *Model) SetLine(idx int, content string) {
|
|
if idx >= 0 && idx < len(m.lines) {
|
|
m.lines[idx] = content
|
|
}
|
|
}
|
|
|
|
func (m *Model) InsertLine(idx int, content string) {
|
|
if idx < 0 {
|
|
idx = 0
|
|
}
|
|
if idx > len(m.lines) {
|
|
idx = len(m.lines)
|
|
}
|
|
m.lines = append(m.lines[:idx], append([]string{content}, m.lines[idx:]...)...)
|
|
}
|
|
|
|
func (m *Model) DeleteLine(idx int) {
|
|
if idx >= 0 && idx < len(m.lines) {
|
|
m.lines = append(m.lines[:idx], m.lines[idx+1:]...)
|
|
}
|
|
}
|
|
|
|
func (m *Model) LineCount() int {
|
|
return len(m.lines)
|
|
}
|
|
|
|
func (m *Model) CursorX() int {
|
|
return m.cursor.x
|
|
}
|
|
|
|
func (m *Model) CursorY() int {
|
|
return m.cursor.y
|
|
}
|
|
|
|
func (m *Model) SetCursorX(x int) {
|
|
m.cursor.x = x
|
|
}
|
|
|
|
func (m *Model) SetCursorY(y int) {
|
|
m.cursor.y = y
|
|
}
|
|
|
|
// Anchor methods
|
|
func (m *Model) AnchorX() int {
|
|
return m.anchor.x
|
|
}
|
|
|
|
func (m *Model) AnchorY() int {
|
|
return m.anchor.y
|
|
}
|
|
|
|
func (m *Model) SetAnchorX(x int) {
|
|
m.anchor.x = x
|
|
}
|
|
|
|
func (m *Model) SetAnchorY(y int) {
|
|
m.anchor.y = y
|
|
}
|
|
|
|
func (m *Model) ClampCursorX() {
|
|
lineLen := len(m.lines[m.cursor.y])
|
|
if lineLen == 0 {
|
|
m.cursor.x = 0
|
|
} else if m.cursor.x >= lineLen {
|
|
m.cursor.x = lineLen
|
|
}
|
|
}
|
|
|
|
func (m *Model) Mode() action.Mode {
|
|
return m.mode
|
|
}
|
|
|
|
func (m *Model) SetMode(mode action.Mode) {
|
|
m.mode = mode
|
|
}
|
|
|
|
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
|
m.insertCount = count
|
|
m.insertKeys = []string{}
|
|
m.insertAction = act
|
|
}
|
|
|
|
func (m *Model) GetCursorPosition() action.Position {
|
|
return action.Position{Line: m.cursor.y, Col: m.cursor.x}
|
|
}
|
|
|
|
func (m *Model) replayInsert() {
|
|
// Replay (count - 1) more times
|
|
for i := 1; i < m.insertCount; i++ {
|
|
// For 'o' and 'O', we need to create a new line first
|
|
switch m.insertAction.(type) {
|
|
case action.OpenLineBelow:
|
|
pos := m.cursor.y
|
|
m.lines = append(m.lines[:pos+1], append([]string{""}, m.lines[pos+1:]...)...)
|
|
m.cursor.y++
|
|
m.cursor.x = 0
|
|
case action.OpenLineAbove:
|
|
pos := m.cursor.y
|
|
m.lines = append(m.lines[:pos], append([]string{""}, m.lines[pos:]...)...)
|
|
m.cursor.x = 0
|
|
// 'i' and 'a' don't need setup - just replay keys
|
|
}
|
|
|
|
// Replay each recorded keystroke
|
|
for _, key := range m.insertKeys {
|
|
m.processInsertKey(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Model) processInsertKey(key string) {
|
|
x := m.CursorX()
|
|
y := m.CursorY()
|
|
l := m.Line(y)
|
|
|
|
switch key {
|
|
case "enter":
|
|
|
|
// Simple case, at end, just create a line
|
|
if x == len(l) {
|
|
m.InsertLine(y+1, "")
|
|
|
|
// otherwise, splice
|
|
} else {
|
|
m.SetLine(y, l[:x])
|
|
m.InsertLine(y+1, l[x:])
|
|
}
|
|
|
|
m.SetCursorY(y + 1)
|
|
m.SetCursorX(0)
|
|
|
|
case "backspace":
|
|
if x > 0 {
|
|
m.SetLine(y, l[:x-1]+l[x:])
|
|
m.SetCursorX(x - 1)
|
|
} else if y > 0 {
|
|
prevLine := m.Line(y - 1)
|
|
newX := len(prevLine)
|
|
m.SetLine(y-1, prevLine+l)
|
|
m.DeleteLine(y)
|
|
m.SetCursorY(y - 1)
|
|
m.SetCursorX(newX)
|
|
}
|
|
|
|
case "delete":
|
|
if x == len(l) && y < m.LineCount() {
|
|
nextLine := m.Line(y + 1)
|
|
m.SetLine(y, l+nextLine)
|
|
m.DeleteLine(y + 1)
|
|
} else if x >= 0 {
|
|
m.SetLine(y, l[:x]+l[x+1:])
|
|
}
|
|
|
|
// TODO: This handling is wrong, we should be able to delete an entire tab with a single space
|
|
case "tab":
|
|
tabs := strings.Repeat(" ", m.tabSize)
|
|
if x < len(l) {
|
|
m.SetLine(y, l[:x]+tabs+l[x:])
|
|
} else {
|
|
m.SetLine(y, l+tabs)
|
|
}
|
|
m.SetCursorX(x + len(tabs))
|
|
|
|
case "up":
|
|
if y > 0 {
|
|
m.SetCursorY(y - 1)
|
|
m.ClampCursorX()
|
|
}
|
|
|
|
case "down":
|
|
if y+1 < m.LineCount() {
|
|
m.SetCursorY(y + 1)
|
|
m.ClampCursorX()
|
|
}
|
|
|
|
case "left":
|
|
if x > 0 {
|
|
m.SetCursorX(x - 1)
|
|
} else if y > 0 {
|
|
prevLine := m.Line(y - 1)
|
|
m.SetCursorX(len(prevLine))
|
|
m.SetCursorY(y - 1)
|
|
}
|
|
|
|
case "right":
|
|
if x < len(l) {
|
|
m.SetCursorX(x + 1)
|
|
} else if y+1 < m.LineCount() {
|
|
m.SetCursorX(0)
|
|
m.SetCursorY(y + 1)
|
|
}
|
|
|
|
// Regular character
|
|
default:
|
|
if x < len(l) {
|
|
m.SetLine(y, l[:x]+key+l[x:])
|
|
} else {
|
|
m.SetLine(y, l+key)
|
|
}
|
|
m.SetCursorX(x + len(key))
|
|
}
|
|
}
|