Compare commits

...

2 Commits

Author SHA1 Message Date
Hayden Hargreaves
64c448c639 test: updated tests and pulled theme into EditorSettings.
All checks were successful
Run Test Suite / test (push) Successful in 37s
2026-04-08 17:19:32 -07:00
Hayden Hargreaves
2cfa17705b feat: created some agents :) 2026-04-08 17:06:55 -07:00
10 changed files with 655 additions and 98 deletions

View File

@ -0,0 +1,42 @@
---
description: Identifies dead code, unused dependencies, and structural bloat in Go projects
mode: subagent
model: openai/gpt-5.4-mini
temperature: 0.1
permissions:
read: allow
list: allow
glob: allow
grep: allow
lsp: allow
---
You are a ruthless but precise code janitor specializing in Go. Your sole objective is to find and flag dead weight, unnecessary abstractions, and repository bloat. You do not review for business logic flaws; you review for cleanliness and minimalism.
Scan the provided codebase or diff for the following:
- **Dead Code & Unused Types:**
- Flag unexported functions, structs, or methods that are never called.
- Identify unused parameters in function signatures (and suggest using `_` if the signature must be maintained for an interface).
- Find unused constants, variables, or redundant struct tags.
- **Dependency & Package Bloat:**
- Identify imported but unused packages.
- Flag opportunities where standard library functions (e.g., `strings`, `slices`, `maps`) can replace a third-party dependency.
- Suggest when it might be time to run `go mod tidy` if the `go.mod` file contains indirect dependencies that appear orphaned.
- **Comment & Documentation Rot:**
- Flag commented-out code blocks (code should be tracked by Git, not comments).
- Identify stale or redundant comments (e.g., `// GetUser gets the user` or comments that no longer match the function signature).
- Point out outdated `TODO` or `FIXME` comments that have been resolved or ignored for too long.
- **Structural Redundancy:**
- Flag duplicated code blocks that could easily be refactored into a single utility function.
- Identify overly complex `switch` or `if/else` chains that can be simplified.
- Look for empty or redundant `init()` functions that add unnecessary overhead.
**Output Guidelines:**
- Be direct and concise.
- Group your findings into three categories: **Dead Code**, **Dependency Bloat**, and **General Clutter**.
- Provide file names and line numbers for every flagged item.
- Do not make direct changes to the codebase; output your findings as a clear, prioritized checklist for the developer to action.

View File

@ -0,0 +1,46 @@
---
description: Reviews Go code for idiomatic patterns, performance, and concurrency safety
mode: subagent
model: openai/gpt-5.4
temperature: 0.1
permission:
edit: deny
bash:
"*": ask
"git diff": allow
"git log*": allow
"grep *": allow
webfetch: deny
---
You are a strict but constructive Principal Go Engineer performing a code review. Focus exclusively on Go-specific best practices, performance, and idiomatic patterns. Your goal is to catch bugs, race conditions, and memory inefficiencies before they are merged.
Focus your review on the following areas:
- **Idiomatic Go:**
- Ensure the code follows standard formatting (`gofmt`/`goimports`).
- Check for proper interface usage (e.g., accepting interfaces, returning structs).
- Verify that errors are handled gracefully and explicitly, using error wrapping (`fmt.Errorf("... %w", err)`) where context is needed. Avoid silent error swallowing.
- **Memory & Performance:**
- Flag unnecessary heap allocations that could trigger excessive garbage collection. Suggest value semantics where appropriate to keep variables on the stack (escape analysis).
- Look for inefficient string concatenations (suggest `strings.Builder`).
- For frequent allocation of byte slices or buffers, suggest `sync.Pool` to reuse memory.
- **Concurrency & State Management:**
- Identify potential goroutine leaks (e.g., blocking on unbuffered channels with no readers).
- Check for race conditions in shared state access. Suggest `sync.RWMutex` or channel-based synchronization where appropriate.
- Ensure `context.Context` is passed as the first argument in blocking operations and is properly checked for cancellation.
- **Bugs & Edge Cases:**
- Flag unchecked `nil` pointers or potential out-of-bounds slice accesses.
- Ensure deferred functions (like `file.Close()` or `mutex.Unlock()`) are called immediately after successful resource acquisition.
- **Testing:**
- Suggest Table-Driven Tests for complex logic.
- Point out missing coverage for edge cases or unhappy paths.
**Output Guidelines:**
- Provide feedback grouped by severity (Critical, Suggested, Nitpick).
- If you identify an anti-pattern, briefly explain *why* it is unidiomatic and provide a short snippet of the preferred Go approach.
- Do not make direct changes to the codebase; output your findings as clearly formatted review comments.

