▍ humdrum codex / glint v1.1.2
license AGPL-3.0
6.8 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package app

import (
	"strings"
	"testing"

	"glint/internal/grammar"
	"glint/internal/theme"
)

// TestApplyGrammarDiagsRendersUnderline checks the app buckets a diagnostic batch
// by line and the editor renders it as a green undercurl. FlexokiDark's Grammar
// green is #879A39 -> the underline-color SGR carries its RGB (135,154,57).
func TestApplyGrammarDiagsRendersUnderline(t *testing.T) {
	a := newApp()
	a.setSize(100, 24)
	a.editor.SetTheme(theme.FlexokiDark())
	a.editor.SetContent([]byte("This is a a test"))
	a.editor.SetGrammar(true)

	a.applyGrammarDiags([]grammar.Diag{{Line: 0, StartCol: 8, EndCol: 11, Message: "repeat"}})

	const greenSGR = "58:2::135:154:57" // undercurl color for #879A39
	if !strings.Contains(a.editor.View(), greenSGR) {
		t.Error("expected a green grammar undercurl (SGR " + greenSGR + ") in the rendered view")
	}
}

// TestGrammarEndToEnd drives the whole pipeline against the real harper: Init
// starts the subprocess and opens the buffer, the listener command blocks for the
// first diagnostic batch, and applying it renders a green undercurl. Skipped
// without harper on PATH or under -short.
func TestGrammarEndToEnd(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping live harper test under -short")
	}
	if !grammar.Available() {
		t.Skip("harper-ls not on PATH")
	}
	a := newApp()
	a.setSize(100, 24)
	a.editor.SetTheme(theme.FlexokiDark())
	a.editor.SetContent([]byte("This is a a test.\n"))

	listen := a.Init() // starts harper, opens the buffer, returns the listener cmd
	defer a.Close()
	if a.grammar == nil {
		t.Fatal("Init did not start a harper client")
	}
	msg := listen() // blocks until harper publishes diagnostics
	diag, ok := msg.(grammarDiagMsg)
	if !ok {
		t.Fatalf("listener returned %T, want grammarDiagMsg", msg)
	}
	a.applyGrammarDiags(diag.diags)

	const greenSGR = "58:2::135:154:57" // #879A39 undercurl
	if !strings.Contains(a.editor.View(), greenSGR) {
		t.Error("no green grammar undercurl after live harper round-trip")
	}
}

// TestGrammarIgnoreClearsUnderline: ignoring a lint drops its underline immediately
// and keeps it dropped when harper re-publishes the same batch.
func TestGrammarIgnoreClearsUnderline(t *testing.T) {
	a := newApp()
	a.setSize(100, 24)
	a.editor.SetTheme(theme.FlexokiDark())
	a.editor.SetContent([]byte("This is a a test"))
	a.editor.SetGrammar(true)

	const greenSGR = "58:2::135:154:57"
	batch := []grammar.Diag{{Line: 0, StartCol: 8, EndCol: 11, Code: "RepeatedWords", Message: "repeat"}}
	a.applyGrammarDiags(batch)
	if !strings.Contains(a.editor.View(), greenSGR) {
		t.Fatal("expected a grammar underline before ignore")
	}

	a.ignoreGrammar("RepeatedWords", "a a")
	if strings.Contains(a.editor.View(), greenSGR) {
		t.Error("underline should vanish immediately after ignore")
	}
	// A fresh identical batch stays suppressed.
	a.applyGrammarDiags(batch)
	if strings.Contains(a.editor.View(), greenSGR) {
		t.Error("ignored lint should stay suppressed across re-batches")
	}
}

// TestApplyGrammarFix applies Harper replacement edits to the buffer.
func TestApplyGrammarFix(t *testing.T) {
	a := newApp()
	a.setSize(100, 24)
	a.editor.SetContent([]byte("This is a a test"))
	a.applyGrammarFix([]grammar.TextEdit{{StartLine: 0, StartCol: 8, EndLine: 0, EndCol: 11, NewText: "a"}})
	if got := string(a.editor.Bytes()); got != "This is a test" {
		t.Errorf("got %q, want %q", got, "This is a test")
	}
}

