@@ -1,7 +1,6 @@
11package list
22
33import (
4− "log/slog"
54 "slices"
65 "strings"
76 "sync"
@@ -15,6 +14,8 @@ import (
1514 "github.com/charmbracelet/crush/internal/tui/util"
1615 "github.com/charmbracelet/lipgloss/v2"
1716 uv "github.com/charmbracelet/ultraviolet"
17+ "github.com/charmbracelet/x/ansi"
18+ "github.com/rivo/uniseg"
1819)
1920
2021type Item interface {
@@ -50,6 +51,11 @@ type List[T Item] interface {
5051 AppendItem(T) tea.Cmd
5152 StartSelection(col, line int)
5253 EndSelection(col, line int)
54+ SelectionStop()
55+ SelectionClear()
56+ SelectWord(col, line int)
57+ SelectParagraph(col, line int)
58+ GetSelectedText(paddingLeft int) string
5359}
5460
5561type direction int
@@ -103,6 +109,8 @@ type list[T Item] struct {
103109 selectionStartLine int
104110 selectionEndCol int
105111 selectionEndLine int
112+
113+ selectionActive bool
106114}
107115
108116type ListOption func(*confOptions)
@@ -298,7 +306,8 @@ func (l *list[T]) View() string {
298306 Height(l.height).
299307 Width(l.width).
300308 Render(strings.Join(lines, "\n"))
301− if l.selectionStartCol < 0 {
309+
310+ if !l.hasSelection() {
302311 return view
303312 }
304313 area := uv.Rect(0, 0, l.width, l.height)
@@ -311,8 +320,8 @@ func (l *list[T]) View() string {
311320 }
312321 selArea = selArea.Canon()
313322
314− specialChars := make(map[string]bool, len(styles.AllIcons))
315− for _, icon := range styles.AllIcons {
323+ specialChars := make(map[string]bool, len(styles.SelectionIgnoreIcons))
324+ for _, icon := range styles.SelectionIgnoreIcons {
316325 specialChars[icon] = true
317326 }
318327
@@ -951,21 +960,67 @@ func (l *list[T]) decrementOffset(n int) {
951960
952961// MoveDown implements List.
953962func (l *list[T]) MoveDown(n int) tea.Cmd {
963+ oldOffset := l.offset
954964 if l.direction == DirectionForward {
955965 l.incrementOffset(n)
956966 } else {
957967 l.decrementOffset(n)
958968 }
969+
970+ if oldOffset == l.offset {
971+ // no change in offset, so no need to change selection
972+ return nil
973+ }
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
979+ } else {
980+ l.selectionStartLine -= n
981+ l.selectionEndLine -= n
982+ }
983+ }
984+ if l.selectionActive {
985+ if l.selectionStartLine < l.selectionEndLine {
986+ l.selectionStartLine -= n
987+ } else {
988+ l.selectionEndLine -= n
989+ }
990+ }
959991 return l.changeSelectionWhenScrolling()
960992}
961993
962994// MoveUp implements List.
963995func (l *list[T]) MoveUp(n int) tea.Cmd {
996+ oldOffset := l.offset
964997 if l.direction == DirectionForward {
965998 l.decrementOffset(n)
966999 } else {
9671000 l.incrementOffset(n)
9681001 }
1002+
1003+ if oldOffset == l.offset {
1004+ // no change in offset, so no need to change selection
1005+ return nil
1006+ }
1007+
1008+ if l.hasSelection() && !l.selectionActive {
1009+ if l.selectionStartLine > l.selectionEndLine {
1010+ l.selectionStartLine += n
1011+ l.selectionEndLine += n
1012+ } else {
1013+ l.selectionStartLine += n
1014+ l.selectionEndLine += n
1015+ }
1016+ }
1017+ if l.selectionActive {
1018+ if l.selectionStartLine > l.selectionEndLine {
1019+ l.selectionStartLine += n
1020+ } else {
1021+ l.selectionEndLine += n
1022+ }
1023+ }
9691024 return l.changeSelectionWhenScrolling()
9701025}
9711026
@@ -1164,18 +1219,224 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
11641219 return tea.Sequence(cmds...)
11651220}
11661221
1222+func (l *list[T]) hasSelection() bool {
1223+ return l.selectionEndCol != l.selectionStartCol || l.selectionEndLine != l.selectionStartLine
1224+}
1225+
11671226// StartSelection implements List.
11681227func (l *list[T]) StartSelection(col, line int) {
11691228 l.selectionStartCol = col
11701229 l.selectionStartLine = line
11711230 l.selectionEndCol = col
11721231 l.selectionEndLine = line
1173− slog.Info("Position", "col", col, "line", line)
1232+ l.selectionActive = true
11741233}
11751234
11761235// EndSelection implements List.
11771236func (l *list[T]) EndSelection(col, line int) {
1237+ if !l.selectionActive {
1238+ return
1239+ }
11781240 l.selectionEndCol = col
11791241 l.selectionEndLine = line
1180− slog.Info("Position", "col", col, "line", line)
1242+}
1243+
1244+func (l *list[T]) SelectionStop() {
1245+ l.selectionActive = false
1246+}
1247+
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
1254+}
1255+
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)
1260+ }
1261+
1262+ if l.direction == DirectionBackward {
1263+ line = ((len(lines) - 1) - l.height) + line + 1
1264+ }
1265+
1266+ if l.offset > 0 {
1267+ if l.direction == DirectionBackward {
1268+ line -= l.offset
1269+ } else {
1270+ line += l.offset
1271+ }
1272+ }
1273+
1274+ currentLine := lines[line]
1275+ gr := uniseg.NewGraphemes(currentLine)
1276+ startCol = -1
1277+ upTo := col
1278+ for gr.Next() {
1279+ if gr.IsWordBoundary() && upTo > 0 {
1280+ startCol = col - upTo + 1
1281+ } else if gr.IsWordBoundary() && upTo < 0 {
1282+ endCol = col - upTo + 1
1283+ break
1284+ }
1285+ if upTo == 0 && gr.Str() == " " {
1286+ return 0, 0
1287+ }
1288+ upTo -= 1
1289+ }
1290+ if startCol == -1 {
1291+ return 0, 0
1292+ }
1293+ return
1294+}
1295+
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, " ")
1302+ }
1303+ }
1304+ if l.direction == DirectionBackward {
1305+ line = (len(lines) - 1) - l.height + line + 1
1306+ }
1307+
1308+ if strings.TrimSpace(lines[line]) == "" {
1309+ return 0, 0, false
1310+ }
1311+
1312+ if l.offset > 0 {
1313+ if l.direction == DirectionBackward {
1314+ line -= l.offset
1315+ } else {
1316+ line += l.offset
1317+ }
1318+ }
1319+
1320+ // Ensure line is within bounds
1321+ if line < 0 || line >= len(lines) {
1322+ return 0, 0, false
1323+ }
1324+
1325+ // Find start of paragraph (search backwards for empty line or start of text)
1326+ startLine = line
1327+ for startLine > 0 && strings.TrimSpace(lines[startLine-1]) != "" {
1328+ startLine--
1329+ }
1330+
1331+ // Find end of paragraph (search forwards for empty line or end of text)
1332+ endLine = line
1333+ for endLine < len(lines)-1 && strings.TrimSpace(lines[endLine+1]) != "" {
1334+ endLine++
1335+ }
1336+
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
1341+ }
1342+ if l.offset > 0 {
1343+ if l.direction == DirectionBackward {
1344+ startLine += l.offset
1345+ endLine += l.offset
1346+ } else {
1347+ startLine -= l.offset
1348+ endLine -= l.offset
1349+ }
1350+ }
1351+ return startLine, endLine, true
1352+}
1353+
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
1362+}
1363+
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)
1367+ if !found {
1368+ return
1369+ }
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
1375+}
1376+
1377+// GetSelectedText returns the currently selected text.
1378+func (l *list[T]) GetSelectedText(paddingLeft int) string {
1379+ return ""
1380+ // if !l.hasSelection() {
1381+ // return ""
1382+ // }
1383+ //
1384+ // startLine := l.selectionStartLine
1385+ // endLine := l.selectionEndLine
1386+ // startCol := l.selectionStartCol
1387+ // endCol := l.selectionEndCol
1388+ //
1389+ // if l.direction == DirectionBackward {
1390+ // startLine = (lipgloss.Height(l.rendered) - 1) - startLine
1391+ // endLine = (lipgloss.Height(l.rendered) - 1) - endLine
1392+ // }
1393+ //
1394+ // if l.offset > 0 {
1395+ // if l.direction == DirectionBackward {
1396+ // startLine += l.offset
1397+ // endLine += l.offset
1398+ // } else {
1399+ // startLine -= l.offset
1400+ // endLine -= l.offset
1401+ // }
1402+ // }
1403+ //
1404+ // lines := strings.Split(l.rendered, "\n")
1405+ //
1406+ // if startLine < 0 || endLine < 0 || startLine >= len(lines) || endLine >= len(lines) {
1407+ // return ""
1408+ // }
1409+ //
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, " ")
1415+ // }
1416+ //
1417+ // if i == startLine {
1418+ // if startCol < 0 || startCol >= len(lines[i]) {
1419+ // startCol = 0
1420+ // }
1421+ // if startCol < paddingLeft {
1422+ // startCol = paddingLeft
1423+ // }
1424+ // if i != endLine {
1425+ // endCol = len(lines[i])
1426+ // }
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])
1433+ // }
1434+ // if endCol < paddingLeft {
1435+ // endCol = paddingLeft
1436+ // }
1437+ // result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:endCol], unicode.IsSpace))
1438+ // }
1439+ // }
1440+ //
1441+ // return result.String()
11811442}