Skip to content

Commit de47ef7

Browse files
ensure @ input completion maintains expected behaviour
1 parent c52d9f5 commit de47ef7

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
@@ -638,48 +638,50 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
638638
return e, cmd
639639

640640
case completion.SelectedMsg:
641-
// If the item has an Execute function, run it instead of inserting text
642-
if msg.Execute != nil && msg.AutoSubmit {
643-
// Remove the trigger character and any typed completion word from the textarea
644-
// before executing. For example, typing "@" then selecting "Browse files..."
645-
// should remove the "@" so AttachFile doesn't produce a double "@@".
646-
if e.currentCompletion != nil {
647-
triggerWord := e.currentCompletion.Trigger() + e.completionWord
648-
currentValue := e.textarea.Value()
649-
if idx := strings.LastIndex(currentValue, triggerWord); idx >= 0 {
650-
e.textarea.SetValue(currentValue[:idx] + currentValue[idx+len(triggerWord):])
651-
e.textarea.MoveToEnd()
652-
}
641+
if e.currentCompletion == nil {
642+
return e, nil
643+
}
644+
645+
atCompletion := e.currentCompletion.Trigger() == "@" && !strings.HasPrefix(msg.Value, "@paste-")
646+
triggerWord := e.currentCompletion.Trigger() + e.completionWord
647+
currentValue := e.textarea.Value()
648+
idx := strings.LastIndex(currentValue, triggerWord)
649+
650+
// Handle Execute functions (e.g., "Browse files...")
651+
// There is an execute function AND you hit enter, or there is an @ directive
652+
if msg.Execute != nil && (msg.AutoSubmit || atCompletion) {
653+
if idx >= 0 {
654+
e.textarea.SetValue(currentValue[:idx] + currentValue[idx+len(triggerWord):])
655+
e.textarea.MoveToEnd()
653656
}
654657
e.clearSuggestion()
655658
return e, msg.Execute()
656659
}
657-
if msg.AutoSubmit {
658-
// For auto-submit completions (like commands), use the selected
659-
// command value (e.g., "/exit") instead of what the user typed
660-
// (e.g., "/e"). Append any extra text after the trigger word
661-
// to preserve arguments (e.g., "/export /tmp/file").
662-
triggerWord := e.currentCompletion.Trigger() + e.completionWord
660+
661+
// Handle Auto-Submit items (e.g., commands like "/exit")
662+
if msg.AutoSubmit && !atCompletion {
663663
extraText := ""
664-
if _, after, found := strings.Cut(e.textarea.Value(), triggerWord); found {
665-
extraText = after
664+
if idx >= 0 {
665+
extraText = currentValue[idx+len(triggerWord):]
666666
}
667667
cmd := e.resetAndSend(msg.Value + extraText)
668668
return e, cmd
669669
}
670-
// For non-auto-submit completions (like file paths), replace the completion word
671-
currentValue := e.textarea.Value()
672-
if lastIdx := strings.LastIndex(currentValue, e.completionWord); lastIdx >= 0 {
673-
newValue := currentValue[:lastIdx-1] + msg.Value + " " + currentValue[lastIdx+len(e.completionWord):]
670+
671+
// Insert standard completions (e.g., file paths or text pastes)
672+
if idx >= 0 {
673+
newValue := currentValue[:idx] + msg.Value + " " + currentValue[idx+len(triggerWord):]
674674
e.textarea.SetValue(newValue)
675675
e.textarea.MoveToEnd()
676676
}
677-
// Track file references when using @ completion (but not paste placeholders)
678-
if e.currentCompletion != nil && e.currentCompletion.Trigger() == "@" && !strings.HasPrefix(msg.Value, "@paste-") {
677+
678+
// Track valid file references
679+
if atCompletion {
679680
if err := e.addFileAttachment(msg.Value); err != nil {
680681
slog.Warn("failed to add file attachment from completion", "value", msg.Value, "error", err)
681682
}
682683
}
684+
683685
e.clearSuggestion()
684686
return e, nil
685687
case completion.ClosedMsg:

0 commit comments

Comments
 (0)