Skip to content

Commit 650f748

Browse files
committed
More test coverage for pkg/etk/comps.
Merge StyleLiner into ListItems, because interface refinement doesn't work with the SoF-based Elvish binding.
1 parent 4e5a3f1 commit 650f748

19 files changed

+277
-112
lines changed

pkg/etk/comps/combobox_test.elvts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//each:combo-box-fixture
2-
//each:eval var just-foo = [&len={ put 1 } &get={|i| put foo } &show={|i| styled foo}]
3-
//each:eval var gen-just-foo = {|q| put $just-foo 0}
2+
//each:extra-bindings
3+
//each:eval fn list-items {|xs| put [&len={ count $xs } &get={|i| put $xs[$i] } &show={|i| styled $xs[$i]; -extra:styling ''}] }
4+
//each:eval var gen-just-foo = {|q| list-items [foo]; put 0}
45

56
/////////////
67
# rendering #
@@ -23,3 +24,49 @@
2324
│foo │
2425
│#̅̂#######################################│
2526
└────────────────────────────────────────┘
27+
28+
////////////
29+
# reacting #
30+
////////////
31+
32+
## query event and regeneration ##
33+
~> setup [&gen-list={|q| list-items [$q]; put 0}]
34+
render
35+
┌────────────────────────────────────────┐
36+
│ │
37+
│ ̅̂ │
38+
└────────────────────────────────────────┘
39+
~> send f
40+
render
41+
┌────────────────────────────────────────┐
42+
│f │
43+
│ ̅̂ │
44+
│f │
45+
│########################################│
46+
└────────────────────────────────────────┘
47+
48+
## list event ##
49+
~> setup [&gen-list={|q| list-items [foo bar baz]; put 0}]
50+
render
51+
┌────────────────────────────────────────┐
52+
│ │
53+
│ ̅̂ │
54+
│foo │
55+
│########################################│
56+
│bar │
57+
│ │
58+
│baz │
59+
│ │
60+
└────────────────────────────────────────┘
61+
~> send [Down]
62+
render
63+
┌────────────────────────────────────────┐
64+
│ │
65+
│ ̅̂ │
66+
│foo │
67+
│ │
68+
│bar │
69+
│########################################│
70+
│baz │
71+
│ │
72+
└────────────────────────────────────────┘

pkg/etk/comps/listbox.go

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,19 @@ type ListItems interface {
1313
Len() int
1414
// Get accesses the underlying item.
1515
Get(i int) any
16-
// Show renders the item at the given zero-based index.
17-
Show(i int) ui.Text
18-
}
19-
20-
// StyleLiner is an optional interface that [ListItems] can implement.
21-
type StyleLiner interface {
22-
// StyleLine returns a "line styling" for item i, which gets applied to
23-
// whole lines occupied by the item, including empty spaces.
24-
StyleLine(i int) ui.Styling
16+
// Show renders the item at the given zero-based index,
17+
// also returning the "area styling" for the item,
18+
// which is applied to the entire area occupied by the item in the listbox.
19+
Show(i int) (ui.Text, ui.Styling)
2520
}
2621

2722
type stringItems []string
2823

2924
// StringItems returns a [ListItems] backed up a slice of strings.
30-
func StringItems(items ...string) ListItems { return stringItems(items) }
31-
func (si stringItems) Len() int { return len(si) }
32-
func (si stringItems) Get(i int) any { return si[i] }
33-
func (si stringItems) Show(i int) ui.Text { return ui.T(si[i]) }
25+
func StringItems(items ...string) ListItems { return stringItems(items) }
26+
func (si stringItems) Len() int { return len(si) }
27+
func (si stringItems) Get(i int) any { return si[i] }
28+
func (si stringItems) Show(i int) (ui.Text, ui.Styling) { return ui.T(si[i]), ui.Nop }
3429

