package editor import ( "fmt" "strings" "git.gophernest.net/azpect/TextEditor/internal/core" ) // 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() { case core.VisualLineMode: startY := min(win.Anchor.Line, win.Cursor.Line) endY := max(win.Anchor.Line, win.Cursor.Line) return line >= startY && line <= endY case core.VisualMode: ax := win.Anchor.Col ay := win.Anchor.Line cx := win.Cursor.Col cy := win.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(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) return col >= startX && col <= endX && line >= startY && line <= endY default: 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 }