@@ -29,23 +29,30 @@ type completionItemCmp struct {
2929 focus bool
3030 matchIndexes []int
3131 bgColor color.Color
32+ shortcut string
3233}
3334
34- type completionOptions func (* completionItemCmp )
35+ type CompletionOption func (* completionItemCmp )
3536
36- func WithBackgroundColor (c color.Color ) completionOptions {
37+ func WithBackgroundColor (c color.Color ) CompletionOption {
3738 return func (cmp * completionItemCmp ) {
3839 cmp .bgColor = c
3940 }
4041}
4142
42- func WithMatchIndexes (indexes ... int ) completionOptions {
43+ func WithMatchIndexes (indexes ... int ) CompletionOption {
4344 return func (cmp * completionItemCmp ) {
4445 cmp .matchIndexes = indexes
4546 }
4647}
4748
48- func NewCompletionItem (text string , value any , opts ... completionOptions ) CompletionItem {
49+ func WithShortcut (shortcut string ) CompletionOption {
50+ return func (cmp * completionItemCmp ) {
51+ cmp .shortcut = shortcut
52+ }
53+ }
54+
55+ func NewCompletionItem (text string , value any , opts ... CompletionOption ) CompletionItem {
4956 c := & completionItemCmp {
5057 text : text ,
5158 value : value ,
@@ -71,44 +78,64 @@ func (c *completionItemCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
7178func (c * completionItemCmp ) View () tea.View {
7279 t := styles .CurrentTheme ()
7380
74- titleStyle := t .S ().Text .Padding (0 , 1 ).Width (c .width )
81+ itemStyle := t .S ().Base .Padding (0 , 1 ).Width (c .width )
82+ innerWidth := c .width - 2 // Account for padding
83+
84+ if c .shortcut != "" {
85+ innerWidth -= lipgloss .Width (c .shortcut )
86+ }
87+
88+ titleStyle := t .S ().Text .Width (innerWidth )
7589 titleMatchStyle := t .S ().Text .Underline (true )
7690 if c .bgColor != nil {
7791 titleStyle = titleStyle .Background (c .bgColor )
7892 titleMatchStyle = titleMatchStyle .Background (c .bgColor )
7993 }
8094
8195 if c .focus {
82- titleStyle = t .S ().TextSelected .Padding ( 0 , 1 ). Width (c . width )
96+ titleStyle = t .S ().TextSelected .Width (innerWidth )
8397 titleMatchStyle = t .S ().TextSelected .Underline (true )
98+ itemStyle = itemStyle .Background (t .Primary )
8499 }
85100
86101 var truncatedTitle string
87- var adjustedMatchIndexes []int
88102
89- availableWidth := c .width - 2 // Account for padding
90- if len (c .matchIndexes ) > 0 && len (c .text ) > availableWidth {
103+ if len (c .matchIndexes ) > 0 && len (c .text ) > innerWidth {
91104 // Smart truncation: ensure the last matching part is visible
92- truncatedTitle , adjustedMatchIndexes = c .smartTruncate (c .text , availableWidth , c .matchIndexes )
105+ truncatedTitle = c .smartTruncate (c .text , innerWidth , c .matchIndexes )
93106 } else {
94107 // No matches, use regular truncation
95- truncatedTitle = ansi .Truncate (c .text , availableWidth , "…" )
96- adjustedMatchIndexes = c .matchIndexes
108+ truncatedTitle = ansi .Truncate (c .text , innerWidth , "…" )
97109 }
98110
99111 text := titleStyle .Render (truncatedTitle )
100- if len (adjustedMatchIndexes ) > 0 {
112+ if len (c . matchIndexes ) > 0 {
101113 var ranges []lipgloss.Range
102- for _ , rng := range matchedRanges (adjustedMatchIndexes ) {
114+ for _ , rng := range matchedRanges (c . matchIndexes ) {
103115 // ansi.Cut is grapheme and ansi sequence aware, we match against a ansi.Stripped string, but we might still have graphemes.
104116 // all that to say that rng is byte positions, but we need to pass it down to ansi.Cut as char positions.
105117 // so we need to adjust it here:
106- start , stop := bytePosToVisibleCharPos (text , rng )
118+ start , stop := bytePosToVisibleCharPos (truncatedTitle , rng )
107119 ranges = append (ranges , lipgloss .NewRange (start , stop + 1 , titleMatchStyle ))
108120 }
109121 text = lipgloss .StyleRanges (text , ranges ... )
110122 }
111- return tea .NewView (text )
123+ parts := []string {text }
124+ if c .shortcut != "" {
125+ // Add the shortcut at the end
126+ shortcutStyle := t .S ().Muted
127+ if c .focus {
128+ shortcutStyle = t .S ().TextSelected
129+ }
130+ parts = append (parts , shortcutStyle .Render (c .shortcut ))
131+ }
132+ item := itemStyle .Render (
133+ lipgloss .JoinHorizontal (
134+ lipgloss .Left ,
135+ parts ... ,
136+ ),
137+ )
138+ return tea .NewView (item )
112139}
113140
114141// Blur implements CommandItem.
@@ -141,9 +168,6 @@ func (c *completionItemCmp) SetSize(width int, height int) tea.Cmd {
141168
142169func (c * completionItemCmp ) MatchIndexes (indexes []int ) {
143170 c .matchIndexes = indexes
144- for i := range c .matchIndexes {
145- c .matchIndexes [i ] += 1 // Adjust for the padding we add in View
146- }
147171}
148172
149173func (c * completionItemCmp ) FilterValue () string {
@@ -155,18 +179,18 @@ func (c *completionItemCmp) Value() any {
155179}
156180
157181// smartTruncate implements fzf-style truncation that ensures the last matching part is visible
158- func (c * completionItemCmp ) smartTruncate (text string , width int , matchIndexes []int ) ( string , [] int ) {
182+ func (c * completionItemCmp ) smartTruncate (text string , width int , matchIndexes []int ) string {
159183 if width <= 0 {
160- return "" , [] int {}
184+ return ""
161185 }
162186
163187 textLen := ansi .StringWidth (text )
164188 if textLen <= width {
165- return text , matchIndexes
189+ return text
166190 }
167191
168192 if len (matchIndexes ) == 0 {
169- return ansi .Truncate (text , width , "…" ), [] int {}
193+ return ansi .Truncate (text , width , "…" )
170194 }
171195
172196 // Find the last match position
@@ -187,7 +211,7 @@ func (c *completionItemCmp) smartTruncate(text string, width int, matchIndexes [
187211
188212 // If the last match is within the available width, truncate from the end
189213 if lastMatchVisualPos < availableWidth {
190- return ansi .Truncate (text , width , "…" ), matchIndexes
214+ return ansi .Truncate (text , width , "…" )
191215 }
192216
193217 // Calculate the start position to ensure the last match is visible
@@ -209,20 +233,7 @@ func (c *completionItemCmp) smartTruncate(text string, width int, matchIndexes [
209233 // Truncate to fit width with ellipsis
210234 truncatedText = ansi .Truncate (truncatedText , availableWidth , "" )
211235 truncatedText = "…" + truncatedText
212-
213- // Adjust match indexes for the new truncated string
214- adjustedIndexes := []int {}
215- for _ , idx := range matchIndexes {
216- if idx >= startBytePos {
217- newIdx := idx - startBytePos + 1 //
218- // Check if this match is still within the truncated string
219- if newIdx < len (truncatedText ) {
220- adjustedIndexes = append (adjustedIndexes , newIdx )
221- }
222- }
223- }
224-
225- return truncatedText , adjustedIndexes
236+ return truncatedText
226237}
227238
228239func matchedRanges (in []int ) [][2 ]int {
0 commit comments