Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 61 additions & 40 deletions packages/tui/internal/components/dialog/complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type CompletionDialog interface {
tea.ViewModel
SetWidth(width int)
IsEmpty() bool
SetInitialText(text string) tea.Cmd
}

type completionDialogComponent struct {
Expand Down Expand Up @@ -64,53 +65,58 @@ func (c *completionDialogComponent) Init() tea.Cmd {
return nil
}

func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
return func() tea.Msg {
allItems := make([]completions.CompletionSuggestion, 0)
providersWithResults := 0
// buildCompletionList queries providers and builds a ranked list of completions
func (c *completionDialogComponent) buildCompletionList(query string) []completions.CompletionSuggestion {
allItems := make([]completions.CompletionSuggestion, 0)
providersWithResults := 0

// Collect results from all providers
for _, provider := range c.providers {
items, err := provider.GetChildEntries(query)
if err != nil {
slog.Error(
"Failed to get completion items",
"provider",
provider.GetId(),
"error",
err,
)
continue
}
if len(items) > 0 {
providersWithResults++
allItems = append(allItems, items...)
}
}

// Collect results from all providers
for _, provider := range c.providers {
items, err := provider.GetChildEntries(query)
if err != nil {
slog.Error(
"Failed to get completion items",
"provider",
provider.GetId(),
"error",
err,
)
continue
}
if len(items) > 0 {
providersWithResults++
allItems = append(allItems, items...)
}
// If there's a query, use fuzzy ranking to sort results
if query != "" && providersWithResults > 1 {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
// Create a slice of display values for fuzzy matching
displayValues := make([]string, len(allItems))
for i, item := range allItems {
displayValues[i] = item.Display(baseStyle)
}

// If there's a query, use fuzzy ranking to sort results
if query != "" && providersWithResults > 1 {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
// Create a slice of display values for fuzzy matching
displayValues := make([]string, len(allItems))
for i, item := range allItems {
displayValues[i] = item.Display(baseStyle)
}
matches := fuzzy.RankFindFold(query, displayValues)
sort.Sort(matches)

matches := fuzzy.RankFindFold(query, displayValues)
sort.Sort(matches)
// Reorder items based on fuzzy ranking
rankedItems := make([]completions.CompletionSuggestion, 0, len(matches))
for _, match := range matches {
rankedItems = append(rankedItems, allItems[match.OriginalIndex])
}

// Reorder items based on fuzzy ranking
rankedItems := make([]completions.CompletionSuggestion, 0, len(matches))
for _, match := range matches {
rankedItems = append(rankedItems, allItems[match.OriginalIndex])
}
return rankedItems
}

return rankedItems
}
return allItems
}

return allItems
func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
return func() tea.Msg {
return c.buildCompletionList(query)
}
}
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Expand Down Expand Up @@ -191,6 +197,21 @@ func (c *completionDialogComponent) IsEmpty() bool {
return c.list.IsEmpty()
}

func (c *completionDialogComponent) SetInitialText(text string) tea.Cmd {
c.pseudoSearchTextArea.SetValue(text)
query := strings.TrimPrefix(text, c.trigger)
c.query = query

// Use the shared completion logic
allItems := c.buildCompletionList(query)

// Update the list with filtered results immediately
c.list.SetItems(allItems)

// Return focus command like the normal flow does
return c.pseudoSearchTextArea.Focus()
}

func (c *completionDialogComponent) complete(item completions.CompletionSuggestion) tea.Cmd {
value := c.pseudoSearchTextArea.Value()
return tea.Batch(
Expand Down
28 changes: 28 additions & 0 deletions packages/tui/internal/components/textarea/textarea.go
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,34 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
break
}
if len(m.value[m.row]) > 0 && m.col > 0 {
if att, ok := m.value[m.row][m.col-1].(*Attachment); ok {
var filePath string
if att.Filename != "" {
filePath = att.Filename
} else {
filePath = strings.TrimPrefix(att.Display, "@")
}

fullText := "@" + filePath
if len(fullText) > 1 {
fullText = fullText[:len(fullText)-1]
}

m.value[m.row] = slices.Delete(m.value[m.row], m.col-1, m.col)

textRunes := []rune(fullText)
textInterfaces := make([]any, len(textRunes))
for i, r := range textRunes {
textInterfaces[i] = r
}
m.value[m.row] = slices.Insert(m.value[m.row], m.col-1, textInterfaces...)

m.SetCursorColumn(m.col - 1 + len(textRunes))

return m, func() tea.Msg {
return tea.KeyPressMsg{Text: fullText}
}
}
m.value[m.row] = slices.Delete(m.value[m.row], m.col-1, m.col)
m.SetCursorColumn(m.col - 1)
}
Expand Down
28 changes: 19 additions & 9 deletions packages/tui/internal/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,20 +167,30 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, tea.Sequence(cmds...)
}

// Handle file completions trigger
if keyString == "@" &&
// Handle file completions trigger - both single @ and text starting with @
if (keyString == "@" || strings.HasPrefix(msg.Text, "@")) &&
!a.showCompletionDialog {
a.showCompletionDialog = true

updated, cmd := a.editor.Update(msg)
a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd)
if len(msg.Text) == 1 || keyString == "@" {
updated, cmd := a.editor.Update(msg)
a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd)
}

// Set both file and symbols providers for @ completion
a.completions = dialog.NewCompletionDialogComponent("@", a.fileProvider, a.symbolsProvider)
updated, cmd = a.completions.Update(msg)
a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd)

// If it's more than just "@", set the initial text and handle the returned command
if len(msg.Text) > 1 && strings.HasPrefix(msg.Text, "@") {
cmd := a.completions.SetInitialText(msg.Text)
cmds = append(cmds, cmd)
// Don't call Update - SetInitialText already handled the text and search
} else {
// Normal single "@" keypress - call Update to process the keypress
updated, cmd := a.completions.Update(msg)
a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd)
}

return a, tea.Sequence(cmds...)
}
Expand Down