fix(tui): rework lists and search dialog

This commit is contained in:
adamdotdevin
2025-07-15 08:07:20 -05:00
parent b5c85d3806
commit 533f64fe26
16 changed files with 579 additions and 972 deletions

View File

@@ -8,7 +8,6 @@ import (
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
@@ -17,7 +16,7 @@ type CommandCompletionProvider struct {
app *app.App
}
func NewCommandCompletionProvider(app *app.App) dialog.CompletionProvider {
func NewCommandCompletionProvider(app *app.App) CompletionProvider {
return &CommandCompletionProvider{app: app}
}
@@ -32,24 +31,28 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
func (c *CommandCompletionProvider) getCommandCompletionItem(
cmd commands.Command,
space int,
t theme.Theme,
) dialog.CompletionItemI {
spacer := strings.Repeat(" ", space)
title := " /" + cmd.PrimaryTrigger() + styles.NewStyle().
Foreground(t.TextMuted()).
Render(spacer+cmd.Description)
) CompletionSuggestion {
displayFunc := func(s styles.Style) string {
t := theme.CurrentTheme()
spacer := strings.Repeat(" ", space)
display := " /" + cmd.PrimaryTrigger() + s.
Foreground(t.TextMuted()).
Render(spacer+cmd.Description)
return display
}
value := string(cmd.Name)
return dialog.NewCompletionItem(dialog.CompletionItem{
Title: title,
return CompletionSuggestion{
Display: displayFunc,
Value: value,
ProviderID: c.GetId(),
}, dialog.WithBackgroundColor(t.BackgroundElement()))
RawData: cmd,
}
}
func (c *CommandCompletionProvider) GetChildEntries(
query string,
) ([]dialog.CompletionItemI, error) {
t := theme.CurrentTheme()
) ([]CompletionSuggestion, error) {
commands := c.app.Commands
space := 1
@@ -63,20 +66,20 @@ func (c *CommandCompletionProvider) GetChildEntries(
sorted := commands.Sorted()
if query == "" {
// If no query, return all commands
items := []dialog.CompletionItemI{}
items := []CompletionSuggestion{}
for _, cmd := range sorted {
if !cmd.HasTrigger() {
continue
}
space := space - lipgloss.Width(cmd.PrimaryTrigger())
items = append(items, c.getCommandCompletionItem(cmd, space, t))
items = append(items, c.getCommandCompletionItem(cmd, space))
}
return items, nil
}
// Use fuzzy matching for commands
var commandNames []string
commandMap := make(map[string]dialog.CompletionItemI)
commandMap := make(map[string]CompletionSuggestion)
for _, cmd := range sorted {
if !cmd.HasTrigger() {
@@ -86,7 +89,7 @@ func (c *CommandCompletionProvider) GetChildEntries(
// Add all triggers as searchable options
for _, trigger := range cmd.Trigger {
commandNames = append(commandNames, trigger)
commandMap[trigger] = c.getCommandCompletionItem(cmd, space, t)
commandMap[trigger] = c.getCommandCompletionItem(cmd, space)
}
}
@@ -97,13 +100,13 @@ func (c *CommandCompletionProvider) GetChildEntries(
sort.Sort(matches)
// Convert matches to completion items, deduplicating by command name
items := []dialog.CompletionItemI{}
items := []CompletionSuggestion{}
seen := make(map[string]bool)
for _, match := range matches {
if item, ok := commandMap[match.Target]; ok {
// Use the command's value (name) as the deduplication key
if !seen[item.GetValue()] {
seen[item.GetValue()] = true
if !seen[item.Value] {
seen[item.Value] = true
items = append(items, item)
}
}

View File

@@ -9,14 +9,13 @@ import (
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type filesContextGroup struct {
app *app.App
gitFiles []dialog.CompletionItemI
gitFiles []CompletionSuggestion
}
func (cg *filesContextGroup) GetId() string {
@@ -27,12 +26,8 @@ func (cg *filesContextGroup) GetEmptyMessage() string {
return "no matching files"
}
func (cg *filesContextGroup) getGitFiles() []dialog.CompletionItemI {
t := theme.CurrentTheme()
items := make([]dialog.CompletionItemI, 0)
base := styles.NewStyle().Background(t.BackgroundElement())
green := base.Foreground(t.Success()).Render
red := base.Foreground(t.Error()).Render
func (cg *filesContextGroup) getGitFiles() []CompletionSuggestion {
items := make([]CompletionSuggestion, 0)
status, _ := cg.app.Client.File.Status(context.Background())
if status != nil {
@@ -42,21 +37,25 @@ func (cg *filesContextGroup) getGitFiles() []dialog.CompletionItemI {
})
for _, file := range files {
title := file.Path
if file.Added > 0 {
title += green(" +" + strconv.Itoa(int(file.Added)))
displayFunc := func(s styles.Style) string {
t := theme.CurrentTheme()
green := s.Foreground(t.Success()).Render
red := s.Foreground(t.Error()).Render
display := file.Path
if file.Added > 0 {
display += green(" +" + strconv.Itoa(int(file.Added)))
}
if file.Removed > 0 {
display += red(" -" + strconv.Itoa(int(file.Removed)))
}
return display
}
if file.Removed > 0 {
title += red(" -" + strconv.Itoa(int(file.Removed)))
}
item := dialog.NewCompletionItem(dialog.CompletionItem{
Title: title,
item := CompletionSuggestion{
Display: displayFunc,
Value: file.Path,
ProviderID: cg.GetId(),
Raw: file,
},
dialog.WithBackgroundColor(t.BackgroundElement()),
)
RawData: file,
}
items = append(items, item)
}
}
@@ -66,8 +65,8 @@ func (cg *filesContextGroup) getGitFiles() []dialog.CompletionItemI {
func (cg *filesContextGroup) GetChildEntries(
query string,
) ([]dialog.CompletionItemI, error) {
items := make([]dialog.CompletionItemI, 0)
) ([]CompletionSuggestion, error) {
items := make([]CompletionSuggestion, 0)
query = strings.TrimSpace(query)
if query == "" {
@@ -89,7 +88,7 @@ func (cg *filesContextGroup) GetChildEntries(
for _, file := range *files {
exists := false
for _, existing := range cg.gitFiles {
if existing.GetValue() == file {
if existing.Value == file {
if query != "" {
items = append(items, existing)
}
@@ -97,14 +96,18 @@ func (cg *filesContextGroup) GetChildEntries(
}
}
if !exists {
item := dialog.NewCompletionItem(dialog.CompletionItem{
Title: file,
displayFunc := func(s styles.Style) string {
// t := theme.CurrentTheme()
// return s.Foreground(t.Text()).Render(file)
return s.Render(file)
}
item := CompletionSuggestion{
Display: displayFunc,
Value: file,
ProviderID: cg.GetId(),
Raw: file,
},
dialog.WithBackgroundColor(theme.CurrentTheme().BackgroundElement()),
)
RawData: file,
}
items = append(items, item)
}
}
@@ -112,7 +115,7 @@ func (cg *filesContextGroup) GetChildEntries(
return items, nil
}
func NewFileContextGroup(app *app.App) dialog.CompletionProvider {
func NewFileContextGroup(app *app.App) CompletionProvider {
cg := &filesContextGroup{
app: app,
}

View File

@@ -0,0 +1,8 @@
package completions
// CompletionProvider defines the interface for completion data providers
type CompletionProvider interface {
GetId() string
GetChildEntries(query string) ([]CompletionSuggestion, error)
GetEmptyMessage() string
}

View File

@@ -0,0 +1,24 @@
package completions
import "github.com/sst/opencode/internal/styles"
// CompletionSuggestion represents a data-only completion suggestion
// with no styling or rendering logic
type CompletionSuggestion struct {
// The text to be displayed in the list. May contain minimal inline
// ANSI styling if intrinsic to the data (e.g., git diff colors).
Display func(styles.Style) string
// The value to be used when the item is selected (e.g., inserted into the editor).
Value string
// An optional, longer description to be displayed.
Description string
// The ID of the provider that generated this suggestion.
ProviderID string
// The raw, underlying data object (e.g., opencode.Symbol, commands.Command).
// This allows the selection handler to perform rich actions.
RawData any
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
@@ -58,8 +57,8 @@ const (
func (cg *symbolsContextGroup) GetChildEntries(
query string,
) ([]dialog.CompletionItemI, error) {
items := make([]dialog.CompletionItemI, 0)
) ([]CompletionSuggestion, error) {
items := make([]CompletionSuggestion, 0)
query = strings.TrimSpace(query)
if query == "" {
@@ -78,40 +77,42 @@ func (cg *symbolsContextGroup) GetChildEntries(
return items, nil
}
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
base := baseStyle.Render
muted := baseStyle.Foreground(t.TextMuted()).Render
for _, sym := range *symbols {
parts := strings.Split(sym.Name, ".")
lastPart := parts[len(parts)-1]
title := base(lastPart)
uriParts := strings.Split(sym.Location.Uri, "/")
lastTwoParts := uriParts[len(uriParts)-2:]
joined := strings.Join(lastTwoParts, "/")
title += muted(fmt.Sprintf(" %s", joined))
start := int(sym.Location.Range.Start.Line)
end := int(sym.Location.Range.End.Line)
title += muted(fmt.Sprintf(":L%d-%d", start, end))
displayFunc := func(s styles.Style) string {
t := theme.CurrentTheme()
base := s.Foreground(t.Text()).Render
muted := s.Foreground(t.TextMuted()).Render
display := base(lastPart)
uriParts := strings.Split(sym.Location.Uri, "/")
lastTwoParts := uriParts[len(uriParts)-2:]
joined := strings.Join(lastTwoParts, "/")
display += muted(fmt.Sprintf(" %s", joined))
display += muted(fmt.Sprintf(":L%d-%d", start, end))
return display
}
value := fmt.Sprintf("%s?start=%d&end=%d", sym.Location.Uri, start, end)
item := dialog.NewCompletionItem(dialog.CompletionItem{
Title: title,
item := CompletionSuggestion{
Display: displayFunc,
Value: value,
ProviderID: cg.GetId(),
Raw: sym,
})
RawData: sym,
}
items = append(items, item)
}
return items, nil
}
func NewSymbolsContextGroup(app *app.App) dialog.CompletionProvider {
func NewSymbolsContextGroup(app *app.App) CompletionProvider {
return &symbolsContextGroup{
app: app,
}