mirror of
https://github.com/sst/opencode.git
synced 2025-07-07 16:14:59 +00:00
274 lines
6.2 KiB
Go
274 lines
6.2 KiB
Go
package dialog
|
|
|
|
import (
|
|
"log/slog"
|
|
|
|
"github.com/charmbracelet/bubbles/v2/key"
|
|
"github.com/charmbracelet/bubbles/v2/textarea"
|
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|
"github.com/charmbracelet/lipgloss/v2"
|
|
"github.com/sst/opencode/internal/app"
|
|
"github.com/sst/opencode/internal/components/list"
|
|
"github.com/sst/opencode/internal/styles"
|
|
"github.com/sst/opencode/internal/theme"
|
|
"github.com/sst/opencode/internal/util"
|
|
)
|
|
|
|
type CompletionItem struct {
|
|
Title string
|
|
Value string
|
|
}
|
|
|
|
type CompletionItemI interface {
|
|
list.ListItem
|
|
GetValue() string
|
|
DisplayValue() string
|
|
}
|
|
|
|
func (ci *CompletionItem) Render(selected bool, width int) string {
|
|
t := theme.CurrentTheme()
|
|
baseStyle := styles.NewStyle().Foreground(t.Text())
|
|
|
|
itemStyle := baseStyle.
|
|
Background(t.BackgroundElement()).
|
|
Width(width).
|
|
Padding(0, 1)
|
|
|
|
if selected {
|
|
itemStyle = itemStyle.Foreground(t.Primary())
|
|
}
|
|
|
|
title := itemStyle.Render(
|
|
ci.DisplayValue(),
|
|
)
|
|
return title
|
|
}
|
|
|
|
func (ci *CompletionItem) DisplayValue() string {
|
|
return ci.Title
|
|
}
|
|
|
|
func (ci *CompletionItem) GetValue() string {
|
|
return ci.Value
|
|
}
|
|
|
|
func NewCompletionItem(completionItem CompletionItem) CompletionItemI {
|
|
return &completionItem
|
|
}
|
|
|
|
type CompletionProvider interface {
|
|
GetId() string
|
|
GetChildEntries(query string) ([]CompletionItemI, error)
|
|
GetEmptyMessage() string
|
|
}
|
|
|
|
type CompletionSelectedMsg struct {
|
|
SearchString string
|
|
CompletionValue string
|
|
IsCommand bool
|
|
}
|
|
|
|
type CompletionDialogCompleteItemMsg struct {
|
|
Value string
|
|
}
|
|
|
|
type CompletionDialogCloseMsg struct{}
|
|
|
|
type CompletionDialog interface {
|
|
tea.Model
|
|
tea.ViewModel
|
|
SetWidth(width int)
|
|
IsEmpty() bool
|
|
SetProvider(provider CompletionProvider)
|
|
}
|
|
|
|
type completionDialogComponent struct {
|
|
query string
|
|
completionProvider CompletionProvider
|
|
width int
|
|
height int
|
|
pseudoSearchTextArea textarea.Model
|
|
list list.List[CompletionItemI]
|
|
}
|
|
|
|
type completionDialogKeyMap struct {
|
|
Complete key.Binding
|
|
Cancel key.Binding
|
|
}
|
|
|
|
var completionDialogKeys = completionDialogKeyMap{
|
|
Complete: key.NewBinding(
|
|
key.WithKeys("tab", "enter", "right"),
|
|
),
|
|
Cancel: key.NewBinding(
|
|
key.WithKeys(" ", "esc", "backspace", "ctrl+c"),
|
|
),
|
|
}
|
|
|
|
func (c *completionDialogComponent) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
var cmds []tea.Cmd
|
|
switch msg := msg.(type) {
|
|
case []CompletionItemI:
|
|
c.list.SetItems(msg)
|
|
case app.CompletionDialogTriggeredMsg:
|
|
c.pseudoSearchTextArea.SetValue(msg.InitialValue)
|
|
case tea.KeyMsg:
|
|
if c.pseudoSearchTextArea.Focused() {
|
|
if !key.Matches(msg, completionDialogKeys.Complete) {
|
|
var cmd tea.Cmd
|
|
c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
|
|
cmds = append(cmds, cmd)
|
|
|
|
var query string
|
|
query = c.pseudoSearchTextArea.Value()
|
|
if query != "" {
|
|
query = query[1:]
|
|
}
|
|
|
|
if query != c.query {
|
|
c.query = query
|
|
cmd = func() tea.Msg {
|
|
items, err := c.completionProvider.GetChildEntries(query)
|
|
if err != nil {
|
|
slog.Error("Failed to get completion items", "error", err)
|
|
}
|
|
return items
|
|
}
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
|
|
u, cmd := c.list.Update(msg)
|
|
c.list = u.(list.List[CompletionItemI])
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
|
|
switch {
|
|
case key.Matches(msg, completionDialogKeys.Complete):
|
|
item, i := c.list.GetSelectedItem()
|
|
if i == -1 {
|
|
return c, nil
|
|
}
|
|
return c, c.complete(item)
|
|
case key.Matches(msg, completionDialogKeys.Cancel):
|
|
// Only close on backspace when there are no characters left
|
|
if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
|
|
return c, c.close()
|
|
}
|
|
}
|
|
|
|
return c, tea.Batch(cmds...)
|
|
} else {
|
|
cmd := func() tea.Msg {
|
|
items, err := c.completionProvider.GetChildEntries("")
|
|
if err != nil {
|
|
slog.Error("Failed to get completion items", "error", err)
|
|
}
|
|
return items
|
|
}
|
|
cmds = append(cmds, cmd)
|
|
cmds = append(cmds, c.pseudoSearchTextArea.Focus())
|
|
return c, tea.Batch(cmds...)
|
|
}
|
|
}
|
|
|
|
return c, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (c *completionDialogComponent) View() string {
|
|
t := theme.CurrentTheme()
|
|
baseStyle := styles.NewStyle().Foreground(t.Text())
|
|
|
|
maxWidth := 40
|
|
completions := c.list.GetItems()
|
|
|
|
for _, cmd := range completions {
|
|
title := cmd.DisplayValue()
|
|
if len(title) > maxWidth-4 {
|
|
maxWidth = len(title) + 4
|
|
}
|
|
}
|
|
|
|
c.list.SetMaxWidth(maxWidth)
|
|
|
|
return baseStyle.
|
|
Padding(0, 0).
|
|
Background(t.BackgroundElement()).
|
|
BorderStyle(lipgloss.ThickBorder()).
|
|
BorderLeft(true).
|
|
BorderRight(true).
|
|
BorderForeground(t.Border()).
|
|
BorderBackground(t.Background()).
|
|
Width(c.width).
|
|
Render(c.list.View())
|
|
}
|
|
|
|
func (c *completionDialogComponent) SetWidth(width int) {
|
|
c.width = width
|
|
}
|
|
|
|
func (c *completionDialogComponent) IsEmpty() bool {
|
|
return c.list.IsEmpty()
|
|
}
|
|
|
|
func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
|
|
if c.completionProvider.GetId() != provider.GetId() {
|
|
c.completionProvider = provider
|
|
c.list.SetEmptyMessage(" " + provider.GetEmptyMessage())
|
|
c.list.SetItems([]CompletionItemI{})
|
|
}
|
|
}
|
|
|
|
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
|
value := c.pseudoSearchTextArea.Value()
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
|
|
// Check if this is a command completion
|
|
isCommand := c.completionProvider.GetId() == "commands"
|
|
|
|
return tea.Batch(
|
|
util.CmdHandler(CompletionSelectedMsg{
|
|
SearchString: value,
|
|
CompletionValue: item.GetValue(),
|
|
IsCommand: isCommand,
|
|
}),
|
|
c.close(),
|
|
)
|
|
}
|
|
|
|
func (c *completionDialogComponent) close() tea.Cmd {
|
|
c.pseudoSearchTextArea.Reset()
|
|
c.pseudoSearchTextArea.Blur()
|
|
return util.CmdHandler(CompletionDialogCloseMsg{})
|
|
}
|
|
|
|
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
|
|
ti := textarea.New()
|
|
|
|
li := list.NewListComponent(
|
|
[]CompletionItemI{},
|
|
7,
|
|
completionProvider.GetEmptyMessage(),
|
|
false,
|
|
)
|
|
|
|
go func() {
|
|
items, err := completionProvider.GetChildEntries("")
|
|
if err != nil {
|
|
slog.Error("Failed to get completion items", "error", err)
|
|
}
|
|
li.SetItems(items)
|
|
}()
|
|
|
|
return &completionDialogComponent{
|
|
query: "",
|
|
completionProvider: completionProvider,
|
|
pseudoSearchTextArea: ti,
|
|
list: li,
|
|
}
|
|
}
|