find.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. package dialog
  2. import (
  3. "log/slog"
  4. tea "github.com/charmbracelet/bubbletea/v2"
  5. "github.com/sst/opencode/internal/completions"
  6. "github.com/sst/opencode/internal/components/list"
  7. "github.com/sst/opencode/internal/components/modal"
  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. const (
  14. findDialogWidth = 76
  15. )
  16. type FindSelectedMsg struct {
  17. FilePath string
  18. }
  19. type FindDialogCloseMsg struct{}
  20. type findInitialSuggestionsMsg struct {
  21. suggestions []completions.CompletionSuggestion
  22. }
  23. type FindDialog interface {
  24. layout.Modal
  25. tea.Model
  26. tea.ViewModel
  27. SetWidth(width int)
  28. SetHeight(height int)
  29. IsEmpty() bool
  30. }
  31. // findItem is a custom list item for file suggestions
  32. type findItem struct {
  33. suggestion completions.CompletionSuggestion
  34. }
  35. func (f findItem) Render(
  36. selected bool,
  37. width int,
  38. baseStyle styles.Style,
  39. ) string {
  40. t := theme.CurrentTheme()
  41. itemStyle := baseStyle.
  42. Background(t.BackgroundPanel()).
  43. Foreground(t.TextMuted())
  44. if selected {
  45. itemStyle = itemStyle.Foreground(t.Primary())
  46. }
  47. return itemStyle.PaddingLeft(1).Render(f.suggestion.Display(itemStyle))
  48. }
  49. func (f findItem) Selectable() bool {
  50. return true
  51. }
  52. type findDialogComponent struct {
  53. completionProvider completions.CompletionProvider
  54. allSuggestions []completions.CompletionSuggestion
  55. width, height int
  56. modal *modal.Modal
  57. searchDialog *SearchDialog
  58. dialogWidth int
  59. }
  60. func (f *findDialogComponent) Init() tea.Cmd {
  61. return tea.Batch(
  62. f.loadInitialSuggestions(),
  63. f.searchDialog.Init(),
  64. )
  65. }
  66. func (f *findDialogComponent) loadInitialSuggestions() tea.Cmd {
  67. return func() tea.Msg {
  68. items, err := f.completionProvider.GetChildEntries("")
  69. if err != nil {
  70. slog.Error("Failed to get initial completion items", "error", err)
  71. return findInitialSuggestionsMsg{suggestions: []completions.CompletionSuggestion{}}
  72. }
  73. return findInitialSuggestionsMsg{suggestions: items}
  74. }
  75. }
  76. func (f *findDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  77. switch msg := msg.(type) {
  78. case findInitialSuggestionsMsg:
  79. // Handle initial suggestions setup
  80. f.allSuggestions = msg.suggestions
  81. // Calculate dialog width
  82. f.dialogWidth = f.calculateDialogWidth()
  83. // Initialize search dialog with calculated width
  84. f.searchDialog = NewSearchDialog("Search files...", 10)
  85. f.searchDialog.SetWidth(f.dialogWidth)
  86. // Convert to list items
  87. items := make([]list.Item, len(f.allSuggestions))
  88. for i, suggestion := range f.allSuggestions {
  89. items[i] = findItem{suggestion: suggestion}
  90. }
  91. f.searchDialog.SetItems(items)
  92. // Update modal with calculated width
  93. f.modal = modal.New(
  94. modal.WithTitle("Find Files"),
  95. modal.WithMaxWidth(f.dialogWidth+4),
  96. )
  97. return f, f.searchDialog.Init()
  98. case []completions.CompletionSuggestion:
  99. // Store suggestions and convert to findItem for the search dialog
  100. f.allSuggestions = msg
  101. items := make([]list.Item, len(msg))
  102. for i, suggestion := range msg {
  103. items[i] = findItem{suggestion: suggestion}
  104. }
  105. f.searchDialog.SetItems(items)
  106. return f, nil
  107. case SearchSelectionMsg:
  108. // Handle selection from search dialog - now we can directly access the suggestion
  109. if item, ok := msg.Item.(findItem); ok {
  110. return f, f.selectFile(item.suggestion)
  111. }
  112. return f, nil
  113. case SearchCancelledMsg:
  114. return f, f.Close()
  115. case SearchQueryChangedMsg:
  116. // Update completion items based on search query
  117. return f, func() tea.Msg {
  118. items, err := f.completionProvider.GetChildEntries(msg.Query)
  119. if err != nil {
  120. slog.Error("Failed to get completion items", "error", err)
  121. return []completions.CompletionSuggestion{}
  122. }
  123. return items
  124. }
  125. case tea.WindowSizeMsg:
  126. f.width = msg.Width
  127. f.height = msg.Height
  128. // Recalculate width based on new viewport size
  129. oldWidth := f.dialogWidth
  130. f.dialogWidth = f.calculateDialogWidth()
  131. if oldWidth != f.dialogWidth {
  132. f.searchDialog.SetWidth(f.dialogWidth)
  133. // Update modal max width too
  134. f.modal = modal.New(
  135. modal.WithTitle("Find Files"),
  136. modal.WithMaxWidth(f.dialogWidth+4),
  137. )
  138. }
  139. f.searchDialog.SetHeight(msg.Height)
  140. }
  141. // Forward all other messages to the search dialog
  142. updatedDialog, cmd := f.searchDialog.Update(msg)
  143. f.searchDialog = updatedDialog.(*SearchDialog)
  144. return f, cmd
  145. }
  146. func (f *findDialogComponent) View() string {
  147. return f.searchDialog.View()
  148. }
  149. func (f *findDialogComponent) calculateDialogWidth() int {
  150. // Use fixed width unless viewport is smaller
  151. if f.width > 0 && f.width < findDialogWidth+10 {
  152. return f.width - 10
  153. }
  154. return findDialogWidth
  155. }
  156. func (f *findDialogComponent) SetWidth(width int) {
  157. f.width = width
  158. f.searchDialog.SetWidth(f.dialogWidth)
  159. }
  160. func (f *findDialogComponent) SetHeight(height int) {
  161. f.height = height
  162. }
  163. func (f *findDialogComponent) IsEmpty() bool {
  164. return f.searchDialog.GetQuery() == ""
  165. }
  166. func (f *findDialogComponent) selectFile(item completions.CompletionSuggestion) tea.Cmd {
  167. return tea.Sequence(
  168. f.Close(),
  169. util.CmdHandler(FindSelectedMsg{
  170. FilePath: item.Value,
  171. }),
  172. )
  173. }
  174. func (f *findDialogComponent) Render(background string) string {
  175. return f.modal.Render(f.View(), background)
  176. }
  177. func (f *findDialogComponent) Close() tea.Cmd {
  178. f.searchDialog.SetQuery("")
  179. f.searchDialog.Blur()
  180. return util.CmdHandler(modal.CloseModalMsg{})
  181. }
  182. func NewFindDialog(completionProvider completions.CompletionProvider) FindDialog {
  183. component := &findDialogComponent{
  184. completionProvider: completionProvider,
  185. dialogWidth: findDialogWidth,
  186. allSuggestions: []completions.CompletionSuggestion{},
  187. }
  188. // Create search dialog and modal with fixed width
  189. component.searchDialog = NewSearchDialog("Search files...", 10)
  190. component.searchDialog.SetWidth(findDialogWidth)
  191. component.modal = modal.New(
  192. modal.WithTitle("Find Files"),
  193. modal.WithMaxWidth(findDialogWidth+4),
  194. )
  195. return component
  196. }