11package textinput
22
33import (
4+ "reflect"
45 "strings"
56 "time"
67 "unicode"
@@ -32,8 +33,6 @@ const (
3233 // EchoNone displays nothing as characters are entered. This is commonly
3334 // seen for password fields on the command line.
3435 EchoNone
35-
36- // EchoOnEdit.
3736)
3837
3938// ValidateFunc is a function that returns an error if the input is invalid.
@@ -54,6 +53,9 @@ type KeyMap struct {
5453 LineStart key.Binding
5554 LineEnd key.Binding
5655 Paste key.Binding
56+ AcceptSuggestion key.Binding
57+ NextSuggestion key.Binding
58+ PrevSuggestion key.Binding
5759}
5860
5961// DefaultKeyMap is the default set of key bindings for navigating and acting
@@ -72,6 +74,9 @@ var DefaultKeyMap = KeyMap{
7274 LineStart : key .NewBinding (key .WithKeys ("home" , "ctrl+a" )),
7375 LineEnd : key .NewBinding (key .WithKeys ("end" , "ctrl+e" )),
7476 Paste : key .NewBinding (key .WithKeys ("ctrl+v" )),
77+ AcceptSuggestion : key .NewBinding (key .WithKeys ("tab" )),
78+ NextSuggestion : key .NewBinding (key .WithKeys ("down" , "ctrl+n" )),
79+ PrevSuggestion : key .NewBinding (key .WithKeys ("up" , "ctrl+p" )),
7580}
7681
7782// Model is the Bubble Tea model for this text input element.
@@ -95,6 +100,7 @@ type Model struct {
95100 PromptStyle lipgloss.Style
96101 TextStyle lipgloss.Style
97102 PlaceholderStyle lipgloss.Style
103+ CompletionStyle lipgloss.Style
98104
99105 // Deprecated: use Cursor.Style instead.
100106 CursorStyle lipgloss.Style
@@ -134,6 +140,15 @@ type Model struct {
134140
135141 // rune sanitizer for input.
136142 rsan runeutil.Sanitizer
143+
144+ // Should the input suggest to complete
145+ ShowSuggestions bool
146+
147+ // suggestions is a list of suggestions that may be used to complete the
148+ // input.
149+ suggestions [][]rune
150+ matchedSuggestions [][]rune
151+ currentSuggestionIndex int
137152}
138153
139154// New creates a new model with default settings.
@@ -143,12 +158,15 @@ func New() Model {
143158 EchoCharacter : '*' ,
144159 CharLimit : 0 ,
145160 PlaceholderStyle : lipgloss .NewStyle ().Foreground (lipgloss .Color ("240" )),
161+ ShowSuggestions : false ,
162+ CompletionStyle : lipgloss .NewStyle ().Foreground (lipgloss .Color ("240" )),
146163 Cursor : cursor .New (),
147164 KeyMap : DefaultKeyMap ,
148165
149- value : nil ,
150- focus : false ,
151- pos : 0 ,
166+ suggestions : [][]rune {},
167+ value : nil ,
168+ focus : false ,
169+ pos : 0 ,
152170 }
153171}
154172
@@ -239,6 +257,17 @@ func (m *Model) Reset() {
239257 m .SetCursor (0 )
240258}
241259
260+ // SetSuggestions sets the suggestions for the input.
261+ func (m * Model ) SetSuggestions (suggestions []string ) {
262+ m .suggestions = [][]rune {}
263+
264+ for _ , s := range suggestions {
265+ m .suggestions = append (m .suggestions , []rune (s ))
266+ }
267+
268+ m .updateSuggestions ()
269+ }
270+
242271// rsan initializes or retrieves the rune sanitizer.
243272func (m * Model ) san () runeutil.Sanitizer {
244273 if m .rsan == nil {
@@ -529,6 +558,15 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
529558 return m , nil
530559 }
531560
561+ // Need to check for completion before, because key is configurable and might be double assigned
562+ keyMsg , ok := msg .(tea.KeyMsg )
563+ if ok && key .Matches (keyMsg , m .KeyMap .AcceptSuggestion ) {
564+ if m .canAcceptSuggestion () {
565+ m .value = append (m .value , m .matchedSuggestions [m .currentSuggestionIndex ][len (m .value ):]... )
566+ m .CursorEnd ()
567+ }
568+ }
569+
532570 // Let's remember where the position of the cursor currently is so that if
533571 // the cursor position changes, we can reset the blink.
534572 oldPos := m .pos //nolint
@@ -577,11 +615,19 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
577615 return m , Paste
578616 case key .Matches (msg , m .KeyMap .DeleteWordForward ):
579617 m .deleteWordForward ()
618+ case key .Matches (msg , m .KeyMap .NextSuggestion ):
619+ m .nextSuggestion ()
620+ case key .Matches (msg , m .KeyMap .PrevSuggestion ):
621+ m .previousSuggestion ()
580622 default :
581623 // Input one or more regular characters.
582624 m .insertRunesFromUserInput (msg .Runes )
583625 }
584626
627+ // Check again if can be completed
628+ // because value might be something that does not match the completion prefix
629+ m .updateSuggestions ()
630+
585631 case pasteMsg :
586632 m .insertRunesFromUserInput ([]rune (msg ))
587633
@@ -622,9 +668,23 @@ func (m Model) View() string {
622668 m .Cursor .SetChar (char )
623669 v += m .Cursor .View () // cursor and text under it
624670 v += styleText (m .echoTransform (string (value [pos + 1 :]))) // text after cursor
671+ v += m .completionView (0 ) // suggested completion
625672 } else {
626- m .Cursor .SetChar (" " )
627- v += m .Cursor .View ()
673+ if m .canAcceptSuggestion () {
674+ suggestion := m .matchedSuggestions [m .currentSuggestionIndex ]
675+ if len (value ) < len (suggestion ) {
676+ m .Cursor .TextStyle = m .CompletionStyle
677+ m .Cursor .SetChar (m .echoTransform (string (suggestion [pos ])))
678+ v += m .Cursor .View ()
679+ v += m .completionView (1 )
680+ } else {
681+ m .Cursor .SetChar (" " )
682+ v += m .Cursor .View ()
683+ }
684+ } else {
685+ m .Cursor .SetChar (" " )
686+ v += m .Cursor .View ()
687+ }
628688 }
629689
630690 // If a max width and background color were set fill the empty spaces with
@@ -721,3 +781,81 @@ func (m Model) CursorMode() CursorMode {
721781func (m * Model ) SetCursorMode (mode CursorMode ) tea.Cmd {
722782 return m .Cursor .SetMode (cursor .Mode (mode ))
723783}
784+
785+ func (m Model ) completionView (offset int ) string {
786+ var (
787+ value = m .value
788+ style = m .PlaceholderStyle .Inline (true ).Render
789+ )
790+
791+ if m .canAcceptSuggestion () {
792+ suggestion := m .matchedSuggestions [m .currentSuggestionIndex ]
793+ if len (value ) < len (suggestion ) {
794+ return style (string (suggestion [len (value )+ offset :]))
795+ }
796+ }
797+ return ""
798+ }
799+
800+ // AvailableSuggestions returns the list of available suggestions.
801+ func (m * Model ) AvailableSuggestions () []string {
802+ suggestions := []string {}
803+ for _ , s := range m .suggestions {
804+ suggestions = append (suggestions , string (s ))
805+ }
806+
807+ return suggestions
808+ }
809+
810+ // CurrentSuggestion returns the currently selected suggestion.
811+ func (m * Model ) CurrentSuggestion () string {
812+ return string (m .matchedSuggestions [m .currentSuggestionIndex ])
813+ }
814+
815+ // canAcceptSuggestion returns whether there is an acceptable suggestion to
816+ // autocomplete the current value.
817+ func (m * Model ) canAcceptSuggestion () bool {
818+ return len (m .matchedSuggestions ) > 0
819+ }
820+
821+ // updateSuggestions refreshes the list of matching suggestions.
822+ func (m * Model ) updateSuggestions () {
823+ if ! m .ShowSuggestions {
824+ return
825+ }
826+
827+ if len (m .value ) <= 0 || len (m .suggestions ) <= 0 {
828+ m .matchedSuggestions = [][]rune {}
829+ return
830+ }
831+
832+ matches := [][]rune {}
833+ for _ , s := range m .suggestions {
834+ suggestion := string (s )
835+
836+ if strings .HasPrefix (strings .ToLower (suggestion ), strings .ToLower (string (m .value ))) {
837+ matches = append (matches , []rune (suggestion ))
838+ }
839+ }
840+ if ! reflect .DeepEqual (matches , m .matchedSuggestions ) {
841+ m .currentSuggestionIndex = 0
842+ }
843+
844+ m .matchedSuggestions = matches
845+ }
846+
847+ // nextSuggestion selects the next suggestion.
848+ func (m * Model ) nextSuggestion () {
849+ m .currentSuggestionIndex = (m .currentSuggestionIndex + 1 )
850+ if m .currentSuggestionIndex >= len (m .matchedSuggestions ) {
851+ m .currentSuggestionIndex = 0
852+ }
853+ }
854+
855+ // previousSuggestion selects the previous suggestion.
856+ func (m * Model ) previousSuggestion () {
857+ m .currentSuggestionIndex = (m .currentSuggestionIndex - 1 )
858+ if m .currentSuggestionIndex < 0 {
859+ m .currentSuggestionIndex = len (m .matchedSuggestions ) - 1
860+ }
861+ }
0 commit comments