diff options
54 files changed, 4615 insertions, 17 deletions
@@ -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 ) @@ -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") |