mirror of
https://github.com/sst/opencode.git
synced 2025-07-07 16:14:59 +00:00
235 lines
5.3 KiB
Go
235 lines
5.3 KiB
Go
package dialog
|
|
|
|
import (
|
|
"log/slog"
|
|
|
|
"github.com/charmbracelet/bubbles/v2/key"
|
|
"github.com/charmbracelet/bubbles/v2/textinput"
|
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|
"github.com/sst/opencode/internal/components/list"
|
|
"github.com/sst/opencode/internal/components/modal"
|
|
"github.com/sst/opencode/internal/layout"
|
|
"github.com/sst/opencode/internal/styles"
|
|
"github.com/sst/opencode/internal/theme"
|
|
"github.com/sst/opencode/internal/util"
|
|
)
|
|
|
|
type FindSelectedMsg struct {
|
|
FilePath string
|
|
}
|
|
|
|
type FindDialogCloseMsg struct{}
|
|
|
|
type FindDialog interface {
|
|
layout.Modal
|
|
tea.Model
|
|
tea.ViewModel
|
|
SetWidth(width int)
|
|
SetHeight(height int)
|
|
IsEmpty() bool
|
|
SetProvider(provider CompletionProvider)
|
|
}
|
|
|
|
type findDialogComponent struct {
|
|
query string
|
|
completionProvider CompletionProvider
|
|
width, height int
|
|
modal *modal.Modal
|
|
textInput textinput.Model
|
|
list list.List[CompletionItemI]
|
|
}
|
|
|
|
type findDialogKeyMap struct {
|
|
Select key.Binding
|
|
Cancel key.Binding
|
|
}
|
|
|
|
var findDialogKeys = findDialogKeyMap{
|
|
Select: key.NewBinding(
|
|
key.WithKeys("enter"),
|
|
),
|
|
Cancel: key.NewBinding(
|
|
key.WithKeys("esc"),
|
|
),
|
|
}
|
|
|
|
func (f *findDialogComponent) Init() tea.Cmd {
|
|
return textinput.Blink
|
|
}
|
|
|
|
func (f *findDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
var cmd tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
|
switch msg := msg.(type) {
|
|
case []CompletionItemI:
|
|
f.list.SetItems(msg)
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "ctrl+c":
|
|
if f.textInput.Value() == "" {
|
|
return f, nil
|
|
}
|
|
f.textInput.SetValue("")
|
|
return f.update(msg)
|
|
}
|
|
|
|
switch {
|
|
case key.Matches(msg, findDialogKeys.Select):
|
|
item, i := f.list.GetSelectedItem()
|
|
if i == -1 {
|
|
return f, nil
|
|
}
|
|
return f, f.selectFile(item)
|
|
case key.Matches(msg, findDialogKeys.Cancel):
|
|
return f, f.Close()
|
|
default:
|
|
f.textInput, cmd = f.textInput.Update(msg)
|
|
cmds = append(cmds, cmd)
|
|
|
|
f, cmd = f.update(msg)
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
}
|
|
|
|
return f, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (f *findDialogComponent) update(msg tea.Msg) (*findDialogComponent, tea.Cmd) {
|
|
var cmd tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
|
query := f.textInput.Value()
|
|
if query != f.query {
|
|
f.query = query
|
|
cmd = func() tea.Msg {
|
|
items, err := f.completionProvider.GetChildEntries(query)
|
|
if err != nil {
|
|
slog.Error("Failed to get completion items", "error", err)
|
|
}
|
|
return items
|
|
}
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
|
|
u, cmd := f.list.Update(msg)
|
|
f.list = u.(list.List[CompletionItemI])
|
|
cmds = append(cmds, cmd)
|
|
|
|
return f, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (f *findDialogComponent) View() string {
|
|
t := theme.CurrentTheme()
|
|
f.textInput.SetWidth(f.width - 8)
|
|
f.list.SetMaxWidth(f.width - 4)
|
|
inputView := f.textInput.View()
|
|
inputView = styles.NewStyle().
|
|
Background(t.BackgroundPanel()).
|
|
Height(1).
|
|
Width(f.width-4).
|
|
Padding(0, 0).
|
|
Render(inputView)
|
|
|
|
listView := f.list.View()
|
|
return styles.NewStyle().Height(12).Render(inputView + "\n" + listView)
|
|
}
|
|
|
|
func (f *findDialogComponent) SetWidth(width int) {
|
|
f.width = width
|
|
if width > 4 {
|
|
f.textInput.SetWidth(width - 4)
|
|
f.list.SetMaxWidth(width - 4)
|
|
}
|
|
}
|
|
|
|
func (f *findDialogComponent) SetHeight(height int) {
|
|
f.height = height
|
|
}
|
|
|
|
func (f *findDialogComponent) IsEmpty() bool {
|
|
return f.list.IsEmpty()
|
|
}
|
|
|
|
func (f *findDialogComponent) SetProvider(provider CompletionProvider) {
|
|
f.completionProvider = provider
|
|
f.list.SetEmptyMessage(" " + provider.GetEmptyMessage())
|
|
f.list.SetItems([]CompletionItemI{})
|
|
}
|
|
|
|
func (f *findDialogComponent) selectFile(item CompletionItemI) tea.Cmd {
|
|
return tea.Sequence(
|
|
f.Close(),
|
|
util.CmdHandler(FindSelectedMsg{
|
|
FilePath: item.GetValue(),
|
|
}),
|
|
)
|
|
}
|
|
|
|
func (f *findDialogComponent) Render(background string) string {
|
|
return f.modal.Render(f.View(), background)
|
|
}
|
|
|
|
func (f *findDialogComponent) Close() tea.Cmd {
|
|
f.textInput.Reset()
|
|
f.textInput.Blur()
|
|
return util.CmdHandler(modal.CloseModalMsg{})
|
|
}
|
|
|
|
func createTextInput(existing *textinput.Model) textinput.Model {
|
|
t := theme.CurrentTheme()
|
|
bgColor := t.BackgroundPanel()
|
|
textColor := t.Text()
|
|
textMutedColor := t.TextMuted()
|
|
|
|
ti := textinput.New()
|
|
|
|
ti.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
|
|
ti.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
|
ti.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
|
|
ti.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
|
ti.Styles.Cursor.Color = t.Primary()
|
|
ti.VirtualCursor = true
|
|
|
|
ti.Prompt = " "
|
|
ti.CharLimit = -1
|
|
ti.Focus()
|
|
|
|
if existing != nil {
|
|
ti.SetValue(existing.Value())
|
|
ti.SetWidth(existing.Width())
|
|
}
|
|
|
|
return ti
|
|
}
|
|
|
|
func NewFindDialog(completionProvider CompletionProvider) FindDialog {
|
|
ti := createTextInput(nil)
|
|
|
|
li := list.NewListComponent(
|
|
[]CompletionItemI{},
|
|
10, // max visible items
|
|
completionProvider.GetEmptyMessage(),
|
|
false,
|
|
)
|
|
|
|
// Load initial items
|
|
go func() {
|
|
items, err := completionProvider.GetChildEntries("")
|
|
if err != nil {
|
|
slog.Error("Failed to get completion items", "error", err)
|
|
}
|
|
li.SetItems(items)
|
|
}()
|
|
|
|
return &findDialogComponent{
|
|
query: "",
|
|
completionProvider: completionProvider,
|
|
textInput: ti,
|
|
list: li,
|
|
modal: modal.New(
|
|
modal.WithTitle("Find Files"),
|
|
modal.WithMaxWidth(80),
|
|
),
|
|
}
|
|
}
|