Skip to content

Commit 568c047

Browse files
committed
feat(tui): expand input to fit message
1 parent 4a06e16 commit 568c047

File tree

8 files changed

+1920
-46
lines changed

8 files changed

+1920
-46
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ You can configure custom keybinds, the values listed below are the defaults.
9898
"input_clear": "ctrl+c",
9999
"input_paste": "ctrl+v",
100100
"input_submit": "enter",
101-
"input_newline": "shift+enter",
101+
"input_newline": "shift+enter,ctrl+j",
102102
"history_previous": "up",
103103
"history_next": "down",
104104
"messages_page_up": "pgup",

packages/tui/internal/commands/command.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -208,18 +208,18 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
208208
{
209209
Name: InputNewlineCommand,
210210
Description: "insert newline",
211-
Keybindings: parseBindings("shift+enter"),
212-
},
213-
{
214-
Name: HistoryPreviousCommand,
215-
Description: "previous prompt",
216-
Keybindings: parseBindings("up"),
217-
},
218-
{
219-
Name: HistoryNextCommand,
220-
Description: "next prompt",
221-
Keybindings: parseBindings("down"),
211+
Keybindings: parseBindings("shift+enter", "ctrl+j"),
222212
},
213+
// {
214+
// Name: HistoryPreviousCommand,
215+
// Description: "previous prompt",
216+
// Keybindings: parseBindings("up"),
217+
// },
218+
// {
219+
// Name: HistoryNextCommand,
220+
// Description: "next prompt",
221+
// Keybindings: parseBindings("down"),
222+
// },
223223
{
224224
Name: MessagesPageUpCommand,
225225
Description: "page up",

packages/tui/internal/components/chat/editor.go

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import (
66
"strings"
77

88
"github.com/charmbracelet/bubbles/v2/spinner"
9-
"github.com/charmbracelet/bubbles/v2/textarea"
109
tea "github.com/charmbracelet/bubbletea/v2"
1110
"github.com/charmbracelet/lipgloss/v2"
1211
"github.com/sst/opencode/internal/app"
1312
"github.com/sst/opencode/internal/commands"
1413
"github.com/sst/opencode/internal/components/dialog"
14+
"github.com/sst/opencode/internal/components/textarea"
1515
"github.com/sst/opencode/internal/image"
1616
"github.com/sst/opencode/internal/layout"
1717
"github.com/sst/opencode/internal/styles"
@@ -23,6 +23,8 @@ type EditorComponent interface {
2323
tea.Model
2424
tea.ViewModel
2525
layout.Sizeable
26+
Content() string
27+
Lines() int
2628
Value() string
2729
Submit() (tea.Model, tea.Cmd)
2830
Clear() (tea.Model, tea.Cmd)
@@ -50,33 +52,27 @@ func (m *editorComponent) Init() tea.Cmd {
5052
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5153
var cmds []tea.Cmd
5254
var cmd tea.Cmd
55+
5356
switch msg := msg.(type) {
5457
case tea.KeyPressMsg:
5558
// Maximize editor responsiveness for printable characters
5659
if msg.Text != "" {
5760
m.textarea, cmd = m.textarea.Update(msg)
58-
return m, cmd
61+
cmds = append(cmds, cmd)
62+
return m, tea.Batch(cmds...)
5963
}
60-
61-
// // TODO: ?
62-
// if key.Matches(msg, messageKeys.PageUp) ||
63-
// key.Matches(msg, messageKeys.PageDown) ||
64-
// key.Matches(msg, messageKeys.HalfPageUp) ||
65-
// key.Matches(msg, messageKeys.HalfPageDown) {
66-
// return m, nil
67-
// }
68-
6964
case dialog.ThemeSelectedMsg:
7065
m.textarea = createTextArea(&m.textarea)
7166
m.spinner = createSpinner()
7267
return m, tea.Batch(m.spinner.Tick, textarea.Blink)
7368
case dialog.CompletionSelectedMsg:
7469
if msg.IsCommand {
7570
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
76-
m.textarea.Reset()
77-
return m, util.CmdHandler(
78-
commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)]),
79-
)
71+
updated, cmd := m.Clear()
72+
m = updated.(*editorComponent)
73+
cmds = append(cmds, cmd)
74+
cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)])))
75+
return m, tea.Batch(cmds...)
8076
} else {
8177
existingValue := m.textarea.Value()
8278
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
@@ -94,7 +90,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9490
return m, tea.Batch(cmds...)
9591
}
9692

