diff --git a/internal/action/buffer.go b/internal/action/buffer.go index 4845cb4..e3eebda 100644 --- a/internal/action/buffer.go +++ b/internal/action/buffer.go @@ -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 +} diff --git a/internal/action/buffer_builder.go b/internal/action/buffer_builder.go new file mode 100644 index 0000000..3506b70 --- /dev/null +++ b/internal/action/buffer_builder.go @@ -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 +} diff --git a/internal/action/window.go b/internal/action/window.go index 273ecf9..28ac87f 100644 --- a/internal/action/window.go +++ b/internal/action/window.go @@ -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 +} diff --git a/internal/action/window_builder.go b/internal/action/window_builder.go new file mode 100644 index 0000000..a213103 --- /dev/null +++ b/internal/action/window_builder.go @@ -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 +} diff --git a/internal/editor/model.go b/internal/editor/model.go index 081d4dd..398c0f2 100644 --- a/internal/editor/model.go +++ b/internal/editor/model.go @@ -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 []*action.Window + // 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 { diff --git a/internal/editor/update.go b/internal/editor/update.go index 22dcf07..352f270 100644 --- a/internal/editor/update.go +++ b/internal/editor/update.go @@ -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 diff --git a/internal/editor/view.go b/internal/editor/view.go index 9575a49..7c61c63 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -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 {