Compare commits

..

4 Commits

Author SHA1 Message Date
Hayden Hargreaves
ea4638d815 wip: implement windows, buffers and builders 2026-02-26 13:20:21 -07:00
Hayden Hargreaves
3339dd4409 wip: first initial commit. Not sure if I like this 2026-02-26 12:14:59 -07:00
Hayden Hargreaves
65f96a5089 chore: cleaned up the file system a bit 2026-02-24 12:16:13 -07:00
Hayden Hargreaves
88fa53a4d7 doc: updated FEATURES.md 2026-02-24 12:07:49 -07:00
19 changed files with 845 additions and 344 deletions

View File

@ -74,8 +74,8 @@
- [x] `yy` - Yank line (double press) - [x] `yy` - Yank line (double press)
### Not Implemented ### Not Implemented
- [ ] `c` - Change operator - [x] `c` - Change operator
- [ ] `cc` - Change line - [x] `cc` - Change line
- [ ] `>` - Indent right - [ ] `>` - Indent right
- [ ] `<` - Indent left - [ ] `<` - Indent left
- [ ] `=` - Auto-indent - [ ] `=` - Auto-indent
@ -96,9 +96,9 @@
- [x] `A` - Insert at end of line - [x] `A` - Insert at end of line
- [x] `o` - Open line below - [x] `o` - Open line below
- [x] `O` - Open line above - [x] `O` - Open line above
- [ ] `s` - Substitute character (delete + insert) - [x] `s` - Substitute character (delete + insert)
- [ ] `S` - Substitute line (delete line + insert) - [x] `S` - Substitute line (delete line + insert)
- [ ] `C` - Change to end of line - [x] `C` - Change to end of line
- [ ] `gi` - Insert at last insert position - [ ] `gi` - Insert at last insert position
### Delete Actions ### Delete Actions
@ -415,6 +415,8 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
- [x] Delete operator (d, dd) - [x] Delete operator (d, dd)
- [x] Yank operator (y, yy) - [x] Yank operator (y, yy)
- [x] Paste actions (p, P) - [x] Paste actions (p, P)
- [x] Change operator (c, cc, C)
- [x] Substitute action (s, S)
- [x] Insert mode entry (i, a, I, A, o, O) - [x] Insert mode entry (i, a, I, A, o, O)
- [x] Insert mode editing (enter, backspace, delete, tab, ctrl+w) - [x] Insert mode editing (enter, backspace, delete, tab, ctrl+w)
- [x] Visual modes (v, V, ctrl+v) - [x] Visual modes (v, V, ctrl+v)
@ -422,3 +424,4 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
- [x] Delete actions (x, D) - [x] Delete actions (x, D)
- [x] Command mode basics - [x] Command mode basics
- [x] Register behavior - [x] Register behavior

View File

@ -8,17 +8,21 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
func generateLines(n int) []string {
lines := make([]string, n)
for i := range n {
lines[i] = fmt.Sprintf("line %d", i+1)
}
return lines
}
func main() { func main() {
tea.NewProgram( m, _ := tea.NewProgram(
editor.NewModel(generateLines(64), action.Position{Line: 0, Col: 0}), editor.NewModel([]string{""}, action.Position{Line: 0, Col: 0}),
tea.WithAltScreen(), tea.WithAltScreen(),
).Run() ).Run()
final, ok := m.(*editor.Model)
if ok {
fmt.Printf("PRINTING WINDOWS: %+v\n", final.Windows())
fmt.Printf("PRINTING ACTIVE WINDOW: %+v\n", final.ActiveWindow())
for _, win := range final.Windows() {
fmt.Printf("\t%+v\n", *win.Buffer)
}
} else {
fmt.Printf("PRINTING ALL: %+v\n", m)
}
} }

111
internal/action/buffer.go Normal file
View File

@ -0,0 +1,111 @@
package action
type BufferOptions struct {
// tabstop expandtab
}
type Buffer struct {
// Buffer data
Id int
// File data
Filename string
Filetype string
Lines []string
// Flags (not used yet)
Modified bool
Loaded bool
Listed bool
// Options BufferOptions
// UndoTree TODO: This will be big
}
// ==================================================
// Helper methods
// ==================================================
// Buffer.Line: Get the line at an index. Returns an empty string if the index
// is out of bounds.
func (b *Buffer) Line(idx int) string {
if idx < 0 || idx >= len(b.Lines) {
return ""
}
return b.Lines[idx]
}
// Buffer.SetLine: Set the content of the line at an index. Does nothing if the
// index is out of bounds.
func (b *Buffer) SetLine(idx int, content string) {
if idx >= 0 && idx < len(b.Lines) {
b.Lines[idx] = content
}
}
// Buffer.InsertLine: Insert a line with content at an index. The index is clamped
// to valid bounds (0 to len(Lines)). The new line is inserted before the line at
// the given index.
func (b *Buffer) InsertLine(idx int, content string) {
if idx < 0 {
idx = 0
}
if idx > len(b.Lines) {
idx = len(b.Lines)
}
b.Lines = append(b.Lines[:idx], append([]string{content}, b.Lines[idx:]...)...)
}
// Buffer.DeleteLine: Delete a line at an index. Does nothing if the index is out
// of bounds.
func (b *Buffer) DeleteLine(idx int) {
if idx >= 0 && idx < len(b.Lines) {
b.Lines = append(b.Lines[:idx], b.Lines[idx+1:]...)
}
}
// Buffer.LineCount: Get the number of lines in the buffer.
func (b *Buffer) LineCount() int {
return len(b.Lines)
}
// ==================================================
// Setters
// ==================================================
// Buffer.SetFilename: Set the filename associated with this buffer. This is
// typically the path to the file on disk that this buffer represents.
func (b *Buffer) SetFilename(filename string) {
b.Filename = filename
}
// Buffer.SetFiletype: Set the filetype of this buffer. The filetype is used
// for syntax highlighting and other language-specific features.
func (b *Buffer) SetFiletype(filetype string) {
b.Filetype = filetype
}
// Buffer.SetLines: Replace all lines in the buffer with the provided lines.
// This is useful when loading a file or resetting buffer content.
func (b *Buffer) SetLines(lines []string) {
b.Lines = lines
}
// Buffer.SetModified: Set the modified flag for this buffer. A modified buffer
// has unsaved changes that differ from the file on disk.
func (b *Buffer) SetModified(modified bool) {
b.Modified = modified
}
// Buffer.SetLoaded: Set the loaded flag for this buffer. A loaded buffer has
// its content in memory, while an unloaded buffer exists only as metadata.
func (b *Buffer) SetLoaded(loaded bool) {
b.Loaded = loaded
}
// Buffer.SetListed: Set the listed flag for this buffer. A listed buffer appears
// in buffer lists (like :ls), while an unlisted buffer is hidden from normal
// buffer navigation.
func (b *Buffer) SetListed(listed bool) {
b.Listed = listed
}

View File

