wip: implement windows, buffers and builders

This commit is contained in:
Hayden Hargreaves 2026-02-26 13:20:21 -07:00
parent 3339dd4409
commit ea4638d815
7 changed files with 387 additions and 75 deletions

View File

@ -22,26 +22,12 @@ type Buffer struct {
// UndoTree TODO: This will be big
}
// Not great, but maybe the best way
var CurrentBufferId int = 1
// ==================================================
// Helper methods
// ==================================================
func NewEmptyBuffer(lines []string) *Buffer {
buf := Buffer{
Id: CurrentBufferId,
Filename: "",
Filetype: "",
Lines: lines,
Modified: false,
Loaded: true,
Listed: true,
}
CurrentBufferId++
return &buf
}
// Get the line at an index
// 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 ""
@ -49,14 +35,17 @@ func (b *Buffer) Line(idx int) string {
return b.Lines[idx]
}
// Set the content at an index.
// 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
}
}
// Insert a line with content at an index
// 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
@ -67,14 +56,56 @@ func (b *Buffer) InsertLine(idx int, content string) {
b.Lines = append(b.Lines[:idx], append([]string{content}, b.Lines[idx:]...)...)
}
// Delete a line at an index
// 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:]...)
}
}
// Get the number of lines in the buffer
// 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

@ -16,36 +16,20 @@ type Window struct {
Anchor Position
ScrollY int
Width int
Height int
Width int
// Folds // TODO
// Options WinOptions
}
// Not great, but maybe the best way
var CurrentWindowId int = 1000
func NewEmptyWindow(lines []string, w, h int) *Window {
win := &Window{
Id: CurrentWindowId,
Number: 1, // Ignored for now
Buffer: NewEmptyBuffer(lines),
Cursor: Position{Line: 0, Col: 0},
Anchor: Position{Line: 0, Col: 0},
ScrollY: 0,
Width: w,
Height: h,
}
// Increment
CurrentWindowId++
return win
}
// ==================================================
// 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 {
@ -54,3 +38,87 @@ func (w *Window) ClampCursorX() {
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
}

View File

@ -10,31 +10,36 @@ import (
)
type Model struct {
// lines []string
// cursor cursor
// anchor cursor // starting point for visual modes
// scrollY int
mode action.Mode
win_h int
win_w int
// Buffers
buffers []*action.Buffer
//next buffer id?
activeWindow int
// Windows
windows []*action.Window
activeWindowId int
// Editor wide state
mode action.Mode
// Terminal dimensions
termWidth int
termHeight int
// Input and key handling
input *input.Handler
// Insert repetition
// Insert mode state & repetition (applied to active window)
insertCount int
insertKeys []string
insertAction action.Action
// Command mode
// Command line state
command string
commandCursor int
commandError error
commandOutput string
// Settings
// Global settings (TODO: This needs to be refactored)
settings action.Settings
// Registers
@ -43,12 +48,6 @@ type Model struct {
func NewModel(lines []string, pos action.Position) *Model {
m := Model{
// lines: lines,
// cursor: cursor{
// x: pos.Col,
// y: pos.Line,
// },
// scrollY: 0,
mode: action.NormalMode,
command: "",
input: input.NewHandler(),
@ -58,11 +57,22 @@ func NewModel(lines []string, pos action.Position) *Model {
windows: []*action.Window{},
}
// Temporary
win := action.NewEmptyWindow(lines, 0, 0)
win.Cursor = pos // Set initial cursor position
m.windows = append(m.windows, win)
m.activeWindow = win.Id
// 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
}
@ -297,7 +307,7 @@ func (m *Model) Windows() []*action.Window {
}
func (m *Model) ActiveWindowId() int {
return m.activeWindow
return m.activeWindowId
}
func (m *Model) ActiveWindow() *action.Window {

View File

@ -10,10 +10,37 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.win_h = msg.Height
m.win_w = msg.Width
m.termHeight = msg.Height
m.termWidth = msg.Width
// TODO: Temp, this is lame
// 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

View File

@ -146,7 +146,7 @@ func drawStatusBar(m Model) string {
left := leftBar(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
if diff <= 0 {