mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 02:04:22 +01:00
feat(complete-module): add completions logic, dialog and providers
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
utilComponents "github.com/sst/opencode/internal/tui/components/util"
|
||||
"github.com/sst/opencode/internal/tui/layout"
|
||||
"github.com/sst/opencode/internal/tui/styles"
|
||||
"github.com/sst/opencode/internal/tui/theme"
|
||||
@@ -18,6 +19,33 @@ type Command struct {
|
||||
Handler func(cmd Command) tea.Cmd
|
||||
}
|
||||
|
||||
func (ci Command) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
descStyle := baseStyle.Width(width).Foreground(t.TextMuted())
|
||||
itemStyle := baseStyle.Width(width).
|
||||
Foreground(t.Text()).
|
||||
Background(t.Background())
|
||||
|
||||
if selected {
|
||||
itemStyle = itemStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Bold(true)
|
||||
descStyle = descStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background())
|
||||
}
|
||||
|
||||
title := itemStyle.Padding(0, 1).Render(ci.Title)
|
||||
if ci.Description != "" {
|
||||
description := descStyle.Padding(0, 1).Render(ci.Description)
|
||||
return lipgloss.JoinVertical(lipgloss.Left, title, description)
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
// CommandSelectedMsg is sent when a command is selected
|
||||
type CommandSelectedMsg struct {
|
||||
Command Command
|
||||
@@ -31,35 +59,20 @@ type CommandDialog interface {
|
||||
tea.Model
|
||||
layout.Bindings
|
||||
SetCommands(commands []Command)
|
||||
SetSelectedCommand(commandID string)
|
||||
}
|
||||
|
||||
type commandDialogCmp struct {
|
||||
commands []Command
|
||||
selectedIdx int
|
||||
width int
|
||||
height int
|
||||
selectedCommandID string
|
||||
listView utilComponents.SimpleList[Command]
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
type commandKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Enter key.Binding
|
||||
Escape key.Binding
|
||||
J key.Binding
|
||||
K key.Binding
|
||||
}
|
||||
|
||||
var commandKeys = commandKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithHelp("↑", "previous command"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithHelp("↓", "next command"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "select command"),
|
||||
@@ -68,38 +81,22 @@ var commandKeys = commandKeyMap{
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "close"),
|
||||
),
|
||||
J: key.NewBinding(
|
||||
key.WithKeys("j"),
|
||||
key.WithHelp("j", "next command"),
|
||||
),
|
||||
K: key.NewBinding(
|
||||
key.WithKeys("k"),
|
||||
key.WithHelp("k", "previous command"),
|
||||
),
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) Init() tea.Cmd {
|
||||
return nil
|
||||
return c.listView.Init()
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, commandKeys.Up) || key.Matches(msg, commandKeys.K):
|
||||
if c.selectedIdx > 0 {
|
||||
c.selectedIdx--
|
||||
}
|
||||
return c, nil
|
||||
case key.Matches(msg, commandKeys.Down) || key.Matches(msg, commandKeys.J):
|
||||
if c.selectedIdx < len(c.commands)-1 {
|
||||
c.selectedIdx++
|
||||
}
|
||||
return c, nil
|
||||
case key.Matches(msg, commandKeys.Enter):
|
||||
if len(c.commands) > 0 {
|
||||
selectedItem, idx := c.listView.GetSelectedItem()
|
||||
if idx != -1 {
|
||||
return c, util.CmdHandler(CommandSelectedMsg{
|
||||
Command: c.commands[c.selectedIdx],
|
||||
Command: selectedItem,
|
||||
})
|
||||
}
|
||||
case key.Matches(msg, commandKeys.Escape):
|
||||
@@ -109,78 +106,35 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
c.width = msg.Width
|
||||
c.height = msg.Height
|
||||
}
|
||||
return c, nil
|
||||
|
||||
u, cmd := c.listView.Update(msg)
|
||||
c.listView = u.(utilComponents.SimpleList[Command])
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return c, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
if len(c.commands) == 0 {
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(40).
|
||||
Render("No commands available")
|
||||
}
|
||||
maxWidth := 40
|
||||
|
||||
// Calculate max width needed for command titles
|
||||
maxWidth := 40 // Minimum width
|
||||
for _, cmd := range c.commands {
|
||||
if len(cmd.Title) > maxWidth-4 { // Account for padding
|
||||
commands := c.listView.GetItems()
|
||||
|
||||
for _, cmd := range commands {
|
||||
if len(cmd.Title) > maxWidth-4 {
|
||||
maxWidth = len(cmd.Title) + 4
|
||||
}
|
||||
if len(cmd.Description) > maxWidth-4 {
|
||||
maxWidth = len(cmd.Description) + 4
|
||||
}
|
||||
}
|
||||
|
||||
// Limit height to avoid taking up too much screen space
|
||||
maxVisibleCommands := min(10, len(c.commands))
|
||||
|
||||
// Build the command list
|
||||
commandItems := make([]string, 0, maxVisibleCommands)
|
||||
startIdx := 0
|
||||
|
||||
// If we have more commands than can be displayed, adjust the start index
|
||||
if len(c.commands) > maxVisibleCommands {
|
||||
// Center the selected item when possible
|
||||
halfVisible := maxVisibleCommands / 2
|
||||
if c.selectedIdx >= halfVisible && c.selectedIdx < len(c.commands)-halfVisible {
|
||||
startIdx = c.selectedIdx - halfVisible
|
||||
} else if c.selectedIdx >= len(c.commands)-halfVisible {
|
||||
startIdx = len(c.commands) - maxVisibleCommands
|
||||
}
|
||||
}
|
||||
|
||||
endIdx := min(startIdx+maxVisibleCommands, len(c.commands))
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
cmd := c.commands[i]
|
||||
itemStyle := baseStyle.Width(maxWidth)
|
||||
descStyle := baseStyle.Width(maxWidth).Foreground(t.TextMuted())
|
||||
|
||||
if i == c.selectedIdx {
|
||||
itemStyle = itemStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Bold(true)
|
||||
descStyle = descStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background())
|
||||
}
|
||||
|
||||
title := itemStyle.Padding(0, 1).Render(cmd.Title)
|
||||
description := ""
|
||||
if cmd.Description != "" {
|
||||
description = descStyle.Padding(0, 1).Render(cmd.Description)
|
||||
commandItems = append(commandItems, lipgloss.JoinVertical(lipgloss.Left, title, description))
|
||||
} else {
|
||||
commandItems = append(commandItems, title)
|
||||
if len(cmd.Description) > maxWidth-4 {
|
||||
maxWidth = len(cmd.Description) + 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.listView.SetMaxWidth(maxWidth)
|
||||
|
||||
title := baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
@@ -192,7 +146,7 @@ func (c *commandDialogCmp) View() string {
|
||||
lipgloss.Left,
|
||||
title,
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
|
||||
baseStyle.Width(maxWidth).Render(c.listView.View()),
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
)
|
||||
|
||||
@@ -209,41 +163,18 @@ func (c *commandDialogCmp) BindingKeys() []key.Binding {
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) SetCommands(commands []Command) {
|
||||
c.commands = commands
|
||||
|
||||
// If we have a selected command ID, find its index
|
||||
if c.selectedCommandID != "" {
|
||||
for i, cmd := range commands {
|
||||
if cmd.ID == c.selectedCommandID {
|
||||
c.selectedIdx = i
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to first command if selected not found
|
||||
c.selectedIdx = 0
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) SetSelectedCommand(commandID string) {
|
||||
c.selectedCommandID = commandID
|
||||
|
||||
// Update the selected index if commands are already loaded
|
||||
if len(c.commands) > 0 {
|
||||
for i, cmd := range c.commands {
|
||||
if cmd.ID == commandID {
|
||||
c.selectedIdx = i
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
c.listView.SetItems(commands)
|
||||
}
|
||||
|
||||
// NewCommandDialogCmp creates a new command selection dialog
|
||||
func NewCommandDialogCmp() CommandDialog {
|
||||
listView := utilComponents.NewSimpleList[Command](
|
||||
[]Command{},
|
||||
10,
|
||||
"No commands available",
|
||||
true,
|
||||
)
|
||||
return &commandDialogCmp{
|
||||
commands: []Command{},
|
||||
selectedIdx: 0,
|
||||
selectedCommandID: "",
|
||||
listView: listView,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user