find.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. package dialog
  2. import (
  3. "log/slog"
  4. "github.com/charmbracelet/bubbles/v2/key"
  5. "github.com/charmbracelet/bubbles/v2/textinput"
  6. tea "github.com/charmbracelet/bubbletea/v2"
  7. "github.com/sst/opencode/internal/components/list"
  8. "github.com/sst/opencode/internal/components/modal"
  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 FindSelectedMsg struct {
  15. FilePath string
  16. }
  17. type FindDialogCloseMsg struct{}
  18. type FindDialog interface {
  19. layout.Modal
  20. tea.Model
  21. tea.ViewModel
  22. SetWidth(width int)
  23. SetHeight(height int)
  24. IsEmpty() bool
  25. }
  26. type findDialogComponent struct {
  27. query string
  28. completionProvider CompletionProvider
  29. width, height int
  30. modal *modal.Modal
  31. textInput textinput.Model
  32. list list.List[CompletionItemI]
  33. }
  34. type findDialogKeyMap struct {
  35. Select key.Binding
  36. Cancel key.Binding
  37. }
  38. var findDialogKeys = findDialogKeyMap{
  39. Select: key.NewBinding(
  40. key.WithKeys("enter"),
  41. ),
  42. Cancel: key.NewBinding(
  43. key.WithKeys("esc"),
  44. ),
  45. }
  46. func (f *findDialogComponent) Init() tea.Cmd {
  47. return textinput.Blink
  48. }
  49. func (f *findDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  50. var cmd tea.Cmd
  51. var cmds []tea.Cmd
  52. switch msg := msg.(type) {
  53. case []CompletionItemI:
  54. f.list.SetItems(msg)
  55. case tea.KeyMsg:
  56. switch msg.String() {
  57. case "ctrl+c":
  58. if f.textInput.Value() == "" {
  59. return f, nil
  60. }
  61. f.textInput.SetValue("")
  62. return f.update(msg)
  63. }
  64. switch {
  65. case key.Matches(msg, findDialogKeys.Select):
  66. item, i := f.list.GetSelectedItem()
  67. if i == -1 {
  68. return f, nil
  69. }
  70. return f, f.selectFile(item)
  71. case key.Matches(msg, findDialogKeys.Cancel):
  72. return f, f.Close()
  73. default:
  74. f.textInput, cmd = f.textInput.Update(msg)
  75. cmds = append(cmds, cmd)
  76. f, cmd = f.update(msg)
  77. cmds = append(cmds, cmd)
  78. }
  79. }
  80. return f, tea.Batch(cmds...)
  81. }
  82. func (f *findDialogComponent) update(msg tea.Msg) (*findDialogComponent, tea.Cmd) {
  83. var cmd tea.Cmd
  84. var cmds []tea.Cmd
  85. query := f.textInput.Value()
  86. if query != f.query {
  87. f.query = query
  88. cmd = func() tea.Msg {
  89. items, err := f.completionProvider.GetChildEntries(query)
  90. if err != nil {
  91. slog.Error("Failed to get completion items", "error", err)
  92. }
  93. return items
  94. }
  95. cmds = append(cmds, cmd)
  96. }
  97. u, cmd := f.list.Update(msg)
  98. f.list = u.(list.List[CompletionItemI])
  99. cmds = append(cmds, cmd)
  100. return f, tea.Batch(cmds...)
  101. }
  102. func (f *findDialogComponent) View() string {
  103. t := theme.CurrentTheme()
  104. f.textInput.SetWidth(f.width - 8)
  105. f.list.SetMaxWidth(f.width - 4)
  106. inputView := f.textInput.View()
  107. inputView = styles.NewStyle().
  108. Background(t.BackgroundElement()).
  109. Height(1).
  110. Width(f.width-4).
  111. Padding(0, 0).
  112. Render(inputView)
  113. listView := f.list.View()
  114. return styles.NewStyle().Height(12).Render(inputView + "\n" + listView)
  115. }
  116. func (f *findDialogComponent) SetWidth(width int) {
  117. f.width = width
  118. if width > 4 {
  119. f.textInput.SetWidth(width - 4)
  120. f.list.SetMaxWidth(width - 4)
  121. }
  122. }
  123. func (f *findDialogComponent) SetHeight(height int) {
  124. f.height = height
  125. }
  126. func (f *findDialogComponent) IsEmpty() bool {
  127. return f.list.IsEmpty()
  128. }
  129. func (f *findDialogComponent) selectFile(item CompletionItemI) tea.Cmd {
  130. return tea.Sequence(
  131. f.Close(),
  132. util.CmdHandler(FindSelectedMsg{
  133. FilePath: item.GetValue(),
  134. }),
  135. )
  136. }
  137. func (f *findDialogComponent) Render(background string) string {
  138. return f.modal.Render(f.View(), background)
  139. }
  140. func (f *findDialogComponent) Close() tea.Cmd {
  141. f.textInput.Reset()
  142. f.textInput.Blur()
  143. return util.CmdHandler(modal.CloseModalMsg{})
  144. }
  145. func createTextInput(existing *textinput.Model) textinput.Model {
  146. t := theme.CurrentTheme()
  147. bgColor := t.BackgroundElement()
  148. textColor := t.Text()
  149. textMutedColor := t.TextMuted()
  150. ti := textinput.New()
  151. ti.Styles.Blurred.Placeholder = styles.NewStyle().
  152. Foreground(textMutedColor).
  153. Background(bgColor).
  154. Lipgloss()
  155. ti.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
  156. ti.Styles.Focused.Placeholder = styles.NewStyle().
  157. Foreground(textMutedColor).
  158. Background(bgColor).
  159. Lipgloss()
  160. ti.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
  161. ti.Styles.Cursor.Color = t.Primary()
  162. ti.VirtualCursor = true
  163. ti.Prompt = " "
  164. ti.CharLimit = -1
  165. ti.Focus()
  166. if existing != nil {
  167. ti.SetValue(existing.Value())
  168. ti.SetWidth(existing.Width())
  169. }
  170. return ti
  171. }
  172. func NewFindDialog(completionProvider CompletionProvider) FindDialog {
  173. ti := createTextInput(nil)
  174. li := list.NewListComponent(
  175. []CompletionItemI{},
  176. 10, // max visible items
  177. completionProvider.GetEmptyMessage(),
  178. false,
  179. )
  180. go func() {
  181. items, err := completionProvider.GetChildEntries("")
  182. if err != nil {
  183. slog.Error("Failed to get completion items", "error", err)
  184. }
  185. li.SetItems(items)
  186. }()
  187. return &findDialogComponent{
  188. query: "",
  189. completionProvider: completionProvider,
  190. textInput: ti,
  191. list: li,
  192. modal: modal.New(
  193. modal.WithTitle("Find Files"),
  194. modal.WithMaxWidth(80),
  195. ),
  196. }
  197. }