package editor import ( "fmt" "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(w *core.Window, mode core.Mode, col, line int) bool { switch mode { case core.VisualLineMode: 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 := w.Anchor.Col ay := w.Anchor.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 if ay < cy || (ay == cy && ax <= cx) { startX, startY = ax, ay endX, endY = cx, cy } else { startX, startY = cx, cy endX, endY = ax, ay } // Position is inside if it falls within [start, end] inclusive afterStart := line > startY || (line == startY && col >= startX) beforeEnd := line < endY || (line == endY && col <= endX) return afterStart && beforeEnd case core.VisualBlockMode: 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 default: return false } }