fix: viewing is much better, dynamic as well :)
This commit is contained in:
parent
ccb061989a
commit
03c3a41162
@ -62,8 +62,10 @@ func (w *Window) AdjustScroll() {
|
||||
return
|
||||
}
|
||||
|
||||
viewPort := w.ViewportHeight()
|
||||
|
||||
// Effective scrollOff (can't be more than half the viewport)
|
||||
off := min(w.Options.ScrollOff, w.Height/2)
|
||||
off := min(w.Options.ScrollOff, viewPort/2)
|
||||
|
||||
// Cursor too close to top — scroll up
|
||||
if w.Cursor.Line < w.ScrollY+off {
|
||||
@ -71,15 +73,27 @@ func (w *Window) AdjustScroll() {
|
||||
}
|
||||
|
||||
// Cursor too close to bottom — scroll down
|
||||
if w.Cursor.Line > w.ScrollY+w.Height-1-off {
|
||||
w.ScrollY = w.Cursor.Line - w.Height + 1 + off
|
||||
if w.Cursor.Line > w.ScrollY+viewPort-1-off {
|
||||
w.ScrollY = w.Cursor.Line - viewPort + 1 + off
|
||||
}
|
||||
|
||||
// Clamp scrollY to valid range
|
||||
maxScroll := max(0, w.Buffer.LineCount()-w.Height)
|
||||
maxScroll := max(0, w.Buffer.LineCount()-viewPort)
|
||||
w.ScrollY = max(0, min(w.ScrollY, maxScroll))
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// Getters (for computed values)
|
||||
// ==================================================
|
||||
|
||||
func (w *Window) ViewportHeight() int {
|
||||
// TODO: This will need more magic when splits come into play
|
||||
|
||||
// -1 for command bar
|
||||
// -1 for status line
|
||||
return w.Height - 2
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// Setters
|
||||
// ==================================================
|
||||
|
||||
@ -5,25 +5,282 @@ import (
|
||||
"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(m Model, col, line int) bool {
|
||||
win := m.ActiveWindow()
|
||||
|
||||
switch m.Mode() {
|
||||
func posInsideSelection(w *core.Window, mode core.Mode, col, line int) bool {
|
||||
switch mode {
|
||||
case core.VisualLineMode:
|
||||
startY := min(win.Anchor.Line, win.Cursor.Line)
|
||||
endY := max(win.Anchor.Line, win.Cursor.Line)
|
||||
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 := win.Anchor.Col
|
||||
ay := win.Anchor.Line
|
||||
ax := w.Anchor.Col
|
||||
ay := w.Anchor.Line
|
||||
|
||||
cx := win.Cursor.Col
|
||||
cy := win.Cursor.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
|
||||
@ -41,10 +298,10 @@ func posInsideSelection(m Model, col, line int) bool {
|
||||
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)
|
||||
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
|
||||
@ -53,183 +310,3 @@ func posInsideSelection(m Model, col, line int) bool {
|
||||
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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user