package editor import ( "fmt" "strings" "git.gophernest.net/azpect/TextEditor/internal/action" ) func posInsideSelection(m Model, col, line int) bool { switch m.Mode() { case action.VisualLineMode: startY := min(m.AnchorY(), m.CursorY()) endY := max(m.AnchorY(), m.CursorY()) return line >= startY && line <= endY case action.VisualMode: ax := m.AnchorX() ay := m.AnchorY() cx := m.CursorX() cy := m.CursorY() // 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 action.VisualBlockMode: startX := min(m.AnchorX(), m.CursorX()) startY := min(m.AnchorY(), m.CursorY()) endX := max(m.AnchorX(), m.CursorX()) endY := max(m.AnchorY(), m.CursorY()) return col >= startX && col <= endX && line >= startY && line <= endY default: return false } } func posIsAnchor(m Model, col, line int) bool { ax := m.AnchorX() ay := m.AnchorY() return col == ax && line == ay } func (m Model) View() string { var view strings.Builder viewportHeight := m.ViewPortH() start := m.ScrollY() end := m.ScrollY() + viewportHeight for i := start; i < end; i++ { if i < m.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 > m.CursorY() { lineNumber = i - m.CursorY() gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) } else if i < m.CursorY() { lineNumber = m.CursorY() - 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 == m.CursorY()) gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber) } view.WriteString(m.gutterStyle(currentLine).Render(gutter)) } runes := []rune(m.Line(i)) for x := 0; x <= len(runes); x++ { if m.CursorY() == i && m.CursorX() == x { if x < len(runes) { view.WriteString(m.cursorStyle().Render(string(runes[x]))) } else { view.WriteString(m.cursorStyle().Render(" ")) } } else if x < len(runes) { if m.Mode().IsVisualMode() && posIsAnchor(m, x, i) { view.WriteString(m.visualAnchorStyle().Render(string(runes[x]))) } else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) { view.WriteString(m.visualHighlightStyle().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.visualHighlightStyle().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 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 } func leftBar(m Model) string { return fmt.Sprintf(" %s", m.Mode().ToString()) } func rightBar(m Model) (bar string) { if m.Mode().IsVisualMode() { lineCount := max(m.AnchorY(), m.CursorY()) - min(m.AnchorY(), m.CursorY()) + 1 bar = fmt.Sprintf("%d:%d <%d>", m.CursorY(), m.CursorX(), lineCount) } else { bar = fmt.Sprintf("%d:%d ", m.CursorY(), m.CursorX()) } return } func drawCommandBar(m Model) (bar string) { if m.Mode() == action.CommandMode { bar = ":" cmd := m.Command() cur := m.CommandCursor() for i := 0; i < len(cmd); i++ { if i == cur { bar += m.cursorStyle().Render(string(cmd[i])) } else { bar += string(cmd[i]) } } // Cursor at end of command if cur >= len(cmd) { bar += m.cursorStyle().Render(" ") } // bar = fmt.Sprintf("%s %d", bar, cur) } else if m.CommandError() != nil { bar = m.commandErrorStyle().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()) } return bar }