mirror of
https://github.com/charmbracelet/crush.git
synced 2025-08-02 05:20:46 +03:00
wip: add to messages list
This commit is contained in:
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/charmbracelet/crush/internal/session"
|
||||
"github.com/charmbracelet/crush/internal/tui/components/chat/messages"
|
||||
"github.com/charmbracelet/crush/internal/tui/components/core/layout"
|
||||
"github.com/charmbracelet/crush/internal/tui/components/core/list"
|
||||
"github.com/charmbracelet/crush/internal/tui/exp/list"
|
||||
"github.com/charmbracelet/crush/internal/tui/styles"
|
||||
"github.com/charmbracelet/crush/internal/tui/util"
|
||||
)
|
||||
@@ -49,8 +49,8 @@ type messageListCmp struct {
|
||||
app *app.App
|
||||
width, height int
|
||||
session session.Session
|
||||
listCmp list.ListModel
|
||||
previousSelected int // Last selected item index for restoring focus
|
||||
listCmp list.List[list.Item]
|
||||
previousSelected string // Last selected item index for restoring focus
|
||||
|
||||
lastUserMessageTime int64
|
||||
defaultListKeyMap list.KeyMap
|
||||
@@ -61,14 +61,15 @@ type messageListCmp struct {
|
||||
func New(app *app.App) MessageListCmp {
|
||||
defaultListKeyMap := list.DefaultKeyMap()
|
||||
listCmp := list.New(
|
||||
list.WithGapSize(1),
|
||||
list.WithReverse(true),
|
||||
[]list.Item{},
|
||||
list.WithGap(1),
|
||||
list.WithDirection(list.Backward),
|
||||
list.WithKeyMap(defaultListKeyMap),
|
||||
)
|
||||
return &messageListCmp{
|
||||
app: app,
|
||||
listCmp: listCmp,
|
||||
previousSelected: list.NoSelection,
|
||||
previousSelected: "",
|
||||
defaultListKeyMap: defaultListKeyMap,
|
||||
}
|
||||
}
|
||||
@@ -89,7 +90,7 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
case SessionClearedMsg:
|
||||
m.session = session.Session{}
|
||||
return m, m.listCmp.SetItems([]util.Model{})
|
||||
return m, m.listCmp.SetItems([]list.Item{})
|
||||
|
||||
case pubsub.Event[message.Message]:
|
||||
cmd := m.handleMessageEvent(msg)
|
||||
@@ -97,7 +98,7 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
default:
|
||||
var cmds []tea.Cmd
|
||||
u, cmd := m.listCmp.Update(msg)
|
||||
m.listCmp = u.(list.ListModel)
|
||||
m.listCmp = u.(list.List[list.Item])
|
||||
cmds = append(cmds, cmd)
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
@@ -169,7 +170,7 @@ func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message])
|
||||
|
||||
toolCall.SetNestedToolCalls(nestedToolCalls)
|
||||
m.listCmp.UpdateItem(
|
||||
toolCallInx,
|
||||
toolCall.ID(),
|
||||
toolCall,
|
||||
)
|
||||
return tea.Batch(cmds...)
|
||||
@@ -233,7 +234,7 @@ func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
|
||||
if toolCallIndex := m.findToolCallByID(items, tr.ToolCallID); toolCallIndex != NotFound {
|
||||
toolCall := items[toolCallIndex].(messages.ToolCallCmp)
|
||||
toolCall.SetToolResult(tr)
|
||||
m.listCmp.UpdateItem(toolCallIndex, toolCall)
|
||||
m.listCmp.UpdateItem(toolCall.ID(), toolCall)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -241,7 +242,7 @@ func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
|
||||
|
||||
// findToolCallByID searches for a tool call with the specified ID.
|
||||
// Returns the index if found, NotFound otherwise.
|
||||
func (m *messageListCmp) findToolCallByID(items []util.Model, toolCallID string) int {
|
||||
func (m *messageListCmp) findToolCallByID(items []list.Item, toolCallID string) int {
|
||||
// Search backwards as tool calls are more likely to be recent
|
||||
for i := len(items) - 1; i >= 0; i-- {
|
||||
if toolCall, ok := items[i].(messages.ToolCallCmp); ok && toolCall.GetToolCall().ID == toolCallID {
|
||||
@@ -274,7 +275,7 @@ func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.C
|
||||
}
|
||||
|
||||
// findAssistantMessageAndToolCalls locates the assistant message and its tool calls.
|
||||
func (m *messageListCmp) findAssistantMessageAndToolCalls(items []util.Model, messageID string) (int, map[int]messages.ToolCallCmp) {
|
||||
func (m *messageListCmp) findAssistantMessageAndToolCalls(items []list.Item, messageID string) (int, map[int]messages.ToolCallCmp) {
|
||||
assistantIndex := NotFound
|
||||
toolCalls := make(map[int]messages.ToolCallCmp)
|
||||
|
||||
@@ -310,7 +311,7 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
|
||||
uiMsg := items[assistantIndex].(messages.MessageCmp)
|
||||
uiMsg.SetMessage(msg)
|
||||
m.listCmp.UpdateItem(
|
||||
assistantIndex,
|
||||
items[assistantIndex].ID(),
|
||||
uiMsg,
|
||||
)
|
||||
if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
|
||||
@@ -322,7 +323,8 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
|
||||
)
|
||||
}
|
||||
} else if hasToolCallsOnly {
|
||||
m.listCmp.DeleteItem(assistantIndex)
|
||||
items := m.listCmp.Items()
|
||||
m.listCmp.DeleteItem(items[assistantIndex].ID())
|
||||
}
|
||||
|
||||
return cmd
|
||||
@@ -349,13 +351,13 @@ func (m *messageListCmp) updateToolCalls(msg message.Message, existingToolCalls
|
||||
// updateOrAddToolCall updates an existing tool call or adds a new one.
|
||||
func (m *messageListCmp) updateOrAddToolCall(msg message.Message, tc message.ToolCall, existingToolCalls map[int]messages.ToolCallCmp) tea.Cmd {
|
||||
// Try to find existing tool call
|
||||
for index, existingTC := range existingToolCalls {
|
||||
for _, existingTC := range existingToolCalls {
|
||||
if tc.ID == existingTC.GetToolCall().ID {
|
||||
existingTC.SetToolCall(tc)
|
||||
if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonCanceled {
|
||||
existingTC.SetCancelled()
|
||||
}
|
||||
m.listCmp.UpdateItem(index, existingTC)
|
||||
m.listCmp.UpdateItem(tc.ID, existingTC)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -400,7 +402,7 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
|
||||
}
|
||||
|
||||
if len(sessionMessages) == 0 {
|
||||
return m.listCmp.SetItems([]util.Model{})
|
||||
return m.listCmp.SetItems([]list.Item{})
|
||||
}
|
||||
|
||||
// Initialize with first message timestamp
|
||||
@@ -427,8 +429,8 @@ func (m *messageListCmp) buildToolResultMap(messages []message.Message) map[stri
|
||||
}
|
||||
|
||||
// convertMessagesToUI converts database messages to UI components.
|
||||
func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message, toolResultMap map[string]message.ToolResult) []util.Model {
|
||||
uiMessages := make([]util.Model, 0)
|
||||
func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message, toolResultMap map[string]message.ToolResult) []list.Item {
|
||||
uiMessages := make([]list.Item, 0)
|
||||
|
||||
for _, msg := range sessionMessages {
|
||||
switch msg.Role {
|
||||
@@ -447,8 +449,8 @@ func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message,
|
||||
}
|
||||
|
||||
// convertAssistantMessage converts an assistant message and its tool calls to UI components.
|
||||
func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResultMap map[string]message.ToolResult) []util.Model {
|
||||
var uiMessages []util.Model
|
||||
func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResultMap map[string]message.ToolResult) []list.Item {
|
||||
var uiMessages []list.Item
|
||||
|
||||
// Add assistant message if it should be displayed
|
||||
if m.shouldShowAssistantMessage(msg) {
|
||||
|
||||
@@ -11,13 +11,14 @@ import (
|
||||
"github.com/charmbracelet/catwalk/pkg/catwalk"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/charmbracelet/crush/internal/config"
|
||||
"github.com/charmbracelet/crush/internal/message"
|
||||
"github.com/charmbracelet/crush/internal/tui/components/anim"
|
||||
"github.com/charmbracelet/crush/internal/tui/components/core"
|
||||
"github.com/charmbracelet/crush/internal/tui/components/core/layout"
|
||||
"github.com/charmbracelet/crush/internal/tui/components/core/list"
|
||||
"github.com/charmbracelet/crush/internal/tui/exp/list"
|
||||
"github.com/charmbracelet/crush/internal/tui/styles"
|
||||
"github.com/charmbracelet/crush/internal/tui/util"
|
||||
)
|
||||
@@ -31,6 +32,7 @@ type MessageCmp interface {
|
||||
GetMessage() message.Message // Access to underlying message data
|
||||
SetMessage(msg message.Message) // Update the message content
|
||||
Spinning() bool // Animation state for loading messages
|
||||
ID() string
|
||||
}
|
||||
|
||||
// messageCmp implements the MessageCmp interface for displaying chat messages.
|
||||
@@ -333,19 +335,25 @@ func (m *messageCmp) Spinning() bool {
|
||||
}
|
||||
|
||||
type AssistantSection interface {
|
||||
util.Model
|
||||
list.Item
|
||||
layout.Sizeable
|
||||
list.SectionHeader
|
||||
}
|
||||
type assistantSectionModel struct {
|
||||
width int
|
||||
id string
|
||||
message message.Message
|
||||
lastUserMessageTime time.Time
|
||||
}
|
||||
|
||||
// ID implements AssistantSection.
|
||||
func (m *assistantSectionModel) ID() string {
|
||||
return m.id
|
||||
}
|
||||
|
||||
func NewAssistantSection(message message.Message, lastUserMessageTime time.Time) AssistantSection {
|
||||
return &assistantSectionModel{
|
||||
width: 0,
|
||||
id: uuid.NewString(),
|
||||
message: message,
|
||||
lastUserMessageTime: lastUserMessageTime,
|
||||
}
|
||||
@@ -392,3 +400,7 @@ func (m *assistantSectionModel) SetSize(width int, height int) tea.Cmd {
|
||||
func (m *assistantSectionModel) IsSectionHeader() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *messageCmp) ID() string {
|
||||
return m.message.ID
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ type ToolCallCmp interface {
|
||||
GetNestedToolCalls() []ToolCallCmp // Get nested tool calls
|
||||
SetNestedToolCalls([]ToolCallCmp) // Set nested tool calls
|
||||
SetIsNested(bool) // Set whether this tool call is nested
|
||||
ID() string
|
||||
}
|
||||
|
||||
// toolCallCmp implements the ToolCallCmp interface for displaying tool calls.
|
||||
@@ -311,3 +312,7 @@ func (m *toolCallCmp) Spinning() bool {
|
||||
}
|
||||
return m.spinning
|
||||
}
|
||||
|
||||
func (m *toolCallCmp) ID() string {
|
||||
return m.call.ID
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ type filterableOptions struct {
|
||||
}
|
||||
type filterableList[T FilterableItem] struct {
|
||||
*list[T]
|
||||
filterableOptions
|
||||
*filterableOptions
|
||||
width, height int
|
||||
// stores all available items
|
||||
items []T
|
||||
@@ -83,13 +83,13 @@ func NewFilterableList[T FilterableItem](items []T, opts ...filterableListOption
|
||||
t := styles.CurrentTheme()
|
||||
|
||||
f := &filterableList[T]{
|
||||
filterableOptions: filterableOptions{
|
||||
filterableOptions: &filterableOptions{
|
||||
inputStyle: t.S().Base,
|
||||
placeholder: "Type to filter",
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&f.filterableOptions)
|
||||
opt(f.filterableOptions)
|
||||
}
|
||||
f.list = New[T](items, f.listOptions...).(*list[T])
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestFilterableList(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("should create simple filterable list", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []FilterableItem{}
|
||||
|
||||
@@ -61,3 +61,16 @@ func DefaultKeyMap() KeyMap {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (k KeyMap) KeyBindings() []key.Binding {
|
||||
return []key.Binding{
|
||||
k.Down,
|
||||
k.Up,
|
||||
k.DownOneItem,
|
||||
k.UpOneItem,
|
||||
k.HalfPageDown,
|
||||
k.HalfPageUp,
|
||||
k.Home,
|
||||
k.End,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
@@ -29,6 +30,11 @@ type List[T Item] interface {
|
||||
SetItems([]T) tea.Cmd
|
||||
SetSelected(string) tea.Cmd
|
||||
SelectedItem() *T
|
||||
Items() []T
|
||||
UpdateItem(string, T)
|
||||
DeleteItem(string)
|
||||
PrependItem(T) tea.Cmd
|
||||
AppendItem(T) tea.Cmd
|
||||
}
|
||||
|
||||
type direction int
|
||||
@@ -63,7 +69,7 @@ type confOptions struct {
|
||||
selectedItem string
|
||||
}
|
||||
type list[T Item] struct {
|
||||
confOptions
|
||||
*confOptions
|
||||
|
||||
focused bool
|
||||
offset int
|
||||
@@ -118,14 +124,14 @@ func WithWrapNavigation() listOption {
|
||||
|
||||
func New[T Item](items []T, opts ...listOption) List[T] {
|
||||
list := &list[T]{
|
||||
confOptions: confOptions{
|
||||
confOptions: &confOptions{
|
||||
direction: Forward,
|
||||
keyMap: DefaultKeyMap(),
|
||||
},
|
||||
items: items,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&list.confOptions)
|
||||
opt(list.confOptions)
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -343,6 +349,7 @@ func (l *list[T]) firstSelectableItemAfter(inx int) int {
|
||||
return NotFound
|
||||
}
|
||||
|
||||
// moveToSelected needs to be called after the view is rendered
|
||||
func (l *list[T]) moveToSelected(center bool) tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
if l.selectedItem == "" || !l.isReady {
|
||||
@@ -421,8 +428,8 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
|
||||
break
|
||||
}
|
||||
}
|
||||
l.moveToSelected(false)
|
||||
l.renderView()
|
||||
l.moveToSelected(false)
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -457,8 +464,8 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
l.moveToSelected(false)
|
||||
l.renderView()
|
||||
l.moveToSelected(false)
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -605,10 +612,10 @@ func (l *list[T]) SetItems(items []T) tea.Cmd {
|
||||
cmds = append(cmds, item.SetSize(l.width, 0))
|
||||
}
|
||||
|
||||
cmds = append(cmds, l.renderItems())
|
||||
if l.selectedItem != "" {
|
||||
cmds = append(cmds, l.moveToSelected(true))
|
||||
}
|
||||
cmds = append(cmds, l.renderItems())
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -691,8 +698,8 @@ func (l *list[T]) SetSelected(id string) tea.Cmd {
|
||||
}
|
||||
}
|
||||
l.selectedItem = id
|
||||
cmds = append(cmds, l.moveToSelected(true))
|
||||
l.renderView()
|
||||
cmds = append(cmds, l.moveToSelected(true))
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -709,3 +716,66 @@ func (l *list[T]) SelectedItem() *T {
|
||||
func (l *list[T]) IsFocused() bool {
|
||||
return l.focused
|
||||
}
|
||||
|
||||
func (l *list[T]) Items() []T {
|
||||
return l.items
|
||||
}
|
||||
|
||||
func (l *list[T]) UpdateItem(id string, item T) {
|
||||
// TODO: preserve offset
|
||||
for inx, item := range l.items {
|
||||
if item.ID() == id {
|
||||
l.items[inx] = item
|
||||
l.renderedItems[inx] = l.renderItem(item)
|
||||
l.renderView()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *list[T]) DeleteItem(id string) {
|
||||
// TODO: preserve offset
|
||||
inx := NotFound
|
||||
for i, item := range l.items {
|
||||
if item.ID() == id {
|
||||
inx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.items = slices.Delete(l.items, inx, inx+1)
|
||||
l.renderedItems = slices.Delete(l.renderedItems, inx, inx+1)
|
||||
l.renderView()
|
||||
}
|
||||
|
||||
func (l *list[T]) PrependItem(item T) tea.Cmd {
|
||||
// TODO: preserve offset
|
||||
var cmd tea.Cmd
|
||||
l.items = append([]T{item}, l.items...)
|
||||
l.renderedItems = append([]renderedItem{l.renderItem(item)}, l.renderedItems...)
|
||||
if len(l.items) == 1 {
|
||||
cmd = l.SetSelected(item.ID())
|
||||
}
|
||||
// the viewport did not move and the last item was focused
|
||||
if l.direction == Backward && l.offset == 0 && l.selectedItem == l.items[0].ID() {
|
||||
cmd = l.SetSelected(item.ID())
|
||||
}
|
||||
l.renderView()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (l *list[T]) AppendItem(item T) tea.Cmd {
|
||||
// TODO: preserve offset
|
||||
var cmd tea.Cmd
|
||||
l.items = append(l.items, item)
|
||||
l.renderedItems = append(l.renderedItems, l.renderItem(item))
|
||||
if len(l.items) == 1 {
|
||||
cmd = l.SetSelected(item.ID())
|
||||
} else if l.direction == Backward && l.offset == 0 && l.selectedItem == l.items[len(l.items)-2].ID() {
|
||||
// the viewport did not move and the last item was focused
|
||||
cmd = l.SetSelected(item.ID())
|
||||
} else {
|
||||
l.renderView()
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func TestListPosition(t *testing.T) {
|
||||
t.Parallel()
|
||||
type positionOffsetTest struct {
|
||||
dir direction
|
||||
test string
|
||||
@@ -75,6 +76,7 @@ func TestListPosition(t *testing.T) {
|
||||
}
|
||||
for _, c := range tests {
|
||||
t.Run(c.test, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []Item{}
|
||||
for i := range c.numItems {
|
||||
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
|
||||
@@ -101,6 +103,7 @@ func TestListPosition(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBackwardList(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("within height", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []Item{}
|
||||
@@ -291,6 +294,7 @@ func TestBackwardList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestForwardList(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("within height", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []Item{}
|
||||
@@ -482,6 +486,7 @@ func TestForwardList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListSelection(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("should skip none selectable items initially", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []Item{}
|
||||
@@ -553,6 +558,7 @@ func TestListSelection(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListSetSelection(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("should move to the selected item", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []Item{}
|
||||
@@ -577,6 +583,55 @@ func TestListSetSelection(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestListChanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("should append an item to the end", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []SelectableItem{}
|
||||
for i := range 20 {
|
||||
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
|
||||
items = append(items, item)
|
||||
}
|
||||
l := New(items, WithDirection(Backward)).(*list[SelectableItem])
|
||||
l.SetSize(100, 10)
|
||||
cmd := l.Init()
|
||||
if cmd != nil {
|
||||
cmd()
|
||||
}
|
||||
|
||||
newItem := NewSelectableItem("New Item")
|
||||
l.AppendItem(newItem)
|
||||
|
||||
assert.Equal(t, 21, len(l.items))
|
||||
assert.Equal(t, 21, len(l.renderedItems))
|
||||
assert.Equal(t, newItem.ID(), l.selectedItem)
|
||||
golden.RequireEqual(t, []byte(l.View()))
|
||||
})
|
||||
t.Run("should should not change the selected if we moved the offset", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
items := []SelectableItem{}
|
||||
for i := range 20 {
|
||||
item := NewSelectableItem(fmt.Sprintf("Item %d\nLine2", i))
|
||||
items = append(items, item)
|
||||
}
|
||||
l := New(items, WithDirection(Backward)).(*list[SelectableItem])
|
||||
l.SetSize(100, 10)
|
||||
cmd := l.Init()
|
||||
if cmd != nil {
|
||||
cmd()
|
||||
}
|
||||
l.MoveUp(1)
|
||||
|
||||
newItem := NewSelectableItem("New Item")
|
||||
l.AppendItem(newItem)
|
||||
|
||||
assert.Equal(t, 21, len(l.items))
|
||||
assert.Equal(t, 21, len(l.renderedItems))
|
||||
assert.Equal(t, l.items[19].ID(), l.selectedItem)
|
||||
golden.RequireEqual(t, []byte(l.View()))
|
||||
})
|
||||
}
|
||||
|
||||
type SelectableItem interface {
|
||||
Item
|
||||
layout.Focusable
|
||||
|
||||
10
internal/tui/exp/list/testdata/TestListChanges/should_append_an_item_to_the_end.golden
generated
vendored
Normal file
10
internal/tui/exp/list/testdata/TestListChanges/should_append_an_item_to_the_end.golden
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
Item 11
|
||||
Item 12
|
||||
Item 13
|
||||
Item 14
|
||||
Item 15
|
||||
Item 16
|
||||
Item 17
|
||||
Item 18
|
||||
Item 19
|
||||
│New Item
|
||||
10
internal/tui/exp/list/testdata/TestListChanges/should_should_not_change_the_selected_if_we_moved_the_offset.golden
generated
vendored
Normal file
10
internal/tui/exp/list/testdata/TestListChanges/should_should_not_change_the_selected_if_we_moved_the_offset.golden
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
Item 15
|
||||
Line2
|
||||
Item 16
|
||||
Line2
|
||||
Item 17
|
||||
Line2
|
||||
Item 18
|
||||
Line2
|
||||
│Item 19
|
||||
│Line2
|
||||
Reference in New Issue
Block a user