From 7cb35401e26514a3c36e45bb2068a39e9a4c8e9d Mon Sep 17 00:00:00 2001 From: Gubarz <1037896+Gubarz@users.noreply.github.com> Date: Sun, 31 May 2026 12:55:34 -0600 Subject: [PATCH 1/3] fix(ui): added reverse tab completion --- internal/ui/var_resolve_keys.go | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/internal/ui/var_resolve_keys.go b/internal/ui/var_resolve_keys.go index 79df803..a6538e1 100644 --- a/internal/ui/var_resolve_keys.go +++ b/internal/ui/var_resolve_keys.go @@ -41,7 +41,9 @@ func (m *mainModel) handleVarResolveKey(msg tea.KeyMsg) tea.Cmd { } return nil case "tab": - return m.handleVarResolveTab(msg) + return m.handleVarResolveTab(msg, 1) + case "shift+tab": + return m.handleVarResolveTab(msg, -1) case " ": if cmd := m.handleVarResolveSpace(msg); cmd != nil { return cmd @@ -85,8 +87,8 @@ func (m *mainModel) handleVarResolveNavKey(msg tea.KeyMsg) tea.Cmd { return nil } -func (m *mainModel) handleVarResolveTab(msg tea.KeyMsg) tea.Cmd { - if m.completePathFromInput() { +func (m *mainModel) handleVarResolveTab(msg tea.KeyMsg, dir int) tea.Cmd { + if m.completePathFromInput(dir) { return func() tea.Msg { return nil } // bypass text input to preserve completion state } if !m.varState.isPromptOnly && m.varState.picker != nil { @@ -143,7 +145,7 @@ func (m *mainModel) handleVarResolveDefaultKey(msg tea.KeyMsg) tea.Cmd { return nil } -func (m *mainModel) completePathFromInput() bool { +func (m *mainModel) completePathFromInput(dir int) bool { if m.varState == nil { return false } @@ -151,11 +153,20 @@ func (m *mainModel) completePathFromInput() bool { // If path picker is active, this is a tab cycle request if m.varState.pathPicker != nil && len(m.varState.pathPicker.Filtered) > 0 { p := m.varState.pathPicker - if p.Cursor >= len(p.Filtered)-1 { - p.Cursor = 0 - p.Offset = 0 + if dir > 0 { + if p.Cursor >= len(p.Filtered)-1 { + p.Cursor = 0 + p.Offset = 0 + } else { + p.MoveCursor(1) + } } else { - p.MoveCursor(1) + if p.Cursor <= 0 { + p.Cursor = len(p.Filtered) - 1 + p.Offset = max(0, p.Cursor-9) + } else { + p.MoveCursor(-1) + } } if opt, ok := p.Selected(); ok { From 425a77331fb8b4d4a1ffddccb3c4601cc8ad1efb Mon Sep 17 00:00:00 2001 From: Gubarz <1037896+Gubarz@users.noreply.github.com> Date: Sun, 31 May 2026 13:03:04 -0600 Subject: [PATCH 2/3] fix(ui): improve list tab completion with path fallback --- internal/ui/var_resolve_keys.go | 93 +++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/internal/ui/var_resolve_keys.go b/internal/ui/var_resolve_keys.go index a6538e1..9a1a19f 100644 --- a/internal/ui/var_resolve_keys.go +++ b/internal/ui/var_resolve_keys.go @@ -91,10 +91,34 @@ func (m *mainModel) handleVarResolveTab(msg tea.KeyMsg, dir int) tea.Cmd { if m.completePathFromInput(dir) { return func() tea.Msg { return nil } // bypass text input to preserve completion state } - if !m.varState.isPromptOnly && m.varState.picker != nil { - if opt, ok := m.varState.picker.Selected(); ok { - m.textInput.SetValue(opt.Display) - m.textInput.CursorEnd() + if !m.varState.isPromptOnly && m.varState.picker != nil && len(m.varState.picker.Filtered) > 0 { + p := m.varState.picker + if opt, ok := p.Selected(); ok { + if m.textInput.Value() == opt.Display { + if dir > 0 { + if p.Cursor >= len(p.Filtered)-1 { + p.Cursor = 0 + p.Offset = 0 + } else { + p.MoveCursor(1) + } + } else { + if p.Cursor <= 0 { + p.Cursor = len(p.Filtered) - 1 + p.Offset = max(0, p.Cursor-9) + } else { + p.MoveCursor(-1) + } + } + } else if dir < 0 { + p.Cursor = len(p.Filtered) - 1 + p.Offset = max(0, p.Cursor-9) + } + + if newOpt, newOk := p.Selected(); newOk { + m.textInput.SetValue(newOpt.Display) + m.textInput.CursorEnd() + } return func() tea.Msg { return nil } // bypass text input } } @@ -153,42 +177,55 @@ func (m *mainModel) completePathFromInput(dir int) bool { // If path picker is active, this is a tab cycle request if m.varState.pathPicker != nil && len(m.varState.pathPicker.Filtered) > 0 { p := m.varState.pathPicker - if dir > 0 { - if p.Cursor >= len(p.Filtered)-1 { - p.Cursor = 0 - p.Offset = 0 - } else { - p.MoveCursor(1) + if opt, ok := p.Selected(); ok { + runes := []rune(m.textInput.Value()) + var currentToken string + if m.varState.pathTokenStart >= 0 && m.varState.pathTokenEnd <= len(runes) && m.varState.pathTokenStart <= m.varState.pathTokenEnd { + currentToken = string(runes[m.varState.pathTokenStart:m.varState.pathTokenEnd]) } - } else { - if p.Cursor <= 0 { + + if currentToken == opt.Token { + if dir > 0 { + if p.Cursor >= len(p.Filtered)-1 { + p.Cursor = 0 + p.Offset = 0 + } else { + p.MoveCursor(1) + } + } else { + if p.Cursor <= 0 { + p.Cursor = len(p.Filtered) - 1 + p.Offset = max(0, p.Cursor-9) + } else { + p.MoveCursor(-1) + } + } + } else if dir < 0 { p.Cursor = len(p.Filtered) - 1 p.Offset = max(0, p.Cursor-9) - } else { - p.MoveCursor(-1) } - } - if opt, ok := p.Selected(); ok { - runes := []rune(m.textInput.Value()) - newRunes := make([]rune, 0) - newRunes = append(newRunes, runes[:m.varState.pathTokenStart]...) - newRunes = append(newRunes, []rune(opt.Token)...) - newCursor := len(newRunes) - if m.varState.pathTokenEnd < len(runes) { - newRunes = append(newRunes, runes[m.varState.pathTokenEnd:]...) - } + if newOpt, newOk := p.Selected(); newOk { + newRunes := make([]rune, 0) + newRunes = append(newRunes, runes[:m.varState.pathTokenStart]...) + newRunes = append(newRunes, []rune(newOpt.Token)...) + newCursor := len(newRunes) + if m.varState.pathTokenEnd < len(runes) { + newRunes = append(newRunes, runes[m.varState.pathTokenEnd:]...) + } - m.textInput.SetValue(string(newRunes)) - m.textInput.SetCursor(newCursor) - m.varState.pathTokenEnd = newCursor + m.textInput.SetValue(string(newRunes)) + m.textInput.SetCursor(newCursor) + m.varState.pathTokenEnd = newCursor + } } return true } value := m.textInput.Value() cursor := m.textInput.Position() - if !m.varState.isPromptOnly && !looksPathLikeInput(value, cursor) { + forcePath := m.varState.isPromptOnly || (m.varState.picker != nil && len(m.varState.picker.Filtered) == 0) + if !forcePath && !looksPathLikeInput(value, cursor) { return false } result, ok := completePathValue(value, cursor) From 5eb49708da36071abb86e95d85e2e38f412b2bb8 Mon Sep 17 00:00:00 2001 From: Gubarz <1037896+Gubarz@users.noreply.github.com> Date: Sun, 31 May 2026 13:03:32 -0600 Subject: [PATCH 3/3] fix(ui): improve list tab completion with path fallback --- internal/ui/var_resolve_keys.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ui/var_resolve_keys.go b/internal/ui/var_resolve_keys.go index 9a1a19f..6acdbdd 100644 --- a/internal/ui/var_resolve_keys.go +++ b/internal/ui/var_resolve_keys.go @@ -114,7 +114,7 @@ func (m *mainModel) handleVarResolveTab(msg tea.KeyMsg, dir int) tea.Cmd { p.Cursor = len(p.Filtered) - 1 p.Offset = max(0, p.Cursor-9) } - + if newOpt, newOk := p.Selected(); newOk { m.textInput.SetValue(newOpt.Display) m.textInput.CursorEnd()