test: updated tests and pulled theme into EditorSettings.
All checks were successful
Run Test Suite / test (push) Successful in 37s
All checks were successful
Run Test Suite / test (push) Successful in 37s
This commit is contained in:
parent
2cfa17705b
commit
64c448c639
@ -23,7 +23,6 @@ type MockModel struct {
|
||||
CommandHistoryList []string
|
||||
CommandHistoryCur int
|
||||
LastFindVal core.LastFindCommand
|
||||
CurrentThemeName string
|
||||
ThemesMap map[string]theme.EditorTheme
|
||||
LastChangeKeysList []string
|
||||
}
|
||||
@ -41,14 +40,13 @@ func NewMockModel() *MockModel {
|
||||
Build()
|
||||
|
||||
return &MockModel{
|
||||
WindowsList: []*core.Window{&win},
|
||||
ActiveWindowVal: &win,
|
||||
BuffersList: []*core.Buffer{&buf},
|
||||
SettingsVal: core.NewDefaultSettings(),
|
||||
ModeVal: core.NormalMode,
|
||||
RegistersMap: core.DefaultRegisters(),
|
||||
CurrentThemeName: "default",
|
||||
ThemesMap: map[string]theme.EditorTheme{"default": {}},
|
||||
WindowsList: []*core.Window{&win},
|
||||
ActiveWindowVal: &win,
|
||||
BuffersList: []*core.Buffer{&buf},
|
||||
SettingsVal: core.NewDefaultSettings(),
|
||||
ModeVal: core.NormalMode,
|
||||
RegistersMap: core.DefaultRegisters(),
|
||||
ThemesMap: map[string]theme.EditorTheme{"default": {}},
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,28 +59,26 @@ func NewMockModelWithBuffer(buf *core.Buffer) *MockModel {
|
||||
Build()
|
||||
|
||||
return &MockModel{
|
||||
WindowsList: []*core.Window{&win},
|
||||
ActiveWindowVal: &win,
|
||||
BuffersList: []*core.Buffer{buf},
|
||||
SettingsVal: core.NewDefaultSettings(),
|
||||
ModeVal: core.NormalMode,
|
||||
RegistersMap: core.DefaultRegisters(),
|
||||
CurrentThemeName: "default",
|
||||
ThemesMap: map[string]theme.EditorTheme{"default": {}},
|
||||
WindowsList: []*core.Window{&win},
|
||||
ActiveWindowVal: &win,
|
||||
BuffersList: []*core.Buffer{buf},
|
||||
SettingsVal: core.NewDefaultSettings(),
|
||||
ModeVal: core.NormalMode,
|
||||
RegistersMap: core.DefaultRegisters(),
|
||||
ThemesMap: map[string]theme.EditorTheme{"default": {}},
|
||||
}
|
||||
}
|
||||
|
||||
// NewMockModelWithWindow creates a mock with a custom window.
|
||||
func NewMockModelWithWindow(win *core.Window) *MockModel {
|
||||
return &MockModel{
|
||||
WindowsList: []*core.Window{win},
|
||||
ActiveWindowVal: win,
|
||||
BuffersList: []*core.Buffer{win.Buffer},
|
||||
SettingsVal: core.NewDefaultSettings(),
|
||||
ModeVal: core.NormalMode,
|
||||
RegistersMap: core.DefaultRegisters(),
|
||||
CurrentThemeName: "default",
|
||||
ThemesMap: map[string]theme.EditorTheme{"default": {}},
|
||||
WindowsList: []*core.Window{win},
|
||||
ActiveWindowVal: win,
|
||||
BuffersList: []*core.Buffer{win.Buffer},
|
||||
SettingsVal: core.NewDefaultSettings(),
|
||||
ModeVal: core.NormalMode,
|
||||
RegistersMap: core.DefaultRegisters(),
|
||||
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) Theme() (string, theme.EditorTheme) {
|
||||
if m.ThemesMap != nil {
|
||||
if t, ok := m.ThemesMap[m.CurrentThemeName]; ok {
|
||||
return m.CurrentThemeName, t
|
||||
if t, ok := m.ThemesMap[m.SettingsVal.CurrentTheme]; ok {
|
||||
return m.SettingsVal.CurrentTheme, t
|
||||
}
|
||||
if t, ok := m.ThemesMap["default"]; ok {
|
||||
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 {
|
||||
if m.ThemesMap == nil {
|
||||
m.ThemesMap = map[string]theme.EditorTheme{}
|
||||
|
||||
@ -2,8 +2,8 @@ package core
|
||||
|
||||
// EditorSettings: Configuration options for editor display and behavior.
|
||||
type EditorSettings struct {
|
||||
TabStop int
|
||||
// TODO: Colors
|
||||
TabStop int
|
||||
CurrentTheme string
|
||||
}
|
||||
|
||||
// NewDefaultSettings: Creates a Settings struct with sensible defaults for
|
||||
@ -11,5 +11,7 @@ type EditorSettings struct {
|
||||
func NewDefaultSettings() EditorSettings {
|
||||
return EditorSettings{
|
||||
TabStop: 2,
|
||||
// TODO: This should be "default" but until we have a startup config, this is fine
|
||||
CurrentTheme: "kanagawa",
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,9 +51,9 @@ type Model struct {
|
||||
registers map[rune]core.Register // name -> register
|
||||
|
||||
// Visual styles
|
||||
currentTheme string // Name of current theme
|
||||
themes map[string]theme.EditorTheme
|
||||
syntax syntax.Engine
|
||||
// currentTheme string // Name of current theme
|
||||
themes map[string]theme.EditorTheme
|
||||
syntax syntax.Engine
|
||||
|
||||
// Dot operator state
|
||||
lastChangeKeys []string
|
||||
@ -346,15 +346,15 @@ func (m *Model) SetSettings(s core.EditorSettings) {
|
||||
// Themes
|
||||
// ==================================================
|
||||
func (m *Model) Theme() (string, theme.EditorTheme) {
|
||||
t, ok := m.themes[m.currentTheme]
|
||||
t, ok := m.themes[m.settings.CurrentTheme]
|
||||
if ok {
|
||||
return m.currentTheme, t
|
||||
return m.settings.CurrentTheme, t
|
||||
}
|
||||
return "default", m.themes["default"]
|
||||
}
|
||||
|
||||
func (m *Model) SetTheme(name string) {
|
||||
m.currentTheme = name
|
||||
m.settings.CurrentTheme = name
|
||||
|
||||
if m.syntax == nil {
|
||||
return
|
||||
|
||||
@ -44,7 +44,6 @@ func NewModelBuilder() *ModelBuilder {
|
||||
settings: core.NewDefaultSettings(),
|
||||
registers: core.DefaultRegisters(),
|
||||
syntax: syntax.NewTreeSitterEngine(editorTheme),
|
||||
currentTheme: "default",
|
||||
themes: embededThemes,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2,44 +2,109 @@ package syntax
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLanguageRegistryResolveByFiletype(t *testing.T) {
|
||||
func TestLanguageRegistryResolve(t *testing.T) {
|
||||
r := newLanguageRegistry()
|
||||
|
||||
res, ok, err := r.resolve("go", "")
|
||||
tests := []struct {
|
||||
name string
|
||||
args struct {
|
||||
filetype string
|
||||
filename string
|
||||
}
|
||||
expected string
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
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)
|
||||
}
|
||||
|
||||
if tc.expected == "" {
|
||||
if ok || res != nil {
|
||||
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 || res == nil {
|
||||
t.Fatalf("expected go to resolve")
|
||||
if !ok || first == nil {
|
||||
t.Fatalf("expected first resolution to succeed")
|
||||
}
|
||||
if res.id != "go" {
|
||||
t.Fatalf("expected go id, got %q", res.id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLanguageRegistryResolveByExtension(t *testing.T) {
|
||||
r := newLanguageRegistry()
|
||||
|
||||
res, ok, err := r.resolve("", "main.js")
|
||||
second, ok, err := r.resolve("golang", "")
|
||||
if err != nil {
|
||||
t.Fatalf("resolve error: %v", err)
|
||||
}
|
||||
if !ok || res == nil {
|
||||
t.Fatalf("expected javascript to resolve")
|
||||
if !ok || second == nil {
|
||||
t.Fatalf("expected second resolution to succeed")
|
||||
}
|
||||
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 {
|
||||
t.Fatalf("expected unknown language to not resolve")
|
||||
|
||||
if first != second {
|
||||
t.Fatalf("expected compiled assets to be reused for same language id")
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/theme/themes"
|
||||
"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 {
|
||||
return styleSignature(a) == styleSignature(b)
|
||||
}
|
||||
|
||||
@ -3,48 +3,281 @@ package syntax
|
||||
import "testing"
|
||||
|
||||
func TestMergeRanges(t *testing.T) {
|
||||
in := []lineRange{{start: 5, end: 8}, {start: 1, end: 2}, {start: 2, end: 4}, {start: 10, end: 10}}
|
||||
out := mergeRanges(in)
|
||||
tests := []struct {
|
||||
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 {
|
||||
t.Fatalf("expected 2 merged ranges, got %d", len(out))
|
||||
}
|
||||
if out[0].start != 1 || out[0].end != 8 {
|
||||
t.Fatalf("unexpected first merged range: %+v", out[0])
|
||||
}
|
||||
if out[1].start != 10 || out[1].end != 10 {
|
||||
t.Fatalf("unexpected second merged range: %+v", out[1])
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := mergeRanges(tc.input)
|
||||
|
||||
if tc.wantErr {
|
||||
t.Fatalf("unexpected wantErr=true for mergeRanges")
|
||||
}
|
||||
|
||||
if len(got) != len(tc.expected) {
|
||||
t.Fatalf("unexpected merged range count: got %d want %d", len(got), len(tc.expected))
|
||||
}
|
||||
for i := range got {
|
||||
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) {
|
||||
ranges := []lineRange{{start: -2, end: 1}, {start: 3, end: 99}}
|
||||
out := normalizedDirtyRanges(ranges, 5)
|
||||
tests := []struct {
|
||||
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 {
|
||||
t.Fatalf("expected 2 normalized ranges, got %d", len(out))
|
||||
}
|
||||
if out[0].start != 0 || out[0].end != 1 {
|
||||
t.Fatalf("unexpected first normalized range: %+v", out[0])
|
||||
}
|
||||
if out[1].start != 3 || out[1].end != 4 {
|
||||
t.Fatalf("unexpected second normalized range: %+v", out[1])
|
||||
for _, tc := range tests {
|
||||
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 len(got) != len(tc.expected) {
|
||||
t.Fatalf("unexpected normalized range count: got %d want %d", len(got), len(tc.expected))
|
||||
}
|
||||
for i := range got {
|
||||
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) {
|
||||
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 {
|
||||
t.Fatalf("expected 0, got %d", got)
|
||||
}
|
||||
if got := byteColToRuneIndex(line, 1); got != 1 {
|
||||
t.Fatalf("expected 1, got %d", got)
|
||||
}
|
||||
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)
|
||||
for _, tc := range tests {
|
||||
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 != tc.expected {
|
||||
t.Fatalf("unexpected rune index: got %d want %d", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user