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