feat(list): add HasSelection method and selectionView for text-only output

Ayman Bagabas created

Change summary

internal/tui/exp/list/list.go | 145 +++++++++++++++---------------------
1 file changed, 59 insertions(+), 86 deletions(-)

Detailed changes

internal/tui/exp/list/list.go 🔗

@@ -56,6 +56,7 @@ type List[T Item] interface {
 	SelectWord(col, line int)
 	SelectParagraph(col, line int)
 	GetSelectedText(paddingLeft int) string
+	HasSelection() bool
 }
 
 type direction int
@@ -286,30 +287,10 @@ func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (tea.Model, tea.Cmd) {
 	return l, cmd
 }
 
-// View implements List.
-func (l *list[T]) View() string {
-	if l.height <= 0 || l.width <= 0 {
-		return ""
-	}
+// selectionView renders the highlighted selection in the view and returns it
+// as a string. If textOnly is true, it won't render any styles.
+func (l *list[T]) selectionView(view string, textOnly bool) string {
 	t := styles.CurrentTheme()
-	view := l.rendered
-	lines := strings.Split(view, "\n")
-
-	start, end := l.viewPosition()
-	viewStart := max(0, start)
-	viewEnd := min(len(lines), end+1)
-	lines = lines[viewStart:viewEnd]
-	if l.resize {
-		return strings.Join(lines, "\n")
-	}
-	view = t.S().Base.
-		Height(l.height).
-		Width(l.width).
-		Render(strings.Join(lines, "\n"))
-
-	if !l.hasSelection() {
-		return view
-	}
 	area := uv.Rect(0, 0, l.width, l.height)
 	scr := uv.NewScreenBuffer(area.Dx(), area.Dy())
 	uv.NewStyledString(view).Draw(scr, area)
@@ -397,6 +378,8 @@ func (l *list[T]) View() string {
 		lineTextBounds[y] = bounds
 	}
 
+	var selectedText strings.Builder
+
 	// Second pass: apply selection highlighting
 	for y := range scr.Height() {
 		selBounds := lineSelections[y]
@@ -421,16 +404,59 @@ func (l *list[T]) View() string {
 
 			cellStr := cell.String()
 			if len(cellStr) > 0 && !specialChars[cellStr] {
+				if textOnly {
+					// Collect selected text without styles
+					selectedText.WriteString(cell.String())
+					continue
+				}
+
 				cell = cell.Clone()
 				cell.Style = cell.Style.Background(t.BgOverlay).Foreground(t.White)
 				scr.SetCell(x, y, cell)
 			}
 		}
+
+		if textOnly {
+			// Make sure we add a newline after each line of selected text
+			selectedText.WriteByte('\n')
+		}
+	}
+
+	if textOnly {
+		return strings.TrimSpace(selectedText.String())
 	}
 
 	return scr.Render()
 }
 
+// View implements List.
+func (l *list[T]) View() string {
+	if l.height <= 0 || l.width <= 0 {
+		return ""
+	}
+	t := styles.CurrentTheme()
+	view := l.rendered
+	lines := strings.Split(view, "\n")
+
+	start, end := l.viewPosition()
+	viewStart := max(0, start)
+	viewEnd := min(len(lines), end+1)
+	lines = lines[viewStart:viewEnd]
+	if l.resize {
+		return strings.Join(lines, "\n")
+	}
+	view = t.S().Base.
+		Height(l.height).
+		Width(l.width).
+		Render(strings.Join(lines, "\n"))
+
+	if !l.hasSelection() {
+		return view
+	}
+
+	return l.selectionView(view, false)
+}
+
 func (l *list[T]) viewPosition() (int, int) {
 	start, end := 0, 0
 	renderedLines := lipgloss.Height(l.rendered) - 1
@@ -1374,69 +1400,16 @@ func (l *list[T]) SelectParagraph(col, line int) {
 	l.selectionActive = false // Not actively selecting, just selected
 }
 
+// HasSelection returns whether there is an active selection.
+func (l *list[T]) HasSelection() bool {
+	return l.hasSelection()
+}
+
 // GetSelectedText returns the currently selected text.
 func (l *list[T]) GetSelectedText(paddingLeft int) string {
-	return ""
-	// if !l.hasSelection() {
-	// 	return ""
-	// }
-	//
-	// startLine := l.selectionStartLine
-	// endLine := l.selectionEndLine
-	// startCol := l.selectionStartCol
-	// endCol := l.selectionEndCol
-	//
-	// if l.direction == DirectionBackward {
-	// 	startLine = (lipgloss.Height(l.rendered) - 1) - startLine
-	// 	endLine = (lipgloss.Height(l.rendered) - 1) - endLine
-	// }
-	//
-	// if l.offset > 0 {
-	// 	if l.direction == DirectionBackward {
-	// 		startLine += l.offset
-	// 		endLine += l.offset
-	// 	} else {
-	// 		startLine -= l.offset
-	// 		endLine -= l.offset
-	// 	}
-	// }
-	//
-	// lines := strings.Split(l.rendered, "\n")
-	//
-	// if startLine < 0 || endLine < 0 || startLine >= len(lines) || endLine >= len(lines) {
-	// 	return ""
-	// }
-	//
-	// var result strings.Builder
-	// for i := range lines {
-	// 	lines[i] = ansi.Strip(lines[i])
-	// 	for _, icon := range styles.SelectionIgnoreIcons {
-	// 		lines[i] = strings.ReplaceAll(lines[i], icon, " ")
-	// 	}
-	//
-	// 	if i == startLine {
-	// 		if startCol < 0 || startCol >= len(lines[i]) {
-	// 			startCol = 0
-	// 		}
-	// 		if startCol < paddingLeft {
-	// 			startCol = paddingLeft
-	// 		}
-	// 		if i != endLine {
-	// 			endCol = len(lines[i])
-	// 		}
-	// 		result.WriteString(strings.TrimRightFunc(lines[i][startCol:endCol], unicode.IsSpace))
-	// 	} else if i > startLine && i < endLine {
-	// 		result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:], unicode.IsSpace))
-	// 	} else if i == endLine {
-	// 		if endCol < 0 || endCol >= len(lines[i]) {
-	// 			endCol = len(lines[i])
-	// 		}
-	// 		if endCol < paddingLeft {
-	// 			endCol = paddingLeft
-	// 		}
-	// 		result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:endCol], unicode.IsSpace))
-	// 	}
-	// }
-	//
-	// return result.String()
+	if !l.hasSelection() {
+		return ""
+	}
+
+	return l.selectionView(l.View(), true)
 }