mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
RefreshSuggestions dispatched to an AsyncHandler worker that read State.FindSuggestions and the prompt's TextArea (via GetPromptInput) from the worker goroutine. The main thread rewrites both in preparePromptPanel when it (re)creates a prompt panel, so an in-flight suggestions worker races those writes -- two data races surfaced under -race (filter_by_path/reword_commit_in_filtering_mode). Capture both on the UI thread (RefreshSuggestions is only ever called from UI-thread handlers) before dispatching to the worker. This is also more correct: we search for the input as it was when dispatched, which is what this request's AsyncHandler id corresponds to. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
3.3 KiB
Go
109 lines
3.3 KiB
Go
package context
|
|
|
|
import (
|
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/tasks"
|
|
)
|
|
|
|
type SuggestionsContext struct {
|
|
*ListViewModel[*types.Suggestion]
|
|
*ListContextTrait
|
|
|
|
State *SuggestionsContextState
|
|
}
|
|
|
|
type SuggestionsContextState struct {
|
|
Suggestions []*types.Suggestion
|
|
OnConfirm func() error
|
|
OnClose func() error
|
|
OnDeleteSuggestion func() error
|
|
AsyncHandler *tasks.AsyncHandler
|
|
|
|
AllowEditSuggestion bool
|
|
|
|
// FindSuggestions will take a string that the user has typed into a prompt
|
|
// and return a slice of suggestions which match that string.
|
|
FindSuggestions func(string) []*types.Suggestion
|
|
}
|
|
|
|
var _ types.IListContext = (*SuggestionsContext)(nil)
|
|
|
|
func NewSuggestionsContext(
|
|
c *ContextCommon,
|
|
) *SuggestionsContext {
|
|
state := &SuggestionsContextState{
|
|
AsyncHandler: tasks.NewAsyncHandler(c.OnWorker),
|
|
}
|
|
getModel := func() []*types.Suggestion {
|
|
return state.Suggestions
|
|
}
|
|
|
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
|
return presentation.GetSuggestionListDisplayStrings(state.Suggestions)
|
|
}
|
|
|
|
viewModel := NewListViewModel(getModel)
|
|
|
|
return &SuggestionsContext{
|
|
State: state,
|
|
ListViewModel: viewModel,
|
|
ListContextTrait: &ListContextTrait{
|
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
|
View: c.Views().Suggestions,
|
|
WindowName: "suggestions",
|
|
Key: SUGGESTIONS_CONTEXT_KEY,
|
|
Kind: types.PERSISTENT_POPUP,
|
|
Focusable: true,
|
|
HasUncontrolledBounds: true,
|
|
})),
|
|
ListRenderer: ListRenderer{
|
|
list: viewModel,
|
|
getDisplayStrings: getDisplayStrings,
|
|
},
|
|
c: c,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (self *SuggestionsContext) SetSuggestions(suggestions []*types.Suggestion) {
|
|
// SetSuggestions is invoked from AsyncHandler (a worker goroutine) when
|
|
// the prompt input changes, as well as from prepareConfirmationPanel on
|
|
// the UI thread. Bounce to the UI thread either way so the worker path
|
|
// keeps flushing once HandleRender stops calling Render() itself.
|
|
self.c.OnUIThread(func() error {
|
|
self.State.Suggestions = suggestions
|
|
self.SetSelection(0)
|
|
self.c.ResetViewOrigin(self.GetView())
|
|
self.HandleRender()
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (self *SuggestionsContext) RefreshSuggestions() {
|
|
// Capture the suggestions function and the prompt input here, on the UI
|
|
// thread, rather than inside the worker below: the main thread rewrites both
|
|
// (State.FindSuggestions and the prompt's TextArea) when it (re)creates a
|
|
// prompt panel, so reading them from the worker races those writes. It's
|
|
// also more correct -- we search for the input as it was when dispatched,
|
|
// which is what this request's AsyncHandler id corresponds to.
|
|
findSuggestionsFn := self.State.FindSuggestions
|
|
promptInput := self.c.GetPromptInput()
|
|
self.State.AsyncHandler.Do(func() func() {
|
|
if findSuggestionsFn != nil {
|
|
suggestions := findSuggestionsFn(promptInput)
|
|
return func() { self.SetSuggestions(suggestions) }
|
|
}
|
|
return func() {}
|
|
})
|
|
}
|
|
|
|
// There is currently no need to use range-select in the suggestions view so we're disabling it.
|
|
func (self *SuggestionsContext) RangeSelectEnabled() bool {
|
|
return false
|
|
}
|
|
|
|
func (self *SuggestionsContext) GetOnDoubleClick() func() error {
|
|
return self.State.OnConfirm
|
|
}
|