Gim/internal/editor/view.go
Hayden Hargreaves b1b3edf810 feat: this is huge, and needs some review, but for now its good
The tests are not passing, something to do with view I think.
2026-02-26 23:18:18 -07:00

213 lines
5.6 KiB
Go

package editor
import (
"fmt"
"strings"
"git.gophernest.net/azpect/TextEditor/internal/action"
)
func posInsideSelection(m Model, col, line int) bool {
win := m.ActiveWindow()
switch m.Mode() {
case action.VisualLineMode:
startY := min(win.Anchor.Line, win.Cursor.Line)
endY := max(win.Anchor.Line, win.Cursor.Line)
return line >= startY && line <= endY
case action.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 action.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
}
}
func posIsAnchor(m Model, col, line int) bool {
win := m.ActiveWindow()
ax := win.Anchor.Col
ay := win.Anchor.Line
return col == ax && line == ay
}
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 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) {
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
}
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.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())
}
return bar
}