summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2021-10-18 19:00:03 +1100
committerJesse Duffield <jessedduffield@gmail.com>2021-10-19 09:02:42 +1100
commitca7252ef8ee26affdc2c74f05c9c20196a8d571b (patch)
tree7323ca472558a435f01ae62e123cc2576e1959b3
parenta496858c629e3c6241b51cf99cd38d6fa1b787bc (diff)
suggest files when picking a path to filter on
async fetching of suggestions remove limit cache the trie for future use more more
-rw-r--r--go.mod5
-rw-r--r--go.sum13
-rw-r--r--pkg/gui/confirmation_panel.go2
-rw-r--r--pkg/gui/editors.go6
-rw-r--r--pkg/gui/filtering.go66
-rw-r--r--pkg/gui/filtering_menu_panel.go3
-rw-r--r--pkg/gui/gui.go34
-rw-r--r--pkg/i18n/english.go2
-rw-r--r--pkg/tasks/async_handler.go56
-rw-r--r--pkg/tasks/async_handler_test.go44
-rw-r--r--vendor/github.com/gobwas/glob/.gitignore8
-rw-r--r--vendor/github.com/gobwas/glob/LICENSE21
-rw-r--r--vendor/github.com/gobwas/glob/bench.sh26
-rw-r--r--vendor/github.com/gobwas/glob/compiler/compiler.go525
-rw-r--r--vendor/github.com/gobwas/glob/glob.go80
-rw-r--r--vendor/github.com/gobwas/glob/match/any.go45
-rw-r--r--vendor/github.com/gobwas/glob/match/any_of.go82
-rw-r--r--vendor/github.com/gobwas/glob/match/btree.go146
-rw-r--r--vendor/github.com/gobwas/glob/match/contains.go58
-rw-r--r--vendor/github.com/gobwas/glob/match/every_of.go99
-rw-r--r--vendor/github.com/gobwas/glob/match/list.go49
-rw-r--r--vendor/github.com/gobwas/glob/match/match.go81
-rw-r--r--vendor/github.com/gobwas/glob/match/max.go49
-rw-r--r--vendor/github.com/gobwas/glob/match/min.go57
-rw-r--r--vendor/github.com/gobwas/glob/match/nothing.go27
-rw-r--r--vendor/github.com/gobwas/glob/match/prefix.go50
-rw-r--r--vendor/github.com/gobwas/glob/match/prefix_any.go55
-rw-r--r--vendor/github.com/gobwas/glob/match/prefix_suffix.go62
-rw-r--r--vendor/github.com/gobwas/glob/match/range.go48
-rw-r--r--vendor/github.com/gobwas/glob/match/row.go77
-rw-r--r--vendor/github.com/gobwas/glob/match/segments.go91
-rw-r--r--vendor/github.com/gobwas/glob/match/single.go43
-rw-r--r--vendor/github.com/gobwas/glob/match/suffix.go35
-rw-r--r--vendor/github.com/gobwas/glob/match/suffix_any.go43
-rw-r--r--vendor/github.com/gobwas/glob/match/super.go33
-rw-r--r--vendor/github.com/gobwas/glob/match/text.go45
-rw-r--r--vendor/github.com/gobwas/glob/readme.md148
-rw-r--r--vendor/github.com/gobwas/glob/syntax/ast/ast.go122
-rw-r--r--vendor/github.com/gobwas/glob/syntax/ast/parser.go157
-rw-r--r--vendor/github.com/gobwas/glob/syntax/lexer/lexer.go273
-rw-r--r--vendor/github.com/gobwas/glob/syntax/lexer/token.go88
-rw-r--r--vendor/github.com/gobwas/glob/syntax/syntax.go14
-rw-r--r--vendor/github.com/gobwas/glob/util/runes/runes.go154
-rw-r--r--vendor/github.com/gobwas/glob/util/strings/strings.go39
-rw-r--r--vendor/github.com/jesseduffield/minimal/gitignore/LICENSE24
-rw-r--r--vendor/github.com/jesseduffield/minimal/gitignore/gitignore.go313
-rw-r--r--vendor/github.com/jesseduffield/minimal/gitignore/go.mod5
-rw-r--r--vendor/github.com/jesseduffield/minimal/gitignore/go.sum2
-rw-r--r--vendor/github.com/jesseduffield/minimal/gitignore/testgitignore10
-rw-r--r--vendor/gopkg.in/ozeidan/fuzzy-patricia.v3/AUTHORS3
-rw-r--r--vendor/gopkg.in/ozeidan/fuzzy-patricia.v3/LICENSE20
-rw-r--r--vendor/gopkg.in/ozeidan/fuzzy-patricia.v3/patricia/children.go182
-rw-r--r--vendor/gopkg.in/ozeidan/fuzzy-patricia.v3/patricia/patricia.go892
-rw-r--r--vendor/modules.txt20
54 files changed, 4615 insertions, 17 deletions
diff --git a/go.mod b/go.mod
index cc498354c..0149feda5 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.4.1
@@ -19,8 +20,10 @@ require (
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
+ github.com/iriri/minimal/gitignore v0.3.2 // indirect
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f
+ github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
@@ -32,6 +35,7 @@ require (
github.com/mgutz/str v1.2.0
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
+ github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible // indirect
github.com/sahilm/fuzzy v0.1.0
github.com/sirupsen/logrus v1.4.2
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
@@ -42,4 +46,5 @@ require (
golang.org/x/sys v0.0.0-20211015200801-69063c4bb744 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
+ gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
)
diff --git a/go.sum b/go.sum
index a3bd86751..1d3c41de1 100644
--- a/go.sum
+++ b/go.sum
@@ -48,6 +48,8 @@ github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -65,6 +67,9 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
+github.com/iriri/minimal v0.0.0-20180828191352-9b2348d09c1a h1:mCZYG6QcX0dz/J0rFc1tcRYGeixlDcCGSPXuPMbiS5U=
+github.com/iriri/minimal/gitignore v0.3.2 h1:MnTVH89iuwiyZ/a1pByw/mAU2ShWai1yvv0tgHSq5Ww=
+github.com/iriri/minimal/gitignore v0.3.2/go.mod h1:v7YhsYBAInyAnQligwCIGRuQmtwQyYxkVy5vEdy2wPU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
@@ -79,6 +84,9 @@ github.com/jesseduffield/gocui v0.3.1-0.20211017091015-8bf4a4666b77 h1:MQUxSxVBT
github.com/jesseduffield/gocui v0.3.1-0.20211017091015-8bf4a4666b77/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f h1:JHrb78pj+gYC3KiJKL1WW6lYzlatBIF46oREn68plTM=
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
+github.com/jesseduffield/minimal v0.0.0-20211018110810-9cde264e6b1e h1:WZc73tBVMMhcO6zXyZBItLEF4jgBpBH0lFCZzDgrjDg=
+github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
+github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -126,6 +134,9 @@ github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/ozeidan/fuzzy-patricia v1.0.1 h1:YExnavqXH3OvCCqE2TunuJJHdFcFQdVEfUoWzrnPxSg=
+github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible h1:Pl61eMyfJqgY/wytiI4vamqPYribq6d8VxeP1CNyg9M=
+github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible/go.mod h1:zgvuCcYS7wB7fVCGblsaFFmEe8+aAH13dTYm8FbrpsM=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -198,6 +209,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0 h1:KzcWKJ0nMAmGoBhYVMnkWc1rXjB42lKy5aIys4TdLOA=
+gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0/go.mod h1:XoytMOotjRRJVkIsQdxsPIioRLYFISEaY9a4tftOXAo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go
index d3e1dac18..6664f765d 100644
--- a/pkg/gui/confirmation_panel.go
+++ b/pkg/gui/confirmation_panel.go
@@ -189,7 +189,7 @@ func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, f
if err != nil {
return err
}
- suggestionsView.Wrap = true
+ suggestionsView.Wrap = false
suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions([]*types.Suggestion{})
suggestionsView.Visible = true
diff --git a/pkg/gui/editors.go b/pkg/gui/editors.go
index b94e6b5bd..430dcfdfe 100644
--- a/pkg/gui/editors.go
+++ b/pkg/gui/editors.go
@@ -77,8 +77,10 @@ func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.M
if gui.findSuggestions != nil {
input := v.TextArea.GetContent()
- suggestions := gui.findSuggestions(input)
- gui.setSuggestions(suggestions)
+ gui.suggestionsAsyncHandler.Do(func() func() {
+ suggestions := gui.findSuggestions(input)
+ return func() { gui.setSuggestions(suggestions) }
+ })
}
return matched
diff --git a/pkg/gui/filtering.go b/pkg/gui/filtering.go
index 1f5c5032a..17fd44e23 100644
--- a/pkg/gui/filtering.go
+++ b/pkg/gui/filtering.go
@@ -1,5 +1,14 @@
package gui
+import (
+ "os"
+
+ "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"
+)
+
func (gui *Gui) validateNotInFilterMode() (bool, error) {
if gui.State.Modes.Filtering.Active() {
err := gui.ask(askOpts{
@@ -40,3 +49,60 @@ func (gui *Gui) setFiltering(path string) error {
gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(0)
}})
}
+
+// here we asynchronously fetch the latest set of paths in the repo and store in
+// gui.State.FilesTrie. On the main thread we'll be doing a fuzzy search via
+// gui.State.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.
+func (gui *Gui) getFindSuggestionsForFilterPath() func(string) []*types.Suggestion {
+ _ = gui.WithWaitingStatus(gui.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
+ gui.State.FilesTrie = trie
+
+ // refresh the selections view
+ gui.suggestionsAsyncHandler.Do(func() func() {
+ // assuming here that the confirmation view is what we're typing into.
+ // This assumption may prove false over time
+ suggestions := gui.findSuggestions(gui.Views.Confirmation.TextArea.GetContent())
+ return func() { gui.setSuggestions(suggestions) }
+ })
+
+ return err
+ })
+
+ return func(input string) []*types.Suggestion {
+ matchingNames := []string{}
+ _ = gui.State.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
+ }
+}
diff --git a/pkg/gui/filtering_menu_panel.go b/pkg/gui/filtering_menu_panel.go
index 649172b45..bd17291ae 100644
--- a/pkg/gui/filtering_menu_panel.go
+++ b/pkg/gui/filtering_menu_panel.go
@@ -35,7 +35,8 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
displayString: gui.Tr.LcFilterPathOption,
onPress: func() error {
return gui.prompt(promptOpts{
- title: gui.Tr.LcEnterFileName,
+ findSuggestionsFunc: gui.getFindSuggestionsForFilterPath(),
+ title: gui.Tr.LcEnterFileName,
handleConfirm: func(response string) error {
return gui.setFiltering(strings.TrimSpace(response))
},
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 95c165a71..1349920e5 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -30,6 +30,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
+ "gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
)
// screen sizing determines how much space your selected window takes up (window
@@ -116,6 +117,8 @@ type Gui struct {
// the extras window contains things like the command log
ShowExtrasWindow bool
+
+ suggestionsAsyncHandler *tasks.AsyncHandler
}
type listPanelState struct {
@@ -336,6 +339,9 @@ type guiState struct {
// flag as to whether or not the diff view should ignore whitespace
IgnoreWhitespaceInDiffView bool
+
+ // for displaying suggestions while typing in a file name
+ FilesTrie *patricia.Trie
}
// reuseState determines if we pull the repo state from our repo state map or
@@ -412,6 +418,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
// TODO: put contexts in the context manager
ContextManager: NewContextManager(initialContext),
Contexts: contexts,
+ FilesTrie: patricia.NewTrie(),
}
gui.RepoStateMap[Repo(currentDir)] = gui.State
@@ -421,19 +428,20 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
gui := &Gui{
- Log: log,
- GitCommand: gitCommand,
- OSCommand: oSCommand,
- Config: config,
- Tr: tr,
- Updater: updater,
- statusManager: &statusManager{},
- viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
- showRecentRepos: showRecentRepos,
- RepoPathStack: []string{},
- RepoStateMap: map[Repo]*guiState{},
- CmdLog: []string{},
- ShowExtrasWindow: config.GetUserConfig().Gui.ShowCommandLog,
+ Log: log,
+ GitCommand: gitCommand,
+ OSCommand: oSCommand,
+ Config: config,
+ Tr: tr,
+ Updater: updater,
+ statusManager: &statusManager{},
+ viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
+ showRecentRepos: showRecentRepos,
+ RepoPathStack: []string{},
+ RepoStateMap: map[Repo]*guiState{},
+ CmdLog: []string{},
+ ShowExtrasWindow: config.GetUserConfig().Gui.ShowCommandLog,
+ suggestionsAsyncHandler: tasks.NewAsyncHandler(),
}
gui.resetState(filterPath, false)
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 1156cfee7..e3712b7b5 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -430,6 +430,7 @@ type TranslationSet struct {
CreatingPullRequestAtUrl string
SelectConfigFile string
NoConfigFileFoundErr string
+ LcLoadingFileSuggestions string
Spans Spans
}
@@ -954,6 +955,7 @@ func englishTranslationSet() TranslationSet {
CreatingPullRequestAtUrl: "Creating pull request at URL: %s",
SelectConfigFile: "Select config file",
NoConfigFileFoundErr: "No config file found",
+ LcLoadingFileSuggestions: "loading file suggestions",
Spans: Spans{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit",
diff --git a/pkg/tasks/async_handler.go b/pkg/tasks/async_handler.go
new file mode 100644
index 000000000..897efb0e2
--- /dev/null
+++ b/pkg/tasks/async_handler.go
@@ -0,0 +1,56 @@
+package tasks
+
+import (
+ "sync"
+
+ "github.com/jesseduffield/lazygit/pkg/utils"
+)
+
+// the purpose of an AsyncHandler is to ensure that if we have multiple long-running
+// requests, we only handle the result of the latest one. For example, if I am
+// searching for 'abc' and I have to type 'a' then 'b' then 'c' and each keypress
+// dispatches a request to search for things with the string so-far, we'll be searching
+// for 'a', 'ab', and 'abc', and it may be that 'abc' comes back first, then 'ab',
+// then 'a' and we don't want to display the result for 'a' just because it came
+// back last. AsyncHandler keeps track of the order in which things were dispatched
+// so that we can ignore anything that comes back late.
+type AsyncHandler struct {
+ currentId int
+ lastId int
+ mutex sync.Mutex
+ onReject func()
+}
+
+func NewAsyncHandler() *AsyncHandler {
+ return &AsyncHandler{
+ mutex: sync.Mutex{},
+ }
+}
+
+func (self *AsyncHandler) Do(f func() func()) {
+ self.mutex.Lock()
+ self.currentId++
+ id := self.currentId
+ self.mutex.Unlock()
+
+ go utils.Safe(func() {
+ after := f()
+ self.handle(after, id)
+ })
+}
+
+// f here is expected to be a function that doesn't take long to run
+func (self *AsyncHandler) handle(f func(), id int) {
+ self.mutex.Lock()
+ defer self.mutex.Unlock()
+
+ if id < self.lastId {
+ if self.onReject != nil {
+ self.onReject()
+ }
+ return
+ }
+
+ self.lastId = id
+ f()
+}
diff --git a/pkg/tasks/async_handler_test.go b/pkg/tasks/async_handler_test.go
new file mode 100644
index 000000000..b6edbec20
--- /dev/null
+++ b/pkg/tasks/async_handler_test.go
@@ -0,0 +1,44 @@
+package tasks
+
+import (
+ "fmt"
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAsyncHandler(t *testing.T) {
+ wg := sync.WaitGroup{}
+ wg.Add(2)
+
+ handler := NewAsyncHandler()
+ handler.onReject = func() {
+ wg.Done()
+ }
+
+ result := 0
+
+ wg2 := sync.WaitGroup{}
+ wg2.Add(1)
+
+ handler.Do(func() func() {
+ wg2.Wait()
+ return func() {
+ fmt.Println("setting to 1")
+ result = 1
+ }
+ })
+ handler.Do(func() func() {
+ return func() {
+ fmt.Println("setting to 2")
+ result = 2
+ wg.Done()
+ wg2.Done()
+ }
+ })
+
+ wg.Wait()
+
+ assert.EqualValues(t, 2, result)
+}
diff --git a/vendor/github.com/gobwas/glob/.gitignore b/vendor/github.com/gobwas/glob/.gitignore
new file mode 100644
index 000000000..b4ae623be
--- /dev/null
+++ b/vendor/github.com/gobwas/glob/.gitignore
@@ -0,0 +1,8 @@
+glob.iml
+.idea
+*.cpu
+*.mem
+*.test
+*.dot
+*.png
+*.svg
diff --git a/vendor/github.com/gobwas/glob/LICENSE b/vendor/github.com/gobwas/glob/LICENSE
new file mode 100644
index 000000000..9d4735cad
--- /dev/null
+++ b/vendor/github.com/gobwas/glob/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Sergey Kamardin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. \ No newline at end of file
diff --git a/vendor/github.com/gobwas/glob/bench.sh b/vendor/github.com/gobwas/glob/bench.sh
new file mode 100644
index 000000000..804cf22e6
--- /dev/null
+++ b/vendor/github.com/gobwas/glob/bench.sh
@@ -0,0 +1,26 @@
+#! /bin/bash
+
+bench() {
+ filename="/tmp/$