1package list
2
3import (
4 "image"
5 "strings"
6
7 tea "charm.land/bubbletea/v2"
8 "charm.land/lipgloss/v2"
9 "github.com/charmbracelet/x/ansi"
10)
11
12// List represents a list of items that can be lazily rendered. A list is
13// always rendered like a chat conversation where items are stacked vertically
14// from top to bottom.
15type List struct {
16 // Viewport size
17 width, height int
18
19 // Items in the list
20 items []Item
21
22 // Gap between items (0 or less means no gap)
23 gap int
24
25 // Focus and selection state
26 focused bool
27 selectedIdx int // The current selected index -1 means no selection
28
29 // Mouse state
30 mouseDown bool
31 mouseDownItem int // Item index where mouse was pressed
32 mouseDownX int // X position in item content (character offset)
33 mouseDownY int // Y position in item (line offset)
34 mouseDragItem int // Current item index being dragged over
35 mouseDragX int // Current X in item content
36 mouseDragY int // Current Y in item
37 lastHighlighted map[int]bool // Track which items were highlighted in last update
38
39 // Rendered content and cache
40 renderedItems map[int]renderedItem
41
42 // offsetIdx is the index of the first visible item in the viewport.
43 offsetIdx int
44 // offsetLine is the number of lines of the item at offsetIdx that are
45 // scrolled out of view (above the viewport).
46 // It must always be >= 0.
47 offsetLine int
48}
49
50// renderedItem holds the rendered content and height of an item.
51type renderedItem struct {
52 content string
53 height int
54}
55
56// NewList creates a new lazy-loaded list.
57func NewList(items ...Item) *List {
58 l := new(List)
59 l.items = items
60 l.renderedItems = make(map[int]renderedItem)
61 l.selectedIdx = -1
62 l.mouseDownItem = -1
63 l.mouseDragItem = -1
64 l.lastHighlighted = make(map[int]bool)
65 return l
66}
67
68// SetSize sets the size of the list viewport.
69func (l *List) SetSize(width, height int) {
70 if width != l.width {
71 l.renderedItems = make(map[int]renderedItem)
72 }
73 l.width = width
74 l.height = height
75 // l.normalizeOffsets()
76}
77
78// SetGap sets the gap between items.
79func (l *List) SetGap(gap int) {
80 l.gap = gap
81}
82
83// Width returns the width of the list viewport.
84func (l *List) Width() int {
85 return l.width
86}
87
88// Height returns the height of the list viewport.
89func (l *List) Height() int {
90 return l.height
91}
92
93// Len returns the number of items in the list.
94func (l *List) Len() int {
95 return len(l.items)
96}
97
98// getItem renders (if needed) and returns the item at the given index.
99func (l *List) getItem(idx int) renderedItem {
100 return l.renderItem(idx, false)
101}
102
103// applyHighlight applies highlighting to the given rendered item.
104func (l *List) applyHighlight(idx int, ri *renderedItem) {
105 // Apply highlight if item supports it
106 if highlightable, ok := l.items[idx].(HighlightStylable); ok {
107 startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
108 if idx >= startItemIdx && idx <= endItemIdx {
109 var sLine, sCol, eLine, eCol int
110 if idx == startItemIdx && idx == endItemIdx {
111 // Single item selection
112 sLine = startLine
113 sCol = startCol
114 eLine = endLine
115 eCol = endCol
116 } else if idx == startItemIdx {
117 // First item - from start position to end of item
118 sLine = startLine
119 sCol = startCol
120 eLine = ri.height - 1
121 eCol = 9999 // 9999 = end of line
122 } else if idx == endItemIdx {
123 // Last item - from start of item to end position
124 sLine = 0
125 sCol = 0
126 eLine = endLine
127 eCol = endCol
128 } else {
129 // Middle item - fully highlighted
130 sLine = 0
131 sCol = 0
132 eLine = ri.height - 1
133 eCol = 9999
134 }
135
136 // Apply offset for styling frame
137 contentArea := image.Rect(0, 0, l.width, ri.height)
138
139 hiStyle := highlightable.HighlightStyle()
140 rendered := Highlight(ri.content, contentArea, sLine, sCol, eLine, eCol, ToHighlighter(hiStyle))
141 ri.content = rendered
142 }
143 }
144}
145
146// renderItem renders (if needed) and returns the item at the given index. If
147// process is true, it applies focus and highlight styling.
148func (l *List) renderItem(idx int, process bool) renderedItem {
149 if idx < 0 || idx >= len(l.items) {
150 return renderedItem{}
151 }
152
153 var style lipgloss.Style
154 focusable, isFocusable := l.items[idx].(FocusStylable)
155 if isFocusable {
156 style = focusable.BlurStyle()
157 if l.focused && idx == l.selectedIdx {
158 style = focusable.FocusStyle()
159 }
160 }
161
162 // Notify item of focus state if it cares.
163 isFocused := l.focused && idx == l.selectedIdx
164 if focusAware, ok := l.items[idx].(FocusAware); ok {
165 focusAware.SetFocused(isFocused)
166 }
167
168 ri, ok := l.renderedItems[idx]
169 if !ok {
170 item := l.items[idx]
171 rendered := item.Render(l.width - style.GetHorizontalFrameSize())
172 rendered = strings.TrimRight(rendered, "\n")
173 height := countLines(rendered)
174
175 ri = renderedItem{
176 content: rendered,
177 height: height,
178 }
179
180 l.renderedItems[idx] = ri
181 }
182
183 if !process {
184 // Simply return cached rendered item with frame size applied
185 if vfs := style.GetVerticalFrameSize(); vfs > 0 {
186 ri.height += vfs
187 }
188 return ri
189 }
190
191 // We apply highlighting before focus styling so that focus styling
192 // overrides highlight styles.
193 if l.mouseDownItem >= 0 {
194 l.applyHighlight(idx, &ri)
195 }
196
197 if isFocusable {
198 // Apply focus/blur styling if needed
199 rendered := style.Render(ri.content)
200 height := countLines(rendered)
201 ri.content = rendered
202 ri.height = height
203 }
204
205 return ri
206}
207
208// invalidateItem invalidates the cached rendered content of the item at the
209// given index.
210func (l *List) invalidateItem(idx int) {
211 delete(l.renderedItems, idx)
212}
213
214// ScrollToIndex scrolls the list to the given item index.
215func (l *List) ScrollToIndex(index int) {
216 if index < 0 {
217 index = 0
218 }
219 if index >= len(l.items) {
220 index = len(l.items) - 1
221 }
222 l.offsetIdx = index
223 l.offsetLine = 0
224}
225
226// ScrollBy scrolls the list by the given number of lines.
227func (l *List) ScrollBy(lines int) {
228 if len(l.items) == 0 || lines == 0 {
229 return
230 }
231
232 if lines > 0 {
233 // Scroll down
234 // Calculate from the bottom how many lines needed to anchor the last
235 // item to the bottom
236 var totalLines int
237 var lastItemIdx int // the last item that can be partially visible
238 for i := len(l.items) - 1; i >= 0; i-- {
239 item := l.getItem(i)
240 totalLines += item.height
241 if l.gap > 0 && i < len(l.items)-1 {
242 totalLines += l.gap
243 }
244 if totalLines > l.height-1 {
245 lastItemIdx = i
246 break
247 }
248 }
249
250 // Now scroll down by lines
251 var item renderedItem
252 l.offsetLine += lines
253 for {
254 item = l.getItem(l.offsetIdx)
255 totalHeight := item.height
256 if l.gap > 0 {
257 totalHeight += l.gap
258 }
259
260 if l.offsetIdx >= lastItemIdx || l.offsetLine < totalHeight {
261 // Valid offset
262 break
263 }
264
265 // Move to next item
266 l.offsetLine -= totalHeight
267 l.offsetIdx++
268 }
269
270 if l.offsetLine >= item.height {
271 l.offsetLine = item.height
272 }
273 } else if lines < 0 {
274 // Scroll up
275 l.offsetLine += lines // lines is negative
276 for l.offsetLine < 0 {
277 if l.offsetIdx <= 0 {
278 // Reached top
279 l.ScrollToTop()
280 break
281 }
282
283 // Move to previous item
284 l.offsetIdx--
285 prevItem := l.getItem(l.offsetIdx)
286 totalHeight := prevItem.height
287 if l.gap > 0 {
288 totalHeight += l.gap
289 }
290 l.offsetLine += totalHeight
291 }
292 }
293}
294
295// findVisibleItems finds the range of items that are visible in the viewport.
296// This is used for checking if selected item is in view.
297func (l *List) findVisibleItems() (startIdx, endIdx int) {
298 if len(l.items) == 0 {
299 return 0, 0
300 }
301
302 startIdx = l.offsetIdx
303 currentIdx := startIdx
304 visibleHeight := -l.offsetLine
305
306 for currentIdx < len(l.items) {
307 item := l.getItem(currentIdx)
308 visibleHeight += item.height
309 if l.gap > 0 {
310 visibleHeight += l.gap
311 }
312
313 if visibleHeight >= l.height {
314 break
315 }
316 currentIdx++
317 }
318
319 endIdx = currentIdx
320 if endIdx >= len(l.items) {
321 endIdx = len(l.items) - 1
322 }
323
324 return startIdx, endIdx
325}
326
327// Render renders the list and returns the visible lines.
328func (l *List) Render() string {
329 if len(l.items) == 0 {
330 return ""
331 }
332
333 var lines []string
334 currentIdx := l.offsetIdx
335 currentOffset := l.offsetLine
336
337 linesNeeded := l.height
338
339 for linesNeeded > 0 && currentIdx < len(l.items) {
340 item := l.renderItem(currentIdx, true)
341 itemLines := strings.Split(item.content, "\n")
342 itemHeight := len(itemLines)
343
344 if currentOffset >= 0 && currentOffset < itemHeight {
345 // Add visible content lines
346 lines = append(lines, itemLines[currentOffset:]...)
347
348 // Add gap if this is not the absolute last visual element (conceptually gaps are between items)
349 // But in the loop we can just add it and trim later
350 if l.gap > 0 {
351 for i := 0; i < l.gap; i++ {
352 lines = append(lines, "")
353 }
354 }
355 } else {
356 // offsetLine starts in the gap
357 gapOffset := currentOffset - itemHeight
358 gapRemaining := l.gap - gapOffset
359 if gapRemaining > 0 {
360 for range gapRemaining {
361 lines = append(lines, "")
362 }
363 }
364 }
365
366 linesNeeded = l.height - len(lines)
367 currentIdx++
368 currentOffset = 0 // Reset offset for subsequent items
369 }
370
371 if len(lines) > l.height {
372 lines = lines[:l.height]
373 }
374
375 return strings.Join(lines, "\n")
376}
377
378// SetItems sets the items in the list.
379func (l *List) SetItems(items ...Item) {
380 l.setItems(true, items...)
381}
382
383// setItems sets the items in the list. If evict is true, it clears the
384// rendered item cache.
385func (l *List) setItems(evict bool, items ...Item) {
386 l.items = items
387 if evict {
388 l.renderedItems = make(map[int]renderedItem)
389 }
390 l.selectedIdx = min(l.selectedIdx, len(l.items)-1)
391 l.offsetIdx = min(l.offsetIdx, len(l.items)-1)
392 l.offsetLine = 0
393}
394
395// AppendItems appends items to the list.
396func (l *List) AppendItems(items ...Item) {
397 l.items = append(l.items, items...)
398}
399
400// GetItemAt returns the item at the given index. Returns nil if the index is
401// out of bounds.
402func (l *List) GetItemAt(idx int) Item {
403 if idx < 0 || idx >= len(l.items) {
404 return nil
405 }
406 return l.items[idx]
407}
408
409// InvalidateItemAt invalidates the render cache for the item at the given
410// index without replacing the item. Use this when you've mutated an item's
411// internal state and need to force a re-render.
412func (l *List) InvalidateItemAt(idx int) {
413 if idx >= 0 && idx < len(l.items) {
414 l.invalidateItem(idx)
415 }
416}
417
418// DeleteItemAt removes the item at the given index. Returns true if the index
419// was valid and the item was removed.
420func (l *List) DeleteItemAt(idx int) bool {
421 if idx < 0 || idx >= len(l.items) {
422 return false
423 }
424
425 // Remove from items slice.
426 l.items = append(l.items[:idx], l.items[idx+1:]...)
427
428 // Clear and rebuild cache with shifted indices.
429 newCache := make(map[int]renderedItem, len(l.renderedItems))
430 for i, val := range l.renderedItems {
431 if i < idx {
432 newCache[i] = val
433 } else if i > idx {
434 newCache[i-1] = val
435 }
436 }
437 l.renderedItems = newCache
438
439 // Adjust selection if needed.
440 if l.selectedIdx >= len(l.items) && len(l.items) > 0 {
441 l.selectedIdx = len(l.items) - 1
442 }
443
444 return true
445}
446
447// Focus sets the focus state of the list.
448func (l *List) Focus() {
449 l.focused = true
450 // Invalidate the selected item if it's focus-aware.
451 if l.selectedIdx >= 0 && l.selectedIdx < len(l.items) {
452 if _, ok := l.items[l.selectedIdx].(FocusAware); ok {
453 l.invalidateItem(l.selectedIdx)
454 }
455 }
456}
457
458// Blur removes the focus state from the list.
459func (l *List) Blur() {
460 l.focused = false
461 // Invalidate the selected item if it's focus-aware.
462 if l.selectedIdx >= 0 && l.selectedIdx < len(l.items) {
463 if _, ok := l.items[l.selectedIdx].(FocusAware); ok {
464 l.invalidateItem(l.selectedIdx)
465 }
466 }
467}
468
469// ScrollToTop scrolls the list to the top.
470func (l *List) ScrollToTop() {
471 l.offsetIdx = 0
472 l.offsetLine = 0
473}
474
475// ScrollToBottom scrolls the list to the bottom.
476func (l *List) ScrollToBottom() {
477 if len(l.items) == 0 {
478 return
479 }
480
481 // Scroll to the last item
482 var totalHeight int
483 for i := len(l.items) - 1; i >= 0; i-- {
484 item := l.getItem(i)
485 totalHeight += item.height
486 if l.gap > 0 && i < len(l.items)-1 {
487 totalHeight += l.gap
488 }
489 if totalHeight >= l.height {
490 l.offsetIdx = i
491 l.offsetLine = totalHeight - l.height
492 break
493 }
494 }
495 if totalHeight < l.height {
496 // All items fit in the viewport
497 l.ScrollToTop()
498 }
499}
500
501// ScrollToSelected scrolls the list to the selected item.
502func (l *List) ScrollToSelected() {
503 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
504 return
505 }
506
507 startIdx, endIdx := l.findVisibleItems()
508 if l.selectedIdx < startIdx {
509 // Selected item is above the visible range
510 l.offsetIdx = l.selectedIdx
511 l.offsetLine = 0
512 } else if l.selectedIdx > endIdx {
513 // Selected item is below the visible range
514 // Scroll so that the selected item is at the bottom
515 var totalHeight int
516 for i := l.selectedIdx; i >= 0; i-- {
517 item := l.getItem(i)
518 totalHeight += item.height
519 if l.gap > 0 && i < l.selectedIdx {
520 totalHeight += l.gap
521 }
522 if totalHeight >= l.height {
523 l.offsetIdx = i
524 l.offsetLine = totalHeight - l.height
525 break
526 }
527 }
528 if totalHeight < l.height {
529 // All items fit in the viewport
530 l.ScrollToTop()
531 }
532 }
533}
534
535// SelectedItemInView returns whether the selected item is currently in view.
536func (l *List) SelectedItemInView() bool {
537 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
538 return false
539 }
540 startIdx, endIdx := l.findVisibleItems()
541 return l.selectedIdx >= startIdx && l.selectedIdx <= endIdx
542}
543
544// SetSelected sets the selected item index in the list.
545func (l *List) SetSelected(index int) {
546 oldIdx := l.selectedIdx
547 if index < 0 || index >= len(l.items) {
548 l.selectedIdx = -1
549 } else {
550 l.selectedIdx = index
551 }
552 l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
553}
554
555// invalidateFocusAwareItems invalidates the cache for items that implement
556// FocusAware when their focus state changes.
557func (l *List) invalidateFocusAwareItems(oldIdx, newIdx int) {
558 if oldIdx == newIdx {
559 return
560 }
561 if oldIdx >= 0 && oldIdx < len(l.items) {
562 if _, ok := l.items[oldIdx].(FocusAware); ok {
563 l.invalidateItem(oldIdx)
564 }
565 }
566 if newIdx >= 0 && newIdx < len(l.items) {
567 if _, ok := l.items[newIdx].(FocusAware); ok {
568 l.invalidateItem(newIdx)
569 }
570 }
571}
572
573// SelectPrev selects the previous item in the list.
574func (l *List) SelectPrev() {
575 if l.selectedIdx > 0 {
576 oldIdx := l.selectedIdx
577 l.selectedIdx--
578 l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
579 }
580}
581
582// SelectNext selects the next item in the list.
583func (l *List) SelectNext() {
584 if l.selectedIdx < len(l.items)-1 {
585 oldIdx := l.selectedIdx
586 l.selectedIdx++
587 l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
588 }
589}
590
591// SelectFirst selects the first item in the list.
592func (l *List) SelectFirst() {
593 if len(l.items) > 0 {
594 oldIdx := l.selectedIdx
595 l.selectedIdx = 0
596 l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
597 }
598}
599
600// SelectLast selects the last item in the list.
601func (l *List) SelectLast() {
602 if len(l.items) > 0 {
603 oldIdx := l.selectedIdx
604 l.selectedIdx = len(l.items) - 1
605 l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
606 }
607}
608
609// SelectedItem returns the currently selected item. It may be nil if no item
610// is selected.
611func (l *List) SelectedItem() Item {
612 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
613 return nil
614 }
615 return l.items[l.selectedIdx]
616}
617
618// SelectFirstInView selects the first item currently in view.
619func (l *List) SelectFirstInView() {
620 startIdx, _ := l.findVisibleItems()
621 oldIdx := l.selectedIdx
622 l.selectedIdx = startIdx
623 l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
624}
625
626// SelectLastInView selects the last item currently in view.
627func (l *List) SelectLastInView() {
628 _, endIdx := l.findVisibleItems()
629 oldIdx := l.selectedIdx
630 l.selectedIdx = endIdx
631 l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
632}
633
634// HandleMouseDown handles mouse down events at the given line in the viewport.
635// x and y are viewport-relative coordinates (0,0 = top-left of visible area).
636// Returns true if the event was handled.
637func (l *List) HandleMouseDown(x, y int) bool {
638 if len(l.items) == 0 {
639 return false
640 }
641
642 // Find which item was clicked
643 itemIdx, itemY := l.findItemAtY(x, y)
644 if itemIdx < 0 {
645 return false
646 }
647
648 l.mouseDown = true
649 l.mouseDownItem = itemIdx
650 l.mouseDownX = x
651 l.mouseDownY = itemY
652 l.mouseDragItem = itemIdx
653 l.mouseDragX = x
654 l.mouseDragY = itemY
655
656 // Select the clicked item
657 l.SetSelected(itemIdx)
658
659 if clickable, ok := l.items[itemIdx].(MouseClickable); ok {
660 clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
661 l.items[itemIdx] = clickable.(Item)
662 l.invalidateItem(itemIdx)
663 }
664
665 return true
666}
667
668// HandleMouseUp handles mouse up events at the given line in the viewport.
669// Returns true if the event was handled.
670func (l *List) HandleMouseUp(x, y int) bool {
671 if !l.mouseDown {
672 return false
673 }
674
675 l.mouseDown = false
676
677 return true
678}
679
680// HandleMouseDrag handles mouse drag events at the given line in the viewport.
681// x and y are viewport-relative coordinates.
682// Returns true if the event was handled.
683func (l *List) HandleMouseDrag(x, y int) bool {
684 if !l.mouseDown {
685 return false
686 }
687
688 if len(l.items) == 0 {
689 return false
690 }
691
692 // Find which item we're dragging over
693 itemIdx, itemY := l.findItemAtY(x, y)
694 if itemIdx < 0 {
695 return false
696 }
697
698 l.mouseDragItem = itemIdx
699 l.mouseDragX = x
700 l.mouseDragY = itemY
701
702 return true
703}
704
705// ClearHighlight clears any active text highlighting.
706func (l *List) ClearHighlight() {
707 l.mouseDownItem = -1
708 l.mouseDragItem = -1
709 l.lastHighlighted = make(map[int]bool)
710}
711
712// HandleKeyPress handles key press events for the currently selected item.
713// Returns true if the event was handled.
714func (l *List) HandleKeyPress(msg tea.KeyPressMsg) bool {
715 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
716 return false
717 }
718
719 if keyable, ok := l.items[l.selectedIdx].(KeyPressable); ok {
720 handled := keyable.HandleKeyPress(msg)
721 if handled {
722 l.invalidateItem(l.selectedIdx)
723 }
724 return handled
725 }
726
727 return false
728}
729
730// UpdateItems propagates a message to all items that implement Updatable.
731// This is typically used for animation messages like anim.StepMsg.
732// Returns commands from updated items.
733func (l *List) UpdateItems(msg tea.Msg) tea.Cmd {
734 var cmds []tea.Cmd
735 for i, item := range l.items {
736 if updatable, ok := item.(Updatable); ok {
737 updated, cmd := updatable.Update(msg)
738 if cmd != nil {
739 cmds = append(cmds, cmd)
740 // Invalidate cache when animation updates, even if pointer is same.
741 l.invalidateItem(i)
742 }
743 if updated != item {
744 l.items[i] = updated
745 l.invalidateItem(i)
746 }
747 }
748 }
749 if len(cmds) == 0 {
750 return nil
751 }
752 return tea.Batch(cmds...)
753}
754
755// findItemAtY finds the item at the given viewport y coordinate.
756// Returns the item index and the y offset within that item. It returns -1, -1
757// if no item is found.
758func (l *List) findItemAtY(_, y int) (itemIdx int, itemY int) {
759 if y < 0 || y >= l.height {
760 return -1, -1
761 }
762
763 // Walk through visible items to find which one contains this y
764 currentIdx := l.offsetIdx
765 currentLine := -l.offsetLine // Negative because offsetLine is how many lines are hidden
766
767 for currentIdx < len(l.items) && currentLine < l.height {
768 item := l.getItem(currentIdx)
769 itemEndLine := currentLine + item.height
770
771 // Check if y is within this item's visible range
772 if y >= currentLine && y < itemEndLine {
773 // Found the item, calculate itemY (offset within the item)
774 itemY = y - currentLine
775 return currentIdx, itemY
776 }
777
778 // Move to next item
779 currentLine = itemEndLine
780 if l.gap > 0 {
781 currentLine += l.gap
782 }
783 currentIdx++
784 }
785
786 return -1, -1
787}
788
789// getHighlightRange returns the current highlight range.
790func (l *List) getHighlightRange() (startItemIdx, startLine, startCol, endItemIdx, endLine, endCol int) {
791 if l.mouseDownItem < 0 {
792 return -1, -1, -1, -1, -1, -1
793 }
794
795 downItemIdx := l.mouseDownItem
796 dragItemIdx := l.mouseDragItem
797
798 // Determine selection direction
799 draggingDown := dragItemIdx > downItemIdx ||
800 (dragItemIdx == downItemIdx && l.mouseDragY > l.mouseDownY) ||
801 (dragItemIdx == downItemIdx && l.mouseDragY == l.mouseDownY && l.mouseDragX >= l.mouseDownX)
802
803 if draggingDown {
804 // Normal forward selection
805 startItemIdx = downItemIdx
806 startLine = l.mouseDownY
807 startCol = l.mouseDownX
808 endItemIdx = dragItemIdx
809 endLine = l.mouseDragY
810 endCol = l.mouseDragX
811 } else {
812 // Backward selection (dragging up)
813 startItemIdx = dragItemIdx
814 startLine = l.mouseDragY
815 startCol = l.mouseDragX
816 endItemIdx = downItemIdx
817 endLine = l.mouseDownY
818 endCol = l.mouseDownX
819 }
820
821 return startItemIdx, startLine, startCol, endItemIdx, endLine, endCol
822}
823
824// countLines counts the number of lines in a string.
825func countLines(s string) int {
826 if s == "" {
827 return 0
828 }
829 return strings.Count(s, "\n") + 1
830}