@ -0,0 +1,73 @@
package action
// Not great, but maybe the best way
var CurrentBufferId int = 1
type BufferBuilder struct {
buffer Buffer
}
// NewBufferBuilder: Creates a new buffer builder. The buffer builder implements a
// builder pattern to create a buffer with the defined properties and values.
func NewBufferBuilder() *BufferBuilder {
return &BufferBuilder{
buffer: Buffer{
Id: 0, // This is set when built
Filename: "",
Filetype: "",
Lines: []string{""},
Modified: false,
Loaded: false,
Listed: false,
},
}
}
// BufferBuilder.WithFilename: Attaches a file name to the buffer that is being built.
func (b *BufferBuilder) WithFilename(filename string) *BufferBuilder {
b.buffer.Filename = filename
return b
}
// BufferBuilder.WithFiletype: Attaches a file type to the buffer that is being built.
func (b *BufferBuilder) WithFiletype(filetype string) *BufferBuilder {
b.buffer.Filetype = filetype
return b
}
// BufferBuilder.WithLines: Attaches a lines to the buffer that is being built.
func (b *BufferBuilder) WithLines(lines []string) *BufferBuilder {
b.buffer.Lines = lines
return b
}
// BufferBuilder.Modified: Sets the modified flag of the buffer being built. By default,
// buffers are built with the modified flag being false.
func (b *BufferBuilder) Modified() *BufferBuilder {
b.buffer.Modified = true
return b
}
// BufferBuilder.Loaded: Sets the loaded flag of the buffer being built. By default,
// buffers are built with the loaded flag being false.
func (b *BufferBuilder) Loaded() *BufferBuilder {
b.buffer.Loaded = true
return b
}
// BufferBuilder.Listed: Sets the listed flag of the buffer being built. By default,
// buffers are built with the listed flag being false.
func (b *BufferBuilder) Listed() *BufferBuilder {
b.buffer.Listed = true
return b
}
// BufferBuilder.Build: Build the final buffer and return it to the caller. Final
// step in the process. This is where the ID is set, so many buffers can be "in-progress"
// but the ID will be set when they are built. Meaning, this is not thread safe.
func (b *BufferBuilder) Build() Buffer {
b.buffer.Id = CurrentBufferId
CurrentBufferId++
return b.buffer
}

View File

@ -4,43 +4,6 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
// Mode constants for editor mode
type Mode int
const (
NormalMode Mode = iota
InsertMode
CommandMode
VisualMode
VisualLineMode
VisualBlockMode
)
func (m Mode) ToString() string {
switch m {
case NormalMode:
return "NORMAL"
case InsertMode:
return "INSERT"
case CommandMode:
return "COMMAND"
case VisualMode:
return "VISUAL"
case VisualLineMode:
return "V-LINE"
case VisualBlockMode:
return "V-BLOCK"
default:
return "-----"
}
}
func (m Mode) IsVisualMode() bool {
return m == VisualMode ||
m == VisualLineMode ||
m == VisualBlockMode
}
// 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 // Text buffer
@ -58,6 +21,11 @@ type Model interface {
SetCursorY(y int) SetCursorY(y int)
ClampCursorX() ClampCursorX()
// Windows
Windows() []*Window
ActiveWindowId() int
ActiveWindow() *Window
// Window // Window
ScrollY() int ScrollY() int
SetScrollY(y int) SetScrollY(y int)

38
internal/action/mode.go Normal file
View File

@ -0,0 +1,38 @@
package action
// Mode constants for editor mode
type Mode int
const (
NormalMode Mode = iota
InsertMode
CommandMode
VisualMode
VisualLineMode
VisualBlockMode
)
func (m Mode) ToString() string {
switch m {
case NormalMode:
return "NORMAL"
case InsertMode:
return "INSERT"
case CommandMode:
return "COMMAND"
case VisualMode:
return "VISUAL"
case VisualLineMode:
return "V-LINE"
case VisualBlockMode:
return "V-BLOCK"
default:
return "-----"
}
}
func (m Mode) IsVisualMode() bool {
return m == VisualMode ||
m == VisualLineMode ||
m == VisualBlockMode
}

View File

@ -1,2 +0,0 @@
hello

124
internal/action/window.go Normal file
View File

@ -0,0 +1,124 @@
package action
// TODO: No more global settings, window-wide settings
type WinOptions struct {
// Number bool
// Wrap bool
// Relnumber bool
}
type Window struct {
Id int
Number int // Ignored for now, will be used when splits come into play
Buffer *Buffer
Cursor Position
Anchor Position
ScrollY int
Height int
Width int
// Folds // TODO
// Options WinOptions
}
// ==================================================
// Helper methods
// ==================================================
// Window.ClampCursorX: Clamps the cursor in the X direction to ensure the cursor
// does not go into an invalid position. Such as negative values or past the end of
// the line.
func (w *Window) ClampCursorX() {
lineLen := len(w.Buffer.Lines[w.Cursor.Line])
if lineLen == 0 {
w.Cursor.Col = 0
} else if w.Cursor.Col >= lineLen {
w.Cursor.Col = lineLen
}
}
// ==================================================
// Setters
// ==================================================
// Window.SetNumber: Sets the position-based number of this window. Currently ignored
// until splits are implemented.
func (w *Window) SetNumber(number int) {
w.Number = number
}
// Window.SetBuffer: Sets the buffer that this window should display. This is used when
// switching between buffers or opening a new file in the current window.
func (w *Window) SetBuffer(buffer *Buffer) {
w.Buffer = buffer
}
// Window.SetCursor: Sets the cursor position in this window to the given position.
func (w *Window) SetCursor(cursor Position) {
w.Cursor = cursor
}
// Window.SetCursorLine: Sets the line number of the cursor position.
func (w *Window) SetCursorLine(line int) {
w.Cursor.Line = line
}
// Window.SetCursorCol: Sets the column number of the cursor position.
func (w *Window) SetCursorCol(col int) {
w.Cursor.Col = col
}
// Window.SetCursorPos: Sets both the line and column of the cursor position. This is
// a convenience method for setting both components at once.
func (w *Window) SetCursorPos(line, col int) {
w.Cursor.Line = line
w.Cursor.Col = col
}
// Window.SetAnchor: Sets the anchor position in this window. The anchor is used for
// visual mode selections as the starting point of the selection.
func (w *Window) SetAnchor(anchor Position) {
w.Anchor = anchor
}
// Window.SetAnchorLine: Sets the line number of the anchor position.
func (w *Window) SetAnchorLine(line int) {
w.Anchor.Line = line
}
// Window.SetAnchorCol: Sets the column number of the anchor position.
func (w *Window) SetAnchorCol(col int) {
w.Anchor.Col = col
}
// Window.SetAnchorPos: Sets both the line and column of the anchor position. This is
// a convenience method for setting both components at once.
func (w *Window) SetAnchorPos(line, col int) {
w.Anchor.Line = line
w.Anchor.Col = col
}
// Window.SetScrollY: Sets the vertical scroll offset of this window. This determines
// which line appears at the top of the visible viewport.
func (w *Window) SetScrollY(scrollY int) {
w.ScrollY = scrollY
}
// Window.SetHeight: Sets the height of this window in lines.
func (w *Window) SetHeight(height int) {
w.Height = height
}
// Window.SetWidth: Sets the width of this window in columns.
func (w *Window) SetWidth(width int) {
w.Width = width
}
// Window.SetDimensions: Sets both the width and height of this window. This is a
// convenience method for setting both dimensions at once.
func (w *Window) SetDimensions(width, height int) {
w.Width = width
w.Height = height
}

View File

@ -0,0 +1,103 @@
package action
// Not great, but maybe the best way
var CurrentWindowId int = 1000
type WindowBuilder struct {
window Window
}
// NewWindowBuilder: Creates a new window builder. The window builder implements a
// builder pattern to create a window with the defined properties and values.
func NewWindowBuilder() *WindowBuilder {
return &WindowBuilder{
window: Window{
Id: 0, // This is set when built
Number: 1, // Ignored for now, will be used for splits
Buffer: nil,
Cursor: Position{Line: 0, Col: 0},
Anchor: Position{Line: 0, Col: 0},
ScrollY: 0,
Height: 0,
Width: 0,
},
}
}
// WindowBuilder.WithNumber: Attaches a window number to the window that is being built.
// Window numbers are position-based and change when windows are rearranged. This is
// ignored for now, but will be used when splits are implemented.
func (w *WindowBuilder) WithNumber(number int) *WindowBuilder {
w.window.Number = number
return w
}
// WindowBuilder.WithBuffer: Attaches a buffer to the window that is being built. The
// window will display and edit the content of this buffer.
func (w *WindowBuilder) WithBuffer(buffer *Buffer) *WindowBuilder {
w.window.Buffer = buffer
return w
}
// WindowBuilder.WithCursor: Sets the cursor position in the window that is being built.
func (w *WindowBuilder) WithCursor(cursor Position) *WindowBuilder {
w.window.Cursor = cursor
return w
}
// WindowBuilder.WithCursorPos: Sets the cursor position in the window that is being built.
// This is an alias for WithCursor that accepts line and column separately.
func (w *WindowBuilder) WithCursorPos(line, col int) *WindowBuilder {
w.window.Cursor = Position{Line: line, Col: col}
return w
}
// WindowBuilder.WithAnchor: Sets the anchor position in the window that is being built.
// The anchor is used for visual mode selections.
func (w *WindowBuilder) WithAnchor(anchor Position) *WindowBuilder {
w.window.Anchor = anchor
return w
}
// WindowBuilder.WithAnchorPos: Sets the anchor position in the window that is being built.
// This is an alias for WithAnchor that accepts line and column separately.
func (w *WindowBuilder) WithAnchorPos(line, col int) *WindowBuilder {
w.window.Anchor = Position{Line: line, Col: col}
return w
}
// WindowBuilder.WithScrollY: Sets the vertical scroll offset of the window that is being built.
func (w *WindowBuilder) WithScrollY(scrollY int) *WindowBuilder {
w.window.ScrollY = scrollY
return w
}
// WindowBuilder.WithHeight: Sets the height of the window that is being built.
func (w *WindowBuilder) WithHeight(height int) *WindowBuilder {
w.window.Height = height
return w
}
// WindowBuilder.WithWidth: Sets the width of the window that is being built.
func (w *WindowBuilder) WithWidth(width int) *WindowBuilder {
w.window.Width = width
return w
}
// WindowBuilder.WithDimensions: Sets both width and height of the window that is being built.
// This is a convenience method for setting dimensions in one call.
func (w *WindowBuilder) WithDimensions(width, height int) *WindowBuilder {
w.window.Width = width
w.window.Height = height
return w
}
// 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"
// but the ID will be set when they are built. Meaning, this is not thread safe.
func (w *WindowBuilder) Build() Window {
w.window.Id = CurrentWindowId
CurrentWindowId++
return w.window
}

BIN
internal/editor/gim Executable file

Binary file not shown.

View File

@ -19,8 +19,8 @@ func TestHelperExamples(t *testing.T) {
WithLines([]string{"hello", "world"}), WithLines([]string{"hello", "world"}),
) )
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 2 { if len(m.Lines()) != 2 {
t.Errorf("expected 2 lines, got %d", len(m.lines)) t.Errorf("expected 2 lines, got %d", len(m.Lines()))
} }
}) })

