feat: implemented horizontal scroll, tested
All checks were successful
Run Test Suite / test (push) Successful in 42s
All checks were successful
Run Test Suite / test (push) Successful in 42s
Updated lots of pieces with this, but it looks good.
This commit is contained in:
parent
9899d1af1f
commit
31629d1908
@ -164,8 +164,8 @@
|
|||||||
- [ ] `~` - Swap case of selection
|
- [ ] `~` - Swap case of selection
|
||||||
- [ ] `u` - Lowercase selection
|
- [ ] `u` - Lowercase selection
|
||||||
- [ ] `U` - Uppercase selection
|
- [ ] `U` - Uppercase selection
|
||||||
- [ ] `J` - Join selected lines
|
|
||||||
- [ ] `o` - Go to other end of selection
|
- [ ] `o` - Go to other end of selection
|
||||||
|
- [ ] `J` - Join selected lines
|
||||||
- [ ] `O` - Go to other corner (block mode)
|
- [ ] `O` - Go to other corner (block mode)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
type WinOptions struct {
|
type WinOptions struct {
|
||||||
Number bool
|
Number bool
|
||||||
RelativeNumber bool
|
RelativeNumber bool
|
||||||
@ -26,6 +28,7 @@ type Window struct {
|
|||||||
Anchor Position
|
Anchor Position
|
||||||
|
|
||||||
ScrollY int
|
ScrollY int
|
||||||
|
ScrollX int
|
||||||
Height int
|
Height int
|
||||||
Width int
|
Width int
|
||||||
|
|
||||||
@ -70,17 +73,17 @@ func (w *Window) ClampCursor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.AdjustScroll ensures the cursor stays within the height with scrollOff margins.
|
// Window.AdjustScroll ensures the cursor stays within the visible viewport on both axes.
|
||||||
// Call this after any cursor movement.
|
// Call this after any cursor movement.
|
||||||
func (w *Window) AdjustScroll() {
|
func (w *Window) AdjustScroll() {
|
||||||
if w.Height <= 0 {
|
if w.Buffer == nil || w.Height <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
viewPort := w.ViewportHeight()
|
viewPortHeight := w.ViewportHeight()
|
||||||
|
|
||||||
// Effective scrollOff (can't be more than half the viewport)
|
// Effective scrollOff (can't be more than half the viewport)
|
||||||
off := min(w.Options.ScrollOff, viewPort/2)
|
off := min(w.Options.ScrollOff, viewPortHeight/2)
|
||||||
|
|
||||||
// Cursor too close to top — scroll up
|
// Cursor too close to top — scroll up
|
||||||
if w.Cursor.Line < w.ScrollY+off {
|
if w.Cursor.Line < w.ScrollY+off {
|
||||||
@ -88,13 +91,29 @@ func (w *Window) AdjustScroll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cursor too close to bottom — scroll down
|
// Cursor too close to bottom — scroll down
|
||||||
if w.Cursor.Line > w.ScrollY+viewPort-1-off {
|
if w.Cursor.Line > w.ScrollY+viewPortHeight-1-off {
|
||||||
w.ScrollY = w.Cursor.Line - viewPort + 1 + off
|
w.ScrollY = w.Cursor.Line - viewPortHeight + 1 + off
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp scrollY to valid range
|
// Clamp scrollY to valid range
|
||||||
maxScroll := max(0, w.Buffer.LineCount()-viewPort)
|
maxScroll := max(0, w.Buffer.LineCount()-viewPortHeight)
|
||||||
w.ScrollY = max(0, min(w.ScrollY, maxScroll))
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
@ -109,6 +128,24 @@ func (w *Window) ViewportHeight() int {
|
|||||||
return w.Height - 2
|
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
|
// Setters
|
||||||
// ==================================================
|
// ==================================================
|
||||||
|
|||||||
@ -279,6 +279,115 @@ func TestWindow_AdjustScroll(t *testing.T) {
|
|||||||
t.Error("cursor should be visible in small viewport")
|
t.Error("cursor should be visible in small viewport")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("scrolls right when cursor moves past visible width", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"0123456789abcdefghij"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithWidth(12).
|
||||||
|
WithHeight(10).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorCol(10)
|
||||||
|
win.AdjustScroll()
|
||||||
|
|
||||||
|
if win.ScrollX == 0 {
|
||||||
|
t.Fatal("expected horizontal scroll to move right")
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport := win.ViewportWidth()
|
||||||
|
if win.Cursor.Col < win.ScrollX || win.Cursor.Col >= win.ScrollX+viewport {
|
||||||
|
t.Errorf("cursor at %d not visible in scroll range [%d, %d)",
|
||||||
|
win.Cursor.Col, win.ScrollX, win.ScrollX+viewport)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scrolls left when cursor moves back into hidden content", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().
|
||||||
|
WithLines([]string{"0123456789abcdefghij"}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithWidth(12).
|
||||||
|
WithHeight(10).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.SetCursorCol(14)
|
||||||
|
win.AdjustScroll()
|
||||||
|
if win.ScrollX == 0 {
|
||||||
|
t.Fatal("expected initial horizontal scroll to move right")
|
||||||
|
}
|
||||||
|
|
||||||
|
win.SetCursorCol(2)
|
||||||
|
win.AdjustScroll()
|
||||||
|
|
||||||
|
if win.ScrollX != 2 {
|
||||||
|
t.Errorf("expected horizontal scroll to follow cursor left, got %d", win.ScrollX)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindow_AdjustScrollHorizontalRuneAware(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
line string
|
||||||
|
width int
|
||||||
|
cursorCol int
|
||||||
|
initialScroll int
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ascii line scrolls using visible columns",
|
||||||
|
line: "0123456789abcdef",
|
||||||
|
width: 12,
|
||||||
|
cursorCol: 10,
|
||||||
|
expected: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multibyte rune line uses rune length not bytes",
|
||||||
|
line: "abécdefghij",
|
||||||
|
width: 10,
|
||||||
|
cursorCol: 10,
|
||||||
|
expected: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "moving left pulls scroll back toward cursor",
|
||||||
|
line: "abécdefghij",
|
||||||
|
width: 10,
|
||||||
|
cursorCol: 2,
|
||||||
|
initialScroll: 6,
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().WithLines([]string{tt.line}).Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithWidth(tt.width).
|
||||||
|
WithHeight(10).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
win.ScrollX = tt.initialScroll
|
||||||
|
win.SetCursorCol(tt.cursorCol)
|
||||||
|
win.AdjustScroll()
|
||||||
|
|
||||||
|
if win.ScrollX != tt.expected {
|
||||||
|
t.Errorf("ScrollX() = %d, want %d", win.ScrollX, tt.expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport := win.ViewportWidth()
|
||||||
|
if viewport > 0 && (win.Cursor.Col < win.ScrollX || win.Cursor.Col >= win.ScrollX+viewport) {
|
||||||
|
t.Errorf("cursor at %d not visible in scroll range [%d, %d)",
|
||||||
|
win.Cursor.Col, win.ScrollX, win.ScrollX+viewport)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWindow_ViewportHeight(t *testing.T) {
|
func TestWindow_ViewportHeight(t *testing.T) {
|
||||||
@ -327,6 +436,34 @@ func TestWindow_ViewportHeight(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWindow_ViewportWidth(t *testing.T) {
|
||||||
|
t.Run("subtracts gutter width", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().WithLines([]string{"line"}).Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithWidth(20).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
expected := 15
|
||||||
|
if win.ViewportWidth() != expected {
|
||||||
|
t.Errorf("expected viewport width %d, got %d", expected, win.ViewportWidth())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns full width when gutter disabled", func(t *testing.T) {
|
||||||
|
buf := NewBufferBuilder().WithLines([]string{"line"}).Build()
|
||||||
|
win := NewWindowBuilder().
|
||||||
|
WithBuffer(&buf).
|
||||||
|
WithWidth(20).
|
||||||
|
WithOptions(WinOptions{Number: false, RelativeNumber: false, GutterSize: 5, ScrollOff: 8}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
if win.ViewportWidth() != 20 {
|
||||||
|
t.Errorf("expected viewport width 20, got %d", win.ViewportWidth())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestWindow_SetOptions(t *testing.T) {
|
func TestWindow_SetOptions(t *testing.T) {
|
||||||
t.Run("updates options", func(t *testing.T) {
|
t.Run("updates options", func(t *testing.T) {
|
||||||
buf := NewBufferBuilder().Build()
|
buf := NewBufferBuilder().Build()
|
||||||
|
|||||||
86
internal/editor/integration_scroll_horizontal_test.go
Normal file
86
internal/editor/integration_scroll_horizontal_test.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package editor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ansiPattern = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||||
|
|
||||||
|
func stripANSI(s string) string {
|
||||||
|
return ansiPattern.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstViewLine(view string) string {
|
||||||
|
lines := strings.Split(view, "\n")
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return stripANSI(lines[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHorizontalScrollRender(t *testing.T) {
|
||||||
|
line := "0123456789abcdef"
|
||||||
|
tm := newTestModelWithTermSize(t, []string{line}, core.Position{Line: 0, Col: 0}, 12, 10)
|
||||||
|
|
||||||
|
for range 10 {
|
||||||
|
sendKeys(tm, "l")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ActiveWindow().ScrollX != 4 {
|
||||||
|
t.Fatalf("ScrollX() = %d, want 4", m.ActiveWindow().ScrollX)
|
||||||
|
}
|
||||||
|
|
||||||
|
visible := firstViewLine(m.View())
|
||||||
|
if !strings.Contains(visible, "456789a") {
|
||||||
|
t.Fatalf("first visible line = %q, want it to contain %q", visible, "456789a")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHorizontalScrollRenderReturnsWhenMovingLeft(t *testing.T) {
|
||||||
|
line := "0123456789abcdef"
|
||||||
|
tm := newTestModelWithTermSize(t, []string{line}, core.Position{Line: 0, Col: 0}, 12, 10)
|
||||||
|
|
||||||
|
for range 10 {
|
||||||
|
sendKeys(tm, "l")
|
||||||
|
}
|
||||||
|
for range 10 {
|
||||||
|
sendKeys(tm, "h")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ActiveWindow().ScrollX != 0 {
|
||||||
|
t.Fatalf("ScrollX() after moving back left = %d, want 0", m.ActiveWindow().ScrollX)
|
||||||
|
}
|
||||||
|
|
||||||
|
visible := firstViewLine(m.View())
|
||||||
|
if !strings.Contains(visible, "0123456") {
|
||||||
|
t.Fatalf("first visible line after moving back left = %q, want it to contain %q", visible, "0123456")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHorizontalScrollResizeClampWithRunes(t *testing.T) {
|
||||||
|
line := "abécdefghij"
|
||||||
|
tm := newTestModelWithTermSize(t, []string{line}, core.Position{Line: 0, Col: 0}, 10, 10)
|
||||||
|
|
||||||
|
for range 10 {
|
||||||
|
sendKeys(tm, "l")
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.Send(tea.WindowSizeMsg{Width: 12, Height: 10})
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.ActiveWindow().ScrollX != 5 {
|
||||||
|
t.Fatalf("ScrollX() after resize = %d, want 5", m.ActiveWindow().ScrollX)
|
||||||
|
}
|
||||||
|
|
||||||
|
visible := firstViewLine(m.View())
|
||||||
|
if !strings.Contains(visible, "efghij") {
|
||||||
|
t.Fatalf("first visible line after resize = %q, want it to contain %q", visible, "efghij")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -141,8 +141,13 @@ func (m *Model) ClearLastChangeKeys() {
|
|||||||
m.lastChangeKeys = []string{}
|
m.lastChangeKeys = []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle key also adjusts the scroll anytime an input is pressed.
|
||||||
func (m *Model) HandleKey(key string) tea.Cmd {
|
func (m *Model) HandleKey(key string) tea.Cmd {
|
||||||
return m.input.Handle(m, key)
|
cmd := m.input.Handle(m, key)
|
||||||
|
if len(m.windows) > 0 {
|
||||||
|
m.ActiveWindow().AdjustScroll()
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) ExitInsertMode() {
|
func (m *Model) ExitInsertMode() {
|
||||||
@ -151,7 +156,7 @@ func (m *Model) ExitInsertMode() {
|
|||||||
m.replayInsert()
|
m.replayInsert()
|
||||||
}
|
}
|
||||||
if win.Cursor.Col > 0 {
|
if win.Cursor.Col > 0 {
|
||||||
win.Cursor.Col--
|
win.SetCursorCol(win.Cursor.Col - 1)
|
||||||
}
|
}
|
||||||
m.mode = core.NormalMode
|
m.mode = core.NormalMode
|
||||||
m.insertCount = 0
|
m.insertCount = 0
|
||||||
@ -271,6 +276,7 @@ func (m *Model) processInsertKey(key string) {
|
|||||||
}
|
}
|
||||||
win.SetCursorCol(col + len(key))
|
win.SetCursorCol(col + len(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
|
|||||||
@ -48,6 +48,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
for i := range m.windows {
|
for i := range m.windows {
|
||||||
m.windows[i].Height = msg.Height
|
m.windows[i].Height = msg.Height
|
||||||
m.windows[i].Width = msg.Width
|
m.windows[i].Width = msg.Width
|
||||||
|
m.windows[i].AdjustScroll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is not great, totally temporary. But I don't like vim's handling, so this is up to me
|
// TODO: This is not great, totally temporary. But I don't like vim's handling, so this is up to me
|
||||||
@ -60,6 +61,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
scrollAction := motion.ScrollDownPage{Divisor: 4} // Quarter page
|
scrollAction := motion.ScrollDownPage{Divisor: 4} // Quarter page
|
||||||
cmd = scrollAction.Execute(m)
|
cmd = scrollAction.Execute(m)
|
||||||
}
|
}
|
||||||
|
if len(m.windows) > 0 {
|
||||||
|
m.ActiveWindow().AdjustScroll()
|
||||||
|
}
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
// TODO: This needs to be removed, but for now its required for the tests.
|
// TODO: This needs to be removed, but for now its required for the tests.
|
||||||
@ -83,13 +87,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.CommandOutput().ScrollUp()
|
m.CommandOutput().ScrollUp()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cmd = m.input.Handle(m, msg.String())
|
cmd = m.HandleKey(msg.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep cursor in view after any update
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
win.AdjustScroll()
|
|
||||||
|
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,12 +61,12 @@ func viewWindow(w *core.Window, t theme.EditorTheme, options core.WinOptions, mo
|
|||||||
// Draw buffer lines
|
// Draw buffer lines
|
||||||
for lineNum := start; lineNum < end; lineNum++ {
|
for lineNum := start; lineNum < end; lineNum++ {
|
||||||
if lineNum < buf.LineCount() {
|
if lineNum < buf.LineCount() {
|
||||||
styleMap := make([]lipgloss.Style, len([]rune(buf.Line(lineNum))))
|
line := buf.Lines[lineNum]
|
||||||
|
styleMap := make([]lipgloss.Style, line.Len())
|
||||||
if sx != nil {
|
if sx != nil {
|
||||||
styleMap = sx.LineStyleMap(buf, lineNum, t)
|
styleMap = sx.LineStyleMap(buf, lineNum, t)
|
||||||
}
|
}
|
||||||
line := drawLine(w, t, options, mode, buf.Line(lineNum), lineNum, styleMap)
|
view.WriteString(drawLine(w, t, options, mode, line, lineNum, styleMap))
|
||||||
view.WriteString(line)
|
|
||||||
} else {
|
} else {
|
||||||
view.WriteString(strings.Repeat(t.Background.Render(" "), w.Width))
|
view.WriteString(strings.Repeat(t.Background.Render(" "), w.Width))
|
||||||
}
|
}
|
||||||
@ -82,46 +82,49 @@ func viewWindow(w *core.Window, t theme.EditorTheme, options core.WinOptions, mo
|
|||||||
|
|
||||||
// drawLine: Renders a single line with syntax highlighting, cursor, and visual selection.
|
// drawLine: Renders a single line with syntax highlighting, cursor, and visual selection.
|
||||||
// Handles gutter, cursor rendering, and visual mode highlighting.
|
// Handles gutter, cursor rendering, and visual mode highlighting.
|
||||||
func drawLine(w *core.Window, t theme.EditorTheme, options core.WinOptions, mode core.Mode, line string, lineNumber int, styleMap []lipgloss.Style) string {
|
func drawLine(w *core.Window, t theme.EditorTheme, options core.WinOptions, mode core.Mode, line *core.GapBuffer, lineNumber int, styleMap []lipgloss.Style) string {
|
||||||
var view strings.Builder
|
var view strings.Builder
|
||||||
runes := []rune(line)
|
lineLen := line.Len()
|
||||||
|
|
||||||
// Draw gutter first
|
// Draw gutter first
|
||||||
gutter := drawGutter(w, t, options, lineNumber)
|
gutter := drawGutter(w, t, options, lineNumber)
|
||||||
view.WriteString(gutter)
|
view.WriteString(gutter)
|
||||||
|
contentWidth := w.ViewportWidth()
|
||||||
|
if contentWidth <= 0 {
|
||||||
|
return view.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw visible content slice only
|
||||||
|
startCol := max(0, w.ScrollX)
|
||||||
|
for screenCol := range contentWidth {
|
||||||
|
col := startCol + screenCol
|
||||||
|
|
||||||
// Now draw the line content
|
|
||||||
for col := 0; col <= len(runes); col++ {
|
|
||||||
// Current char is cursor
|
// Current char is cursor
|
||||||
if col == w.Cursor.Col && lineNumber == w.Cursor.Line {
|
if col == w.Cursor.Col && lineNumber == w.Cursor.Line {
|
||||||
if col < len(runes) {
|
if col < lineLen {
|
||||||
cur := t.Cursor(mode, styleMap[col])
|
cur := t.Cursor(mode, styleMap[col])
|
||||||
view.WriteString(cur.Render(string(runes[col])))
|
view.WriteString(cur.Render(string(line.RuneAt(col))))
|
||||||
} else {
|
} else {
|
||||||
view.WriteString(t.DefaultCursor(mode).Render(" "))
|
view.WriteString(t.DefaultCursor(mode).Render(" "))
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Not cursor, but not end
|
if col < lineLen {
|
||||||
} else if col < len(runes) {
|
|
||||||
s := styleMap[col]
|
s := styleMap[col]
|
||||||
if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
||||||
vis := t.VisualHighlightWithTextColor(s)
|
vis := t.VisualHighlightWithTextColor(s)
|
||||||
view.WriteString(vis.Render(string(runes[col])))
|
view.WriteString(vis.Render(string(line.RuneAt(col))))
|
||||||
} else {
|
} else {
|
||||||
view.WriteString(s.Render(string(runes[col])))
|
view.WriteString(s.Render(string(line.RuneAt(col))))
|
||||||
}
|
}
|
||||||
// Allow highlight on blank lines or chars
|
|
||||||
} else if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
} else if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
||||||
view.WriteString(t.VisualHightlight.Render(" "))
|
view.WriteString(t.VisualHightlight.Render(" "))
|
||||||
|
} else {
|
||||||
|
view.WriteString(t.Background.Render(" "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pad remainder of line to window width with background color
|
|
||||||
dif := w.Width - lipgloss.Width(view.String())
|
|
||||||
if dif > 0 {
|
|
||||||
view.WriteString(strings.Repeat(t.Background.Render(" "), dif))
|
|
||||||
}
|
|
||||||
|
|
||||||
return view.String()
|
return view.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
v0.1.md
4
v0.1.md
@ -20,8 +20,8 @@
|
|||||||
- [ ] Better command-mode autocomplete/hints
|
- [ ] Better command-mode autocomplete/hints
|
||||||
- [ ] Configuration file support (~/.gimrc or similar)
|
- [ ] Configuration file support (~/.gimrc or similar)
|
||||||
- [ ] ~Persistent undo history~
|
- [ ] ~Persistent undo history~
|
||||||
- [ ] Line wrapping display option
|
- [ ] ~Line wrapping display option~
|
||||||
- [ ] Horozontal scroll
|
- [ ] Horizontal scroll
|
||||||
- [ ] Handle large files gracefully (>10k lines) [Pretty sure this is fine]
|
- [ ] Handle large files gracefully (>10k lines) [Pretty sure this is fine]
|
||||||
- [ ] Alternate buffer implementation
|
- [ ] Alternate buffer implementation
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user