View File

@ -0,0 +1,38 @@
---
description: Generates and reviews Go tests, specializing in table-driven patterns and teatest TUI validation
mode: subagent
model: openai/gpt-5.3-codex
temperature: 0.1
permission:
edit: allow
bash:
"go *": allow
webfetch: deny
---
You are a rigorous Test Architect specializing in Go. Your primary focus is ensuring code reliability through deterministic, highly covered tests, particularly for Terminal User Interface (TUI) applications.
Apply the following testing philosophies to any code you review or generate:
- **Table-Driven Testing (Standard Go):**
- Enforce the use of table-driven tests for all pure functions, parsers, and logic handlers.
- Ensure test structs always include `name`, `input` (or `args`), `expected`, and `wantErr` fields.
- Verify that `t.Run()` is used to isolate each subtest for clear, organized failure reporting.
- **Interactive Flow Validation (`teatest`):**
- For UI components, utilize `charmbracelet/teatest` to simulate real terminal workflows.
- Validate keystroke handling by sending specific `tea.KeyMsg` sequences (e.g., simulating complex motions or buffer edits).
- Ensure `tm.WaitFinished()` or `teatest.WaitFor()` is used to handle the asynchronous nature of TUI state updates before making assertions.
- **Golden File Testing:**
- For complex `View()` rendering outputs, enforce the use of golden files (`.golden`).
- Suggest boilerplate for an `-update` test flag so developers can easily overwrite expected visual states when the UI intentionally changes.
- **State vs. Side-Effect Isolation:**
- Ensure the core logic is decoupled from `os` or `io` operations using interfaces, so file operations can be mocked in memory.
- Test `Model.Update()` transitions directly by asserting internal state changes (like cursor position, buffer mutations, or mode switches) independent of the visual render.
**Output Guidelines:**
- If reviewing tests, point out missing edge cases, hardcoded assertions that should be table-driven, or flaky asynchronous TUI tests.
- If writing tests, provide complete, idiomatic Go code snippets.
- Keep feedback focused on reliability, determinism, and execution speed.

View File

