@@ -3,7 +3,6 @@ package prompter
33import (
44 "fmt"
55 "slices"
6- "sync"
76
87 "charm.land/huh/v2"
98 "github.com/cli/cli/v2/internal/ghinstance"
@@ -93,162 +92,19 @@ func (p *huhPrompter) MultiSelect(prompt string, defaults []string, options []st
9392 return * result , nil
9493}
9594
96- // searchOptionsBinding is used as the OptionsFunc binding for MultiSelectWithSearch.
97- // By including both the search query and selected values, the binding hash changes
98- // whenever either changes. This prevents huh's internal Eval cache from serving
99- // stale option sets that would overwrite the user's current selections.
100- type searchOptionsBinding struct {
101- Query * string
102- Selected * []string
103- }
104-
105- // syncAccessor is a thread-safe huh.Accessor implementation.
106- // huh calls OptionsFunc from a goroutine while the main event loop
107- // writes field values via Set(). This accessor synchronizes both
108- // paths through the same mutex.
109- type syncAccessor [T any ] struct {
110- mu * sync.Mutex
111- value T
112- }
113-
114- func (a * syncAccessor [T ]) Get () T {
115- a .mu .Lock ()
116- defer a .mu .Unlock ()
117- return a .value
118- }
119-
120- func (a * syncAccessor [T ]) Set (value T ) {
121- a .mu .Lock ()
122- defer a .mu .Unlock ()
123- a .value = value
124- }
125-
126- func (p * huhPrompter ) buildMultiSelectWithSearchForm (prompt , searchPrompt string , defaultValues , persistentValues []string , searchFunc func (string ) MultiSelectSearchResult ) (* huh.Form , * syncAccessor [[]string ]) {
127- var mu sync.Mutex
128-
129- queryAccessor := & syncAccessor [string ]{mu : & mu }
130- selectAccessor := & syncAccessor [[]string ]{mu : & mu , value : slices .Clone (defaultValues )}
131-
132- optionKeyLabels := make (map [string ]string )
133- for _ , k := range defaultValues {
134- optionKeyLabels [k ] = k
135- }
136-
137- // Cache searchFunc results locally keyed by query string.
138- // This avoids redundant calls when the OptionsFunc binding hash changes
139- // due to selection changes (not query changes).
140- searchCacheValid := false
141- var cachedSearchQuery string
142- var cachedSearchResult MultiSelectSearchResult
143-
144- buildOptions := func () []huh.Option [string ] {
145- mu .Lock ()
146- query := queryAccessor .value
147- needsFetch := ! searchCacheValid || query != cachedSearchQuery
148- mu .Unlock ()
149-
150- if needsFetch {
151- result := searchFunc (query )
152- mu .Lock ()
153- cachedSearchResult = result
154- cachedSearchQuery = query
155- searchCacheValid = true
156- mu .Unlock ()
157- }
158-
159- mu .Lock ()
160- defer mu .Unlock ()
161-
162- selectedValues := selectAccessor .value
163- result := cachedSearchResult
164-
165- if result .Err != nil {
166- return nil
167- }
168- for i , k := range result .Keys {
169- optionKeyLabels [k ] = result .Labels [i ]
170- }
171-
172- var formOptions []huh.Option [string ]
173- seen := make (map [string ]bool )
174-
175- // 1. Currently selected values (persisted across searches).
176- for _ , k := range selectedValues {
177- if seen [k ] {
178- continue
179- }
180- seen [k ] = true
181- l := optionKeyLabels [k ]
182- if l == "" {
183- l = k
184- }
185- formOptions = append (formOptions , huh .NewOption (l , k ).Selected (true ))
186- }
187-
188- // 2. Search results.
189- for i , k := range result .Keys {
190- if seen [k ] {
191- continue
192- }
193- seen [k ] = true
194- l := result .Labels [i ]
195- if l == "" {
196- l = k
197- }
198- formOptions = append (formOptions , huh .NewOption (l , k ))
199- }
200-
201- // 3. Persistent options.
202- for _ , k := range persistentValues {
203- if seen [k ] {
204- continue
205- }
206- seen [k ] = true
207- l := optionKeyLabels [k ]
208- if l == "" {
209- l = k
210- }
211- formOptions = append (formOptions , huh .NewOption (l , k ))
212- }
213-
214- if len (formOptions ) == 0 {
215- formOptions = append (formOptions , huh .NewOption ("No results" , "" ))
216- }
217-
218- return formOptions
219- }
220-
221- binding := & searchOptionsBinding {
222- Query : & queryAccessor .value ,
223- Selected : & selectAccessor .value ,
224- }
225-
226- form := p .newForm (
227- huh .NewGroup (
228- huh .NewInput ().
229- Title (searchPrompt ).
230- Placeholder ("Type to search, Ctrl+U to clear" ).
231- Accessor (queryAccessor ),
232- huh .NewMultiSelect [string ]().
233- Title (prompt ).
234- Options (buildOptions ()... ).
235- OptionsFunc (func () []huh.Option [string ] {
236- return buildOptions ()
237- }, binding ).
238- Accessor (selectAccessor ).
239- Limit (0 ),
240- ),
241- )
242- return form , selectAccessor
95+ func (p * huhPrompter ) buildMultiSelectWithSearchForm (prompt , searchPrompt string , defaultValues , persistentValues []string , searchFunc func (string ) MultiSelectSearchResult ) (* huh.Form , * multiSelectSearchField ) {
96+ field := newMultiSelectSearchField (prompt , searchPrompt , defaultValues , persistentValues , searchFunc )
97+ form := p .newForm (huh .NewGroup (field ))
98+ return form , field
24399}
244100
245101func (p * huhPrompter ) MultiSelectWithSearch (prompt , searchPrompt string , defaultValues , persistentValues []string , searchFunc func (string ) MultiSelectSearchResult ) ([]string , error ) {
246- form , accessor := p .buildMultiSelectWithSearchForm (prompt , searchPrompt , defaultValues , persistentValues , searchFunc )
102+ form , field := p .buildMultiSelectWithSearchForm (prompt , searchPrompt , defaultValues , persistentValues , searchFunc )
247103 err := form .Run ()
248104 if err != nil {
249105 return nil , err
250106 }
251- return accessor . Get (), nil
107+ return field . selectedKeys (), nil
252108}
253109
254110func (p * huhPrompter ) buildInputForm (prompt , defaultValue string ) (* huh.Form , * string ) {
0 commit comments