wip: cleaned up the separation of concerns in the MWB model
This commit is contained in:
parent
ea4638d815
commit
770cbcceb7
@ -6,44 +6,29 @@ import (
|
|||||||
|
|
||||||
// Model defines the interface for editor state that actions can modify
|
// Model defines the interface for editor state that actions can modify
|
||||||
type Model interface {
|
type Model interface {
|
||||||
// Text buffer
|
// ==================================================
|
||||||
Lines() []string
|
// Core Data Access
|
||||||
Line(idx int) string
|
// ==================================================
|
||||||
SetLine(idx int, content string)
|
|
||||||
InsertLine(idx int, content string)
|
|
||||||
DeleteLine(idx int)
|
|
||||||
LineCount() int
|
|
||||||
|
|
||||||
// Cursor
|
|
||||||
CursorX() int
|
|
||||||
CursorY() int
|
|
||||||
SetCursorX(x int)
|
|
||||||
SetCursorY(y int)
|
|
||||||
ClampCursorX()
|
|
||||||
|
|
||||||
// Windows
|
|
||||||
Windows() []*Window
|
Windows() []*Window
|
||||||
ActiveWindowId() int
|
|
||||||
ActiveWindow() *Window
|
ActiveWindow() *Window
|
||||||
|
Buffers() []*Buffer
|
||||||
|
ActiveBuffer() *Buffer
|
||||||
|
|
||||||
// Window
|
// ==================================================
|
||||||
ScrollY() int
|
// Insert Mode State
|
||||||
SetScrollY(y int)
|
// ==================================================
|
||||||
WinH() int
|
|
||||||
WinW() int
|
|
||||||
ViewPortH() int
|
|
||||||
|
|
||||||
// Anchor
|
|
||||||
AnchorX() int
|
|
||||||
AnchorY() int
|
|
||||||
SetAnchorX(x int)
|
|
||||||
SetAnchorY(y int)
|
|
||||||
|
|
||||||
// Insert
|
|
||||||
InsertKeys() []string
|
InsertKeys() []string
|
||||||
SetInsertKeys(keys []string)
|
SetInsertKeys(keys []string)
|
||||||
|
|
||||||
// Command mode
|
// Insert recording (for count replay)
|
||||||
|
SetInsertRecording(count int, action Action)
|
||||||
|
|
||||||
|
// ExitInsertMode handles replay, cursor step-back, and mode transition on esc
|
||||||
|
ExitInsertMode()
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Command Mode State
|
||||||
|
// ==================================================
|
||||||
Command() string
|
Command() string
|
||||||
SetCommand(cmd string)
|
SetCommand(cmd string)
|
||||||
CommandCursor() int
|
CommandCursor() int
|
||||||
@ -53,25 +38,54 @@ type Model interface {
|
|||||||
CommandOutput() string
|
CommandOutput() string
|
||||||
SetCommandOutput(out string)
|
SetCommandOutput(out string)
|
||||||
|
|
||||||
// Settings
|
// ==================================================
|
||||||
|
// Editor-wide State
|
||||||
|
// ==================================================
|
||||||
|
Mode() Mode
|
||||||
|
SetMode(mode Mode)
|
||||||
|
|
||||||
Settings() Settings
|
Settings() Settings
|
||||||
SetSettings(s Settings)
|
SetSettings(s Settings)
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
// Registers
|
// Registers
|
||||||
|
// ==================================================
|
||||||
Registers() map[rune]Register
|
Registers() map[rune]Register
|
||||||
GetRegister(name rune) (Register, bool)
|
GetRegister(name rune) (Register, bool)
|
||||||
SetRegister(name rune, t RegisterType, cnt []string) error
|
SetRegister(name rune, t RegisterType, cnt []string) error
|
||||||
UpdateDefaultRegister(t RegisterType, cnt []string)
|
UpdateDefaultRegister(t RegisterType, cnt []string)
|
||||||
|
|
||||||
// Mode
|
// ==================================================
|
||||||
Mode() Mode
|
// Depreciated
|
||||||
SetMode(mode Mode)
|
// ==================================================
|
||||||
|
// Text buffer
|
||||||
|
// Lines() []string
|
||||||
|
// Line(idx int) string
|
||||||
|
// SetLine(idx int, content string)
|
||||||
|
// InsertLine(idx int, content string)
|
||||||
|
// DeleteLine(idx int)
|
||||||
|
// LineCount() int
|
||||||
|
|
||||||
// Insert recording (for count replay)
|
// Cursor
|
||||||
SetInsertRecording(count int, action Action)
|
// CursorX() int
|
||||||
|
// CursorY() int
|
||||||
|
// SetCursorX(x int)
|
||||||
|
// SetCursorY(y int)
|
||||||
|
// ClampCursorX()
|
||||||
|
|
||||||
|
// Window
|
||||||
|
// ScrollY() int
|
||||||
|
// SetScrollY(y int)
|
||||||
|
// WinH() int
|
||||||
|
// WinW() int
|
||||||
|
// ViewPortH() int
|
||||||
|
//
|
||||||
|
// Anchor
|
||||||
|
// AnchorX() int
|
||||||
|
// AnchorY() int
|
||||||
|
// SetAnchorX(x int)
|
||||||
|
// SetAnchorY(y int)
|
||||||
|
|
||||||
// ExitInsertMode handles replay, cursor step-back, and mode transition on esc
|
|
||||||
ExitInsertMode()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position represents a location in the buffer
|
// Position represents a location in the buffer
|
||||||
|
|||||||
@ -5,6 +5,7 @@ type WinOptions struct {
|
|||||||
// Number bool
|
// Number bool
|
||||||
// Wrap bool
|
// Wrap bool
|
||||||
// Relnumber bool
|
// Relnumber bool
|
||||||
|
ScrollOff int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Window struct {
|
type Window struct {
|
||||||
@ -12,33 +13,73 @@ type Window struct {
|
|||||||
Number int // Ignored for now, will be used when splits come into play
|
Number int // Ignored for now, will be used when splits come into play
|
||||||
Buffer *Buffer
|
Buffer *Buffer
|
||||||
|
|
||||||
Cursor Position
|
Cursor Position // DO NOT MODIFY DIRECTLY, USE SETTERS
|
||||||
Anchor Position
|
Anchor Position
|
||||||
|
|
||||||
ScrollY int
|
ScrollY int
|
||||||
Height int
|
Height int
|
||||||
Width int
|
Width int
|
||||||
|
|
||||||
// Folds // TODO
|
// Folds TODO
|
||||||
// Options WinOptions
|
Options WinOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// Helper methods
|
// Helper methods
|
||||||
// ==================================================
|
// ==================================================
|
||||||
|
|
||||||
// Window.ClampCursorX: Clamps the cursor in the X direction to ensure the cursor
|
// Window.ClampCursor: Clamps the cursor in the all directions to ensure the cursor
|
||||||
// does not go into an invalid position. Such as negative values or past the end of
|
// does not go into an invalid position. Such as negative values or past the end of
|
||||||
// the line.
|
// the line. In the Y direction it validates that the cursor does not pass the end
|
||||||
func (w *Window) ClampCursorX() {
|
// of the content or attempt to be "above" the content (negative value).
|
||||||
lineLen := len(w.Buffer.Lines[w.Cursor.Line])
|
func (w *Window) clampCursor() {
|
||||||
if lineLen == 0 {
|
// Clamp line to valid range [0, lineCount-1]
|
||||||
|
maxLine := w.Buffer.LineCount() - 1
|
||||||
|
if maxLine < 0 {
|
||||||
|
maxLine = 0 // Empty buffer edge case
|
||||||
|
}
|
||||||
|
if w.Cursor.Line < 0 {
|
||||||
|
w.Cursor.Line = 0
|
||||||
|
} else if w.Cursor.Line > maxLine {
|
||||||
|
w.Cursor.Line = maxLine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp column to valid range [0, lineLen]
|
||||||
|
lineLen := len(w.Buffer.Lines[w.Cursor.Line]) // Safe now - Line is valid
|
||||||
|
if w.Cursor.Col < 0 {
|
||||||
|
w.Cursor.Col = 0
|
||||||
|
} else if lineLen == 0 {
|
||||||
w.Cursor.Col = 0
|
w.Cursor.Col = 0
|
||||||
} else if w.Cursor.Col >= lineLen {
|
} else if w.Cursor.Col >= lineLen {
|
||||||
w.Cursor.Col = lineLen
|
w.Cursor.Col = lineLen // Allow cursor after last char (insert mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Window.AdjustScroll ensures the cursor stays within the height with scrollOff margins.
|
||||||
|
// Call this after any cursor movement.
|
||||||
|
func (w *Window) AdjustScroll() {
|
||||||
|
if w.Height <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effective scrollOff (can't be more than half the viewport)
|
||||||
|
off := min(w.Options.ScrollOff, w.Height/2)
|
||||||
|
|
||||||
|
// Cursor too close to top — scroll up
|
||||||
|
if w.Cursor.Line < w.ScrollY+off {
|
||||||
|
w.ScrollY = w.Cursor.Line - off
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor too close to bottom — scroll down
|
||||||
|
if w.Cursor.Line > w.ScrollY+w.Height-1-off {
|
||||||
|
w.ScrollY = w.Cursor.Line - w.Height + 1 + off
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp scrollY to valid range
|
||||||
|
maxScroll := max(0, w.Buffer.LineCount()-w.Height)
|
||||||
|
w.ScrollY = max(0, min(w.ScrollY, maxScroll))
|
||||||
|
}
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// Setters
|
// Setters
|
||||||
// ==================================================
|
// ==================================================
|
||||||
@ -58,16 +99,19 @@ func (w *Window) SetBuffer(buffer *Buffer) {
|
|||||||
// Window.SetCursor: Sets the cursor position in this window to the given position.
|
// Window.SetCursor: Sets the cursor position in this window to the given position.
|
||||||
func (w *Window) SetCursor(cursor Position) {
|
func (w *Window) SetCursor(cursor Position) {
|
||||||
w.Cursor = cursor
|
w.Cursor = cursor
|
||||||
|
w.clampCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetCursorLine: Sets the line number of the cursor position.
|
// Window.SetCursorLine: Sets the line number of the cursor position.
|
||||||
func (w *Window) SetCursorLine(line int) {
|
func (w *Window) SetCursorLine(line int) {
|
||||||
w.Cursor.Line = line
|
w.Cursor.Line = line
|
||||||
|
w.clampCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetCursorCol: Sets the column number of the cursor position.
|
// Window.SetCursorCol: Sets the column number of the cursor position.
|
||||||
func (w *Window) SetCursorCol(col int) {
|
func (w *Window) SetCursorCol(col int) {
|
||||||
w.Cursor.Col = col
|
w.Cursor.Col = col
|
||||||
|
w.clampCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetCursorPos: Sets both the line and column of the cursor position. This is
|
// Window.SetCursorPos: Sets both the line and column of the cursor position. This is
|
||||||
@ -75,6 +119,7 @@ func (w *Window) SetCursorCol(col int) {
|
|||||||
func (w *Window) SetCursorPos(line, col int) {
|
func (w *Window) SetCursorPos(line, col int) {
|
||||||
w.Cursor.Line = line
|
w.Cursor.Line = line
|
||||||
w.Cursor.Col = col
|
w.Cursor.Col = col
|
||||||
|
w.clampCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetAnchor: Sets the anchor position in this window. The anchor is used for
|
// Window.SetAnchor: Sets the anchor position in this window. The anchor is used for
|
||||||
|
|||||||
@ -20,6 +20,9 @@ func NewWindowBuilder() *WindowBuilder {
|
|||||||
ScrollY: 0,
|
ScrollY: 0,
|
||||||
Height: 0,
|
Height: 0,
|
||||||
Width: 0,
|
Width: 0,
|
||||||
|
Options: WinOptions{
|
||||||
|
ScrollOff: 8, // 8 is default
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,6 +95,13 @@ func (w *WindowBuilder) WithDimensions(width, height int) *WindowBuilder {
|
|||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WindowBuilder.WithOptions: Applies the options to the window that is being built.
|
||||||
|
// This is a convenience method for setting all options in one call.
|
||||||
|
func (w *WindowBuilder) WithOptions(options WinOptions) *WindowBuilder {
|
||||||
|
w.window.Options = options
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
// WindowBuilder.Build: Build the final window and return it to the caller. Final
|
// WindowBuilder.Build: Build the final window and return it to the caller. Final
|
||||||
// step in the process. This is where the ID is set, so many windows can be "in-progress"
|
// step in the process. This is where the ID is set, so many windows can be "in-progress"
|
||||||
// but the ID will be set when they are built. Meaning, this is not thread safe.
|
// but the ID will be set when they are built. Meaning, this is not thread safe.
|
||||||
|
|||||||
@ -77,84 +77,43 @@ func NewModel(lines []string, pos action.Position) *Model {
|
|||||||
return &m
|
return &m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func (m Model) Init() tea.Cmd {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement action.Model interface
|
// Implement action.Model interface
|
||||||
|
|
||||||
func (m *Model) Lines() []string {
|
// ==================================================
|
||||||
win := m.ActiveWindow()
|
// Core Data Access
|
||||||
return win.Buffer.Lines
|
// ==================================================
|
||||||
|
func (m *Model) Windows() []*action.Window {
|
||||||
|
return m.windows
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Line(idx int) string {
|
func (m *Model) ActiveWindow() *action.Window {
|
||||||
win := m.ActiveWindow()
|
winId := m.activeWindowId
|
||||||
return win.Buffer.Line(idx)
|
for i := range m.Windows() {
|
||||||
|
if m.windows[i].Id == winId {
|
||||||
|
return m.windows[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("Could not find window")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetLine(idx int, content string) {
|
func (m *Model) Buffers() []*action.Buffer {
|
||||||
win := m.ActiveWindow()
|
return m.buffers
|
||||||
win.Buffer.SetLine(idx, content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) InsertLine(idx int, content string) {
|
func (m *Model) ActiveBuffer() *action.Buffer {
|
||||||
win := m.ActiveWindow()
|
win := m.ActiveWindow()
|
||||||
win.Buffer.InsertLine(idx, content)
|
return win.Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) DeleteLine(idx int) {
|
// ==================================================
|
||||||
win := m.ActiveWindow()
|
// Insert Mode Methods
|
||||||
win.Buffer.DeleteLine(idx)
|
// ==================================================
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) LineCount() int {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
return win.Buffer.LineCount()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) CursorX() int {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
return win.Cursor.Col
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) CursorY() int {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
return win.Cursor.Line
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetCursorX(x int) {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
win.Cursor.Col = x
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetCursorY(y int) {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
win.Cursor.Line = y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anchor methods
|
|
||||||
func (m *Model) AnchorX() int {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
return win.Anchor.Col
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) AnchorY() int {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
return win.Anchor.Line
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetAnchorX(x int) {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
win.Anchor.Col = x
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetAnchorY(y int) {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
win.Anchor.Line = y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert methods
|
|
||||||
func (m *Model) InsertKeys() []string {
|
func (m *Model) InsertKeys() []string {
|
||||||
return m.insertKeys
|
return m.insertKeys
|
||||||
}
|
}
|
||||||
@ -163,7 +122,145 @@ func (m *Model) SetInsertKeys(keys []string) {
|
|||||||
m.insertKeys = keys
|
m.insertKeys = keys
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command mode
|
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
||||||
|
m.insertCount = count
|
||||||
|
m.insertKeys = []string{}
|
||||||
|
m.insertAction = act
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) ExitInsertMode() {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
if m.insertCount > 1 {
|
||||||
|
m.replayInsert()
|
||||||
|
}
|
||||||
|
if win.Cursor.Col > 0 {
|
||||||
|
win.Cursor.Col--
|
||||||
|
}
|
||||||
|
m.mode = action.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().TabSize)
|
||||||
|
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)
|
||||||
|
win.ClampCursorX()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "down":
|
||||||
|
if line+1 < buf.LineCount() {
|
||||||
|
win.SetCursorLine(line + 1)
|
||||||
|
win.ClampCursorX()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
func (m *Model) Command() string {
|
||||||
return m.command
|
return m.command
|
||||||
}
|
}
|
||||||
@ -202,7 +299,17 @@ func (m *Model) SetCommandOutput(out string) {
|
|||||||
m.commandOutput = out
|
m.commandOutput = out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings
|
// ==================================================
|
||||||
|
// Editor-wide State
|
||||||
|
// ==================================================
|
||||||
|
func (m *Model) Mode() action.Mode {
|
||||||
|
return m.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) SetMode(mode action.Mode) {
|
||||||
|
m.mode = mode
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) Settings() action.Settings {
|
func (m *Model) Settings() action.Settings {
|
||||||
return m.settings
|
return m.settings
|
||||||
}
|
}
|
||||||
@ -211,7 +318,9 @@ func (m *Model) SetSettings(s action.Settings) {
|
|||||||
m.settings = s
|
m.settings = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
// Registers
|
// Registers
|
||||||
|
// ==================================================
|
||||||
func (m *Model) Registers() map[rune]action.Register {
|
func (m *Model) Registers() map[rune]action.Register {
|
||||||
return m.registers
|
return m.registers
|
||||||
}
|
}
|
||||||
@ -244,224 +353,145 @@ func (m *Model) UpdateDefaultRegister(t action.RegisterType, cnt []string) {
|
|||||||
m.SetRegister('"', t, cnt)
|
m.SetRegister('"', t, cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window
|
// ==================================================
|
||||||
func (m *Model) ScrollY() int {
|
// Depreciated
|
||||||
win := m.ActiveWindow()
|
// ==================================================
|
||||||
return win.ScrollY
|
// func (m *Model) Lines() []string {
|
||||||
}
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Buffer.Lines
|
||||||
func (m *Model) SetScrollY(y int) {
|
// }
|
||||||
win := m.ActiveWindow()
|
//
|
||||||
win.ScrollY = y
|
// func (m *Model) Line(idx int) string {
|
||||||
}
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Buffer.Line(idx)
|
||||||
func (m *Model) WinH() int {
|
// }
|
||||||
win := m.ActiveWindow()
|
//
|
||||||
return win.Height
|
// func (m *Model) SetLine(idx int, content string) {
|
||||||
}
|
// win := m.ActiveWindow()
|
||||||
|
// win.Buffer.SetLine(idx, content)
|
||||||
func (m *Model) WinW() int {
|
// }
|
||||||
win := m.ActiveWindow()
|
//
|
||||||
return win.Width
|
// func (m *Model) InsertLine(idx int, content string) {
|
||||||
}
|
// win := m.ActiveWindow()
|
||||||
|
// win.Buffer.InsertLine(idx, content)
|
||||||
func (m *Model) ViewPortH() int {
|
// }
|
||||||
win := m.ActiveWindow()
|
//
|
||||||
return win.Height - 2
|
// func (m *Model) DeleteLine(idx int) {
|
||||||
}
|
// win := m.ActiveWindow()
|
||||||
|
// win.Buffer.DeleteLine(idx)
|
||||||
func (m *Model) ClampCursorX() {
|
// }
|
||||||
win := m.ActiveWindow()
|
//
|
||||||
win.ClampCursorX()
|
// func (m *Model) LineCount() int {
|
||||||
}
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Buffer.LineCount()
|
||||||
// AdjustScroll ensures the cursor stays within the viewport with scrollOff margins.
|
// }
|
||||||
// Call this after any cursor movement.
|
//
|
||||||
func (m *Model) AdjustScroll() {
|
// func (m *Model) CursorX() int {
|
||||||
viewportHeight := m.ViewPortH()
|
// win := m.ActiveWindow()
|
||||||
if viewportHeight <= 0 {
|
// return win.Cursor.Col
|
||||||
return
|
// }
|
||||||
}
|
//
|
||||||
|
// func (m *Model) CursorY() int {
|
||||||
// Effective scrollOff (can't be more than half the viewport)
|
// win := m.ActiveWindow()
|
||||||
off := min(m.Settings().ScrollOff, viewportHeight/2)
|
// return win.Cursor.Line
|
||||||
|
// }
|
||||||
// Cursor too close to top — scroll up
|
//
|
||||||
if m.CursorY() < m.ScrollY()+off {
|
// func (m *Model) SetCursorX(x int) {
|
||||||
m.SetScrollY(m.CursorY() - off)
|
// win := m.ActiveWindow()
|
||||||
}
|
// win.Cursor.Col = x
|
||||||
|
// }
|
||||||
// Cursor too close to bottom — scroll down
|
//
|
||||||
if m.CursorY() > m.ScrollY()+viewportHeight-1-off {
|
// func (m *Model) SetCursorY(y int) {
|
||||||
m.SetScrollY(m.CursorY() - viewportHeight + 1 + off)
|
// win := m.ActiveWindow()
|
||||||
}
|
// win.Cursor.Line = y
|
||||||
|
// }
|
||||||
// Clamp scrollY to valid range
|
//
|
||||||
maxScroll := max(0, m.LineCount()-viewportHeight)
|
// // Anchor methods
|
||||||
m.SetScrollY(max(0, min(m.ScrollY(), maxScroll)))
|
// func (m *Model) AnchorX() int {
|
||||||
}
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Anchor.Col
|
||||||
// Windows
|
// }
|
||||||
func (m *Model) Windows() []*action.Window {
|
//
|
||||||
return m.windows
|
// func (m *Model) AnchorY() int {
|
||||||
}
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Anchor.Line
|
||||||
func (m *Model) ActiveWindowId() int {
|
// }
|
||||||
return m.activeWindowId
|
//
|
||||||
}
|
// func (m *Model) SetAnchorX(x int) {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
func (m *Model) ActiveWindow() *action.Window {
|
// win.Anchor.Col = x
|
||||||
winId := m.ActiveWindowId()
|
// }
|
||||||
for i := range m.Windows() {
|
//
|
||||||
if m.windows[i].Id == winId {
|
// func (m *Model) SetAnchorY(y int) {
|
||||||
return m.windows[i]
|
// win := m.ActiveWindow()
|
||||||
}
|
// win.Anchor.Line = y
|
||||||
}
|
// }
|
||||||
panic("Could not find window")
|
//
|
||||||
}
|
// func (m *Model) GetCursorPosition() *action.Position {
|
||||||
|
// // Return a copy of the position
|
||||||
func (m *Model) Mode() action.Mode {
|
// win := m.ActiveWindow()
|
||||||
return m.mode
|
// pos := win.Cursor
|
||||||
}
|
// return &pos
|
||||||
|
// }
|
||||||
func (m *Model) SetMode(mode action.Mode) {
|
//
|
||||||
m.mode = mode
|
// // Window
|
||||||
}
|
// func (m *Model) ScrollY() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
// return win.ScrollY
|
||||||
m.insertCount = count
|
// }
|
||||||
m.insertKeys = []string{}
|
//
|
||||||
m.insertAction = act
|
// func (m *Model) SetScrollY(y int) {
|
||||||
}
|
// win := m.ActiveWindow()
|
||||||
|
// win.ScrollY = y
|
||||||
func (m *Model) GetCursorPosition() *action.Position {
|
// }
|
||||||
// Return a copy of the position
|
//
|
||||||
win := m.ActiveWindow()
|
// func (m *Model) WinH() int {
|
||||||
pos := win.Cursor
|
// win := m.ActiveWindow()
|
||||||
return &pos
|
// return win.Height
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func (m *Model) replayInsert() {
|
// func (m *Model) WinW() int {
|
||||||
win := m.ActiveWindow()
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Width
|
||||||
// Replay (count - 1) more times
|
// }
|
||||||
for i := 1; i < m.insertCount; i++ {
|
//
|
||||||
// For 'o' and 'O', we need to create a new line first
|
// func (m *Model) ViewPortH() int {
|
||||||
switch m.insertAction.(type) {
|
// win := m.ActiveWindow()
|
||||||
case action.OpenLineBelow:
|
// return win.Height - 2
|
||||||
pos := win.Cursor.Line
|
// }
|
||||||
win.Buffer.Lines = append(win.Buffer.Lines[:pos+1], append([]string{""}, win.Buffer.Lines[pos+1:]...)...)
|
//
|
||||||
win.Cursor.Line++
|
// func (m *Model) ClampCursorX() {
|
||||||
win.Cursor.Col = 0
|
// win := m.ActiveWindow()
|
||||||
case action.OpenLineAbove:
|
// win.ClampCursorX()
|
||||||
pos := win.Cursor.Line
|
// }
|
||||||
win.Buffer.Lines = append(win.Buffer.Lines[:pos], append([]string{""}, win.Buffer.Lines[pos:]...)...)
|
//
|
||||||
win.Cursor.Col = 0
|
// func (m *Model) ActiveWindowId() int {
|
||||||
// 'i' and 'a' don't need setup - just replay keys
|
// return m.activeWindowId
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Replay each recorded keystroke
|
// // TODO: MOVE THIS
|
||||||
for _, key := range m.insertKeys {
|
// // AdjustScroll ensures the cursor stays within the viewport with scrollOff margins.
|
||||||
m.processInsertKey(key)
|
// // Call this after any cursor movement.
|
||||||
}
|
// func (m *Model) AdjustScroll() {
|
||||||
}
|
// viewportHeight := m.ViewPortH()
|
||||||
}
|
// if viewportHeight <= 0 {
|
||||||
|
// return
|
||||||
func (m *Model) ExitInsertMode() {
|
// }
|
||||||
win := m.ActiveWindow()
|
//
|
||||||
if m.insertCount > 1 {
|
// // Effective scrollOff (can't be more than half the viewport)
|
||||||
m.replayInsert()
|
// off := min(m.Settings().ScrollOff, viewportHeight/2)
|
||||||
}
|
//
|
||||||
if win.Cursor.Col > 0 {
|
// // Cursor too close to top — scroll up
|
||||||
win.Cursor.Col--
|
// if m.CursorY() < m.ScrollY()+off {
|
||||||
}
|
// m.SetScrollY(m.CursorY() - off)
|
||||||
m.mode = action.NormalMode
|
// }
|
||||||
m.insertCount = 0
|
//
|
||||||
m.insertKeys = nil
|
// // Cursor too close to bottom — scroll down
|
||||||
}
|
// if m.CursorY() > m.ScrollY()+viewportHeight-1-off {
|
||||||
|
// m.SetScrollY(m.CursorY() - viewportHeight + 1 + off)
|
||||||
func (m *Model) processInsertKey(key string) {
|
// }
|
||||||
x := m.CursorX()
|
//
|
||||||
y := m.CursorY()
|
// // Clamp scrollY to valid range
|
||||||
l := m.Line(y)
|
// maxScroll := max(0, m.LineCount()-viewportHeight)
|
||||||
|
// m.SetScrollY(max(0, min(m.ScrollY(), maxScroll)))
|
||||||
switch key {
|
// }
|
||||||
case "enter":
|
|
||||||
if x == len(l) {
|
|
||||||
m.InsertLine(y+1, "")
|
|
||||||
} 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()-1 {
|
|
||||||
nextLine := m.Line(y + 1)
|
|
||||||
m.SetLine(y, l+nextLine)
|
|
||||||
m.DeleteLine(y + 1)
|
|
||||||
} else if x < len(l) {
|
|
||||||
m.SetLine(y, l[:x]+l[x+1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
case "tab":
|
|
||||||
tabs := strings.Repeat(" ", m.Settings().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)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
if x < len(l) {
|
|
||||||
m.SetLine(y, l[:x]+key+l[x:])
|
|
||||||
} else {
|
|
||||||
m.SetLine(y, l+key)
|
|
||||||
}
|
|
||||||
m.SetCursorX(x + len(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user