@ -23,7 +23,6 @@ type MockModel struct {
CommandHistoryList []string CommandHistoryList []string
CommandHistoryCur int CommandHistoryCur int
LastFindVal core.LastFindCommand LastFindVal core.LastFindCommand
CurrentThemeName string
ThemesMap map[string]theme.EditorTheme ThemesMap map[string]theme.EditorTheme
LastChangeKeysList []string LastChangeKeysList []string
} }
@ -47,7 +46,6 @@ func NewMockModel() *MockModel {
SettingsVal: core.NewDefaultSettings(), SettingsVal: core.NewDefaultSettings(),
ModeVal: core.NormalMode, ModeVal: core.NormalMode,
RegistersMap: core.DefaultRegisters(), RegistersMap: core.DefaultRegisters(),
CurrentThemeName: "default",
ThemesMap: map[string]theme.EditorTheme{"default": {}}, ThemesMap: map[string]theme.EditorTheme{"default": {}},
} }
} }
@ -67,7 +65,6 @@ func NewMockModelWithBuffer(buf *core.Buffer) *MockModel {
SettingsVal: core.NewDefaultSettings(), SettingsVal: core.NewDefaultSettings(),
ModeVal: core.NormalMode, ModeVal: core.NormalMode,
RegistersMap: core.DefaultRegisters(), RegistersMap: core.DefaultRegisters(),
CurrentThemeName: "default",
ThemesMap: map[string]theme.EditorTheme{"default": {}}, ThemesMap: map[string]theme.EditorTheme{"default": {}},
} }
} }
@ -81,7 +78,6 @@ func NewMockModelWithWindow(win *core.Window) *MockModel {
SettingsVal: core.NewDefaultSettings(), SettingsVal: core.NewDefaultSettings(),
ModeVal: core.NormalMode, ModeVal: core.NormalMode,
RegistersMap: core.DefaultRegisters(), RegistersMap: core.DefaultRegisters(),
CurrentThemeName: "default",
ThemesMap: map[string]theme.EditorTheme{"default": {}}, ThemesMap: map[string]theme.EditorTheme{"default": {}},
} }
} }
@ -126,17 +122,17 @@ func (m *MockModel) Settings() core.EditorSettings { return m.SettingsVal }
func (m *MockModel) SetSettings(s core.EditorSettings) { m.SettingsVal = s } func (m *MockModel) SetSettings(s core.EditorSettings) { m.SettingsVal = s }
func (m *MockModel) Theme() (string, theme.EditorTheme) { func (m *MockModel) Theme() (string, theme.EditorTheme) {
if m.ThemesMap != nil { if m.ThemesMap != nil {
if t, ok := m.ThemesMap[m.CurrentThemeName]; ok { if t, ok := m.ThemesMap[m.SettingsVal.CurrentTheme]; ok {
return m.CurrentThemeName, t return m.SettingsVal.CurrentTheme, t
} }
if t, ok := m.ThemesMap["default"]; ok { if t, ok := m.ThemesMap["default"]; ok {
return "default", t return "default", t
} }
} }
return m.CurrentThemeName, theme.EditorTheme{} return m.SettingsVal.CurrentTheme, theme.EditorTheme{}
} }
func (m *MockModel) SetTheme(name string) { m.CurrentThemeName = name } func (m *MockModel) SetTheme(name string) { m.SettingsVal.CurrentTheme = name }
func (m *MockModel) Themes() map[string]theme.EditorTheme { func (m *MockModel) Themes() map[string]theme.EditorTheme {
if m.ThemesMap == nil { if m.ThemesMap == nil {
m.ThemesMap = map[string]theme.EditorTheme{} m.ThemesMap = map[string]theme.EditorTheme{}

View File

@ -3,7 +3,7 @@ package core
// EditorSettings: Configuration options for editor display and behavior. // EditorSettings: Configuration options for editor display and behavior.
type EditorSettings struct { type EditorSettings struct {
TabStop int TabStop int
// TODO: Colors CurrentTheme string
} }
// NewDefaultSettings: Creates a Settings struct with sensible defaults for // NewDefaultSettings: Creates a Settings struct with sensible defaults for
@ -11,5 +11,7 @@ type EditorSettings struct {
func NewDefaultSettings() EditorSettings { func NewDefaultSettings() EditorSettings {
return EditorSettings{ return EditorSettings{
TabStop: 2, TabStop: 2,
// TODO: This should be "default" but until we have a startup config, this is fine
CurrentTheme: "kanagawa",
} }
} }

View File

@ -51,7 +51,7 @@ type Model struct {
registers map[rune]core.Register // name -> register registers map[rune]core.Register // name -> register
// Visual styles // Visual styles
currentTheme string // Name of current theme // currentTheme string // Name of current theme
themes map[string]theme.EditorTheme themes map[string]theme.EditorTheme
syntax syntax.Engine syntax syntax.Engine
@ -346,15 +346,15 @@ func (m *Model) SetSettings(s core.EditorSettings) {
// Themes // Themes
// ================================================== // ==================================================
func (m *Model) Theme() (string, theme.EditorTheme) { func (m *Model) Theme() (string, theme.EditorTheme) {
t, ok := m.themes[m.currentTheme] t, ok := m.themes[m.settings.CurrentTheme]
if ok { if ok {
return m.currentTheme, t return m.settings.CurrentTheme, t
} }
return "default", m.themes["default"] return "default", m.themes["default"]
} }
func (m *Model) SetTheme(name string) { func (m *Model) SetTheme(name string) {
m.currentTheme = name m.settings.CurrentTheme = name
if m.syntax == nil { if m.syntax == nil {
return return

View File

@ -44,7 +44,6 @@ func NewModelBuilder() *ModelBuilder {
settings: core.NewDefaultSettings(), settings: core.NewDefaultSettings(),
registers: core.DefaultRegisters(), registers: core.DefaultRegisters(),
syntax: syntax.NewTreeSitterEngine(editorTheme), syntax: syntax.NewTreeSitterEngine(editorTheme),
currentTheme: "default",
themes: embededThemes, themes: embededThemes,
}, },
} }

View File

@ -2,44 +2,109 @@ package syntax
import "testing" import "testing"
func TestLanguageRegistryResolveByFiletype(t *testing.T) { func TestLanguageRegistryResolve(t *testing.T) {
r := newLanguageRegistry() r := newLanguageRegistry()
res, ok, err := r.resolve("go", "") tests := []struct {
if err != nil { name string
t.Fatalf("resolve error: %v", err) args struct {
filetype string
filename string
} }
if !ok || res == nil { expected string
t.Fatalf("expected go to resolve") wantErr bool
}{
{
name: "resolve by filetype",
args: struct {
filetype string
filename string
}{filetype: "go"},
expected: "go",
wantErr: false,
},
{
name: "resolve by extension",
args: struct {
filetype string
filename string
}{filename: "main.js"},
expected: "javascript",
wantErr: false,
},
{
name: "filetype has precedence over extension",
args: struct {
filetype string
filename string
}{filetype: "python", filename: "main.go"},
expected: "python",
wantErr: false,
},
{
name: "normalizes case and whitespace",
args: struct {
filetype string
filename string
}{filetype: " Go "},
expected: "go",
wantErr: false,
},
{
name: "unknown language does not resolve",
args: struct {
filetype string
filename string
}{filetype: "txt", filename: "notes.txt"},
expected: "",
wantErr: false,
},
} }
if res.id != "go" {
t.Fatalf("expected go id, got %q", res.id)
}
}
func TestLanguageRegistryResolveByExtension(t *testing.T) { for _, tc := range tests {
r := newLanguageRegistry() t.Run(tc.name, func(t *testing.T) {
res, ok, err := r.resolve(tc.args.filetype, tc.args.filename)
if (err != nil) != tc.wantErr {
t.Fatalf("resolve error = %v, wantErr=%v", err, tc.wantErr)
}
res, ok, err := r.resolve("", "main.js") if tc.expected == "" {
if err != nil {
t.Fatalf("resolve error: %v", err)
}
if !ok || res == nil {
t.Fatalf("expected javascript to resolve")
}
if res.id != "javascript" {
t.Fatalf("expected javascript id, got %q", res.id)
}
}
func TestLanguageRegistryUnknown(t *testing.T) {
r := newLanguageRegistry()
res, ok, err := r.resolve("txt", "notes.txt")
if err != nil {
t.Fatalf("expected no error for unknown language, got: %v", err)
}
if ok || res != nil { if ok || res != nil {
t.Fatalf("expected unknown language to not resolve") t.Fatalf("expected unresolved language, got ok=%v res=%+v", ok, res)
}
return
}
if !ok || res == nil {
t.Fatalf("expected language %q to resolve", tc.expected)
}
if res.id != tc.expected {
t.Fatalf("resolved id mismatch: got %q want %q", res.id, tc.expected)
}
})
}
}
func TestLanguageRegistryResolveReusesCompiledAssets(t *testing.T) {
r := newLanguageRegistry()
first, ok, err := r.resolve("go", "")
if err != nil {
t.Fatalf("resolve error: %v", err)
}
if !ok || first == nil {
t.Fatalf("expected first resolution to succeed")
}
second, ok, err := r.resolve("golang", "")
if err != nil {
t.Fatalf("resolve error: %v", err)
}
if !ok || second == nil {
t.Fatalf("expected second resolution to succeed")
}
if first != second {
t.Fatalf("expected compiled assets to be reused for same language id")
} }
} }

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"git.gophernest.net/azpect/TextEditor/internal/core" "git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/theme"
"git.gophernest.net/azpect/TextEditor/internal/theme/themes" "git.gophernest.net/azpect/TextEditor/internal/theme/themes"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@ -231,6 +232,141 @@ func TestTreeSitterEngineLastLineEditDoesNotPanicAndRebuilds(t *testing.T) {
} }
} }
func TestTreeSitterEngineThemeChangeRebuildsWithNewCaptureStylesAfterInvalidation(t *testing.T) {
b := core.NewBufferBuilder().
WithFilename("sample.go").
WithFiletype("go").
WithLines([]string{"package main", "func main() {", " return", "}"}).
Build()
buf := &b
themeA := themes.NewDefaultTheme()
themeB := makeThemeWithCaptureOverrides(lipgloss.Color("#ffffff"), lipgloss.Color("#ff00ff"), lipgloss.Color("#00ffaa"))
engine := NewTreeSitterEngine(themeA)
engine.PrepareBuffer(buf, themeA)
line := buf.Line(0)
keywordIdx := strings.Index(line, "package")
if keywordIdx < 0 {
t.Fatalf("test setup failed: expected package keyword")
}
before := engine.LineStyleMap(buf, 0, themeA)
beforeStyle := before[keywordIdx]
if styleEquivalent(beforeStyle, themeA.Line) {
t.Fatalf("expected keyword style to differ from base style before theme switch")
}
engine.InvalidateBuffer(buf)
engine.PrepareBuffer(buf, themeB)
after := engine.LineStyleMap(buf, 0, themeB)
afterStyle := after[keywordIdx]
if styleEquivalent(afterStyle, themeB.Line) {
t.Fatalf("expected keyword style to differ from base style after theme switch")
}
if styleEquivalent(beforeStyle, afterStyle) {
t.Fatalf("expected keyword style to change after theme switch and invalidation")
}
}
func TestTreeSitterEngineThemeChangeRebuildsFallbackLineStylesAfterInvalidation(t *testing.T) {
b := core.NewBufferBuilder().
WithFilename("notes.txt").
WithFiletype("txt").
WithLines([]string{"plain text"}).
Build()
buf := &b
themeA := themes.NewDefaultTheme()
themeB := makeThemeWithCaptureOverrides(lipgloss.Color("#101010"), lipgloss.Color("#ff00ff"), lipgloss.Color("#00ffaa"))
engine := NewTreeSitterEngine(themeA)
mapA := engine.LineStyleMap(buf, 0, themeA)
if len(mapA) == 0 {
t.Fatalf("expected non-empty style map for text line")
}
if !styleEquivalent(mapA[0], themeA.Line) {
t.Fatalf("expected fallback style to use first theme line style")
}
engine.InvalidateBuffer(buf)
mapB := engine.LineStyleMap(buf, 0, themeB)
if !styleEquivalent(mapB[0], themeB.Line) {
t.Fatalf("expected fallback style to use second theme line style after invalidation")
}
if styleEquivalent(mapA[0], mapB[0]) {
t.Fatalf("expected fallback style to change after theme switch and invalidation")
}
}
func TestTreeSitterEngineUnsupportedApplyEditFallsBackToDefaultStyles(t *testing.T) {
b := core.NewBufferBuilder().
WithFilename("notes.txt").
WithFiletype("txt").
WithLines([]string{"just text"}).
Build()
buf := &b
editorTheme := themes.NewDefaultTheme()
engine := NewTreeSitterEngine(editorTheme)
engine.PrepareBuffer(buf, editorTheme)
baseline := engine.LineStyleMap(buf, 0, editorTheme)
if len(baseline) == 0 {
t.Fatalf("expected baseline style map")
}
var edit *core.BufferEdit
buf.OnChange = func(change core.BufferChange) {
edit = change.Edit
}
buf.SetLine(0, "still plain text")
if edit == nil {
t.Fatalf("expected edit metadata from SetLine")
}
engine.ApplyEdit(buf, edit)
engine.PrepareBuffer(buf, editorTheme)
after := engine.LineStyleMap(buf, 0, editorTheme)
if len(after) != len([]rune(buf.Line(0))) {
t.Fatalf("style map length mismatch after unsupported apply edit")
}
for i := range after {
if !styleEquivalent(after[i], editorTheme.Line) {
t.Fatalf("expected fallback line style at rune %d after unsupported apply edit", i)
}
}
}
func makeThemeWithCaptureOverrides(lineFg, keywordFg, stringFg lipgloss.Color) theme.EditorTheme {
t := themes.NewDefaultTheme()
t.Line = t.Line.Foreground(lineFg)
t.Syntax.Group = cloneStyleMap(t.Syntax.Group)
t.Syntax.Exact = cloneStyleMap(t.Syntax.Exact)
t.Syntax.Group["keyword"] = lipgloss.NewStyle().Foreground(keywordFg)
t.Syntax.Group["string"] = lipgloss.NewStyle().Foreground(stringFg)
for key := range t.Syntax.Exact {
if strings.HasPrefix(key, "keyword") {
t.Syntax.Exact[key] = lipgloss.NewStyle().Foreground(keywordFg)
}
if strings.HasPrefix(key, "string") {
t.Syntax.Exact[key] = lipgloss.NewStyle().Foreground(stringFg)
}
}
return t
}
func cloneStyleMap(in map[string]lipgloss.Style) map[string]lipgloss.Style {
out := make(map[string]lipgloss.Style, len(in))
for k, v := range in {
out[k] = v
}
return out
}
func styleEquivalent(a, b lipgloss.Style) bool { func styleEquivalent(a, b lipgloss.Style) bool {
return styleSignature(a) == styleSignature(b) return styleSignature(a) == styleSignature(b)
} }

View File

@ -3,48 +3,281 @@ package syntax
import "testing" import "testing"
func TestMergeRanges(t *testing.T) { func TestMergeRanges(t *testing.T) {
in := []lineRange{{start: 5, end: 8}, {start: 1, end: 2}, {start: 2, end: 4}, {start: 10, end: 10}} tests := []struct {
out := mergeRanges(in) name string
input []lineRange
expected []lineRange
wantErr bool
}{
{
name: "overlapping and unsorted ranges are merged",
input: []lineRange{{start: 5, end: 8}, {start: 1, end: 2}, {start: 2, end: 4}, {start: 10, end: 10}},
expected: []lineRange{{start: 1, end: 8}, {start: 10, end: 10}},
wantErr: false,
},
{
name: "adjacent ranges are merged",
input: []lineRange{{start: 0, end: 1}, {start: 2, end: 3}},
expected: []lineRange{{start: 0, end: 3}},
wantErr: false,
},
{
name: "empty input returns nil",
input: nil,
expected: nil,
wantErr: false,
},
}
if len(out) != 2 { for _, tc := range tests {
t.Fatalf("expected 2 merged ranges, got %d", len(out)) t.Run(tc.name, func(t *testing.T) {
got := mergeRanges(tc.input)
if tc.wantErr {
t.Fatalf("unexpected wantErr=true for mergeRanges")
} }
if out[0].start != 1 || out[0].end != 8 {
t.Fatalf("unexpected first merged range: %+v", out[0]) if len(got) != len(tc.expected) {
t.Fatalf("unexpected merged range count: got %d want %d", len(got), len(tc.expected))
} }
if out[1].start != 10 || out[1].end != 10 { for i := range got {
t.Fatalf("unexpected second merged range: %+v", out[1]) if got[i] != tc.expected[i] {
t.Fatalf("range %d mismatch: got %+v want %+v", i, got[i], tc.expected[i])
}
}
})
} }
} }
func TestNormalizedDirtyRanges(t *testing.T) { func TestNormalizedDirtyRanges(t *testing.T) {
ranges := []lineRange{{start: -2, end: 1}, {start: 3, end: 99}} tests := []struct {
out := normalizedDirtyRanges(ranges, 5) name string
args struct {
ranges []lineRange
lineCount int
}
expected []lineRange
wantErr bool
}{
{
name: "clamps negative and overflowing ranges",
args: struct {
ranges []lineRange
lineCount int
}{
ranges: []lineRange{{start: -2, end: 1}, {start: 3, end: 99}},
lineCount: 5,
},
expected: []lineRange{{start: 0, end: 1}, {start: 3, end: 4}},
wantErr: false,
},
{
name: "drops invalid clamped ranges",
args: struct {
ranges []lineRange
lineCount int
}{
ranges: []lineRange{{start: 8, end: 9}},
lineCount: 5,
},
expected: nil,
wantErr: false,
},
{
name: "merges adjacent ranges after clamping",
args: struct {
ranges []lineRange
lineCount int
}{
ranges: []lineRange{{start: -3, end: 0}, {start: 1, end: 2}},
lineCount: 4,
},
expected: []lineRange{{start: 0, end: 2}},
wantErr: false,
},
}
if len(out) != 2 { for _, tc := range tests {
t.Fatalf("expected 2 normalized ranges, got %d", len(out)) t.Run(tc.name, func(t *testing.T) {
got := normalizedDirtyRanges(tc.args.ranges, tc.args.lineCount)
if tc.wantErr {
t.Fatalf("unexpected wantErr=true for normalizedDirtyRanges")
} }
if out[0].start != 0 || out[0].end != 1 {
t.Fatalf("unexpected first normalized range: %+v", out[0]) if len(got) != len(tc.expected) {
t.Fatalf("unexpected normalized range count: got %d want %d", len(got), len(tc.expected))
} }
if out[1].start != 3 || out[1].end != 4 { for i := range got {
t.Fatalf("unexpected second normalized range: %+v", out[1]) if got[i] != tc.expected[i] {
t.Fatalf("range %d mismatch: got %+v want %+v", i, got[i], tc.expected[i])
}
}
})
} }
} }
func TestByteColToRuneIndexUTF8(t *testing.T) { func TestByteColToRuneIndexUTF8(t *testing.T) {
line := []byte("aéb") tests := []struct {
name string
args struct {
line []byte
col int
}
expected int
wantErr bool
}{
{
name: "zero column maps to first rune",
args: struct {
line []byte
col int
}{line: []byte("aéb"), col: 0},
expected: 0,
wantErr: false,
},
{
name: "middle byte offset on multibyte rune",
args: struct {
line []byte
col int
}{line: []byte("aéb"), col: 1},
expected: 1,
wantErr: false,
},
{
name: "end of multibyte rune maps after rune",
args: struct {
line []byte
col int
}{line: []byte("aéb"), col: 3},
expected: 2,
wantErr: false,
},
{
name: "column at line end maps to rune length",
args: struct {
line []byte
col int
}{line: []byte("aéb"), col: len([]byte("aéb"))},
expected: 3,
wantErr: false,
},
}
if got := byteColToRuneIndex(line, 0); got != 0 { for _, tc := range tests {
t.Fatalf("expected 0, got %d", got) t.Run(tc.name, func(t *testing.T) {
got := byteColToRuneIndex(tc.args.line, tc.args.col)
if tc.wantErr {
t.Fatalf("unexpected wantErr=true for byteColToRuneIndex")
} }
if got := byteColToRuneIndex(line, 1); got != 1 { if got != tc.expected {
t.Fatalf("expected 1, got %d", got) t.Fatalf("unexpected rune index: got %d want %d", got, tc.expected)
} }
if got := byteColToRuneIndex(line, 3); got != 2 { })
t.Fatalf("expected 2, got %d", got) }
} }
if got := byteColToRuneIndex(line, len(line)); got != 3 {
t.Fatalf("expected 3, got %d", got) func TestAddDirtyRangeNormalizesAndMerges(t *testing.T) {
tests := []struct {
name string
args struct {
initial []lineRange
start int
end int
}
expected []lineRange
wantErr bool
}{
{
name: "swaps start and end when reversed",
args: struct {
initial []lineRange
start int
end int
}{initial: nil, start: 7, end: 3},
expected: []lineRange{{start: 3, end: 7}},
wantErr: false,
},
{
name: "clamps negative values",
args: struct {
initial []lineRange
start int
end int
}{initial: nil, start: -5, end: -1},
expected: []lineRange{{start: 0, end: 0}},
wantErr: false,
},
{
name: "merges with existing adjacent range",
args: struct {
initial []lineRange
start int
end int
}{initial: []lineRange{{start: 1, end: 2}}, start: 3, end: 4},
expected: []lineRange{{start: 1, end: 4}},
wantErr: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
bc := &bufferCache{dirty: append([]lineRange{}, tc.args.initial...)}
addDirtyRange(bc, tc.args.start, tc.args.end)
if tc.wantErr {
t.Fatalf("unexpected wantErr=true for addDirtyRange")
}
if len(bc.dirty) != len(tc.expected) {
t.Fatalf("unexpected dirty range count: got %d want %d", len(bc.dirty), len(tc.expected))
}
for i := range bc.dirty {
if bc.dirty[i] != tc.expected[i] {
t.Fatalf("dirty range %d mismatch: got %+v want %+v", i, bc.dirty[i], tc.expected[i])
}
}
})
}
}
func TestRowInRanges(t *testing.T) {
tests := []struct {
name string
input struct {
row int
ranges []lineRange
}
expected bool
wantErr bool
}{
{
name: "row inside range",
input: struct {
row int
ranges []lineRange
}{row: 3, ranges: []lineRange{{start: 1, end: 4}}},
expected: true,
wantErr: false,
},
{
name: "row outside all ranges",
input: struct {
row int
ranges []lineRange
}{row: 8, ranges: []lineRange{{start: 1, end: 4}, {start: 10, end: 12}}},
expected: false,
wantErr: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := rowInRanges(tc.input.row, tc.input.ranges)
if tc.wantErr {
t.Fatalf("unexpected wantErr=true for rowInRanges")
}
if got != tc.expected {
t.Fatalf("unexpected result: got %v want %v", got, tc.expected)
}
})
} }
} }