mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	Use fuzzy search when filtering a view
This adds fuzzy filtering instead of exact match filtering, which is more forgiving of typos and allows more efficiency.
This commit is contained in:
		| @@ -1,7 +1,11 @@ | ||||
| package context | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/jesseduffield/lazygit/pkg/utils" | ||||
| 	"github.com/sahilm/fuzzy" | ||||
| 	"github.com/samber/lo" | ||||
| 	"github.com/sasha-s/go-deadlock" | ||||
| ) | ||||
|  | ||||
| @@ -53,6 +57,21 @@ func (self *FilteredList[T]) UnfilteredLen() int { | ||||
| 	return len(self.getList()) | ||||
| } | ||||
|  | ||||
| type fuzzySource[T any] struct { | ||||
| 	list            []T | ||||
| 	getFilterFields func(T) []string | ||||
| } | ||||
|  | ||||
| var _ fuzzy.Source = &fuzzySource[string]{} | ||||
|  | ||||
| func (self *fuzzySource[T]) String(i int) string { | ||||
| 	return strings.Join(self.getFilterFields(self.list[i]), " ") | ||||
| } | ||||
|  | ||||
| func (self *fuzzySource[T]) Len() int { | ||||
| 	return len(self.list) | ||||
| } | ||||
|  | ||||
| func (self *FilteredList[T]) applyFilter() { | ||||
| 	self.mutex.Lock() | ||||
| 	defer self.mutex.Unlock() | ||||
| @@ -60,20 +79,16 @@ func (self *FilteredList[T]) applyFilter() { | ||||
| 	if self.filter == "" { | ||||
| 		self.filteredIndices = nil | ||||
| 	} else { | ||||
| 		self.filteredIndices = []int{} | ||||
| 		for i, item := range self.getList() { | ||||
| 			for _, field := range self.getFilterFields(item) { | ||||
| 				if self.match(field, self.filter) { | ||||
| 					self.filteredIndices = append(self.filteredIndices, i) | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		source := &fuzzySource[T]{ | ||||
| 			list:            self.getList(), | ||||
| 			getFilterFields: self.getFilterFields, | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *FilteredList[T]) match(haystack string, needle string) bool { | ||||
| 	return utils.CaseAwareContains(haystack, needle) | ||||
| 		matches := fuzzy.FindFrom(self.filter, source) | ||||
| 		self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int { | ||||
| 			return match.Index | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *FilteredList[T]) UnfilteredIndex(index int) int { | ||||
|   | ||||
| @@ -539,6 +539,7 @@ func (self *ViewDriver) FilterOrSearch(text string) *ViewDriver { | ||||
| 	self.Press(self.t.keys.Universal.StartSearch). | ||||
| 		Tap(func() { | ||||
| 			self.t.ExpectSearch(). | ||||
| 				Clear(). | ||||
| 				Type(text). | ||||
| 				Confirm() | ||||
|  | ||||
|   | ||||
							
								
								
									
										35
									
								
								pkg/integration/tests/filter_and_search/filter_fuzzy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								pkg/integration/tests/filter_and_search/filter_fuzzy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| package filter_and_search | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/lazygit/pkg/config" | ||||
| 	. "github.com/jesseduffield/lazygit/pkg/integration/components" | ||||
| ) | ||||
|  | ||||
| var FilterFuzzy = NewIntegrationTest(NewIntegrationTestArgs{ | ||||
| 	Description:  "Verify that fuzzy filtering works (not just exact matches)", | ||||
| 	ExtraCmdArgs: []string{}, | ||||
| 	Skip:         false, | ||||
| 	SetupConfig:  func(config *config.AppConfig) {}, | ||||
| 	SetupRepo: func(shell *Shell) { | ||||
| 		shell.NewBranch("this-is-my-branch") | ||||
| 		shell.EmptyCommit("first commit") | ||||
| 		shell.NewBranch("other-branch") | ||||
| 	}, | ||||
| 	Run: func(t *TestDriver, keys config.KeybindingConfig) { | ||||
| 		t.Views().Branches(). | ||||
| 			Focus(). | ||||
| 			Lines( | ||||
| 				Contains(`other-branch`).IsSelected(), | ||||
| 				Contains(`this-is-my-branch`), | ||||
| 			). | ||||
| 			FilterOrSearch("timb"). // using first letters of words | ||||
| 			Lines( | ||||
| 				Contains(`this-is-my-branch`).IsSelected(), | ||||
| 			). | ||||
| 			FilterOrSearch("brnch"). // allows missing letter | ||||
| 			Lines( | ||||
| 				Contains(`other-branch`).IsSelected(), | ||||
| 				Contains(`this-is-my-branch`), | ||||
| 			) | ||||
| 	}, | ||||
| }) | ||||
| @@ -98,6 +98,7 @@ var tests = []*components.IntegrationTest{ | ||||
| 	file.RememberCommitMessageAfterFail, | ||||
| 	filter_and_search.FilterCommitFiles, | ||||
| 	filter_and_search.FilterFiles, | ||||
| 	filter_and_search.FilterFuzzy, | ||||
| 	filter_and_search.FilterMenu, | ||||
| 	filter_and_search.FilterRemoteBranches, | ||||
| 	filter_and_search.NestedFilter, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user