complete.go 6.2 KB

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