Skip to content

Commit 72717f0

Browse files
joshbarringtonkrissetto
authored andcommitted
ensure @ input completion maintains expected behaviour
1 parent cb623bb commit 72717f0

2 files changed

Lines changed: 126 additions & 26 deletions

File tree

pkg/tui/components/editor/completion_autosubmit_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,102 @@ func TestEditorHandlesAutoSubmit(t *testing.T) {
8484
// It should also clear the trigger and completion word from textarea
8585
assert.Empty(t, e.textarea.Value(), "should clear the trigger and completion word")
8686
})
87+
88+
t.Run("@ completion inserts value even if AutoSubmit is true", func(t *testing.T) {
89+
t.Parallel()
90+
91+
e := newTestEditor("@he", "he")
92+
e.currentCompletion = &mockCompletion{trigger: "@"}
93+
94+
msg := completion.SelectedMsg{
95+
Value: "@hello",
96+
AutoSubmit: true,
97+
}
98+
99+
_, cmd := e.Update(msg)
100+
101+
// Command should be nil because atCompletion is true, preventing AutoSubmit behavior
102+
assert.Nil(t, cmd)
103+
104+
// Value should have trigger replaced with selected value and a space appended
105+
assert.Equal(t, "@hello ", e.textarea.Value())
106+
})
107+
108+
t.Run("@ completion adds file attachment", func(t *testing.T) {
109+
t.Parallel()
110+
111+
e := newTestEditor("@main.go", "main.go")
112+
e.currentCompletion = &mockCompletion{trigger: "@"}
113+
114+
// Use a real file that exists
115+
msg := completion.SelectedMsg{
116+
Value: "@editor.go",
117+
AutoSubmit: false,
118+
}
119+
120+
_, cmd := e.Update(msg)
121+
assert.Nil(t, cmd)
122+
123+
// Value should have trigger replaced with selected value and a space appended
124+
assert.Equal(t, "@editor.go ", e.textarea.Value())
125+
126+
// File should be tracked as attachment
127+
require.Len(t, e.attachments, 1)
128+
assert.Equal(t, "@editor.go", e.attachments[0].placeholder)
129+
assert.False(t, e.attachments[0].isTemp)
130+
})
131+
132+
t.Run("@ completion with Execute runs execute command even if AutoSubmit is false", func(t *testing.T) {
133+
t.Parallel()
134+
135+
e := newTestEditor("@he", "he")
136+
e.currentCompletion = &mockCompletion{trigger: "@"}
137+
138+
type testMsg struct{}
139+
msg := completion.SelectedMsg{
140+
Value: "@hello",
141+
AutoSubmit: false,
142+
Execute: func() tea.Cmd {
143+
return func() tea.Msg { return testMsg{} }
144+
},
145+
}
146+
147+
_, cmd := e.Update(msg)
148+
require.NotNil(t, cmd)
149+
150+
// Execute should return the provided command
151+
msgs := collectMsgs(cmd)
152+
require.Len(t, msgs, 1)
153+
_, ok := msgs[0].(testMsg)
154+
assert.True(t, ok, "should return the command from Execute")
155+
156+
// It should also clear the trigger and completion word from textarea
157+
assert.Empty(t, e.textarea.Value(), "should clear the trigger and completion word")
158+
})
159+
160+
t.Run("@paste- completion sends message if AutoSubmit is true", func(t *testing.T) {
161+
t.Parallel()
162+
163+
e := newTestEditor("@paste", "paste")
164+
e.currentCompletion = &mockCompletion{trigger: "@"}
165+
166+
msg := completion.SelectedMsg{
167+
Value: "@paste-1",
168+
AutoSubmit: true,
169+
}
170+
171+
_, cmd := e.Update(msg)
172+
require.NotNil(t, cmd)
173+
174+
// Find SendMsg
175+
found := false
176+
for _, m := range collectMsgs(cmd) {
177+
if sm, ok := m.(messages.SendMsg); ok {
178+
assert.Equal(t, "@paste-1", sm.Content)
179+
found = true
180+
break
181+
}
182+
}
183+
assert.True(t, found, "should return SendMsg")
184+
})
87185
}

