complete.go 6.4 KB

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