list_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. package list
  2. import (
  3. "fmt"
  4. "strings"
  5. "testing"
  6. tea "github.com/charmbracelet/bubbletea/v2"
  7. "github.com/charmbracelet/crush/internal/tui/components/core/layout"
  8. "github.com/charmbracelet/crush/internal/tui/util"
  9. "github.com/charmbracelet/lipgloss/v2"
  10. "github.com/charmbracelet/x/exp/golden"
  11. "github.com/google/uuid"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. )
  15. func TestList(t *testing.T) {
  16. t.Parallel()
  17. t.Run("should have correct positions in list that fits the items", func(t *testing.T) {
  18. t.Parallel()
  19. items := []Item{}
  20. for i := range 5 {
  21. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  22. items = append(items, item)
  23. }
  24. l := New(items, WithDirectionForward(), WithSize(10, 20)).(*list[Item])
  25. execCmd(l, l.Init())
  26. // should select the last item
  27. assert.Equal(t, 0, l.selectedItemIdx)
  28. assert.Equal(t, 0, l.offset)
  29. require.Equal(t, 5, len(l.indexMap))
  30. require.Equal(t, 5, len(l.items))
  31. require.Equal(t, 5, len(l.renderedItems))
  32. assert.Equal(t, 5, lipgloss.Height(l.rendered))
  33. assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
  34. start, end := l.viewPosition()
  35. assert.Equal(t, 0, start)
  36. assert.Equal(t, 4, end)
  37. for i := range 5 {
  38. item, ok := l.renderedItems[items[i].ID()]
  39. require.True(t, ok)
  40. assert.Equal(t, i, item.start)
  41. assert.Equal(t, i, item.end)
  42. }
  43. golden.RequireEqual(t, []byte(l.View()))
  44. })
  45. t.Run("should have correct positions in list that fits the items backwards", func(t *testing.T) {
  46. t.Parallel()
  47. items := []Item{}
  48. for i := range 5 {
  49. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  50. items = append(items, item)
  51. }
  52. l := New(items, WithDirectionBackward(), WithSize(10, 20)).(*list[Item])
  53. execCmd(l, l.Init())
  54. // should select the last item
  55. assert.Equal(t, 4, l.selectedItemIdx)
  56. assert.Equal(t, 0, l.offset)
  57. require.Equal(t, 5, len(l.indexMap))
  58. require.Equal(t, 5, len(l.items))
  59. require.Equal(t, 5, len(l.renderedItems))
  60. assert.Equal(t, 5, lipgloss.Height(l.rendered))
  61. assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
  62. start, end := l.viewPosition()
  63. assert.Equal(t, 0, start)
  64. assert.Equal(t, 4, end)
  65. for i := range 5 {
  66. item, ok := l.renderedItems[items[i].ID()]
  67. require.True(t, ok)
  68. assert.Equal(t, i, item.start)
  69. assert.Equal(t, i, item.end)
  70. }
  71. golden.RequireEqual(t, []byte(l.View()))
  72. })
  73. t.Run("should have correct positions in list that does not fits the items", func(t *testing.T) {
  74. t.Parallel()
  75. items := []Item{}
  76. for i := range 30 {
  77. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  78. items = append(items, item)
  79. }
  80. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  81. execCmd(l, l.Init())
  82. // should select the last item
  83. assert.Equal(t, 0, l.selectedItemIdx)
  84. assert.Equal(t, 0, l.offset)
  85. require.Equal(t, 30, len(l.indexMap))
  86. require.Equal(t, 30, len(l.items))
  87. require.Equal(t, 30, len(l.renderedItems))
  88. assert.Equal(t, 30, lipgloss.Height(l.rendered))
  89. assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
  90. start, end := l.viewPosition()
  91. assert.Equal(t, 0, start)
  92. assert.Equal(t, 9, end)
  93. for i := range 30 {
  94. item, ok := l.renderedItems[items[i].ID()]
  95. require.True(t, ok)
  96. assert.Equal(t, i, item.start)
  97. assert.Equal(t, i, item.end)
  98. }
  99. golden.RequireEqual(t, []byte(l.View()))
  100. })
  101. t.Run("should have correct positions in list that does not fits the items backwards", func(t *testing.T) {
  102. t.Parallel()
  103. items := []Item{}
  104. for i := range 30 {
  105. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  106. items = append(items, item)
  107. }
  108. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  109. execCmd(l, l.Init())
  110. // should select the last item
  111. assert.Equal(t, 29, l.selectedItemIdx)
  112. assert.Equal(t, 0, l.offset)
  113. require.Equal(t, 30, len(l.indexMap))
  114. require.Equal(t, 30, len(l.items))
  115. require.Equal(t, 30, len(l.renderedItems))
  116. assert.Equal(t, 30, lipgloss.Height(l.rendered))
  117. assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
  118. start, end := l.viewPosition()
  119. assert.Equal(t, 20, start)
  120. assert.Equal(t, 29, end)
  121. for i := range 30 {
  122. item, ok := l.renderedItems[items[i].ID()]
  123. require.True(t, ok)
  124. assert.Equal(t, i, item.start)
  125. assert.Equal(t, i, item.end)
  126. }
  127. golden.RequireEqual(t, []byte(l.View()))
  128. })
  129. t.Run("should have correct positions in list that does not fits the items and has multi line items", func(t *testing.T) {
  130. t.Parallel()
  131. items := []Item{}
  132. for i := range 30 {
  133. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  134. content = strings.TrimSuffix(content, "\n")
  135. item := NewSelectableItem(content)
  136. items = append(items, item)
  137. }
  138. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  139. execCmd(l, l.Init())
  140. // should select the last item
  141. assert.Equal(t, 0, l.selectedItemIdx)
  142. assert.Equal(t, 0, l.offset)
  143. require.Equal(t, 30, len(l.indexMap))
  144. require.Equal(t, 30, len(l.items))
  145. require.Equal(t, 30, len(l.renderedItems))
  146. expectedLines := 0
  147. for i := range 30 {
  148. expectedLines += (i + 1) * 1
  149. }
  150. assert.Equal(t, expectedLines, lipgloss.Height(l.rendered))
  151. assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
  152. start, end := l.viewPosition()
  153. assert.Equal(t, 0, start)
  154. assert.Equal(t, 9, end)
  155. currentPosition := 0
  156. for i := range 30 {
  157. rItem, ok := l.renderedItems[items[i].ID()]
  158. require.True(t, ok)
  159. assert.Equal(t, currentPosition, rItem.start)
  160. assert.Equal(t, currentPosition+i, rItem.end)
  161. currentPosition += i + 1
  162. }
  163. golden.RequireEqual(t, []byte(l.View()))
  164. })
  165. t.Run("should have correct positions in list that does not fits the items and has multi line items backwards", func(t *testing.T) {
  166. t.Parallel()
  167. items := []Item{}
  168. for i := range 30 {
  169. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  170. content = strings.TrimSuffix(content, "\n")
  171. item := NewSelectableItem(content)
  172. items = append(items, item)
  173. }
  174. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  175. execCmd(l, l.Init())
  176. // should select the last item
  177. assert.Equal(t, 29, l.selectedItemIdx)
  178. assert.Equal(t, 0, l.offset)
  179. require.Equal(t, 30, len(l.indexMap))
  180. require.Equal(t, 30, len(l.items))
  181. require.Equal(t, 30, len(l.renderedItems))
  182. expectedLines := 0
  183. for i := range 30 {
  184. expectedLines += (i + 1) * 1
  185. }
  186. assert.Equal(t, expectedLines, lipgloss.Height(l.rendered))
  187. assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
  188. start, end := l.viewPosition()
  189. assert.Equal(t, expectedLines-10, start)
  190. assert.Equal(t, expectedLines-1, end)
  191. currentPosition := 0
  192. for i := range 30 {
  193. rItem, ok := l.renderedItems[items[i].ID()]
  194. require.True(t, ok)
  195. assert.Equal(t, currentPosition, rItem.start)
  196. assert.Equal(t, currentPosition+i, rItem.end)
  197. currentPosition += i + 1
  198. }
  199. golden.RequireEqual(t, []byte(l.View()))
  200. })
  201. t.Run("should go to selected item at the beginning", func(t *testing.T) {
  202. t.Parallel()
  203. items := []Item{}
  204. for i := range 30 {
  205. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  206. content = strings.TrimSuffix(content, "\n")
  207. item := NewSelectableItem(content)
  208. items = append(items, item)
  209. }
  210. l := New(items, WithDirectionForward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item])
  211. execCmd(l, l.Init())
  212. // should select the last item
  213. assert.Equal(t, 10, l.selectedItemIdx)
  214. golden.RequireEqual(t, []byte(l.View()))
  215. })
  216. t.Run("should go to selected item at the beginning backwards", func(t *testing.T) {
  217. t.Parallel()
  218. items := []Item{}
  219. for i := range 30 {
  220. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  221. content = strings.TrimSuffix(content, "\n")
  222. item := NewSelectableItem(content)
  223. items = append(items, item)
  224. }
  225. l := New(items, WithDirectionBackward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item])
  226. execCmd(l, l.Init())
  227. // should select the last item
  228. assert.Equal(t, 10, l.selectedItemIdx)
  229. golden.RequireEqual(t, []byte(l.View()))
  230. })
  231. }
  232. func TestListMovement(t *testing.T) {
  233. t.Parallel()
  234. t.Run("should move viewport up", func(t *testing.T) {
  235. t.Parallel()
  236. items := []Item{}
  237. for i := range 30 {
  238. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  239. content = strings.TrimSuffix(content, "\n")
  240. item := NewSelectableItem(content)
  241. items = append(items, item)
  242. }
  243. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  244. execCmd(l, l.Init())
  245. execCmd(l, l.MoveUp(25))
  246. assert.Equal(t, 25, l.offset)
  247. golden.RequireEqual(t, []byte(l.View()))
  248. })
  249. t.Run("should move viewport up and down", func(t *testing.T) {
  250. t.Parallel()
  251. items := []Item{}
  252. for i := range 30 {
  253. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  254. content = strings.TrimSuffix(content, "\n")
  255. item := NewSelectableItem(content)
  256. items = append(items, item)
  257. }
  258. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  259. execCmd(l, l.Init())
  260. execCmd(l, l.MoveUp(25))
  261. execCmd(l, l.MoveDown(25))
  262. assert.Equal(t, 0, l.offset)
  263. golden.RequireEqual(t, []byte(l.View()))
  264. })
  265. t.Run("should move viewport down", func(t *testing.T) {
  266. t.Parallel()
  267. items := []Item{}
  268. for i := range 30 {
  269. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  270. content = strings.TrimSuffix(content, "\n")
  271. item := NewSelectableItem(content)
  272. items = append(items, item)
  273. }
  274. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  275. execCmd(l, l.Init())
  276. execCmd(l, l.MoveDown(25))
  277. assert.Equal(t, 25, l.offset)
  278. golden.RequireEqual(t, []byte(l.View()))
  279. })
  280. t.Run("should move viewport down and up", func(t *testing.T) {
  281. t.Parallel()
  282. items := []Item{}
  283. for i := range 30 {
  284. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  285. content = strings.TrimSuffix(content, "\n")
  286. item := NewSelectableItem(content)
  287. items = append(items, item)
  288. }
  289. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  290. execCmd(l, l.Init())
  291. execCmd(l, l.MoveDown(25))
  292. execCmd(l, l.MoveUp(25))
  293. assert.Equal(t, 0, l.offset)
  294. golden.RequireEqual(t, []byte(l.View()))
  295. })
  296. t.Run("should not change offset when new items are appended and we are at the bottom in backwards list", func(t *testing.T) {
  297. t.Parallel()
  298. items := []Item{}
  299. for i := range 30 {
  300. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  301. content = strings.TrimSuffix(content, "\n")
  302. item := NewSelectableItem(content)
  303. items = append(items, item)
  304. }
  305. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  306. execCmd(l, l.Init())
  307. execCmd(l, l.AppendItem(NewSelectableItem("Testing")))
  308. assert.Equal(t, 0, l.offset)
  309. golden.RequireEqual(t, []byte(l.View()))
  310. })
  311. t.Run("should stay at the position it is when new items are added but we moved up in backwards list", func(t *testing.T) {
  312. t.Parallel()
  313. items := []Item{}
  314. for i := range 30 {
  315. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  316. items = append(items, item)
  317. }
  318. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  319. execCmd(l, l.Init())
  320. execCmd(l, l.MoveUp(2))
  321. viewBefore := l.View()
  322. execCmd(l, l.AppendItem(NewSelectableItem("Testing\nHello\n")))
  323. viewAfter := l.View()
  324. assert.Equal(t, viewBefore, viewAfter)
  325. assert.Equal(t, 5, l.offset)
  326. assert.Equal(t, 33, lipgloss.Height(l.rendered))
  327. golden.RequireEqual(t, []byte(l.View()))
  328. })
  329. t.Run("should stay at the position it is when the hight of an item below is increased in backwards list", func(t *testing.T) {
  330. t.Parallel()
  331. items := []Item{}
  332. for i := range 30 {
  333. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  334. items = append(items, item)
  335. }
  336. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  337. execCmd(l, l.Init())
  338. execCmd(l, l.MoveUp(2))
  339. viewBefore := l.View()
  340. item := items[29]
  341. execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
  342. viewAfter := l.View()
  343. assert.Equal(t, viewBefore, viewAfter)
  344. assert.Equal(t, 4, l.offset)
  345. assert.Equal(t, 32, lipgloss.Height(l.rendered))
  346. golden.RequireEqual(t, []byte(l.View()))
  347. })
  348. t.Run("should stay at the position it is when the hight of an item below is decreases in backwards list", func(t *testing.T) {
  349. t.Parallel()
  350. items := []Item{}
  351. for i := range 30 {
  352. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  353. items = append(items, item)
  354. }
  355. items = append(items, NewSelectableItem("Item 30\nLine 2\nLine 3"))
  356. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  357. execCmd(l, l.Init())
  358. execCmd(l, l.MoveUp(2))
  359. viewBefore := l.View()
  360. item := items[30]
  361. execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 30")))
  362. viewAfter := l.View()
  363. assert.Equal(t, viewBefore, viewAfter)
  364. assert.Equal(t, 0, l.offset)
  365. assert.Equal(t, 31, lipgloss.Height(l.rendered))
  366. golden.RequireEqual(t, []byte(l.View()))
  367. })
  368. t.Run("should stay at the position it is when the hight of an item above is increased in backwards list", func(t *testing.T) {
  369. t.Parallel()
  370. items := []Item{}
  371. for i := range 30 {
  372. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  373. items = append(items, item)
  374. }
  375. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  376. execCmd(l, l.Init())
  377. execCmd(l, l.MoveUp(2))
  378. viewBefore := l.View()
  379. item := items[1]
  380. execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 1\nLine 2\nLine 3")))
  381. viewAfter := l.View()
  382. assert.Equal(t, viewBefore, viewAfter)
  383. assert.Equal(t, 2, l.offset)
  384. assert.Equal(t, 32, lipgloss.Height(l.rendered))
  385. golden.RequireEqual(t, []byte(l.View()))
  386. })
  387. t.Run("should stay at the position it is if an item is prepended and we are in backwards list", func(t *testing.T) {
  388. t.Parallel()
  389. items := []Item{}
  390. for i := range 30 {
  391. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  392. items = append(items, item)
  393. }
  394. l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
  395. execCmd(l, l.Init())
  396. execCmd(l, l.MoveUp(2))
  397. viewBefore := l.View()
  398. execCmd(l, l.PrependItem(NewSelectableItem("New")))
  399. viewAfter := l.View()
  400. assert.Equal(t, viewBefore, viewAfter)
  401. assert.Equal(t, 2, l.offset)
  402. assert.Equal(t, 31, lipgloss.Height(l.rendered))
  403. golden.RequireEqual(t, []byte(l.View()))
  404. })
  405. t.Run("should not change offset when new items are prepended and we are at the top in forward list", func(t *testing.T) {
  406. t.Parallel()
  407. items := []Item{}
  408. for i := range 30 {
  409. content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
  410. content = strings.TrimSuffix(content, "\n")
  411. item := NewSelectableItem(content)
  412. items = append(items, item)
  413. }
  414. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  415. execCmd(l, l.Init())
  416. execCmd(l, l.PrependItem(NewSelectableItem("Testing")))
  417. assert.Equal(t, 0, l.offset)
  418. golden.RequireEqual(t, []byte(l.View()))
  419. })
  420. t.Run("should stay at the position it is when new items are added but we moved down in forward list", func(t *testing.T) {
  421. t.Parallel()
  422. items := []Item{}
  423. for i := range 30 {
  424. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  425. items = append(items, item)
  426. }
  427. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  428. execCmd(l, l.Init())
  429. execCmd(l, l.MoveDown(2))
  430. viewBefore := l.View()
  431. execCmd(l, l.PrependItem(NewSelectableItem("Testing\nHello\n")))
  432. viewAfter := l.View()
  433. assert.Equal(t, viewBefore, viewAfter)
  434. assert.Equal(t, 5, l.offset)
  435. assert.Equal(t, 33, lipgloss.Height(l.rendered))
  436. golden.RequireEqual(t, []byte(l.View()))
  437. })
  438. t.Run("should stay at the position it is when the hight of an item above is increased in forward list", func(t *testing.T) {
  439. t.Parallel()
  440. items := []Item{}
  441. for i := range 30 {
  442. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  443. items = append(items, item)
  444. }
  445. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  446. execCmd(l, l.Init())
  447. execCmd(l, l.MoveDown(2))
  448. viewBefore := l.View()
  449. item := items[0]
  450. execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
  451. viewAfter := l.View()
  452. assert.Equal(t, viewBefore, viewAfter)
  453. assert.Equal(t, 4, l.offset)
  454. assert.Equal(t, 32, lipgloss.Height(l.rendered))
  455. golden.RequireEqual(t, []byte(l.View()))
  456. })
  457. t.Run("should stay at the position it is when the hight of an item above is decreases in forward list", func(t *testing.T) {
  458. t.Parallel()
  459. items := []Item{}
  460. items = append(items, NewSelectableItem("At top\nLine 2\nLine 3"))
  461. for i := range 30 {
  462. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  463. items = append(items, item)
  464. }
  465. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  466. execCmd(l, l.Init())
  467. execCmd(l, l.MoveDown(3))
  468. viewBefore := l.View()
  469. item := items[0]
  470. execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("At top")))
  471. viewAfter := l.View()
  472. assert.Equal(t, viewBefore, viewAfter)
  473. assert.Equal(t, 1, l.offset)
  474. assert.Equal(t, 31, lipgloss.Height(l.rendered))
  475. golden.RequireEqual(t, []byte(l.View()))
  476. })
  477. t.Run("should stay at the position it is when the hight of an item below is increased in forward list", func(t *testing.T) {
  478. t.Parallel()
  479. items := []Item{}
  480. for i := range 30 {
  481. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  482. items = append(items, item)
  483. }
  484. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  485. execCmd(l, l.Init())
  486. execCmd(l, l.MoveDown(2))
  487. viewBefore := l.View()
  488. item := items[29]
  489. execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
  490. viewAfter := l.View()
  491. assert.Equal(t, viewBefore, viewAfter)
  492. assert.Equal(t, 2, l.offset)
  493. assert.Equal(t, 32, lipgloss.Height(l.rendered))
  494. golden.RequireEqual(t, []byte(l.View()))
  495. })
  496. t.Run("should stay at the position it is if an item is appended and we are in forward list", func(t *testing.T) {
  497. t.Parallel()
  498. items := []Item{}
  499. for i := range 30 {
  500. item := NewSelectableItem(fmt.Sprintf("Item %d", i))
  501. items = append(items, item)
  502. }
  503. l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
  504. execCmd(l, l.Init())
  505. execCmd(l, l.MoveDown(2))
  506. viewBefore := l.View()
  507. execCmd(l, l.AppendItem(NewSelectableItem("New")))
  508. viewAfter := l.View()
  509. assert.Equal(t, viewBefore, viewAfter)
  510. assert.Equal(t, 2, l.offset)
  511. assert.Equal(t, 31, lipgloss.Height(l.rendered))
  512. golden.RequireEqual(t, []byte(l.View()))
  513. })
  514. }
  515. type SelectableItem interface {
  516. Item
  517. layout.Focusable
  518. }
  519. type simpleItem struct {
  520. width int
  521. content string
  522. id string
  523. }
  524. type selectableItem struct {
  525. *simpleItem
  526. focused bool
  527. }
  528. func NewSimpleItem(content string) *simpleItem {
  529. return &simpleItem{
  530. id: uuid.NewString(),
  531. width: 0,
  532. content: content,
  533. }
  534. }
  535. func NewSelectableItem(content string) SelectableItem {
  536. return &selectableItem{
  537. simpleItem: NewSimpleItem(content),
  538. focused: false,
  539. }
  540. }
  541. func (s *simpleItem) ID() string {
  542. return s.id
  543. }
  544. func (s *simpleItem) Init() tea.Cmd {
  545. return nil
  546. }
  547. func (s *simpleItem) Update(msg tea.Msg) (util.Model, tea.Cmd) {
  548. return s, nil
  549. }
  550. func (s *simpleItem) View() string {
  551. return lipgloss.NewStyle().Width(s.width).Render(s.content)
  552. }
  553. func (l *simpleItem) GetSize() (int, int) {
  554. return l.width, 0
  555. }
  556. // SetSize implements Item.
  557. func (s *simpleItem) SetSize(width int, height int) tea.Cmd {
  558. s.width = width
  559. return nil
  560. }
  561. func (s *selectableItem) View() string {
  562. if s.focused {
  563. return lipgloss.NewStyle().BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).Width(s.width).Render(s.content)
  564. }
  565. return lipgloss.NewStyle().Width(s.width).Render(s.content)
  566. }
  567. // Blur implements SimpleItem.
  568. func (s *selectableItem) Blur() tea.Cmd {
  569. s.focused = false
  570. return nil
  571. }
  572. // Focus implements SimpleItem.
  573. func (s *selectableItem) Focus() tea.Cmd {
  574. s.focused = true
  575. return nil
  576. }
  577. // IsFocused implements SimpleItem.
  578. func (s *selectableItem) IsFocused() bool {
  579. return s.focused
  580. }
  581. func execCmd(m util.Model, cmd tea.Cmd) {
  582. for cmd != nil {
  583. msg := cmd()
  584. m, cmd = m.Update(msg)
  585. }
  586. }