complete.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. package dialog
  2. import (
  3. "github.com/charmbracelet/bubbles/key"
  4. "github.com/charmbracelet/bubbles/textarea"
  5. tea "github.com/charmbracelet/bubbletea"
  6. "github.com/charmbracelet/lipgloss"
  7. "github.com/sst/opencode/internal/status"
  8. utilComponents "github.com/sst/opencode/internal/components/util"
  9. "github.com/sst/opencode/internal/layout"
  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. utilComponents.SimpleListItem
  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. Width(width).
  29. Padding(0, 1)
  30. if selected {
  31. itemStyle = itemStyle.
  32. Background(t.Background()).
  33. Foreground(t.Primary()).
  34. Bold(true)
  35. }
  36. title := itemStyle.Render(
  37. ci.GetValue(),
  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. }
  59. type CompletionDialogCompleteItemMsg struct {
  60. Value string
  61. }
  62. type CompletionDialogCloseMsg struct{}
  63. type CompletionDialog interface {
  64. tea.Model
  65. layout.Bindings
  66. SetWidth(width int)
  67. }
  68. type completionDialogCmp struct {
  69. query string
  70. completionProvider CompletionProvider
  71. width int
  72. height int
  73. pseudoSearchTextArea textarea.Model
  74. listView utilComponents.SimpleList[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 *completionDialogCmp) Init() tea.Cmd {
  89. return nil
  90. }
  91. func (c *completionDialogCmp) complete(item CompletionItemI) tea.Cmd {
  92. value := c.pseudoSearchTextArea.Value()
  93. if value == "" {
  94. return nil
  95. }
  96. return tea.Batch(
  97. util.CmdHandler(CompletionSelectedMsg{
  98. SearchString: value,
  99. CompletionValue: item.GetValue(),
  100. }),
  101. c.close(),
  102. )
  103. }
  104. func (c *completionDialogCmp) close() tea.Cmd {
  105. c.listView.SetItems([]CompletionItemI{})
  106. c.pseudoSearchTextArea.Reset()
  107. c.pseudoSearchTextArea.Blur()
  108. return util.CmdHandler(CompletionDialogCloseMsg{})
  109. }
  110. func (c *completionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  111. var cmds []tea.Cmd
  112. switch msg := msg.(type) {
  113. case tea.KeyMsg:
  114. if c.pseudoSearchTextArea.Focused() {
  115. if !key.Matches(msg, completionDialogKeys.Complete) {
  116. var cmd tea.Cmd
  117. c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
  118. cmds = append(cmds, cmd)
  119. var query string
  120. query = c.pseudoSearchTextArea.Value()
  121. if query != "" {
  122. query = query[1:]
  123. }
  124. if query != c.query {
  125. items, err := c.completionProvider.GetChildEntries(query)
  126. if err != nil {
  127. status.Error(err.Error())
  128. }
  129. c.listView.SetItems(items)
  130. c.query = query
  131. }
  132. u, cmd := c.listView.Update(msg)
  133. c.listView = u.(utilComponents.SimpleList[CompletionItemI])
  134. cmds = append(cmds, cmd)
  135. }
  136. switch {
  137. case key.Matches(msg, completionDialogKeys.Complete):
  138. item, i := c.listView.GetSelectedItem()
  139. if i == -1 {
  140. return c, nil
  141. }
  142. cmd := c.complete(item)
  143. return c, cmd
  144. case key.Matches(msg, completionDialogKeys.Cancel):
  145. // Only close on backspace when there are no characters left
  146. if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
  147. return c, c.close()
  148. }
  149. }
  150. return c, tea.Batch(cmds...)
  151. } else {
  152. items, err := c.completionProvider.GetChildEntries("")
  153. if err != nil {
  154. status.Error(err.Error())
  155. }
  156. c.listView.SetItems(items)
  157. c.pseudoSearchTextArea.SetValue(msg.String())
  158. return c, c.pseudoSearchTextArea.Focus()
  159. }
  160. case tea.WindowSizeMsg:
  161. c.width = msg.Width
  162. c.height = msg.Height
  163. }
  164. return c, tea.Batch(cmds...)
  165. }
  166. func (c *completionDialogCmp) View() string {
  167. t := theme.CurrentTheme()
  168. baseStyle := styles.BaseStyle()
  169. maxWidth := 40
  170. completions := c.listView.GetItems()
  171. for _, cmd := range completions {
  172. title := cmd.DisplayValue()
  173. if len(title) > maxWidth-4 {
  174. maxWidth = len(title) + 4
  175. }
  176. }
  177. c.listView.SetMaxWidth(maxWidth)
  178. return baseStyle.Padding(0, 0).
  179. Border(lipgloss.NormalBorder()).
  180. BorderBottom(false).
  181. BorderRight(false).
  182. BorderLeft(false).
  183. BorderBackground(t.Background()).
  184. BorderForeground(t.TextMuted()).
  185. Width(c.width).
  186. Render(c.listView.View())
  187. }
  188. func (c *completionDialogCmp) SetWidth(width int) {
  189. c.width = width
  190. }
  191. func (c *completionDialogCmp) BindingKeys() []key.Binding {
  192. return layout.KeyMapToSlice(completionDialogKeys)
  193. }
  194. func NewCompletionDialogCmp(completionProvider CompletionProvider) CompletionDialog {
  195. ti := textarea.New()
  196. items, err := completionProvider.GetChildEntries("")
  197. if err != nil {
  198. status.Error(err.Error())
  199. }
  200. li := utilComponents.NewSimpleList(
  201. items,
  202. 7,
  203. "No file matches found",
  204. false,
  205. )
  206. return &completionDialogCmp{
  207. query: "",
  208. completionProvider: completionProvider,
  209. pseudoSearchTextArea: ti,
  210. listView: li,
  211. }
  212. }