| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- 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/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.BaseStyle()
- 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
- GetEntry() CompletionItemI
- 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.CompletionDialogTriggerdMsg:
- 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())
- // c.pseudoSearchTextArea.SetValue(msg.String())
- return c, tea.Batch(cmds...)
- }
- case tea.WindowSizeMsg:
- c.width = msg.Width
- c.height = msg.Height
- }
- return c, tea.Batch(cmds...)
- }
- func (c *completionDialogComponent) View() string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
- 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()).
- 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())
- }
- }
- 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.list.SetItems([]CompletionItemI{})
- 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,
- }
- }
|