Now we can load them in via JSON files at launch time. They are embded in the final exe though...
450 lines
9.4 KiB
Go
450 lines
9.4 KiB
Go
package editor
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
|
"git.gophernest.net/azpect/TextEditor/internal/syntax"
|
|
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
type Model struct {
|
|
// Buffers
|
|
buffers []*core.Buffer
|
|
//next buffer id?
|
|
|
|
// Windows
|
|
windows []*core.Window
|
|
activeWindowId int
|
|
|
|
// Editor wide state
|
|
mode core.Mode
|
|
|
|
// Terminal dimensions
|
|
termWidth int
|
|
termHeight int
|
|
|
|
// Input and key handling
|
|
input *input.Handler
|
|
|
|
// Insert mode state & repetition (applied to active window)
|
|
insertCount int
|
|
insertKeys []string
|
|
insertAction action.Action
|
|
lastFind core.LastFindCommand
|
|
|
|
// Command line state
|
|
command string
|
|
commandCursor int
|
|
commandOutput *core.CommandOutput
|
|
commandHistory []string
|
|
commandHistoryCursor int
|
|
|
|
// Global settings
|
|
settings core.EditorSettings
|
|
|
|
// Registers
|
|
registers map[rune]core.Register // name -> register
|
|
|
|
// Visual styles
|
|
currentTheme string // Name of current theme
|
|
themes map[string]theme.EditorTheme
|
|
syntax syntax.Engine
|
|
|
|
// Dot operator state
|
|
lastChangeKeys []string
|
|
}
|
|
|
|
// Model.Init: Initialize the model and start any commands that may need to run. Required
|
|
// for the bubbletea architecture.
|
|
func (m Model) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
// Implement action.Model interface
|
|
|
|
// ==================================================
|
|
// Core Data Access
|
|
// ==================================================
|
|
func (m *Model) Windows() []*core.Window {
|
|
return m.windows
|
|
}
|
|
|
|
func (m *Model) ActiveWindow() *core.Window {
|
|
winId := m.activeWindowId
|
|
for i := range m.Windows() {
|
|
if m.windows[i].Id == winId {
|
|
return m.windows[i]
|
|
}
|
|
}
|
|
panic("Could not find window")
|
|
}
|
|
|
|
func (m *Model) Buffers() []*core.Buffer {
|
|
return m.buffers
|
|
}
|
|
|
|
func (m *Model) SetBuffers(bufs []*core.Buffer) {
|
|
m.buffers = bufs
|
|
m.bindBufferSyntaxHooks(bufs)
|
|
}
|
|
|
|
func (m *Model) ActiveBuffer() *core.Buffer {
|
|
win := m.ActiveWindow()
|
|
return win.Buffer
|
|
}
|
|
|
|
// ==================================================
|
|
// Insert Mode Methods
|
|
// ==================================================
|
|
func (m *Model) InsertKeys() []string {
|
|
return m.insertKeys
|
|
}
|
|
|
|
func (m *Model) SetInsertKeys(keys []string) {
|
|
m.insertKeys = keys
|
|
}
|
|
|
|
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
|
m.insertCount = count
|
|
m.insertKeys = []string{}
|
|
m.insertAction = act
|
|
}
|
|
|
|
func (m *Model) SetLastFind(char string, forward, inclusive bool) {
|
|
m.lastFind = core.LastFindCommand{
|
|
Char: char,
|
|
Forward: forward,
|
|
Inclusive: inclusive,
|
|
}
|
|
}
|
|
|
|
func (m *Model) GetLastFind() *core.LastFindCommand {
|
|
return &m.lastFind
|
|
}
|
|
|
|
// Does update the '.' register
|
|
func (m *Model) SetLastChangeKeys(keys []string) {
|
|
m.lastChangeKeys = keys
|
|
|
|
m.SetRegister('.', core.CharwiseRegister, []string{strings.Join(keys, "")})
|
|
}
|
|
|
|
func (m *Model) LastChangeKeys() []string {
|
|
return m.lastChangeKeys
|
|
}
|
|
|
|
func (m *Model) ClearLastChangeKeys() {
|
|
m.lastChangeKeys = []string{}
|
|
}
|
|
|
|
func (m *Model) HandleKey(key string) tea.Cmd {
|
|
return m.input.Handle(m, key)
|
|
}
|
|
|
|
func (m *Model) ExitInsertMode() {
|
|
win := m.ActiveWindow()
|
|
if m.insertCount > 1 {
|
|
m.replayInsert()
|
|
}
|
|
if win.Cursor.Col > 0 {
|
|
win.Cursor.Col--
|
|
}
|
|
m.mode = core.NormalMode
|
|
m.insertCount = 0
|
|
m.insertKeys = nil
|
|
}
|
|
|
|
func (m *Model) replayInsert() {
|
|
win := m.ActiveWindow()
|
|
buf := m.ActiveBuffer()
|
|
|
|
// 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 := win.Cursor.Line
|
|
buf.InsertLine(pos+1, "")
|
|
win.SetCursorLine(pos + 1)
|
|
|
|
case action.OpenLineAbove:
|
|
pos := win.Cursor.Line
|
|
buf.InsertLine(pos, "")
|
|
|
|
// 'i' and 'a' don't need setup - just replay keys
|
|
}
|
|
|
|
// Replay each recorded keystroke
|
|
for _, key := range m.insertKeys {
|
|
m.processInsertKey(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Fix this shitty shit shit shit
|
|
func (m *Model) processInsertKey(key string) {
|
|
win := m.ActiveWindow()
|
|
buf := m.ActiveBuffer()
|
|
|
|
col := win.Cursor.Col
|
|
line := win.Cursor.Line
|
|
l := buf.Line(line)
|
|
|
|
switch key {
|
|
case "enter":
|
|
if col == len(l) {
|
|
buf.InsertLine(line+1, "")
|
|
} else {
|
|
buf.SetLine(line, l[:col])
|
|
buf.InsertLine(line+1, l[col:])
|
|
}
|
|
win.SetCursorLine(line + 1)
|
|
win.SetCursorCol(0)
|
|
|
|
case "backspace":
|
|
if col > 0 {
|
|
buf.SetLine(line, l[:col-1]+l[col:])
|
|
win.SetCursorCol(col - 1)
|
|
} else if line > 0 {
|
|
prevLine := buf.Line(line - 1)
|
|
newCol := len(prevLine)
|
|
buf.SetLine(line-1, prevLine+l)
|
|
buf.DeleteLine(line)
|
|
win.SetCursorLine(line - 1)
|
|
win.SetCursorCol(newCol)
|
|
}
|
|
|
|
case "delete":
|
|
if col == len(l) && line < buf.LineCount()-1 {
|
|
nextLine := buf.Line(line + 1)
|
|
buf.SetLine(line, l+nextLine)
|
|
buf.DeleteLine(line + 1)
|
|
} else if col < len(l) {
|
|
buf.SetLine(line, l[:col]+l[col+1:])
|
|
}
|
|
|
|
case "tab":
|
|
tabs := strings.Repeat(" ", m.Settings().TabStop)
|
|
if col < len(l) {
|
|
buf.SetLine(line, l[:col]+tabs+l[col:])
|
|
} else {
|
|
buf.SetLine(line, l+tabs)
|
|
}
|
|
win.SetCursorCol(col + len(tabs))
|
|
|
|
case "up":
|
|
if line > 0 {
|
|
win.SetCursorLine(line - 1)
|
|
}
|
|
|
|
case "down":
|
|
if line+1 < buf.LineCount() {
|
|
win.SetCursorLine(line + 1)
|
|
}
|
|
|
|
case "left":
|
|
if col > 0 {
|
|
win.SetCursorCol(col - 1)
|
|
} else if line > 0 {
|
|
prevLine := buf.Line(line - 1)
|
|
win.SetCursorCol(len(prevLine))
|
|
win.SetCursorLine(line - 1)
|
|
}
|
|
|
|
case "right":
|
|
if col < len(l) {
|
|
win.SetCursorCol(col + 1)
|
|
} else if line+1 < buf.LineCount() {
|
|
win.SetCursorCol(0)
|
|
win.SetCursorLine(line + 1)
|
|
}
|
|
|
|
default:
|
|
if col < len(l) {
|
|
buf.SetLine(line, l[:col]+key+l[col:])
|
|
} else {
|
|
buf.SetLine(line, l+key)
|
|
}
|
|
win.SetCursorCol(col + len(key))
|
|
}
|
|
}
|
|
|
|
// ==================================================
|
|
// Command Mode State
|
|
// ==================================================
|
|
func (m *Model) Command() string {
|
|
return m.command
|
|
}
|
|
|
|
func (m *Model) SetCommand(cmd string) {
|
|
m.command = cmd
|
|
}
|
|
|
|
func (m *Model) CommandCursor() int {
|
|
return m.commandCursor
|
|
}
|
|
|
|
func (m *Model) SetCommandCursor(cur int) {
|
|
if cur < 0 {
|
|
m.commandCursor = 0
|
|
} else if cur >= len(m.command) {
|
|
m.commandCursor = len(m.command)
|
|
} else {
|
|
m.commandCursor = cur
|
|
}
|
|
}
|
|
|
|
func (m *Model) CommandOutput() *core.CommandOutput {
|
|
return m.commandOutput
|
|
}
|
|
|
|
func (m *Model) SetCommandOutput(out *core.CommandOutput) {
|
|
m.commandOutput = out
|
|
}
|
|
|
|
func (m *Model) CommandHistory() []string {
|
|
return m.commandHistory
|
|
}
|
|
|
|
func (m *Model) SetCommandHistory(history []string) {
|
|
m.commandHistory = history
|
|
}
|
|
|
|
func (m *Model) CommandHistoryCursor() int {
|
|
return m.commandHistoryCursor
|
|
}
|
|
|
|
func (m *Model) SetCommandHistoryCursor(cur int) {
|
|
m.commandHistoryCursor = cur
|
|
}
|
|
|
|
// ==================================================
|
|
// Editor-wide State
|
|
// ==================================================
|
|
func (m *Model) Mode() core.Mode {
|
|
return m.mode
|
|
}
|
|
|
|
func (m *Model) SetMode(mode core.Mode) {
|
|
m.mode = mode
|
|
}
|
|
|
|
func (m *Model) Settings() core.EditorSettings {
|
|
return m.settings
|
|
}
|
|
|
|
func (m *Model) SetSettings(s core.EditorSettings) {
|
|
m.settings = s
|
|
}
|
|
|
|
// ==================================================
|
|
// Themes
|
|
// ==================================================
|
|
func (m *Model) Theme() (string, theme.EditorTheme) {
|
|
t, ok := m.themes[m.currentTheme]
|
|
if ok {
|
|
return m.currentTheme, t
|
|
}
|
|
return "default", m.themes["default"]
|
|
}
|
|
|
|
func (m *Model) SetTheme(name string) {
|
|
m.currentTheme = name
|
|
|
|
if m.syntax == nil {
|
|
return
|
|
}
|
|
|
|
// Need to invalidate the buffers to force a redraw
|
|
for _, buf := range m.buffers {
|
|
if buf == nil {
|
|
continue
|
|
}
|
|
m.syntax.InvalidateBuffer(buf)
|
|
}
|
|
}
|
|
|
|
func (m *Model) Themes() map[string]theme.EditorTheme {
|
|
return m.themes
|
|
}
|
|
|
|
func (m *Model) SetThemes(t map[string]theme.EditorTheme) {
|
|
m.themes = t
|
|
}
|
|
|
|
func (m *Model) Syntax() syntax.Engine {
|
|
return m.syntax
|
|
}
|
|
|
|
func (m *Model) SetSyntax(s syntax.Engine) {
|
|
m.syntax = s
|
|
m.bindBufferSyntaxHooks(m.buffers)
|
|
}
|
|
|
|
func (m *Model) bindBufferSyntaxHooks(bufs []*core.Buffer) {
|
|
if m.syntax == nil {
|
|
return
|
|
}
|
|
|
|
for _, buf := range bufs {
|
|
if buf == nil {
|
|
continue
|
|
}
|
|
|
|
b := buf
|
|
b.OnChange = func(change core.BufferChange) {
|
|
if change.Edit != nil {
|
|
m.syntax.ApplyEdit(b, change.Edit)
|
|
return
|
|
}
|
|
|
|
switch change.Kind {
|
|
case core.BufferChangeSetLine:
|
|
m.syntax.InvalidateLines(b, change.StartLine, change.EndLine)
|
|
default:
|
|
m.syntax.InvalidateBuffer(b)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================================================
|
|
// Registers
|
|
// ==================================================
|
|
func (m *Model) Registers() map[rune]core.Register {
|
|
return m.registers
|
|
}
|
|
|
|
func (m *Model) GetRegister(name rune) (core.Register, bool) {
|
|
reg, found := m.registers[name]
|
|
return reg, found
|
|
}
|
|
|
|
func (m *Model) SetRegister(name rune, t core.RegisterType, cnt []string) error {
|
|
if _, found := m.GetRegister(name); !found {
|
|
return fmt.Errorf("Register '%c' does not exist.", name)
|
|
}
|
|
|
|
// TODO: This might be slow, pointers maybe?
|
|
reg := core.Register{Type: t, Content: cnt}
|
|
m.registers[name] = reg
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
|
|
// Shift numbered registers: 0 -> 1 -> 2 -> ... -> 9 -> _ (discarded)
|
|
for i := rune('9'); i > '0'; i-- {
|
|
m.registers[i] = m.registers[i-1]
|
|
}
|
|
|
|
// 0 and " both hold the new content independently
|
|
m.SetRegister('0', t, cnt)
|
|
m.SetRegister('"', t, cnt)
|
|
}
|