@@ -1,1126 +0,0 @@
6− uv "github.com/charmbracelet/ultraviolet"
7− "github.com/charmbracelet/ultraviolet/screen"
8− "github.com/charmbracelet/x/exp/ordered"
11−// List is a scrollable list component that implements uv.Drawable.
12−// It efficiently manages a large number of items by caching rendered content
13−// in a master buffer and extracting only the visible viewport when drawn.
23− selectedIdx int // Currently selected item index (-1 if none)
25− // Master buffer containing ALL rendered items
26− masterBuffer *uv.ScreenBuffer
29− // Item positioning in master buffer
30− itemPositions []itemPosition
33− offset int // Scroll offset in lines from top
37− mouseDownItem int // Item index where mouse was pressed
38− mouseDownX int // X position in item content (character offset)
39− mouseDownY int // Y position in item (line offset)
40− mouseDragItem int // Current item index being dragged over
41− mouseDragX int // Current X in item content
42− mouseDragY int // Current Y in item
46− dirtyItems map[int]bool
49−type itemPosition struct {
54−// New creates a new list with the given items.
55−func New(items ...Item) *List {
58− itemPositions: make([]itemPosition, len(items)),
59− dirtyItems: make(map[int]bool),
69−// ensureBuilt ensures the master buffer is built.
70−// This is called by methods that need itemPositions or totalHeight.
71−func (l *List) ensureBuilt() {
72− if l.width <= 0 || l.height <= 0 {
77− l.rebuildMasterBuffer()
78− } else if len(l.dirtyItems) > 0 {
83−// Draw implements uv.Drawable.
84−// Draws the visible viewport of the list to the given screen buffer.
85−func (l *List) Draw(scr uv.Screen, area uv.Rectangle) {
86− if area.Dx() <= 0 || area.Dy() <= 0 {
90− // Update internal dimensions if area size changed
91− widthChanged := l.width != area.Dx()
92− heightChanged := l.height != area.Dy()
97− // Only width changes require rebuilding master buffer
98− // Height changes only affect viewport clipping, not item rendering
103− // Height changes require clamping offset to new bounds
108− if len(l.items) == 0 {
109− screen.ClearArea(scr, area)
113− // Ensure buffer is built
116− // Draw visible portion to the target screen
117− l.drawViewport(scr, area)
120−// Render renders the visible viewport to a string.
121−// This is a convenience method that creates a temporary screen buffer,
122−// draws to it, and returns the rendered string.
123−func (l *List) Render() string {
124− if l.width <= 0 || l.height <= 0 {
128− if len(l.items) == 0 {
132− // Ensure buffer is built
135− // Extract visible lines directly from master buffer
136− return l.renderViewport()
139−// renderViewport renders the visible portion of the master buffer to a string.
140−func (l *List) renderViewport() string {
141− if l.masterBuffer == nil {
145− buf := l.masterBuffer.Buffer
147− // Calculate visible region in master buffer
148− srcStartY := l.offset
149− srcEndY := l.offset + l.height
151− // Clamp to actual buffer bounds
152− if srcStartY >= len(buf.Lines) {
153− // Beyond end of content, return empty lines
154− emptyLine := strings.Repeat(" ", l.width)
155− lines := make([]string, l.height)
156− for i := range lines {
157− lines[i] = emptyLine
159− return strings.Join(lines, "\n")
161− if srcEndY > len(buf.Lines) {
162− srcEndY = len(buf.Lines)
165− // Build result with proper line handling
166− lines := make([]string, l.height)
169− // Render visible lines from buffer
170− for y := srcStartY; y < srcEndY && lineIdx < l.height; y++ {
171− lines[lineIdx] = buf.Lines[y].Render()
175− // Pad remaining lines with spaces to maintain viewport height
176− emptyLine := strings.Repeat(" ", l.width)
177− for ; lineIdx < l.height; lineIdx++ {
178− lines[lineIdx] = emptyLine
181− return strings.Join(lines, "\n")
184−// drawViewport draws the visible portion from master buffer to target screen.
185−func (l *List) drawViewport(scr uv.Screen, area uv.Rectangle) {
186− if l.masterBuffer == nil {
187− screen.ClearArea(scr, area)
191− buf := l.masterBuffer.Buffer
193− // Calculate visible region in master buffer
194− srcStartY := l.offset
195− srcEndY := l.offset + area.Dy()
197− // Clamp to actual buffer bounds
198− if srcStartY >= buf.Height() {
199− screen.ClearArea(scr, area)
202− if srcEndY > buf.Height() {
203− srcEndY = buf.Height()
206− // Copy visible lines to target screen
208− for srcY := srcStartY; srcY < srcEndY && destY < area.Max.Y; srcY++ {
209− line := buf.Line(srcY)
212− for x := 0; x < len(line) && x < area.Dx() && destX < area.Max.X; x++ {
214− scr.SetCell(destX, destY, cell)
220− // Clear any remaining area if content is shorter than viewport
221− if destY < area.Max.Y {
222− clearArea := uv.Rect(area.Min.X, destY, area.Dx(), area.Max.Y-destY)
223− screen.ClearArea(scr, clearArea)
227−// rebuildMasterBuffer composes all items into the master buffer.
228−func (l *List) rebuildMasterBuffer() {
229− if len(l.items) == 0 {
235− // Calculate total height
236− l.totalHeight = l.calculateTotalHeight()
238− // Create or resize master buffer
239− if l.masterBuffer == nil || l.masterBuffer.Width() != l.width || l.masterBuffer.Height() != l.totalHeight {
240− buf := uv.NewScreenBuffer(l.width, l.totalHeight)
241− l.masterBuffer = &buf
245− screen.Clear(l.masterBuffer)
249− for i, item := range l.items {
250− itemHeight := item.Height(l.width)
252− // Draw item to master buffer
253− area := uv.Rect(0, currentY, l.width, itemHeight)
254− item.Draw(l.masterBuffer, area)
257− l.itemPositions[i] = itemPosition{
258− startLine: currentY,
263− currentY += itemHeight
267− l.dirtyItems = make(map[int]bool)
270−// updateDirtyItems efficiently updates only changed items using slice operations.
271−func (l *List) updateDirtyItems() {
272− if len(l.dirtyItems) == 0 {
276− // Check if all dirty items have unchanged heights
277− allSameHeight := true
278− for idx := range l.dirtyItems {
279− item := l.items[idx]
280− pos := l.itemPositions[idx]
281− newHeight := item.Height(l.width)
282− if newHeight != pos.height {
283− allSameHeight = false
288− // Optimization: If all dirty items have unchanged heights, re-render in place
290− buf := l.masterBuffer.Buffer
291− for idx := range l.dirtyItems {
292− item := l.items[idx]
293− pos := l.itemPositions[idx]
295− // Clear the item's area
296− for y := pos.startLine; y < pos.startLine+pos.height && y < len(buf.Lines); y++ {
297− buf.Lines[y] = uv.NewLine(l.width)
301− area := uv.Rect(0, pos.startLine, l.width, pos.height)
302− item.Draw(l.masterBuffer, area)
305− l.dirtyItems = make(map[int]bool)
309− // Height changed - full rebuild
311− l.dirtyItems = make(map[int]bool)
312− l.rebuildMasterBuffer()
315−// updatePositionsBelow updates the startLine for all items below the given index.
316−func (l *List) updatePositionsBelow(fromIdx int, delta int) {
317− for i := fromIdx + 1; i < len(l.items); i++ {
318− pos := l.itemPositions[i]
319− pos.startLine += delta
320− l.itemPositions[i] = pos
324−// calculateTotalHeight calculates the total height of all items plus gaps.
325−func (l *List) calculateTotalHeight() int {
326− if len(l.items) == 0 {
331− for _, item := range l.items {
332− total += item.Height(l.width)
337−// SetSize updates the viewport size.
338−func (l *List) SetSize(width, height int) {
339− widthChanged := l.width != width
340− heightChanged := l.height != height
345− // Width changes require full rebuild (items may reflow)
350− // Height changes require clamping offset to new bounds
356−// Height returns the current viewport height.
357−func (l *List) Height() int {
361−// Width returns the current viewport width.
362−func (l *List) Width() int {
366−// GetSize returns the current viewport size.
367−func (l *List) GetSize() (int, int) {
368− return l.width, l.height
371−// Len returns the number of items in the list.
372−func (l *List) Len() int {
376−// SetItems replaces all items in the list.
377−func (l *List) SetItems(items []Item) {
379− l.itemPositions = make([]itemPosition, len(items))
383−// Items returns all items in the list.
384−func (l *List) Items() []Item {
388−// AppendItem adds an item to the end of the list. Returns true if successful.
389−func (l *List) AppendItem(item Item) bool {
390− l.items = append(l.items, item)
391− l.itemPositions = append(l.itemPositions, itemPosition{})
393− // If buffer not built yet, mark dirty for full rebuild
394− if l.masterBuffer == nil || l.width <= 0 {
399− // Process any pending dirty items before modifying buffer structure
400− if len(l.dirtyItems) > 0 {
401− l.updateDirtyItems()
404− // Efficient append: insert lines at end of buffer
405− itemHeight := item.Height(l.width)
406− startLine := l.totalHeight
409− newLines := make([]uv.Line, itemHeight)
410− for i := range newLines {
411− newLines[i] = uv.NewLine(l.width)
413− l.masterBuffer.Buffer.Lines = append(l.masterBuffer.Buffer.Lines, newLines...)
416− area := uv.Rect(0, startLine, l.width, itemHeight)
417− item.Draw(l.masterBuffer, area)
420− l.itemPositions[len(l.items)-1] = itemPosition{
421− startLine: startLine,
424− l.totalHeight += itemHeight
429−// PrependItem adds an item to the beginning of the list. Returns true if
431−func (l *List) PrependItem(item Item) bool {
432− l.items = append([]Item{item}, l.items...)
433− l.itemPositions = append([]itemPosition{{}}, l.itemPositions...)
434− if l.selectedIdx >= 0 {
438− // If buffer not built yet, mark dirty for full rebuild
439− if l.masterBuffer == nil || l.width <= 0 {
444− // Process any pending dirty items before modifying buffer structure
445− if len(l.dirtyItems) > 0 {
446− l.updateDirtyItems()
449− // Efficient prepend: insert lines at start of buffer
450− itemHeight := item.Height(l.width)
453− newLines := make([]uv.Line, itemHeight)
454− for i := range newLines {
455− newLines[i] = uv.NewLine(l.width)
458− // Insert at beginning
459− buf := l.masterBuffer.Buffer
460− buf.Lines = append(newLines, buf.Lines...)
463− area := uv.Rect(0, 0, l.width, itemHeight)
464− item.Draw(l.masterBuffer, area)
466− // Update all positions (shift everything down)
467− for i := range l.itemPositions {
468− pos := l.itemPositions[i]
469− pos.startLine += itemHeight
470− l.itemPositions[i] = pos
473− // Add position for new item at start
474− l.itemPositions[0] = itemPosition{
479− l.totalHeight += itemHeight
484−// UpdateItem replaces an item with the same index. Returns true if successful.
485−func (l *List) UpdateItem(idx int, item Item) bool {
486− if idx < 0 || idx >= len(l.items) {
490− l.dirtyItems[idx] = true
494−// DeleteItem removes an item by index. Returns true if successful.
495−func (l *List) DeleteItem(idx int) bool {
496− if idx < 0 || idx >= len(l.items) {
500− // Get position before deleting
501− pos := l.itemPositions[idx]
503− // Process any pending dirty items before modifying buffer structure
504− if len(l.dirtyItems) > 0 {
505− l.updateDirtyItems()
508− l.items = append(l.items[:idx], l.items[idx+1:]...)
509− l.itemPositions = append(l.itemPositions[:idx], l.itemPositions[idx+1:]...)
512− if l.selectedIdx == idx {
514− l.selectedIdx = idx - 1
515− } else if len(l.items) > 0 {
520− } else if l.selectedIdx > idx {
524− // If buffer not built yet, mark dirty for full rebuild
525− if l.masterBuffer == nil {
530− // Efficient delete: remove lines from buffer
531− deleteStart := pos.startLine
532− deleteEnd := pos.startLine + pos.height
533− buf := l.masterBuffer.Buffer
535− if deleteEnd <= len(buf.Lines) {
536− buf.Lines = append(buf.Lines[:deleteStart], buf.Lines[deleteEnd:]...)
537− l.totalHeight -= pos.height
538− l.updatePositionsBelow(idx-1, -pos.height)
540− // Position data corrupt, rebuild
547−// Focus focuses the list and the selected item (if focusable).
548−func (l *List) Focus() {
550− l.focusSelectedItem()
553−// Blur blurs the list and the selected item (if focusable).
554−func (l *List) Blur() {
556− l.blurSelectedItem()
559−// Focused returns whether the list is focused.
560−func (l *List) Focused() bool {
564−// SetSelected sets the selected item by ID.
565−func (l *List) SetSelected(idx int) {
566− if idx < 0 || idx >= len(l.items) {
569− if l.selectedIdx == idx {
573− prevIdx := l.selectedIdx
576− // Update focus states if list is focused
578− if prevIdx >= 0 && prevIdx < len(l.items) {
579− if f, ok := l.items[prevIdx].(Focusable); ok {
581− l.dirtyItems[prevIdx] = true
585− if f, ok := l.items[idx].(Focusable); ok {
587− l.dirtyItems[idx] = true
592−// SelectFirst selects the first item in the list.
593−func (l *List) SelectFirst() {
597−// SelectLast selects the last item in the list.
598−func (l *List) SelectLast() {
599− l.SetSelected(len(l.items) - 1)
602−// SelectNextWrap selects the next item in the list (wraps to beginning).
603−// When the list is focused, skips non-focusable items.
604−func (l *List) SelectNextWrap() {
608−// SelectNext selects the next item in the list (no wrap).
609−// When the list is focused, skips non-focusable items.
610−func (l *List) SelectNext() {
614−func (l *List) selectNext(wrap bool) {
615− if len(l.items) == 0 {
619− startIdx := l.selectedIdx
620− for i := 0; i < len(l.items); i++ {
623− nextIdx = (startIdx + 1 + i) % len(l.items)
625− nextIdx = startIdx + 1 + i
626− if nextIdx >= len(l.items) {
631− // If list is focused and item is not focusable, skip it
633− if _, ok := l.items[nextIdx].(Focusable); !ok {
638− // Select and scroll to this item
639− l.SetSelected(nextIdx)
644−// SelectPrevWrap selects the previous item in the list (wraps to end).
645−// When the list is focused, skips non-focusable items.
646−func (l *List) SelectPrevWrap() {
650−// SelectPrev selects the previous item in the list (no wrap).
651−// When the list is focused, skips non-focusable items.
652−func (l *List) SelectPrev() {
656−func (l *List) selectPrev(wrap bool) {
657− if len(l.items) == 0 {
661− startIdx := l.selectedIdx
662− for i := 0; i < len(l.items); i++ {
665− prevIdx = (startIdx - 1 - i + len(l.items)) % len(l.items)
667− prevIdx = startIdx - 1 - i
673− // If list is focused and item is not focusable, skip it
675− if _, ok := l.items[prevIdx].(Focusable); !ok {
680− // Select and scroll to this item
681− l.SetSelected(prevIdx)
686−// SelectedItem returns the currently selected item, or nil if none.
687−func (l *List) SelectedItem() Item {
688− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
691− return l.items[l.selectedIdx]
694−// SelectedIndex returns the index of the currently selected item, or -1 if none.
695−func (l *List) SelectedIndex() int {
696− return l.selectedIdx
699−// AtBottom returns whether the viewport is scrolled to the bottom.
700−func (l *List) AtBottom() bool {
702− return l.offset >= l.totalHeight-l.height
705−// AtTop returns whether the viewport is scrolled to the top.
706−func (l *List) AtTop() bool {
707− return l.offset <= 0
710−// ScrollBy scrolls the viewport by the given number of lines.
711−// Positive values scroll down, negative scroll up.
712−func (l *List) ScrollBy(deltaLines int) {
713− l.offset += deltaLines
717−// ScrollToTop scrolls to the top of the list.
718−func (l *List) ScrollToTop() {
722−// ScrollToBottom scrolls to the bottom of the list.
723−func (l *List) ScrollToBottom() {
725− if l.totalHeight > l.height {
726− l.offset = l.totalHeight - l.height
732−// ScrollToItem scrolls to make the item with the given ID visible.
733−func (l *List) ScrollToItem(idx int) {
735− pos := l.itemPositions[idx]
736− itemStart := pos.startLine
737− itemEnd := pos.startLine + pos.height
738− viewStart := l.offset
739− viewEnd := l.offset + l.height
741− // Check if item is already fully visible
742− if itemStart >= viewStart && itemEnd <= viewEnd {
746− // Scroll to show item
747− if itemStart < viewStart {
748− l.offset = itemStart
749− } else if itemEnd > viewEnd {
750− l.offset = itemEnd - l.height
756−// ScrollToSelected scrolls to make the selected item visible.
757−func (l *List) ScrollToSelected() {
758− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
761− l.ScrollToItem(l.selectedIdx)
764−// Offset returns the current scroll offset.
765−func (l *List) Offset() int {
769−// TotalHeight returns the total height of all items including gaps.
770−func (l *List) TotalHeight() int {
771− return l.totalHeight
774−// SelectFirstInView selects the first item that is fully visible in the viewport.
775−func (l *List) SelectFirstInView() {
778− viewportStart := l.offset
779− viewportEnd := l.offset + l.height
781− for i := range l.items {
782− pos := l.itemPositions[i]
784− // Check if item is fully within viewport bounds
785− if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
792−// SelectLastInView selects the last item that is fully visible in the viewport.
793−func (l *List) SelectLastInView() {
796− viewportStart := l.offset
797− viewportEnd := l.offset + l.height
799− for i := len(l.items) - 1; i >= 0; i-- {
800− pos := l.itemPositions[i]
802− // Check if item is fully within viewport bounds
803− if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
810−// SelectedItemInView returns true if the selected item is currently visible in the viewport.
811−func (l *List) SelectedItemInView() bool {
812− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
816− // Get selected item ID and position
817− pos := l.itemPositions[l.selectedIdx]
819− // Check if item is within viewport bounds
820− viewportStart := l.offset
821− viewportEnd := l.offset + l.height
823− // Item is visible if any part of it overlaps with the viewport
824− return pos.startLine < viewportEnd && (pos.startLine+pos.height) > viewportStart
827−// clampOffset ensures offset is within valid bounds.
828−func (l *List) clampOffset() {
829− l.offset = ordered.Clamp(l.offset, 0, l.totalHeight-l.height)
832−// focusSelectedItem focuses the currently selected item if it's focusable.
833−func (l *List) focusSelectedItem() {
834− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
838− item := l.items[l.selectedIdx]
839− if f, ok := item.(Focusable); ok {
841− l.dirtyItems[l.selectedIdx] = true
845−// blurSelectedItem blurs the currently selected item if it's focusable.
846−func (l *List) blurSelectedItem() {
847− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
851− item := l.items[l.selectedIdx]
852− if f, ok := item.(Focusable); ok {
854− l.dirtyItems[l.selectedIdx] = true
858−// HandleMouseDown handles mouse button press events.
859−// x and y are viewport-relative coordinates (0,0 = top-left of visible area).
860−// Returns true if the event was handled.
861−func (l *List) HandleMouseDown(x, y int) bool {
864− // Convert viewport y to master buffer y
865− bufferY := y + l.offset
867− // Find which item was clicked
868− itemIdx, itemY := l.findItemAtPosition(bufferY)
873− // Calculate x position within item content
874− // For now, x is just the viewport x coordinate
875− // Items can interpret this as character offset in their content
878− l.mouseDownItem = itemIdx
880− l.mouseDownY = itemY
881− l.mouseDragItem = itemIdx
883− l.mouseDragY = itemY
885− // Select the clicked item
886− l.SetSelected(itemIdx)
891−// HandleMouseDrag handles mouse drag events during selection.
892−// x and y are viewport-relative coordinates.
893−// Returns true if the event was handled.
894−func (l *List) HandleMouseDrag(x, y int) bool {
901− // Convert viewport y to master buffer y
902− bufferY := y + l.offset
904− // Find which item we're dragging over
905− itemIdx, itemY := l.findItemAtPosition(bufferY)
910− l.mouseDragItem = itemIdx
912− l.mouseDragY = itemY
914− // Update highlight if item supports it
920−// HandleMouseUp handles mouse button release events.
921−// Returns true if the event was handled.
922−func (l *List) HandleMouseUp(x, y int) bool {
929− // Final highlight update
935−// ClearHighlight clears any active text highlighting.
936−func (l *List) ClearHighlight() {
937− for i, item := range l.items {
938− if h, ok := item.(Highlightable); ok {
939− h.SetHighlight(-1, -1, -1, -1)
940− l.dirtyItems[i] = true
943− l.mouseDownItem = -1
944− l.mouseDragItem = -1
947−// findItemAtPosition finds the item at the given master buffer y coordinate.
948−// Returns the item index and the y offset within that item. It returns -1, -1
949−// if no item is found.
950−func (l *List) findItemAtPosition(bufferY int) (itemIdx int, itemY int) {
951− if bufferY < 0 || bufferY >= l.totalHeight {
955− // Linear search through items to find which one contains this y
956− // This could be optimized with binary search if needed
957− for i := range l.items {
958− pos := l.itemPositions[i]
959− if bufferY >= pos.startLine && bufferY < pos.startLine+pos.height {
960− return i, bufferY - pos.startLine
967−// updateHighlight updates the highlight range for highlightable items.
968−// Supports highlighting across multiple items and respects drag direction.
969−func (l *List) updateHighlight() {
970− if l.mouseDownItem < 0 {
974− // Get start and end item indices
975− downItemIdx := l.mouseDownItem
976− dragItemIdx := l.mouseDragItem
978− // Determine selection direction
979− draggingDown := dragItemIdx > downItemIdx ||
980− (dragItemIdx == downItemIdx && l.mouseDragY > l.mouseDownY) ||
981− (dragItemIdx == downItemIdx && l.mouseDragY == l.mouseDownY && l.mouseDragX >= l.mouseDownX)
983− // Determine actual start and end based on direction
984− var startItemIdx, endItemIdx int
985− var startLine, startCol, endLine, endCol int
988− // Normal forward selection
989− startItemIdx = downItemIdx
990− endItemIdx = dragItemIdx
991− startLine = l.mouseDownY
992− startCol = l.mouseDownX
993− endLine = l.mouseDragY
994− endCol = l.mouseDragX
996− // Backward selection (dragging up)
997− startItemIdx = dragItemIdx
998− endItemIdx = downItemIdx
999− startLine = l.mouseDragY
1000− startCol = l.mouseDragX
1001− endLine = l.mouseDownY
1002− endCol = l.mouseDownX
1005− // Clear all highlights first
1006− for i, item := range l.items {
1007− if h, ok := item.(Highlightable); ok {
1008− h.SetHighlight(-1, -1, -1, -1)
1009− l.dirtyItems[i] = true
1013− // Highlight all items in range
1014− for idx := startItemIdx; idx <= endItemIdx; idx++ {
1015− item, ok := l.items[idx].(Highlightable)
1020− if idx == startItemIdx && idx == endItemIdx {
1021− // Single item selection
1022− item.SetHighlight(startLine, startCol, endLine, endCol)
1023− } else if idx == startItemIdx {
1024− // First item - from start position to end of item
1025− pos := l.itemPositions[idx]
1026− item.SetHighlight(startLine, startCol, pos.height-1, 9999) // 9999 = end of line
1027− } else if idx == endItemIdx {
1028− // Last item - from start of item to end position
1029− item.SetHighlight(0, 0, endLine, endCol)
1031− // Middle item - fully highlighted
1032− pos := l.itemPositions[idx]
1033− item.SetHighlight(0, 0, pos.height-1, 9999)
1036− l.dirtyItems[idx] = true
1040−// GetHighlightedText returns the plain text content of all highlighted regions
1041−// across items, without any styling. Returns empty string if no highlights exist.
1042−func (l *List) GetHighlightedText() string {
1045− if l.masterBuffer == nil {
1049− var result strings.Builder
1051− // Iterate through items to find highlighted ones
1052− for i, item := range l.items {
1053− h, ok := item.(Highlightable)
1058− startLine, startCol, endLine, endCol := h.GetHighlight()
1063− pos := l.itemPositions[i]
1065− // Extract text from highlighted region in master buffer
1066− for y := startLine; y <= endLine && y < pos.height; y++ {
1067− bufferY := pos.startLine + y
1068− if bufferY >= l.masterBuffer.Height() {
1072− line := l.masterBuffer.Line(bufferY)
1074− // Determine column range for this line
1076− if y == startLine {
1077− colStart = startCol
1080− colEnd := len(line)
1082− colEnd = min(endCol, len(line))
1085− // Track last non-empty position to trim trailing spaces
1087− for x := colStart; x < colEnd && x < len(line); x++ {
1089− if cell == nil || cell.IsZero() {
1092− if cell.Content != "" && cell.Content != " " {
1097− // Extract text from cells using String() method, up to last content
1099− if lastContentX >= 0 {
1100− endX = lastContentX + 1
1103− for x := colStart; x < endX && x < len(line); x++ {
1105− if cell == nil || cell.IsZero() {
1108− result.WriteString(cell.String())
1111− // Add newline between lines (but not after the last line)
1112− if y < endLine && y < pos.height-1 {
1113− result.WriteRune('\n')
1117− // Add newline between items if there are more highlighted items
1118− if result.Len() > 0 {
1119− result.WriteRune('\n')
1123− // Trim trailing newline if present
1124− text := result.String()
1125− return strings.TrimSuffix(text, "\n")