From 03c3a411626e3f94fd165d5842f06ce265bc906b Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Thu, 5 Mar 2026 14:07:51 -0700 Subject: [PATCH] fix: viewing is much better, dynamic as well :) --- internal/core/window.go | 22 +- internal/editor/view.go | 465 +++++++++++++++++++++++----------------- 2 files changed, 289 insertions(+), 198 deletions(-) diff --git a/internal/core/window.go b/internal/core/window.go index 4b8a09d..8571412 100644 --- a/internal/core/window.go +++ b/internal/core/window.go @@ -62,8 +62,10 @@ func (w *Window) AdjustScroll() { return } + viewPort := w.ViewportHeight() + // Effective scrollOff (can't be more than half the viewport) - off := min(w.Options.ScrollOff, w.Height/2) + off := min(w.Options.ScrollOff, viewPort/2) // Cursor too close to top — scroll up if w.Cursor.Line < w.ScrollY+off { @@ -71,15 +73,27 @@ func (w *Window) AdjustScroll() { } // 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 + if w.Cursor.Line > w.ScrollY+viewPort-1-off { + w.ScrollY = w.Cursor.Line - viewPort + 1 + off } // Clamp scrollY to valid range - maxScroll := max(0, w.Buffer.LineCount()-w.Height) + maxScroll := max(0, w.Buffer.LineCount()-viewPort) w.ScrollY = max(0, min(w.ScrollY, maxScroll)) } +// ================================================== +// Getters (for computed values) +// ================================================== + +func (w *Window) ViewportHeight() int { + // TODO: This will need more magic when splits come into play + + // -1 for command bar + // -1 for status line + return w.Height - 2 +} + // ================================================== // Setters // ================================================== diff --git a/internal/editor/view.go b/internal/editor/view.go index 9044c97..92871aa 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -5,25 +5,282 @@ import ( "strings" "git.gophernest.net/azpect/TextEditor/internal/core" + "git.gophernest.net/azpect/TextEditor/internal/style" ) +// Model.View: Renders the complete editor view including buffer content, line +// numbers, status bar, and command line. +func (m Model) View() string { + win := m.ActiveWindow() + + // NOTES: + // One single command line across entire viewport + // Each window has its own line numbers and gutter + // Each window has its own status bar and mode + + styles := m.Styles() + settings := m.Settings() + + // Draw window + view := viewWindow(win, styles, settings, m.Mode()) + + // Command bar is seperate + cmdBar := drawCommandBar(m) + + return view + cmdBar + + // view.WriteString(drawStatusBar(m)) + // view.WriteString("\n") + // view.WriteString(drawCommandBar(m)) + // + // return view.String() +} + +// viewWindow: Renders a single window's content including line numbers and buffer text. +// Each window has its own line numbers, gutter, and viewport dimensions. +func viewWindow(w *core.Window, styles style.Styles, settings core.Settings, mode core.Mode) string { + buf := w.Buffer + var view strings.Builder + + // Compute window size (y) + start := w.ScrollY + end := w.ScrollY + w.ViewportHeight() + + // Draw buffer lines + for lineNum := start; lineNum < end; lineNum++ { + if lineNum < buf.LineCount() { + line := drawLine(w, styles, settings, mode, buf.Line(lineNum), lineNum) + view.WriteString(line) + } + view.WriteRune('\n') + } + + // Draw status line + statusBar := drawStatusBar(w, mode) + view.WriteString(statusBar + "\n") + + return view.String() +} + +// drawLine: Renders a single line with syntax highlighting, cursor, and visual selection. +// Handles gutter, cursor rendering, and visual mode highlighting. +func drawLine(w *core.Window, styles style.Styles, settings core.Settings, mode core.Mode, line string, lineNumber int) string { + runes := []rune(line) + + curStyle := styles.CursorStyle(mode) + visStyle := styles.VisualHighlight + + var view strings.Builder + + // Draw gutter first + gutter := drawGutter(w, styles, settings, lineNumber) + view.WriteString(gutter) + + // Now draw the line content + for col := 0; col <= len(runes); col++ { + // Current char is cursor + if col == w.Cursor.Col && lineNumber == w.Cursor.Line { + if col < len(runes) { + view.WriteString(curStyle.Render(string(runes[col]))) + } else { + view.WriteString(curStyle.Render(" ")) + } + + // Not cursor, but not end + } else if col < len(runes) { + if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) { + view.WriteString(visStyle.Render(string(runes[col]))) + } else { + view.WriteRune(runes[col]) + } + // Allow highlight on blank lines or chars + } else if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) { + view.WriteString(visStyle.Render(" ")) + } + } + + return view.String() +} + +// drawGutter: Renders the line number gutter with support for both absolute and +// relative line numbers, highlighting the current line differently. +func drawGutter(w *core.Window, styles style.Styles, settings core.Settings, curLine int) string { + if !(settings.Number || settings.RelativeNumber) { + return "" + } + + // Required vars + var ( + view strings.Builder + gutSize int = settings.GutterSize - 1 // -1 is for padding + currentLine bool = curLine == w.Cursor.Line + lineNumber int + + gutter string + gutterStyle = styles.Gutter + gutterStyleCur = styles.GutterCurrentLine + ) + + // If we have relative setting, set the numbers relatively + if settings.RelativeNumber { + if curLine > w.Cursor.Line { + lineNumber = curLine - w.Cursor.Line + } + + if curLine < w.Cursor.Line { + lineNumber = w.Cursor.Line - curLine + } + } + + // If we have number setting AND not relative setting OR we are on current line, use current line number + if (settings.Number && !settings.RelativeNumber) || (settings.Number && currentLine) { + lineNumber = curLine + 1 + } + + // Draw the gutter + gutter = fmt.Sprintf("%*d ", gutSize, lineNumber) + if currentLine { + view.WriteString(gutterStyleCur.Render(gutter)) + } else { + view.WriteString(gutterStyle.Render(gutter)) + } + + return view.String() + + // if m.Settings().Number || m.Settings().RelativeNumber { + // var ( + // gutter string + // currentLine bool = false + // lineNumber int + // ) + // + // if m.Settings().RelativeNumber { + // // Relative line numbers: show distance from cursor, current line shows absolute + // if i > win.Cursor.Line { + // lineNumber = i - win.Cursor.Line + // gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) + // } else if i < win.Cursor.Line { + // lineNumber = win.Cursor.Line - i + // gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) + // } else { + // // Current line: show absolute number if Number is also set, otherwise show 0 + // currentLine = true + // if m.Settings().Number { + // lineNumber = i + 1 + // gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) + // } else { + // gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, 0) + // } + // } + // } else if m.Settings().Number { + // // Absolute line numbers only + // lineNumber = i + 1 + // currentLine = (i == win.Cursor.Line) + // gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) + // } + // if currentLine { + // view.WriteString(m.Styles().GutterCurrentLine.Render(gutter)) + // } else { + // view.WriteString(m.Styles().Gutter.Render(gutter)) + // } + // } +} + +// drawStatusBar: Renders the status bar with mode and cursor position, +// padding the middle with spaces to fill the terminal width. +func drawStatusBar(w *core.Window, mode core.Mode) string { + left := leftBar(w, mode) + right := rightBar(w, mode) + + diff := w.Width - (len(left) + len(right)) + + // This happens when the terminal spawns + if diff <= 0 { + return "" + } + + middle := strings.Repeat(" ", diff) + return left + middle + right +} + +// leftBar: Returns the left side of the status bar showing the current mode. +func leftBar(w *core.Window, mode core.Mode) string { + buf := w.Buffer + return fmt.Sprintf(" %s %s", mode.ToString(), buf.Filename) +} + +// rightBar: Returns the right side of the status bar showing cursor position +// and selection count in visual mode. +func rightBar(w *core.Window, mode core.Mode) (bar string) { + if mode.IsVisualMode() { + lineCount := max(w.Anchor.Line, w.Cursor.Line) - min(w.Anchor.Line, w.Cursor.Line) + 1 + bar = fmt.Sprintf("%d:%d <%d>", w.Cursor.Line+1, w.Cursor.Col+1, lineCount) + } else { + bar = fmt.Sprintf("%d:%d ", w.Cursor.Line+1, w.Cursor.Col+1) + } + buf := w.Buffer + bar = fmt.Sprintf("%s %s", buf.Filetype, bar) + return +} + +// drawCommandBar: Renders the command line showing command input, errors, or +// output depending on the current mode and state. +func drawCommandBar(m Model) string { + // Compute left bar (command side) + var leftBar string + if m.Mode() == core.CommandMode { + leftBar = ":" + cmd := m.Command() + cur := m.CommandCursor() + for i := 0; i < len(cmd); i++ { + if i == cur { + leftBar += m.Styles().CursorStyle(m.Mode()).Render(string(cmd[i])) + } else { + leftBar += string(cmd[i]) + } + } + // Cursor at end of command + if cur >= len(cmd) { + leftBar += m.Styles().CursorStyle(m.Mode()).Render(" ") + } + // bar = fmt.Sprintf("%s %d", bar, cur) + } else if m.CommandError() != nil { + leftBar = m.Styles().CommandError.Render(m.CommandError().Error()) + } else if strings.TrimSpace(m.CommandOutput()) != "" { + leftBar = m.CommandOutput() + } else if strings.TrimSpace(m.Command()) != "" { + leftBar = fmt.Sprintf(":%s", m.Command()) + } + + // Compute right bar + + var rightBar string + if len(m.input.Pending()) > 0 { + width := 10 // Size of the block to display + rightBar = fmt.Sprintf("%-*s", width, m.input.Pending()) + } + + dif := m.termWidth - (len(leftBar) + len(rightBar)) + + bar := leftBar + strings.Repeat(" ", dif) + rightBar + return bar +} + // posInsideSelection: Returns true if the given position is inside the current // visual selection, handling all three visual modes differently. -func posInsideSelection(m Model, col, line int) bool { - win := m.ActiveWindow() - - switch m.Mode() { +func posInsideSelection(w *core.Window, mode core.Mode, col, line int) bool { + switch mode { case core.VisualLineMode: - startY := min(win.Anchor.Line, win.Cursor.Line) - endY := max(win.Anchor.Line, win.Cursor.Line) + startY := min(w.Anchor.Line, w.Cursor.Line) + endY := max(w.Anchor.Line, w.Cursor.Line) return line >= startY && line <= endY case core.VisualMode: - ax := win.Anchor.Col - ay := win.Anchor.Line + ax := w.Anchor.Col + ay := w.Anchor.Line - cx := win.Cursor.Col - cy := win.Cursor.Line + cx := w.Cursor.Col + cy := w.Cursor.Line // Normalize so start is always before end in document order var startX, startY, endX, endY int @@ -41,10 +298,10 @@ func posInsideSelection(m Model, col, line int) bool { return afterStart && beforeEnd case core.VisualBlockMode: - startX := min(win.Anchor.Col, win.Cursor.Col) - startY := min(win.Anchor.Line, win.Cursor.Line) - endX := max(win.Anchor.Col, win.Cursor.Col) - endY := max(win.Anchor.Line, win.Cursor.Line) + startX := min(w.Anchor.Col, w.Cursor.Col) + startY := min(w.Anchor.Line, w.Cursor.Line) + endX := max(w.Anchor.Col, w.Cursor.Col) + endY := max(w.Anchor.Line, w.Cursor.Line) return col >= startX && col <= endX && line >= startY && line <= endY @@ -53,183 +310,3 @@ func posInsideSelection(m Model, col, line int) bool { return false } } - -// posIsAnchor: Returns true if the given position matches the anchor position -// used for visual mode debugging/rendering. -func posIsAnchor(m Model, col, line int) bool { - win := m.ActiveWindow() - ax := win.Anchor.Col - ay := win.Anchor.Line - return col == ax && line == ay -} - -// Model.View: Renders the complete editor view including buffer content, line -// numbers, status bar, and command line. -func (m Model) View() string { - win := m.ActiveWindow() - buf := m.ActiveBuffer() - - var view strings.Builder - - viewportHeight := win.Height - 2 - start := win.ScrollY - end := win.ScrollY + viewportHeight - - for i := start; i < end; i++ { - - if i < buf.LineCount() { - - if m.Settings().Number || m.Settings().RelativeNumber { - var ( - gutter string - currentLine bool = false - lineNumber int - ) - - if m.Settings().RelativeNumber { - // Relative line numbers: show distance from cursor, current line shows absolute - if i > win.Cursor.Line { - lineNumber = i - win.Cursor.Line - gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) - } else if i < win.Cursor.Line { - lineNumber = win.Cursor.Line - i - gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) - } else { - // Current line: show absolute number if Number is also set, otherwise show 0 - currentLine = true - if m.Settings().Number { - lineNumber = i + 1 - gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) - } else { - gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, 0) - } - } - } else if m.Settings().Number { - // Absolute line numbers only - lineNumber = i + 1 - currentLine = (i == win.Cursor.Line) - gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) - } - if currentLine { - view.WriteString(m.Styles().GutterCurrentLine.Render(gutter)) - } else { - view.WriteString(m.Styles().Gutter.Render(gutter)) - } - } - - runes := []rune(buf.Lines[i]) - for x := 0; x <= len(runes); x++ { - if win.Cursor.Line == i && win.Cursor.Col == x { - if x < len(runes) { - view.WriteString(m.Styles().CursorStyle(m.Mode()).Render(string(runes[x]))) - } else { - view.WriteString(m.Styles().CursorStyle(m.Mode()).Render(" ")) - } - } else if x < len(runes) { - if m.Mode().IsVisualMode() && posIsAnchor(m, x, i) { - view.WriteString(m.Styles().VisualAnchor.Render(string(runes[x]))) - } else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) { - view.WriteString(m.Styles().VisualHighlight.Render(string(runes[x]))) - } else { - view.WriteRune(runes[x]) - } - // To highlight blank lines when in visual mode - } else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) { - view.WriteString(m.Styles().VisualHighlight.Render(" ")) - } - } - } else { - // Empty lines beyond file content - if m.Settings().Number || m.Settings().RelativeNumber { - format := fmt.Sprintf("%%-%ds ", m.Settings().GutterSize-1) - fmt.Fprintf(&view, format, "~") - } else { - view.WriteString("~") - } - } - - view.WriteString("\n") - } - - view.WriteString(drawStatusBar(m)) - view.WriteString("\n") - view.WriteString(drawCommandBar(m)) - - return view.String() -} - -func viewWindow(w core.Window) string { - var view string - - return view -} - -// drawStatusBar: Renders the status bar with mode and cursor position, -// padding the middle with spaces to fill the terminal width. -func drawStatusBar(m Model) string { - left := leftBar(m) - right := rightBar(m) - - diff := m.termWidth - (len(left) + len(right)) - - // This happens when the terminal spawns - if diff <= 0 { - return "" - } - - middle := strings.Repeat(" ", diff) - return left + middle + right -} - -// leftBar: Returns the left side of the status bar showing the current mode. -func leftBar(m Model) string { - return fmt.Sprintf(" %s", m.Mode().ToString()) -} - -// rightBar: Returns the right side of the status bar showing cursor position -// and selection count in visual mode. -func rightBar(m Model) (bar string) { - win := m.ActiveWindow() - - if m.Mode().IsVisualMode() { - lineCount := max(win.Anchor.Line, win.Cursor.Line) - min(win.Anchor.Line, win.Cursor.Line) + 1 - bar = fmt.Sprintf("%d:%d <%d>", win.Cursor.Line, win.Cursor.Col, lineCount) - } else { - bar = fmt.Sprintf("%d:%d ", win.Cursor.Line, win.Cursor.Col) - } - return -} - -// drawCommandBar: Renders the command line showing command input, errors, or -// output depending on the current mode and state. -func drawCommandBar(m Model) (bar string) { - if m.Mode() == core.CommandMode { - bar = ":" - cmd := m.Command() - cur := m.CommandCursor() - for i := 0; i < len(cmd); i++ { - if i == cur { - bar += m.Styles().CursorStyle(m.Mode()).Render(string(cmd[i])) - } else { - bar += string(cmd[i]) - } - } - // Cursor at end of command - if cur >= len(cmd) { - bar += m.Styles().CursorStyle(m.Mode()).Render(" ") - } - // bar = fmt.Sprintf("%s %d", bar, cur) - } else if m.CommandError() != nil { - bar = m.Styles().CommandError.Render(m.CommandError().Error()) - } else if strings.TrimSpace(m.CommandOutput()) != "" { - bar = m.CommandOutput() - } else if strings.TrimSpace(m.Command()) != "" { - bar = fmt.Sprintf(":%s", m.Command()) - } else if len(m.input.Pending()) > 0 { - // Get width of window and padding - rep := m.ActiveWindow().Width - 10 // 10 is padding - bar = fmt.Sprintf("%s%s", strings.Repeat(" ", rep), m.input.Pending()) - } - - return bar -}