View File

@ -123,8 +123,8 @@ func newTestModelWithTermSize(t *testing.T, lines []string, pos action.Position,
} }
// getFinalModel extracts the final model state (sends ctrl+c to quit first) // getFinalModel extracts the final model state (sends ctrl+c to quit first)
func getFinalModel(t *testing.T, tm *teatest.TestModel) Model { func getFinalModel(t *testing.T, tm *teatest.TestModel) *Model {
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) tm.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
fm := tm.FinalModel(t, teatest.WithFinalTimeout(time.Second)) fm := tm.FinalModel(t, teatest.WithFinalTimeout(time.Second))
return fm.(Model) return fm.(*Model)
} }

View File

@ -13,8 +13,8 @@ func TestDeleteChar(t *testing.T) {
sendKeys(tm, "x") sendKeys(tm, "x")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "ello" { if m.Line(0) != "ello" {
t.Errorf("lines[0] = %q, want 'ello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'ello'", m.Line(0))
} }
}) })
@ -24,8 +24,8 @@ func TestDeleteChar(t *testing.T) {
sendKeys(tm, "x") sendKeys(tm, "x")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "helo" { if m.Line(0) != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'helo'", m.Line(0))
} }
}) })
@ -35,8 +35,8 @@ func TestDeleteChar(t *testing.T) {
sendKeys(tm, "x") sendKeys(tm, "x")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hell" { if m.Line(0) != "hell" {
t.Errorf("lines[0] = %q, want 'hell'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hell'", m.Line(0))
} }
}) })
@ -46,8 +46,8 @@ func TestDeleteChar(t *testing.T) {
sendKeys(tm, "x", "x") sendKeys(tm, "x", "x")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "llo" { if m.Line(0) != "llo" {
t.Errorf("lines[0] = %q, want 'llo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'llo'", m.Line(0))
} }
}) })
} }
@ -59,8 +59,8 @@ func TestDeleteCharWithCount(t *testing.T) {
sendKeys(tm, "3", "x") sendKeys(tm, "3", "x")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "lo" { if m.Line(0) != "lo" {
t.Errorf("lines[0] = %q, want 'lo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'lo'", m.Line(0))
} }
}) })
@ -70,8 +70,8 @@ func TestDeleteCharWithCount(t *testing.T) {
sendKeys(tm, "1", "0", "x") sendKeys(tm, "1", "0", "x")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "" { if m.Line(0) != "" {
t.Errorf("lines[0] = %q, want ''", m.lines[0]) t.Errorf("lines[0] = %q, want ''", m.Line(0))
} }
}) })
@ -81,8 +81,8 @@ func TestDeleteCharWithCount(t *testing.T) {
sendKeys(tm, "2", "x") sendKeys(tm, "2", "x")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hlo" { if m.Line(0) != "hlo" {
t.Errorf("lines[0] = %q, want 'hlo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hlo'", m.Line(0))
} }
}) })
} }

View File

