201 lines
5.3 KiB
Go
201 lines
5.3 KiB
Go
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.win_h - 2 // -2 for status bar and command bar
|
|
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.win_w - (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
|
|
}
|