package action import ( "strings" ) // TODO: No more global settings, window-wide settings type WinOptions struct { // Number bool // Wrap bool // Relnumber bool ScrollOff int } type Window struct { Id int Number int // Ignored for now, will be used when splits come into play Buffer *Buffer Cursor Position // DO NOT MODIFY DIRECTLY, USE SETTERS Anchor Position ScrollY int Height int Width int // Folds TODO Options WinOptions } // ================================================== // Helper methods // ================================================== // Window.ClampCursor: Clamps the cursor in the all directions to ensure the cursor // does not go into an invalid position. Such as negative values or past the end of // the line. In the Y direction it validates that the cursor does not pass the end // of the content or attempt to be "above" the content (negative value). func (w *Window) clampCursor() { // Clamp line to valid range [0, lineCount-1] maxLine := w.Buffer.LineCount() - 1 if maxLine < 0 { maxLine = 0 // Empty buffer edge case } if w.Cursor.Line < 0 { w.Cursor.Line = 0 } else if w.Cursor.Line > maxLine { w.Cursor.Line = maxLine } // Clamp column to valid range [0, lineLen] lineLen := len(w.Buffer.Lines[w.Cursor.Line]) // Safe now - Line is valid if w.Cursor.Col < 0 { w.Cursor.Col = 0 } else if lineLen == 0 { w.Cursor.Col = 0 } else if w.Cursor.Col >= lineLen { w.Cursor.Col = lineLen // Allow cursor after last char (insert mode) } } // Window.AdjustScroll ensures the cursor stays within the height with scrollOff margins. // Call this after any cursor movement. func (w *Window) AdjustScroll() { if w.Height <= 0 { return } // Effective scrollOff (can't be more than half the viewport) off := min(w.Options.ScrollOff, w.Height/2) // Cursor too close to top — scroll up if w.Cursor.Line < w.ScrollY+off { w.ScrollY = w.Cursor.Line - off } // Cursor too close to bottom — scroll down if w.Cursor.Line > w.ScrollY+w.Height-1-off { w.ScrollY = w.Cursor.Line - w.Height + 1 + off } // Clamp scrollY to valid range maxScroll := max(0, w.Buffer.LineCount()-w.Height) w.ScrollY = max(0, min(w.ScrollY, maxScroll)) } // ================================================== // View methods // ================================================== // Window.View ... func (w *Window) View(m Model) string { buf := w.Buffer viewport := w.Height - 2 // command bar (1) + status line (1) start := w.ScrollY end := w.ScrollY + viewport var view strings.Builder for i := start; i < end; i++ { // past the file, just draw the '~' if i >= buf.LineCount() { // TODO: Handle gutter and line numbers view.WriteString("~") view.WriteString("\n") continue } line := w.drawLine(m, buf.Line(i), i) view.WriteString(line) // Break to next line view.WriteString("\n") } return view.String() } // TODO: Only pass what we need from the model, not the entire model. func (w *Window) drawLine(m Model, line string, lineNum int) string { chars := []rune(line) for col := 0; col <= len(chars); col++ { // Currently on the cursor if w.Cursor.Line == lineNum && w.Cursor.Col == col { if col < len(chars) { return m.Styles().CursorStyle(m.Mode()).Render(string(chars[col])) } else { return m.Styles().CursorStyle(m.Mode()).Render(" ") } } } return "" } func (w *Window) drawGutter() string { return "" } // ================================================== // 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 w.clampCursor() } // Window.SetCursorLine: Sets the line number of the cursor position. func (w *Window) SetCursorLine(line int) { w.Cursor.Line = line w.clampCursor() } // Window.SetCursorCol: Sets the column number of the cursor position. func (w *Window) SetCursorCol(col int) { w.Cursor.Col = col w.clampCursor() } // 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 w.clampCursor() } // 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 }