@ -25,8 +25,8 @@ func TestEnterInsert(t *testing.T) {
sendKeys(tm, "i", "X", "esc") sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "Xhello" { if m.Line(0) != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'Xhello'", m.Line(0))
} }
}) })
@ -36,8 +36,8 @@ func TestEnterInsert(t *testing.T) {
sendKeys(tm, "i", "X", "esc") sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "heXllo" { if m.Line(0) != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'heXllo'", m.Line(0))
} }
}) })
@ -47,8 +47,8 @@ func TestEnterInsert(t *testing.T) {
sendKeys(tm, "i", "X", "esc") sendKeys(tm, "i", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 2 { if m.CursorX() != 2 {
t.Errorf("cursor.x = %d, want 2", m.cursor.x) t.Errorf("cursor.x = %d, want 2", m.CursorX())
} }
}) })
} }
@ -70,8 +70,8 @@ func TestEnterInsertAfter(t *testing.T) {
sendKeys(tm, "a", "X", "esc") sendKeys(tm, "a", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hXello" { if m.Line(0) != "hXello" {
t.Errorf("lines[0] = %q, want 'hXello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hXello'", m.Line(0))
} }
}) })
@ -81,8 +81,8 @@ func TestEnterInsertAfter(t *testing.T) {
sendKeys(tm, "a", "X", "esc") sendKeys(tm, "a", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "helXlo" { if m.Line(0) != "helXlo" {
t.Errorf("lines[0] = %q, want 'helXlo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'helXlo'", m.Line(0))
} }
}) })
} }
@ -94,8 +94,8 @@ func TestEnterInsertLineStart(t *testing.T) {
sendKeys(tm, "I", "X", "esc") sendKeys(tm, "I", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "Xhello" { if m.Line(0) != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'Xhello'", m.Line(0))
} }
}) })
@ -105,8 +105,8 @@ func TestEnterInsertLineStart(t *testing.T) {
sendKeys(tm, "I", "X", "esc") sendKeys(tm, "I", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "Xhello" { if m.Line(0) != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'Xhello'", m.Line(0))
} }
}) })
} }
@ -118,8 +118,8 @@ func TestEnterInsertLineEnd(t *testing.T) {
sendKeys(tm, "A", "X", "esc") sendKeys(tm, "A", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "helloX" { if m.Line(0) != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0]) t.Errorf("lines[0] = %q, want 'helloX'", m.Line(0))
} }
}) })
@ -129,8 +129,8 @@ func TestEnterInsertLineEnd(t *testing.T) {
sendKeys(tm, "A", "X", "esc") sendKeys(tm, "A", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "helloX" { if m.Line(0) != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0]) t.Errorf("lines[0] = %q, want 'helloX'", m.Line(0))
} }
}) })
} }
@ -144,11 +144,11 @@ func TestOpenLineBelow(t *testing.T) {
sendKeys(tm, "o", "n", "e", "w", "esc") sendKeys(tm, "o", "n", "e", "w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 3 { if m.LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines)) t.Errorf("len(lines) = %d, want 3", m.LineCount())
} }
if m.lines[1] != "new" { if m.Line(1) != "new" {
t.Errorf("lines[1] = %q, want 'new'", m.lines[1]) t.Errorf("lines[1] = %q, want 'new'", m.Line(1))
} }
}) })
@ -158,11 +158,11 @@ func TestOpenLineBelow(t *testing.T) {
sendKeys(tm, "o", "n", "e", "w", "esc") sendKeys(tm, "o", "n", "e", "w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 4 { if m.LineCount() != 4 {
t.Errorf("len(lines) = %d, want 4", len(m.lines)) t.Errorf("len(lines) = %d, want 4", m.LineCount())
} }
if m.lines[2] != "new" { if m.Line(2) != "new" {
t.Errorf("lines[2] = %q, want 'new'", m.lines[2]) t.Errorf("lines[2] = %q, want 'new'", m.Line(2))
} }
}) })
@ -172,11 +172,11 @@ func TestOpenLineBelow(t *testing.T) {
sendKeys(tm, "o", "n", "e", "w", "esc") sendKeys(tm, "o", "n", "e", "w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 3 { if m.LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines)) t.Errorf("len(lines) = %d, want 3", m.LineCount())
} }
if m.lines[2] != "new" { if m.Line(2) != "new" {
t.Errorf("lines[2] = %q, want 'new'", m.lines[2]) t.Errorf("lines[2] = %q, want 'new'", m.Line(2))
} }
}) })
@ -186,11 +186,11 @@ func TestOpenLineBelow(t *testing.T) {
sendKeys(tm, "o", "esc") sendKeys(tm, "o", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 1 { if m.CursorY() != 1 {
t.Errorf("cursor.y = %d, want 1", m.cursor.y) t.Errorf("cursor.y = %d, want 1", m.CursorY())
} }
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("cursor.x = %d, want 0", m.CursorX())
} }
}) })
} }
@ -202,12 +202,12 @@ func TestOpenLineBelowWithCount(t *testing.T) {
sendKeys(tm, "3", "o", "x", "esc") sendKeys(tm, "3", "o", "x", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 4 { if m.LineCount() != 4 {
t.Errorf("len(lines) = %d, want 4", len(m.lines)) t.Errorf("len(lines) = %d, want 4", m.LineCount())
} }
for i := 1; i <= 3; i++ { for i := 1; i <= 3; i++ {
if m.lines[i] != "x" { if m.Line(i) != "x" {
t.Errorf("lines[%d] = %q, want 'x'", i, m.lines[i]) t.Errorf("lines[%d] = %q, want 'x'", i, m.Line(i))
} }
} }
}) })
@ -218,14 +218,14 @@ func TestOpenLineBelowWithCount(t *testing.T) {
sendKeys(tm, "2", "o", "a", "b", "esc") sendKeys(tm, "2", "o", "a", "b", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 3 { if m.LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines)) t.Errorf("len(lines) = %d, want 3", m.LineCount())
} }
if m.lines[1] != "ab" { if m.Line(1) != "ab" {
t.Errorf("lines[1] = %q, want 'ab'", m.lines[1]) t.Errorf("lines[1] = %q, want 'ab'", m.Line(1))
} }
if m.lines[2] != "ab" { if m.Line(2) != "ab" {
t.Errorf("lines[2] = %q, want 'ab'", m.lines[2]) t.Errorf("lines[2] = %q, want 'ab'", m.Line(2))
} }
}) })
} }
@ -237,11 +237,11 @@ func TestOpenLineAbove(t *testing.T) {
sendKeys(tm, "O", "n", "e", "w", "esc") sendKeys(tm, "O", "n", "e", "w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 3 { if m.LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines)) t.Errorf("len(lines) = %d, want 3", m.LineCount())
} }
if m.lines[1] != "new" { if m.Line(1) != "new" {
t.Errorf("lines[1] = %q, want 'new'", m.lines[1]) t.Errorf("lines[1] = %q, want 'new'", m.Line(1))
} }
}) })
@ -251,11 +251,11 @@ func TestOpenLineAbove(t *testing.T) {
sendKeys(tm, "O", "n", "e", "w", "esc") sendKeys(tm, "O", "n", "e", "w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 3 { if m.LineCount() != 3 {
t.Errorf("len(lines) = %d, want 3", len(m.lines)) t.Errorf("len(lines) = %d, want 3", m.LineCount())
} }
if m.lines[0] != "new" { if m.Line(0) != "new" {
t.Errorf("lines[0] = %q, want 'new'", m.lines[0]) t.Errorf("lines[0] = %q, want 'new'", m.Line(0))
} }
}) })
@ -265,8 +265,8 @@ func TestOpenLineAbove(t *testing.T) {
sendKeys(tm, "O", "esc") sendKeys(tm, "O", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("cursor.x = %d, want 0", m.CursorX())
} }
}) })
} }
@ -278,12 +278,12 @@ func TestOpenLineAboveWithCount(t *testing.T) {
sendKeys(tm, "3", "O", "x", "esc") sendKeys(tm, "3", "O", "x", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 4 { if m.LineCount() != 4 {
t.Errorf("len(lines) = %d, want 4", len(m.lines)) t.Errorf("len(lines) = %d, want 4", m.LineCount())
} }
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
if m.lines[i] != "x" { if m.Line(i) != "x" {
t.Errorf("lines[%d] = %q, want 'x'", i, m.lines[i]) t.Errorf("lines[%d] = %q, want 'x'", i, m.Line(i))
} }
} }
}) })
@ -298,14 +298,14 @@ func TestInsertModeEnter(t *testing.T) {
sendKeys(tm, "i", "enter", "esc") sendKeys(tm, "i", "enter", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 2 { if m.LineCount() != 2 {
t.Errorf("len(lines) = %d, want 2", len(m.lines)) t.Errorf("len(lines) = %d, want 2", m.LineCount())
} }
if m.lines[0] != "hello" { if m.Line(0) != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hello'", m.Line(0))
} }
if m.lines[1] != " world" { if m.Line(1) != " world" {
t.Errorf("lines[1] = %q, want ' world'", m.lines[1]) t.Errorf("lines[1] = %q, want ' world'", m.Line(1))
} }
}) })
@ -315,14 +315,14 @@ func TestInsertModeEnter(t *testing.T) {
sendKeys(tm, "i", "enter", "esc") sendKeys(tm, "i", "enter", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 2 { if m.LineCount() != 2 {
t.Errorf("len(lines) = %d, want 2", len(m.lines)) t.Errorf("len(lines) = %d, want 2", m.LineCount())
} }
if m.lines[0] != "hello" { if m.Line(0) != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hello'", m.Line(0))
} }
if m.lines[1] != "" { if m.Line(1) != "" {
t.Errorf("lines[1] = %q, want ''", m.lines[1]) t.Errorf("lines[1] = %q, want ''", m.Line(1))
} }
}) })
@ -332,14 +332,14 @@ func TestInsertModeEnter(t *testing.T) {
sendKeys(tm, "i", "enter", "esc") sendKeys(tm, "i", "enter", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 2 { if m.LineCount() != 2 {
t.Errorf("len(lines) = %d, want 2", len(m.lines)) t.Errorf("len(lines) = %d, want 2", m.LineCount())
} }
if m.lines[0] != "" { if m.Line(0) != "" {
t.Errorf("lines[0] = %q, want ''", m.lines[0]) t.Errorf("lines[0] = %q, want ''", m.Line(0))
} }
if m.lines[1] != "hello" { if m.Line(1) != "hello" {
t.Errorf("lines[1] = %q, want 'hello'", m.lines[1]) t.Errorf("lines[1] = %q, want 'hello'", m.Line(1))
} }
}) })
} }
@ -351,8 +351,8 @@ func TestInsertModeBackspace(t *testing.T) {
sendKeys(tm, "i", "backspace", "esc") sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "helo" { if m.Line(0) != "helo" {
t.Errorf("lines[0] = %q, want 'helo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'helo'", m.Line(0))
} }
}) })
@ -362,11 +362,11 @@ func TestInsertModeBackspace(t *testing.T) {
sendKeys(tm, "i", "backspace", "esc") sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 1 { if m.LineCount() != 1 {
t.Errorf("len(lines) = %d, want 1", len(m.lines)) t.Errorf("len(lines) = %d, want 1", m.LineCount())
} }
if m.lines[0] != "helloworld" { if m.Line(0) != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0]) t.Errorf("lines[0] = %q, want 'helloworld'", m.Line(0))
} }
}) })
@ -376,8 +376,8 @@ func TestInsertModeBackspace(t *testing.T) {
sendKeys(tm, "i", "backspace", "esc") sendKeys(tm, "i", "backspace", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hello" { if m.Line(0) != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hello'", m.Line(0))
} }
}) })
@ -387,8 +387,8 @@ func TestInsertModeBackspace(t *testing.T) {
sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc") sendKeys(tm, "i", "backspace", "backspace", "backspace", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "he" { if m.Line(0) != "he" {
t.Errorf("lines[0] = %q, want 'he'", m.lines[0]) t.Errorf("lines[0] = %q, want 'he'", m.Line(0))
} }
}) })
} }
@ -400,8 +400,8 @@ func TestInsertModeDelete(t *testing.T) {
sendKeys(tm, "i", "delete", "esc") sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "word" { if m.Line(0) != "word" {
t.Errorf("lines[0] = %q, want 'word'", m.lines[0]) t.Errorf("lines[0] = %q, want 'word'", m.Line(0))
} }
}) })
@ -411,11 +411,11 @@ func TestInsertModeDelete(t *testing.T) {
sendKeys(tm, "i", "delete", "esc") sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 1 { if m.LineCount() != 1 {
t.Errorf("len(lines) = %d, want 1", len(m.lines)) t.Errorf("len(lines) = %d, want 1", m.LineCount())
} }
if m.lines[0] != "helloworld" { if m.Line(0) != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0]) t.Errorf("lines[0] = %q, want 'helloworld'", m.Line(0))
} }
}) })
@ -425,11 +425,11 @@ func TestInsertModeDelete(t *testing.T) {
sendKeys(tm, "i", "delete", "esc") sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if len(m.lines) != 1 { if m.LineCount() != 1 {
t.Errorf("len(lines) = %d, want 1", len(m.lines)) t.Errorf("len(lines) = %d, want 1", m.LineCount())
} }
if m.lines[0] != "world" { if m.Line(0) != "world" {
t.Errorf("lines[0] = %q, want 'world'", m.lines[0]) t.Errorf("lines[0] = %q, want 'world'", m.Line(0))
} }
}) })
@ -439,8 +439,8 @@ func TestInsertModeDelete(t *testing.T) {
sendKeys(tm, "i", "delete", "esc") sendKeys(tm, "i", "delete", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hello" { if m.Line(0) != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hello'", m.Line(0))
} }
}) })
@ -450,8 +450,8 @@ func TestInsertModeDelete(t *testing.T) {
sendKeys(tm, "i", "delete", "delete", "delete", "esc") sendKeys(tm, "i", "delete", "delete", "delete", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "ho" { if m.Line(0) != "ho" {
t.Errorf("lines[0] = %q, want 'he'", m.lines[0]) t.Errorf("lines[0] = %q, want 'he'", m.Line(0))
} }
}) })
@ -464,8 +464,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "left", "X", "esc") sendKeys(tm, "i", "left", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "heXllo" { if m.Line(0) != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'heXllo'", m.Line(0))
} }
}) })
@ -475,8 +475,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "right", "X", "esc") sendKeys(tm, "i", "right", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "heXllo" { if m.Line(0) != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'heXllo'", m.Line(0))
} }
}) })
@ -486,11 +486,11 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "up", "X", "esc") sendKeys(tm, "i", "up", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "heXllo" { if m.Line(0) != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'heXllo'", m.Line(0))
} }
if m.lines[1] != "world" { if m.Line(1) != "world" {
t.Errorf("lines[1] = %q, want 'world'", m.lines[1]) t.Errorf("lines[1] = %q, want 'world'", m.Line(1))
} }
}) })
@ -500,11 +500,11 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "down", "X", "esc") sendKeys(tm, "i", "down", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hello" { if m.Line(0) != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hello'", m.Line(0))
} }
if m.lines[1] != "woXrld" { if m.Line(1) != "woXrld" {
t.Errorf("lines[1] = %q, want 'woXrld'", m.lines[1]) t.Errorf("lines[1] = %q, want 'woXrld'", m.Line(1))
} }
}) })
@ -514,8 +514,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "left", "X", "esc") sendKeys(tm, "i", "left", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "Xhello" { if m.Line(0) != "Xhello" {
t.Errorf("lines[0] = %q, want 'Xhello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'Xhello'", m.Line(0))
} }
}) })
@ -525,8 +525,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "a", "right", "X", "esc") sendKeys(tm, "a", "right", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "helloX" { if m.Line(0) != "helloX" {
t.Errorf("lines[0] = %q, want 'helloX'", m.lines[0]) t.Errorf("lines[0] = %q, want 'helloX'", m.Line(0))
} }
}) })
@ -536,8 +536,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "up", "X", "esc") sendKeys(tm, "i", "up", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "heXllo" { if m.Line(0) != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'heXllo'", m.Line(0))
} }
}) })
@ -547,8 +547,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "down", "X", "esc") sendKeys(tm, "i", "down", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "heXllo" { if m.Line(0) != "heXllo" {
t.Errorf("lines[0] = %q, want 'heXllo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'heXllo'", m.Line(0))
} }
}) })
@ -558,8 +558,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "up", "X", "esc") sendKeys(tm, "i", "up", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hiX" { if m.Line(0) != "hiX" {
t.Errorf("lines[0] = %q, want 'hiX'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hiX'", m.Line(0))
} }
}) })
@ -569,8 +569,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "down", "X", "esc") sendKeys(tm, "i", "down", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[1] != "hiX" { if m.Line(1) != "hiX" {
t.Errorf("lines[1] = %q, want 'hiX'", m.lines[1]) t.Errorf("lines[1] = %q, want 'hiX'", m.Line(1))
} }
}) })
@ -580,8 +580,8 @@ func TestInsertModeArrowKeys(t *testing.T) {
sendKeys(tm, "i", "right", "right", "down", "X", "esc") sendKeys(tm, "i", "right", "right", "down", "X", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[1] != "woXrld" { if m.Line(1) != "woXrld" {
t.Errorf("lines[1] = %q, want 'woXrld'", m.lines[1]) t.Errorf("lines[1] = %q, want 'woXrld'", m.Line(1))
} }
}) })
} }
@ -593,8 +593,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc") sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hello " { if m.Line(0) != "hello " {
t.Errorf("lines[0] = %q, want 'hello '", m.lines[0]) t.Errorf("lines[0] = %q, want 'hello '", m.Line(0))
} }
if m.CursorX() != 5 { if m.CursorX() != 5 {
t.Errorf("CursorX() = %d, want '5'", m.CursorX()) t.Errorf("CursorX() = %d, want '5'", m.CursorX())
@ -607,8 +607,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc") sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "" { if m.Line(0) != "" {
t.Errorf("lines[0] = %q, want ''", m.lines[0]) t.Errorf("lines[0] = %q, want ''", m.Line(0))
} }
if m.CursorX() != 0 { if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want '0'", m.CursorX()) t.Errorf("CursorX() = %d, want '0'", m.CursorX())
@ -621,8 +621,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc") sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hello wo" { if m.Line(0) != "hello wo" {
t.Errorf("lines[0] = %q, want 'hello wo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hello wo'", m.Line(0))
} }
if m.CursorX() != 7 { if m.CursorX() != 7 {
t.Errorf("CursorX() = %d, want '7'", m.CursorX()) t.Errorf("CursorX() = %d, want '7'", m.CursorX())
@ -672,8 +672,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "i", "ctrl+w", "esc") sendKeys(tm, "i", "ctrl+w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hello" { if m.Line(0) != "hello" {
t.Errorf("lines[0] = %q, want 'hello'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hello'", m.Line(0))
} }
if m.CursorX() != 0 { if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX()) t.Errorf("CursorX() = %d, want 0", m.CursorX())
@ -686,8 +686,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "i", "ctrl+w", "esc") sendKeys(tm, "i", "ctrl+w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "lo" { if m.Line(0) != "lo" {
t.Errorf("lines[0] = %q, want 'lo'", m.lines[0]) t.Errorf("lines[0] = %q, want 'lo'", m.Line(0))
} }
if m.CursorX() != 0 { if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX()) t.Errorf("CursorX() = %d, want 0", m.CursorX())
@ -700,8 +700,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc") sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "..." { if m.Line(0) != "..." {
t.Errorf("lines[0] = %q, want '...'", m.lines[0]) t.Errorf("lines[0] = %q, want '...'", m.Line(0))
} }
if m.CursorX() != 2 { if m.CursorX() != 2 {
t.Errorf("CursorX() = %d, want 2", m.CursorX()) t.Errorf("CursorX() = %d, want 2", m.CursorX())
@ -714,8 +714,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc") sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "hello\t" { if m.Line(0) != "hello\t" {
t.Errorf("lines[0] = %q, want 'hello\\t'", m.lines[0]) t.Errorf("lines[0] = %q, want 'hello\\t'", m.Line(0))
} }
if m.CursorX() != 5 { if m.CursorX() != 5 {
t.Errorf("CursorX() = %d, want 5", m.CursorX()) t.Errorf("CursorX() = %d, want 5", m.CursorX())
@ -731,8 +731,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
if m.LineCount() != 1 { if m.LineCount() != 1 {
t.Errorf("LineCount() = %d, want 1", m.LineCount()) t.Errorf("LineCount() = %d, want 1", m.LineCount())
} }
if m.lines[0] != "helloworld" { if m.Line(0) != "helloworld" {
t.Errorf("lines[0] = %q, want 'helloworld'", m.lines[0]) t.Errorf("lines[0] = %q, want 'helloworld'", m.Line(0))
} }
if m.CursorX() != 4 { if m.CursorX() != 4 {
t.Errorf("CursorX() = %d, want 4", m.CursorX()) t.Errorf("CursorX() = %d, want 4", m.CursorX())
@ -748,8 +748,8 @@ func TestInsertModeDeletePreviousWord(t *testing.T) {
sendKeys(tm, "a", "ctrl+w", "esc") sendKeys(tm, "a", "ctrl+w", "esc")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.lines[0] != "" { if m.Line(0) != "" {
t.Errorf("lines[0] = %q, want ''", m.lines[0]) t.Errorf("lines[0] = %q, want ''", m.Line(0))
} }
if m.CursorX() != 0 { if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX()) t.Errorf("CursorX() = %d, want 0", m.CursorX())

View File

@ -12,8 +12,8 @@ func TestMoveDown(t *testing.T) {
sendKeys(tm, "j") sendKeys(tm, "j")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 1 { if m.CursorY() != 1 {
t.Errorf("cursor.y = %d, want 1", m.cursor.y) t.Errorf("cursor.y = %d, want 1", m.CursorY())
} }
}) })
@ -22,8 +22,8 @@ func TestMoveDown(t *testing.T) {
sendKeys(tm, "j", "j", "j", "j") sendKeys(tm, "j", "j", "j", "j")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 4 { if m.CursorY() != 4 {
t.Errorf("cursor.y = %d, want 4", m.cursor.y) t.Errorf("cursor.y = %d, want 4", m.CursorY())
} }
}) })
@ -32,8 +32,8 @@ func TestMoveDown(t *testing.T) {
sendKeys(tm, "j", "j", "j", "j", "j", "j", "j", "j", "j") sendKeys(tm, "j", "j", "j", "j", "j", "j", "j", "j", "j")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 5 { if m.CursorY() != 5 {
t.Errorf("cursor.y = %d, want 5", m.cursor.y) t.Errorf("cursor.y = %d, want 5", m.CursorY())
} }
}) })
} }
@ -44,8 +44,8 @@ func TestMoveDownWithCount(t *testing.T) {
sendKeys(tm, "3", "j") sendKeys(tm, "3", "j")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 3 { if m.CursorY() != 3 {
t.Errorf("cursor.y = %d, want 3", m.cursor.y) t.Errorf("cursor.y = %d, want 3", m.CursorY())
} }
}) })
@ -54,8 +54,8 @@ func TestMoveDownWithCount(t *testing.T) {
sendKeys(tm, "1", "0", "j") sendKeys(tm, "1", "0", "j")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 5 { if m.CursorY() != 5 {
t.Errorf("cursor.y = %d, want 5", m.cursor.y) t.Errorf("cursor.y = %d, want 5", m.CursorY())
} }
}) })
} }
@ -69,8 +69,8 @@ func TestMoveDownWithOverflow(t *testing.T) {
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
want := len(lines[1]) want := len(lines[1])
if m.cursor.x != want { if m.CursorX() != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) t.Errorf("cursor.x = %d, want %d", m.CursorX(), want)
} }
}) })
@ -79,8 +79,8 @@ func TestMoveDownWithOverflow(t *testing.T) {
sendKeys(tm, "j") sendKeys(tm, "j")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 3 { if m.CursorX() != 3 {
t.Errorf("cursor.x = %d, want 3", m.cursor.x) t.Errorf("cursor.x = %d, want 3", m.CursorX())
} }
}) })
} }
@ -91,8 +91,8 @@ func TestMoveUp(t *testing.T) {
sendKeys(tm, "k") sendKeys(tm, "k")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 1 { if m.CursorY() != 1 {
t.Errorf("cursor.y = %d, want 1", m.cursor.y) t.Errorf("cursor.y = %d, want 1", m.CursorY())
} }
}) })
@ -101,8 +101,8 @@ func TestMoveUp(t *testing.T) {
sendKeys(tm, "k", "k", "k", "k") sendKeys(tm, "k", "k", "k", "k")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 0 { if m.CursorY() != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y) t.Errorf("cursor.y = %d, want 0", m.CursorY())
} }
}) })
@ -111,8 +111,8 @@ func TestMoveUp(t *testing.T) {
sendKeys(tm, "k", "k", "k") sendKeys(tm, "k", "k", "k")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 0 { if m.CursorY() != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y) t.Errorf("cursor.y = %d, want 0", m.CursorY())
} }
}) })
} }
@ -123,8 +123,8 @@ func TestMoveUpWithCount(t *testing.T) {
sendKeys(tm, "3", "k") sendKeys(tm, "3", "k")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 2 { if m.CursorY() != 2 {
t.Errorf("cursor.y = %d, want 2", m.cursor.y) t.Errorf("cursor.y = %d, want 2", m.CursorY())
} }
}) })
@ -133,8 +133,8 @@ func TestMoveUpWithCount(t *testing.T) {
sendKeys(tm, "1", "0", "k") sendKeys(tm, "1", "0", "k")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 0 { if m.CursorY() != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y) t.Errorf("cursor.y = %d, want 0", m.CursorY())
} }
}) })
} }
@ -148,8 +148,8 @@ func TestMoveUpWithOverflow(t *testing.T) {
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
want := len(lines[0]) want := len(lines[0])
if m.cursor.x != want { if m.CursorX() != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) t.Errorf("cursor.x = %d, want %d", m.CursorX(), want)
} }
}) })
@ -158,8 +158,8 @@ func TestMoveUpWithOverflow(t *testing.T) {
sendKeys(tm, "k") sendKeys(tm, "k")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 3 { if m.CursorX() != 3 {
t.Errorf("cursor.x = %d, want 3", m.cursor.x) t.Errorf("cursor.x = %d, want 3", m.CursorX())
} }
}) })
} }
@ -170,8 +170,8 @@ func TestMoveRight(t *testing.T) {
sendKeys(tm, "l") sendKeys(tm, "l")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 1 { if m.CursorX() != 1 {
t.Errorf("cursor.x = %d, want 1", m.cursor.x) t.Errorf("cursor.x = %d, want 1", m.CursorX())
} }
}) })
@ -180,8 +180,8 @@ func TestMoveRight(t *testing.T) {
sendKeys(tm, "l", "l", "l", "l") sendKeys(tm, "l", "l", "l", "l")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 4 { if m.CursorX() != 4 {
t.Errorf("cursor.x = %d, want 4", m.cursor.x) t.Errorf("cursor.x = %d, want 4", m.CursorX())
} }
}) })
@ -192,8 +192,8 @@ func TestMoveRight(t *testing.T) {
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
want := len(lines[0]) want := len(lines[0])
if m.cursor.x != want { if m.CursorX() != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) t.Errorf("cursor.x = %d, want %d", m.CursorX(), want)
} }
}) })
} }
@ -204,8 +204,8 @@ func TestMoveRightWithCount(t *testing.T) {
sendKeys(tm, "3", "l") sendKeys(tm, "3", "l")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 3 { if m.CursorX() != 3 {
t.Errorf("cursor.x = %d, want 3", m.cursor.x) t.Errorf("cursor.x = %d, want 3", m.CursorX())
} }
}) })
@ -216,8 +216,8 @@ func TestMoveRightWithCount(t *testing.T) {
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
want := len(lines[0]) want := len(lines[0])
if m.cursor.x != want { if m.CursorX() != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) t.Errorf("cursor.x = %d, want %d", m.CursorX(), want)
} }
}) })
} }
@ -228,8 +228,8 @@ func TestMoveLeft(t *testing.T) {
sendKeys(tm, "h") sendKeys(tm, "h")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 2 { if m.CursorX() != 2 {
t.Errorf("cursor.x = %d, want 2", m.cursor.x) t.Errorf("cursor.x = %d, want 2", m.CursorX())
} }
}) })
@ -238,8 +238,8 @@ func TestMoveLeft(t *testing.T) {
sendKeys(tm, "h", "h", "h", "h") sendKeys(tm, "h", "h", "h", "h")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("cursor.x = %d, want 0", m.CursorX())
} }
}) })
@ -248,8 +248,8 @@ func TestMoveLeft(t *testing.T) {
sendKeys(tm, "h", "h", "h") sendKeys(tm, "h", "h", "h")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("cursor.x = %d, want 0", m.CursorX())
} }
}) })
} }
@ -260,8 +260,8 @@ func TestMoveLeftWithCount(t *testing.T) {
sendKeys(tm, "3", "h") sendKeys(tm, "3", "h")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 2 { if m.CursorX() != 2 {
t.Errorf("cursor.x = %d, want 2", m.cursor.x) t.Errorf("cursor.x = %d, want 2", m.CursorX())
} }
}) })
@ -270,8 +270,8 @@ func TestMoveLeftWithCount(t *testing.T) {
sendKeys(tm, "1", "0", "h") sendKeys(tm, "1", "0", "h")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("cursor.x = %d, want 0", m.CursorX())
} }
}) })
} }