// TestApplyGrammarFixMultiEdit applies two edits on one line; last-first ordering
// keeps the earlier edit's range valid after the later one shifts the text.
func TestApplyGrammarFixMultiEdit(t *testing.T) {
	a := newApp()
	a.setSize(100, 24)
	a.editor.SetContent([]byte("aa bb"))
	a.applyGrammarFix([]grammar.TextEdit{
		{StartLine: 0, StartCol: 0, EndLine: 0, EndCol: 2, NewText: "AA"},
		{StartLine: 0, StartCol: 3, EndLine: 0, EndCol: 5, NewText: "BBB"},
	})
	if got := string(a.editor.Bytes()); got != "AA BBB" {
		t.Errorf("got %q, want %q", got, "AA BBB")
	}
}

// TestGrammarCodeAt maps a span back to its rule code from the retained batch.
func TestGrammarCodeAt(t *testing.T) {
	a := newApp()
	a.lastGrammarDiags = []grammar.Diag{{Line: 0, StartCol: 8, EndCol: 11, Code: "RepeatedWords"}}
	if got := a.grammarCodeAt(0, 8, 11); got != "RepeatedWords" {
		t.Errorf("grammarCodeAt = %q, want RepeatedWords", got)
	}
	if got := a.grammarCodeAt(0, 0, 3); got != "" {
		t.Errorf("unmatched span code = %q, want empty", got)
	}
}

// TestGrammarPopupApplyFix drives the popup apply path directly (no client needed):
// a grammarFix option, when applied, edits the buffer and returns to editor mode.
func TestGrammarPopupApplyFix(t *testing.T) {
	a := newApp()
	a.setSize(100, 24)
	a.editor.SetContent([]byte("This is a a test"))
	a.mode = ModeSpell
	a.spell = spellPopup{
		word: "a a", code: "RepeatedWords", row: 0, start: 8, end: 11,
		options: []spellOption{
			{label: `Replace with: "a"`, kind: grammarFix, edits: []grammar.TextEdit{{StartLine: 0, StartCol: 8, EndLine: 0, EndCol: 11, NewText: "a"}}},
			{label: "Ignore", kind: grammarIgnore},
		},
	}
	a.applySpell(0)
	if got := string(a.editor.Bytes()); got != "This is a test" {
		t.Errorf("got %q, want %q", got, "This is a test")
	}
	if a.mode != ModeEditor {
		t.Error("popup should close after applying a fix")
	}
}

// TestGrammarPopupLive opens the popup on a real harper-flagged span and applies the
// first fix. Gated on harper + not -short.
func TestGrammarPopupLive(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping live harper test under -short")
	}
	if !grammar.Available() {
		t.Skip("harper-ls not on PATH")
	}
	a := newApp()
	a.setSize(100, 24)
	a.editor.SetContent([]byte("This is a a test.\n"))
	listen := a.Init()
	defer a.Close()
	msg := listen()
	diag, ok := msg.(grammarDiagMsg)
	if !ok {
		t.Fatalf("listener returned %T", msg)
	}
	a.applyGrammarDiags(diag.diags)

	d := diag.diags[0]
	if !a.openGrammarPopupAt(d.Line, d.StartCol) {
		t.Fatal("openGrammarPopupAt did not open on a flagged span")
	}
	if a.mode != ModeSpell {
		t.Fatal("expected ModeSpell after opening the grammar popup")
	}
	fixes := 0
	for _, o := range a.spell.options {
		if o.kind == grammarFix {
			fixes++
		}
	}
	if fixes == 0 {
		t.Fatal("grammar popup listed no replacement fixes from harper")
	}
	before := string(a.editor.Bytes())
	a.applySpell(0) // apply the first fix
	if string(a.editor.Bytes()) == before {
		t.Error("applying a harper fix did not change the buffer")
	}
}

// TestGrammarNilClientNoOps confirms the debounce/flush path is inert without a
// running harper client, so plain app tests never touch a subprocess.
func TestGrammarNilClientNoOps(t *testing.T) {
	a := newApp()
	if a.grammar != nil {
		t.Fatal("newApp should not start a harper client")
	}
	if cmd := a.grammarChanged(); cmd != nil {
		t.Error("grammarChanged should return nil without a client")
	}
	a.grammarFlush(a.grammarGen) // must not panic
	a.grammarOpen()              // must not panic
}