97-
func (m *editorComponent) View() string {
93+
func (m *editorComponent) Content() string {
9894
t := theme.CurrentTheme()
9995
base := styles.BaseStyle().Background(t.Background()).Render
10096
muted := styles.Muted().Background(t.Background()).Render
@@ -139,25 +135,35 @@ func (m *editorComponent) View() string {
139135
return content
140136
}
141137

138+
func (m *editorComponent) View() string {
139+
if m.Lines() > 1 {
140+
return ""
141+
}
142+
return m.Content()
143+
}
144+
142145
func (m *editorComponent) GetSize() (width, height int) {
143146
return m.width, m.height
144147
}
145148

146149
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
147150
m.width = width
148151
m.height = height
149-
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
150-
m.textarea.SetHeight(height - 4) // account for info underneath
152+
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
153+
// m.textarea.SetHeight(height - 4)
151154
return nil
152155
}
153156

157+
func (m *editorComponent) Lines() int {
158+
return m.textarea.LineCount()
159+
}
160+
154161
func (m *editorComponent) Value() string {
155162
return m.textarea.Value()
156163
}
157164

158165
func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
159166
value := strings.TrimSpace(m.Value())
160-
m.textarea.Reset()
161167
if value == "" {
162168
return m, nil
163169
}
@@ -167,6 +173,11 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
167173
return m, nil
168174
}
169175

176+
var cmds []tea.Cmd
177+
updated, cmd := m.Clear()
178+
m = updated.(*editorComponent)
179+
cmds = append(cmds, cmd)
180+
170181
attachments := m.attachments
171182

172183
// Save to history if not empty and not a duplicate of the last entry
@@ -180,12 +191,8 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
180191

181192
m.attachments = nil
182193

183-
return m, tea.Batch(
184-
util.CmdHandler(app.SendMsg{
185-
Text: value,
186-
Attachments: attachments,
187-
}),
188-
)
194+
cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: attachments}))
195+
return m, tea.Batch(cmds...)
189196
}
190197