pkg/tui/components/editor/editor.go

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -651,48 +651,50 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
651651
return e, cmd
652652

653653
case completion.SelectedMsg:
654-
// If the item has an Execute function, run it instead of inserting text
655-
if msg.Execute != nil && msg.AutoSubmit {
656-
// Remove the trigger character and any typed completion word from the textarea
657-
// before executing. For example, typing "@" then selecting "Browse files..."
658-
// should remove the "@" so AttachFile doesn't produce a double "@@".
659-
if e.currentCompletion != nil {
660-
triggerWord := e.currentCompletion.Trigger() + e.completionWord
661-
currentValue := e.textarea.Value()
662-
if idx := strings.LastIndex(currentValue, triggerWord); idx >= 0 {
663-
e.textarea.SetValue(currentValue[:idx] + currentValue[idx+len(triggerWord):])
664-
e.textarea.MoveToEnd()
665-
}
654+
if e.currentCompletion == nil {
655+
return e, nil
656+
}
657+
658+
atCompletion := e.currentCompletion.Trigger() == "@" && !strings.HasPrefix(msg.Value, "@paste-")
659+
triggerWord := e.currentCompletion.Trigger() + e.completionWord
660+
currentValue := e.textarea.Value()
661+
idx := strings.LastIndex(currentValue, triggerWord)
662+
663+
// Handle Execute functions (e.g., "Browse files...")
664+
// There is an execute function AND you hit enter, or there is an @ directive
665+
if msg.Execute != nil && (msg.AutoSubmit || atCompletion) {
666+
if idx >= 0 {
667+
e.textarea.SetValue(currentValue[:idx] + currentValue[idx+len(triggerWord):])
668+
e.textarea.MoveToEnd()
666669
}
667670
e.clearSuggestion()
668671
return e, msg.Execute()
669672
}
670-
if msg.AutoSubmit {
671-
// For auto-submit completions (like commands), use the selected
672-
// command value (e.g., "/exit") instead of what the user typed
673-
// (e.g., "/e"). Append any extra text after the trigger word
674-
// to preserve arguments (e.g., "/export /tmp/file").
675-
triggerWord := e.currentCompletion.Trigger() + e.completionWord
673+
674+
// Handle Auto-Submit items (e.g., commands like "/exit")
675+
if msg.AutoSubmit && !atCompletion {
676676
extraText := ""
677-
if _, after, found := strings.Cut(e.textarea.Value(), triggerWord); found {
678-
extraText = after
677+
if idx >= 0 {
678+
extraText = currentValue[idx+len(triggerWord):]
679679
}
680680
cmd := e.resetAndSend(msg.Value + extraText)
681681
return e, cmd
682682
}
683-
// For non-auto-submit completions (like file paths), replace the completion word
684-
currentValue := e.textarea.Value()
685-
if lastIdx := strings.LastIndex(currentValue, e.completionWord); lastIdx >= 0 {
686-
newValue := currentValue[:lastIdx-1] + msg.Value + " " + currentValue[lastIdx+len(e.completionWord):]
683+
684+
// Insert standard completions (e.g., file paths or text pastes)
685+
if idx >= 0 {
686+
newValue := currentValue[:idx] + msg.Value + " " + currentValue[idx+len(triggerWord):]
687687
e.textarea.SetValue(newValue)
688688
e.textarea.MoveToEnd()
689689
}
690-
// Track file references when using @ completion (but not paste placeholders)
691-
if e.currentCompletion != nil && e.currentCompletion.Trigger() == "@" && !strings.HasPrefix(msg.Value, "@paste-") {
690+
691+
// Track valid file references
692+
if atCompletion {
692693
if err := e.addFileAttachment(msg.Value); err != nil {
693694
slog.Warn("failed to add file attachment from completion", "value", msg.Value, "error", err)
694695
}
695696
}
697+
696698
e.clearSuggestion()
697699
return e, nil
698700
case completion.ClosedMsg:

0 commit comments

Comments
 (0)