All checks were successful
Run Test Suite / test (push) Successful in 42s
Updated lots of pieces with this, but it looks good.
243 lines
6.4 KiB
Go
243 lines
6.4 KiB
Go
package core
|
|
|
|
import "strconv"
|
|
|
|
type WinOptions struct {
|
|
Number bool
|
|
RelativeNumber bool
|
|
GutterSize int
|
|
// Wrap bool
|
|
ScrollOff int
|
|
}
|
|
|
|
func NewDefaultWinOptions() WinOptions {
|
|
return WinOptions{
|
|
Number: true,
|
|
RelativeNumber: true,
|
|
GutterSize: 5,
|
|
ScrollOff: 8,
|
|
}
|
|
}
|
|
|
|
type Window struct {
|
|
Id int
|
|
Number int // Ignored for now, will be used when splits come into play
|
|
Buffer *Buffer
|
|
|
|
Cursor Position // DO NOT MODIFY DIRECTLY, USE SETTERS
|
|
Anchor Position
|
|
|
|
ScrollY int
|
|
ScrollX int
|
|
Height int
|
|
Width int
|
|
|
|
// Folds TODO
|
|
Options WinOptions
|
|
}
|
|
|
|
// ==================================================
|
|
// Helper methods
|
|
// ==================================================
|
|
|
|
// Window.ClampCursor: Clamps the cursor in the all directions to ensure the cursor
|
|
// does not go into an invalid position. Such as negative values or past the end of
|
|
// the line. In the Y direction it validates that the cursor does not pass the end
|
|
// of the content or attempt to be "above" the content (negative value). This function
|
|
// is automatically called in any time the cursor changes. It only needs to be called
|
|
// when a force clamp is needed.
|
|
func (w *Window) ClampCursor() {
|
|
// Clamp line to valid range [0, lineCount-1]
|
|
maxLine := max(w.Buffer.LineCount()-1, 0)
|
|
if w.Cursor.Line < 0 {
|
|
w.Cursor.Line = 0
|
|
} else if w.Cursor.Line > maxLine {
|
|
w.Cursor.Line = maxLine
|
|
}
|
|
|
|
// Handle empty buffer - no lines to clamp column against
|
|
if w.Buffer.LineCount() == 0 {
|
|
w.Cursor.Line = 0
|
|
w.Cursor.Col = 0
|
|
return
|
|
}
|
|
|
|
// Clamp column to valid range [0, lineLen]
|
|
lineLen := w.Buffer.Lines[w.Cursor.Line].Len()
|
|
if w.Cursor.Col < 0 {
|
|
w.Cursor.Col = 0
|
|
} else if lineLen == 0 {
|
|
w.Cursor.Col = 0
|
|
} else if w.Cursor.Col >= lineLen {
|
|
w.Cursor.Col = lineLen // Allow cursor after last char (insert mode)
|
|
}
|
|
}
|
|
|
|
// Window.AdjustScroll ensures the cursor stays within the visible viewport on both axes.
|
|
// Call this after any cursor movement.
|
|
func (w *Window) AdjustScroll() {
|
|
if w.Buffer == nil || w.Height <= 0 {
|
|
return
|
|
}
|
|
|
|
viewPortHeight := w.ViewportHeight()
|
|
|
|
// Effective scrollOff (can't be more than half the viewport)
|
|
off := min(w.Options.ScrollOff, viewPortHeight/2)
|
|
|
|
// Cursor too close to top — scroll up
|
|
if w.Cursor.Line < w.ScrollY+off {
|
|
w.ScrollY = w.Cursor.Line - off
|
|
}
|
|
|
|
// Cursor too close to bottom — scroll down
|
|
if w.Cursor.Line > w.ScrollY+viewPortHeight-1-off {
|
|
w.ScrollY = w.Cursor.Line - viewPortHeight + 1 + off
|
|
}
|
|
|
|
// Clamp scrollY to valid range
|
|
maxScroll := max(0, w.Buffer.LineCount()-viewPortHeight)
|
|
w.ScrollY = max(0, min(w.ScrollY, maxScroll))
|
|
|
|
viewPortWidth := w.ViewportWidth()
|
|
if viewPortWidth <= 0 {
|
|
w.ScrollX = 0
|
|
return
|
|
}
|
|
|
|
if w.Cursor.Col < w.ScrollX {
|
|
w.ScrollX = w.Cursor.Col
|
|
} else if w.Cursor.Col >= w.ScrollX+viewPortWidth {
|
|
w.ScrollX = w.Cursor.Col - viewPortWidth + 1
|
|
}
|
|
|
|
lineLen := w.Buffer.Lines[w.Cursor.Line].Len()
|
|
maxScrollX := max(0, lineLen-viewPortWidth+1)
|
|
w.ScrollX = max(0, min(w.ScrollX, maxScrollX))
|
|
}
|
|
|
|
// ==================================================
|
|
// 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
|
|
}
|
|
|
|
func (w *Window) GutterWidth() int {
|
|
if !(w.Options.Number || w.Options.RelativeNumber) {
|
|
return 0
|
|
}
|
|
|
|
lineCount := 1
|
|
if w.Buffer != nil {
|
|
lineCount = max(1, w.Buffer.LineCount())
|
|
}
|
|
|
|
maxLineLen := len(strconv.Itoa(lineCount))
|
|
return max(w.Options.GutterSize, maxLineLen+2)
|
|
}
|
|
|
|
func (w *Window) ViewportWidth() int {
|
|
return max(0, w.Width-w.GutterWidth())
|
|
}
|
|
|
|
// ==================================================
|
|
// Setters
|
|
// ==================================================
|
|
|
|
// Window.SetNumber: Sets the position-based number of this window. Currently ignored
|
|
// until splits are implemented.
|
|
func (w *Window) SetNumber(number int) {
|
|
w.Number = number
|
|
}
|
|
|
|
// Window.SetBuffer: Sets the buffer that this window should display. This is used when
|
|
// switching between buffers or opening a new file in the current window. This function
|
|
// does clamp the cursor to the current buffer
|
|
func (w *Window) SetBuffer(buffer *Buffer) {
|
|
w.Buffer = buffer
|
|
w.ClampCursor()
|
|
}
|
|
|
|
// Window.SetCursor: Sets the cursor position in this window to the given position.
|
|
func (w *Window) SetCursor(cursor Position) {
|
|
w.Cursor = cursor
|
|
w.ClampCursor()
|
|
}
|
|
|
|
// Window.SetCursorLine: Sets the line number of the cursor position.
|
|
func (w *Window) SetCursorLine(line int) {
|
|
w.Cursor.Line = line
|
|
w.ClampCursor()
|
|
}
|
|
|
|
// Window.SetCursorCol: Sets the column number of the cursor position.
|
|
func (w *Window) SetCursorCol(col int) {
|
|
w.Cursor.Col = col
|
|
w.ClampCursor()
|
|
}
|
|
|
|
// Window.SetCursorPos: Sets both the line and column of the cursor position. This is
|
|
// a convenience method for setting both components at once.
|
|
func (w *Window) SetCursorPos(line, col int) {
|
|
w.Cursor.Line = line
|
|
w.Cursor.Col = col
|
|
w.ClampCursor()
|
|
}
|
|
|
|
// Window.SetAnchor: Sets the anchor position in this window. The anchor is used for
|
|
// visual mode selections as the starting point of the selection.
|
|
func (w *Window) SetAnchor(anchor Position) {
|
|
w.Anchor = anchor
|
|
}
|
|
|
|
// Window.SetAnchorLine: Sets the line number of the anchor position.
|
|
func (w *Window) SetAnchorLine(line int) {
|
|
w.Anchor.Line = line
|
|
}
|
|
|
|
// Window.SetAnchorCol: Sets the column number of the anchor position.
|
|
func (w *Window) SetAnchorCol(col int) {
|
|
w.Anchor.Col = col
|
|
}
|
|
|
|
// Window.SetAnchorPos: Sets both the line and column of the anchor position. This is
|
|
// a convenience method for setting both components at once.
|
|
func (w *Window) SetAnchorPos(line, col int) {
|
|
w.Anchor.Line = line
|
|
w.Anchor.Col = col
|
|
}
|
|
|
|
// Window.SetScrollY: Sets the vertical scroll offset of this window. This determines
|
|
// which line appears at the top of the visible viewport.
|
|
func (w *Window) SetScrollY(scrollY int) {
|
|
w.ScrollY = scrollY
|
|
}
|
|
|
|
// Window.SetHeight: Sets the height of this window in lines.
|
|
func (w *Window) SetHeight(height int) {
|
|
w.Height = height
|
|
}
|
|
|
|
// Window.SetWidth: Sets the width of this window in columns.
|
|
func (w *Window) SetWidth(width int) {
|
|
w.Width = width
|
|
}
|
|
|
|
// Window.SetDimensions: Sets both the width and height of this window. This is a
|
|
// convenience method for setting both dimensions at once.
|
|
func (w *Window) SetDimensions(width, height int) {
|
|
w.Width = width
|
|
w.Height = height
|
|
}
|
|
|
|
// Window.SetOptions: Sets the options of this window.
|
|
func (w *Window) SetOptions(opts WinOptions) {
|
|
w.Options = opts
|
|
}
|