191198
func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {

packages/tui/internal/components/dialog/complete.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,10 @@ type completionDialogKeyMap struct {
101101

102102
var completionDialogKeys = completionDialogKeyMap{
103103
Complete: key.NewBinding(
104-
key.WithKeys("tab", "enter"),
104+
key.WithKeys("tab", "enter", "right"),
105105
),
106106
Cancel: key.NewBinding(
107-
key.WithKeys(" ", "esc", "backspace"),
107+
key.WithKeys(" ", "esc", "backspace", "ctrl+c"),
108108
),
109109
}
110110

@@ -209,7 +209,7 @@ func (c *completionDialogComponent) View() string {
209209
BorderRight(true).
210210
BorderLeft(true).
211211
BorderBackground(t.Background()).
212-
BorderForeground(t.BackgroundSubtle()).
212+
BorderForeground(t.BackgroundElement()).
213213
Width(c.width).
214214
Render(c.list.View())
215215
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Package memoization implement a simple memoization cache. It's designed to
2+
// improve performance in textarea.
3+
package textarea
4+
5+
import (
6+
"container/list"
7+
"crypto/sha256"
8+
"fmt"
9+
"sync"
10+
)
11+
12+
// Hasher is an interface that requires a Hash method. The Hash method is
13+
// expected to return a string representation of the hash of the object.
14+
type Hasher interface {
15+
Hash() string
16+
}
17+
18+
// entry is a struct that holds a key-value pair. It is used as an element
19+
// in the evictionList of the MemoCache.
20+
type entry[T any] struct {
21+
key string
22+
value T
23+
}
24+
25+
// MemoCache is a struct that represents a cache with a set capacity. It
26+
// uses an LRU (Least Recently Used) eviction policy. It is safe for
27+
// concurrent use.
28+
type MemoCache[H Hasher, T any] struct {
29+
capacity int
30+
mutex sync.Mutex
31+
cache map[string]*list.Element // The cache holding the results
32+
evictionList *list.List // A list to keep track of the order for LRU
33+
hashableItems map[string]T // This map keeps track of the original hashable items (optional)
34+
}
35+
36+
// NewMemoCache is a function that creates a new MemoCache with a given
37+
// capacity. It returns a pointer to the created MemoCache.
38+
func NewMemoCache[H Hasher, T any](capacity int) *MemoCache[H, T] {
39+
return &MemoCache[H, T]{
40+
capacity: capacity,
41+
cache: make(map[string]*list.Element),
42+
evictionList: list.New(),
43+
hashableItems: make(map[string]T),
44+
}
45+
}
46+
47+
// Capacity is a method that returns the capacity of the MemoCache.
48+
func (m *MemoCache[H, T]) Capacity() int {
49+
return m.capacity
50+
}
51+
52+
// Size is a method that returns the current size of the MemoCache. It is
53+
// the number of items currently stored in the cache.
54+
func (m *MemoCache[H, T]) Size() int {
55+
m.mutex.Lock()
56+
defer m.mutex.Unlock()
57+
return m.evictionList.Len()
58+
}
59+
60+
// Get is a method that returns the value associated with the given
61+
// hashable item in the MemoCache. If there is no corresponding value, the
62+
// method returns nil.
63+
func (m *MemoCache[H, T]) Get(h H) (T, bool) {
64+
m.mutex.Lock()
65+
defer m.mutex.Unlock()
66+
67+
hashedKey := h.Hash()
68+
if element, found := m.cache[hashedKey]; found {
69+
m.evictionList.MoveToFront(element)
70+
return element.Value.(*entry[T]).value, true
71+
}
72+
var result T
73+
return result, false
74+
}
75+
76+
// Set is a method that sets the value for the given hashable item in the
77+
// MemoCache. If the cache is at capacity, it evicts the least recently
78+
// used item before adding the new item.
79+
func (m *MemoCache[H, T]) Set(h H, value T) {
80+
m.mutex.Lock()
81+
defer m.mutex.Unlock()
82+
83+
hashedKey := h.Hash()
84+
if element, found := m.cache[hashedKey]; found {
85+
m.evictionList.MoveToFront(element)
86+
element.Value.(*entry[T]).value = value
87+
return
88+
}
89+
90+
// Check if the cache is at capacity
91+
if m.evictionList.Len() >= m.capacity {
92+
// Evict the least recently used item from the cache
93+
toEvict := m.evictionList.Back()
94+
if toEvict != nil {
95+
evictedEntry := m.evictionList.Remove(toEvict).(*entry[T])
96+
delete(m.cache, evictedEntry.key)
97+
delete(m.hashableItems, evictedEntry.key) // if you're keeping track of original items
98+
}
99+
}
100+
101+
// Add the value to the cache and the evictionList
102+
newEntry := &entry[T]{
103+
key: hashedKey,
104+
value: value,
105+
}
106+
element := m.evictionList.PushFront(newEntry)
107+
m.cache[hashedKey] = element
108+
m.hashableItems[hashedKey] = value // if you're keeping track of original items
109+
}
110+
111+
// HString is a type that implements the Hasher interface for strings.
112+
type HString string
113+
114+
// Hash is a method that returns the hash of the string.
115+
func (h HString) Hash() string {
116+
return fmt.Sprintf("%x", sha256.Sum256([]byte(h)))
117+
}
118+
119+
// HInt is a type that implements the Hasher interface for integers.
120+
type HInt int
121+
122+
// Hash is a method that returns the hash of the integer.
123+
func (h HInt) Hash() string {
124+
return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d", h))))
125+
}

0 commit comments

Comments
 (0)