diff options
Diffstat (limited to 'pkg/gui/controllers/helpers/suggestions_helper.go')
-rw-r--r-- | pkg/gui/controllers/helpers/suggestions_helper.go | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go new file mode 100644 index 000000000..a48e325b1 --- /dev/null +++ b/pkg/gui/controllers/helpers/suggestions_helper.go @@ -0,0 +1,207 @@ +package helpers + +import ( + "fmt" + "os" + + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/jesseduffield/minimal/gitignore" + "gopkg.in/ozeidan/fuzzy-patricia.v3/patricia" +) + +// Thinking out loud: I'm typically a staunch advocate of organising code by feature rather than type, +// because colocating code that relates to the same feature means far less effort +// to get all the context you need to work on any particular feature. But the one +// major benefit of grouping by type is that it makes it makes it less likely that +// somebody will re-implement the same logic twice, because they can quickly see +// if a certain method has been used for some use case, given that as a starting point +// they know about the type. In that vein, I'm including all our functions for +// finding suggestions in this file, so that it's easy to see if a function already +// exists for fetching a particular model. + +type ISuggestionsHelper interface { + GetRemoteSuggestionsFunc() func(string) []*types.Suggestion + GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion + GetFilePathSuggestionsFunc() func(string) []*types.Suggestion + GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion + GetRefsSuggestionsFunc() func(string) []*types.Suggestion +} + +type SuggestionsHelper struct { + c *types.HelperCommon + + model *types.Model + refreshSuggestionsFn func() +} + +var _ ISuggestionsHelper = &SuggestionsHelper{} + +func NewSuggestionsHelper( + c *types.HelperCommon, + model *types.Model, + refreshSuggestionsFn func(), +) *SuggestionsHelper { + return &SuggestionsHelper{ + c: c, + model: model, + refreshSuggestionsFn: refreshSuggestionsFn, + } +} + +func (self *SuggestionsHelper) getRemoteNames() []string { + result := make([]string, len(self.model.Remotes)) + for i, remote := range self.model.Remotes { + result[i] = remote.Name + } + return result +} + +func matchesToSuggestions(matches []string) []*types.Suggestion { + suggestions := make([]*types.Suggestion, len(matches)) + for i, match := range matches { + suggestions[i] = &types.Suggestion{ + Value: match, + Label: match, + } + } + return suggestions +} + +func (self *SuggestionsHelper) GetRemoteSuggestionsFunc() func(string) []*types.Suggestion { + remoteNames := self.getRemoteNames() + + return FuzzySearchFunc(remoteNames) +} + +func (self *SuggestionsHelper) getBranchNames() []string { + result := make([]string, len(self.model.Branches)) + for i, branch := range self.model.Branches { + result[i] = branch.Name + } + return result +} + +func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion { + branchNames := self.getBranchNames() + + return func(input string) []*types.Suggestion { + var matchingBranchNames []string + if input == "" { + matchingBranchNames = branchNames + } else { + matchingBranchNames = utils.FuzzySearch(input, branchNames) + } + + suggestions := make([]*types.Suggestion, len(matchingBranchNames)) + for i, branchName := range matchingBranchNames { + suggestions[i] = &types.Suggestion{ + Value: branchName, + Label: presentation.GetBranchTextStyle(branchName).Sprint(branchName), + } + } + + return suggestions + } +} + +// here we asynchronously fetch the latest set of paths in the repo and store in +// self.model.FilesTrie. On the main thread we'll be doing a fuzzy search via +// self.model.FilesTrie. So if we've looked for a file previously, we'll start with +// the old trie and eventually it'll be swapped out for the new one. +// Notably, unlike other suggestion functions we're not showing all the options +// if nothing has been typed because there'll be too much to display efficiently +func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*types.Suggestion { + _ = self.c.WithWaitingStatus(self.c.Tr.LcLoadingFileSuggestions, func() error { + trie := patricia.NewTrie() + // load every non-gitignored file in the repo + ignore, err := gitignore.FromGit() + if err != nil { + return err + } + + err = ignore.Walk(".", + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + trie.Insert(patricia.Prefix(path), path) + return nil + }) + + // cache the trie for future use + self.model.FilesTrie = trie + + self.refreshSuggestionsFn() + + return err + }) + + return func(input string) []*types.Suggestion { + matchingNames := []string{} + _ = self.model.FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error { + matchingNames = append(matchingNames, item.(string)) + return nil + }) + + // doing another fuzzy search for good measure + matchingNames = utils.FuzzySearch(input, matchingNames) + + suggestions := make([]*types.Suggestion, len(matchingNames)) + for i, name := range matchingNames { + suggestions[i] = &types.Suggestion{ + Value: name, + Label: name, + } + } + + return suggestions + } +} + +func (self *SuggestionsHelper) getRemoteBranchNames(separator string) []string { + result := []string{} + for _, remote := range self.model.Remotes { + for _, branch := range remote.Branches { + result = append(result, fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name)) + } + } + return result +} + +func (self *SuggestionsHelper) GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion { + return FuzzySearchFunc(self.getRemoteBranchNames(separator)) +} + +func (self *SuggestionsHelper) getTagNames() []string { + result := make([]string, len(self.model.Tags)) + for i, tag := range self.model.Tags { + result[i] = tag.Name + } + return result +} + +func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Suggestion { + remoteBranchNames := self.getRemoteBranchNames("/") + localBranchNames := self.getBranchNames() + tagNames := self.getTagNames() + additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"} + + refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...) + + return FuzzySearchFunc(refNames) +} + +func FuzzySearchFunc(options []string) func(string) []*types.Suggestion { + return func(input string) []*types.Suggestion { + var matches []string + if input == "" { + matches = options + } else { + matches = utils.FuzzySearch(input, options) + } + + return matchesToSuggestions(matches) + } +} |