@@ -1,7 +1,6 @@
1package list
1package list
@@ -15,6 +14,8 @@ import (
15 "github.com/charmbracelet/crush/internal/tui/util"
14 "github.com/charmbracelet/crush/internal/tui/util"
16 "github.com/charmbracelet/lipgloss/v2"
15 "github.com/charmbracelet/lipgloss/v2"
17 uv "github.com/charmbracelet/ultraviolet"
16 uv "github.com/charmbracelet/ultraviolet"
17+ "github.com/charmbracelet/x/ansi"
18+ "github.com/rivo/uniseg"
20type Item interface {
21type Item interface {
@@ -50,6 +51,11 @@ type List[T Item] interface {
50 AppendItem(T) tea.Cmd
51 AppendItem(T) tea.Cmd
51 StartSelection(col, line int)
52 StartSelection(col, line int)
52 EndSelection(col, line int)
53 EndSelection(col, line int)
56+ SelectWord(col, line int)
57+ SelectParagraph(col, line int)
58+ GetSelectedText(paddingLeft int) string
55type direction int
61type direction int
@@ -103,6 +109,8 @@ type list[T Item] struct {
103 selectionStartLine int
109 selectionStartLine int
104 selectionEndCol int
110 selectionEndCol int
105 selectionEndLine int
111 selectionEndLine int
113+ selectionActive bool
108type ListOption func(*confOptions)
116type ListOption func(*confOptions)
@@ -298,7 +306,8 @@ func (l *list[T]) View() string {
298 Height(l.height).
306 Height(l.height).
299 Width(l.width).
307 Width(l.width).
300 Render(strings.Join(lines, "\n"))
308 Render(strings.Join(lines, "\n"))
301− if l.selectionStartCol < 0 {
309+
310+ if !l.hasSelection() {
302 return view
311 return view
304 area := uv.Rect(0, 0, l.width, l.height)
313 area := uv.Rect(0, 0, l.width, l.height)
@@ -311,8 +320,8 @@ func (l *list[T]) View() string {
312 selArea = selArea.Canon()
321 selArea = selArea.Canon()
314− specialChars := make(map[string]bool, len(styles.AllIcons))
323+ specialChars := make(map[string]bool, len(styles.SelectionIgnoreIcons))
315− for _, icon := range styles.AllIcons {
324+ for _, icon := range styles.SelectionIgnoreIcons {
316 specialChars[icon] = true
325 specialChars[icon] = true
@@ -951,21 +960,67 @@ func (l *list[T]) decrementOffset(n int) {
952// MoveDown implements List.
961// MoveDown implements List.
953func (l *list[T]) MoveDown(n int) tea.Cmd {
962func (l *list[T]) MoveDown(n int) tea.Cmd {
963+ oldOffset := l.offset
954 if l.direction == DirectionForward {
964 if l.direction == DirectionForward {
955 l.incrementOffset(n)
965 l.incrementOffset(n)
957 l.decrementOffset(n)
967 l.decrementOffset(n)
970+ if oldOffset == l.offset {
971+ // no change in offset, so no need to change selection
974+ // if we are not actively selecting move the whole selection down
975+ if l.hasSelection() && !l.selectionActive {
976+ if l.selectionStartLine < l.selectionEndLine {
977+ l.selectionStartLine -= n
978+ l.selectionEndLine -= n
980+ l.selectionStartLine -= n
981+ l.selectionEndLine -= n
984+ if l.selectionActive {
985+ if l.selectionStartLine < l.selectionEndLine {
986+ l.selectionStartLine -= n
988+ l.selectionEndLine -= n
959 return l.changeSelectionWhenScrolling()
991 return l.changeSelectionWhenScrolling()
962// MoveUp implements List.
994// MoveUp implements List.
963func (l *list[T]) MoveUp(n int) tea.Cmd {
995func (l *list[T]) MoveUp(n int) tea.Cmd {
996+ oldOffset := l.offset
964 if l.direction == DirectionForward {
997 if l.direction == DirectionForward {
965 l.decrementOffset(n)
998 l.decrementOffset(n)
967 l.incrementOffset(n)
1000 l.incrementOffset(n)
1003+ if oldOffset == l.offset {
1004+ // no change in offset, so no need to change selection
1008+ if l.hasSelection() && !l.selectionActive {
1009+ if l.selectionStartLine > l.selectionEndLine {
1010+ l.selectionStartLine += n
1011+ l.selectionEndLine += n
1013+ l.selectionStartLine += n
1014+ l.selectionEndLine += n
1017+ if l.selectionActive {
1018+ if l.selectionStartLine > l.selectionEndLine {
1019+ l.selectionStartLine += n
1021+ l.selectionEndLine += n
969 return l.changeSelectionWhenScrolling()
1024 return l.changeSelectionWhenScrolling()
@@ -1164,18 +1219,224 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
1164 return tea.Sequence(cmds...)
1219 return tea.Sequence(cmds...)
1222+func (l *list[T]) hasSelection() bool {
1223+ return l.selectionEndCol != l.selectionStartCol || l.selectionEndLine != l.selectionStartLine
1167// StartSelection implements List.
1226// StartSelection implements List.
1168func (l *list[T]) StartSelection(col, line int) {
1227func (l *list[T]) StartSelection(col, line int) {
1169 l.selectionStartCol = col
1228 l.selectionStartCol = col
1170 l.selectionStartLine = line
1229 l.selectionStartLine = line
1171 l.selectionEndCol = col
1230 l.selectionEndCol = col
1172 l.selectionEndLine = line
1231 l.selectionEndLine = line
1173− slog.Info("Position", "col", col, "line", line)
1232+ l.selectionActive = true
1176// EndSelection implements List.
1235// EndSelection implements List.
1177func (l *list[T]) EndSelection(col, line int) {
1236func (l *list[T]) EndSelection(col, line int) {
1237+ if !l.selectionActive {
1178 l.selectionEndCol = col
1240 l.selectionEndCol = col
1179 l.selectionEndLine = line
1241 l.selectionEndLine = line
1180− slog.Info("Position", "col", col, "line", line)
1242+}
1244+func (l *list[T]) SelectionStop() {
1245+ l.selectionActive = false
1248+func (l *list[T]) SelectionClear() {
1249+ l.selectionStartCol = -1
1250+ l.selectionStartLine = -1
1251+ l.selectionEndCol = -1
1252+ l.selectionEndLine = -1
1253+ l.selectionActive = false
1256+func (l *list[T]) findWordBoundaries(col, line int) (startCol, endCol int) {
1257+ lines := strings.Split(l.rendered, "\n")
1258+ for i, l := range lines {
1259+ lines[i] = ansi.Strip(l)
1262+ if l.direction == DirectionBackward {
1263+ line = ((len(lines) - 1) - l.height) + line + 1
1267+ if l.direction == DirectionBackward {
1274+ currentLine := lines[line]
1275+ gr := uniseg.NewGraphemes(currentLine)
1279+ if gr.IsWordBoundary() && upTo > 0 {
1280+ startCol = col - upTo + 1
1281+ } else if gr.IsWordBoundary() && upTo < 0 {
1282+ endCol = col - upTo + 1
1285+ if upTo == 0 && gr.Str() == " " {
1290+ if startCol == -1 {
1296+func (l *list[T]) findParagraphBoundaries(line int) (startLine, endLine int, found bool) {
1297+ lines := strings.Split(l.rendered, "\n")
1298+ for i, l := range lines {
1299+ lines[i] = ansi.Strip(l)
1300+ for _, icon := range styles.SelectionIgnoreIcons {
1301+ lines[i] = strings.ReplaceAll(lines[i], icon, " ")
1304+ if l.direction == DirectionBackward {
1305+ line = (len(lines) - 1) - l.height + line + 1
1308+ if strings.TrimSpace(lines[line]) == "" {
1313+ if l.direction == DirectionBackward {
1320+ // Ensure line is within bounds
1321+ if line < 0 || line >= len(lines) {
1325+ // Find start of paragraph (search backwards for empty line or start of text)
1327+ for startLine > 0 && strings.TrimSpace(lines[startLine-1]) != "" {
1331+ // Find end of paragraph (search forwards for empty line or end of text)
1333+ for endLine < len(lines)-1 && strings.TrimSpace(lines[endLine+1]) != "" {
1337+ // revert the line numbers if we are in backward direction
1338+ if l.direction == DirectionBackward {
1339+ startLine = startLine - (len(lines) - 1) + l.height - 1
1340+ endLine = endLine - (len(lines) - 1) + l.height - 1
1343+ if l.direction == DirectionBackward {
1344+ startLine += l.offset
1345+ endLine += l.offset
1347+ startLine -= l.offset
1348+ endLine -= l.offset
1351+ return startLine, endLine, true
1354+// SelectWord selects the word at the given position.
1355+func (l *list[T]) SelectWord(col, line int) {
1356+ startCol, endCol := l.findWordBoundaries(col, line)
1357+ l.selectionStartCol = startCol
1358+ l.selectionStartLine = line
1359+ l.selectionEndCol = endCol
1360+ l.selectionEndLine = line
1361+ l.selectionActive = false // Not actively selecting, just selected
1364+// SelectParagraph selects the paragraph at the given position.
1365+func (l *list[T]) SelectParagraph(col, line int) {
1366+ startLine, endLine, found := l.findParagraphBoundaries(line)
1370+ l.selectionStartCol = 0
1371+ l.selectionStartLine = startLine
1372+ l.selectionEndCol = l.width - 1
1373+ l.selectionEndLine = endLine
1374+ l.selectionActive = false // Not actively selecting, just selected
1377+// GetSelectedText returns the currently selected text.
1378+func (l *list[T]) GetSelectedText(paddingLeft int) string {
1380+ // if !l.hasSelection() {
1384+ // startLine := l.selectionStartLine
1385+ // endLine := l.selectionEndLine
1386+ // startCol := l.selectionStartCol
1387+ // endCol := l.selectionEndCol
1389+ // if l.direction == DirectionBackward {
1390+ // startLine = (lipgloss.Height(l.rendered) - 1) - startLine
1391+ // endLine = (lipgloss.Height(l.rendered) - 1) - endLine
1394+ // if l.offset > 0 {
1395+ // if l.direction == DirectionBackward {
1396+ // startLine += l.offset
1397+ // endLine += l.offset
1399+ // startLine -= l.offset
1400+ // endLine -= l.offset
1404+ // lines := strings.Split(l.rendered, "\n")
1406+ // if startLine < 0 || endLine < 0 || startLine >= len(lines) || endLine >= len(lines) {
1410+ // var result strings.Builder
1411+ // for i := range lines {
1412+ // lines[i] = ansi.Strip(lines[i])
1413+ // for _, icon := range styles.SelectionIgnoreIcons {
1414+ // lines[i] = strings.ReplaceAll(lines[i], icon, " ")
1417+ // if i == startLine {
1418+ // if startCol < 0 || startCol >= len(lines[i]) {
1421+ // if startCol < paddingLeft {
1422+ // startCol = paddingLeft
1424+ // if i != endLine {
1425+ // endCol = len(lines[i])
1427+ // result.WriteString(strings.TrimRightFunc(lines[i][startCol:endCol], unicode.IsSpace))
1428+ // } else if i > startLine && i < endLine {
1429+ // result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:], unicode.IsSpace))
1430+ // } else if i == endLine {
1431+ // if endCol < 0 || endCol >= len(lines[i]) {
1432+ // endCol = len(lines[i])
1434+ // if endCol < paddingLeft {
1435+ // endCol = paddingLeft
1437+ // result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:endCol], unicode.IsSpace))
1441+ // return result.String()