complete.go 6.0 KB

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