list.go 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694
  1. package list
  2. import (
  3. "strings"
  4. "sync"
  5. "github.com/charmbracelet/bubbles/v2/key"
  6. tea "github.com/charmbracelet/bubbletea/v2"
  7. "github.com/charmbracelet/crush/internal/tui/components/anim"
  8. "github.com/charmbracelet/crush/internal/tui/components/core/layout"
  9. "github.com/charmbracelet/crush/internal/tui/styles"
  10. "github.com/charmbracelet/crush/internal/tui/util"
  11. "github.com/charmbracelet/lipgloss/v2"
  12. uv "github.com/charmbracelet/ultraviolet"
  13. "github.com/charmbracelet/x/ansi"
  14. "github.com/charmbracelet/x/exp/ordered"
  15. "github.com/rivo/uniseg"
  16. )
  17. const maxGapSize = 100
  18. var newlineBuffer = strings.Repeat("\n", maxGapSize)
  19. var (
  20. specialCharsMap map[string]struct{}
  21. specialCharsOnce sync.Once
  22. )
  23. func getSpecialCharsMap() map[string]struct{} {
  24. specialCharsOnce.Do(func() {
  25. specialCharsMap = make(map[string]struct{}, len(styles.SelectionIgnoreIcons))
  26. for _, icon := range styles.SelectionIgnoreIcons {
  27. specialCharsMap[icon] = struct{}{}
  28. }
  29. })
  30. return specialCharsMap
  31. }
  32. type Item interface {
  33. util.Model
  34. layout.Sizeable
  35. ID() string
  36. }
  37. type HasAnim interface {
  38. Item
  39. Spinning() bool
  40. }
  41. type List[T Item] interface {
  42. util.Model
  43. layout.Sizeable
  44. layout.Focusable
  45. MoveUp(int) tea.Cmd
  46. MoveDown(int) tea.Cmd
  47. GoToTop() tea.Cmd
  48. GoToBottom() tea.Cmd
  49. SelectItemAbove() tea.Cmd
  50. SelectItemBelow() tea.Cmd
  51. SetItems([]T) tea.Cmd
  52. SetSelected(string) tea.Cmd
  53. SelectedItem() *T
  54. Items() []T
  55. UpdateItem(string, T) tea.Cmd
  56. DeleteItem(string) tea.Cmd
  57. PrependItem(T) tea.Cmd
  58. AppendItem(T) tea.Cmd
  59. StartSelection(col, line int)
  60. EndSelection(col, line int)
  61. SelectionStop()
  62. SelectionClear()
  63. SelectWord(col, line int)
  64. SelectParagraph(col, line int)
  65. GetSelectedText(paddingLeft int) string
  66. HasSelection() bool
  67. }
  68. type direction int
  69. const (
  70. DirectionForward direction = iota
  71. DirectionBackward
  72. )
  73. const (
  74. ItemNotFound = -1
  75. ViewportDefaultScrollSize = 5
  76. )
  77. type renderedItem struct {
  78. view string
  79. height int
  80. start int
  81. end int
  82. }
  83. type confOptions struct {
  84. width, height int
  85. gap int
  86. wrap bool
  87. keyMap KeyMap
  88. direction direction
  89. selectedItemIdx int // Index of selected item (-1 if none)
  90. selectedItemID string // Temporary storage for WithSelectedItem (resolved in New())
  91. focused bool
  92. resize bool
  93. enableMouse bool
  94. }
  95. type list[T Item] struct {
  96. *confOptions
  97. offset int
  98. indexMap map[string]int
  99. items []T
  100. renderedItems map[string]renderedItem
  101. rendered string
  102. renderedHeight int // cached height of rendered content
  103. lineOffsets []int // cached byte offsets for each line (for fast slicing)
  104. cachedView string
  105. cachedViewOffset int
  106. cachedViewDirty bool
  107. movingByItem bool
  108. prevSelectedItemIdx int // Index of previously selected item (-1 if none)
  109. selectionStartCol int
  110. selectionStartLine int
  111. selectionEndCol int
  112. selectionEndLine int
  113. selectionActive bool
  114. }
  115. type ListOption func(*confOptions)
  116. // WithSize sets the size of the list.
  117. func WithSize(width, height int) ListOption {
  118. return func(l *confOptions) {
  119. l.width = width
  120. l.height = height
  121. }
  122. }
  123. // WithGap sets the gap between items in the list.
  124. func WithGap(gap int) ListOption {
  125. return func(l *confOptions) {
  126. l.gap = gap
  127. }
  128. }
  129. // WithDirectionForward sets the direction to forward
  130. func WithDirectionForward() ListOption {
  131. return func(l *confOptions) {
  132. l.direction = DirectionForward
  133. }
  134. }
  135. // WithDirectionBackward sets the direction to forward
  136. func WithDirectionBackward() ListOption {
  137. return func(l *confOptions) {
  138. l.direction = DirectionBackward
  139. }
  140. }
  141. // WithSelectedItem sets the initially selected item in the list.
  142. func WithSelectedItem(id string) ListOption {
  143. return func(l *confOptions) {
  144. l.selectedItemID = id // Will be resolved to index in New()
  145. }
  146. }
  147. func WithKeyMap(keyMap KeyMap) ListOption {
  148. return func(l *confOptions) {
  149. l.keyMap = keyMap
  150. }
  151. }
  152. func WithWrapNavigation() ListOption {
  153. return func(l *confOptions) {
  154. l.wrap = true
  155. }
  156. }
  157. func WithFocus(focus bool) ListOption {
  158. return func(l *confOptions) {
  159. l.focused = focus
  160. }
  161. }
  162. func WithResizeByList() ListOption {
  163. return func(l *confOptions) {
  164. l.resize = true
  165. }
  166. }
  167. func WithEnableMouse() ListOption {
  168. return func(l *confOptions) {
  169. l.enableMouse = true
  170. }
  171. }
  172. func New[T Item](items []T, opts ...ListOption) List[T] {
  173. list := &list[T]{
  174. confOptions: &confOptions{
  175. direction: DirectionForward,
  176. keyMap: DefaultKeyMap(),
  177. focused: true,
  178. selectedItemIdx: -1,
  179. },
  180. items: items,
  181. indexMap: make(map[string]int, len(items)),
  182. renderedItems: make(map[string]renderedItem),
  183. prevSelectedItemIdx: -1,
  184. selectionStartCol: -1,
  185. selectionStartLine: -1,
  186. selectionEndLine: -1,
  187. selectionEndCol: -1,
  188. }
  189. for _, opt := range opts {
  190. opt(list.confOptions)
  191. }
  192. for inx, item := range items {
  193. if i, ok := any(item).(Indexable); ok {
  194. i.SetIndex(inx)
  195. }
  196. list.indexMap[item.ID()] = inx
  197. }
  198. // Resolve selectedItemID to selectedItemIdx if specified
  199. if list.selectedItemID != "" {
  200. if idx, ok := list.indexMap[list.selectedItemID]; ok {
  201. list.selectedItemIdx = idx
  202. }
  203. list.selectedItemID = "" // Clear temporary storage
  204. }
  205. return list
  206. }
  207. // Init implements List.
  208. func (l *list[T]) Init() tea.Cmd {
  209. return l.render()
  210. }
  211. // Update implements List.
  212. func (l *list[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) {
  213. switch msg := msg.(type) {
  214. case tea.MouseWheelMsg:
  215. if l.enableMouse {
  216. return l.handleMouseWheel(msg)
  217. }
  218. return l, nil
  219. case anim.StepMsg:
  220. // Fast path: if no items, skip processing
  221. if len(l.items) == 0 {
  222. return l, nil
  223. }
  224. // Fast path: check if ANY items are actually spinning before processing
  225. if !l.hasSpinningItems() {
  226. return l, nil
  227. }
  228. var cmds []tea.Cmd
  229. itemsLen := len(l.items)
  230. for i := range itemsLen {
  231. if i >= len(l.items) {
  232. continue
  233. }
  234. item := l.items[i]
  235. if animItem, ok := any(item).(HasAnim); ok && animItem.Spinning() {
  236. updated, cmd := animItem.Update(msg)
  237. cmds = append(cmds, cmd)
  238. if u, ok := updated.(T); ok {
  239. cmds = append(cmds, l.UpdateItem(u.ID(), u))
  240. }
  241. }
  242. }
  243. return l, tea.Batch(cmds...)
  244. case tea.KeyPressMsg:
  245. if l.focused {
  246. switch {
  247. case key.Matches(msg, l.keyMap.Down):
  248. return l, l.MoveDown(ViewportDefaultScrollSize)
  249. case key.Matches(msg, l.keyMap.Up):
  250. return l, l.MoveUp(ViewportDefaultScrollSize)
  251. case key.Matches(msg, l.keyMap.DownOneItem):
  252. return l, l.SelectItemBelow()
  253. case key.Matches(msg, l.keyMap.UpOneItem):
  254. return l, l.SelectItemAbove()
  255. case key.Matches(msg, l.keyMap.HalfPageDown):
  256. return l, l.MoveDown(l.height / 2)
  257. case key.Matches(msg, l.keyMap.HalfPageUp):
  258. return l, l.MoveUp(l.height / 2)
  259. case key.Matches(msg, l.keyMap.PageDown):
  260. return l, l.MoveDown(l.height)
  261. case key.Matches(msg, l.keyMap.PageUp):
  262. return l, l.MoveUp(l.height)
  263. case key.Matches(msg, l.keyMap.End):
  264. return l, l.GoToBottom()
  265. case key.Matches(msg, l.keyMap.Home):
  266. return l, l.GoToTop()
  267. }
  268. s := l.SelectedItem()
  269. if s == nil {
  270. return l, nil
  271. }
  272. item := *s
  273. var cmds []tea.Cmd
  274. updated, cmd := item.Update(msg)
  275. cmds = append(cmds, cmd)
  276. if u, ok := updated.(T); ok {
  277. cmds = append(cmds, l.UpdateItem(u.ID(), u))
  278. }
  279. return l, tea.Batch(cmds...)
  280. }
  281. }
  282. return l, nil
  283. }
  284. func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (util.Model, tea.Cmd) {
  285. var cmd tea.Cmd
  286. switch msg.Button {
  287. case tea.MouseWheelDown:
  288. cmd = l.MoveDown(ViewportDefaultScrollSize)
  289. case tea.MouseWheelUp:
  290. cmd = l.MoveUp(ViewportDefaultScrollSize)
  291. }
  292. return l, cmd
  293. }
  294. func (l *list[T]) hasSpinningItems() bool {
  295. for i := range l.items {
  296. item := l.items[i]
  297. if animItem, ok := any(item).(HasAnim); ok && animItem.Spinning() {
  298. return true
  299. }
  300. }
  301. return false
  302. }
  303. func (l *list[T]) selectionView(view string, textOnly bool) string {
  304. t := styles.CurrentTheme()
  305. area := uv.Rect(0, 0, l.width, l.height)
  306. scr := uv.NewScreenBuffer(area.Dx(), area.Dy())
  307. uv.NewStyledString(view).Draw(scr, area)
  308. selArea := uv.Rectangle{
  309. Min: uv.Pos(l.selectionStartCol, l.selectionStartLine),
  310. Max: uv.Pos(l.selectionEndCol, l.selectionEndLine),
  311. }
  312. selArea = selArea.Canon()
  313. specialChars := getSpecialCharsMap()
  314. isNonWhitespace := func(r rune) bool {
  315. return r != ' ' && r != '\t' && r != 0 && r != '\n' && r != '\r'
  316. }
  317. type selectionBounds struct {
  318. startX, endX int
  319. inSelection bool
  320. }
  321. lineSelections := make([]selectionBounds, scr.Height())
  322. for y := range scr.Height() {
  323. bounds := selectionBounds{startX: -1, endX: -1, inSelection: false}
  324. if y >= selArea.Min.Y && y <= selArea.Max.Y {
  325. bounds.inSelection = true
  326. if selArea.Min.Y == selArea.Max.Y {
  327. // Single line selection
  328. bounds.startX = selArea.Min.X
  329. bounds.endX = selArea.Max.X
  330. } else if y == selArea.Min.Y {
  331. // First line of multi-line selection
  332. bounds.startX = selArea.Min.X
  333. bounds.endX = scr.Width()
  334. } else if y == selArea.Max.Y {
  335. // Last line of multi-line selection
  336. bounds.startX = 0
  337. bounds.endX = selArea.Max.X
  338. } else {
  339. // Middle lines
  340. bounds.startX = 0
  341. bounds.endX = scr.Width()
  342. }
  343. }
  344. lineSelections[y] = bounds
  345. }
  346. type lineBounds struct {
  347. start, end int
  348. }
  349. lineTextBounds := make([]lineBounds, scr.Height())
  350. // First pass: find text bounds for lines that have selections
  351. for y := range scr.Height() {
  352. bounds := lineBounds{start: -1, end: -1}
  353. // Only process lines that might have selections
  354. if lineSelections[y].inSelection {
  355. for x := range scr.Width() {
  356. cell := scr.CellAt(x, y)
  357. if cell == nil {
  358. continue
  359. }
  360. cellStr := cell.String()
  361. if len(cellStr) == 0 {
  362. continue
  363. }
  364. char := rune(cellStr[0])
  365. _, isSpecial := specialChars[cellStr]
  366. if (isNonWhitespace(char) && !isSpecial) || cell.Style.Bg != nil {
  367. if bounds.start == -1 {
  368. bounds.start = x
  369. }
  370. bounds.end = x + 1 // Position after last character
  371. }
  372. }
  373. }
  374. lineTextBounds[y] = bounds
  375. }
  376. var selectedText strings.Builder
  377. // Second pass: apply selection highlighting
  378. for y := range scr.Height() {
  379. selBounds := lineSelections[y]
  380. if !selBounds.inSelection {
  381. continue
  382. }
  383. textBounds := lineTextBounds[y]
  384. if textBounds.start < 0 {
  385. if textOnly {
  386. // We don't want to get rid of all empty lines in text-only mode
  387. selectedText.WriteByte('\n')
  388. }
  389. continue // No text on this line
  390. }
  391. // Only scan within the intersection of text bounds and selection bounds
  392. scanStart := max(textBounds.start, selBounds.startX)
  393. scanEnd := min(textBounds.end, selBounds.endX)
  394. for x := scanStart; x < scanEnd; x++ {
  395. cell := scr.CellAt(x, y)
  396. if cell == nil {
  397. continue
  398. }
  399. cellStr := cell.String()
  400. if len(cellStr) > 0 {
  401. if _, isSpecial := specialChars[cellStr]; isSpecial {
  402. continue
  403. }
  404. if textOnly {
  405. // Collect selected text without styles
  406. selectedText.WriteString(cell.String())
  407. continue
  408. }
  409. // Text selection styling, which is a Lip Gloss style. We must
  410. // extract the values to use in a UV style, below.
  411. ts := t.TextSelection
  412. cell = cell.Clone()
  413. cell.Style = cell.Style.Background(ts.GetBackground()).Foreground(ts.GetForeground())
  414. scr.SetCell(x, y, cell)
  415. }
  416. }
  417. if textOnly {
  418. // Make sure we add a newline after each line of selected text
  419. selectedText.WriteByte('\n')
  420. }
  421. }
  422. if textOnly {
  423. return strings.TrimSpace(selectedText.String())
  424. }
  425. return scr.Render()
  426. }
  427. func (l *list[T]) View() string {
  428. if l.height <= 0 || l.width <= 0 {
  429. return ""
  430. }
  431. if !l.cachedViewDirty && l.cachedViewOffset == l.offset && !l.hasSelection() && l.cachedView != "" {
  432. return l.cachedView
  433. }
  434. t := styles.CurrentTheme()
  435. start, end := l.viewPosition()
  436. viewStart := max(0, start)
  437. viewEnd := end
  438. if viewStart > viewEnd {
  439. return ""
  440. }
  441. view := l.getLines(viewStart, viewEnd)
  442. if l.resize {
  443. return view
  444. }
  445. view = t.S().Base.
  446. Height(l.height).
  447. Width(l.width).
  448. Render(view)
  449. if !l.hasSelection() {
  450. l.cachedView = view
  451. l.cachedViewOffset = l.offset
  452. l.cachedViewDirty = false
  453. return view
  454. }
  455. return l.selectionView(view, false)
  456. }
  457. func (l *list[T]) viewPosition() (int, int) {
  458. start, end := 0, 0
  459. renderedLines := l.renderedHeight - 1
  460. if l.direction == DirectionForward {
  461. start = max(0, l.offset)
  462. end = min(l.offset+l.height-1, renderedLines)
  463. } else {
  464. start = max(0, renderedLines-l.offset-l.height+1)
  465. end = max(0, renderedLines-l.offset)
  466. }
  467. start = min(start, end)
  468. return start, end
  469. }
  470. func (l *list[T]) setRendered(rendered string) {
  471. l.rendered = rendered
  472. l.renderedHeight = lipgloss.Height(rendered)
  473. l.cachedViewDirty = true // Mark view cache as dirty
  474. if len(rendered) > 0 {
  475. l.lineOffsets = make([]int, 0, l.renderedHeight)
  476. l.lineOffsets = append(l.lineOffsets, 0)
  477. offset := 0
  478. for {
  479. idx := strings.IndexByte(rendered[offset:], '\n')
  480. if idx == -1 {
  481. break
  482. }
  483. offset += idx + 1
  484. l.lineOffsets = append(l.lineOffsets, offset)
  485. }
  486. } else {
  487. l.lineOffsets = nil
  488. }
  489. }
  490. func (l *list[T]) getLines(start, end int) string {
  491. if len(l.lineOffsets) == 0 || start >= len(l.lineOffsets) {
  492. return ""
  493. }
  494. if end >= len(l.lineOffsets) {
  495. end = len(l.lineOffsets) - 1
  496. }
  497. if start > end {
  498. return ""
  499. }
  500. startOffset := l.lineOffsets[start]
  501. var endOffset int
  502. if end+1 < len(l.lineOffsets) {
  503. endOffset = l.lineOffsets[end+1] - 1
  504. } else {
  505. endOffset = len(l.rendered)
  506. }
  507. if startOffset >= len(l.rendered) {
  508. return ""
  509. }
  510. endOffset = min(endOffset, len(l.rendered))
  511. return l.rendered[startOffset:endOffset]
  512. }
  513. // getLine returns a single line from the rendered content using lineOffsets.
  514. // This avoids allocating a new string for each line like strings.Split does.
  515. func (l *list[T]) getLine(index int) string {
  516. if len(l.lineOffsets) == 0 || index < 0 || index >= len(l.lineOffsets) {
  517. return ""
  518. }
  519. startOffset := l.lineOffsets[index]
  520. var endOffset int
  521. if index+1 < len(l.lineOffsets) {
  522. endOffset = l.lineOffsets[index+1] - 1 // -1 to exclude the newline
  523. } else {
  524. endOffset = len(l.rendered)
  525. }
  526. if startOffset >= len(l.rendered) {
  527. return ""
  528. }
  529. endOffset = min(endOffset, len(l.rendered))
  530. return l.rendered[startOffset:endOffset]
  531. }
  532. // lineCount returns the number of lines in the rendered content.
  533. func (l *list[T]) lineCount() int {
  534. return len(l.lineOffsets)
  535. }
  536. func (l *list[T]) recalculateItemPositions() {
  537. l.recalculateItemPositionsFrom(0)
  538. }
  539. func (l *list[T]) recalculateItemPositionsFrom(startIdx int) {
  540. var currentContentHeight int
  541. if startIdx > 0 && startIdx <= len(l.items) {
  542. prevItem := l.items[startIdx-1]
  543. if rItem, ok := l.renderedItems[prevItem.ID()]; ok {
  544. currentContentHeight = rItem.end + 1 + l.gap
  545. }
  546. }
  547. for i := startIdx; i < len(l.items); i++ {
  548. item := l.items[i]
  549. rItem, ok := l.renderedItems[item.ID()]
  550. if !ok {
  551. continue
  552. }
  553. rItem.start = currentContentHeight
  554. rItem.end = currentContentHeight + rItem.height - 1
  555. l.renderedItems[item.ID()] = rItem
  556. currentContentHeight = rItem.end + 1 + l.gap
  557. }
  558. }
  559. func (l *list[T]) render() tea.Cmd {
  560. if l.width <= 0 || l.height <= 0 || len(l.items) == 0 {
  561. return nil
  562. }
  563. l.setDefaultSelected()
  564. var focusChangeCmd tea.Cmd
  565. if l.focused {
  566. focusChangeCmd = l.focusSelectedItem()
  567. } else {
  568. focusChangeCmd = l.blurSelectedItem()
  569. }
  570. if l.rendered != "" {
  571. rendered, _ := l.renderIterator(0, false, "")
  572. l.setRendered(rendered)
  573. if l.direction == DirectionBackward {
  574. l.recalculateItemPositions()
  575. }
  576. if l.focused {
  577. l.scrollToSelection()
  578. }
  579. return focusChangeCmd
  580. }
  581. rendered, finishIndex := l.renderIterator(0, true, "")
  582. l.setRendered(rendered)
  583. if l.direction == DirectionBackward {
  584. l.recalculateItemPositions()
  585. }
  586. l.offset = 0
  587. rendered, _ = l.renderIterator(finishIndex, false, l.rendered)
  588. l.setRendered(rendered)
  589. if l.direction == DirectionBackward {
  590. l.recalculateItemPositions()
  591. }
  592. if l.focused {
  593. l.scrollToSelection()
  594. }
  595. return focusChangeCmd
  596. }
  597. func (l *list[T]) setDefaultSelected() {
  598. if l.selectedItemIdx < 0 {
  599. if l.direction == DirectionForward {
  600. l.selectFirstItem()
  601. } else {
  602. l.selectLastItem()
  603. }
  604. }
  605. }
  606. func (l *list[T]) scrollToSelection() {
  607. if l.selectedItemIdx < 0 || l.selectedItemIdx >= len(l.items) {
  608. l.selectedItemIdx = -1
  609. l.setDefaultSelected()
  610. return
  611. }
  612. item := l.items[l.selectedItemIdx]
  613. rItem, ok := l.renderedItems[item.ID()]
  614. if !ok {
  615. l.selectedItemIdx = -1
  616. l.setDefaultSelected()
  617. return
  618. }
  619. start, end := l.viewPosition()
  620. if rItem.start <= start && rItem.end >= end {
  621. return
  622. }
  623. if l.movingByItem {
  624. if rItem.start >= start && rItem.end <= end {
  625. return
  626. }
  627. defer func() { l.movingByItem = false }()
  628. } else {
  629. if rItem.start >= start && rItem.start <= end {
  630. return
  631. }
  632. if rItem.end >= start && rItem.end <= end {
  633. return
  634. }
  635. }
  636. if rItem.height >= l.height {
  637. if l.direction == DirectionForward {
  638. l.offset = rItem.start
  639. } else {
  640. l.offset = max(0, l.renderedHeight-(rItem.start+l.height))
  641. }
  642. return
  643. }
  644. renderedLines := l.renderedHeight - 1
  645. if rItem.start < start {
  646. if l.direction == DirectionForward {
  647. l.offset = rItem.start
  648. } else {
  649. l.offset = max(0, renderedLines-rItem.start-l.height+1)
  650. }
  651. } else if rItem.end > end {
  652. if l.direction == DirectionForward {
  653. l.offset = max(0, rItem.end-l.height+1)
  654. } else {
  655. l.offset = max(0, renderedLines-rItem.end)
  656. }
  657. }
  658. }
  659. func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
  660. if l.selectedItemIdx < 0 || l.selectedItemIdx >= len(l.items) {
  661. return nil
  662. }
  663. item := l.items[l.selectedItemIdx]
  664. rItem, ok := l.renderedItems[item.ID()]
  665. if !ok {
  666. return nil
  667. }
  668. start, end := l.viewPosition()
  669. // item bigger than the viewport do nothing
  670. if rItem.start <= start && rItem.end >= end {
  671. return nil
  672. }
  673. // item already in view do nothing
  674. if rItem.start >= start && rItem.end <= end {
  675. return nil
  676. }
  677. itemMiddle := rItem.start + rItem.height/2
  678. if itemMiddle < start {
  679. // select the first item in the viewport
  680. // the item is most likely an item coming after this item
  681. inx := l.selectedItemIdx
  682. for {
  683. inx = l.firstSelectableItemBelow(inx)
  684. if inx == ItemNotFound {
  685. return nil
  686. }
  687. if inx < 0 || inx >= len(l.items) {
  688. continue
  689. }
  690. item := l.items[inx]
  691. renderedItem, ok := l.renderedItems[item.ID()]
  692. if !ok {
  693. continue
  694. }
  695. // If the item is bigger than the viewport, select it
  696. if renderedItem.start <= start && renderedItem.end >= end {
  697. l.selectedItemIdx = inx
  698. return l.render()
  699. }
  700. // item is in the view
  701. if renderedItem.start >= start && renderedItem.start <= end {
  702. l.selectedItemIdx = inx
  703. return l.render()
  704. }
  705. }
  706. } else if itemMiddle > end {
  707. // select the first item in the viewport
  708. // the item is most likely an item coming after this item
  709. inx := l.selectedItemIdx
  710. for {
  711. inx = l.firstSelectableItemAbove(inx)
  712. if inx == ItemNotFound {
  713. return nil
  714. }
  715. if inx < 0 || inx >= len(l.items) {
  716. continue
  717. }
  718. item := l.items[inx]
  719. renderedItem, ok := l.renderedItems[item.ID()]
  720. if !ok {
  721. continue
  722. }
  723. // If the item is bigger than the viewport, select it
  724. if renderedItem.start <= start && renderedItem.end >= end {
  725. l.selectedItemIdx = inx
  726. return l.render()
  727. }
  728. // item is in the view
  729. if renderedItem.end >= start && renderedItem.end <= end {
  730. l.selectedItemIdx = inx
  731. return l.render()
  732. }
  733. }
  734. }
  735. return nil
  736. }
  737. func (l *list[T]) selectFirstItem() {
  738. inx := l.firstSelectableItemBelow(-1)
  739. if inx != ItemNotFound {
  740. l.selectedItemIdx = inx
  741. }
  742. }
  743. func (l *list[T]) selectLastItem() {
  744. inx := l.firstSelectableItemAbove(len(l.items))
  745. if inx != ItemNotFound {
  746. l.selectedItemIdx = inx
  747. }
  748. }
  749. func (l *list[T]) firstSelectableItemAbove(inx int) int {
  750. for i := inx - 1; i >= 0; i-- {
  751. if i < 0 || i >= len(l.items) {
  752. continue
  753. }
  754. item := l.items[i]
  755. if _, ok := any(item).(layout.Focusable); ok {
  756. return i
  757. }
  758. }
  759. if inx == 0 && l.wrap {
  760. return l.firstSelectableItemAbove(len(l.items))
  761. }
  762. return ItemNotFound
  763. }
  764. func (l *list[T]) firstSelectableItemBelow(inx int) int {
  765. itemsLen := len(l.items)
  766. for i := inx + 1; i < itemsLen; i++ {
  767. if i < 0 || i >= len(l.items) {
  768. continue
  769. }
  770. item := l.items[i]
  771. if _, ok := any(item).(layout.Focusable); ok {
  772. return i
  773. }
  774. }
  775. if inx == itemsLen-1 && l.wrap {
  776. return l.firstSelectableItemBelow(-1)
  777. }
  778. return ItemNotFound
  779. }
  780. func (l *list[T]) focusSelectedItem() tea.Cmd {
  781. if l.selectedItemIdx < 0 || !l.focused {
  782. return nil
  783. }
  784. // Pre-allocate with expected capacity
  785. cmds := make([]tea.Cmd, 0, 2)
  786. // Blur the previously selected item if it's different
  787. if l.prevSelectedItemIdx >= 0 && l.prevSelectedItemIdx != l.selectedItemIdx && l.prevSelectedItemIdx < len(l.items) {
  788. prevItem := l.items[l.prevSelectedItemIdx]
  789. if f, ok := any(prevItem).(layout.Focusable); ok && f.IsFocused() {
  790. cmds = append(cmds, f.Blur())
  791. // Mark cache as needing update, but don't delete yet
  792. // This allows the render to potentially reuse it
  793. delete(l.renderedItems, prevItem.ID())
  794. }
  795. }
  796. // Focus the currently selected item
  797. if l.selectedItemIdx >= 0 && l.selectedItemIdx < len(l.items) {
  798. item := l.items[l.selectedItemIdx]
  799. if f, ok := any(item).(layout.Focusable); ok && !f.IsFocused() {
  800. cmds = append(cmds, f.Focus())
  801. // Mark for re-render
  802. delete(l.renderedItems, item.ID())
  803. }
  804. }
  805. l.prevSelectedItemIdx = l.selectedItemIdx
  806. return tea.Batch(cmds...)
  807. }
  808. func (l *list[T]) blurSelectedItem() tea.Cmd {
  809. if l.selectedItemIdx < 0 || l.focused {
  810. return nil
  811. }
  812. // Blur the currently selected item
  813. if l.selectedItemIdx >= 0 && l.selectedItemIdx < len(l.items) {
  814. item := l.items[l.selectedItemIdx]
  815. if f, ok := any(item).(layout.Focusable); ok && f.IsFocused() {
  816. delete(l.renderedItems, item.ID())
  817. return f.Blur()
  818. }
  819. }
  820. return nil
  821. }
  822. // renderFragment holds updated rendered view fragments
  823. type renderFragment struct {
  824. view string
  825. gap int
  826. }
  827. // renderIterator renders items starting from the specific index and limits height if limitHeight != -1
  828. // returns the last index and the rendered content so far
  829. // we pass the rendered content around and don't use l.rendered to prevent jumping of the content
  830. func (l *list[T]) renderIterator(startInx int, limitHeight bool, rendered string) (string, int) {
  831. // Pre-allocate fragments with expected capacity
  832. itemsLen := len(l.items)
  833. expectedFragments := itemsLen - startInx
  834. if limitHeight && l.height > 0 {
  835. expectedFragments = min(expectedFragments, l.height)
  836. }
  837. fragments := make([]renderFragment, 0, expectedFragments)
  838. currentContentHeight := lipgloss.Height(rendered) - 1
  839. finalIndex := itemsLen
  840. // first pass: accumulate all fragments to render until the height limit is
  841. // reached
  842. for i := startInx; i < itemsLen; i++ {
  843. if limitHeight && currentContentHeight >= l.height {
  844. finalIndex = i
  845. break
  846. }
  847. // cool way to go through the list in both directions
  848. inx := i
  849. if l.direction != DirectionForward {
  850. inx = (itemsLen - 1) - i
  851. }
  852. if inx < 0 || inx >= len(l.items) {
  853. continue
  854. }
  855. item := l.items[inx]
  856. var rItem renderedItem
  857. if cache, ok := l.renderedItems[item.ID()]; ok {
  858. rItem = cache
  859. } else {
  860. rItem = l.renderItem(item)
  861. rItem.start = currentContentHeight
  862. rItem.end = currentContentHeight + rItem.height - 1
  863. l.renderedItems[item.ID()] = rItem
  864. }
  865. gap := l.gap + 1
  866. if inx == itemsLen-1 {
  867. gap = 0
  868. }
  869. fragments = append(fragments, renderFragment{view: rItem.view, gap: gap})
  870. currentContentHeight = rItem.end + 1 + l.gap
  871. }
  872. // second pass: build rendered string efficiently
  873. var b strings.Builder
  874. // Pre-size the builder to reduce allocations
  875. estimatedSize := len(rendered)
  876. for _, f := range fragments {
  877. estimatedSize += len(f.view) + f.gap
  878. }
  879. b.Grow(estimatedSize)
  880. if l.direction == DirectionForward {
  881. b.WriteString(rendered)
  882. for i := range fragments {
  883. f := &fragments[i]
  884. b.WriteString(f.view)
  885. // Optimized gap writing using pre-allocated buffer
  886. if f.gap > 0 {
  887. if f.gap <= maxGapSize {
  888. b.WriteString(newlineBuffer[:f.gap])
  889. } else {
  890. b.WriteString(strings.Repeat("\n", f.gap))
  891. }
  892. }
  893. }
  894. return b.String(), finalIndex
  895. }
  896. // iterate backwards as fragments are in reversed order
  897. for i := len(fragments) - 1; i >= 0; i-- {
  898. f := &fragments[i]
  899. b.WriteString(f.view)
  900. // Optimized gap writing using pre-allocated buffer
  901. if f.gap > 0 {
  902. if f.gap <= maxGapSize {
  903. b.WriteString(newlineBuffer[:f.gap])
  904. } else {
  905. b.WriteString(strings.Repeat("\n", f.gap))
  906. }
  907. }
  908. }
  909. b.WriteString(rendered)
  910. return b.String(), finalIndex
  911. }
  912. func (l *list[T]) renderItem(item Item) renderedItem {
  913. view := item.View()
  914. return renderedItem{
  915. view: view,
  916. height: lipgloss.Height(view),
  917. }
  918. }
  919. // AppendItem implements List.
  920. func (l *list[T]) AppendItem(item T) tea.Cmd {
  921. // Pre-allocate with expected capacity
  922. cmds := make([]tea.Cmd, 0, 4)
  923. cmd := item.Init()
  924. if cmd != nil {
  925. cmds = append(cmds, cmd)
  926. }
  927. newIndex := len(l.items)
  928. l.items = append(l.items, item)
  929. l.indexMap[item.ID()] = newIndex
  930. if l.width > 0 && l.height > 0 {
  931. cmd = item.SetSize(l.width, l.height)
  932. if cmd != nil {
  933. cmds = append(cmds, cmd)
  934. }
  935. }
  936. cmd = l.render()
  937. if cmd != nil {
  938. cmds = append(cmds, cmd)
  939. }
  940. if l.direction == DirectionBackward {
  941. if l.offset == 0 {
  942. cmd = l.GoToBottom()
  943. if cmd != nil {
  944. cmds = append(cmds, cmd)
  945. }
  946. } else {
  947. newItem, ok := l.renderedItems[item.ID()]
  948. if ok {
  949. newLines := newItem.height
  950. if len(l.items) > 1 {
  951. newLines += l.gap
  952. }
  953. l.offset = min(l.renderedHeight-1, l.offset+newLines)
  954. }
  955. }
  956. }
  957. return tea.Sequence(cmds...)
  958. }
  959. // Blur implements List.
  960. func (l *list[T]) Blur() tea.Cmd {
  961. l.focused = false
  962. return l.render()
  963. }
  964. // DeleteItem implements List.
  965. func (l *list[T]) DeleteItem(id string) tea.Cmd {
  966. inx, ok := l.indexMap[id]
  967. if !ok {
  968. return nil
  969. }
  970. l.items = append(l.items[:inx], l.items[inx+1:]...)
  971. delete(l.renderedItems, id)
  972. delete(l.indexMap, id)
  973. // Only update indices for items after the deleted one
  974. itemsLen := len(l.items)
  975. for i := inx; i < itemsLen; i++ {
  976. if i >= 0 && i < len(l.items) {
  977. item := l.items[i]
  978. l.indexMap[item.ID()] = i
  979. }
  980. }
  981. // Adjust selectedItemIdx if the deleted item was selected or before it
  982. if l.selectedItemIdx == inx {
  983. // Deleted item was selected, select the previous item if possible
  984. if inx > 0 {
  985. l.selectedItemIdx = inx - 1
  986. } else {
  987. l.selectedItemIdx = -1
  988. }
  989. } else if l.selectedItemIdx > inx {
  990. // Selected item is after the deleted one, shift index down
  991. l.selectedItemIdx--
  992. }
  993. cmd := l.render()
  994. if l.rendered != "" {
  995. if l.renderedHeight <= l.height {
  996. l.offset = 0
  997. } else {
  998. maxOffset := l.renderedHeight - l.height
  999. if l.offset > maxOffset {
  1000. l.offset = maxOffset
  1001. }
  1002. }
  1003. }
  1004. return cmd
  1005. }
  1006. // Focus implements List.
  1007. func (l *list[T]) Focus() tea.Cmd {
  1008. l.focused = true
  1009. return l.render()
  1010. }
  1011. // GetSize implements List.
  1012. func (l *list[T]) GetSize() (int, int) {
  1013. return l.width, l.height
  1014. }
  1015. // GoToBottom implements List.
  1016. func (l *list[T]) GoToBottom() tea.Cmd {
  1017. l.offset = 0
  1018. l.selectedItemIdx = -1
  1019. l.direction = DirectionBackward
  1020. return l.render()
  1021. }
  1022. // GoToTop implements List.
  1023. func (l *list[T]) GoToTop() tea.Cmd {
  1024. l.offset = 0
  1025. l.selectedItemIdx = -1
  1026. l.direction = DirectionForward
  1027. return l.render()
  1028. }
  1029. // IsFocused implements List.
  1030. func (l *list[T]) IsFocused() bool {
  1031. return l.focused
  1032. }
  1033. // Items implements List.
  1034. func (l *list[T]) Items() []T {
  1035. itemsLen := len(l.items)
  1036. result := make([]T, 0, itemsLen)
  1037. for i := range itemsLen {
  1038. if i >= 0 && i < len(l.items) {
  1039. item := l.items[i]
  1040. result = append(result, item)
  1041. }
  1042. }
  1043. return result
  1044. }
  1045. func (l *list[T]) incrementOffset(n int) {
  1046. // no need for offset
  1047. if l.renderedHeight <= l.height {
  1048. return
  1049. }
  1050. maxOffset := l.renderedHeight - l.height
  1051. n = min(n, maxOffset-l.offset)
  1052. if n <= 0 {
  1053. return
  1054. }
  1055. l.offset += n
  1056. l.cachedViewDirty = true
  1057. }
  1058. func (l *list[T]) decrementOffset(n int) {
  1059. n = min(n, l.offset)
  1060. if n <= 0 {
  1061. return
  1062. }
  1063. l.offset -= n
  1064. if l.offset < 0 {
  1065. l.offset = 0
  1066. }
  1067. l.cachedViewDirty = true
  1068. }
  1069. // MoveDown implements List.
  1070. func (l *list[T]) MoveDown(n int) tea.Cmd {
  1071. oldOffset := l.offset
  1072. if l.direction == DirectionForward {
  1073. l.incrementOffset(n)
  1074. } else {
  1075. l.decrementOffset(n)
  1076. }
  1077. if oldOffset == l.offset {
  1078. // no change in offset, so no need to change selection
  1079. return nil
  1080. }
  1081. // if we are not actively selecting move the whole selection down
  1082. if l.hasSelection() && !l.selectionActive {
  1083. if l.selectionStartLine < l.selectionEndLine {
  1084. l.selectionStartLine -= n
  1085. l.selectionEndLine -= n
  1086. } else {
  1087. l.selectionStartLine -= n
  1088. l.selectionEndLine -= n
  1089. }
  1090. }
  1091. if l.selectionActive {
  1092. if l.selectionStartLine < l.selectionEndLine {
  1093. l.selectionStartLine -= n
  1094. } else {
  1095. l.selectionEndLine -= n
  1096. }
  1097. }
  1098. return l.changeSelectionWhenScrolling()
  1099. }
  1100. // MoveUp implements List.
  1101. func (l *list[T]) MoveUp(n int) tea.Cmd {
  1102. oldOffset := l.offset
  1103. if l.direction == DirectionForward {
  1104. l.decrementOffset(n)
  1105. } else {
  1106. l.incrementOffset(n)
  1107. }
  1108. if oldOffset == l.offset {
  1109. // no change in offset, so no need to change selection
  1110. return nil
  1111. }
  1112. if l.hasSelection() && !l.selectionActive {
  1113. if l.selectionStartLine > l.selectionEndLine {
  1114. l.selectionStartLine += n
  1115. l.selectionEndLine += n
  1116. } else {
  1117. l.selectionStartLine += n
  1118. l.selectionEndLine += n
  1119. }
  1120. }
  1121. if l.selectionActive {
  1122. if l.selectionStartLine > l.selectionEndLine {
  1123. l.selectionStartLine += n
  1124. } else {
  1125. l.selectionEndLine += n
  1126. }
  1127. }
  1128. return l.changeSelectionWhenScrolling()
  1129. }
  1130. // PrependItem implements List.
  1131. func (l *list[T]) PrependItem(item T) tea.Cmd {
  1132. // Pre-allocate with expected capacity
  1133. cmds := make([]tea.Cmd, 0, 4)
  1134. cmds = append(cmds, item.Init())
  1135. l.items = append([]T{item}, l.items...)
  1136. // Shift selectedItemIdx since all items moved down by 1
  1137. if l.selectedItemIdx >= 0 {
  1138. l.selectedItemIdx++
  1139. }
  1140. // Update index map incrementally: shift all existing indices up by 1
  1141. // This is more efficient than rebuilding from scratch
  1142. newIndexMap := make(map[string]int, len(l.indexMap)+1)
  1143. for id, idx := range l.indexMap {
  1144. newIndexMap[id] = idx + 1 // All existing items shift down by 1
  1145. }
  1146. newIndexMap[item.ID()] = 0 // New item is at index 0
  1147. l.indexMap = newIndexMap
  1148. if l.width > 0 && l.height > 0 {
  1149. cmds = append(cmds, item.SetSize(l.width, l.height))
  1150. }
  1151. cmds = append(cmds, l.render())
  1152. if l.direction == DirectionForward {
  1153. if l.offset == 0 {
  1154. cmd := l.GoToTop()
  1155. if cmd != nil {
  1156. cmds = append(cmds, cmd)
  1157. }
  1158. } else {
  1159. newItem, ok := l.renderedItems[item.ID()]
  1160. if ok {
  1161. newLines := newItem.height
  1162. if len(l.items) > 1 {
  1163. newLines += l.gap
  1164. }
  1165. l.offset = min(l.renderedHeight-1, l.offset+newLines)
  1166. }
  1167. }
  1168. }
  1169. return tea.Batch(cmds...)
  1170. }
  1171. // SelectItemAbove implements List.
  1172. func (l *list[T]) SelectItemAbove() tea.Cmd {
  1173. if l.selectedItemIdx < 0 {
  1174. return nil
  1175. }
  1176. newIndex := l.firstSelectableItemAbove(l.selectedItemIdx)
  1177. if newIndex == ItemNotFound {
  1178. // no item above
  1179. return nil
  1180. }
  1181. // Pre-allocate with expected capacity
  1182. cmds := make([]tea.Cmd, 0, 2)
  1183. if newIndex == 1 {
  1184. peakAboveIndex := l.firstSelectableItemAbove(newIndex)
  1185. if peakAboveIndex == ItemNotFound {
  1186. // this means there is a section above move to the top
  1187. cmd := l.GoToTop()
  1188. if cmd != nil {
  1189. cmds = append(cmds, cmd)
  1190. }
  1191. }
  1192. }
  1193. if newIndex < 0 || newIndex >= len(l.items) {
  1194. return nil
  1195. }
  1196. l.prevSelectedItemIdx = l.selectedItemIdx
  1197. l.selectedItemIdx = newIndex
  1198. l.movingByItem = true
  1199. renderCmd := l.render()
  1200. if renderCmd != nil {
  1201. cmds = append(cmds, renderCmd)
  1202. }
  1203. return tea.Sequence(cmds...)
  1204. }
  1205. // SelectItemBelow implements List.
  1206. func (l *list[T]) SelectItemBelow() tea.Cmd {
  1207. if l.selectedItemIdx < 0 {
  1208. return nil
  1209. }
  1210. newIndex := l.firstSelectableItemBelow(l.selectedItemIdx)
  1211. if newIndex == ItemNotFound {
  1212. // no item above
  1213. return nil
  1214. }
  1215. if newIndex < 0 || newIndex >= len(l.items) {
  1216. return nil
  1217. }
  1218. l.prevSelectedItemIdx = l.selectedItemIdx
  1219. l.selectedItemIdx = newIndex
  1220. l.movingByItem = true
  1221. return l.render()
  1222. }
  1223. // SelectedItem implements List.
  1224. func (l *list[T]) SelectedItem() *T {
  1225. if l.selectedItemIdx < 0 || l.selectedItemIdx >= len(l.items) {
  1226. return nil
  1227. }
  1228. item := l.items[l.selectedItemIdx]
  1229. return &item
  1230. }
  1231. // SetItems implements List.
  1232. func (l *list[T]) SetItems(items []T) tea.Cmd {
  1233. l.items = items
  1234. var cmds []tea.Cmd
  1235. for inx, item := range items {
  1236. if i, ok := any(item).(Indexable); ok {
  1237. i.SetIndex(inx)
  1238. }
  1239. cmds = append(cmds, item.Init())
  1240. }
  1241. cmds = append(cmds, l.reset(""))
  1242. return tea.Batch(cmds...)
  1243. }
  1244. // SetSelected implements List.
  1245. func (l *list[T]) SetSelected(id string) tea.Cmd {
  1246. l.prevSelectedItemIdx = l.selectedItemIdx
  1247. if idx, ok := l.indexMap[id]; ok {
  1248. l.selectedItemIdx = idx
  1249. } else {
  1250. l.selectedItemIdx = -1
  1251. }
  1252. return l.render()
  1253. }
  1254. func (l *list[T]) reset(selectedItemID string) tea.Cmd {
  1255. var cmds []tea.Cmd
  1256. l.rendered = ""
  1257. l.renderedHeight = 0
  1258. l.offset = 0
  1259. l.indexMap = make(map[string]int)
  1260. l.renderedItems = make(map[string]renderedItem)
  1261. itemsLen := len(l.items)
  1262. for i := range itemsLen {
  1263. if i < 0 || i >= len(l.items) {
  1264. continue
  1265. }
  1266. item := l.items[i]
  1267. l.indexMap[item.ID()] = i
  1268. if l.width > 0 && l.height > 0 {
  1269. cmds = append(cmds, item.SetSize(l.width, l.height))
  1270. }
  1271. }
  1272. // Convert selectedItemID to index after rebuilding indexMap
  1273. if selectedItemID != "" {
  1274. if idx, ok := l.indexMap[selectedItemID]; ok {
  1275. l.selectedItemIdx = idx
  1276. } else {
  1277. l.selectedItemIdx = -1
  1278. }
  1279. } else {
  1280. l.selectedItemIdx = -1
  1281. }
  1282. cmds = append(cmds, l.render())
  1283. return tea.Batch(cmds...)
  1284. }
  1285. // SetSize implements List.
  1286. func (l *list[T]) SetSize(width int, height int) tea.Cmd {
  1287. oldWidth := l.width
  1288. l.width = width
  1289. l.height = height
  1290. if oldWidth != width {
  1291. // Get current selected item ID before reset
  1292. selectedID := ""
  1293. if l.selectedItemIdx >= 0 && l.selectedItemIdx < len(l.items) {
  1294. item := l.items[l.selectedItemIdx]
  1295. selectedID = item.ID()
  1296. }
  1297. cmd := l.reset(selectedID)
  1298. return cmd
  1299. }
  1300. return nil
  1301. }
  1302. // UpdateItem implements List.
  1303. func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
  1304. // Pre-allocate with expected capacity
  1305. cmds := make([]tea.Cmd, 0, 1)
  1306. if inx, ok := l.indexMap[id]; ok {
  1307. l.items[inx] = item
  1308. oldItem, hasOldItem := l.renderedItems[id]
  1309. oldPosition := l.offset
  1310. if l.direction == DirectionBackward {
  1311. oldPosition = (l.renderedHeight - 1) - l.offset
  1312. }
  1313. delete(l.renderedItems, id)
  1314. cmd := l.render()
  1315. // need to check for nil because of sequence not handling nil
  1316. if cmd != nil {
  1317. cmds = append(cmds, cmd)
  1318. }
  1319. if hasOldItem && l.direction == DirectionBackward {
  1320. // if we are the last item and there is no offset
  1321. // make sure to go to the bottom
  1322. if oldPosition < oldItem.end {
  1323. newItem, ok := l.renderedItems[item.ID()]
  1324. if ok {
  1325. newLines := newItem.height - oldItem.height
  1326. l.offset = ordered.Clamp(l.offset+newLines, 0, l.renderedHeight-1)
  1327. }
  1328. }
  1329. } else if hasOldItem && l.offset > oldItem.start {
  1330. newItem, ok := l.renderedItems[item.ID()]
  1331. if ok {
  1332. newLines := newItem.height - oldItem.height
  1333. l.offset = ordered.Clamp(l.offset+newLines, 0, l.renderedHeight-1)
  1334. }
  1335. }
  1336. }
  1337. return tea.Sequence(cmds...)
  1338. }
  1339. func (l *list[T]) hasSelection() bool {
  1340. return l.selectionEndCol != l.selectionStartCol || l.selectionEndLine != l.selectionStartLine
  1341. }
  1342. // StartSelection implements List.
  1343. func (l *list[T]) StartSelection(col, line int) {
  1344. l.selectionStartCol = col
  1345. l.selectionStartLine = line
  1346. l.selectionEndCol = col
  1347. l.selectionEndLine = line
  1348. l.selectionActive = true
  1349. }
  1350. // EndSelection implements List.
  1351. func (l *list[T]) EndSelection(col, line int) {
  1352. if !l.selectionActive {
  1353. return
  1354. }
  1355. l.selectionEndCol = col
  1356. l.selectionEndLine = line
  1357. }
  1358. func (l *list[T]) SelectionStop() {
  1359. l.selectionActive = false
  1360. }
  1361. func (l *list[T]) SelectionClear() {
  1362. l.selectionStartCol = -1
  1363. l.selectionStartLine = -1
  1364. l.selectionEndCol = -1
  1365. l.selectionEndLine = -1
  1366. l.selectionActive = false
  1367. }
  1368. func (l *list[T]) findWordBoundaries(col, line int) (startCol, endCol int) {
  1369. numLines := l.lineCount()
  1370. if l.direction == DirectionBackward && numLines > l.height {
  1371. line = ((numLines - 1) - l.height) + line + 1
  1372. }
  1373. if l.offset > 0 {
  1374. if l.direction == DirectionBackward {
  1375. line -= l.offset
  1376. } else {
  1377. line += l.offset
  1378. }
  1379. }
  1380. if line < 0 || line >= numLines {
  1381. return 0, 0
  1382. }
  1383. currentLine := ansi.Strip(l.getLine(line))
  1384. gr := uniseg.NewGraphemes(currentLine)
  1385. startCol = -1
  1386. upTo := col
  1387. for gr.Next() {
  1388. if gr.IsWordBoundary() && upTo > 0 {
  1389. startCol = col - upTo + 1
  1390. } else if gr.IsWordBoundary() && upTo < 0 {
  1391. endCol = col - upTo + 1
  1392. break
  1393. }
  1394. if upTo == 0 && gr.Str() == " " {
  1395. return 0, 0
  1396. }
  1397. upTo -= 1
  1398. }
  1399. if startCol == -1 {
  1400. return 0, 0
  1401. }
  1402. return startCol, endCol
  1403. }
  1404. func (l *list[T]) findParagraphBoundaries(line int) (startLine, endLine int, found bool) {
  1405. // Helper function to get a line with ANSI stripped and icons replaced
  1406. getCleanLine := func(index int) string {
  1407. rawLine := l.getLine(index)
  1408. cleanLine := ansi.Strip(rawLine)
  1409. for _, icon := range styles.SelectionIgnoreIcons {
  1410. cleanLine = strings.ReplaceAll(cleanLine, icon, " ")
  1411. }
  1412. return cleanLine
  1413. }
  1414. numLines := l.lineCount()
  1415. if l.direction == DirectionBackward && numLines > l.height {
  1416. line = (numLines - 1) - l.height + line + 1
  1417. }
  1418. if l.offset > 0 {
  1419. if l.direction == DirectionBackward {
  1420. line -= l.offset
  1421. } else {
  1422. line += l.offset
  1423. }
  1424. }
  1425. // Ensure line is within bounds
  1426. if line < 0 || line >= numLines {
  1427. return 0, 0, false
  1428. }
  1429. if strings.TrimSpace(getCleanLine(line)) == "" {
  1430. return 0, 0, false
  1431. }
  1432. // Find start of paragraph (search backwards for empty line or start of text)
  1433. startLine = line
  1434. for startLine > 0 && strings.TrimSpace(getCleanLine(startLine-1)) != "" {
  1435. startLine--
  1436. }
  1437. // Find end of paragraph (search forwards for empty line or end of text)
  1438. endLine = line
  1439. for endLine < numLines-1 && strings.TrimSpace(getCleanLine(endLine+1)) != "" {
  1440. endLine++
  1441. }
  1442. // revert the line numbers if we are in backward direction
  1443. if l.direction == DirectionBackward && numLines > l.height {
  1444. startLine = startLine - (numLines - 1) + l.height - 1
  1445. endLine = endLine - (numLines - 1) + l.height - 1
  1446. }
  1447. if l.offset > 0 {
  1448. if l.direction == DirectionBackward {
  1449. startLine += l.offset
  1450. endLine += l.offset
  1451. } else {
  1452. startLine -= l.offset
  1453. endLine -= l.offset
  1454. }
  1455. }
  1456. return startLine, endLine, true
  1457. }
  1458. // SelectWord selects the word at the given position.
  1459. func (l *list[T]) SelectWord(col, line int) {
  1460. startCol, endCol := l.findWordBoundaries(col, line)
  1461. l.selectionStartCol = startCol
  1462. l.selectionStartLine = line
  1463. l.selectionEndCol = endCol
  1464. l.selectionEndLine = line
  1465. l.selectionActive = false // Not actively selecting, just selected
  1466. }
  1467. // SelectParagraph selects the paragraph at the given position.
  1468. func (l *list[T]) SelectParagraph(col, line int) {
  1469. startLine, endLine, found := l.findParagraphBoundaries(line)
  1470. if !found {
  1471. return
  1472. }
  1473. l.selectionStartCol = 0
  1474. l.selectionStartLine = startLine
  1475. l.selectionEndCol = l.width - 1
  1476. l.selectionEndLine = endLine
  1477. l.selectionActive = false // Not actively selecting, just selected
  1478. }
  1479. // HasSelection returns whether there is an active selection.
  1480. func (l *list[T]) HasSelection() bool {
  1481. return l.hasSelection()
  1482. }
  1483. // GetSelectedText returns the currently selected text.
  1484. func (l *list[T]) GetSelectedText(paddingLeft int) string {
  1485. if !l.hasSelection() {
  1486. return ""
  1487. }
  1488. return l.selectionView(l.View(), true)
  1489. }