complete.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package dialog
  2. import (
  3. "log/slog"
  4. "github.com/charmbracelet/bubbles/v2/key"
  5. "github.com/charmbracelet/bubbles/v2/textarea"
  6. tea "github.com/charmbracelet/bubbletea/v2"
  7. "github.com/sst/opencode/internal/app"
  8. "github.com/sst/opencode/internal/components/list"
  9. "github.com/sst/opencode/internal/styles"
  10. "github.com/sst/opencode/internal/theme"
  11. "github.com/sst/opencode/internal/util"
  12. )
  13. type CompletionItem struct {
  14. Title string
  15. Value string
  16. }
  17. type CompletionItemI interface {
  18. list.ListItem
  19. GetValue() string
  20. DisplayValue() string
  21. }
  22. func (ci *CompletionItem) Render(selected bool, width int) string {
  23. t := theme.CurrentTheme()
  24. baseStyle := styles.BaseStyle()
  25. itemStyle := baseStyle.
  26. Background(t.BackgroundElement()).
  27. Width(width).
  28. Padding(0, 1)
  29. if selected {
  30. itemStyle = itemStyle.
  31. Foreground(t.Primary())
  32. }
  33. title := itemStyle.Render(
  34. ci.DisplayValue(),
  35. )
  36. return title
  37. }
  38. func (ci *CompletionItem) DisplayValue() string {
  39. return ci.Title
  40. }
  41. func (ci *CompletionItem) GetValue() string {
  42. return ci.Value
  43. }
  44. func NewCompletionItem(completionItem CompletionItem) CompletionItemI {
  45. return &completionItem
  46. }
  47. type CompletionProvider interface {
  48. GetId() string
  49. GetEntry() CompletionItemI
  50. GetChildEntries(query string) ([]CompletionItemI, error)
  51. GetEmptyMessage() string
  52. }
  53. type CompletionSelectedMsg struct {
  54. SearchString string
  55. CompletionValue string
  56. IsCommand bool
  57. }
  58. type CompletionDialogCompleteItemMsg struct {
  59. Value string
  60. }
  61. type CompletionDialogCloseMsg struct{}
  62. type CompletionDialog interface {
  63. tea.Model
  64. tea.ViewModel
  65. SetWidth(width int)
  66. IsEmpty() bool
  67. SetProvider(provider CompletionProvider)
  68. }
  69. type completionDialogComponent struct {
  70. query string
  71. completionProvider CompletionProvider
  72. width int
  73. height int
  74. pseudoSearchTextArea textarea.Model
  75. list list.List[CompletionItemI]
  76. }
  77. type completionDialogKeyMap struct {
  78. Complete key.Binding
  79. Cancel key.Binding
  80. }
  81. var completionDialogKeys = completionDialogKeyMap{
  82. Complete: key.NewBinding(
  83. key.WithKeys("tab", "enter", "right"),
  84. ),
  85. Cancel: key.NewBinding(
  86. key.WithKeys(" ", "esc", "backspace", "ctrl+c"),
  87. ),
  88. }
  89. func (c *completionDialogComponent) Init() tea.Cmd {
  90. return nil
  91. }
  92. func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  93. var cmds []tea.Cmd
  94. switch msg := msg.(type) {
  95. case []CompletionItemI:
  96. c.list.SetItems(msg)
  97. case app.CompletionDialogTriggerdMsg:
  98. c.pseudoSearchTextArea.SetValue(msg.InitialValue)
  99. case tea.KeyMsg:
  100. if c.pseudoSearchTextArea.Focused() {
  101. if !key.Matches(msg, completionDialogKeys.Complete) {
  102. var cmd tea.Cmd
  103. c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
  104. cmds = append(cmds, cmd)
  105. var query string
  106. query = c.pseudoSearchTextArea.Value()
  107. if query != "" {
  108. query = query[1:]
  109. }
  110. if query != c.query {
  111. c.query = query
  112. cmd = func() tea.Msg {
  113. items, err := c.completionProvider.GetChildEntries(query)
  114. if err != nil {
  115. slog.Error("Failed to get completion items", "error", err)
  116. }
  117. return items
  118. }
  119. cmds = append(cmds, cmd)
  120. }
  121. u, cmd := c.list.Update(msg)
  122. c.list = u.(list.List[CompletionItemI])
  123. cmds = append(cmds, cmd)
  124. }
  125. switch {
  126. case key.Matches(msg, completionDialogKeys.Complete):
  127. item, i := c.list.GetSelectedItem()
  128. if i == -1 {
  129. return c, nil
  130. }
  131. return c, c.complete(item)
  132. case key.Matches(msg, completionDialogKeys.Cancel):
  133. // Only close on backspace when there are no characters left
  134. if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
  135. return c, c.close()
  136. }
  137. }
  138. return c, tea.Batch(cmds...)
  139. } else {
  140. cmd := func() tea.Msg {
  141. items, err := c.completionProvider.GetChildEntries("")
  142. if err != nil {
  143. slog.Error("Failed to get completion items", "error", err)
  144. }
  145. return items
  146. }
  147. cmds = append(cmds, cmd)
  148. cmds = append(cmds, c.pseudoSearchTextArea.Focus())
  149. // c.pseudoSearchTextArea.SetValue(msg.String())
  150. return c, tea.Batch(cmds...)
  151. }
  152. case tea.WindowSizeMsg:
  153. c.width = msg.Width
  154. c.height = msg.Height
  155. }
  156. return c, tea.Batch(cmds...)
  157. }
  158. func (c *completionDialogComponent) View() string {
  159. t := theme.CurrentTheme()
  160. baseStyle := styles.BaseStyle()
  161. maxWidth := 40
  162. completions := c.list.GetItems()
  163. for _, cmd := range completions {
  164. title := cmd.DisplayValue()
  165. if len(title) > maxWidth-4 {
  166. maxWidth = len(title) + 4
  167. }
  168. }
  169. c.list.SetMaxWidth(maxWidth)
  170. return baseStyle.Padding(0, 0).
  171. Background(t.BackgroundElement()).
  172. Width(c.width).
  173. Render(c.list.View())
  174. }
  175. func (c *completionDialogComponent) SetWidth(width int) {
  176. c.width = width
  177. }
  178. func (c *completionDialogComponent) IsEmpty() bool {
  179. return c.list.IsEmpty()
  180. }
  181. func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
  182. if c.completionProvider.GetId() != provider.GetId() {
  183. c.completionProvider = provider
  184. c.list.SetEmptyMessage(" " + provider.GetEmptyMessage())
  185. }
  186. }
  187. func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
  188. value := c.pseudoSearchTextArea.Value()
  189. if value == "" {
  190. return nil
  191. }
  192. // Check if this is a command completion
  193. isCommand := c.completionProvider.GetId() == "commands"
  194. return tea.Batch(
  195. util.CmdHandler(CompletionSelectedMsg{
  196. SearchString: value,
  197. CompletionValue: item.GetValue(),
  198. IsCommand: isCommand,
  199. }),
  200. c.close(),
  201. )
  202. }
  203. func (c *completionDialogComponent) close() tea.Cmd {
  204. c.list.SetItems([]CompletionItemI{})
  205. c.pseudoSearchTextArea.Reset()
  206. c.pseudoSearchTextArea.Blur()
  207. return util.CmdHandler(CompletionDialogCloseMsg{})
  208. }
  209. func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
  210. ti := textarea.New()
  211. li := list.NewListComponent(
  212. []CompletionItemI{},
  213. 7,
  214. completionProvider.GetEmptyMessage(),
  215. false,
  216. )
  217. go func() {
  218. items, err := completionProvider.GetChildEntries("")
  219. if err != nil {
  220. slog.Error("Failed to get completion items", "error", err)
  221. }
  222. li.SetItems(items)
  223. }()
  224. return &completionDialogComponent{
  225. query: "",
  226. completionProvider: completionProvider,
  227. pseudoSearchTextArea: ti,
  228. list: li,
  229. }
  230. }