View File

@ -9,53 +9,72 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
type cursor struct {
x int
y int
}
type Model struct { type Model struct {
lines []string // Buffers
cursor cursor buffers []*action.Buffer
anchor cursor // starting point for visual modes //next buffer id?
scrollY int
// Windows
windows []*action.Window
activeWindowId int
// Editor wide state
mode action.Mode mode action.Mode
win_h int
win_w int // Terminal dimensions
termWidth int
termHeight int
// Input and key handling
input *input.Handler input *input.Handler
// Insert repetition // Insert mode state & repetition (applied to active window)
insertCount int insertCount int
insertKeys []string insertKeys []string
insertAction action.Action insertAction action.Action
// Command mode // Command line state
command string command string
commandCursor int commandCursor int
commandError error commandError error
commandOutput string commandOutput string
// Settings // Global settings (TODO: This needs to be refactored)
settings action.Settings settings action.Settings
// Registers // Registers
registers map[rune]action.Register // name -> register registers map[rune]action.Register // name -> register
} }
func NewModel(lines []string, pos action.Position) Model { func NewModel(lines []string, pos action.Position) *Model {
return Model{ m := Model{
lines: lines,
cursor: cursor{
x: pos.Col,
y: pos.Line,
},
scrollY: 0,
mode: action.NormalMode, mode: action.NormalMode,
command: "", command: "",
input: input.NewHandler(), input: input.NewHandler(),
settings: action.NewDefaultSettings(), settings: action.NewDefaultSettings(),
registers: action.DefaultRegisters(), registers: action.DefaultRegisters(),
windows: []*action.Window{},
} }
// TODO: Temporary: Build the single buffer and window
buf := action.
NewBufferBuilder().
WithLines(lines).
Build()
m.buffers = append(m.buffers, &buf)
win := action.
NewWindowBuilder().
WithBuffer(&buf).
WithCursor(pos).
Build()
m.windows = append(m.windows, &win)
m.activeWindowId = win.Id
return &m
} }
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
@ -65,73 +84,74 @@ func (m Model) Init() tea.Cmd {
// Implement action.Model interface // Implement action.Model interface
func (m *Model) Lines() []string { func (m *Model) Lines() []string {
return m.lines win := m.ActiveWindow()
return win.Buffer.Lines
} }
func (m *Model) Line(idx int) string { func (m *Model) Line(idx int) string {
if idx < 0 || idx >= len(m.lines) { win := m.ActiveWindow()
return "" return win.Buffer.Line(idx)
}
return m.lines[idx]
} }
func (m *Model) SetLine(idx int, content string) { func (m *Model) SetLine(idx int, content string) {
if idx >= 0 && idx < len(m.lines) { win := m.ActiveWindow()
m.lines[idx] = content win.Buffer.SetLine(idx, content)
}
} }
func (m *Model) InsertLine(idx int, content string) { func (m *Model) InsertLine(idx int, content string) {
if idx < 0 { win := m.ActiveWindow()
idx = 0 win.Buffer.InsertLine(idx, content)
}
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) { func (m *Model) DeleteLine(idx int) {
if idx >= 0 && idx < len(m.lines) { win := m.ActiveWindow()
m.lines = append(m.lines[:idx], m.lines[idx+1:]...) win.Buffer.DeleteLine(idx)
}
} }
func (m *Model) LineCount() int { func (m *Model) LineCount() int {
return len(m.lines) win := m.ActiveWindow()
return win.Buffer.LineCount()
} }
func (m *Model) CursorX() int { func (m *Model) CursorX() int {
return m.cursor.x win := m.ActiveWindow()
return win.Cursor.Col
} }
func (m *Model) CursorY() int { func (m *Model) CursorY() int {
return m.cursor.y win := m.ActiveWindow()
return win.Cursor.Line
} }
func (m *Model) SetCursorX(x int) { func (m *Model) SetCursorX(x int) {
m.cursor.x = x win := m.ActiveWindow()
win.Cursor.Col = x
} }
func (m *Model) SetCursorY(y int) { func (m *Model) SetCursorY(y int) {
m.cursor.y = y win := m.ActiveWindow()
win.Cursor.Line = y
} }
// Anchor methods // Anchor methods
func (m *Model) AnchorX() int { func (m *Model) AnchorX() int {
return m.anchor.x win := m.ActiveWindow()
return win.Anchor.Col
} }
func (m *Model) AnchorY() int { func (m *Model) AnchorY() int {
return m.anchor.y win := m.ActiveWindow()
return win.Anchor.Line
} }
func (m *Model) SetAnchorX(x int) { func (m *Model) SetAnchorX(x int) {
m.anchor.x = x win := m.ActiveWindow()
win.Anchor.Col = x
} }
func (m *Model) SetAnchorY(y int) { func (m *Model) SetAnchorY(y int) {
m.anchor.y = y win := m.ActiveWindow()
win.Anchor.Line = y
} }
// Insert methods // Insert methods
@ -226,32 +246,33 @@ func (m *Model) UpdateDefaultRegister(t action.RegisterType, cnt []string) {
// Window // Window
func (m *Model) ScrollY() int { func (m *Model) ScrollY() int {
return m.scrollY win := m.ActiveWindow()
return win.ScrollY
} }
func (m *Model) SetScrollY(y int) { func (m *Model) SetScrollY(y int) {
m.scrollY = y win := m.ActiveWindow()
win.ScrollY = y
} }
func (m *Model) WinH() int { func (m *Model) WinH() int {
return m.win_h win := m.ActiveWindow()
return win.Height
} }
func (m *Model) WinW() int { func (m *Model) WinW() int {
return m.win_w win := m.ActiveWindow()
return win.Width
} }
func (m *Model) ViewPortH() int { func (m *Model) ViewPortH() int {
return m.win_h - 2 // -2 for status bar and commmand bar win := m.ActiveWindow()
return win.Height - 2
} }
func (m *Model) ClampCursorX() { func (m *Model) ClampCursorX() {
lineLen := len(m.lines[m.cursor.y]) win := m.ActiveWindow()
if lineLen == 0 { win.ClampCursorX()
m.cursor.x = 0
} else if m.cursor.x >= lineLen {
m.cursor.x = lineLen
}
} }
// AdjustScroll ensures the cursor stays within the viewport with scrollOff margins. // AdjustScroll ensures the cursor stays within the viewport with scrollOff margins.
@ -280,6 +301,25 @@ func (m *Model) AdjustScroll() {
m.SetScrollY(max(0, min(m.ScrollY(), maxScroll))) m.SetScrollY(max(0, min(m.ScrollY(), maxScroll)))
} }
// Windows
func (m *Model) Windows() []*action.Window {
return m.windows
}
func (m *Model) ActiveWindowId() int {
return m.activeWindowId
}
func (m *Model) ActiveWindow() *action.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) Mode() action.Mode { func (m *Model) Mode() action.Mode {
return m.mode return m.mode
} }
@ -294,24 +334,29 @@ func (m *Model) SetInsertRecording(count int, act action.Action) {
m.insertAction = act m.insertAction = act
} }
func (m *Model) GetCursorPosition() action.Position { func (m *Model) GetCursorPosition() *action.Position {
return action.Position{Line: m.cursor.y, Col: m.cursor.x} // Return a copy of the position
win := m.ActiveWindow()
pos := win.Cursor
return &pos
} }
func (m *Model) replayInsert() { func (m *Model) replayInsert() {
win := m.ActiveWindow()
// Replay (count - 1) more times // Replay (count - 1) more times
for i := 1; i < m.insertCount; i++ { for i := 1; i < m.insertCount; i++ {
// For 'o' and 'O', we need to create a new line first // For 'o' and 'O', we need to create a new line first
switch m.insertAction.(type) { switch m.insertAction.(type) {
case action.OpenLineBelow: case action.OpenLineBelow:
pos := m.cursor.y pos := win.Cursor.Line
m.lines = append(m.lines[:pos+1], append([]string{""}, m.lines[pos+1:]...)...) win.Buffer.Lines = append(win.Buffer.Lines[:pos+1], append([]string{""}, win.Buffer.Lines[pos+1:]...)...)
m.cursor.y++ win.Cursor.Line++
m.cursor.x = 0 win.Cursor.Col = 0
case action.OpenLineAbove: case action.OpenLineAbove:
pos := m.cursor.y pos := win.Cursor.Line
m.lines = append(m.lines[:pos], append([]string{""}, m.lines[pos:]...)...) win.Buffer.Lines = append(win.Buffer.Lines[:pos], append([]string{""}, win.Buffer.Lines[pos:]...)...)
m.cursor.x = 0 win.Cursor.Col = 0
// 'i' and 'a' don't need setup - just replay keys // 'i' and 'a' don't need setup - just replay keys
} }
@ -323,11 +368,12 @@ func (m *Model) replayInsert() {
} }
func (m *Model) ExitInsertMode() { func (m *Model) ExitInsertMode() {
win := m.ActiveWindow()
if m.insertCount > 1 { if m.insertCount > 1 {
m.replayInsert() m.replayInsert()
} }
if m.cursor.x > 0 { if win.Cursor.Col > 0 {
m.cursor.x-- win.Cursor.Col--
} }
m.mode = action.NormalMode m.mode = action.NormalMode
m.insertCount = 0 m.insertCount = 0

View File

@ -4,14 +4,47 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.win_h = msg.Height m.termHeight = msg.Height
m.win_w = msg.Width m.termWidth = msg.Width
// TODO: Implement a layout method that handles this
//
// func (m *Model) layoutWindows() {
// if len(m.windows) == 0 {
// return
// }
//
// if len(m.windows) == 1 {
// // Single window - full screen
// m.windows[0].Width = m.termWidth
// m.windows[0].Height = m.termHeight
// return
// }
//
// // Multiple windows - distribute space
// // This is where you'd implement split layout logic
// // For example, horizontal split:
// halfHeight := m.termHeight / 2
// for i, win := range m.windows {
// win.Width = m.termWidth
// if i < len(m.windows)-1 {
// win.Height = halfHeight
// } else {
// // Last window gets remainder
// win.Height = m.termHeight - (halfHeight * (len(m.windows) - 1))
// }
// }
// }
for i := range m.windows {
m.windows[i].Height = msg.Height
m.windows[i].Width = msg.Width
}
case tea.KeyMsg: case tea.KeyMsg:
// TODO: This needs to be removed, but for now its required for the tests. // TODO: This needs to be removed, but for now its required for the tests.
@ -19,7 +52,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.Type == tea.KeyCtrlC { if msg.Type == tea.KeyCtrlC {
return m, tea.Quit return m, tea.Quit
} }
cmd = m.input.Handle(&m, msg.String()) cmd = m.input.Handle(m, msg.String())
} }
// Keep cursor in view after any update // Keep cursor in view after any update

View File

@ -146,7 +146,7 @@ func drawStatusBar(m Model) string {
left := leftBar(m) left := leftBar(m)
right := rightBar(m) right := rightBar(m)
diff := m.win_w - (len(left) + len(right)) diff := m.termWidth - (len(left) + len(right))
// This happens when the terminal spawns // This happens when the terminal spawns
if diff <= 0 { if diff <= 0 {

View File

@ -16,7 +16,7 @@ const (
// PositionGetter is used to get cursor position for operator ranges // PositionGetter is used to get cursor position for operator ranges
type PositionGetter interface { type PositionGetter interface {
GetCursorPosition() action.Position GetCursorPosition() *action.Position
} }
type Handler struct { type Handler struct {
@ -205,7 +205,7 @@ func (h *Handler) handleAfterOperator(m action.Model, kind string, binding any,
start := pg.GetCursorPosition() start := pg.GetCursorPosition()
mot.Execute(m) mot.Execute(m)
end := pg.GetCursorPosition() end := pg.GetCursorPosition()
cmd := h.operator.Operate(m, start, end, mot.Type()) cmd := h.operator.Operate(m, *start, *end, mot.Type())
h.Reset() h.Reset()
return cmd return cmd
} }