Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs-master/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,19 @@ gui:
portraitMode: auto

# How things are filtered when typing '/'.
# One of 'substring' (default) | 'fuzzy'
# One of 'substring' (default) | 'fuzzy' | 'regexp'
# In substring or fuzzy mode, prefix the filter with regexpFilterPrefix (default
# 're:') to use a Go regular
# expression for that filter only.
# In regexp mode, '.' and other metacharacters are regexp syntax (escape '.' to
# match a literal dot in paths).
filterMode: substring

# When filterMode is substring or fuzzy, if the filter starts with this string,
# the rest is treated as a
# Go regexp. Default is 're:'. Leave empty in config to use the default.
regexpFilterPrefix: 're:'

# Config relating to the spinner.
spinner:
# The frames of the spinner animation.
Expand Down
9 changes: 9 additions & 0 deletions docs-master/Searching.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

Depending on the currently focused view, hitting '/' will bring up a filter or search prompt. When filtering, the contents of the view will be filtered down to only those lines which match the query string. When searching, the contents of the view are not filtered, but matching lines are highlighted and you can iterate through matches with `n`/`N`.

### Regular expression filters

You can filter with a [Go regular expression](https://go.dev/s/re2syntax) in two ways:

- Set `gui.filterMode` to `regexp` in your config so every filter string is treated as a regexp.
- Or keep `substring` / `fuzzy` and prefix a single filter with `gui.regexpFilterPrefix` (default `re:`, for example `re:^main` to match branch names that start with `main`). You can change the prefix in config if `re:` collides with how you name branches or paths.

If the pattern has no uppercase letters, matching is case-insensitive (the same rule as plain substring filters). Invalid regexps match nothing. In regexp mode, `.` and other metacharacters are active, so file paths like `foo.go` need `foo\.go` to match a literal dot. Unlike substring mode, regexp mode uses one pattern for the whole line (whitespace inside the pattern is not split into multiple AND terms).

We intend to support filtering for the files view soon, but at the moment it uses searching. We intend to continue using search for the commits view because you typically care about the commits that come before/after a matching commit.

If you would like both filtering and searching to be enabled on a given view, please raise an issue for this.
Expand Down
10 changes: 9 additions & 1 deletion docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,17 @@ gui:
portraitMode: auto

# How things are filtered when typing '/'.
# One of 'substring' (default) | 'fuzzy'
# One of 'substring' (default) | 'fuzzy' | 'regexp'
# In substring or fuzzy mode, prefix the filter with regexpFilterPrefix (default
# 're:') to use a Go regular expression for that filter only. In regexp mode, the
# whole filter is a regexp ('.' matches any character; escape as '\\.' in YAML for
# a literal dot).
filterMode: substring

# When filterMode is substring or fuzzy, filters starting with this prefix use the
# remainder as a Go regexp. Default is 're:'. Omit or leave empty to use the default.
regexpFilterPrefix: 're:'

# Config relating to the spinner.
spinner:
# The frames of the spinner animation.
Expand Down
11 changes: 10 additions & 1 deletion docs/Searching.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@

Depending on the currently focused view, hitting '/' will bring up a filter or search prompt. When filtering, the contents of the view will be filtered down to only those lines which match the query string. When searching, the contents of the view are not filtered, but matching lines are highlighted and you can iterate through matches with `n`/`N`.

We intend to support filtering for the files view soon, but at the moment it uses searching. We intend to continue using search for the commits view because you typically care about the commits that come before/after a matching commit.
### Regular expression filters

You can filter with a [Go regular expression](https://go.dev/s/re2syntax) in two ways:

- Set `gui.filterMode` to `regexp` in your config so every filter string is treated as a regexp.
- Or keep `substring` / `fuzzy` and prefix a single filter with `gui.regexpFilterPrefix` (default `re:`, for example `re:^main` to match branch names that start with `main`). You can change the prefix in config if `re:` collides with how you name branches or paths.

If the pattern has no uppercase letters, matching is case-insensitive (the same rule as plain substring filters). Invalid regexps match nothing. In regexp mode, `.` and other metacharacters are active, so file paths like `foo.go` need `foo\.go` to match a literal dot. Unlike substring mode, regexp mode uses one pattern for the whole line (whitespace inside the pattern is not split into multiple AND terms).

We intend to continue using search for the commits view because you typically care about the commits that come before/after a matching commit.

If you would like both filtering and searching to be enabled on a given view, please raise an issue for this.

Expand Down
23 changes: 21 additions & 2 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"strings"
"time"

"github.com/karimkhaleel/jsonschema"
Expand Down Expand Up @@ -183,8 +184,14 @@ type GuiConfig struct {
// One of 'auto' (default) | 'always' | 'never'
PortraitMode string `yaml:"portraitMode"`
// How things are filtered when typing '/'.
// One of 'substring' (default) | 'fuzzy'
FilterMode string `yaml:"filterMode" jsonschema:"enum=substring,enum=fuzzy"`
// One of 'substring' (default) | 'fuzzy' | 'regexp'
// In substring or fuzzy mode, prefix the filter with regexpFilterPrefix (default 're:') to use a Go regular
// expression for that filter only.
// In regexp mode, '.' and other metacharacters are regexp syntax (escape '.' to match a literal dot in paths).
FilterMode string `yaml:"filterMode" jsonschema:"enum=substring,enum=fuzzy,enum=regexp"`
// When filterMode is substring or fuzzy, if the filter starts with this string, the rest is treated as a
// Go regexp. Default is 're:'. Leave empty in config to use the default.
RegexpFilterPrefix string `yaml:"regexpFilterPrefix"`
// Config relating to the spinner.
Spinner SpinnerConfig `yaml:"spinner"`
// Status panel view.
Expand All @@ -202,6 +209,17 @@ func (c *GuiConfig) UseFuzzySearch() bool {
return c.FilterMode == "fuzzy"
}

// DefaultRegexpFilterPrefix is used when regexpFilterPrefix is unset or whitespace-only in config.
const DefaultRegexpFilterPrefix = "re:"

// RegexpFilterPrefixOrDefault returns the configured one-off regexp filter prefix, or DefaultRegexpFilterPrefix.
func (c *GuiConfig) RegexpFilterPrefixOrDefault() string {
if strings.TrimSpace(c.RegexpFilterPrefix) == "" {
return DefaultRegexpFilterPrefix
}
return c.RegexpFilterPrefix
}

type ThemeConfig struct {
// Border color of focused window
ActiveBorderColor []string `yaml:"activeBorderColor" jsonschema:"minItems=1,uniqueItems=true"`
Expand Down Expand Up @@ -814,6 +832,7 @@ func GetDefaultConfig() *UserConfig {
AnimateExplosion: true,
PortraitMode: "auto",
FilterMode: "substring",
RegexpFilterPrefix: DefaultRegexpFilterPrefix,
Spinner: SpinnerConfig{
Frames: []string{"|", "/", "-", "\\"},
Rate: 50,
Expand Down
11 changes: 11 additions & 0 deletions pkg/config/user_config_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,14 @@ func TestUserConfigValidate_enums(t *testing.T) {
})
}
}

func TestRegexpFilterPrefixOrDefault(t *testing.T) {
c := GuiConfig{}
assert.Equal(t, DefaultRegexpFilterPrefix, c.RegexpFilterPrefixOrDefault())

c.RegexpFilterPrefix = " "
assert.Equal(t, DefaultRegexpFilterPrefix, c.RegexpFilterPrefixOrDefault())

c.RegexpFilterPrefix = "rx:"
assert.Equal(t, "rx:", c.RegexpFilterPrefixOrDefault())
}
55 changes: 45 additions & 10 deletions pkg/gui/context/filtered_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type FilteredList[T any] struct {
getFilterFields func(T) []string
preprocessFilter func(string) string
filter string
lastFilterMode string // last non-empty gui.filterMode passed to SetFilter; default substring when empty
lastRegexpPrefix string // last gui.regexpFilterPrefix (OrDefault) passed to SetFilter

mutex deadlock.Mutex
}
Expand All @@ -35,18 +37,46 @@ func (self *FilteredList[T]) GetFilter() string {
return self.filter
}

func (self *FilteredList[T]) SetFilter(filter string, useFuzzySearch bool) {
func (self *FilteredList[T]) SetFilter(filter string, useFuzzySearch bool, filterMode string, regexpPrefix string) {
self.filter = filter
if filterMode != "" {
self.lastFilterMode = filterMode
}
if regexpPrefix != "" {
self.lastRegexpPrefix = regexpPrefix
}

self.applyFilter(useFuzzySearch)
self.applyFilter(useFuzzySearch, filterMode, regexpPrefix)
}

func (self *FilteredList[T]) ClearFilter() {
self.SetFilter("", false)
mode := self.lastFilterMode
if mode == "" {
mode = "substring"
}
prefix := self.lastRegexpPrefix
if prefix == "" {
prefix = "re:"
}
self.SetFilter("", false, mode, prefix)
}

func (self *FilteredList[T]) ReApplyFilter(useFuzzySearch bool) {
self.applyFilter(useFuzzySearch)
func (self *FilteredList[T]) ReApplyFilter(useFuzzySearch bool, filterMode string, regexpPrefix string) {
mode := filterMode
if mode == "" {
mode = self.lastFilterMode
}
if mode == "" {
mode = "substring"
}
prefix := regexpPrefix
if prefix == "" {
prefix = self.lastRegexpPrefix
}
if prefix == "" {
prefix = "re:"
}
self.applyFilter(useFuzzySearch, mode, prefix)
}

func (self *FilteredList[T]) IsFiltering() bool {
Expand Down Expand Up @@ -80,24 +110,29 @@ func (self *fuzzySource[T]) Len() int {
return len(self.list)
}

func (self *FilteredList[T]) applyFilter(useFuzzySearch bool) {
func (self *FilteredList[T]) applyFilter(useFuzzySearch bool, filterMode string, regexpPrefix string) {
self.mutex.Lock()
defer self.mutex.Unlock()

filter := self.filter
processed := self.filter
if self.preprocessFilter != nil {
filter = self.preprocessFilter(filter)
processed = self.preprocessFilter(self.filter)
}

if filter == "" {
if processed == "" {
self.filteredIndices = nil
} else {
source := &fuzzySource[T]{
list: self.getList(),
getFilterFields: self.getFilterFields,
}

matches := utils.FindFrom(filter, source, useFuzzySearch)
prefix := regexpPrefix
if prefix == "" {
prefix = "re:"
}
pattern, useRegexp := utils.ViewFilterPattern(filterMode, processed, prefix)
matches := utils.FindFrom(pattern, source, useFuzzySearch, useRegexp)
self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int {
return match.Index
})
Expand Down
6 changes: 4 additions & 2 deletions pkg/gui/controllers/helpers/search_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) {
case types.IFilterableContext:
context.SetSelection(0)
context.GetView().SetOriginY(0)
context.SetFilter(searchString, self.c.UserConfig().Gui.UseFuzzySearch())
gui := self.c.UserConfig().Gui
context.SetFilter(searchString, gui.UseFuzzySearch(), gui.FilterMode, gui.RegexpFilterPrefixOrDefault())
self.c.PostRefreshUpdate(context)
case types.ISearchableContext:
// do nothing
Expand All @@ -244,7 +245,8 @@ func (self *SearchHelper) ReApplyFilter(context types.Context) {
filterableContext.SetSelection(0)
filterableContext.GetView().SetOriginY(0)
}
filterableContext.ReApplyFilter(self.c.UserConfig().Gui.UseFuzzySearch())
gui := self.c.UserConfig().Gui
filterableContext.ReApplyFilter(gui.UseFuzzySearch(), gui.FilterMode, gui.RegexpFilterPrefixOrDefault())
}
}

Expand Down
24 changes: 19 additions & 5 deletions pkg/gui/filetree/commit_file_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ type ICommitFileTree interface {
GetAllItems() []*CommitFileNode
GetAllFiles() []*models.CommitFile
GetRoot() *CommitFileNode
SetTextFilter(filter string, useFuzzySearch bool)
SetTextFilter(filter string, useFuzzySearch bool, filterMode string, regexpPrefix string)
GetTextFilter() string
TextFilterMode() string
TextRegexpFilterPrefix() string
}

type CommitFileTree struct {
Expand All @@ -25,8 +27,10 @@ type CommitFileTree struct {
showTree bool
common *common.Common
collapsedPaths *CollapsedPaths
textFilter string
useFuzzySearch bool
textFilter string
useFuzzySearch bool
textFilterMode string
textRegexpPrefix string
}

func (self *CommitFileTree) CollapseAll() {
Expand Down Expand Up @@ -100,7 +104,7 @@ func (self *CommitFileTree) GetAllFiles() []*models.CommitFile {
func (self *CommitFileTree) getFilesForDisplay() []*models.CommitFile {
files := self.getFiles()
if self.textFilter != "" {
files = filterCommitFilesByText(files, self.textFilter, self.useFuzzySearch)
files = filterCommitFilesByText(files, self.textFilter, self.useFuzzySearch, self.textFilterMode, self.textRegexpPrefix)
}
return files
}
Expand All @@ -115,16 +119,26 @@ func (self *CommitFileTree) SetTree() {
}
}

func (self *CommitFileTree) SetTextFilter(filter string, useFuzzySearch bool) {
func (self *CommitFileTree) SetTextFilter(filter string, useFuzzySearch bool, filterMode string, regexpPrefix string) {
self.textFilter = filter
self.useFuzzySearch = useFuzzySearch
self.textFilterMode = filterMode
self.textRegexpPrefix = regexpPrefix
self.SetTree()
}

func (self *CommitFileTree) GetTextFilter() string {
return self.textFilter
}

func (self *CommitFileTree) TextFilterMode() string {
return self.textFilterMode
}

func (self *CommitFileTree) TextRegexpFilterPrefix() string {
return self.textRegexpPrefix
}

func (self *CommitFileTree) IsCollapsed(path string) bool {
return self.collapsedPaths.IsCollapsed(path)
}
Expand Down
32 changes: 27 additions & 5 deletions pkg/gui/filetree/commit_file_tree_view_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ func (self *CommitFileTreeViewModel) SelectPath(filepath string, showRootItem bo

// IFilterableContext methods

func (self *CommitFileTreeViewModel) SetFilter(filter string, useFuzzySearch bool) {
self.ICommitFileTree.SetTextFilter(filter, useFuzzySearch)
func (self *CommitFileTreeViewModel) SetFilter(filter string, useFuzzySearch bool, filterMode string, regexpPrefix string) {
self.ICommitFileTree.SetTextFilter(filter, useFuzzySearch, filterMode, regexpPrefix)
}

func (self *CommitFileTreeViewModel) GetFilter() string {
Expand All @@ -226,7 +226,15 @@ func (self *CommitFileTreeViewModel) ClearFilter() {
selectedPath = selectedNode.GetInternalPath()
}

self.ICommitFileTree.SetTextFilter("", false)
mode := self.ICommitFileTree.TextFilterMode()
if mode == "" {
mode = "substring"
}
prefix := self.ICommitFileTree.TextRegexpFilterPrefix()
if prefix == "" {
prefix = "re:"
}
self.ICommitFileTree.SetTextFilter("", false, mode, prefix)

if selectedPath != "" {
self.ExpandToPath(selectedPath)
Expand All @@ -238,8 +246,22 @@ func (self *CommitFileTreeViewModel) ClearFilter() {
self.ClampSelection()
}

func (self *CommitFileTreeViewModel) ReApplyFilter(useFuzzySearch bool) {
self.ICommitFileTree.SetTextFilter(self.ICommitFileTree.GetTextFilter(), useFuzzySearch)
func (self *CommitFileTreeViewModel) ReApplyFilter(useFuzzySearch bool, filterMode string, regexpPrefix string) {
mode := filterMode
if mode == "" {
mode = self.ICommitFileTree.TextFilterMode()
}
if mode == "" {
mode = "substring"
}
prefix := regexpPrefix
if prefix == "" {
prefix = self.ICommitFileTree.TextRegexpFilterPrefix()
}
if prefix == "" {
prefix = "re:"
}
self.ICommitFileTree.SetTextFilter(self.ICommitFileTree.GetTextFilter(), useFuzzySearch, mode, prefix)
}

func (self *CommitFileTreeViewModel) IsFiltering() bool {
Expand Down
10 changes: 6 additions & 4 deletions pkg/gui/filetree/file_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ func (s *filePathSource) Len() int {
return len(s.files)
}

func filterFilesByText(files []*models.File, filter string, useFuzzySearch bool) []*models.File {
func filterFilesByText(files []*models.File, filter string, useFuzzySearch bool, filterMode string, regexpPrefix string) []*models.File {
source := &filePathSource{files: files}
matches := utils.FindFrom(filter, source, useFuzzySearch)
pattern, useRegexp := utils.ViewFilterPattern(filterMode, filter, regexpPrefix)
matches := utils.FindFrom(pattern, source, useFuzzySearch, useRegexp)
return lo.Map(matches, func(match fuzzy.Match, _ int) *models.File {
return files[match.Index]
})
Expand All @@ -39,9 +40,10 @@ func (s *commitFilePathSource) Len() int {
return len(s.files)
}

func filterCommitFilesByText(files []*models.CommitFile, filter string, useFuzzySearch bool) []*models.CommitFile {
func filterCommitFilesByText(files []*models.CommitFile, filter string, useFuzzySearch bool, filterMode string, regexpPrefix string) []*models.CommitFile {
source := &commitFilePathSource{files: files}
matches := utils.FindFrom(filter, source, useFuzzySearch)
pattern, useRegexp := utils.ViewFilterPattern(filterMode, filter, regexpPrefix)
matches := utils.FindFrom(pattern, source, useFuzzySearch, useRegexp)
return lo.Map(matches, func(match fuzzy.Match, _ int) *models.CommitFile {
return files[match.Index]
})
Expand Down
Loading