3530
// ListBox shows a list of items and supports choosing one of them.
3631
//
@@ -138,23 +133,22 @@ func (v *listBoxView) renderSingleColumn(width, height int) *term.Buffer {
138133
n := v.items.Len()
139134
var i int
140135
for i = first; i < n && len(lv.Lines) < height; i++ {
141-
text := v.items.Show(i)
142-
lineStyling := ui.Nop
143-
if styleLiner, ok := v.items.(StyleLiner); ok {
144-
lineStyling = styleLiner.StyleLine(i)
145-
}
136+
text, areaStyling := v.items.Show(i)
146137
if i == v.selected {
147138
lv.DotAtLine = len(lv.Lines)
148-
lineStyling = ui.Stylings(lineStyling, ui.Inverse)
139+
areaStyling = ui.Stylings(areaStyling, ui.Inverse)
149140
}
150141

151142
lines := text.SplitByRune('\n')
152143
if i == first {
153144
lines = lines[firstCrop:]
154145
}
155146
for _, line := range lines {
147+
if len(lv.Lines) == height {
148+
break
149+
}
156150
lv.Lines = append(lv.Lines, line)
157-
lv.LineStylings = append(lv.LineStylings, lineStyling)
151+
lv.LineStylings = append(lv.LineStylings, areaStyling)
158152
}
159153
}
160154
if first == 0 && i == n && firstCrop == 0 && len(lv.Lines) < height {
@@ -187,19 +181,15 @@ func (w *listBoxView) renderMultiColumn(width, height int) *term.Buffer {
187181
// Render the column starting from i.
188182
for j := i; j < i+colHeight && j < n; j++ {
189183
last = j
190-
text := items.Show(j)
191-
lineStyling := ui.Nop
192-
if styleLiner, ok := w.items.(StyleLiner); ok {
193-
lineStyling = styleLiner.StyleLine(i)
194-
}
184+
text, areaStyling := items.Show(j)
195185
if j == selected {
196186
col.DotAtLine = len(col.Lines)
197-
lineStyling = ui.Stylings(lineStyling, ui.Inverse)
187+
areaStyling = ui.Stylings(areaStyling, ui.Inverse)
198188
}
199189

200190
// TODO: Complain about multi-line items more loudly.
201191
col.Lines = append(col.Lines, text.SplitByRune('\n')[0])
202-
col.LineStylings = append(col.LineStylings, lineStyling)
192+
col.LineStylings = append(col.LineStylings, areaStyling)
203193
}
204194

205195
colWidth := maxWidth(items, padding, i, i+colHeight)

pkg/etk/comps/listbox_test.elvts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//each:list-box-fixture
2+
//each:extra-bindings
3+
//each:eval fn list-items {|xs| put [&len={ count $xs } &get={|i| put $xs[$i] } &show={|i| styled $xs[$i]; -extra:styling ''}] }
4+
5+
///////////////////////////
6+
# single-column rendering #
7+
///////////////////////////
8+
9+
## empty items ##
10+
~> setup [&items=(list-items [])]
11+
render
12+
┌────────────────────────────────────────┐
13+
│(no item) │
14+
│ ̅̂ │
15+
└────────────────────────────────────────┘
16+
17+
## rendering all items ##
18+
~> setup [&items=(list-items [foo bar baz])]
19+
render &height=10
20+
┌────────────────────────────────────────┐
21+
│foo │
22+
│#̅̂#######################################│
23+
│bar │
24+
│ │
25+
│baz │
26+
│ │
27+
└────────────────────────────────────────┘
28+
29+
## area styling ##
30+
~> fn area-style {|i|
31+
if (== $i 1) {
32+
-extra:styling "inverse"
33+
} else {
34+
-extra:styling ""
35+
}
36+
}
37+
~> setup [&items=(assoc (list-items [foo "bar\nbaz"]) area-style $area-style~)]
38+
render
39+
┌────────────────────────────────────────┐
40+
│foo │
41+
│#̅̂#######################################│
42+
│bar │
43+
│ │
44+
│baz │
45+
│ │
46+
└────────────────────────────────────────┘
47+
48+
## cropping long lines ##
49+
~> setup [&items=(list-items [foo this-is-a-very-long-item baz])]
50+
render &width=10 &height=10
51+
┌──────────┐
52+
│foo │
53+
│#̅̂#########│
54+
│this-is-a-│
55+
│ │
56+
│baz │
57+
│ │
58+
└──────────┘
59+
60+
## rendering a subset of items, with scrollbar, when viewport is short ##
61+
~> setup [&items=(list-items [(range 10 | each $to-string~)])]
62+
render &height=5
63+
┌────────────────────────────────────────┐
64+
│0 │
65+
│#̅̂######################################W│
66+
│1 │
67+
│ W│
68+
│2 │
69+
│ W│
70+
│3 ││
71+
│ M│
72+
│4 ││
73+
│ M│
74+
└────────────────────────────────────────┘
75+
76+
## also showing scrollbar when the last item has lines outside the viewport ##
77+
~> setup [&items=(list-items [foo "bar\nbaz"])]
78+
render &height=2
79+
┌────────────────────────────────────────┐
80+
│foo │
81+
│#̅̂######################################W│
82+
│bar │
83+
│ W│
84+
└────────────────────────────────────────┘
85+
86+
## left and right padding ##
87+
~> setup [&items=(list-items [foo bar rather-long]) &left-padding=1 &right-padding=2]
88+
render &width=10
89+
┌──────────┐
90+
│ foo │
91+
│#̅̂#########│
92+
│ bar │
93+
│ │
94+
│ rather- │
95+
│ │
96+
└──────────┘
97+

pkg/etk/comps/listbox_window.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ func getVerticalWindow(items ListItems, selected int, lastFirst int, height int)
3333
} else if selected >= n {
3434
selected = n - 1
3535
}
36-
selectedHeight := items.Show(selected).CountLines()
36+
countLines := func(i int) int {
37+
t, _ := items.Show(i)
38+
return t.CountLines()
39+
}
40+
selectedHeight := countLines(selected)
3741

3842
if height <= selectedHeight {
3943
// The height is not big enough (or just big enough) to fit the selected
@@ -58,7 +62,7 @@ func getVerticalWindow(items ListItems, selected int, lastFirst int, height int)
5862
// upward later.
5963
useDown := 0
6064
for i := selected + 1; i < n; i++ {
61-
useDown += items.Show(i).CountLines()
65+
useDown += countLines(i)
6266
if useDown >= budget {
6367
break
6468
}
@@ -85,7 +89,7 @@ func getVerticalWindow(items ListItems, selected int, lastFirst int, height int)
8589
// distance, and will be able to use up the entire budget when expanding
8690
// downwards later.
8791
for i := selected - 1; i >= 0; i-- {
88-
useUp += items.Show(i).CountLines()
92+
useUp += countLines(i)
8993
if useUp >= budgetUp {
9094
return i, useUp - budgetUp
9195
}
@@ -140,7 +144,8 @@ func maxWidth(items ListItems, padding, low, high int) int {
140144
width := 0
141145
for i := low; i < high && i < n; i++ {
142146
w := 0
143-
for _, seg := range items.Show(i) {
147+
t, _ := items.Show(i)
148+
for _, seg := range t {
144149
w += wcwidth.Of(seg.Text)
145150
}
146151
if width < w {

pkg/etk/comps/textarea.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func textAreaCore(c etk.Context) (etk.View, etk.React) {
134134
func(code string) (ui.Text, []ui.Text) { return ui.T(code), nil })
135135

136136
buffer := bufferVar.Get()
137-
code, pFrom, pTo := patchPending(buffer, pendingVar.Get())
137+
code, pFrom, pTo := PatchPending(buffer, pendingVar.Get())
138138
styledCode, tips := highlighterVar.Get()(code.Content)
139139
if pFrom < pTo {
140140
// Apply stylingForPending to [pFrom, pTo)
@@ -213,6 +213,8 @@ func isFuncKey(key ui.Key) bool {
213213

214214
// Duplicate with pkg/cli/tk/codearea_render.go
215215

216+
// PatchPending applies the PendingText to the given TextBuffer,
217+
// returning the patched TextBuffer and the range in it from PendingText.
216218
func PatchPending(buf TextBuffer, p PendingText) (TextBuffer, int, int) {
217219
if p.From > p.To || p.From < 0 || p.To > len(buf.Content) {
218220
// Invalid Pending.

pkg/etk/comps/textarea_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package comps
2+
3+
import (
4+
"testing"
5+
6+
"src.elv.sh/pkg/tt"
7+
)
8+
9+
var Args = tt.Args
10+
11+
func TestPatchPending(t *testing.T) {
12+
tt.Test(t, PatchPending,
13+
// Invalid PendingText.
14+
Args(TextBuffer{Content: "x", Dot: 1}, PendingText{From: 2, To: 1, Content: "ls"}).
15+
Rets(TextBuffer{Content: "x", Dot: 1}, 0, 0),
16+
Args(TextBuffer{Content: "x", Dot: 1}, PendingText{From: -1, To: 0, Content: "ls"}).
17+
Rets(TextBuffer{Content: "x", Dot: 1}, 0, 0),
18+
Args(TextBuffer{Content: "x", Dot: 1}, PendingText{From: 0, To: 2, Content: "ls"}).
19+
Rets(TextBuffer{Content: "x", Dot: 1}, 0, 0),
20+
21+
// No-op when Pending is empty.
22+
Args(TextBuffer{Content: "x", Dot: 1}, PendingText{}).
23+
Rets(TextBuffer{Content: "x", Dot: 1}, 0, 0),
24+
Args(TextBuffer{Content: "x", Dot: 1}, PendingText{From: 1, To: 1}).
25+
Rets(TextBuffer{Content: "x", Dot: 1}, 0, 0),
26+
27+
// Pending to the right of the dot - dot doesn't move
28+
Args(TextBuffer{Content: "abc", Dot: 1}, PendingText{From: 2, To: 3, Content: "X"}).
29+
Rets(TextBuffer{Content: "abX", Dot: 1}, 2, 3),
30+
// Pending overlaps with the dot - dot moves to the end of the pending
31+
Args(TextBuffer{Content: "abc", Dot: 1}, PendingText{From: 1, To: 3, Content: "X"}).
32+
Rets(TextBuffer{Content: "aX", Dot: 2}, 1, 2),
33+
// Pending to the left of the dot - dot moves right but maintains relative position
34+
Args(TextBuffer{Content: "abc", Dot: 1}, PendingText{From: 0, To: 0, Content: "X"}).
35+
Rets(TextBuffer{Content: "Xabc", Dot: 2}, 0, 1),
36+
)
37+
}

pkg/etk/comps/textarea_view.go

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,6 @@ type textAreaView struct {
1616

1717
var stylingForPending = ui.Underlined
1818

19-
func patchPending(c TextBuffer, p PendingText) (TextBuffer, int, int) {
20-
if p.From > p.To || p.From < 0 || p.To > len(c.Content) {
21-
// Invalid Pending.
22-
return c, 0, 0
23-
}
24-
if p.From == p.To && p.Content == "" {
25-
return c, 0, 0
26-
}
27-
newContent := c.Content[:p.From] + p.Content + c.Content[p.To:]
28-
newDot := 0
29-
switch {
30-
case c.Dot < p.From:
31-
// Dot is before the replaced region. Keep it.
32-
newDot = c.Dot
33-
case c.Dot >= p.From && c.Dot < p.To:
34-
// Dot is within the replaced region. Place the dot at the end.
35-
newDot = p.From + len(p.Content)
36-
case c.Dot >= p.To:
37-
// Dot is after the replaced region. Maintain the relative position of
38-
// the dot.
39-
newDot = c.Dot - (p.To - p.From) + len(p.Content)
40-
}
41-
return TextBuffer{Content: newContent, Dot: newDot}, p.From, p.From + len(p.Content)
42-
}
43-
4419
func (v *textAreaView) Render(width, height int) *term.Buffer {
4520
bb := term.NewBufferBuilder(width)
4621
bb.EagerWrap = true

0 commit comments

Comments
 (0)