From 4df353d0066173a6967d8e8ddd082791a053bbbc Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sun, 2 Jul 2023 15:46:06 +1000
Subject: [PATCH 01/20] Bump gocui

---
 go.mod                                        |  8 ++--
 go.sum                                        | 15 +++---
 vendor/github.com/jesseduffield/gocui/view.go |  4 ++
 vendor/golang.org/x/sys/cpu/endian_little.go  |  4 +-
 vendor/golang.org/x/sys/unix/mkall.sh         |  2 +-
 vendor/golang.org/x/sys/unix/mkerrors.sh      |  6 ++-
 vendor/golang.org/x/sys/unix/syscall_linux.go | 30 +++++++++++-
 .../golang.org/x/sys/unix/syscall_openbsd.go  | 17 ++++++-
 .../x/sys/unix/zerrors_linux_sparc64.go       | 48 +++++++++++++++++++
 .../golang.org/x/sys/unix/zsyscall_linux.go   | 14 ++++++
 .../x/sys/unix/zsyscall_openbsd_386.go        | 22 +++++++++
 .../x/sys/unix/zsyscall_openbsd_386.s         | 10 ++++
 .../x/sys/unix/zsyscall_openbsd_amd64.go      | 36 +++++++++++---
 .../x/sys/unix/zsyscall_openbsd_amd64.s       | 10 ++++
 .../x/sys/unix/zsyscall_openbsd_arm.go        | 22 +++++++++
 .../x/sys/unix/zsyscall_openbsd_arm.s         | 10 ++++
 .../x/sys/unix/zsyscall_openbsd_arm64.go      | 22 +++++++++
 .../x/sys/unix/zsyscall_openbsd_arm64.s       | 10 ++++
 .../x/sys/unix/zsyscall_openbsd_mips64.go     | 22 +++++++++
 .../x/sys/unix/zsyscall_openbsd_mips64.s      | 10 ++++
 .../x/sys/unix/zsyscall_openbsd_ppc64.go      | 22 +++++++++
 .../x/sys/unix/zsyscall_openbsd_ppc64.s       | 12 +++++
 .../x/sys/unix/zsyscall_openbsd_riscv64.go    | 22 +++++++++
 .../x/sys/unix/zsyscall_openbsd_riscv64.s     | 10 ++++
 vendor/golang.org/x/sys/unix/ztypes_linux.go  | 46 ++++++++++++++++++
 .../x/sys/windows/syscall_windows.go          | 13 ++++-
 .../x/sys/windows/zsyscall_windows.go         |  8 +---
 vendor/modules.txt                            |  8 ++--
 28 files changed, 426 insertions(+), 37 deletions(-)

diff --git a/go.mod b/go.mod
index 691c9b1a3..28a3781d6 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
 	github.com/integrii/flaggy v1.4.0
 	github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
 	github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
-	github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e
+	github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce
 	github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
 	github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
 	github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@@ -67,8 +67,8 @@ require (
 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
 	golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
 	golang.org/x/net v0.7.0 // indirect
-	golang.org/x/sys v0.8.0 // indirect
-	golang.org/x/term v0.8.0 // indirect
-	golang.org/x/text v0.9.0 // indirect
+	golang.org/x/sys v0.9.0 // indirect
+	golang.org/x/term v0.9.0 // indirect
+	golang.org/x/text v0.10.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 )
diff --git a/go.sum b/go.sum
index 4f961d11c..0b63b9e13 100644
--- a/go.sum
+++ b/go.sum
@@ -72,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
 github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
 github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
 github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
-github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e h1:NpsrRAbYUmMkxDgNAVSlu3LxtfwdDe140vWVo/VldgA=
-github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
+github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce h1:Xgm21B1an/outcRxnkDfMT6wKb6SKBR05jXOyfPA8WQ=
+github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
 github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
 github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
 github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
@@ -206,21 +206,22 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
+golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go
index 6bc1c195c..f4b3ad528 100644
--- a/vendor/github.com/jesseduffield/gocui/view.go
+++ b/vendor/github.com/jesseduffield/gocui/view.go
@@ -207,6 +207,10 @@ func (v *View) gotoPreviousMatch() error {
 	return v.SelectSearchResult(v.searcher.currentSearchIndex)
 }
 
+func (v *View) SelectCurrentSearchResult() error {
+	return v.SelectSearchResult(v.searcher.currentSearchIndex)
+}
+
 func (v *View) SelectSearchResult(index int) error {
 	itemCount := len(v.searcher.searchPositions)
 	if itemCount == 0 {
diff --git a/vendor/golang.org/x/sys/cpu/endian_little.go b/vendor/golang.org/x/sys/cpu/endian_little.go
index fe545966b..55db853ef 100644
--- a/vendor/golang.org/x/sys/cpu/endian_little.go
+++ b/vendor/golang.org/x/sys/cpu/endian_little.go
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build 386 || amd64 || amd64p32 || alpha || arm || arm64 || loong64 || mipsle || mips64le || mips64p32le || nios2 || ppc64le || riscv || riscv64 || sh
-// +build 386 amd64 amd64p32 alpha arm arm64 loong64 mipsle mips64le mips64p32le nios2 ppc64le riscv riscv64 sh
+//go:build 386 || amd64 || amd64p32 || alpha || arm || arm64 || loong64 || mipsle || mips64le || mips64p32le || nios2 || ppc64le || riscv || riscv64 || sh || wasm
+// +build 386 amd64 amd64p32 alpha arm arm64 loong64 mipsle mips64le mips64p32le nios2 ppc64le riscv riscv64 sh wasm
 
 package cpu
 
diff --git a/vendor/golang.org/x/sys/unix/mkall.sh b/vendor/golang.org/x/sys/unix/mkall.sh
index 8e3947c36..e6f31d374 100644
--- a/vendor/golang.org/x/sys/unix/mkall.sh
+++ b/vendor/golang.org/x/sys/unix/mkall.sh
@@ -50,7 +50,7 @@ if [[ "$GOOS" = "linux" ]]; then
 	# Use the Docker-based build system
 	# Files generated through docker (use $cmd so you can Ctl-C the build or run)
 	$cmd docker build --tag generate:$GOOS $GOOS
-	$cmd docker run --interactive --tty --volume $(cd -- "$(dirname -- "$0")/.." && /bin/pwd):/build generate:$GOOS
+	$cmd docker run --interactive --tty --volume $(cd -- "$(dirname -- "$0")/.." && pwd):/build generate:$GOOS
 	exit
 fi
 
diff --git a/vendor/golang.org/x/sys/unix/mkerrors.sh b/vendor/golang.org/x/sys/unix/mkerrors.sh
index be0423e68..315646271 100644
--- a/vendor/golang.org/x/sys/unix/mkerrors.sh
+++ b/vendor/golang.org/x/sys/unix/mkerrors.sh
@@ -741,7 +741,8 @@ main(void)
 		e = errors[i].num;
 		if(i > 0 && errors[i-1].num == e)
 			continue;
-		strcpy(buf, strerror(e));
+		strncpy(buf, strerror(e), sizeof(buf) - 1);
+		buf[sizeof(buf) - 1] = '\0';
 		// lowercase first letter: Bad -> bad, but STREAM -> STREAM.
 		if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z)
 			buf[0] += a - A;
@@ -760,7 +761,8 @@ main(void)
 		e = signals[i].num;
 		if(i > 0 && signals[i-1].num == e)
 			continue;
-		strcpy(buf, strsignal(e));
+		strncpy(buf, strsignal(e), sizeof(buf) - 1);
+		buf[sizeof(buf) - 1] = '\0';
 		// lowercase first letter: Bad -> bad, but STREAM -> STREAM.
 		if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z)
 			buf[0] += a - A;
diff --git a/vendor/golang.org/x/sys/unix/syscall_linux.go b/vendor/golang.org/x/sys/unix/syscall_linux.go
index fbaeb5fff..6de486bef 100644
--- a/vendor/golang.org/x/sys/unix/syscall_linux.go
+++ b/vendor/golang.org/x/sys/unix/syscall_linux.go
@@ -1699,12 +1699,23 @@ func PtracePokeUser(pid int, addr uintptr, data []byte) (count int, err error) {
 	return ptracePoke(PTRACE_POKEUSR, PTRACE_PEEKUSR, pid, addr, data)
 }
 
+// elfNT_PRSTATUS is a copy of the debug/elf.NT_PRSTATUS constant so
+// x/sys/unix doesn't need to depend on debug/elf and thus
+// compress/zlib, debug/dwarf, and other packages.
+const elfNT_PRSTATUS = 1
+
 func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) {
-	return ptracePtr(PTRACE_GETREGS, pid, 0, unsafe.Pointer(regsout))
+	var iov Iovec
+	iov.Base = (*byte)(unsafe.Pointer(regsout))
+	iov.SetLen(int(unsafe.Sizeof(*regsout)))
+	return ptracePtr(PTRACE_GETREGSET, pid, uintptr(elfNT_PRSTATUS), unsafe.Pointer(&iov))
 }
 
 func PtraceSetRegs(pid int, regs *PtraceRegs) (err error) {
-	return ptracePtr(PTRACE_SETREGS, pid, 0, unsafe.Pointer(regs))
+	var iov Iovec
+	iov.Base = (*byte)(unsafe.Pointer(regs))
+	iov.SetLen(int(unsafe.Sizeof(*regs)))
+	return ptracePtr(PTRACE_SETREGSET, pid, uintptr(elfNT_PRSTATUS), unsafe.Pointer(&iov))
 }
 
 func PtraceSetOptions(pid int, options int) (err error) {
@@ -2420,6 +2431,21 @@ func PthreadSigmask(how int, set, oldset *Sigset_t) error {
 	return rtSigprocmask(how, set, oldset, _C__NSIG/8)
 }
 
+//sysnb	getresuid(ruid *_C_int, euid *_C_int, suid *_C_int)
+//sysnb	getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int)
+
+func Getresuid() (ruid, euid, suid int) {
+	var r, e, s _C_int
+	getresuid(&r, &e, &s)
+	return int(r), int(e), int(s)
+}
+
+func Getresgid() (rgid, egid, sgid int) {
+	var r, e, s _C_int
+	getresgid(&r, &e, &s)
+	return int(r), int(e), int(s)
+}
+
 /*
  * Unimplemented
  */
diff --git a/vendor/golang.org/x/sys/unix/syscall_openbsd.go b/vendor/golang.org/x/sys/unix/syscall_openbsd.go
index f9c7a9663..c5f166a11 100644
--- a/vendor/golang.org/x/sys/unix/syscall_openbsd.go
+++ b/vendor/golang.org/x/sys/unix/syscall_openbsd.go
@@ -151,6 +151,21 @@ func Getfsstat(buf []Statfs_t, flags int) (n int, err error) {
 	return
 }
 
+//sysnb	getresuid(ruid *_C_int, euid *_C_int, suid *_C_int)
+//sysnb	getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int)
+
+func Getresuid() (ruid, euid, suid int) {
+	var r, e, s _C_int
+	getresuid(&r, &e, &s)
+	return int(r), int(e), int(s)
+}
+
+func Getresgid() (rgid, egid, sgid int) {
+	var r, e, s _C_int
+	getresgid(&r, &e, &s)
+	return int(r), int(e), int(s)
+}
+
 //sys	ioctl(fd int, req uint, arg uintptr) (err error)
 //sys	ioctlPtr(fd int, req uint, arg unsafe.Pointer) (err error) = SYS_IOCTL
 
@@ -338,8 +353,6 @@ func Uname(uname *Utsname) error {
 // getgid
 // getitimer
 // getlogin
-// getresgid
-// getresuid
 // getthrid
 // ktrace
 // lfs_bmapv
diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go
index f61925269..48984202c 100644
--- a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go
+++ b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go
@@ -329,6 +329,54 @@ const (
 	SCM_WIFI_STATUS                  = 0x25
 	SFD_CLOEXEC                      = 0x400000
 	SFD_NONBLOCK                     = 0x4000
+	SF_FP                            = 0x38
+	SF_I0                            = 0x20
+	SF_I1                            = 0x24
+	SF_I2                            = 0x28
+	SF_I3                            = 0x2c
+	SF_I4                            = 0x30
+	SF_I5                            = 0x34
+	SF_L0                            = 0x0
+	SF_L1                            = 0x4
+	SF_L2                            = 0x8
+	SF_L3                            = 0xc
+	SF_L4                            = 0x10
+	SF_L5                            = 0x14
+	SF_L6                            = 0x18
+	SF_L7                            = 0x1c
+	SF_PC                            = 0x3c
+	SF_RETP                          = 0x40
+	SF_V9_FP                         = 0x70
+	SF_V9_I0                         = 0x40
+	SF_V9_I1                         = 0x48
+	SF_V9_I2                         = 0x50
+	SF_V9_I3                         = 0x58
+	SF_V9_I4                         = 0x60
+	SF_V9_I5                         = 0x68
+	SF_V9_L0                         = 0x0
+	SF_V9_L1                         = 0x8
+	SF_V9_L2                         = 0x10
+	SF_V9_L3                         = 0x18
+	SF_V9_L4                         = 0x20
+	SF_V9_L5                         = 0x28
+	SF_V9_L6                         = 0x30
+	SF_V9_L7                         = 0x38
+	SF_V9_PC                         = 0x78
+	SF_V9_RETP                       = 0x80
+	SF_V9_XARG0                      = 0x88
+	SF_V9_XARG1                      = 0x90
+	SF_V9_XARG2                      = 0x98
+	SF_V9_XARG3                      = 0xa0
+	SF_V9_XARG4                      = 0xa8
+	SF_V9_XARG5                      = 0xb0
+	SF_V9_XXARG                      = 0xb8
+	SF_XARG0                         = 0x44
+	SF_XARG1                         = 0x48
+	SF_XARG2                         = 0x4c
+	SF_XARG3                         = 0x50
+	SF_XARG4                         = 0x54
+	SF_XARG5                         = 0x58
+	SF_XXARG                         = 0x5c
 	SIOCATMARK                       = 0x8905
 	SIOCGPGRP                        = 0x8904
 	SIOCGSTAMPNS_NEW                 = 0x40108907
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_linux.go b/vendor/golang.org/x/sys/unix/zsyscall_linux.go
index da63d9d78..722c29a00 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_linux.go
+++ b/vendor/golang.org/x/sys/unix/zsyscall_linux.go
@@ -2172,3 +2172,17 @@ func rtSigprocmask(how int, set *Sigset_t, oldset *Sigset_t, sigsetsize uintptr)
 	}
 	return
 }
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func getresuid(ruid *_C_int, euid *_C_int, suid *_C_int) {
+	RawSyscallNoError(SYS_GETRESUID, uintptr(unsafe.Pointer(ruid)), uintptr(unsafe.Pointer(euid)), uintptr(unsafe.Pointer(suid)))
+	return
+}
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int) {
+	RawSyscallNoError(SYS_GETRESGID, uintptr(unsafe.Pointer(rgid)), uintptr(unsafe.Pointer(egid)), uintptr(unsafe.Pointer(sgid)))
+	return
+}
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.go
index 6699a783e..9ab9abf72 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.go
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.go
@@ -519,6 +519,28 @@ var libc_getcwd_trampoline_addr uintptr
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
+func getresuid(ruid *_C_int, euid *_C_int, suid *_C_int) {
+	syscall_rawSyscall(libc_getresuid_trampoline_addr, uintptr(unsafe.Pointer(ruid)), uintptr(unsafe.Pointer(euid)), uintptr(unsafe.Pointer(suid)))
+	return
+}
+
+var libc_getresuid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresuid getresuid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int) {
+	syscall_rawSyscall(libc_getresgid_trampoline_addr, uintptr(unsafe.Pointer(rgid)), uintptr(unsafe.Pointer(egid)), uintptr(unsafe.Pointer(sgid)))
+	return
+}
+
+var libc_getresgid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresgid getresgid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
 func ioctl(fd int, req uint, arg uintptr) (err error) {
 	_, _, e1 := syscall_syscall(libc_ioctl_trampoline_addr, uintptr(fd), uintptr(req), uintptr(arg))
 	if e1 != 0 {
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.s
index 04f0de34b..3dcacd30d 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.s
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.s
@@ -158,6 +158,16 @@ TEXT libc_getcwd_trampoline<>(SB),NOSPLIT,$0-0
 GLOBL	·libc_getcwd_trampoline_addr(SB), RODATA, $4
 DATA	·libc_getcwd_trampoline_addr(SB)/4, $libc_getcwd_trampoline<>(SB)
 
+TEXT libc_getresuid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresuid(SB)
+GLOBL	·libc_getresuid_trampoline_addr(SB), RODATA, $4
+DATA	·libc_getresuid_trampoline_addr(SB)/4, $libc_getresuid_trampoline<>(SB)
+
+TEXT libc_getresgid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresgid(SB)
+GLOBL	·libc_getresgid_trampoline_addr(SB), RODATA, $4
+DATA	·libc_getresgid_trampoline_addr(SB)/4, $libc_getresgid_trampoline<>(SB)
+
 TEXT libc_ioctl_trampoline<>(SB),NOSPLIT,$0-0
 	JMP	libc_ioctl(SB)
 GLOBL	·libc_ioctl_trampoline_addr(SB), RODATA, $4
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.go
index 1e775fe05..915761eab 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.go
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.go
@@ -519,15 +519,29 @@ var libc_getcwd_trampoline_addr uintptr
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
-func ioctl(fd int, req uint, arg uintptr) (err error) {
-	_, _, e1 := syscall_syscall(libc_ioctl_trampoline_addr, uintptr(fd), uintptr(req), uintptr(arg))
-	if e1 != 0 {
-		err = errnoErr(e1)
-	}
+func getresuid(ruid *_C_int, euid *_C_int, suid *_C_int) {
+	syscall_rawSyscall(libc_getresuid_trampoline_addr, uintptr(unsafe.Pointer(ruid)), uintptr(unsafe.Pointer(euid)), uintptr(unsafe.Pointer(suid)))
 	return
 }
 
-func ioctlPtr(fd int, req uint, arg unsafe.Pointer) (err error) {
+var libc_getresuid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresuid getresuid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int) {
+	syscall_rawSyscall(libc_getresgid_trampoline_addr, uintptr(unsafe.Pointer(rgid)), uintptr(unsafe.Pointer(egid)), uintptr(unsafe.Pointer(sgid)))
+	return
+}
+
+var libc_getresgid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresgid getresgid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func ioctl(fd int, req uint, arg uintptr) (err error) {
 	_, _, e1 := syscall_syscall(libc_ioctl_trampoline_addr, uintptr(fd), uintptr(req), uintptr(arg))
 	if e1 != 0 {
 		err = errnoErr(e1)
@@ -541,6 +555,16 @@ var libc_ioctl_trampoline_addr uintptr
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
+func ioctlPtr(fd int, req uint, arg unsafe.Pointer) (err error) {
+	_, _, e1 := syscall_syscall(libc_ioctl_trampoline_addr, uintptr(fd), uintptr(req), uintptr(arg))
+	if e1 != 0 {
+		err = errnoErr(e1)
+	}
+	return
+}
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
 func sysctl(mib []_C_int, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) {
 	var _p0 unsafe.Pointer
 	if len(mib) > 0 {
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.s
index 27b6f4df7..2763620b0 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.s
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.s
@@ -158,6 +158,16 @@ TEXT libc_getcwd_trampoline<>(SB),NOSPLIT,$0-0
 GLOBL	·libc_getcwd_trampoline_addr(SB), RODATA, $8
 DATA	·libc_getcwd_trampoline_addr(SB)/8, $libc_getcwd_trampoline<>(SB)
 
+TEXT libc_getresuid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresuid(SB)
+GLOBL	·libc_getresuid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresuid_trampoline_addr(SB)/8, $libc_getresuid_trampoline<>(SB)
+
+TEXT libc_getresgid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresgid(SB)
+GLOBL	·libc_getresgid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresgid_trampoline_addr(SB)/8, $libc_getresgid_trampoline<>(SB)
+
 TEXT libc_ioctl_trampoline<>(SB),NOSPLIT,$0-0
 	JMP	libc_ioctl(SB)
 GLOBL	·libc_ioctl_trampoline_addr(SB), RODATA, $8
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.go
index 7f6427899..8e87fdf15 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.go
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.go
@@ -519,6 +519,28 @@ var libc_getcwd_trampoline_addr uintptr
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
+func getresuid(ruid *_C_int, euid *_C_int, suid *_C_int) {
+	syscall_rawSyscall(libc_getresuid_trampoline_addr, uintptr(unsafe.Pointer(ruid)), uintptr(unsafe.Pointer(euid)), uintptr(unsafe.Pointer(suid)))
+	return
+}
+
+var libc_getresuid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresuid getresuid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int) {
+	syscall_rawSyscall(libc_getresgid_trampoline_addr, uintptr(unsafe.Pointer(rgid)), uintptr(unsafe.Pointer(egid)), uintptr(unsafe.Pointer(sgid)))
+	return
+}
+
+var libc_getresgid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresgid getresgid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
 func ioctl(fd int, req uint, arg uintptr) (err error) {
 	_, _, e1 := syscall_syscall(libc_ioctl_trampoline_addr, uintptr(fd), uintptr(req), uintptr(arg))
 	if e1 != 0 {
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.s
index b797045fd..c92231404 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.s
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.s
@@ -158,6 +158,16 @@ TEXT libc_getcwd_trampoline<>(SB),NOSPLIT,$0-0
 GLOBL	·libc_getcwd_trampoline_addr(SB), RODATA, $4
 DATA	·libc_getcwd_trampoline_addr(SB)/4, $libc_getcwd_trampoline<>(SB)
 
+TEXT libc_getresuid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresuid(SB)
+GLOBL	·libc_getresuid_trampoline_addr(SB), RODATA, $4
+DATA	·libc_getresuid_trampoline_addr(SB)/4, $libc_getresuid_trampoline<>(SB)
+
+TEXT libc_getresgid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresgid(SB)
+GLOBL	·libc_getresgid_trampoline_addr(SB), RODATA, $4
+DATA	·libc_getresgid_trampoline_addr(SB)/4, $libc_getresgid_trampoline<>(SB)
+
 TEXT libc_ioctl_trampoline<>(SB),NOSPLIT,$0-0
 	JMP	libc_ioctl(SB)
 GLOBL	·libc_ioctl_trampoline_addr(SB), RODATA, $4
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.go
index 756ef7b17..12a7a2160 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.go
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.go
@@ -519,6 +519,28 @@ var libc_getcwd_trampoline_addr uintptr
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
+func getresuid(ruid *_C_int, euid *_C_int, suid *_C_int) {
+	syscall_rawSyscall(libc_getresuid_trampoline_addr, uintptr(unsafe.Pointer(ruid)), uintptr(unsafe.Pointer(euid)), uintptr(unsafe.Pointer(suid)))
+	return
+}
+
+var libc_getresuid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresuid getresuid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int) {
+	syscall_rawSyscall(libc_getresgid_trampoline_addr, uintptr(unsafe.Pointer(rgid)), uintptr(unsafe.Pointer(egid)), uintptr(unsafe.Pointer(sgid)))
+	return
+}
+
+var libc_getresgid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresgid getresgid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
 func ioctl(fd int, req uint, arg uintptr) (err error) {
 	_, _, e1 := syscall_syscall(libc_ioctl_trampoline_addr, uintptr(fd), uintptr(req), uintptr(arg))
 	if e1 != 0 {
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.s
index a87126622..a6bc32c92 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.s
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.s
@@ -158,6 +158,16 @@ TEXT libc_getcwd_trampoline<>(SB),NOSPLIT,$0-0
 GLOBL	·libc_getcwd_trampoline_addr(SB), RODATA, $8
 DATA	·libc_getcwd_trampoline_addr(SB)/8, $libc_getcwd_trampoline<>(SB)
 
+TEXT libc_getresuid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresuid(SB)
+GLOBL	·libc_getresuid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresuid_trampoline_addr(SB)/8, $libc_getresuid_trampoline<>(SB)
+
+TEXT libc_getresgid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresgid(SB)
+GLOBL	·libc_getresgid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresgid_trampoline_addr(SB)/8, $libc_getresgid_trampoline<>(SB)
+
 TEXT libc_ioctl_trampoline<>(SB),NOSPLIT,$0-0
 	JMP	libc_ioctl(SB)
 GLOBL	·libc_ioctl_trampoline_addr(SB), RODATA, $8
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.go
index 7bc2e24eb..b19e8aa03 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.go
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.go
@@ -519,6 +519,28 @@ var libc_getcwd_trampoline_addr uintptr
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
+func getresuid(ruid *_C_int, euid *_C_int, suid *_C_int) {
+	syscall_rawSyscall(libc_getresuid_trampoline_addr, uintptr(unsafe.Pointer(ruid)), uintptr(unsafe.Pointer(euid)), uintptr(unsafe.Pointer(suid)))
+	return
+}
+
+var libc_getresuid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresuid getresuid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int) {
+	syscall_rawSyscall(libc_getresgid_trampoline_addr, uintptr(unsafe.Pointer(rgid)), uintptr(unsafe.Pointer(egid)), uintptr(unsafe.Pointer(sgid)))
+	return
+}
+
+var libc_getresgid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresgid getresgid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
 func ioctl(fd int, req uint, arg uintptr) (err error) {
 	_, _, e1 := syscall_syscall(libc_ioctl_trampoline_addr, uintptr(fd), uintptr(req), uintptr(arg))
 	if e1 != 0 {
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.s
index 05d4bffd7..b4e7bceab 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.s
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.s
@@ -158,6 +158,16 @@ TEXT libc_getcwd_trampoline<>(SB),NOSPLIT,$0-0
 GLOBL	·libc_getcwd_trampoline_addr(SB), RODATA, $8
 DATA	·libc_getcwd_trampoline_addr(SB)/8, $libc_getcwd_trampoline<>(SB)
 
+TEXT libc_getresuid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresuid(SB)
+GLOBL	·libc_getresuid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresuid_trampoline_addr(SB)/8, $libc_getresuid_trampoline<>(SB)
+
+TEXT libc_getresgid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresgid(SB)
+GLOBL	·libc_getresgid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresgid_trampoline_addr(SB)/8, $libc_getresgid_trampoline<>(SB)
+
 TEXT libc_ioctl_trampoline<>(SB),NOSPLIT,$0-0
 	JMP	libc_ioctl(SB)
 GLOBL	·libc_ioctl_trampoline_addr(SB), RODATA, $8
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.go
index 739be6217..fb99594c9 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.go
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.go
@@ -519,6 +519,28 @@ var libc_getcwd_trampoline_addr uintptr
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
+func getresuid(ruid *_C_int, euid *_C_int, suid *_C_int) {
+	syscall_rawSyscall(libc_getresuid_trampoline_addr, uintptr(unsafe.Pointer(ruid)), uintptr(unsafe.Pointer(euid)), uintptr(unsafe.Pointer(suid)))
+	return
+}
+
+var libc_getresuid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresuid getresuid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int) {
+	syscall_rawSyscall(libc_getresgid_trampoline_addr, uintptr(unsafe.Pointer(rgid)), uintptr(unsafe.Pointer(egid)), uintptr(unsafe.Pointer(sgid)))
+	return
+}
+
+var libc_getresgid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresgid getresgid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
 func ioctl(fd int, req uint, arg uintptr) (err error) {
 	_, _, e1 := syscall_syscall(libc_ioctl_trampoline_addr, uintptr(fd), uintptr(req), uintptr(arg))
 	if e1 != 0 {
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.s
index 74a25f8d6..ca3f76600 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.s
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.s
@@ -189,6 +189,18 @@ TEXT libc_getcwd_trampoline<>(SB),NOSPLIT,$0-0
 GLOBL	·libc_getcwd_trampoline_addr(SB), RODATA, $8
 DATA	·libc_getcwd_trampoline_addr(SB)/8, $libc_getcwd_trampoline<>(SB)
 
+TEXT libc_getresuid_trampoline<>(SB),NOSPLIT,$0-0
+	CALL	libc_getresuid(SB)
+	RET
+GLOBL	·libc_getresuid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresuid_trampoline_addr(SB)/8, $libc_getresuid_trampoline<>(SB)
+
+TEXT libc_getresgid_trampoline<>(SB),NOSPLIT,$0-0
+	CALL	libc_getresgid(SB)
+	RET
+GLOBL	·libc_getresgid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresgid_trampoline_addr(SB)/8, $libc_getresgid_trampoline<>(SB)
+
 TEXT libc_ioctl_trampoline<>(SB),NOSPLIT,$0-0
 	CALL	libc_ioctl(SB)
 	RET
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.go
index 7d95a1978..32cbbbc52 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.go
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.go
@@ -519,6 +519,28 @@ var libc_getcwd_trampoline_addr uintptr
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
+func getresuid(ruid *_C_int, euid *_C_int, suid *_C_int) {
+	syscall_rawSyscall(libc_getresuid_trampoline_addr, uintptr(unsafe.Pointer(ruid)), uintptr(unsafe.Pointer(euid)), uintptr(unsafe.Pointer(suid)))
+	return
+}
+
+var libc_getresuid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresuid getresuid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
+func getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int) {
+	syscall_rawSyscall(libc_getresgid_trampoline_addr, uintptr(unsafe.Pointer(rgid)), uintptr(unsafe.Pointer(egid)), uintptr(unsafe.Pointer(sgid)))
+	return
+}
+
+var libc_getresgid_trampoline_addr uintptr
+
+//go:cgo_import_dynamic libc_getresgid getresgid "libc.so"
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
 func ioctl(fd int, req uint, arg uintptr) (err error) {
 	_, _, e1 := syscall_syscall(libc_ioctl_trampoline_addr, uintptr(fd), uintptr(req), uintptr(arg))
 	if e1 != 0 {
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.s
index 990be2457..477a7d5b2 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.s
+++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.s
@@ -158,6 +158,16 @@ TEXT libc_getcwd_trampoline<>(SB),NOSPLIT,$0-0
 GLOBL	·libc_getcwd_trampoline_addr(SB), RODATA, $8
 DATA	·libc_getcwd_trampoline_addr(SB)/8, $libc_getcwd_trampoline<>(SB)
 
+TEXT libc_getresuid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresuid(SB)
+GLOBL	·libc_getresuid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresuid_trampoline_addr(SB)/8, $libc_getresuid_trampoline<>(SB)
+
+TEXT libc_getresgid_trampoline<>(SB),NOSPLIT,$0-0
+	JMP	libc_getresgid(SB)
+GLOBL	·libc_getresgid_trampoline_addr(SB), RODATA, $8
+DATA	·libc_getresgid_trampoline_addr(SB)/8, $libc_getresgid_trampoline<>(SB)
+
 TEXT libc_ioctl_trampoline<>(SB),NOSPLIT,$0-0
 	JMP	libc_ioctl(SB)
 GLOBL	·libc_ioctl_trampoline_addr(SB), RODATA, $8
diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go
index ca84727cf..00c3b8c20 100644
--- a/vendor/golang.org/x/sys/unix/ztypes_linux.go
+++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go
@@ -2555,6 +2555,11 @@ const (
 	BPF_REG_8                                  = 0x8
 	BPF_REG_9                                  = 0x9
 	BPF_REG_10                                 = 0xa
+	BPF_CGROUP_ITER_ORDER_UNSPEC               = 0x0
+	BPF_CGROUP_ITER_SELF_ONLY                  = 0x1
+	BPF_CGROUP_ITER_DESCENDANTS_PRE            = 0x2
+	BPF_CGROUP_ITER_DESCENDANTS_POST           = 0x3
+	BPF_CGROUP_ITER_ANCESTORS_UP               = 0x4
 	BPF_MAP_CREATE                             = 0x0
 	BPF_MAP_LOOKUP_ELEM                        = 0x1
 	BPF_MAP_UPDATE_ELEM                        = 0x2
@@ -2566,6 +2571,7 @@ const (
 	BPF_PROG_ATTACH                            = 0x8
 	BPF_PROG_DETACH                            = 0x9
 	BPF_PROG_TEST_RUN                          = 0xa
+	BPF_PROG_RUN                               = 0xa
 	BPF_PROG_GET_NEXT_ID                       = 0xb
 	BPF_MAP_GET_NEXT_ID                        = 0xc
 	BPF_PROG_GET_FD_BY_ID                      = 0xd
@@ -2610,6 +2616,7 @@ const (
 	BPF_MAP_TYPE_CPUMAP                        = 0x10
 	BPF_MAP_TYPE_XSKMAP                        = 0x11
 	BPF_MAP_TYPE_SOCKHASH                      = 0x12
+	BPF_MAP_TYPE_CGROUP_STORAGE_DEPRECATED     = 0x13
 	BPF_MAP_TYPE_CGROUP_STORAGE                = 0x13
 	BPF_MAP_TYPE_REUSEPORT_SOCKARRAY           = 0x14
 	BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE         = 0x15
@@ -2620,6 +2627,10 @@ const (
 	BPF_MAP_TYPE_STRUCT_OPS                    = 0x1a
 	BPF_MAP_TYPE_RINGBUF                       = 0x1b
 	BPF_MAP_TYPE_INODE_STORAGE                 = 0x1c
+	BPF_MAP_TYPE_TASK_STORAGE                  = 0x1d
+	BPF_MAP_TYPE_BLOOM_FILTER                  = 0x1e
+	BPF_MAP_TYPE_USER_RINGBUF                  = 0x1f
+	BPF_MAP_TYPE_CGRP_STORAGE                  = 0x20
 	BPF_PROG_TYPE_UNSPEC                       = 0x0
 	BPF_PROG_TYPE_SOCKET_FILTER                = 0x1
 	BPF_PROG_TYPE_KPROBE                       = 0x2
@@ -2651,6 +2662,7 @@ const (
 	BPF_PROG_TYPE_EXT                          = 0x1c
 	BPF_PROG_TYPE_LSM                          = 0x1d
 	BPF_PROG_TYPE_SK_LOOKUP                    = 0x1e
+	BPF_PROG_TYPE_SYSCALL                      = 0x1f
 	BPF_CGROUP_INET_INGRESS                    = 0x0
 	BPF_CGROUP_INET_EGRESS                     = 0x1
 	BPF_CGROUP_INET_SOCK_CREATE                = 0x2
@@ -2689,6 +2701,12 @@ const (
 	BPF_XDP_CPUMAP                             = 0x23
 	BPF_SK_LOOKUP                              = 0x24
 	BPF_XDP                                    = 0x25
+	BPF_SK_SKB_VERDICT                         = 0x26
+	BPF_SK_REUSEPORT_SELECT                    = 0x27
+	BPF_SK_REUSEPORT_SELECT_OR_MIGRATE         = 0x28
+	BPF_PERF_EVENT                             = 0x29
+	BPF_TRACE_KPROBE_MULTI                     = 0x2a
+	BPF_LSM_CGROUP                             = 0x2b
 	BPF_LINK_TYPE_UNSPEC                       = 0x0
 	BPF_LINK_TYPE_RAW_TRACEPOINT               = 0x1
 	BPF_LINK_TYPE_TRACING                      = 0x2
@@ -2696,6 +2714,9 @@ const (
 	BPF_LINK_TYPE_ITER                         = 0x4
 	BPF_LINK_TYPE_NETNS                        = 0x5
 	BPF_LINK_TYPE_XDP                          = 0x6
+	BPF_LINK_TYPE_PERF_EVENT                   = 0x7
+	BPF_LINK_TYPE_KPROBE_MULTI                 = 0x8
+	BPF_LINK_TYPE_STRUCT_OPS                   = 0x9
 	BPF_ANY                                    = 0x0
 	BPF_NOEXIST                                = 0x1
 	BPF_EXIST                                  = 0x2
@@ -2733,6 +2754,7 @@ const (
 	BPF_F_ZERO_CSUM_TX                         = 0x2
 	BPF_F_DONT_FRAGMENT                        = 0x4
 	BPF_F_SEQ_NUMBER                           = 0x8
+	BPF_F_TUNINFO_FLAGS                        = 0x10
 	BPF_F_INDEX_MASK                           = 0xffffffff
 	BPF_F_CURRENT_CPU                          = 0xffffffff
 	BPF_F_CTXLEN_MASK                          = 0xfffff00000000
@@ -2747,6 +2769,7 @@ const (
 	BPF_F_ADJ_ROOM_ENCAP_L4_GRE                = 0x8
 	BPF_F_ADJ_ROOM_ENCAP_L4_UDP                = 0x10
 	BPF_F_ADJ_ROOM_NO_CSUM_RESET               = 0x20
+	BPF_F_ADJ_ROOM_ENCAP_L2_ETH                = 0x40
 	BPF_ADJ_ROOM_ENCAP_L2_MASK                 = 0xff
 	BPF_ADJ_ROOM_ENCAP_L2_SHIFT                = 0x38
 	BPF_F_SYSCTL_BASE_NAME                     = 0x1
@@ -2771,10 +2794,16 @@ const (
 	BPF_LWT_ENCAP_SEG6                         = 0x0
 	BPF_LWT_ENCAP_SEG6_INLINE                  = 0x1
 	BPF_LWT_ENCAP_IP                           = 0x2
+	BPF_F_BPRM_SECUREEXEC                      = 0x1
+	BPF_F_BROADCAST                            = 0x8
+	BPF_F_EXCLUDE_INGRESS                      = 0x10
+	BPF_SKB_TSTAMP_UNSPEC                      = 0x0
+	BPF_SKB_TSTAMP_DELIVERY_MONO               = 0x1
 	BPF_OK                                     = 0x0
 	BPF_DROP                                   = 0x2
 	BPF_REDIRECT                               = 0x7
 	BPF_LWT_REROUTE                            = 0x80
+	BPF_FLOW_DISSECTOR_CONTINUE                = 0x81
 	BPF_SOCK_OPS_RTO_CB_FLAG                   = 0x1
 	BPF_SOCK_OPS_RETRANS_CB_FLAG               = 0x2
 	BPF_SOCK_OPS_STATE_CB_FLAG                 = 0x4
@@ -2838,6 +2867,10 @@ const (
 	BPF_FIB_LKUP_RET_UNSUPP_LWT                = 0x6
 	BPF_FIB_LKUP_RET_NO_NEIGH                  = 0x7
 	BPF_FIB_LKUP_RET_FRAG_NEEDED               = 0x8
+	BPF_MTU_CHK_SEGS                           = 0x1
+	BPF_MTU_CHK_RET_SUCCESS                    = 0x0
+	BPF_MTU_CHK_RET_FRAG_NEEDED                = 0x1
+	BPF_MTU_CHK_RET_SEGS_TOOBIG                = 0x2
 	BPF_FD_TYPE_RAW_TRACEPOINT                 = 0x0
 	BPF_FD_TYPE_TRACEPOINT                     = 0x1
 	BPF_FD_TYPE_KPROBE                         = 0x2
@@ -2847,6 +2880,19 @@ const (
 	BPF_FLOW_DISSECTOR_F_PARSE_1ST_FRAG        = 0x1
 	BPF_FLOW_DISSECTOR_F_STOP_AT_FLOW_LABEL    = 0x2
 	BPF_FLOW_DISSECTOR_F_STOP_AT_ENCAP         = 0x4
+	BPF_CORE_FIELD_BYTE_OFFSET                 = 0x0
+	BPF_CORE_FIELD_BYTE_SIZE                   = 0x1
+	BPF_CORE_FIELD_EXISTS                      = 0x2
+	BPF_CORE_FIELD_SIGNED                      = 0x3
+	BPF_CORE_FIELD_LSHIFT_U64                  = 0x4
+	BPF_CORE_FIELD_RSHIFT_U64                  = 0x5
+	BPF_CORE_TYPE_ID_LOCAL                     = 0x6
+	BPF_CORE_TYPE_ID_TARGET                    = 0x7
+	BPF_CORE_TYPE_EXISTS                       = 0x8
+	BPF_CORE_TYPE_SIZE                         = 0x9
+	BPF_CORE_ENUMVAL_EXISTS                    = 0xa
+	BPF_CORE_ENUMVAL_VALUE                     = 0xb
+	BPF_CORE_TYPE_MATCHES                      = 0xc
 )
 
 const (
diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go
index 3723b2c22..964590075 100644
--- a/vendor/golang.org/x/sys/windows/syscall_windows.go
+++ b/vendor/golang.org/x/sys/windows/syscall_windows.go
@@ -405,7 +405,7 @@ func NewCallbackCDecl(fn interface{}) uintptr {
 //sys	VerQueryValue(block unsafe.Pointer, subBlock string, pointerToBufferPointer unsafe.Pointer, bufSize *uint32) (err error) = version.VerQueryValueW
 
 // Process Status API (PSAPI)
-//sys	EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
+//sys	enumProcesses(processIds *uint32, nSize uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
 //sys	EnumProcessModules(process Handle, module *Handle, cb uint32, cbNeeded *uint32) (err error) = psapi.EnumProcessModules
 //sys	EnumProcessModulesEx(process Handle, module *Handle, cb uint32, cbNeeded *uint32, filterFlag uint32) (err error) = psapi.EnumProcessModulesEx
 //sys	GetModuleInformation(process Handle, module Handle, modinfo *ModuleInfo, cb uint32) (err error) = psapi.GetModuleInformation
@@ -1354,6 +1354,17 @@ func SetsockoptIPv6Mreq(fd Handle, level, opt int, mreq *IPv6Mreq) (err error) {
 	return syscall.EWINDOWS
 }
 
+func EnumProcesses(processIds []uint32, bytesReturned *uint32) error {
+	// EnumProcesses syscall expects the size parameter to be in bytes, but the code generated with mksyscall uses
+	// the length of the processIds slice instead. Hence, this wrapper function is added to fix the discrepancy.
+	var p *uint32
+	if len(processIds) > 0 {
+		p = &processIds[0]
+	}
+	size := uint32(len(processIds) * 4)
+	return enumProcesses(p, size, bytesReturned)
+}
+
 func Getpid() (pid int) { return int(GetCurrentProcessId()) }
 
 func FindFirstFile(name *uint16, data *Win32finddata) (handle Handle, err error) {
diff --git a/vendor/golang.org/x/sys/windows/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/zsyscall_windows.go
index a81ea2c70..566dd3e31 100644
--- a/vendor/golang.org/x/sys/windows/zsyscall_windows.go
+++ b/vendor/golang.org/x/sys/windows/zsyscall_windows.go
@@ -3516,12 +3516,8 @@ func EnumProcessModulesEx(process Handle, module *Handle, cb uint32, cbNeeded *u
 	return
 }
 
-func EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) {
-	var _p0 *uint32
-	if len(processIds) > 0 {
-		_p0 = &processIds[0]
-	}
-	r1, _, e1 := syscall.Syscall(procEnumProcesses.Addr(), 3, uintptr(unsafe.Pointer(_p0)), uintptr(len(processIds)), uintptr(unsafe.Pointer(bytesReturned)))
+func enumProcesses(processIds *uint32, nSize uint32, bytesReturned *uint32) (err error) {
+	r1, _, e1 := syscall.Syscall(procEnumProcesses.Addr(), 3, uintptr(unsafe.Pointer(processIds)), uintptr(nSize), uintptr(unsafe.Pointer(bytesReturned)))
 	if r1 == 0 {
 		err = errnoErr(e1)
 	}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 3012b04de..f59049c58 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
 github.com/jesseduffield/go-git/v5/utils/merkletrie/index
 github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
 github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
-# github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e
+# github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce
 ## explicit; go 1.12
 github.com/jesseduffield/gocui
 # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
@@ -293,17 +293,17 @@ golang.org/x/exp/slices
 golang.org/x/net/context
 golang.org/x/net/internal/socks
 golang.org/x/net/proxy
-# golang.org/x/sys v0.8.0
+# golang.org/x/sys v0.9.0
 ## explicit; go 1.17
 golang.org/x/sys/cpu
 golang.org/x/sys/internal/unsafeheader
 golang.org/x/sys/plan9
 golang.org/x/sys/unix
 golang.org/x/sys/windows
-# golang.org/x/term v0.8.0
+# golang.org/x/term v0.9.0
 ## explicit; go 1.17
 golang.org/x/term
-# golang.org/x/text v0.9.0
+# golang.org/x/text v0.10.0
 ## explicit; go 1.17
 golang.org/x/text/encoding
 golang.org/x/text/encoding/internal/identifier

From c9a917b83034bb657cb4cb0a73f4dd2124aac02a Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sun, 2 Jul 2023 14:21:26 +1000
Subject: [PATCH 02/20] Print entire panic message

For some reason, the panic message was being truncated. So here we're printing it first, and then calling panic
---
 pkg/gui/gui_driver.go | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go
index a90578b65..824c9ed33 100644
--- a/pkg/gui/gui_driver.go
+++ b/pkg/gui/gui_driver.go
@@ -2,6 +2,7 @@ package gui
 
 import (
 	"fmt"
+	"os"
 	"strings"
 	"time"
 
@@ -70,7 +71,8 @@ func (self *GuiDriver) Fail(message string) {
 	self.gui.g.Close()
 	// need to give the gui time to close
 	time.Sleep(time.Millisecond * 100)
-	panic(fullMessage)
+	fmt.Fprintln(os.Stderr, fullMessage)
+	panic("Test failed")
 }
 
 // logs to the normal place that you log to i.e. viewable with `lazygit --logs`

From fd861826bc11754caf4ee4651dbadf9544792d1f Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 3 Jun 2023 19:17:18 +1000
Subject: [PATCH 03/20] Add integration tests for discarding files

---
 .../tests/file/discard_all_dir_changes.go     | 117 ++++++++++++++++++
 .../file/discard_unstaged_dir_changes.go      |  56 +++++++++
 .../file/discard_unstaged_file_changes.go     |  41 ++++++
 pkg/integration/tests/test_list.go            |   3 +
 4 files changed, 217 insertions(+)
 create mode 100644 pkg/integration/tests/file/discard_all_dir_changes.go
 create mode 100644 pkg/integration/tests/file/discard_unstaged_dir_changes.go
 create mode 100644 pkg/integration/tests/file/discard_unstaged_file_changes.go

diff --git a/pkg/integration/tests/file/discard_all_dir_changes.go b/pkg/integration/tests/file/discard_all_dir_changes.go
new file mode 100644
index 000000000..1032a180a
--- /dev/null
+++ b/pkg/integration/tests/file/discard_all_dir_changes.go
@@ -0,0 +1,117 @@
+package file
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var DiscardAllDirChanges = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Discarding all changes in a directory",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+	},
+	SetupRepo: func(shell *Shell) {
+		// typically we would use more bespoke shell methods here, but I struggled to find a way to do that,
+		// and this is copied over from a legacy integration test which did everything in a big shell script
+		// so I'm just copying it across.
+
+		shell.CreateDir("dir")
+
+		// common stuff
+		shell.RunShellCommand(`echo test > dir/both-deleted.txt`)
+		shell.RunShellCommand(`git checkout -b conflict && git add dir/both-deleted.txt`)
+		shell.RunShellCommand(`echo bothmodded > dir/both-modded.txt && git add dir/both-modded.txt`)
+		shell.RunShellCommand(`echo haha > dir/deleted-them.txt && git add dir/deleted-them.txt`)
+		shell.RunShellCommand(`echo haha2 > dir/deleted-us.txt && git add dir/deleted-us.txt`)
+		shell.RunShellCommand(`echo mod > dir/modded.txt && git add dir/modded.txt`)
+		shell.RunShellCommand(`echo mod > dir/modded-staged.txt && git add dir/modded-staged.txt`)
+		shell.RunShellCommand(`echo del > dir/deleted.txt && git add dir/deleted.txt`)
+		shell.RunShellCommand(`echo del > dir/deleted-staged.txt && git add dir/deleted-staged.txt`)
+		shell.RunShellCommand(`echo change-delete > dir/change-delete.txt && git add dir/change-delete.txt`)
+		shell.RunShellCommand(`echo delete-change > dir/delete-change.txt && git add dir/delete-change.txt`)
+		shell.RunShellCommand(`echo double-modded > dir/double-modded.txt && git add dir/double-modded.txt`)
+		shell.RunShellCommand(`echo "renamed\nhaha" > dir/renamed.txt && git add dir/renamed.txt`)
+		shell.RunShellCommand(`git commit -m one`)
+
+		// stuff on other branch
+		shell.RunShellCommand(`git branch conflict_second && git mv dir/both-deleted.txt dir/added-them-changed-us.txt`)
+		shell.RunShellCommand(`git commit -m "dir/both-deleted.txt renamed in dir/added-them-changed-us.txt"`)
+		shell.RunShellCommand(`echo blah > dir/both-added.txt && git add dir/both-added.txt`)
+		shell.RunShellCommand(`echo mod1 > dir/both-modded.txt && git add dir/both-modded.txt`)
+		shell.RunShellCommand(`rm dir/deleted-them.txt && git add dir/deleted-them.txt`)
+		shell.RunShellCommand(`echo modded > dir/deleted-us.txt && git add dir/deleted-us.txt`)
+		shell.RunShellCommand(`git commit -m "two"`)
+
+		// stuff on our branch
+		shell.RunShellCommand(`git checkout conflict_second`)
+		shell.RunShellCommand(`git mv dir/both-deleted.txt dir/changed-them-added-us.txt`)
+		shell.RunShellCommand(`git commit -m "both-deleted.txt renamed in dir/changed-them-added-us.txt"`)
+		shell.RunShellCommand(`echo mod2 > dir/both-modded.txt && git add dir/both-modded.txt`)
+		shell.RunShellCommand(`echo blah2 > dir/both-added.txt && git add dir/both-added.txt`)
+		shell.RunShellCommand(`echo modded > dir/deleted-them.txt && git add dir/deleted-them.txt`)
+		shell.RunShellCommand(`rm dir/deleted-us.txt && git add dir/deleted-us.txt`)
+		shell.RunShellCommand(`git commit -m "three"`)
+		shell.RunShellCommand(`git reset --hard conflict_second`)
+		shell.RunCommandExpectError([]string{"git", "merge", "conflict"})
+
+		shell.RunShellCommand(`echo "new" > dir/new.txt`)
+		shell.RunShellCommand(`echo "new staged" > dir/new-staged.txt && git add dir/new-staged.txt`)
+		shell.RunShellCommand(`echo mod2 > dir/modded.txt`)
+		shell.RunShellCommand(`echo mod2 > dir/modded-staged.txt && git add dir/modded-staged.txt`)
+		shell.RunShellCommand(`rm dir/deleted.txt`)
+		shell.RunShellCommand(`rm dir/deleted-staged.txt && git add dir/deleted-staged.txt`)
+		shell.RunShellCommand(`echo change-delete2 > dir/change-delete.txt && git add dir/change-delete.txt`)
+		shell.RunShellCommand(`rm dir/change-delete.txt`)
+		shell.RunShellCommand(`rm dir/delete-change.txt && git add dir/delete-change.txt`)
+		shell.RunShellCommand(`echo "changed" > dir/delete-change.txt`)
+		shell.RunShellCommand(`echo "change1" > dir/double-modded.txt && git add dir/double-modded.txt`)
+		shell.RunShellCommand(`echo "change2" > dir/double-modded.txt`)
+		shell.RunShellCommand(`echo before > dir/added-changed.txt && git add dir/added-changed.txt`)
+		shell.RunShellCommand(`echo after > dir/added-changed.txt`)
+		shell.RunShellCommand(`rm dir/renamed.txt && git add dir/renamed.txt`)
+		shell.RunShellCommand(`echo "renamed\nhaha" > dir/renamed2.txt && git add dir/renamed2.txt`)
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Files().
+			IsFocused().
+			Lines(
+				Contains("dir").IsSelected(),
+				Contains("UA").Contains("added-them-changed-us.txt"),
+				Contains("AA").Contains("both-added.txt"),
+				Contains("DD").Contains("both-deleted.txt"),
+				Contains("UU").Contains("both-modded.txt"),
+				Contains("AU").Contains("changed-them-added-us.txt"),
+				Contains("UD").Contains("deleted-them.txt"),
+				Contains("DU").Contains("deleted-us.txt"),
+			).
+			Press(keys.Universal.Remove).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("dir")).
+					Select(Contains("Discard all changes")).
+					Confirm()
+			}).
+			Tap(func() {
+				t.Common().ContinueOnConflictsResolved()
+			}).
+			Lines(
+				Contains("dir").IsSelected(),
+				Contains(" M").Contains("added-changed.txt"),
+				Contains(" D").Contains("change-delete.txt"),
+				Contains("??").Contains("delete-change.txt"),
+				Contains(" D").Contains("deleted.txt"),
+				Contains(" M").Contains("double-modded.txt"),
+				Contains(" M").Contains("modded.txt"),
+				Contains("??").Contains("new.txt"),
+			).
+			Press(keys.Universal.Remove).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("dir")).
+					Select(Contains("Discard all changes")).
+					Confirm()
+			}).
+			IsEmpty()
+	},
+})
diff --git a/pkg/integration/tests/file/discard_unstaged_dir_changes.go b/pkg/integration/tests/file/discard_unstaged_dir_changes.go
new file mode 100644
index 000000000..89e53cab5
--- /dev/null
+++ b/pkg/integration/tests/file/discard_unstaged_dir_changes.go
@@ -0,0 +1,56 @@
+package file
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var DiscardUnstagedDirChanges = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Discarding unstaged changes in a directory",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+	},
+	SetupRepo: func(shell *Shell) {
+		shell.CreateDir("dir")
+		shell.CreateFileAndAdd("dir/file-one", "original content\n")
+
+		shell.Commit("first commit")
+
+		shell.UpdateFileAndAdd("dir/file-one", "original content\nnew content\n")
+		shell.UpdateFile("dir/file-one", "original content\nnew content\neven newer content\n")
+
+		shell.CreateDir("dir/subdir")
+		shell.CreateFile("dir/subdir/unstaged-file-one", "unstaged file")
+		shell.CreateFile("dir/unstaged-file-two", "unstaged file")
+
+		shell.CreateFile("unstaged-file-three", "unstaged file")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Files().
+			IsFocused().
+			Lines(
+				Contains("dir").IsSelected(),
+				Contains("subdir"),
+				Contains("??").Contains("unstaged-file-one"),
+				Contains("MM").Contains("file-one"),
+				Contains("??").Contains("unstaged-file-two"),
+				Contains("??").Contains("unstaged-file-three"),
+			).
+			Press(keys.Universal.Remove).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("dir")).
+					Select(Contains("Discard unstaged changes")).
+					Confirm()
+			}).
+			Lines(
+				Contains("dir").IsSelected(),
+				Contains("M ").Contains("file-one"),
+				// this guy remains untouched because it wasn't inside the 'dir' directory
+				Contains("??").Contains("unstaged-file-three"),
+			)
+
+		t.FileSystem().FileContent("dir/file-one", Equals("original content\nnew content\n"))
+	},
+})
diff --git a/pkg/integration/tests/file/discard_unstaged_file_changes.go b/pkg/integration/tests/file/discard_unstaged_file_changes.go
new file mode 100644
index 000000000..caa5ef4ab
--- /dev/null
+++ b/pkg/integration/tests/file/discard_unstaged_file_changes.go
@@ -0,0 +1,41 @@
+package file
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var DiscardUnstagedFileChanges = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Discarding unstaged changes in a file",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+	},
+	SetupRepo: func(shell *Shell) {
+		shell.CreateFileAndAdd("file-one", "original content\n")
+
+		shell.Commit("first commit")
+
+		shell.UpdateFileAndAdd("file-one", "original content\nnew content\n")
+		shell.UpdateFile("file-one", "original content\nnew content\neven newer content\n")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Files().
+			IsFocused().
+			Lines(
+				Contains("MM").Contains("file-one").IsSelected(),
+			).
+			Press(keys.Universal.Remove).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("file-one")).
+					Select(Contains("Discard unstaged changes")).
+					Confirm()
+			}).
+			Lines(
+				Contains("M ").Contains("file-one").IsSelected(),
+			)
+
+		t.FileSystem().FileContent("file-one", Equals("original content\nnew content\n"))
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index c1549df83..9665f616e 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -87,8 +87,11 @@ var tests = []*components.IntegrationTest{
 	diff.DiffCommits,
 	diff.IgnoreWhitespace,
 	file.DirWithUntrackedFile,
+	file.DiscardAllDirChanges,
 	file.DiscardChanges,
 	file.DiscardStagedChanges,
+	file.DiscardUnstagedDirChanges,
+	file.DiscardUnstagedFileChanges,
 	file.Gitignore,
 	file.RememberCommitMessageAfterFail,
 	filter_by_path.CliArg,

From a9e2c8129f6e1cdfd58446d7ce5080fcabc2ea04 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 27 May 2023 14:14:43 +1000
Subject: [PATCH 04/20] Introduce filtered list view model

We're going to start supporting filtering of list views
---
 pkg/gui/command_log_panel.go                  |   4 -
 pkg/gui/context.go                            |  53 ++++-
 pkg/gui/context/branches_context.go           |  13 +-
 pkg/gui/context/filtered_list.go              |  56 +++++
 pkg/gui/context/filtered_list_view_model.go   |  26 +++
 ...basic_view_model.go => list_view_model.go} |  14 +-
 pkg/gui/context/local_commits_context.go      |  15 +-
 pkg/gui/context/menu_context.go               |  13 +-
 pkg/gui/context/patch_explorer_context.go     |  14 +-
 pkg/gui/context/reflog_commits_context.go     |  13 +-
 pkg/gui/context/remote_branches_context.go    |  15 +-
 pkg/gui/context/remotes_context.go            |  13 +-
 pkg/gui/context/search_trait.go               |  70 +++++++
 pkg/gui/context/stash_context.go              |  13 +-
 pkg/gui/context/sub_commits_context.go        |  18 +-
 pkg/gui/context/submodules_context.go         |  13 +-
 pkg/gui/context/suggestions_context.go        |   8 +-
 pkg/gui/context/tags_context.go               |  13 +-
 pkg/gui/controllers.go                        |  15 ++
 pkg/gui/controllers/filter_controller.go      |  48 +++++
 .../helpers/confirmation_helper.go            |   4 +-
 pkg/gui/controllers/helpers/helpers.go        |   2 +
 pkg/gui/controllers/helpers/search_helper.go  | 196 ++++++++++++++++++
 .../helpers/window_arrangement_helper.go      |  12 +-
 pkg/gui/controllers/list_controller.go        |  13 +-
 .../controllers/local_commits_controller.go   |   4 +-
 .../controllers/patch_explorer_controller.go  |   6 -
 pkg/gui/controllers/search_controller.go      |  48 +++++
 .../controllers/search_prompt_controller.go   |  53 +++++
 pkg/gui/editors.go                            |  11 +
 pkg/gui/filetree/file_tree_view_model.go      |   2 +
 pkg/gui/gui.go                                |  20 +-
 pkg/gui/gui_common.go                         |   4 -
 pkg/gui/keybindings.go                        |  12 --
 pkg/gui/layout.go                             |  14 --
 pkg/gui/menu_panel.go                         |   3 -
 pkg/gui/searching.go                          | 103 ---------
 pkg/gui/types/common.go                       |   5 +-
 pkg/gui/types/context.go                      |  21 +-
 pkg/gui/types/search_state.go                 |  31 +++
 pkg/gui/views.go                              |   2 +
 pkg/i18n/english.go                           |   6 +-
 pkg/utils/slice.go                            |  11 +
 43 files changed, 798 insertions(+), 232 deletions(-)
 create mode 100644 pkg/gui/context/filtered_list.go
 create mode 100644 pkg/gui/context/filtered_list_view_model.go
 rename pkg/gui/context/{basic_view_model.go => list_view_model.go} (56%)
 create mode 100644 pkg/gui/context/search_trait.go
 create mode 100644 pkg/gui/controllers/filter_controller.go
 create mode 100644 pkg/gui/controllers/helpers/search_helper.go
 create mode 100644 pkg/gui/controllers/search_controller.go
 create mode 100644 pkg/gui/controllers/search_prompt_controller.go
 delete mode 100644 pkg/gui/searching.go
 create mode 100644 pkg/gui/types/search_state.go

diff --git a/pkg/gui/command_log_panel.go b/pkg/gui/command_log_panel.go
index 0a5ccfae3..2faee3572 100644
--- a/pkg/gui/command_log_panel.go
+++ b/pkg/gui/command_log_panel.go
@@ -135,10 +135,6 @@ func (gui *Gui) getRandomTip() string {
 			"To escape a mode, for example cherry-picking, patch-building, diffing, or filtering mode, you can just spam the '%s' button. Unless of course you have `quitOnTopLevelReturn` enabled in your config",
 			formattedKey(config.Universal.Return),
 		),
-		fmt.Sprintf(
-			"To search for a string in your panel, press '%s'",
-			formattedKey(config.Universal.StartSearch),
-		),
 		fmt.Sprintf(
 			"You can page through the items of a panel using '%s' and '%s'",
 			formattedKey(config.Universal.PrevPage),
diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index b55713f27..26cec4c23 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -200,9 +200,21 @@ func (self *ContextMgr) RemoveContexts(contextsToRemove []types.Context) error {
 func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
 	view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
 
-	if view != nil && view.IsSearching() {
-		if err := self.gui.onSearchEscape(); err != nil {
-			return err
+	if opts.NewContextKey != context.SEARCH_CONTEXT_KEY {
+
+		if searchableContext, ok := c.(types.ISearchableContext); ok {
+			if view != nil && view.IsSearching() {
+				view.ClearSearch()
+				searchableContext.ClearSearchString()
+				self.gui.helpers.Search.Cancel()
+			}
+		}
+
+		if filterableContext, ok := c.(types.IFilterableContext); ok {
+			if filterableContext.GetFilter() != "" {
+				filterableContext.ClearFilter()
+				self.gui.helpers.Search.Cancel()
+			}
 		}
 	}
 
@@ -234,6 +246,17 @@ func (self *ContextMgr) ActivateContext(c types.Context, opts types.OnFocusOpts)
 		return err
 	}
 
+	if searchableContext, ok := c.(types.ISearchableContext); ok {
+		if searchableContext.GetSearchString() != "" {
+			self.gui.helpers.Search.DisplaySearchPrompt(searchableContext)
+		}
+	}
+	if filterableContext, ok := c.(types.IFilterableContext); ok {
+		if filterableContext.GetFilter() != "" {
+			self.gui.helpers.Search.DisplayFilterPrompt(filterableContext)
+		}
+	}
+
 	desiredTitle := c.Title()
 	if desiredTitle != "" {
 		v.Title = desiredTitle
@@ -326,6 +349,30 @@ func (self *ContextMgr) IsCurrent(c types.Context) bool {
 	return self.Current().GetKey() == c.GetKey()
 }
 
+func (self *ContextMgr) AllFilterable() []types.IFilterableContext {
+	var result []types.IFilterableContext
+
+	for _, context := range self.allContexts.Flatten() {
+		if ctx, ok := context.(types.IFilterableContext); ok {
+			result = append(result, ctx)
+		}
+	}
+
+	return result
+}
+
+func (self *ContextMgr) AllSearchable() []types.ISearchableContext {
+	var result []types.ISearchableContext
+
+	for _, context := range self.allContexts.Flatten() {
+		if ctx, ok := context.(types.ISearchableContext); ok {
+			result = append(result, ctx)
+		}
+	}
+
+	return result
+}
+
 // all list contexts
 func (self *ContextMgr) AllList() []types.IListContext {
 	var listContexts []types.IListContext
diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go
index c2463ad20..497b3a2c4 100644
--- a/pkg/gui/context/branches_context.go
+++ b/pkg/gui/context/branches_context.go
@@ -7,7 +7,7 @@ import (
 )
 
 type BranchesContext struct {
-	*BasicViewModel[*models.Branch]
+	*FilteredListViewModel[*models.Branch]
 	*ListContextTrait
 }
 
@@ -17,11 +17,16 @@ var (
 )
 
 func NewBranchesContext(c *ContextCommon) *BranchesContext {
-	viewModel := NewBasicViewModel(func() []*models.Branch { return c.Model().Branches })
+	viewModel := NewFilteredListViewModel(
+		func() []*models.Branch { return c.Model().Branches },
+		func(branch *models.Branch) []string {
+			return []string{branch.Name}
+		},
+	)
 
 	getDisplayStrings := func(startIdx int, length int) [][]string {
 		return presentation.GetBranchListDisplayStrings(
-			c.Model().Branches,
+			viewModel.GetItems(),
 			c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
 			c.Modes().Diffing.Ref,
 			c.Tr,
@@ -30,7 +35,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
 	}
 
 	self := &BranchesContext{
-		BasicViewModel: viewModel,
+		FilteredListViewModel: viewModel,
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
 				View:       c.Views().Branches,
diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go
new file mode 100644
index 000000000..1317924ad
--- /dev/null
+++ b/pkg/gui/context/filtered_list.go
@@ -0,0 +1,56 @@
+package context
+
+import (
+	"strings"
+
+	"github.com/jesseduffield/lazygit/pkg/utils"
+)
+
+type FilteredList[T any] struct {
+	filteredIndices []int // if nil, we are not filtering
+
+	getList         func() []T
+	getFilterFields func(T) []string
+	filter          string
+}
+
+func (self *FilteredList[T]) GetFilter() string {
+	return self.filter
+}
+
+func (self *FilteredList[T]) SetFilter(filter string) {
+	self.filter = filter
+
+	self.applyFilter()
+}
+
+func (self *FilteredList[T]) ClearFilter() {
+	self.SetFilter("")
+}
+
+func (self *FilteredList[T]) GetList() []T {
+	if self.filteredIndices == nil {
+		return self.getList()
+	}
+	return utils.ValuesAtIndices(self.getList(), self.filteredIndices)
+}
+
+func (self *FilteredList[T]) UnfilteredLen() int {
+	return len(self.getList())
+}
+
+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 strings.Contains(field, self.filter) {
+					self.filteredIndices = append(self.filteredIndices, i)
+					break
+				}
+			}
+		}
+	}
+}
diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go
new file mode 100644
index 000000000..01b020841
--- /dev/null
+++ b/pkg/gui/context/filtered_list_view_model.go
@@ -0,0 +1,26 @@
+package context
+
+type FilteredListViewModel[T any] struct {
+	*FilteredList[T]
+	*ListViewModel[T]
+}
+
+func NewFilteredListViewModel[T any](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] {
+	filteredList := &FilteredList[T]{
+		getList:         getList,
+		getFilterFields: getFilterFields,
+	}
+
+	self := &FilteredListViewModel[T]{
+		FilteredList: filteredList,
+	}
+
+	listViewModel := NewListViewModel(filteredList.GetList)
+
+	self.ListViewModel = listViewModel
+
+	return self
+}
+
+// used for type switch
+func (self *FilteredListViewModel[T]) IsFilterableContext() {}
diff --git a/pkg/gui/context/basic_view_model.go b/pkg/gui/context/list_view_model.go
similarity index 56%
rename from pkg/gui/context/basic_view_model.go
rename to pkg/gui/context/list_view_model.go
index a53be4d91..b70330d7d 100644
--- a/pkg/gui/context/basic_view_model.go
+++ b/pkg/gui/context/list_view_model.go
@@ -2,13 +2,13 @@ package context
 
 import "github.com/jesseduffield/lazygit/pkg/gui/context/traits"
 
-type BasicViewModel[T any] struct {
+type ListViewModel[T any] struct {
 	*traits.ListCursor
 	getModel func() []T
 }
 
-func NewBasicViewModel[T any](getModel func() []T) *BasicViewModel[T] {
-	self := &BasicViewModel[T]{
+func NewListViewModel[T any](getModel func() []T) *ListViewModel[T] {
+	self := &ListViewModel[T]{
 		getModel: getModel,
 	}
 
@@ -17,11 +17,11 @@ func NewBasicViewModel[T any](getModel func() []T) *BasicViewModel[T] {
 	return self
 }
 
-func (self *BasicViewModel[T]) Len() int {
+func (self *ListViewModel[T]) Len() int {
 	return len(self.getModel())
 }
 
-func (self *BasicViewModel[T]) GetSelected() T {
+func (self *ListViewModel[T]) GetSelected() T {
 	if self.Len() == 0 {
 		return Zero[T]()
 	}
@@ -29,6 +29,10 @@ func (self *BasicViewModel[T]) GetSelected() T {
 	return self.getModel()[self.GetSelectedLineIdx()]
 }
 
+func (self *ListViewModel[T]) GetItems() []T {
+	return self.getModel()
+}
+
 func Zero[T any]() T {
 	return *new(T)
 }
diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go
index 74363c52f..84204591c 100644
--- a/pkg/gui/context/local_commits_context.go
+++ b/pkg/gui/context/local_commits_context.go
@@ -13,6 +13,7 @@ import (
 type LocalCommitsContext struct {
 	*LocalCommitsViewModel
 	*ListContextTrait
+	*SearchTrait
 }
 
 var (
@@ -57,8 +58,9 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
 		)
 	}
 
-	return &LocalCommitsContext{
+	ctx := &LocalCommitsContext{
 		LocalCommitsViewModel: viewModel,
+		SearchTrait:           NewSearchTrait(c),
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
 				View:       c.Views().Commits,
@@ -73,6 +75,13 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
 			refreshViewportOnChange: true,
 		},
 	}
+
+	ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error {
+		ctx.GetList().SetSelectedLineIdx(selectedLineIdx)
+		return ctx.HandleFocus(types.OnFocusOpts{})
+	}))
+
+	return ctx
 }
 
 func (self *LocalCommitsContext) GetSelectedItemId() string {
@@ -85,7 +94,7 @@ func (self *LocalCommitsContext) GetSelectedItemId() string {
 }
 
 type LocalCommitsViewModel struct {
-	*BasicViewModel[*models.Commit]
+	*ListViewModel[*models.Commit]
 
 	// If this is true we limit the amount of commits we load, for the sake of keeping things fast.
 	// If the user attempts to scroll past the end of the list, we will load more commits.
@@ -97,7 +106,7 @@ type LocalCommitsViewModel struct {
 
 func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel {
 	self := &LocalCommitsViewModel{
-		BasicViewModel:    NewBasicViewModel(getModel),
+		ListViewModel:     NewListViewModel(getModel),
 		limitCommits:      true,
 		showWholeGitGraph: c.UserConfig.Git.Log.ShowWholeGraph,
 	}
diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go
index 6f84a8274..088640ea0 100644
--- a/pkg/gui/context/menu_context.go
+++ b/pkg/gui/context/menu_context.go
@@ -56,7 +56,7 @@ func (self *MenuContext) GetSelectedItemId() string {
 type MenuViewModel struct {
 	c         *ContextCommon
 	menuItems []*types.MenuItem
-	*BasicViewModel[*types.MenuItem]
+	*FilteredListViewModel[*types.MenuItem]
 }
 
 func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
@@ -65,7 +65,10 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
 		c:         c,
 	}
 
-	self.BasicViewModel = NewBasicViewModel(func() []*types.MenuItem { return self.menuItems })
+	self.FilteredListViewModel = NewFilteredListViewModel(
+		func() []*types.MenuItem { return self.menuItems },
+		func(item *types.MenuItem) []string { return item.LabelColumns },
+	)
 
 	return self
 }
@@ -76,11 +79,12 @@ func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem) {
 
 // TODO: move into presentation package
 func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]string {
-	showKeys := slices.Some(self.menuItems, func(item *types.MenuItem) bool {
+	menuItems := self.FilteredListViewModel.GetItems()
+	showKeys := slices.Some(menuItems, func(item *types.MenuItem) bool {
 		return item.Key != nil
 	})
 
-	return slices.Map(self.menuItems, func(item *types.MenuItem) []string {
+	return slices.Map(menuItems, func(item *types.MenuItem) []string {
 		displayStrings := item.LabelColumns
 
 		if !showKeys {
@@ -93,6 +97,7 @@ func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]str
 			self.c.UserConfig.Keybinding.Universal.Confirm,
 			self.c.UserConfig.Keybinding.Universal.Select,
 			self.c.UserConfig.Keybinding.Universal.Return,
+			self.c.UserConfig.Keybinding.Universal.StartSearch,
 		}
 		keyLabel := keybindings.LabelFromKey(item.Key)
 		keyStyle := style.FgCyan
diff --git a/pkg/gui/context/patch_explorer_context.go b/pkg/gui/context/patch_explorer_context.go
index 1c986ee1d..17ecae4ae 100644
--- a/pkg/gui/context/patch_explorer_context.go
+++ b/pkg/gui/context/patch_explorer_context.go
@@ -9,6 +9,7 @@ import (
 
 type PatchExplorerContext struct {
 	*SimpleContext
+	*SearchTrait
 
 	state                  *patch_exploring.State
 	viewTrait              *ViewTrait
@@ -28,7 +29,7 @@ func NewPatchExplorerContext(
 
 	c *ContextCommon,
 ) *PatchExplorerContext {
-	return &PatchExplorerContext{
+	ctx := &PatchExplorerContext{
 		state:                  nil,
 		viewTrait:              NewViewTrait(view),
 		c:                      c,
@@ -42,7 +43,18 @@ func NewPatchExplorerContext(
 			Focusable:        true,
 			HighlightOnFocus: true,
 		})),
+		SearchTrait: NewSearchTrait(c),
 	}
+
+	ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(
+		func(selectedLineIdx int) error {
+			ctx.GetMutex().Lock()
+			defer ctx.GetMutex().Unlock()
+			return ctx.NavigateTo(ctx.c.IsCurrentContext(ctx), selectedLineIdx)
+		}),
+	)
+
+	return ctx
 }
 
 func (self *PatchExplorerContext) IsPatchExplorerContext() {}
diff --git a/pkg/gui/context/reflog_commits_context.go b/pkg/gui/context/reflog_commits_context.go
index a92d605c8..421a7c8d5 100644
--- a/pkg/gui/context/reflog_commits_context.go
+++ b/pkg/gui/context/reflog_commits_context.go
@@ -9,7 +9,7 @@ import (
 )
 
 type ReflogCommitsContext struct {
-	*BasicViewModel[*models.Commit]
+	*FilteredListViewModel[*models.Commit]
 	*ListContextTrait
 }
 
@@ -19,11 +19,16 @@ var (
 )
 
 func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
-	viewModel := NewBasicViewModel(func() []*models.Commit { return c.Model().FilteredReflogCommits })
+	viewModel := NewFilteredListViewModel(
+		func() []*models.Commit { return c.Model().FilteredReflogCommits },
+		func(commit *models.Commit) []string {
+			return []string{commit.ShortSha(), commit.Name}
+		},
+	)
 
 	getDisplayStrings := func(startIdx int, length int) [][]string {
 		return presentation.GetReflogCommitListDisplayStrings(
-			c.Model().FilteredReflogCommits,
+			viewModel.GetItems(),
 			c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
 			c.Modes().CherryPicking.SelectedShaSet(),
 			c.Modes().Diffing.Ref,
@@ -35,7 +40,7 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
 	}
 
 	return &ReflogCommitsContext{
-		BasicViewModel: viewModel,
+		FilteredListViewModel: viewModel,
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
 				View:       c.Views().ReflogCommits,
diff --git a/pkg/gui/context/remote_branches_context.go b/pkg/gui/context/remote_branches_context.go
index a085c18cc..602a19a65 100644
--- a/pkg/gui/context/remote_branches_context.go
+++ b/pkg/gui/context/remote_branches_context.go
@@ -7,7 +7,7 @@ import (
 )
 
 type RemoteBranchesContext struct {
-	*BasicViewModel[*models.RemoteBranch]
+	*FilteredListViewModel[*models.RemoteBranch]
 	*ListContextTrait
 	*DynamicTitleBuilder
 }
@@ -20,15 +20,20 @@ var (
 func NewRemoteBranchesContext(
 	c *ContextCommon,
 ) *RemoteBranchesContext {
-	viewModel := NewBasicViewModel(func() []*models.RemoteBranch { return c.Model().RemoteBranches })
+	viewModel := NewFilteredListViewModel(
+		func() []*models.RemoteBranch { return c.Model().RemoteBranches },
+		func(remoteBranch *models.RemoteBranch) []string {
+			return []string{remoteBranch.Name}
+		},
+	)
 
 	getDisplayStrings := func(startIdx int, length int) [][]string {
-		return presentation.GetRemoteBranchListDisplayStrings(c.Model().RemoteBranches, c.Modes().Diffing.Ref)
+		return presentation.GetRemoteBranchListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
 	}
 
 	return &RemoteBranchesContext{
-		BasicViewModel:      viewModel,
-		DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
+		FilteredListViewModel: viewModel,
+		DynamicTitleBuilder:   NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
 				View:       c.Views().RemoteBranches,
diff --git a/pkg/gui/context/remotes_context.go b/pkg/gui/context/remotes_context.go
index d1082ab52..f5e2a97ab 100644
--- a/pkg/gui/context/remotes_context.go
+++ b/pkg/gui/context/remotes_context.go
@@ -7,7 +7,7 @@ import (
 )
 
 type RemotesContext struct {
-	*BasicViewModel[*models.Remote]
+	*FilteredListViewModel[*models.Remote]
 	*ListContextTrait
 }
 
@@ -17,14 +17,19 @@ var (
 )
 
 func NewRemotesContext(c *ContextCommon) *RemotesContext {
-	viewModel := NewBasicViewModel(func() []*models.Remote { return c.Model().Remotes })
+	viewModel := NewFilteredListViewModel(
+		func() []*models.Remote { return c.Model().Remotes },
+		func(remote *models.Remote) []string {
+			return []string{remote.Name}
+		},
+	)
 
 	getDisplayStrings := func(startIdx int, length int) [][]string {
-		return presentation.GetRemoteListDisplayStrings(c.Model().Remotes, c.Modes().Diffing.Ref)
+		return presentation.GetRemoteListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
 	}
 
 	return &RemotesContext{
-		BasicViewModel: viewModel,
+		FilteredListViewModel: viewModel,
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
 				View:       c.Views().Remotes,
diff --git a/pkg/gui/context/search_trait.go b/pkg/gui/context/search_trait.go
new file mode 100644
index 000000000..5e745f995
--- /dev/null
+++ b/pkg/gui/context/search_trait.go
@@ -0,0 +1,70 @@
+package context
+
+import (
+	"fmt"
+
+	"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
+	"github.com/jesseduffield/lazygit/pkg/theme"
+)
+
+type SearchTrait struct {
+	c *ContextCommon
+
+	searchString string
+}
+
+func NewSearchTrait(c *ContextCommon) *SearchTrait {
+	return &SearchTrait{c: c}
+}
+
+func (self *SearchTrait) GetSearchString() string {
+	return self.searchString
+}
+
+func (self *SearchTrait) SetSearchString(searchString string) {
+	self.searchString = searchString
+}
+
+func (self *SearchTrait) ClearSearchString() {
+	self.SetSearchString("")
+}
+
+// used for type switch
+func (self *SearchTrait) IsSearchableContext() {}
+
+func (self *SearchTrait) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
+	keybindingConfig := self.c.UserConfig.Keybinding
+
+	return func(y int, index int, total int) error {
+		if total == 0 {
+			self.c.SetViewContent(
+				self.c.Views().Search,
+				fmt.Sprintf(
+					self.c.Tr.NoMatchesFor,
+					self.searchString,
+					theme.OptionsFgColor.Sprintf(self.c.Tr.ExitSearchMode, keybindings.Label(keybindingConfig.Universal.Return)),
+				),
+			)
+			return nil
+		}
+		self.c.SetViewContent(
+			self.c.Views().Search,
+			fmt.Sprintf(
+				self.c.Tr.MatchesFor,
+				self.searchString,
+				index+1,
+				total,
+				theme.OptionsFgColor.Sprintf(
+					self.c.Tr.SearchKeybindings,
+					keybindings.Label(keybindingConfig.Universal.NextMatch),
+					keybindings.Label(keybindingConfig.Universal.PrevMatch),
+					keybindings.Label(keybindingConfig.Universal.Return),
+				),
+			),
+		)
+		if err := innerFunc(y); err != nil {
+			return err
+		}
+		return nil
+	}
+}
diff --git a/pkg/gui/context/stash_context.go b/pkg/gui/context/stash_context.go
index 386292c00..7bd4740f8 100644
--- a/pkg/gui/context/stash_context.go
+++ b/pkg/gui/context/stash_context.go
@@ -7,7 +7,7 @@ import (
 )
 
 type StashContext struct {
-	*BasicViewModel[*models.StashEntry]
+	*FilteredListViewModel[*models.StashEntry]
 	*ListContextTrait
 }
 
@@ -19,14 +19,19 @@ var (
 func NewStashContext(
 	c *ContextCommon,
 ) *StashContext {
-	viewModel := NewBasicViewModel(func() []*models.StashEntry { return c.Model().StashEntries })
+	viewModel := NewFilteredListViewModel(
+		func() []*models.StashEntry { return c.Model().StashEntries },
+		func(stashEntry *models.StashEntry) []string {
+			return []string{stashEntry.Name}
+		},
+	)
 
 	getDisplayStrings := func(startIdx int, length int) [][]string {
-		return presentation.GetStashEntryListDisplayStrings(c.Model().StashEntries, c.Modes().Diffing.Ref)
+		return presentation.GetStashEntryListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
 	}
 
 	return &StashContext{
-		BasicViewModel: viewModel,
+		FilteredListViewModel: viewModel,
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
 				View:       c.Views().Stash,
diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go
index e2c89aa14..0cf884589 100644
--- a/pkg/gui/context/sub_commits_context.go
+++ b/pkg/gui/context/sub_commits_context.go
@@ -12,9 +12,12 @@ import (
 )
 
 type SubCommitsContext struct {
+	c *ContextCommon
+
 	*SubCommitsViewModel
 	*ListContextTrait
 	*DynamicTitleBuilder
+	*SearchTrait
 }
 
 var (
@@ -26,7 +29,7 @@ func NewSubCommitsContext(
 	c *ContextCommon,
 ) *SubCommitsContext {
 	viewModel := &SubCommitsViewModel{
-		BasicViewModel: NewBasicViewModel(
+		ListViewModel: NewListViewModel(
 			func() []*models.Commit { return c.Model().SubCommits },
 		),
 		ref:          nil,
@@ -60,8 +63,10 @@ func NewSubCommitsContext(
 		)
 	}
 
-	return &SubCommitsContext{
+	ctx := &SubCommitsContext{
+		c:                   c,
 		SubCommitsViewModel: viewModel,
+		SearchTrait:         NewSearchTrait(c),
 		DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.SubCommitsDynamicTitle),
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
@@ -78,12 +83,19 @@ func NewSubCommitsContext(
 			refreshViewportOnChange: true,
 		},
 	}
+
+	ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error {
+		ctx.GetList().SetSelectedLineIdx(selectedLineIdx)
+		return ctx.HandleFocus(types.OnFocusOpts{})
+	}))
+
+	return ctx
 }
 
 type SubCommitsViewModel struct {
 	// name of the ref that the sub-commits are shown for
 	ref types.Ref
-	*BasicViewModel[*models.Commit]
+	*ListViewModel[*models.Commit]
 
 	limitCommits bool
 }
diff --git a/pkg/gui/context/submodules_context.go b/pkg/gui/context/submodules_context.go
index 675e01cd1..e97fa4f5c 100644
--- a/pkg/gui/context/submodules_context.go
+++ b/pkg/gui/context/submodules_context.go
@@ -7,21 +7,26 @@ import (
 )
 
 type SubmodulesContext struct {
-	*BasicViewModel[*models.SubmoduleConfig]
+	*FilteredListViewModel[*models.SubmoduleConfig]
 	*ListContextTrait
 }
 
 var _ types.IListContext = (*SubmodulesContext)(nil)
 
 func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext {
-	viewModel := NewBasicViewModel(func() []*models.SubmoduleConfig { return c.Model().Submodules })
+	viewModel := NewFilteredListViewModel(
+		func() []*models.SubmoduleConfig { return c.Model().Submodules },
+		func(submodule *models.SubmoduleConfig) []string {
+			return []string{submodule.Name}
+		},
+	)
 
 	getDisplayStrings := func(startIdx int, length int) [][]string {
-		return presentation.GetSubmoduleListDisplayStrings(c.Model().Submodules)
+		return presentation.GetSubmoduleListDisplayStrings(viewModel.GetItems())
 	}
 
 	return &SubmodulesContext{
-		BasicViewModel: viewModel,
+		FilteredListViewModel: viewModel,
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
 				View:       c.Views().Submodules,
diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go
index 022e96daf..58b2205a4 100644
--- a/pkg/gui/context/suggestions_context.go
+++ b/pkg/gui/context/suggestions_context.go
@@ -7,7 +7,7 @@ import (
 )
 
 type SuggestionsContext struct {
-	*BasicViewModel[*types.Suggestion]
+	*ListViewModel[*types.Suggestion]
 	*ListContextTrait
 
 	State *SuggestionsContextState
@@ -40,11 +40,11 @@ func NewSuggestionsContext(
 		return presentation.GetSuggestionListDisplayStrings(state.Suggestions)
 	}
 
-	viewModel := NewBasicViewModel(getModel)
+	viewModel := NewListViewModel(getModel)
 
 	return &SuggestionsContext{
-		State:          state,
-		BasicViewModel: viewModel,
+		State:         state,
+		ListViewModel: viewModel,
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
 				View:                  c.Views().Suggestions,
diff --git a/pkg/gui/context/tags_context.go b/pkg/gui/context/tags_context.go
index e49cdad9b..71ea36981 100644
--- a/pkg/gui/context/tags_context.go
+++ b/pkg/gui/context/tags_context.go
@@ -7,7 +7,7 @@ import (
 )
 
 type TagsContext struct {
-	*BasicViewModel[*models.Tag]
+	*FilteredListViewModel[*models.Tag]
 	*ListContextTrait
 }
 
@@ -19,14 +19,19 @@ var (
 func NewTagsContext(
 	c *ContextCommon,
 ) *TagsContext {
-	viewModel := NewBasicViewModel(func() []*models.Tag { return c.Model().Tags })
+	viewModel := NewFilteredListViewModel(
+		func() []*models.Tag { return c.Model().Tags },
+		func(tag *models.Tag) []string {
+			return []string{tag.Name, tag.Message}
+		},
+	)
 
 	getDisplayStrings := func(startIdx int, length int) [][]string {
-		return presentation.GetTagListDisplayStrings(c.Model().Tags, c.Modes().Diffing.Ref)
+		return presentation.GetTagListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
 	}
 
 	return &TagsContext{
-		BasicViewModel: viewModel,
+		FilteredListViewModel: viewModel,
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
 				View:       c.Views().Tags,
diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go
index 78943e798..a17592513 100644
--- a/pkg/gui/controllers.go
+++ b/pkg/gui/controllers.go
@@ -99,6 +99,7 @@ func (gui *Gui) resetHelpersAndControllers() {
 			modeHelper,
 			appStatusHelper,
 		),
+		Search: helpers.NewSearchHelper(helperCommon),
 	}
 
 	gui.CustomCommandsClient = custom_commands.NewClient(
@@ -162,6 +163,16 @@ func (gui *Gui) resetHelpersAndControllers() {
 
 	sideWindowControllerFactory := controllers.NewSideWindowControllerFactory(common)
 
+	filterControllerFactory := controllers.NewFilterControllerFactory(common)
+	for _, context := range gui.c.Context().AllFilterable() {
+		controllers.AttachControllers(context, filterControllerFactory.Create(context))
+	}
+
+	searchControllerFactory := controllers.NewSearchControllerFactory(common)
+	for _, context := range gui.c.Context().AllSearchable() {
+		controllers.AttachControllers(context, searchControllerFactory.Create(context))
+	}
+
 	// allow for navigating between side window contexts
 	for _, context := range []types.Context{
 		gui.State.Contexts.Status,
@@ -323,6 +334,10 @@ func (gui *Gui) resetHelpersAndControllers() {
 		suggestionsController,
 	)
 
+	controllers.AttachControllers(gui.State.Contexts.Search,
+		controllers.NewSearchPromptController(common),
+	)
+
 	controllers.AttachControllers(gui.State.Contexts.Global,
 		syncController,
 		undoController,
diff --git a/pkg/gui/controllers/filter_controller.go b/pkg/gui/controllers/filter_controller.go
new file mode 100644
index 000000000..8b049b26c
--- /dev/null
+++ b/pkg/gui/controllers/filter_controller.go
@@ -0,0 +1,48 @@
+package controllers
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/gui/types"
+)
+
+type FilterControllerFactory struct {
+	c *ControllerCommon
+}
+
+func NewFilterControllerFactory(c *ControllerCommon) *FilterControllerFactory {
+	return &FilterControllerFactory{
+		c: c,
+	}
+}
+
+func (self *FilterControllerFactory) Create(context types.IFilterableContext) *FilterController {
+	return &FilterController{
+		baseController: baseController{},
+		c:              self.c,
+		context:        context,
+	}
+}
+
+type FilterController struct {
+	baseController
+	c *ControllerCommon
+
+	context types.IFilterableContext
+}
+
+func (self *FilterController) Context() types.Context {
+	return self.context
+}
+
+func (self *FilterController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
+	return []*types.Binding{
+		{
+			Key:         opts.GetKey(opts.Config.Universal.StartSearch),
+			Handler:     self.OpenFilterPrompt,
+			Description: self.c.Tr.StartFilter,
+		},
+	}
+}
+
+func (self *FilterController) OpenFilterPrompt() error {
+	return self.c.Helpers().Search.OpenFilterPrompt(self.context)
+}
diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go
index 7968933fc..c721310b2 100644
--- a/pkg/gui/controllers/helpers/confirmation_helper.go
+++ b/pkg/gui/controllers/helpers/confirmation_helper.go
@@ -292,7 +292,9 @@ func (self *ConfirmationHelper) ResizePopupPanel(v *gocui.View, content string)
 }
 
 func (self *ConfirmationHelper) resizeMenu() {
-	itemCount := self.c.Contexts().Menu.GetList().Len()
+	// we want the unfiltered length here so that if we're filtering we don't
+	// resize the window
+	itemCount := self.c.Contexts().Menu.UnfilteredLen()
 	offset := 3
 	panelWidth := self.getPopupPanelWidth()
 	x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go
index faf342f0a..846638249 100644
--- a/pkg/gui/controllers/helpers/helpers.go
+++ b/pkg/gui/controllers/helpers/helpers.go
@@ -46,6 +46,7 @@ type Helpers struct {
 	Mode              *ModeHelper
 	AppStatus         *AppStatusHelper
 	WindowArrangement *WindowArrangementHelper
+	Search            *SearchHelper
 }
 
 func NewStubHelpers() *Helpers {
@@ -78,5 +79,6 @@ func NewStubHelpers() *Helpers {
 		Mode:              &ModeHelper{},
 		AppStatus:         &AppStatusHelper{},
 		WindowArrangement: &WindowArrangementHelper{},
+		Search:            &SearchHelper{},
 	}
 }
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
new file mode 100644
index 000000000..9c0e09db4
--- /dev/null
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -0,0 +1,196 @@
+package helpers
+
+import (
+	"github.com/jesseduffield/gocui"
+	"github.com/jesseduffield/lazygit/pkg/gui/types"
+)
+
+// NOTE: this helper supports both filtering and searching. Filtering is when
+// the contents of the list are filtered, whereas searching does not actually
+// change the contents of the list but instead just highlights the search.
+// The general term we use to capture both searching and filtering is...
+// 'searching', which is unfortunate but I can't think of a better name.
+
+type SearchHelper struct {
+	c *HelperCommon
+}
+
+func NewSearchHelper(
+	c *HelperCommon,
+) *SearchHelper {
+	return &SearchHelper{
+		c: c,
+	}
+}
+
+func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) error {
+	state := self.searchState()
+
+	state.Context = context
+
+	self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix)
+	promptView := self.promptView()
+	promptView.ClearTextArea()
+	promptView.TextArea.TypeString(context.GetFilter())
+	promptView.RenderTextArea()
+
+	if err := self.c.PushContext(self.c.Contexts().Search); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (self *SearchHelper) OpenSearchPrompt(context types.Context) error {
+	state := self.searchState()
+
+	state.Context = context
+
+	self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
+	promptView := self.promptView()
+	// TODO: should we show the currently searched thing here? Perhaps we can store that on the context
+	promptView.ClearTextArea()
+	promptView.RenderTextArea()
+
+	if err := self.c.PushContext(self.c.Contexts().Search); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (self *SearchHelper) DisplayFilterPrompt(context types.IFilterableContext) {
+	state := self.searchState()
+
+	state.Context = context
+	searchString := context.GetFilter()
+
+	self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix)
+	promptView := self.promptView()
+	promptView.ClearTextArea()
+	promptView.TextArea.TypeString(searchString)
+	promptView.RenderTextArea()
+}
+
+func (self *SearchHelper) DisplaySearchPrompt(context types.ISearchableContext) {
+	state := self.searchState()
+
+	state.Context = context
+	searchString := context.GetSearchString()
+
+	self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
+	promptView := self.promptView()
+	promptView.ClearTextArea()
+	promptView.TextArea.TypeString(searchString)
+	promptView.RenderTextArea()
+}
+
+func (self *SearchHelper) searchState() *types.SearchState {
+	return self.c.State().GetRepoState().GetSearchState()
+}
+
+func (self *SearchHelper) searchPrefixView() *gocui.View {
+	return self.c.Views().SearchPrefix
+}
+
+func (self *SearchHelper) promptView() *gocui.View {
+	return self.c.Contexts().Search.GetView()
+}
+
+func (self *SearchHelper) promptContent() string {
+	return self.c.Contexts().Search.GetView().TextArea.GetContent()
+}
+
+func (self *SearchHelper) Confirm() error {
+	state := self.searchState()
+	if self.promptContent() == "" {
+		return self.CancelPrompt()
+	}
+
+	switch state.SearchType() {
+	case types.SearchTypeFilter:
+		return self.ConfirmFilter()
+	case types.SearchTypeSearch:
+		return self.ConfirmSearch()
+	case types.SearchTypeNone:
+		return self.c.PopContext()
+	}
+
+	return nil
+}
+
+func (self *SearchHelper) ConfirmFilter() error {
+	// We also do this on each keypress but we do it here again just in case
+	state := self.searchState()
+
+	context, ok := state.Context.(types.IFilterableContext)
+	if !ok {
+		self.c.Log.Warnf("Context %s is not filterable", state.Context.GetKey())
+		return nil
+	}
+
+	context.SetFilter(self.promptContent())
+	_ = self.c.PostRefreshUpdate(state.Context)
+
+	return self.c.PopContext()
+}
+
+func (self *SearchHelper) ConfirmSearch() error {
+	state := self.searchState()
+
+	if err := self.c.PopContext(); err != nil {
+		return err
+	}
+
+	context, ok := state.Context.(types.ISearchableContext)
+	if !ok {
+		self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey())
+		return nil
+	}
+
+	searchString := self.promptContent()
+	context.SetSearchString(searchString)
+
+	view := context.GetView()
+
+	if err := view.Search(searchString); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (self *SearchHelper) CancelPrompt() error {
+	self.Cancel()
+
+	return self.c.PopContext()
+}
+
+func (self *SearchHelper) Cancel() {
+	state := self.searchState()
+
+	switch context := state.Context.(type) {
+	case types.IFilterableContext:
+		context.SetFilter("")
+		_ = self.c.PostRefreshUpdate(context)
+	case types.ISearchableContext:
+		context.GetView().ClearSearch()
+	default:
+		// do nothing
+	}
+
+	state.Context = nil
+}
+
+func (self *SearchHelper) OnPromptContentChanged(searchString string) {
+	state := self.searchState()
+	switch context := state.Context.(type) {
+	case types.IFilterableContext:
+		context.SetFilter(searchString)
+		_ = self.c.PostRefreshUpdate(context)
+	case types.ISearchableContext:
+		// do nothing
+	default:
+		// do nothing (shouldn't land here)
+	}
+}
diff --git a/pkg/gui/controllers/helpers/window_arrangement_helper.go b/pkg/gui/controllers/helpers/window_arrangement_helper.go
index 20459993f..b45586764 100644
--- a/pkg/gui/controllers/helpers/window_arrangement_helper.go
+++ b/pkg/gui/controllers/helpers/window_arrangement_helper.go
@@ -55,7 +55,7 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string,
 	self.c.Modes().Filtering.Active()
 
 	showInfoSection := self.c.UserConfig.Gui.ShowBottomLine ||
-		self.c.State().GetRepoState().IsSearching() ||
+		self.c.State().GetRepoState().InSearchPrompt() ||
 		self.modeHelper.IsAnyModeActive() ||
 		self.appStatusHelper.HasStatus()
 	infoSectionSize := 0
@@ -174,11 +174,17 @@ func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) {
 }
 
 func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
-	if self.c.State().GetRepoState().IsSearching() {
+	if self.c.State().GetRepoState().InSearchPrompt() {
+		var prefix string
+		if self.c.State().GetRepoState().GetSearchState().SearchType() == types.SearchTypeSearch {
+			prefix = self.c.Tr.SearchPrefix
+		} else {
+			prefix = self.c.Tr.FilterPrefix
+		}
 		return []*boxlayout.Box{
 			{
 				Window: "searchPrefix",
-				Size:   runewidth.StringWidth(self.c.Tr.SearchPrefix),
+				Size:   runewidth.StringWidth(prefix),
 			},
 			{
 				Window: "search",
diff --git a/pkg/gui/controllers/list_controller.go b/pkg/gui/controllers/list_controller.go
index 2f995ebc8..fb6d8736a 100644
--- a/pkg/gui/controllers/list_controller.go
+++ b/pkg/gui/controllers/list_controller.go
@@ -150,18 +150,7 @@ func (self *ListController) GetKeybindings(opts types.KeybindingsOpts) []*types.
 		{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoTop), Handler: self.HandleGotoTop, Description: self.c.Tr.GotoTop},
 		{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.ScrollLeft), Handler: self.HandleScrollLeft},
 		{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.ScrollRight), Handler: self.HandleScrollRight},
-		{
-			Key:         opts.GetKey(opts.Config.Universal.StartSearch),
-			Handler:     func() error { self.c.OpenSearch(); return nil },
-			Description: self.c.Tr.StartSearch,
-			Tag:         "navigation",
-		},
-		{
-			Key:         opts.GetKey(opts.Config.Universal.GotoBottom),
-			Description: self.c.Tr.GotoBottom,
-			Handler:     self.HandleGotoBottom,
-			Tag:         "navigation",
-		},
+		{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoBottom), Handler: self.HandleGotoBottom, Description: self.c.Tr.GotoBottom},
 	}
 }
 
diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go
index 0ba80c768..49abe02ff 100644
--- a/pkg/gui/controllers/local_commits_controller.go
+++ b/pkg/gui/controllers/local_commits_controller.go
@@ -693,9 +693,7 @@ func (self *LocalCommitsController) openSearch() error {
 		}
 	}
 
-	self.c.OpenSearch()
-
-	return nil
+	return self.c.Helpers().Search.OpenSearchPrompt(self.context())
 }
 
 func (self *LocalCommitsController) gotoBottom() error {
diff --git a/pkg/gui/controllers/patch_explorer_controller.go b/pkg/gui/controllers/patch_explorer_controller.go
index 6de8fb8b9..dd19d08db 100644
--- a/pkg/gui/controllers/patch_explorer_controller.go
+++ b/pkg/gui/controllers/patch_explorer_controller.go
@@ -123,12 +123,6 @@ func (self *PatchExplorerController) GetKeybindings(opts types.KeybindingsOpts)
 			Key:     opts.GetKey(opts.Config.Universal.ScrollRight),
 			Handler: self.withRenderAndFocus(self.HandleScrollRight),
 		},
-		{
-			Tag:         "navigation",
-			Key:         opts.GetKey(opts.Config.Universal.StartSearch),
-			Handler:     func() error { self.c.OpenSearch(); return nil },
-			Description: self.c.Tr.StartSearch,
-		},
 		{
 			Key:         opts.GetKey(opts.Config.Universal.CopyToClipboard),
 			Handler:     self.withLock(self.CopySelectedToClipboard),
diff --git a/pkg/gui/controllers/search_controller.go b/pkg/gui/controllers/search_controller.go
new file mode 100644
index 000000000..395784d10
--- /dev/null
+++ b/pkg/gui/controllers/search_controller.go
@@ -0,0 +1,48 @@
+package controllers
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/gui/types"
+)
+
+type SearchControllerFactory struct {
+	c *ControllerCommon
+}
+
+func NewSearchControllerFactory(c *ControllerCommon) *SearchControllerFactory {
+	return &SearchControllerFactory{
+		c: c,
+	}
+}
+
+func (self *SearchControllerFactory) Create(context types.ISearchableContext) *SearchController {
+	return &SearchController{
+		baseController: baseController{},
+		c:              self.c,
+		context:        context,
+	}
+}
+
+type SearchController struct {
+	baseController
+	c *ControllerCommon
+
+	context types.ISearchableContext
+}
+
+func (self *SearchController) Context() types.Context {
+	return self.context
+}
+
+func (self *SearchController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
+	return []*types.Binding{
+		{
+			Key:         opts.GetKey(opts.Config.Universal.StartSearch),
+			Handler:     self.OpenSearchPrompt,
+			Description: self.c.Tr.StartSearch,
+		},
+	}
+}
+
+func (self *SearchController) OpenSearchPrompt() error {
+	return self.c.Helpers().Search.OpenSearchPrompt(self.context)
+}
diff --git a/pkg/gui/controllers/search_prompt_controller.go b/pkg/gui/controllers/search_prompt_controller.go
new file mode 100644
index 000000000..2326ed1c1
--- /dev/null
+++ b/pkg/gui/controllers/search_prompt_controller.go
@@ -0,0 +1,53 @@
+package controllers
+
+import (
+	"github.com/jesseduffield/gocui"
+	"github.com/jesseduffield/lazygit/pkg/gui/types"
+)
+
+type SearchPromptController struct {
+	baseController
+	c *ControllerCommon
+}
+
+var _ types.IController = &SearchPromptController{}
+
+func NewSearchPromptController(
+	common *ControllerCommon,
+) *SearchPromptController {
+	return &SearchPromptController{
+		baseController: baseController{},
+		c:              common,
+	}
+}
+
+func (self *SearchPromptController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
+	return []*types.Binding{
+		{
+			Key:      opts.GetKey(opts.Config.Universal.Confirm),
+			Modifier: gocui.ModNone,
+			Handler:  self.confirm,
+		},
+		{
+			Key:      opts.GetKey(opts.Config.Universal.Return),
+			Modifier: gocui.ModNone,
+			Handler:  self.cancel,
+		},
+	}
+}
+
+func (self *SearchPromptController) Context() types.Context {
+	return self.context()
+}
+
+func (self *SearchPromptController) context() types.Context {
+	return self.c.Contexts().Search
+}
+
+func (self *SearchPromptController) confirm() error {
+	return self.c.Helpers().Search.Confirm()
+}
+
+func (self *SearchPromptController) cancel() error {
+	return self.c.Helpers().Search.CancelPrompt()
+}
diff --git a/pkg/gui/editors.go b/pkg/gui/editors.go
index 1fbba2aad..b095630ec 100644
--- a/pkg/gui/editors.go
+++ b/pkg/gui/editors.go
@@ -89,3 +89,14 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Mo
 
 	return matched
 }
+
+func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
+	matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false)
+	v.RenderTextArea()
+
+	searchString := v.TextArea.GetContent()
+
+	gui.helpers.Search.OnPromptContentChanged(searchString)
+
+	return matched
+}
diff --git a/pkg/gui/filetree/file_tree_view_model.go b/pkg/gui/filetree/file_tree_view_model.go
index 333be8da2..b48aaffab 100644
--- a/pkg/gui/filetree/file_tree_view_model.go
+++ b/pkg/gui/filetree/file_tree_view_model.go
@@ -26,6 +26,8 @@ type FileTreeViewModel struct {
 
 var _ IFileTreeViewModel = &FileTreeViewModel{}
 
+// how to tackle this? We could just filter down the list of files at a high point and then the rest will take care of itself.
+
 func NewFileTreeViewModel(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel {
 	fileTree := NewFileTree(getFiles, log, showTree)
 	listCursor := traits.NewListCursor(fileTree)
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index b922c5f77..106aee7a9 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -201,7 +201,7 @@ type GuiRepoState struct {
 	SplitMainPanel bool
 	LimitCommits   bool
 
-	Searching    searchingState
+	SearchState  *types.SearchState
 	StartupStage types.StartupStage // Allows us to not load everything at once
 
 	ContextMgr *ContextMgr
@@ -256,8 +256,12 @@ func (self *GuiRepoState) SetScreenMode(value types.WindowMaximisation) {
 	self.ScreenMode = value
 }
 
-func (self *GuiRepoState) IsSearching() bool {
-	return self.Searching.isSearching
+func (self *GuiRepoState) InSearchPrompt() bool {
+	return self.SearchState.SearchType() != types.SearchTypeNone
+}
+
+func (self *GuiRepoState) GetSearchState() *types.SearchState {
+	return self.SearchState
 }
 
 func (self *GuiRepoState) SetSplitMainPanel(value bool) {
@@ -268,12 +272,6 @@ func (self *GuiRepoState) GetSplitMainPanel() bool {
 	return self.SplitMainPanel
 }
 
-type searchingState struct {
-	view         *gocui.View
-	isSearching  bool
-	searchString string
-}
-
 func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
 	var err error
 	gui.git, err = commands.NewGitCommand(
@@ -358,6 +356,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.
 		ContextMgr:        NewContextMgr(gui, contextTree),
 		Contexts:          contextTree,
 		WindowViewNameMap: initialWindowViewNameMap(contextTree),
+		SearchState:       types.NewSearchState(),
 	}
 
 	gui.RepoStateMap[Repo(currentDir)] = gui.State
@@ -584,11 +583,12 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
 	})
 	deadlock.Opts.Disable = !gui.Debug
 
-	gui.g.OnSearchEscape = gui.onSearchEscape
 	if err := gui.Config.ReloadUserConfig(); err != nil {
 		return nil
 	}
 	userConfig := gui.UserConfig
+
+	gui.g.OnSearchEscape = func() error { gui.helpers.Search.Cancel(); return nil }
 	gui.g.SearchEscapeKey = keybindings.GetKey(userConfig.Keybinding.Universal.Return)
 	gui.g.NextSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.NextMatch)
 	gui.g.PrevSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.PrevMatch)
diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go
index dfed29c44..8fc7732fc 100644
--- a/pkg/gui/gui_common.go
+++ b/pkg/gui/gui_common.go
@@ -128,10 +128,6 @@ func (self *guiCommon) Mutexes() types.Mutexes {
 	return self.gui.Mutexes
 }
 
-func (self *guiCommon) OpenSearch() {
-	_ = self.gui.handleOpenSearch(self.gui.currentViewName())
-}
-
 func (self *guiCommon) GocuiGui() *gocui.Gui {
 	return self.gui.g
 }
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 30ddb8951..cb14d0266 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -215,18 +215,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
 			Modifier: gocui.ModNone,
 			Handler:  self.scrollUpSecondary,
 		},
-		{
-			ViewName: "search",
-			Key:      opts.GetKey(opts.Config.Universal.Confirm),
-			Modifier: gocui.ModNone,
-			Handler:  self.handleSearch,
-		},
-		{
-			ViewName: "search",
-			Key:      opts.GetKey(opts.Config.Universal.Return),
-			Modifier: gocui.ModNone,
-			Handler:  self.handleSearchEscape,
-		},
 		{
 			ViewName: "confirmation",
 			Key:      opts.GetKey(opts.Config.Universal.PrevItem),
diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go
index 88ddeca5f..ed10fda92 100644
--- a/pkg/gui/layout.go
+++ b/pkg/gui/layout.go
@@ -132,20 +132,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
 		}
 
 		view.SelBgColor = theme.GocuiSelectedLineBgColor
-
-		// I doubt this is expensive though it's admittedly redundant after the first render
-		view.SetOnSelectItem(gui.onSelectItemWrapper(listContext.OnSearchSelect))
-	}
-
-	for _, context := range gui.c.Context().AllPatchExplorer() {
-		context := context
-		context.GetView().SetOnSelectItem(gui.onSelectItemWrapper(
-			func(selectedLineIdx int) error {
-				context.GetMutex().Lock()
-				defer context.GetMutex().Unlock()
-				return context.NavigateTo(gui.c.IsCurrentContext(context), selectedLineIdx)
-			}),
-		)
 	}
 
 	mainViewWidth, mainViewHeight := gui.Views.Main.Size()
diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go
index f830526db..19c58b145 100644
--- a/pkg/gui/menu_panel.go
+++ b/pkg/gui/menu_panel.go
@@ -46,9 +46,6 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
 
 	gui.Views.Menu.Title = opts.Title
 	gui.Views.Menu.FgColor = theme.GocuiDefaultTextColor
-	gui.Views.Menu.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
-		return nil
-	}))
 
 	gui.Views.Tooltip.Wrap = true
 	gui.Views.Tooltip.FgColor = theme.GocuiDefaultTextColor
diff --git a/pkg/gui/searching.go b/pkg/gui/searching.go
deleted file mode 100644
index 21a07c4c2..000000000
--- a/pkg/gui/searching.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package gui
-
-import (
-	"fmt"
-
-	"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
-	"github.com/jesseduffield/lazygit/pkg/theme"
-)
-
-func (gui *Gui) handleOpenSearch(viewName string) error {
-	view, err := gui.g.View(viewName)
-	if err != nil {
-		return nil
-	}
-
-	gui.State.Searching.isSearching = true
-	gui.State.Searching.view = view
-
-	gui.Views.Search.ClearTextArea()
-
-	if err := gui.c.PushContext(gui.State.Contexts.Search); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func (gui *Gui) handleSearch() error {
-	gui.State.Searching.searchString = gui.Views.Search.TextArea.GetContent()
-	if err := gui.c.PopContext(); err != nil {
-		return err
-	}
-
-	view := gui.State.Searching.view
-	if view == nil {
-		return nil
-	}
-
-	if err := view.Search(gui.State.Searching.searchString); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
-	keybindingConfig := gui.c.UserConfig.Keybinding
-
-	return func(y int, index int, total int) error {
-		if total == 0 {
-			gui.c.SetViewContent(
-				gui.Views.Search,
-				fmt.Sprintf(
-					gui.Tr.NoMatchesFor,
-					gui.State.Searching.searchString,
-					theme.OptionsFgColor.Sprintf(gui.Tr.ExitSearchMode, keybindings.Label(keybindingConfig.Universal.Return)),
-				),
-			)
-			return nil
-		}
-		gui.c.SetViewContent(
-			gui.Views.Search,
-			fmt.Sprintf(
-				gui.Tr.MatchesFor,
-				gui.State.Searching.searchString,
-				index+1,
-				total,
-				theme.OptionsFgColor.Sprintf(
-					gui.Tr.SearchKeybindings,
-					keybindings.Label(keybindingConfig.Universal.NextMatch),
-					keybindings.Label(keybindingConfig.Universal.PrevMatch),
-					keybindings.Label(keybindingConfig.Universal.Return),
-				),
-			),
-		)
-		if err := innerFunc(y); err != nil {
-			return err
-		}
-		return nil
-	}
-}
-
-func (gui *Gui) onSearchEscape() error {
-	gui.State.Searching.isSearching = false
-	if gui.State.Searching.view != nil {
-		gui.State.Searching.view.ClearSearch()
-		gui.State.Searching.view = nil
-	}
-
-	return nil
-}
-
-func (gui *Gui) handleSearchEscape() error {
-	if err := gui.onSearchEscape(); err != nil {
-		return err
-	}
-
-	if err := gui.c.PopContext(); err != nil {
-		return err
-	}
-
-	return nil
-}
diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go
index 3709d3c7b..09ab040f2 100644
--- a/pkg/gui/types/common.go
+++ b/pkg/gui/types/common.go
@@ -68,8 +68,6 @@ type IGuiCommon interface {
 	Context() IContextMgr
 
 	ActivateContext(context Context) error
-	// enters search mode for the current view
-	OpenSearch()
 
 	GetConfig() config.AppConfigurer
 	GetAppState() *config.AppState
@@ -251,7 +249,8 @@ type IRepoStateAccessor interface {
 	SetCurrentPopupOpts(*CreatePopupPanelOpts)
 	GetScreenMode() WindowMaximisation
 	SetScreenMode(WindowMaximisation)
-	IsSearching() bool
+	InSearchPrompt() bool
+	GetSearchState() *SearchState
 	SetSplitMainPanel(bool)
 	GetSplitMainPanel() bool
 }
diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go
index bb8630bff..9c2ccd0d5 100644
--- a/pkg/gui/types/context.go
+++ b/pkg/gui/types/context.go
@@ -87,6 +87,24 @@ type Context interface {
 	HandleRenderToMain() error
 }
 
+type IFilterableContext interface {
+	Context
+
+	SetFilter(string)
+	GetFilter() string
+	ClearFilter()
+	IsFilterableContext()
+}
+
+type ISearchableContext interface {
+	Context
+
+	SetSearchString(string)
+	GetSearchString() string
+	ClearSearchString()
+	IsSearchableContext()
+}
+
 type DiffableContext interface {
 	Context
 
@@ -104,7 +122,6 @@ type IListContext interface {
 
 	GetList() IList
 
-	OnSearchSelect(selectedLineIdx int) error
 	FocusLine()
 	IsListContext() // used for type switch
 }
@@ -211,5 +228,7 @@ type IContextMgr interface {
 	IsCurrent(c Context) bool
 	ForEach(func(Context))
 	AllList() []IListContext
+	AllFilterable() []IFilterableContext
+	AllSearchable() []ISearchableContext
 	AllPatchExplorer() []IPatchExplorerContext
 }
diff --git a/pkg/gui/types/search_state.go b/pkg/gui/types/search_state.go
new file mode 100644
index 000000000..9b24af095
--- /dev/null
+++ b/pkg/gui/types/search_state.go
@@ -0,0 +1,31 @@
+package types
+
+type SearchType int
+
+const (
+	SearchTypeNone SearchType = iota
+	// searching is where matches are highlighted but the content is not filtered down
+	SearchTypeSearch
+	// filter is where the list is filtered down to only matches
+	SearchTypeFilter
+)
+
+// TODO: could we remove this entirely?
+type SearchState struct {
+	Context Context
+}
+
+func NewSearchState() *SearchState {
+	return &SearchState{}
+}
+
+func (self *SearchState) SearchType() SearchType {
+	switch self.Context.(type) {
+	case IFilterableContext:
+		return SearchTypeFilter
+	case ISearchableContext:
+		return SearchTypeSearch
+	default:
+		return SearchTypeNone
+	}
+}
diff --git a/pkg/gui/views.go b/pkg/gui/views.go
index 6b97f17b7..043acdaed 100644
--- a/pkg/gui/views.go
+++ b/pkg/gui/views.go
@@ -95,6 +95,8 @@ func (gui *Gui) createAllViews() error {
 	gui.Views.SearchPrefix.Frame = false
 	gui.c.SetViewContent(gui.Views.SearchPrefix, gui.Tr.SearchPrefix)
 
+	gui.Views.Search.Editor = gocui.EditorFunc(gui.searchEditor)
+
 	gui.Views.Stash.Title = gui.c.Tr.StashTitle
 
 	gui.Views.Commits.Title = gui.c.Tr.CommitsTitle
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index b803df360..e59d5e4df 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -371,6 +371,7 @@ type TranslationSet struct {
 	NextScreenMode                      string
 	PrevScreenMode                      string
 	StartSearch                         string
+	StartFilter                         string
 	Panel                               string
 	Keybindings                         string
 	KeybindingsLegend                   string
@@ -536,6 +537,7 @@ type TranslationSet struct {
 	MatchesFor                          string
 	SearchKeybindings                   string
 	SearchPrefix                        string
+	FilterPrefix                        string
 	ExitSearchMode                      string
 	Actions                             Actions
 	Bisect                              Bisect
@@ -1061,7 +1063,8 @@ func EnglishTranslationSet() TranslationSet {
 		ViewResetToUpstreamOptions:          "View upstream reset options",
 		NextScreenMode:                      "Next screen mode (normal/half/fullscreen)",
 		PrevScreenMode:                      "Prev screen mode",
-		StartSearch:                         "Start search",
+		StartSearch:                         "Search the current view",
+		StartFilter:                         "Filter the current view",
 		Panel:                               "Panel",
 		KeybindingsLegend:                   "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
 		RenameBranch:                        "Rename branch",
@@ -1226,6 +1229,7 @@ func EnglishTranslationSet() TranslationSet {
 		MatchesFor:                          "matches for '%s' (%d of %d) %s", // lowercase because it's after other text
 		SearchKeybindings:                   "%s: Next match, %s: Previous match, %s: Exit search mode",
 		SearchPrefix:                        "Search: ",
+		FilterPrefix:                        "Filter: ",
 		Actions: Actions{
 			// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
 			CheckoutCommit:                    "Checkout commit",
diff --git a/pkg/utils/slice.go b/pkg/utils/slice.go
index aff6ae470..4a47f43b1 100644
--- a/pkg/utils/slice.go
+++ b/pkg/utils/slice.go
@@ -113,3 +113,14 @@ func MoveElement[T any](slice []T, from int, to int) []T {
 
 	return newSlice
 }
+
+func ValuesAtIndices[T any](slice []T, indices []int) []T {
+	result := make([]T, len(indices))
+	for i, index := range indices {
+		// gracefully handling the situation where the index is out of bounds
+		if index < len(slice) {
+			result[i] = slice[index]
+		}
+	}
+	return result
+}

From 84870d45038e206a71aa3160d681a2b9d9827aa9 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 27 May 2023 20:38:49 +1000
Subject: [PATCH 05/20] Cancel filter/search when hitting escape

---
 pkg/gui/context.go                           |  6 +++---
 pkg/gui/context/search_trait.go              |  4 ++++
 pkg/gui/controllers/helpers/search_helper.go | 11 ++++++-----
 pkg/gui/controllers/quit_actions.go          | 13 +++++++++++++
 pkg/gui/types/context.go                     |  2 ++
 5 files changed, 28 insertions(+), 8 deletions(-)

diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index 26cec4c23..7c669cbff 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -211,7 +211,7 @@ func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLos
 		}
 
 		if filterableContext, ok := c.(types.IFilterableContext); ok {
-			if filterableContext.GetFilter() != "" {
+			if filterableContext.IsFiltering() {
 				filterableContext.ClearFilter()
 				self.gui.helpers.Search.Cancel()
 			}
@@ -247,12 +247,12 @@ func (self *ContextMgr) ActivateContext(c types.Context, opts types.OnFocusOpts)
 	}
 
 	if searchableContext, ok := c.(types.ISearchableContext); ok {
-		if searchableContext.GetSearchString() != "" {
+		if searchableContext.IsSearching() {
 			self.gui.helpers.Search.DisplaySearchPrompt(searchableContext)
 		}
 	}
 	if filterableContext, ok := c.(types.IFilterableContext); ok {
-		if filterableContext.GetFilter() != "" {
+		if filterableContext.IsFiltering() {
 			self.gui.helpers.Search.DisplayFilterPrompt(filterableContext)
 		}
 	}
diff --git a/pkg/gui/context/search_trait.go b/pkg/gui/context/search_trait.go
index 5e745f995..fad68d794 100644
--- a/pkg/gui/context/search_trait.go
+++ b/pkg/gui/context/search_trait.go
@@ -68,3 +68,7 @@ func (self *SearchTrait) onSelectItemWrapper(innerFunc func(int) error) func(int
 		return nil
 	}
 }
+
+func (self *SearchTrait) IsSearching() bool {
+	return self.searchString != ""
+}
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
index 9c0e09db4..e825fba3e 100644
--- a/pkg/gui/controllers/helpers/search_helper.go
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -138,10 +138,6 @@ func (self *SearchHelper) ConfirmFilter() error {
 func (self *SearchHelper) ConfirmSearch() error {
 	state := self.searchState()
 
-	if err := self.c.PopContext(); err != nil {
-		return err
-	}
-
 	context, ok := state.Context.(types.ISearchableContext)
 	if !ok {
 		self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey())
@@ -153,6 +149,10 @@ func (self *SearchHelper) ConfirmSearch() error {
 
 	view := context.GetView()
 
+	if err := self.c.PopContext(); err != nil {
+		return err
+	}
+
 	if err := view.Search(searchString); err != nil {
 		return err
 	}
@@ -171,9 +171,10 @@ func (self *SearchHelper) Cancel() {
 
 	switch context := state.Context.(type) {
 	case types.IFilterableContext:
-		context.SetFilter("")
+		context.ClearFilter()
 		_ = self.c.PostRefreshUpdate(context)
 	case types.ISearchableContext:
+		context.ClearSearchString()
 		context.GetView().ClearSearch()
 	default:
 		// do nothing
diff --git a/pkg/gui/controllers/quit_actions.go b/pkg/gui/controllers/quit_actions.go
index 2487a62fe..a163f66c8 100644
--- a/pkg/gui/controllers/quit_actions.go
+++ b/pkg/gui/controllers/quit_actions.go
@@ -50,6 +50,19 @@ func (self *QuitActions) confirmQuitDuringUpdate() error {
 func (self *QuitActions) Escape() error {
 	currentContext := self.c.CurrentContext()
 
+	switch ctx := currentContext.(type) {
+	case types.IFilterableContext:
+		if ctx.IsFiltering() {
+			self.c.Helpers().Search.Cancel()
+			return nil
+		}
+	case types.ISearchableContext:
+		if ctx.IsSearching() {
+			self.c.Helpers().Search.Cancel()
+			return nil
+		}
+	}
+
 	parentContext, hasParent := currentContext.GetParentContext()
 	if hasParent && currentContext != nil && parentContext != nil {
 		// TODO: think about whether this should be marked as a return rather than adding to the stack
diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go
index 9c2ccd0d5..9da9ad2b1 100644
--- a/pkg/gui/types/context.go
+++ b/pkg/gui/types/context.go
@@ -93,6 +93,7 @@ type IFilterableContext interface {
 	SetFilter(string)
 	GetFilter() string
 	ClearFilter()
+	IsFiltering() bool
 	IsFilterableContext()
 }
 
@@ -102,6 +103,7 @@ type ISearchableContext interface {
 	SetSearchString(string)
 	GetSearchString() string
 	ClearSearchString()
+	IsSearching() bool
 	IsSearchableContext()
 }
 

From 13326344f092e23d4a54887505c92060377adeb6 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 27 May 2023 19:58:48 +1000
Subject: [PATCH 06/20] Support filtering files

---
 pkg/commands/models/commit_file.go            |  4 +++
 pkg/gui/context/commit_files_context.go       | 23 ++++++++++++++-
 pkg/gui/context/filtered_list.go              | 10 ++++++-
 pkg/gui/context/filtered_list_view_model.go   |  7 ++---
 pkg/gui/context/working_tree_context.go       | 29 +++++++++++++++++--
 pkg/gui/controllers/files_controller.go       |  2 +-
 pkg/gui/controllers/helpers/refresh_helper.go |  4 +--
 pkg/gui/filetree/file_tree.go                 |  6 ++--
 pkg/gui/filetree/file_tree_view_model.go      |  6 ++--
 9 files changed, 72 insertions(+), 19 deletions(-)

diff --git a/pkg/commands/models/commit_file.go b/pkg/commands/models/commit_file.go
index 45b56d2dd..d05b5b3fd 100644
--- a/pkg/commands/models/commit_file.go
+++ b/pkg/commands/models/commit_file.go
@@ -23,3 +23,7 @@ func (f *CommitFile) Added() bool {
 func (f *CommitFile) Deleted() bool {
 	return f.ChangeStatus == "D"
 }
+
+func (f *CommitFile) GetPath() string {
+	return f.Name
+}
diff --git a/pkg/gui/context/commit_files_context.go b/pkg/gui/context/commit_files_context.go
index 96b6f2fcf..54d2a02e3 100644
--- a/pkg/gui/context/commit_files_context.go
+++ b/pkg/gui/context/commit_files_context.go
@@ -10,6 +10,7 @@ import (
 )
 
 type CommitFilesContext struct {
+	*FilteredList[*models.CommitFile]
 	*filetree.CommitFileTreeViewModel
 	*ListContextTrait
 	*DynamicTitleBuilder
@@ -21,8 +22,13 @@ var (
 )
 
 func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
-	viewModel := filetree.NewCommitFileTreeViewModel(
+	filteredList := NewFilteredList(
 		func() []*models.CommitFile { return c.Model().CommitFiles },
+		func(file *models.CommitFile) []string { return []string{file.GetPath()} },
+	)
+
+	viewModel := filetree.NewCommitFileTreeViewModel(
+		func() []*models.CommitFile { return filteredList.GetFilteredList() },
 		c.Log,
 		c.UserConfig.Gui.ShowFileTree,
 	)
@@ -39,6 +45,7 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
 	}
 
 	return &CommitFilesContext{
+		FilteredList:            filteredList,
 		CommitFileTreeViewModel: viewModel,
 		DynamicTitleBuilder:     NewDynamicTitleBuilder(c.Tr.CommitFilesDynamicTitle),
 		ListContextTrait: &ListContextTrait{
@@ -71,3 +78,17 @@ func (self *CommitFilesContext) GetSelectedItemId() string {
 func (self *CommitFilesContext) GetDiffTerminals() []string {
 	return []string{self.GetRef().RefName()}
 }
+
+// used for type switch
+func (self *CommitFilesContext) IsFilterableContext() {}
+
+// TODO: see if we can just call SetTree() within HandleRender(). It doesn't seem
+// right that we need to imperatively refresh the view model like this
+func (self *CommitFilesContext) SetFilter(filter string) {
+	self.FilteredList.SetFilter(filter)
+	self.SetTree()
+}
+
+func (self *CommitFilesContext) ClearFilter() {
+	self.SetFilter("")
+}
diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go
index 1317924ad..5a7257388 100644
--- a/pkg/gui/context/filtered_list.go
+++ b/pkg/gui/context/filtered_list.go
@@ -14,6 +14,13 @@ type FilteredList[T any] struct {
 	filter          string
 }
 
+func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string) *FilteredList[T] {
+	return &FilteredList[T]{
+		getList:         getList,
+		getFilterFields: getFilterFields,
+	}
+}
+
 func (self *FilteredList[T]) GetFilter() string {
 	return self.filter
 }
@@ -28,13 +35,14 @@ func (self *FilteredList[T]) ClearFilter() {
 	self.SetFilter("")
 }
 
-func (self *FilteredList[T]) GetList() []T {
+func (self *FilteredList[T]) GetFilteredList() []T {
 	if self.filteredIndices == nil {
 		return self.getList()
 	}
 	return utils.ValuesAtIndices(self.getList(), self.filteredIndices)
 }
 
+// TODO: update to just 'Len'
 func (self *FilteredList[T]) UnfilteredLen() int {
 	return len(self.getList())
 }
diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go
index 01b020841..6196ed180 100644
--- a/pkg/gui/context/filtered_list_view_model.go
+++ b/pkg/gui/context/filtered_list_view_model.go
@@ -6,16 +6,13 @@ type FilteredListViewModel[T any] struct {
 }
 
 func NewFilteredListViewModel[T any](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] {
-	filteredList := &FilteredList[T]{
-		getList:         getList,
-		getFilterFields: getFilterFields,
-	}
+	filteredList := NewFilteredList(getList, getFilterFields)
 
 	self := &FilteredListViewModel[T]{
 		FilteredList: filteredList,
 	}
 
-	listViewModel := NewListViewModel(filteredList.GetList)
+	listViewModel := NewListViewModel(filteredList.GetFilteredList)
 
 	self.ListViewModel = listViewModel
 
diff --git a/pkg/gui/context/working_tree_context.go b/pkg/gui/context/working_tree_context.go
index 45502eb60..0c8c2bc5e 100644
--- a/pkg/gui/context/working_tree_context.go
+++ b/pkg/gui/context/working_tree_context.go
@@ -9,20 +9,30 @@ import (
 )
 
 type WorkingTreeContext struct {
+	*FilteredList[*models.File]
 	*filetree.FileTreeViewModel
 	*ListContextTrait
 }
 
-var _ types.IListContext = (*WorkingTreeContext)(nil)
+var (
+	_ types.IListContext       = (*WorkingTreeContext)(nil)
+	_ types.IFilterableContext = (*WorkingTreeContext)(nil)
+)
 
 func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
-	viewModel := filetree.NewFileTreeViewModel(
+	filteredList := NewFilteredList(
 		func() []*models.File { return c.Model().Files },
+		func(file *models.File) []string { return []string{file.GetPath()} },
+	)
+
+	viewModel := filetree.NewFileTreeViewModel(
+		func() []*models.File { return filteredList.GetFilteredList() },
 		c.Log,
 		c.UserConfig.Gui.ShowFileTree,
 	)
 
 	getDisplayStrings := func(startIdx int, length int) [][]string {
+		c.Log.Warn("in get display strings")
 		lines := presentation.RenderFileTree(viewModel, c.Modes().Diffing.Ref, c.Model().Submodules)
 		return slices.Map(lines, func(line string) []string {
 			return []string{line}
@@ -30,6 +40,7 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
 	}
 
 	return &WorkingTreeContext{
+		FilteredList:      filteredList,
 		FileTreeViewModel: viewModel,
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
@@ -54,3 +65,17 @@ func (self *WorkingTreeContext) GetSelectedItemId() string {
 
 	return item.ID()
 }
+
+// used for type switch
+func (self *WorkingTreeContext) IsFilterableContext() {}
+
+// TODO: see if we can just call SetTree() within HandleRender(). It doesn't seem
+// right that we need to imperatively refresh the view model like this
+func (self *WorkingTreeContext) SetFilter(filter string) {
+	self.FilteredList.SetFilter(filter)
+	self.SetTree()
+}
+
+func (self *WorkingTreeContext) ClearFilter() {
+	self.SetFilter("")
+}
diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go
index 11d39ee9f..93e52c192 100644
--- a/pkg/gui/controllers/files_controller.go
+++ b/pkg/gui/controllers/files_controller.go
@@ -658,7 +658,7 @@ func (self *FilesController) handleStatusFilterPressed() error {
 }
 
 func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error {
-	self.context().FileTreeViewModel.SetFilter(filter)
+	self.context().FileTreeViewModel.SetStatusFilter(filter)
 	return self.c.PostRefreshUpdate(self.context())
 }
 
diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go
index 7b37585a5..f0827dc41 100644
--- a/pkg/gui/controllers/helpers/refresh_helper.go
+++ b/pkg/gui/controllers/helpers/refresh_helper.go
@@ -464,10 +464,10 @@ func (self *RefreshHelper) refreshStateFiles() error {
 	// I'd prefer to maintain as little state as possible.
 	if conflictFileCount > 0 {
 		if fileTreeViewModel.GetFilter() == filetree.DisplayAll {
-			fileTreeViewModel.SetFilter(filetree.DisplayConflicted)
+			fileTreeViewModel.SetStatusFilter(filetree.DisplayConflicted)
 		}
 	} else if fileTreeViewModel.GetFilter() == filetree.DisplayConflicted {
-		fileTreeViewModel.SetFilter(filetree.DisplayAll)
+		fileTreeViewModel.SetStatusFilter(filetree.DisplayAll)
 	}
 
 	self.c.Model().Files = files
diff --git a/pkg/gui/filetree/file_tree.go b/pkg/gui/filetree/file_tree.go
index 950bf24be..3f244edfe 100644
--- a/pkg/gui/filetree/file_tree.go
+++ b/pkg/gui/filetree/file_tree.go
@@ -34,7 +34,7 @@ type IFileTree interface {
 	ITree[models.File]
 
 	FilterFiles(test func(*models.File) bool) []*models.File
-	SetFilter(filter FileTreeDisplayFilter)
+	SetStatusFilter(filter FileTreeDisplayFilter)
 	Get(index int) *FileNode
 	GetFile(path string) *models.File
 	GetAllItems() []*FileNode
@@ -91,7 +91,7 @@ func (self *FileTree) FilterFiles(test func(*models.File) bool) []*models.File {
 	return slices.Filter(self.getFiles(), test)
 }
 
-func (self *FileTree) SetFilter(filter FileTreeDisplayFilter) {
+func (self *FileTree) SetStatusFilter(filter FileTreeDisplayFilter) {
 	self.filter = filter
 	self.SetTree()
 }
@@ -102,7 +102,7 @@ func (self *FileTree) ToggleShowTree() {
 }
 
 func (self *FileTree) Get(index int) *FileNode {
-	// need to traverse the three depth first until we get to the index.
+	// need to traverse the tree depth first until we get to the index.
 	return NewFileNode(self.tree.GetNodeAtIndex(index+1, self.collapsedPaths)) // ignoring root
 }
 
diff --git a/pkg/gui/filetree/file_tree_view_model.go b/pkg/gui/filetree/file_tree_view_model.go
index b48aaffab..547b62b91 100644
--- a/pkg/gui/filetree/file_tree_view_model.go
+++ b/pkg/gui/filetree/file_tree_view_model.go
@@ -26,8 +26,6 @@ type FileTreeViewModel struct {
 
 var _ IFileTreeViewModel = &FileTreeViewModel{}
 
-// how to tackle this? We could just filter down the list of files at a high point and then the rest will take care of itself.
-
 func NewFileTreeViewModel(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel {
 	fileTree := NewFileTree(getFiles, log, showTree)
 	listCursor := traits.NewListCursor(fileTree)
@@ -128,8 +126,8 @@ func (self *FileTreeViewModel) findNewSelectedIdx(prevNodes []*FileNode, currNod
 	return -1
 }
 
-func (self *FileTreeViewModel) SetFilter(filter FileTreeDisplayFilter) {
-	self.IFileTree.SetFilter(filter)
+func (self *FileTreeViewModel) SetStatusFilter(filter FileTreeDisplayFilter) {
+	self.IFileTree.SetStatusFilter(filter)
 	self.IListCursor.SetSelectedLineIdx(0)
 }
 

From bf5871cc4fe8bd7f431e5603447ea0bc29b3d642 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 27 May 2023 20:38:37 +1000
Subject: [PATCH 07/20] Case insensitive string comparison

---
 pkg/gui/context/filtered_list.go         | 12 +++-
 pkg/gui/context/working_tree_context.go  |  1 -
 pkg/utils/fuzzy_search_test.go           | 53 ----------------
 pkg/utils/{fuzzy_search.go => search.go} |  8 +++
 pkg/utils/search_test.go                 | 80 ++++++++++++++++++++++++
 5 files changed, 97 insertions(+), 57 deletions(-)
 delete mode 100644 pkg/utils/fuzzy_search_test.go
 rename pkg/utils/{fuzzy_search.go => search.go} (72%)
 create mode 100644 pkg/utils/search_test.go

diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go
index 5a7257388..b848b96d4 100644
--- a/pkg/gui/context/filtered_list.go
+++ b/pkg/gui/context/filtered_list.go
@@ -1,8 +1,6 @@
 package context
 
 import (
-	"strings"
-
 	"github.com/jesseduffield/lazygit/pkg/utils"
 )
 
@@ -35,6 +33,10 @@ func (self *FilteredList[T]) ClearFilter() {
 	self.SetFilter("")
 }
 
+func (self *FilteredList[T]) IsFiltering() bool {
+	return self.filter != ""
+}
+
 func (self *FilteredList[T]) GetFilteredList() []T {
 	if self.filteredIndices == nil {
 		return self.getList()
@@ -54,7 +56,7 @@ func (self *FilteredList[T]) applyFilter() {
 		self.filteredIndices = []int{}
 		for i, item := range self.getList() {
 			for _, field := range self.getFilterFields(item) {
-				if strings.Contains(field, self.filter) {
+				if self.match(field, self.filter) {
 					self.filteredIndices = append(self.filteredIndices, i)
 					break
 				}
@@ -62,3 +64,7 @@ func (self *FilteredList[T]) applyFilter() {
 		}
 	}
 }
+
+func (self *FilteredList[T]) match(haystack string, needle string) bool {
+	return utils.CaseInsensitiveContains(haystack, needle)
+}
diff --git a/pkg/gui/context/working_tree_context.go b/pkg/gui/context/working_tree_context.go
index 0c8c2bc5e..ee053eea1 100644
--- a/pkg/gui/context/working_tree_context.go
+++ b/pkg/gui/context/working_tree_context.go
@@ -32,7 +32,6 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
 	)
 
 	getDisplayStrings := func(startIdx int, length int) [][]string {
-		c.Log.Warn("in get display strings")
 		lines := presentation.RenderFileTree(viewModel, c.Modes().Diffing.Ref, c.Model().Submodules)
 		return slices.Map(lines, func(line string) []string {
 			return []string{line}
diff --git a/pkg/utils/fuzzy_search_test.go b/pkg/utils/fuzzy_search_test.go
deleted file mode 100644
index 808d83772..000000000
--- a/pkg/utils/fuzzy_search_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package utils
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-// TestFuzzySearch is a function.
-func TestFuzzySearch(t *testing.T) {
-	type scenario struct {
-		needle   string
-		haystack []string
-		expected []string
-	}
-
-	scenarios := []scenario{
-		{
-			needle:   "",
-			haystack: []string{"test"},
-			expected: []string{},
-		},
-		{
-			needle:   "test",
-			haystack: []string{"test"},
-			expected: []string{"test"},
-		},
-		{
-			needle:   "o",
-			haystack: []string{"a", "o", "e"},
-			expected: []string{"o"},
-		},
-		{
-			needle:   "mybranch",
-			haystack: []string{"my_branch", "mybranch", "branch", "this is my branch"},
-			expected: []string{"mybranch", "my_branch", "this is my branch"},
-		},
-		{
-			needle:   "test",
-			haystack: []string{"not a good match", "this 'test' is a good match", "test"},
-			expected: []string{"test", "this 'test' is a good match"},
-		},
-		{
-			needle:   "test",
-			haystack: []string{"Test"},
-			expected: []string{"Test"},
-		},
-	}
-
-	for _, s := range scenarios {
-		assert.EqualValues(t, s.expected, FuzzySearch(s.needle, s.haystack))
-	}
-}
diff --git a/pkg/utils/fuzzy_search.go b/pkg/utils/search.go
similarity index 72%
rename from pkg/utils/fuzzy_search.go
rename to pkg/utils/search.go
index 5fce3dde9..e90d2b5b0 100644
--- a/pkg/utils/fuzzy_search.go
+++ b/pkg/utils/search.go
@@ -2,6 +2,7 @@ package utils
 
 import (
 	"sort"
+	"strings"
 
 	"github.com/jesseduffield/generics/slices"
 	"github.com/sahilm/fuzzy"
@@ -19,3 +20,10 @@ func FuzzySearch(needle string, haystack []string) []string {
 		return match.Str
 	})
 }
+
+func CaseInsensitiveContains(a, b string) bool {
+	return strings.Contains(
+		strings.ToLower(a),
+		strings.ToLower(b),
+	)
+}
diff --git a/pkg/utils/search_test.go b/pkg/utils/search_test.go
new file mode 100644
index 000000000..79668c0f5
--- /dev/null
+++ b/pkg/utils/search_test.go
@@ -0,0 +1,80 @@
+package utils
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// TestFuzzySearch is a function.
+func TestFuzzySearch(t *testing.T) {
+	type scenario struct {
+		needle   string
+		haystack []string
+		expected []string
+	}
+
+	scenarios := []scenario{
+		{
+			needle:   "",
+			haystack: []string{"test"},
+			expected: []string{},
+		},
+		{
+			needle:   "test",
+			haystack: []string{"test"},
+			expected: []string{"test"},
+		},
+		{
+			needle:   "o",
+			haystack: []string{"a", "o", "e"},
+			expected: []string{"o"},
+		},
+		{
+			needle:   "mybranch",
+			haystack: []string{"my_branch", "mybranch", "branch", "this is my branch"},
+			expected: []string{"mybranch", "my_branch", "this is my branch"},
+		},
+		{
+			needle:   "test",
+			haystack: []string{"not a good match", "this 'test' is a good match", "test"},
+			expected: []string{"test", "this 'test' is a good match"},
+		},
+		{
+			needle:   "test",
+			haystack: []string{"Test"},
+			expected: []string{"Test"},
+		},
+	}
+
+	for _, s := range scenarios {
+		assert.EqualValues(t, s.expected, FuzzySearch(s.needle, s.haystack))
+	}
+}
+
+func TestCaseInsensitiveContains(t *testing.T) {
+	testCases := []struct {
+		haystack string
+		needle   string
+		expected bool
+	}{
+		{"Hello, World!", "world", true},           // Case-insensitive match
+		{"Hello, World!", "WORLD", true},           // Case-insensitive match
+		{"Hello, World!", "orl", true},             // Case-insensitive match
+		{"Hello, World!", "o, W", true},            // Case-insensitive match
+		{"Hello, World!", "hello", true},           // Case-insensitive match
+		{"Hello, World!", "Foo", false},            // No match
+		{"Hello, World!", "Hello, World!!", false}, // No match
+		{"Hello, World!", "", true},                // Empty needle matches
+		{"", "Hello", false},                       // Empty haystack doesn't match
+		{"", "", true},                             // Empty strings match
+		{"", " ", false},                           // Empty haystack, non-empty needle
+		{" ", "", true},                            // Non-empty haystack, empty needle
+	}
+
+	for i, testCase := range testCases {
+		result := CaseInsensitiveContains(testCase.haystack, testCase.needle)
+		assert.Equal(t, testCase.expected, result, fmt.Sprintf("Test case %d failed. Expected '%v', got '%v' for '%s' in '%s'", i, testCase.expected, result, testCase.needle, testCase.haystack))
+	}
+}

From d67b209e62f8254d5f17ecdb8fe7ff810f000566 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sun, 28 May 2023 13:25:52 +1000
Subject: [PATCH 08/20] Move more logic into search helper

---
 pkg/gui/context.go                           | 27 ++--------------
 pkg/gui/controllers/helpers/search_helper.go | 33 ++++++++++++++++++++
 2 files changed, 35 insertions(+), 25 deletions(-)

diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index 7c669cbff..1c235a93f 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -201,21 +201,7 @@ func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLos
 	view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
 
 	if opts.NewContextKey != context.SEARCH_CONTEXT_KEY {
-
-		if searchableContext, ok := c.(types.ISearchableContext); ok {
-			if view != nil && view.IsSearching() {
-				view.ClearSearch()
-				searchableContext.ClearSearchString()
-				self.gui.helpers.Search.Cancel()
-			}
-		}
-
-		if filterableContext, ok := c.(types.IFilterableContext); ok {
-			if filterableContext.IsFiltering() {
-				filterableContext.ClearFilter()
-				self.gui.helpers.Search.Cancel()
-			}
-		}
+		self.gui.helpers.Search.CancelSearchIfSearching(c)
 	}
 
 	// if we are the kind of context that is sent to back upon deactivation, we should do that
@@ -246,16 +232,7 @@ func (self *ContextMgr) ActivateContext(c types.Context, opts types.OnFocusOpts)
 		return err
 	}
 
-	if searchableContext, ok := c.(types.ISearchableContext); ok {
-		if searchableContext.IsSearching() {
-			self.gui.helpers.Search.DisplaySearchPrompt(searchableContext)
-		}
-	}
-	if filterableContext, ok := c.(types.IFilterableContext); ok {
-		if filterableContext.IsFiltering() {
-			self.gui.helpers.Search.DisplayFilterPrompt(filterableContext)
-		}
-	}
+	self.gui.helpers.Search.DisplaySearchInfoIfSearching(c)
 
 	desiredTitle := c.Title()
 	if desiredTitle != "" {
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
index e825fba3e..f8bfcbd9c 100644
--- a/pkg/gui/controllers/helpers/search_helper.go
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -195,3 +195,36 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) {
 		// do nothing (shouldn't land here)
 	}
 }
+
+func (self *SearchHelper) DisplaySearchInfoIfSearching(c types.Context) {
+	if searchableContext, ok := c.(types.ISearchableContext); ok {
+		if searchableContext.IsSearching() {
+			self.DisplaySearchPrompt(searchableContext)
+		}
+	}
+	if filterableContext, ok := c.(types.IFilterableContext); ok {
+		if filterableContext.IsFiltering() {
+			self.DisplayFilterPrompt(filterableContext)
+		}
+	}
+}
+
+func (self *SearchHelper) CancelSearchIfSearching(c types.Context) {
+	if searchableContext, ok := c.(types.ISearchableContext); ok {
+		view := searchableContext.GetView()
+		if view != nil && view.IsSearching() {
+			view.ClearSearch()
+			searchableContext.ClearSearchString()
+			self.Cancel()
+		}
+		return
+	}
+
+	if filterableContext, ok := c.(types.IFilterableContext); ok {
+		if filterableContext.IsFiltering() {
+			filterableContext.ClearFilter()
+			self.Cancel()
+		}
+		return
+	}
+}

From b8bee4de5158a6f285127eff018c442bf45ae970 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 3 Jun 2023 12:12:32 +1000
Subject: [PATCH 09/20] Scroll to top when filtering and retain selection when
 cancelling filter

---
 pkg/gui/context/filtered_list.go             | 14 ++++++++++++++
 pkg/gui/context/filtered_list_view_model.go  | 10 ++++++++++
 pkg/gui/controllers/helpers/search_helper.go |  7 ++++---
 pkg/gui/types/context.go                     |  1 +
 4 files changed, 29 insertions(+), 3 deletions(-)

diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go
index b848b96d4..bffd0eddb 100644
--- a/pkg/gui/context/filtered_list.go
+++ b/pkg/gui/context/filtered_list.go
@@ -68,3 +68,17 @@ func (self *FilteredList[T]) applyFilter() {
 func (self *FilteredList[T]) match(haystack string, needle string) bool {
 	return utils.CaseInsensitiveContains(haystack, needle)
 }
+
+func (self *FilteredList[T]) UnfilteredIndex(index int) int {
+	if self.filteredIndices == nil {
+		return index
+	}
+
+	// we use -1 when there are no items
+	if index == -1 {
+		return -1
+	}
+
+	// TODO: mutex
+	return self.filteredIndices[index]
+}
diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go
index 6196ed180..77f6e1174 100644
--- a/pkg/gui/context/filtered_list_view_model.go
+++ b/pkg/gui/context/filtered_list_view_model.go
@@ -21,3 +21,13 @@ func NewFilteredListViewModel[T any](getList func() []T, getFilterFields func(T)
 
 // used for type switch
 func (self *FilteredListViewModel[T]) IsFilterableContext() {}
+
+func (self *FilteredListViewModel[T]) ClearFilter() {
+	// Set the selected line index to the unfiltered index of the currently selected line,
+	// so that the current item is still selected after the filter is cleared.
+	unfilteredIndex := self.FilteredList.UnfilteredIndex(self.GetSelectedLineIdx())
+
+	self.FilteredList.ClearFilter()
+
+	self.SetSelectedLineIdx(unfilteredIndex)
+}
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
index f8bfcbd9c..68af20b35 100644
--- a/pkg/gui/controllers/helpers/search_helper.go
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -123,14 +123,13 @@ func (self *SearchHelper) ConfirmFilter() error {
 	// We also do this on each keypress but we do it here again just in case
 	state := self.searchState()
 
-	context, ok := state.Context.(types.IFilterableContext)
+	_, ok := state.Context.(types.IFilterableContext)
 	if !ok {
 		self.c.Log.Warnf("Context %s is not filterable", state.Context.GetKey())
 		return nil
 	}
 
-	context.SetFilter(self.promptContent())
-	_ = self.c.PostRefreshUpdate(state.Context)
+	self.OnPromptContentChanged(self.promptContent())
 
 	return self.c.PopContext()
 }
@@ -187,6 +186,8 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) {
 	state := self.searchState()
 	switch context := state.Context.(type) {
 	case types.IFilterableContext:
+		context.SetSelectedLineIdx(0)
+		_ = context.GetView().SetOriginY(0)
 		context.SetFilter(searchString)
 		_ = self.c.PostRefreshUpdate(context)
 	case types.ISearchableContext:
diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go
index 9da9ad2b1..dca5b042c 100644
--- a/pkg/gui/types/context.go
+++ b/pkg/gui/types/context.go
@@ -89,6 +89,7 @@ type Context interface {
 
 type IFilterableContext interface {
 	Context
+	IListPanelState
 
 	SetFilter(string)
 	GetFilter() string

From 13c1103815e791966615647726000f2e3a7de828 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 3 Jun 2023 13:15:41 +1000
Subject: [PATCH 10/20] Only cancel search if main or temporary context loses
 focus

This is a pickle: initially I wanted it so that a filter would cancel automatically if the current context lost focus.
But there are situations where you want to retain the focus, e.g. when a popup appears, or when you view the commits
of a branch. The issue is that when you view the commits of a branch, the branches context is removed from the context
stack. Even if this were not the case, you could imagine going branches -> sub-commits -> files -> sub-commits, where
in that case branches would definitely be off the stack upon navigating to the files context.

So because I'm too lazy to find a proper solution to this problem, I'm just making it so that filters in side contexts
are retained unless explicitly cancelled.

There's another edge case this commit handles which is that if I'm in the sub-commits context via the branches context
and start a search, then navigate to the reflog context and hit enter to get to the sub-commits context again, I need
to cancel the search before I switch. Likewise with the commit files context.
---
 pkg/gui/context.go                                |  5 ++++-
 pkg/gui/controllers/helpers/search_helper.go      | 15 +++++++++++----
 .../switch_to_diff_files_controller.go            |  1 +
 .../switch_to_sub_commits_controller.go           | 15 +++++++++------
 4 files changed, 25 insertions(+), 11 deletions(-)

diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index 1c235a93f..e9a15870b 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -201,7 +201,10 @@ func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLos
 	view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
 
 	if opts.NewContextKey != context.SEARCH_CONTEXT_KEY {
-		self.gui.helpers.Search.CancelSearchIfSearching(c)
+		self.gui.helpers.Search.HidePrompt()
+		if c.GetKind() == types.MAIN_CONTEXT || c.GetKind() == types.TEMPORARY_POPUP {
+			self.gui.helpers.Search.CancelSearchIfSearching(c)
+		}
 	}
 
 	// if we are the kind of context that is sent to back upon deactivation, we should do that
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
index 68af20b35..294ce9dbf 100644
--- a/pkg/gui/controllers/helpers/search_helper.go
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -41,15 +41,16 @@ func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) err
 	return nil
 }
 
-func (self *SearchHelper) OpenSearchPrompt(context types.Context) error {
+func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) error {
 	state := self.searchState()
 
 	state.Context = context
+	searchString := context.GetSearchString()
 
 	self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
 	promptView := self.promptView()
-	// TODO: should we show the currently searched thing here? Perhaps we can store that on the context
 	promptView.ClearTextArea()
+	promptView.TextArea.TypeString(searchString)
 	promptView.RenderTextArea()
 
 	if err := self.c.PushContext(self.c.Contexts().Search); err != nil {
@@ -78,7 +79,8 @@ func (self *SearchHelper) DisplaySearchPrompt(context types.ISearchableContext)
 	state.Context = context
 	searchString := context.GetSearchString()
 
-	self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
+	_ = context.GetView().SelectCurrentSearchResult()
+
 	promptView := self.promptView()
 	promptView.ClearTextArea()
 	promptView.TextArea.TypeString(searchString)
@@ -179,7 +181,7 @@ func (self *SearchHelper) Cancel() {
 		// do nothing
 	}
 
-	state.Context = nil
+	self.HidePrompt()
 }
 
 func (self *SearchHelper) OnPromptContentChanged(searchString string) {
@@ -229,3 +231,8 @@ func (self *SearchHelper) CancelSearchIfSearching(c types.Context) {
 		return
 	}
 }
+
+func (self *SearchHelper) HidePrompt() {
+	state := self.searchState()
+	state.Context = nil
+}
diff --git a/pkg/gui/controllers/switch_to_diff_files_controller.go b/pkg/gui/controllers/switch_to_diff_files_controller.go
index ffec6936e..767520d20 100644
--- a/pkg/gui/controllers/switch_to_diff_files_controller.go
+++ b/pkg/gui/controllers/switch_to_diff_files_controller.go
@@ -83,6 +83,7 @@ func (self *SwitchToDiffFilesController) viewFiles(opts SwitchToCommitFilesConte
 	diffFilesContext.SetCanRebase(opts.CanRebase)
 	diffFilesContext.SetParentContext(opts.Context)
 	diffFilesContext.SetWindowName(opts.Context.GetWindowName())
+	diffFilesContext.ClearFilter()
 
 	if err := self.c.Refresh(types.RefreshOptions{
 		Scope: []types.RefreshableView{types.COMMIT_FILES},
diff --git a/pkg/gui/controllers/switch_to_sub_commits_controller.go b/pkg/gui/controllers/switch_to_sub_commits_controller.go
index 5ae86f02b..8163181e5 100644
--- a/pkg/gui/controllers/switch_to_sub_commits_controller.go
+++ b/pkg/gui/controllers/switch_to_sub_commits_controller.go
@@ -71,12 +71,15 @@ func (self *SwitchToSubCommitsController) viewCommits() error {
 
 	self.setSubCommits(commits)
 
-	self.c.Contexts().SubCommits.SetSelectedLineIdx(0)
-	self.c.Contexts().SubCommits.SetParentContext(self.context)
-	self.c.Contexts().SubCommits.SetWindowName(self.context.GetWindowName())
-	self.c.Contexts().SubCommits.SetTitleRef(ref.Description())
-	self.c.Contexts().SubCommits.SetRef(ref)
-	self.c.Contexts().SubCommits.SetLimitCommits(true)
+	subCommitsContext := self.c.Contexts().SubCommits
+	subCommitsContext.SetSelectedLineIdx(0)
+	subCommitsContext.SetParentContext(self.context)
+	subCommitsContext.SetWindowName(self.context.GetWindowName())
+	subCommitsContext.SetTitleRef(ref.Description())
+	subCommitsContext.SetRef(ref)
+	subCommitsContext.SetLimitCommits(true)
+	subCommitsContext.ClearSearchString()
+	subCommitsContext.GetView().ClearSearch()
 
 	err = self.c.PostRefreshUpdate(self.c.Contexts().SubCommits)
 	if err != nil {

From 3ca1292fb4d688559c81f08c449d14ae4031e60b Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 3 Jun 2023 13:50:26 +1000
Subject: [PATCH 11/20] Show filter status similar to what we show with search

---
 pkg/gui/context.go                           |  2 +-
 pkg/gui/controllers/helpers/search_helper.go | 27 ++++++++++----------
 pkg/i18n/english.go                          |  2 ++
 3 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index e9a15870b..fde888a1c 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -235,7 +235,7 @@ func (self *ContextMgr) ActivateContext(c types.Context, opts types.OnFocusOpts)
 		return err
 	}
 
-	self.gui.helpers.Search.DisplaySearchInfoIfSearching(c)
+	self.gui.helpers.Search.DisplaySearchStatusIfSearching(c)
 
 	desiredTitle := c.Title()
 	if desiredTitle != "" {
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
index 294ce9dbf..7d552a373 100644
--- a/pkg/gui/controllers/helpers/search_helper.go
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -1,8 +1,12 @@
 package helpers
 
 import (
+	"fmt"
+
 	"github.com/jesseduffield/gocui"
+	"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
 	"github.com/jesseduffield/lazygit/pkg/gui/types"
+	"github.com/jesseduffield/lazygit/pkg/theme"
 )
 
 // NOTE: this helper supports both filtering and searching. Filtering is when
@@ -60,31 +64,26 @@ func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) err
 	return nil
 }
 
-func (self *SearchHelper) DisplayFilterPrompt(context types.IFilterableContext) {
+func (self *SearchHelper) DisplayFilterStatus(context types.IFilterableContext) {
 	state := self.searchState()
 
 	state.Context = context
 	searchString := context.GetFilter()
 
 	self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix)
+
 	promptView := self.promptView()
-	promptView.ClearTextArea()
-	promptView.TextArea.TypeString(searchString)
-	promptView.RenderTextArea()
+	keybindingConfig := self.c.UserConfig.Keybinding
+	promptView.SetContent(fmt.Sprintf("matches for '%s' ", searchString) + theme.OptionsFgColor.Sprintf(self.c.Tr.ExitTextFilterMode, keybindings.Label(keybindingConfig.Universal.Return)))
 }
 
-func (self *SearchHelper) DisplaySearchPrompt(context types.ISearchableContext) {
+func (self *SearchHelper) DisplaySearchStatus(context types.ISearchableContext) {
 	state := self.searchState()
 
 	state.Context = context
-	searchString := context.GetSearchString()
 
+	self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
 	_ = context.GetView().SelectCurrentSearchResult()
-
-	promptView := self.promptView()
-	promptView.ClearTextArea()
-	promptView.TextArea.TypeString(searchString)
-	promptView.RenderTextArea()
 }
 
 func (self *SearchHelper) searchState() *types.SearchState {
@@ -199,15 +198,15 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) {
 	}
 }
 
-func (self *SearchHelper) DisplaySearchInfoIfSearching(c types.Context) {
+func (self *SearchHelper) DisplaySearchStatusIfSearching(c types.Context) {
 	if searchableContext, ok := c.(types.ISearchableContext); ok {
 		if searchableContext.IsSearching() {
-			self.DisplaySearchPrompt(searchableContext)
+			self.DisplaySearchStatus(searchableContext)
 		}
 	}
 	if filterableContext, ok := c.(types.IFilterableContext); ok {
 		if filterableContext.IsFiltering() {
-			self.DisplayFilterPrompt(filterableContext)
+			self.DisplayFilterStatus(filterableContext)
 		}
 	}
 }
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index e59d5e4df..62b9c0015 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -539,6 +539,7 @@ type TranslationSet struct {
 	SearchPrefix                        string
 	FilterPrefix                        string
 	ExitSearchMode                      string
+	ExitTextFilterMode                  string
 	Actions                             Actions
 	Bisect                              Bisect
 }
@@ -1226,6 +1227,7 @@ func EnglishTranslationSet() TranslationSet {
 		CopyPatchToClipboard:                "Copy patch to clipboard",
 		NoMatchesFor:                        "No matches for '%s' %s",
 		ExitSearchMode:                      "%s: Exit search mode",
+		ExitTextFilterMode:                  "%s: Exit filter mode",
 		MatchesFor:                          "matches for '%s' (%d of %d) %s", // lowercase because it's after other text
 		SearchKeybindings:                   "%s: Next match, %s: Previous match, %s: Exit search mode",
 		SearchPrefix:                        "Search: ",

From 9df634f13f6dd8f1e766e81c8ad2ff036df8fe20 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 3 Jun 2023 14:11:03 +1000
Subject: [PATCH 12/20] Color view frame differently when searching/filtering

Given that we now persist search/filter states even after a side context loses focus, we need to make it really
clear to the user that the context is currently being searched/filtered
---
 docs/Config.md                               |  3 ++
 pkg/config/user_config.go                    | 38 ++++++++++----------
 pkg/gui/controllers/helpers/search_helper.go | 14 ++++++++
 pkg/gui/views.go                             | 11 +++---
 pkg/theme/theme.go                           |  4 +++
 5 files changed, 46 insertions(+), 24 deletions(-)

diff --git a/docs/Config.md b/docs/Config.md
index 86681264c..08e82b5ab 100644
--- a/docs/Config.md
+++ b/docs/Config.md
@@ -36,6 +36,9 @@ gui:
       - bold
     inactiveBorderColor:
       - white
+    searchingActiveBorderColor:
+      - cyan
+      - bold
     optionsTextColor:
       - blue
     selectedLineBgColor:
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index c3e4fc07f..8faff4326 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -60,15 +60,16 @@ type GuiConfig struct {
 }
 
 type ThemeConfig struct {
-	ActiveBorderColor         []string `yaml:"activeBorderColor"`
-	InactiveBorderColor       []string `yaml:"inactiveBorderColor"`
-	OptionsTextColor          []string `yaml:"optionsTextColor"`
-	SelectedLineBgColor       []string `yaml:"selectedLineBgColor"`
-	SelectedRangeBgColor      []string `yaml:"selectedRangeBgColor"`
-	CherryPickedCommitBgColor []string `yaml:"cherryPickedCommitBgColor"`
-	CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor"`
-	UnstagedChangesColor      []string `yaml:"unstagedChangesColor"`
-	DefaultFgColor            []string `yaml:"defaultFgColor"`
+	ActiveBorderColor          []string `yaml:"activeBorderColor"`
+	InactiveBorderColor        []string `yaml:"inactiveBorderColor"`
+	SearchingActiveBorderColor []string `yaml:"searchingActiveBorderColor"`
+	OptionsTextColor           []string `yaml:"optionsTextColor"`
+	SelectedLineBgColor        []string `yaml:"selectedLineBgColor"`
+	SelectedRangeBgColor       []string `yaml:"selectedRangeBgColor"`
+	CherryPickedCommitBgColor  []string `yaml:"cherryPickedCommitBgColor"`
+	CherryPickedCommitFgColor  []string `yaml:"cherryPickedCommitFgColor"`
+	UnstagedChangesColor       []string `yaml:"unstagedChangesColor"`
+	DefaultFgColor             []string `yaml:"defaultFgColor"`
 }
 
 type CommitLengthConfig struct {
@@ -409,15 +410,16 @@ func GetDefaultConfig() *UserConfig {
 			TimeFormat:               "02 Jan 06",
 			ShortTimeFormat:          time.Kitchen,
 			Theme: ThemeConfig{
-				ActiveBorderColor:         []string{"green", "bold"},
-				InactiveBorderColor:       []string{"default"},
-				OptionsTextColor:          []string{"blue"},
-				SelectedLineBgColor:       []string{"blue"},
-				SelectedRangeBgColor:      []string{"blue"},
-				CherryPickedCommitBgColor: []string{"cyan"},
-				CherryPickedCommitFgColor: []string{"blue"},
-				UnstagedChangesColor:      []string{"red"},
-				DefaultFgColor:            []string{"default"},
+				ActiveBorderColor:          []string{"green", "bold"},
+				SearchingActiveBorderColor: []string{"cyan", "bold"},
+				InactiveBorderColor:        []string{"default"},
+				OptionsTextColor:           []string{"blue"},
+				SelectedLineBgColor:        []string{"blue"},
+				SelectedRangeBgColor:       []string{"blue"},
+				CherryPickedCommitBgColor:  []string{"cyan"},
+				CherryPickedCommitFgColor:  []string{"blue"},
+				UnstagedChangesColor:       []string{"red"},
+				DefaultFgColor:             []string{"default"},
 			},
 			CommitLength:                CommitLengthConfig{Show: true},
 			SkipNoStagedFilesWarning:    false,
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
index 7d552a373..9f0e51e88 100644
--- a/pkg/gui/controllers/helpers/search_helper.go
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -201,11 +201,13 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) {
 func (self *SearchHelper) DisplaySearchStatusIfSearching(c types.Context) {
 	if searchableContext, ok := c.(types.ISearchableContext); ok {
 		if searchableContext.IsSearching() {
+			self.setSearchingFrameColor()
 			self.DisplaySearchStatus(searchableContext)
 		}
 	}
 	if filterableContext, ok := c.(types.IFilterableContext); ok {
 		if filterableContext.IsFiltering() {
+			self.setSearchingFrameColor()
 			self.DisplayFilterStatus(filterableContext)
 		}
 	}
@@ -232,6 +234,18 @@ func (self *SearchHelper) CancelSearchIfSearching(c types.Context) {
 }
 
 func (self *SearchHelper) HidePrompt() {
+	self.setNonSearchingFrameColor()
+
 	state := self.searchState()
 	state.Context = nil
 }
+
+func (self *SearchHelper) setSearchingFrameColor() {
+	self.c.GocuiGui().SelFgColor = theme.SearchingActiveBorderColor
+	self.c.GocuiGui().SelFrameColor = theme.SearchingActiveBorderColor
+}
+
+func (self *SearchHelper) setNonSearchingFrameColor() {
+	self.c.GocuiGui().SelFgColor = theme.ActiveBorderColor
+	self.c.GocuiGui().SelFrameColor = theme.ActiveBorderColor
+}
diff --git a/pkg/gui/views.go b/pkg/gui/views.go
index 043acdaed..15bd3c867 100644
--- a/pkg/gui/views.go
+++ b/pkg/gui/views.go
@@ -91,10 +91,14 @@ func (gui *Gui) createAllViews() error {
 	gui.Views.Options.Frame = false
 
 	gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
-	gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
+	gui.Views.SearchPrefix.FgColor = gocui.ColorCyan
 	gui.Views.SearchPrefix.Frame = false
 	gui.c.SetViewContent(gui.Views.SearchPrefix, gui.Tr.SearchPrefix)
 
+	gui.Views.Search.BgColor = gocui.ColorDefault
+	gui.Views.Search.FgColor = gocui.ColorCyan
+	gui.Views.Search.Editable = true
+	gui.Views.Search.Frame = false
 	gui.Views.Search.Editor = gocui.EditorFunc(gui.searchEditor)
 
 	gui.Views.Stash.Title = gui.c.Tr.StashTitle
@@ -143,11 +147,6 @@ func (gui *Gui) createAllViews() error {
 
 	gui.Views.Status.Title = gui.c.Tr.StatusTitle
 
-	gui.Views.Search.BgColor = gocui.ColorDefault
-	gui.Views.Search.FgColor = gocui.ColorGreen
-	gui.Views.Search.Editable = true
-	gui.Views.Search.Frame = false
-
 	gui.Views.AppStatus.BgColor = gocui.ColorDefault
 	gui.Views.AppStatus.FgColor = gocui.ColorCyan
 	gui.Views.AppStatus.Visible = false
diff --git a/pkg/theme/theme.go b/pkg/theme/theme.go
index bb6ab43de..0a1624029 100644
--- a/pkg/theme/theme.go
+++ b/pkg/theme/theme.go
@@ -19,6 +19,9 @@ var (
 	// InactiveBorderColor is the border color of the inactive active frames
 	InactiveBorderColor gocui.Attribute
 
+	// FilteredActiveBorderColor is the border color of the active frame, when it's being searched/filtered
+	SearchingActiveBorderColor gocui.Attribute
+
 	// GocuiSelectedLineBgColor is the background color for the selected line in gocui
 	GocuiSelectedLineBgColor gocui.Attribute
 
@@ -44,6 +47,7 @@ var (
 func UpdateTheme(themeConfig config.ThemeConfig) {
 	ActiveBorderColor = GetGocuiStyle(themeConfig.ActiveBorderColor)
 	InactiveBorderColor = GetGocuiStyle(themeConfig.InactiveBorderColor)
+	SearchingActiveBorderColor = GetGocuiStyle(themeConfig.SearchingActiveBorderColor)
 	SelectedLineBgColor = GetTextStyle(themeConfig.SelectedLineBgColor, true)
 	SelectedRangeBgColor = GetTextStyle(themeConfig.SelectedRangeBgColor, true)
 

From 7d7399a89f18fe58389ecdcd1f69f4494c6567e4 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 3 Jun 2023 14:56:15 +1000
Subject: [PATCH 13/20] Support case sensitive filtering

---
 pkg/gui/context/filtered_list.go |  2 +-
 pkg/utils/search.go              | 25 ++++++++++++++++++++++---
 2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go
index bffd0eddb..ce2e12590 100644
--- a/pkg/gui/context/filtered_list.go
+++ b/pkg/gui/context/filtered_list.go
@@ -66,7 +66,7 @@ func (self *FilteredList[T]) applyFilter() {
 }
 
 func (self *FilteredList[T]) match(haystack string, needle string) bool {
-	return utils.CaseInsensitiveContains(haystack, needle)
+	return utils.CaseAwareContains(haystack, needle)
 }
 
 func (self *FilteredList[T]) UnfilteredIndex(index int) int {
diff --git a/pkg/utils/search.go b/pkg/utils/search.go
index e90d2b5b0..14b3d7b3e 100644
--- a/pkg/utils/search.go
+++ b/pkg/utils/search.go
@@ -21,9 +21,28 @@ func FuzzySearch(needle string, haystack []string) []string {
 	})
 }
 
-func CaseInsensitiveContains(a, b string) bool {
+func CaseAwareContains(haystack, needle string) bool {
+	// if needle contains an uppercase letter, we'll do a case sensitive search
+	if ContainsUppercase(needle) {
+		return strings.Contains(haystack, needle)
+	}
+
+	return CaseInsensitiveContains(haystack, needle)
+}
+
+func ContainsUppercase(s string) bool {
+	for _, r := range s {
+		if r >= 'A' && r <= 'Z' {
+			return true
+		}
+	}
+
+	return false
+}
+
+func CaseInsensitiveContains(haystack, needle string) bool {
 	return strings.Contains(
-		strings.ToLower(a),
-		strings.ToLower(b),
+		strings.ToLower(haystack),
+		strings.ToLower(needle),
 	)
 }

From 261f30f49c552e3abe3bb7378208b0b355595c86 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 3 Jun 2023 16:10:53 +1000
Subject: [PATCH 14/20] Add integration tests for searching/filtering

---
 pkg/gui/context.go                            |   3 +-
 pkg/gui/controllers/helpers/search_helper.go  |  11 +-
 pkg/integration/components/int_matcher.go     |  12 +-
 pkg/integration/components/menu_driver.go     |  12 ++
 pkg/integration/components/view_driver.go     |  61 ++++++--
 pkg/integration/tests/commit/search.go        |   1 +
 .../filter_and_search/filter_commit_files.go  |  84 ++++++++++
 .../tests/filter_and_search/filter_files.go   |  76 +++++++++
 .../tests/filter_and_search/filter_menu.go    |  48 ++++++
 .../tests/filter_and_search/nested_filter.go  | 147 ++++++++++++++++++
 .../nested_filter_transient.go                | 105 +++++++++++++
 pkg/integration/tests/test_list.go            |   6 +
 12 files changed, 540 insertions(+), 26 deletions(-)
 create mode 100644 pkg/integration/tests/filter_and_search/filter_commit_files.go
 create mode 100644 pkg/integration/tests/filter_and_search/filter_files.go
 create mode 100644 pkg/integration/tests/filter_and_search/filter_menu.go
 create mode 100644 pkg/integration/tests/filter_and_search/nested_filter.go
 create mode 100644 pkg/integration/tests/filter_and_search/nested_filter_transient.go

diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index fde888a1c..38e1212c7 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -201,7 +201,6 @@ func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLos
 	view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
 
 	if opts.NewContextKey != context.SEARCH_CONTEXT_KEY {
-		self.gui.helpers.Search.HidePrompt()
 		if c.GetKind() == types.MAIN_CONTEXT || c.GetKind() == types.TEMPORARY_POPUP {
 			self.gui.helpers.Search.CancelSearchIfSearching(c)
 		}
@@ -235,7 +234,7 @@ func (self *ContextMgr) ActivateContext(c types.Context, opts types.OnFocusOpts)
 		return err
 	}
 
-	self.gui.helpers.Search.DisplaySearchStatusIfSearching(c)
+	self.gui.helpers.Search.RenderSearchStatus(c)
 
 	desiredTitle := c.Title()
 	if desiredTitle != "" {
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
index 9f0e51e88..b244f20e4 100644
--- a/pkg/gui/controllers/helpers/search_helper.go
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/jesseduffield/gocui"
+	"github.com/jesseduffield/lazygit/pkg/gui/context"
 	"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
 	"github.com/jesseduffield/lazygit/pkg/gui/types"
 	"github.com/jesseduffield/lazygit/pkg/theme"
@@ -198,19 +199,27 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) {
 	}
 }
 
-func (self *SearchHelper) DisplaySearchStatusIfSearching(c types.Context) {
+func (self *SearchHelper) RenderSearchStatus(c types.Context) {
+	if c.GetKey() == context.SEARCH_CONTEXT_KEY {
+		return
+	}
+
 	if searchableContext, ok := c.(types.ISearchableContext); ok {
 		if searchableContext.IsSearching() {
 			self.setSearchingFrameColor()
 			self.DisplaySearchStatus(searchableContext)
+			return
 		}
 	}
 	if filterableContext, ok := c.(types.IFilterableContext); ok {
 		if filterableContext.IsFiltering() {
 			self.setSearchingFrameColor()
 			self.DisplayFilterStatus(filterableContext)
+			return
 		}
 	}
+
+	self.HidePrompt()
 }
 
 func (self *SearchHelper) CancelSearchIfSearching(c types.Context) {
diff --git a/pkg/integration/components/int_matcher.go b/pkg/integration/components/int_matcher.go
index c80a60c85..4cfd0f958 100644
--- a/pkg/integration/components/int_matcher.go
+++ b/pkg/integration/components/int_matcher.go
@@ -10,9 +10,9 @@ type IntMatcher struct {
 
 func (self *IntMatcher) EqualsInt(target int) *IntMatcher {
 	self.appendRule(matcherRule[int]{
-		name: fmt.Sprintf("equals '%d'", target),
+		name: fmt.Sprintf("equals %d", target),
 		testFn: func(value int) (bool, string) {
-			return value == target, fmt.Sprintf("Expected '%d' to equal '%d'", value, target)
+			return value == target, fmt.Sprintf("Expected %d to equal %d", value, target)
 		},
 	})
 
@@ -21,9 +21,9 @@ func (self *IntMatcher) EqualsInt(target int) *IntMatcher {
 
 func (self *IntMatcher) GreaterThan(target int) *IntMatcher {
 	self.appendRule(matcherRule[int]{
-		name: fmt.Sprintf("greater than '%d'", target),
+		name: fmt.Sprintf("greater than %d", target),
 		testFn: func(value int) (bool, string) {
-			return value > target, fmt.Sprintf("Expected '%d' to greater than '%d'", value, target)
+			return value > target, fmt.Sprintf("Expected %d to greater than %d", value, target)
 		},
 	})
 
@@ -32,9 +32,9 @@ func (self *IntMatcher) GreaterThan(target int) *IntMatcher {
 
 func (self *IntMatcher) LessThan(target int) *IntMatcher {
 	self.appendRule(matcherRule[int]{
-		name: fmt.Sprintf("less than '%d'", target),
+		name: fmt.Sprintf("less than %d", target),
 		testFn: func(value int) (bool, string) {
-			return value < target, fmt.Sprintf("Expected '%d' to less than '%d'", value, target)
+			return value < target, fmt.Sprintf("Expected %d to less than %d", value, target)
 		},
 	})
 
diff --git a/pkg/integration/components/menu_driver.go b/pkg/integration/components/menu_driver.go
index ac620f5a4..4e0b0f6da 100644
--- a/pkg/integration/components/menu_driver.go
+++ b/pkg/integration/components/menu_driver.go
@@ -48,6 +48,18 @@ func (self *MenuDriver) TopLines(matchers ...*TextMatcher) *MenuDriver {
 	return self
 }
 
+func (self *MenuDriver) Filter(text string) *MenuDriver {
+	self.getViewDriver().FilterOrSearch(text)
+
+	return self
+}
+
+func (self *MenuDriver) LineCount(matcher *IntMatcher) *MenuDriver {
+	self.getViewDriver().LineCount(matcher)
+
+	return self
+}
+
 func (self *MenuDriver) checkNecessaryChecksCompleted() {
 	if !self.hasCheckedTitle {
 		self.t.Fail("You must check the title of a menu popup by calling Title() before calling Confirm()/Cancel().")
diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go
index db7e76134..2c4a23572 100644
--- a/pkg/integration/components/view_driver.go
+++ b/pkg/integration/components/view_driver.go
@@ -66,7 +66,7 @@ func (self *ViewDriver) Title(expected *TextMatcher) *ViewDriver {
 // If you only care about a subset of lines, use the ContainsLines method instead.
 func (self *ViewDriver) Lines(matchers ...*TextMatcher) *ViewDriver {
 	self.validateMatchersPassed(matchers)
-	self.LineCount(len(matchers))
+	self.LineCount(EqualsInt(len(matchers)))
 
 	return self.assertLines(0, matchers...)
 }
@@ -470,33 +470,60 @@ func (self *ViewDriver) IsEmpty() *ViewDriver {
 	return self
 }
 
-func (self *ViewDriver) LineCount(expectedCount int) *ViewDriver {
-	if expectedCount == 0 {
-		return self.IsEmpty()
-	}
-
+func (self *ViewDriver) LineCount(matcher *IntMatcher) *ViewDriver {
 	view := self.getView()
 
 	self.t.assertWithRetries(func() (bool, string) {
-		lines := view.BufferLines()
-		return len(lines) == expectedCount, fmt.Sprintf("unexpected number of lines in view '%s'. Expected %d, got %d", view.Name(), expectedCount, len(lines))
+		lineCount := self.getLineCount()
+		ok, _ := matcher.test(lineCount)
+		return ok, fmt.Sprintf("unexpected number of lines in view '%s'. Expected %s, got %d", view.Name(), matcher.name(), lineCount)
 	})
 
+	return self
+}
+
+func (self *ViewDriver) getLineCount() int {
+	// can't rely entirely on view.BufferLines because it returns 1 even if there's nothing in the view
+	if strings.TrimSpace(self.getView().Buffer()) == "" {
+		return 0
+	}
+
+	view := self.getView()
+	return len(view.BufferLines())
+}
+
+func (self *ViewDriver) IsVisible() *ViewDriver {
 	self.t.assertWithRetries(func() (bool, string) {
-		lines := view.BufferLines()
-
-		// if the view has a single blank line (often the case) we want to treat that as having no lines
-		if len(lines) == 1 && expectedCount == 1 {
-			actual := strings.TrimSpace(view.Buffer())
-			return actual != "", fmt.Sprintf("unexpected number of lines in view '%s'. Expected 1, got 0", view.Name())
-		}
-
-		return len(lines) == expectedCount, fmt.Sprintf("unexpected number of lines in view '%s'. Expected %d, got %d", view.Name(), expectedCount, len(lines))
+		return self.getView().Visible, fmt.Sprintf("%s: Expected view to be visible, but it was not", self.context)
 	})
 
 	return self
 }
 
+func (self *ViewDriver) IsInvisible() *ViewDriver {
+	self.t.assertWithRetries(func() (bool, string) {
+		return !self.getView().Visible, fmt.Sprintf("%s: Expected view to be visible, but it was not", self.context)
+	})
+
+	return self
+}
+
+// will filter or search depending on whether the view supports filtering/searching
+func (self *ViewDriver) FilterOrSearch(text string) *ViewDriver {
+	self.IsFocused()
+
+	self.Press(self.t.keys.Universal.StartSearch).
+		Tap(func() {
+			self.t.ExpectSearch().
+				Type(text).
+				Confirm()
+
+			self.t.Views().Search().IsVisible().Content(Contains(fmt.Sprintf("matches for '%s'", text)))
+		})
+
+	return self
+}
+
 // for when you want to make some assertion unrelated to the current view
 // without breaking the method chain
 func (self *ViewDriver) Tap(f func()) *ViewDriver {
diff --git a/pkg/integration/tests/commit/search.go b/pkg/integration/tests/commit/search.go
index 1d9390dc7..c0a9dfd0b 100644
--- a/pkg/integration/tests/commit/search.go
+++ b/pkg/integration/tests/commit/search.go
@@ -42,6 +42,7 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
 			Press(keys.Universal.StartSearch).
 			Tap(func() {
 				t.ExpectSearch().
+					Clear().
 					Type("o").
 					Confirm()
 
diff --git a/pkg/integration/tests/filter_and_search/filter_commit_files.go b/pkg/integration/tests/filter_and_search/filter_commit_files.go
new file mode 100644
index 000000000..a1a39f1f4
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_commit_files.go
@@ -0,0 +1,84 @@
+package filter_and_search
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FilterCommitFiles = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Basic commit file filtering by text",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.CreateDir("folder1")
+		shell.CreateFileAndAdd("folder1/apple-grape", "apple-grape")
+		shell.CreateFileAndAdd("folder1/apple-orange", "apple-orange")
+		shell.CreateFileAndAdd("folder1/grape-orange", "grape-orange")
+		shell.Commit("first commit")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains(`first commit`).IsSelected(),
+			).
+			Press(keys.Universal.Confirm)
+
+		t.Views().CommitFiles().
+			IsFocused().
+			Lines(
+				Contains(`folder1`).IsSelected(),
+				Contains(`apple-grape`),
+				Contains(`apple-orange`),
+				Contains(`grape-orange`),
+			).
+			Press(keys.Files.ToggleTreeView).
+			Lines(
+				Contains(`folder1/apple-grape`).IsSelected(),
+				Contains(`folder1/apple-orange`),
+				Contains(`folder1/grape-orange`),
+			).
+			FilterOrSearch("apple").
+			Lines(
+				Contains(`folder1/apple-grape`).IsSelected(),
+				Contains(`folder1/apple-orange`),
+			).
+			Press(keys.Files.ToggleTreeView).
+			// filter still applies when we toggle tree view
+			Lines(
+				Contains(`folder1`),
+				Contains(`apple-grape`).IsSelected(),
+				Contains(`apple-orange`),
+			).
+			Press(keys.Files.ToggleTreeView).
+			Lines(
+				Contains(`folder1/apple-grape`).IsSelected(),
+				Contains(`folder1/apple-orange`),
+			).
+			NavigateToLine(Contains(`folder1/apple-orange`)).
+			Press(keys.Universal.Return).
+			Lines(
+				Contains(`folder1/apple-grape`),
+				// selection is retained after escaping filter mode
+				Contains(`folder1/apple-orange`).IsSelected(),
+				Contains(`folder1/grape-orange`),
+			).
+			Tap(func() {
+				t.Views().Search().IsInvisible()
+			}).
+			Press(keys.Files.ToggleTreeView).
+			Lines(
+				Contains(`folder1`),
+				Contains(`apple-grape`),
+				Contains(`apple-orange`).IsSelected(),
+				Contains(`grape-orange`),
+			).
+			FilterOrSearch("folder1/grape").
+			Lines(
+				// first item is always selected after filtering
+				Contains(`folder1`).IsSelected(),
+				Contains(`grape-orange`),
+			)
+	},
+})
diff --git a/pkg/integration/tests/filter_and_search/filter_files.go b/pkg/integration/tests/filter_and_search/filter_files.go
new file mode 100644
index 000000000..5a029b146
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_files.go
@@ -0,0 +1,76 @@
+package filter_and_search
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FilterFiles = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Basic file filtering by text",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.CreateDir("folder1")
+		shell.CreateFile("folder1/apple-grape", "apple-grape")
+		shell.CreateFile("folder1/apple-orange", "apple-orange")
+		shell.CreateFile("folder1/grape-orange", "grape-orange")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Files().
+			Focus().
+			Lines(
+				Contains(`folder1`).IsSelected(),
+				Contains(`apple-grape`),
+				Contains(`apple-orange`),
+				Contains(`grape-orange`),
+			).
+			Press(keys.Files.ToggleTreeView).
+			Lines(
+				Contains(`folder1/apple-grape`).IsSelected(),
+				Contains(`folder1/apple-orange`),
+				Contains(`folder1/grape-orange`),
+			).
+			FilterOrSearch("apple").
+			Lines(
+				Contains(`folder1/apple-grape`).IsSelected(),
+				Contains(`folder1/apple-orange`),
+			).
+			Press(keys.Files.ToggleTreeView).
+			// filter still applies when we toggle tree view
+			Lines(
+				Contains(`folder1`),
+				Contains(`apple-grape`).IsSelected(),
+				Contains(`apple-orange`),
+			).
+			Press(keys.Files.ToggleTreeView).
+			Lines(
+				Contains(`folder1/apple-grape`).IsSelected(),
+				Contains(`folder1/apple-orange`),
+			).
+			NavigateToLine(Contains(`folder1/apple-orange`)).
+			Press(keys.Universal.Return).
+			Lines(
+				Contains(`folder1/apple-grape`),
+				// selection is retained after escaping filter mode
+				Contains(`folder1/apple-orange`).IsSelected(),
+				Contains(`folder1/grape-orange`),
+			).
+			Tap(func() {
+				t.Views().Search().IsInvisible()
+			}).
+			Press(keys.Files.ToggleTreeView).
+			Lines(
+				Contains(`folder1`),
+				Contains(`apple-grape`),
+				Contains(`apple-orange`).IsSelected(),
+				Contains(`grape-orange`),
+			).
+			FilterOrSearch("folder1/grape").
+			Lines(
+				// first item is always selected after filtering
+				Contains(`folder1`).IsSelected(),
+				Contains(`grape-orange`),
+			)
+	},
+})
diff --git a/pkg/integration/tests/filter_and_search/filter_menu.go b/pkg/integration/tests/filter_and_search/filter_menu.go
new file mode 100644
index 000000000..5dc15b663
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_menu.go
@@ -0,0 +1,48 @@
+package filter_and_search
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FilterMenu = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Filtering the keybindings menu",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.CreateFile("myfile", "myfile")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Files().
+			IsFocused().
+			Lines(
+				Contains(`??`).Contains(`myfile`).IsSelected(),
+			).
+			Press(keys.Universal.OptionMenu).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Keybindings")).
+					Filter("Toggle staged").
+					Lines(
+						// menu has filtered down to the one item that matches the filter
+						Contains(`Toggle staged`).IsSelected(),
+					).
+					Confirm()
+			})
+
+		t.Views().Files().
+			IsFocused().
+			Lines(
+				// file has been staged
+				Contains(`A `).Contains(`myfile`).IsSelected(),
+			).
+			// Upon opening the menu again, the filter should have been reset
+			Press(keys.Universal.OptionMenu).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Keybindings")).
+					LineCount(GreaterThan(1))
+			})
+	},
+})
diff --git a/pkg/integration/tests/filter_and_search/nested_filter.go b/pkg/integration/tests/filter_and_search/nested_filter.go
new file mode 100644
index 000000000..7dad9d61a
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/nested_filter.go
@@ -0,0 +1,147 @@
+package filter_and_search
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var NestedFilter = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Filter in the several nested panels and verify the filters are preserved as you escape back to the surface",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		// need to create some branches, each with their own commits
+		shell.NewBranch("branch-gold")
+		shell.CreateFileAndAdd("apple", "apple")
+		shell.CreateFileAndAdd("orange", "orange")
+		shell.CreateFileAndAdd("grape", "grape")
+		shell.Commit("commit-knife")
+
+		shell.NewBranch("branch-silver")
+		shell.UpdateFileAndAdd("apple", "apple-2")
+		shell.UpdateFileAndAdd("orange", "orange-2")
+		shell.UpdateFileAndAdd("grape", "grape-2")
+		shell.Commit("commit-spoon")
+
+		shell.NewBranch("branch-bronze")
+		shell.UpdateFileAndAdd("apple", "apple-3")
+		shell.UpdateFileAndAdd("orange", "orange-3")
+		shell.UpdateFileAndAdd("grape", "grape-3")
+		shell.Commit("commit-fork")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Branches().
+			Focus().
+			Lines(
+				Contains(`branch-bronze`).IsSelected(),
+				Contains(`branch-silver`),
+				Contains(`branch-gold`),
+			).
+			FilterOrSearch("sil").
+			Lines(
+				Contains(`branch-silver`).IsSelected(),
+			).
+			PressEnter()
+
+		t.Views().SubCommits().
+			IsFocused().
+			Lines(
+				Contains(`commit-spoon`).IsSelected(),
+				Contains(`commit-knife`),
+			).
+			FilterOrSearch("knife").
+			Lines(
+				// sub-commits view searches, it doesn't filter, so we haven't filtered down the list
+				Contains(`commit-spoon`),
+				Contains(`commit-knife`).IsSelected(),
+			).
+			PressEnter()
+
+		t.Views().CommitFiles().
+			IsFocused().
+			Lines(
+				Contains(`apple`).IsSelected(),
+				Contains(`grape`),
+				Contains(`orange`),
+			).
+			FilterOrSearch("grape").
+			Lines(
+				Contains(`grape`).IsSelected(),
+			).
+			PressEnter()
+
+		t.Views().PatchBuilding().
+			IsFocused().
+			FilterOrSearch("newline").
+			SelectedLine(Contains("No newline at end of file")).
+			PressEscape(). // cancel search
+			Tap(func() {
+				t.Views().Search().IsInvisible()
+			}).
+			// escape to commit-files view
+			PressEscape()
+
+		t.Views().CommitFiles().
+			IsFocused().
+			Lines(
+				Contains(`grape`).IsSelected(),
+			).
+			Tap(func() {
+				t.Views().Search().IsVisible().Content(Contains("matches for 'grape'"))
+			}).
+			// cancel search
+			PressEscape().
+			Tap(func() {
+				t.Views().Search().IsInvisible()
+			}).
+			Lines(
+				Contains(`apple`),
+				Contains(`grape`).IsSelected(),
+				Contains(`orange`),
+			).
+			// escape to sub-commits view
+			PressEscape()
+
+		t.Views().SubCommits().
+			IsFocused().
+			Lines(
+				Contains(`commit-spoon`),
+				Contains(`commit-knife`).IsSelected(),
+			).
+			Tap(func() {
+				t.Views().Search().IsVisible().Content(Contains("matches for 'knife'"))
+			}).
+			// cancel search
+			PressEscape().
+			Tap(func() {
+				t.Views().Search().IsInvisible()
+			}).
+			Lines(
+				Contains(`commit-spoon`),
+				// still selected
+				Contains(`commit-knife`).IsSelected(),
+			).
+			// escape to branches view
+			PressEscape()
+
+		t.Views().Branches().
+			IsFocused().
+			Lines(
+				Contains(`branch-silver`).IsSelected(),
+			).
+			Tap(func() {
+				t.Views().Search().IsVisible().Content(Contains("matches for 'sil'"))
+			}).
+			// cancel search
+			PressEscape().
+			Tap(func() {
+				t.Views().Search().IsInvisible()
+			}).
+			Lines(
+				Contains(`branch-bronze`),
+				Contains(`branch-silver`).IsSelected(),
+				Contains(`branch-gold`),
+			)
+	},
+})
diff --git a/pkg/integration/tests/filter_and_search/nested_filter_transient.go b/pkg/integration/tests/filter_and_search/nested_filter_transient.go
new file mode 100644
index 000000000..300519784
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/nested_filter_transient.go
@@ -0,0 +1,105 @@
+package filter_and_search
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+// This one requires some explanation: the sub-commits and diff-file contexts are
+// 'transient' in that they are spawned inside a window when you need them, but
+// can be relocated elsewhere if you need them somewhere else. So for example if
+// I hit enter on a branch I'll see the sub-commits view, but if I then navigate
+// to the reflog context and hit enter on a reflog, the sub-commits view is moved
+// to the reflog window. This is because we re-use the same view (it's a limitation
+// that would be nice to remove in the future).
+// Nonetheless, we need to ensure that upon moving the view, the filter is cancelled.
+
+var NestedFilterTransient = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Filter in a transient panel (sub-commits and diff-files) and ensure filter is cancelled when the panel is moved",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		// need to create some branches, each with their own commits
+		shell.NewBranch("mybranch")
+		shell.CreateFileAndAdd("file-one", "file-one")
+		shell.CreateFileAndAdd("file-two", "file-two")
+		shell.Commit("commit-one")
+		shell.EmptyCommit("commit-two")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Branches().
+			Focus().
+			Lines(
+				Contains(`mybranch`).IsSelected(),
+			).
+			PressEnter()
+
+		t.Views().SubCommits().
+			IsFocused().
+			Lines(
+				Contains(`commit-two`).IsSelected(),
+				Contains(`commit-one`),
+			).
+			FilterOrSearch("one").
+			Lines(
+				Contains(`commit-two`),
+				Contains(`commit-one`).IsSelected(),
+			)
+
+		t.Views().ReflogCommits().
+			Focus().
+			SelectedLine(Contains("commit: commit-two")).
+			PressEnter()
+
+		t.Views().SubCommits().
+			IsFocused().
+			// the search on the sub-commits context has been cancelled
+			Lines(
+				Contains(`commit-two`).IsSelected(),
+				Contains(`commit-one`),
+			).
+			Tap(func() {
+				t.Views().Search().IsInvisible()
+			}).
+			NavigateToLine(Contains("commit-one")).
+			PressEnter()
+
+		// Now let's test the commit files context
+		t.Views().CommitFiles().
+			IsFocused().
+			Lines(
+				Contains(`file-one`).IsSelected(),
+				Contains(`file-two`),
+			).
+			FilterOrSearch("one").
+			Lines(
+				Contains(`file-one`).IsSelected(),
+			)
+
+		t.Views().Branches().
+			Focus().
+			SelectedLine(Contains("mybranch")).
+			PressEnter()
+
+		t.Views().SubCommits().
+			IsFocused().
+			Lines(
+				Contains(`commit-two`).IsSelected(),
+				Contains(`commit-one`),
+			).
+			NavigateToLine(Contains("commit-one")).
+			PressEnter()
+
+		t.Views().CommitFiles().
+			IsFocused().
+			// the search on the commit-files context has been cancelled
+			Lines(
+				Contains(`file-one`).IsSelected(),
+				Contains(`file-two`),
+			).
+			Tap(func() {
+				t.Views().Search().IsInvisible()
+			})
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 9665f616e..e3a6e51dd 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -13,6 +13,7 @@ import (
 	"github.com/jesseduffield/lazygit/pkg/integration/tests/custom_commands"
 	"github.com/jesseduffield/lazygit/pkg/integration/tests/diff"
 	"github.com/jesseduffield/lazygit/pkg/integration/tests/file"
+	"github.com/jesseduffield/lazygit/pkg/integration/tests/filter_and_search"
 	"github.com/jesseduffield/lazygit/pkg/integration/tests/filter_by_path"
 	"github.com/jesseduffield/lazygit/pkg/integration/tests/interactive_rebase"
 	"github.com/jesseduffield/lazygit/pkg/integration/tests/misc"
@@ -94,6 +95,11 @@ var tests = []*components.IntegrationTest{
 	file.DiscardUnstagedFileChanges,
 	file.Gitignore,
 	file.RememberCommitMessageAfterFail,
+	filter_and_search.FilterCommitFiles,
+	filter_and_search.FilterFiles,
+	filter_and_search.FilterMenu,
+	filter_and_search.NestedFilter,
+	filter_and_search.NestedFilterTransient,
 	filter_by_path.CliArg,
 	filter_by_path.SelectFile,
 	filter_by_path.TypeFile,

From cd989d8ebeb923af74e8074cee09863b50862410 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 3 Jun 2023 19:55:17 +1000
Subject: [PATCH 15/20] Fix escape logic for remote branches

The remote branches controller was using its own escape method meaning it didn't go through the flow of cancelling
an active filter. It's now using the same approach as the sub-commits and commit-files contexts: defining a parent
context to return to upon hittin escape.
---
 .../controllers/remote_branches_controller.go |  9 ---
 pkg/gui/controllers/remotes_controller.go     | 10 ++--
 .../filter_remote_branches.go                 | 59 +++++++++++++++++++
 pkg/integration/tests/test_list.go            |  1 +
 4 files changed, 66 insertions(+), 13 deletions(-)
 create mode 100644 pkg/integration/tests/filter_and_search/filter_remote_branches.go

diff --git a/pkg/gui/controllers/remote_branches_controller.go b/pkg/gui/controllers/remote_branches_controller.go
index c1cc9d46b..b26230d90 100644
--- a/pkg/gui/controllers/remote_branches_controller.go
+++ b/pkg/gui/controllers/remote_branches_controller.go
@@ -59,11 +59,6 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts)
 			Handler:     self.checkSelected(self.setAsUpstream),
 			Description: self.c.Tr.SetAsUpstream,
 		},
-		{
-			Key:         opts.GetKey(opts.Config.Universal.Return),
-			Handler:     self.escape,
-			Description: self.c.Tr.ReturnToRemotesList,
-		},
 		{
 			Key:         opts.GetKey(opts.Config.Commits.ViewResetOptions),
 			Handler:     self.checkSelected(self.createResetMenu),
@@ -115,10 +110,6 @@ func (self *RemoteBranchesController) checkSelected(callback func(*models.Remote
 	}
 }
 
-func (self *RemoteBranchesController) escape() error {
-	return self.c.PushContext(self.c.Contexts().Remotes)
-}
-
 func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch) error {
 	message := fmt.Sprintf("%s '%s'?", self.c.Tr.DeleteRemoteBranchMessage, selectedBranch.FullName())
 
diff --git a/pkg/gui/controllers/remotes_controller.go b/pkg/gui/controllers/remotes_controller.go
index 283119886..b6d9a963b 100644
--- a/pkg/gui/controllers/remotes_controller.go
+++ b/pkg/gui/controllers/remotes_controller.go
@@ -104,14 +104,16 @@ func (self *RemotesController) enter(remote *models.Remote) error {
 	if len(remote.Branches) == 0 {
 		newSelectedLine = -1
 	}
-	self.c.Contexts().RemoteBranches.SetSelectedLineIdx(newSelectedLine)
-	self.c.Contexts().RemoteBranches.SetTitleRef(remote.Name)
+	remoteBranchesContext := self.c.Contexts().RemoteBranches
+	remoteBranchesContext.SetSelectedLineIdx(newSelectedLine)
+	remoteBranchesContext.SetTitleRef(remote.Name)
+	remoteBranchesContext.SetParentContext(self.Context())
 
-	if err := self.c.PostRefreshUpdate(self.c.Contexts().RemoteBranches); err != nil {
+	if err := self.c.PostRefreshUpdate(remoteBranchesContext); err != nil {
 		return err
 	}
 
-	return self.c.PushContext(self.c.Contexts().RemoteBranches)
+	return self.c.PushContext(remoteBranchesContext)
 }
 
 func (self *RemotesController) add() error {
diff --git a/pkg/integration/tests/filter_and_search/filter_remote_branches.go b/pkg/integration/tests/filter_and_search/filter_remote_branches.go
new file mode 100644
index 000000000..11cfea30b
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_remote_branches.go
@@ -0,0 +1,59 @@
+package filter_and_search
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FilterRemoteBranches = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Filtering remote branches",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.NewBranch("branch-apple")
+		shell.EmptyCommit("commit-one")
+		shell.NewBranch("branch-grape")
+		shell.NewBranch("branch-orange")
+
+		shell.CloneIntoRemote("origin")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Remotes().
+			Focus().
+			Lines(
+				Contains(`origin`).IsSelected(),
+			).
+			PressEnter()
+
+		t.Views().RemoteBranches().
+			IsFocused().
+			Lines(
+				Contains(`branch-apple`).IsSelected(),
+				Contains(`branch-grape`),
+				Contains(`branch-orange`),
+			).
+			FilterOrSearch("grape").
+			Lines(
+				Contains(`branch-grape`).IsSelected(),
+			).
+			// cancel the filter
+			PressEscape().
+			Tap(func() {
+				t.Views().Search().IsInvisible()
+			}).
+			Lines(
+				Contains(`branch-apple`),
+				Contains(`branch-grape`).IsSelected(),
+				Contains(`branch-orange`),
+			).
+			// return to remotes view
+			PressEscape()
+
+		t.Views().Remotes().
+			IsFocused().
+			Lines(
+				Contains(`origin`).IsSelected(),
+			)
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index e3a6e51dd..9aab7ea43 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -98,6 +98,7 @@ var tests = []*components.IntegrationTest{
 	filter_and_search.FilterCommitFiles,
 	filter_and_search.FilterFiles,
 	filter_and_search.FilterMenu,
+	filter_and_search.FilterRemoteBranches,
 	filter_and_search.NestedFilter,
 	filter_and_search.NestedFilterTransient,
 	filter_by_path.CliArg,

From 8e46b8a275587ef442cca8497d9c5056556bfa93 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Mon, 26 Jun 2023 11:15:47 +1000
Subject: [PATCH 16/20] Use searching, not filtering, in file tree views

There's more work to be done to support filtering for these views so we're sticking with searching for now
---
 pkg/gui/context/commit_files_context.go       | 34 ++++++----------
 pkg/gui/context/working_tree_context.go       | 39 ++++++-------------
 .../switch_to_diff_files_controller.go        |  2 +-
 .../filter_and_search/filter_commit_files.go  |  2 +-
 .../tests/filter_and_search/filter_files.go   |  2 +-
 .../tests/filter_and_search/nested_filter.go  |  4 ++
 .../nested_filter_transient.go                |  5 ++-
 7 files changed, 33 insertions(+), 55 deletions(-)

diff --git a/pkg/gui/context/commit_files_context.go b/pkg/gui/context/commit_files_context.go
index 54d2a02e3..5cb11d9cc 100644
--- a/pkg/gui/context/commit_files_context.go
+++ b/pkg/gui/context/commit_files_context.go
@@ -10,10 +10,10 @@ import (
 )
 
 type CommitFilesContext struct {
-	*FilteredList[*models.CommitFile]
 	*filetree.CommitFileTreeViewModel
 	*ListContextTrait
 	*DynamicTitleBuilder
+	*SearchTrait
 }
 
 var (
@@ -22,13 +22,8 @@ var (
 )
 
 func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
-	filteredList := NewFilteredList(
-		func() []*models.CommitFile { return c.Model().CommitFiles },
-		func(file *models.CommitFile) []string { return []string{file.GetPath()} },
-	)
-
 	viewModel := filetree.NewCommitFileTreeViewModel(
-		func() []*models.CommitFile { return filteredList.GetFilteredList() },
+		func() []*models.CommitFile { return c.Model().CommitFiles },
 		c.Log,
 		c.UserConfig.Gui.ShowFileTree,
 	)
@@ -44,10 +39,10 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
 		})
 	}
 
-	return &CommitFilesContext{
-		FilteredList:            filteredList,
+	ctx := &CommitFilesContext{
 		CommitFileTreeViewModel: viewModel,
 		DynamicTitleBuilder:     NewDynamicTitleBuilder(c.Tr.CommitFilesDynamicTitle),
+		SearchTrait:             NewSearchTrait(c),
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(
 				NewBaseContext(NewBaseContextOpts{
@@ -64,6 +59,13 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
 			c:                 c,
 		},
 	}
+
+	ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error {
+		ctx.GetList().SetSelectedLineIdx(selectedLineIdx)
+		return ctx.HandleFocus(types.OnFocusOpts{})
+	}))
+
+	return ctx
 }
 
 func (self *CommitFilesContext) GetSelectedItemId() string {
@@ -78,17 +80,3 @@ func (self *CommitFilesContext) GetSelectedItemId() string {
 func (self *CommitFilesContext) GetDiffTerminals() []string {
 	return []string{self.GetRef().RefName()}
 }
-
-// used for type switch
-func (self *CommitFilesContext) IsFilterableContext() {}
-
-// TODO: see if we can just call SetTree() within HandleRender(). It doesn't seem
-// right that we need to imperatively refresh the view model like this
-func (self *CommitFilesContext) SetFilter(filter string) {
-	self.FilteredList.SetFilter(filter)
-	self.SetTree()
-}
-
-func (self *CommitFilesContext) ClearFilter() {
-	self.SetFilter("")
-}
diff --git a/pkg/gui/context/working_tree_context.go b/pkg/gui/context/working_tree_context.go
index ee053eea1..107228ee8 100644
--- a/pkg/gui/context/working_tree_context.go
+++ b/pkg/gui/context/working_tree_context.go
@@ -9,24 +9,16 @@ import (
 )
 
 type WorkingTreeContext struct {
-	*FilteredList[*models.File]
 	*filetree.FileTreeViewModel
 	*ListContextTrait
+	*SearchTrait
 }
 
-var (
-	_ types.IListContext       = (*WorkingTreeContext)(nil)
-	_ types.IFilterableContext = (*WorkingTreeContext)(nil)
-)
+var _ types.IListContext = (*WorkingTreeContext)(nil)
 
 func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
-	filteredList := NewFilteredList(
-		func() []*models.File { return c.Model().Files },
-		func(file *models.File) []string { return []string{file.GetPath()} },
-	)
-
 	viewModel := filetree.NewFileTreeViewModel(
-		func() []*models.File { return filteredList.GetFilteredList() },
+		func() []*models.File { return c.Model().Files },
 		c.Log,
 		c.UserConfig.Gui.ShowFileTree,
 	)
@@ -38,8 +30,8 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
 		})
 	}
 
-	return &WorkingTreeContext{
-		FilteredList:      filteredList,
+	ctx := &WorkingTreeContext{
+		SearchTrait:       NewSearchTrait(c),
 		FileTreeViewModel: viewModel,
 		ListContextTrait: &ListContextTrait{
 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
@@ -54,6 +46,13 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
 			c:                 c,
 		},
 	}
+
+	ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error {
+		ctx.GetList().SetSelectedLineIdx(selectedLineIdx)
+		return ctx.HandleFocus(types.OnFocusOpts{})
+	}))
+
+	return ctx
 }
 
 func (self *WorkingTreeContext) GetSelectedItemId() string {
@@ -64,17 +63,3 @@ func (self *WorkingTreeContext) GetSelectedItemId() string {
 
 	return item.ID()
 }
-
-// used for type switch
-func (self *WorkingTreeContext) IsFilterableContext() {}
-
-// TODO: see if we can just call SetTree() within HandleRender(). It doesn't seem
-// right that we need to imperatively refresh the view model like this
-func (self *WorkingTreeContext) SetFilter(filter string) {
-	self.FilteredList.SetFilter(filter)
-	self.SetTree()
-}
-
-func (self *WorkingTreeContext) ClearFilter() {
-	self.SetFilter("")
-}
diff --git a/pkg/gui/controllers/switch_to_diff_files_controller.go b/pkg/gui/controllers/switch_to_diff_files_controller.go
index 767520d20..971efb7a1 100644
--- a/pkg/gui/controllers/switch_to_diff_files_controller.go
+++ b/pkg/gui/controllers/switch_to_diff_files_controller.go
@@ -83,7 +83,7 @@ func (self *SwitchToDiffFilesController) viewFiles(opts SwitchToCommitFilesConte
 	diffFilesContext.SetCanRebase(opts.CanRebase)
 	diffFilesContext.SetParentContext(opts.Context)
 	diffFilesContext.SetWindowName(opts.Context.GetWindowName())
-	diffFilesContext.ClearFilter()
+	diffFilesContext.ClearSearchString()
 
 	if err := self.c.Refresh(types.RefreshOptions{
 		Scope: []types.RefreshableView{types.COMMIT_FILES},
diff --git a/pkg/integration/tests/filter_and_search/filter_commit_files.go b/pkg/integration/tests/filter_and_search/filter_commit_files.go
index a1a39f1f4..953eaf34d 100644
--- a/pkg/integration/tests/filter_and_search/filter_commit_files.go
+++ b/pkg/integration/tests/filter_and_search/filter_commit_files.go
@@ -8,7 +8,7 @@ import (
 var FilterCommitFiles = NewIntegrationTest(NewIntegrationTestArgs{
 	Description:  "Basic commit file filtering by text",
 	ExtraCmdArgs: []string{},
-	Skip:         false,
+	Skip:         true, // skipping until we have implemented file view filtering
 	SetupConfig:  func(config *config.AppConfig) {},
 	SetupRepo: func(shell *Shell) {
 		shell.CreateDir("folder1")
diff --git a/pkg/integration/tests/filter_and_search/filter_files.go b/pkg/integration/tests/filter_and_search/filter_files.go
index 5a029b146..6eae90c18 100644
--- a/pkg/integration/tests/filter_and_search/filter_files.go
+++ b/pkg/integration/tests/filter_and_search/filter_files.go
@@ -8,7 +8,7 @@ import (
 var FilterFiles = NewIntegrationTest(NewIntegrationTestArgs{
 	Description:  "Basic file filtering by text",
 	ExtraCmdArgs: []string{},
-	Skip:         false,
+	Skip:         true, // Skipping until we have implemented file view filtering
 	SetupConfig:  func(config *config.AppConfig) {},
 	SetupRepo: func(shell *Shell) {
 		shell.CreateDir("folder1")
diff --git a/pkg/integration/tests/filter_and_search/nested_filter.go b/pkg/integration/tests/filter_and_search/nested_filter.go
index 7dad9d61a..6444ad523 100644
--- a/pkg/integration/tests/filter_and_search/nested_filter.go
+++ b/pkg/integration/tests/filter_and_search/nested_filter.go
@@ -67,7 +67,9 @@ var NestedFilter = NewIntegrationTest(NewIntegrationTestArgs{
 			).
 			FilterOrSearch("grape").
 			Lines(
+				Contains(`apple`),
 				Contains(`grape`).IsSelected(),
+				Contains(`orange`),
 			).
 			PressEnter()
 
@@ -85,7 +87,9 @@ var NestedFilter = NewIntegrationTest(NewIntegrationTestArgs{
 		t.Views().CommitFiles().
 			IsFocused().
 			Lines(
+				Contains(`apple`),
 				Contains(`grape`).IsSelected(),
+				Contains(`orange`),
 			).
 			Tap(func() {
 				t.Views().Search().IsVisible().Content(Contains("matches for 'grape'"))
diff --git a/pkg/integration/tests/filter_and_search/nested_filter_transient.go b/pkg/integration/tests/filter_and_search/nested_filter_transient.go
index 300519784..bf04406f5 100644
--- a/pkg/integration/tests/filter_and_search/nested_filter_transient.go
+++ b/pkg/integration/tests/filter_and_search/nested_filter_transient.go
@@ -72,9 +72,10 @@ var NestedFilterTransient = NewIntegrationTest(NewIntegrationTestArgs{
 				Contains(`file-one`).IsSelected(),
 				Contains(`file-two`),
 			).
-			FilterOrSearch("one").
+			FilterOrSearch("two").
 			Lines(
-				Contains(`file-one`).IsSelected(),
+				Contains(`file-one`),
+				Contains(`file-two`).IsSelected(),
 			)
 
 		t.Views().Branches().

From db7b472f9a8cdeff5c5b466707dd857538664a56 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sat, 27 May 2023 20:57:17 +1000
Subject: [PATCH 17/20] Update cheatsheets

---
 docs/keybindings/Keybindings_en.md    | 17 +++++++++++++++--
 docs/keybindings/Keybindings_ja.md    | 17 +++++++++++++++--
 docs/keybindings/Keybindings_ko.md    | 17 +++++++++++++++--
 docs/keybindings/Keybindings_nl.md    | 17 +++++++++++++++--
 docs/keybindings/Keybindings_pl.md    | 17 +++++++++++++++--
 docs/keybindings/Keybindings_zh-CN.md | 17 +++++++++++++++--
 docs/keybindings/Keybindings_zh-TW.md | 17 +++++++++++++++--
 7 files changed, 105 insertions(+), 14 deletions(-)

diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md
index 4dee9cbad..d00277ddd 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>,</kbd>: Previous page
   <kbd>.</kbd>: Next page
   <kbd>&lt;</kbd>: Scroll to top
-  <kbd>/</kbd>: Start search
   <kbd>&gt;</kbd>: Scroll to bottom
+  <kbd>/</kbd>: Search the current view
   <kbd>H</kbd>: Scroll left
   <kbd>L</kbd>: Scroll right
   <kbd>]</kbd>: Next tab
@@ -56,6 +56,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>a</kbd>: Toggle all files included in patch
   <kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
   <kbd>`</kbd>: Toggle file tree view
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Commit summary
@@ -96,6 +97,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Copy commit (cherry-pick)
   <kbd>C</kbd>: Copy commit range (cherry-pick)
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Confirmation panel
@@ -129,6 +131,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>`</kbd>: Toggle file tree view
   <kbd>M</kbd>: Open external merge tool (git mergetool)
   <kbd>f</kbd>: Fetch
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Local branches
@@ -152,6 +155,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: Rename branch
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: View commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Main panel (merging)
@@ -190,6 +194,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Edit file
   <kbd>&lt;space&gt;</kbd>: Add/Remove line(s) to patch
   <kbd>&lt;esc&gt;</kbd>: Exit custom patch builder
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Main panel (staging)
@@ -211,6 +216,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Commit changes
   <kbd>w</kbd>: Commit changes without pre-commit hook
   <kbd>C</kbd>: Commit changes using git editor
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Menu
@@ -218,6 +224,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: Execute
   <kbd>&lt;esc&gt;</kbd>: Close
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Reflog
@@ -233,6 +240,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Copy commit range (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: View commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Remote branches
@@ -245,9 +253,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>r</kbd>: Rebase checked-out branch onto this branch
   <kbd>d</kbd>: Delete branch
   <kbd>u</kbd>: Set as upstream of checked-out branch
-  <kbd>&lt;esc&gt;</kbd>: Return to remotes list
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: View commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Remotes
@@ -257,6 +265,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Add new remote
   <kbd>d</kbd>: Remove remote
   <kbd>e</kbd>: Edit remote
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Stash
@@ -268,6 +277,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: New branch
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Status
@@ -293,6 +303,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Copy commit range (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Submodules
@@ -306,6 +317,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Update submodule URL
   <kbd>i</kbd>: Initialize submodule
   <kbd>b</kbd>: View bulk submodule options
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Tags
@@ -317,4 +329,5 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Create tag
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: View commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md
index 1fbef475a..dadf78f4a 100644
--- a/docs/keybindings/Keybindings_ja.md
+++ b/docs/keybindings/Keybindings_ja.md
@@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>,</kbd>: 前のページ
   <kbd>.</kbd>: 次のページ
   <kbd>&lt;</kbd>: 最上部までスクロール
-  <kbd>/</kbd>: 検索を開始
   <kbd>&gt;</kbd>: 最下部までスクロール
+  <kbd>/</kbd>: 検索を開始
   <kbd>H</kbd>: 左スクロール
   <kbd>L</kbd>: 右スクロール
   <kbd>]</kbd>: 次のタブ
@@ -53,6 +53,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 新しいブランチを作成
   <kbd>r</kbd>: Stashを変更
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Sub-commits
@@ -68,6 +69,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
+  <kbd>/</kbd>: 検索を開始
 </pre>
 
 ## コミット
@@ -101,6 +103,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: コミットをコピー (cherry-pick)
   <kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
+  <kbd>/</kbd>: 検索を開始
 </pre>
 
 ## コミットファイル
@@ -115,6 +118,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>a</kbd>: Toggle all files included in patch
   <kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
   <kbd>`</kbd>: ファイルツリーの表示を切り替え
+  <kbd>/</kbd>: 検索を開始
 </pre>
 
 ## コミットメッセージ
@@ -135,6 +139,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: サブモジュールのURLを更新
   <kbd>i</kbd>: サブモジュールを初期化
   <kbd>b</kbd>: View bulk submodule options
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## ステータス
@@ -156,6 +161,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: タグを作成
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## ファイル
@@ -182,6 +188,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>`</kbd>: ファイルツリーの表示を切り替え
   <kbd>M</kbd>: Git mergetoolを開く
   <kbd>f</kbd>: Fetch
+  <kbd>/</kbd>: 検索を開始
 </pre>
 
 ## ブランチ
@@ -205,6 +212,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: ブランチ名を変更
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## メインパネル (Merging)
@@ -243,6 +251,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: ファイルを編集
   <kbd>&lt;space&gt;</kbd>: 行をパッチに追加/削除
   <kbd>&lt;esc&gt;</kbd>: Exit custom patch builder
+  <kbd>/</kbd>: 検索を開始
 </pre>
 
 ## メインパネル (Staging)
@@ -264,6 +273,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: 変更をコミット
   <kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
   <kbd>C</kbd>: gitエディタを使用して変更をコミット
+  <kbd>/</kbd>: 検索を開始
 </pre>
 
 ## メニュー
@@ -271,6 +281,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: 実行
   <kbd>&lt;esc&gt;</kbd>: 閉じる
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## リモート
@@ -280,6 +291,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: リモートを新規追加
   <kbd>d</kbd>: リモートを削除
   <kbd>e</kbd>: リモートを編集
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## リモートブランチ
@@ -292,9 +304,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>r</kbd>: Rebase checked-out branch onto this branch
   <kbd>d</kbd>: ブランチを削除
   <kbd>u</kbd>: Set as upstream of checked-out branch
-  <kbd>&lt;esc&gt;</kbd>: リモート一覧に戻る
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 参照ログ
@@ -310,6 +322,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 確認パネル
diff --git a/docs/keybindings/Keybindings_ko.md b/docs/keybindings/Keybindings_ko.md
index 54c09e7e8..e9067ca96 100644
--- a/docs/keybindings/Keybindings_ko.md
+++ b/docs/keybindings/Keybindings_ko.md
@@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>,</kbd>: 이전 페이지
   <kbd>.</kbd>: 다음 페이지
   <kbd>&lt;</kbd>: 맨 위로 스크롤 
-  <kbd>/</kbd>: 검색 시작
   <kbd>&gt;</kbd>: 맨 아래로 스크롤 
+  <kbd>/</kbd>: 검색 시작
   <kbd>H</kbd>: 우 스크롤
   <kbd>L</kbd>: 좌 스크롤
   <kbd>]</kbd>: 이전 탭
@@ -57,6 +57,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: 커밋 보기
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Stash
@@ -68,6 +69,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 새 브랜치 생성
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Sub-commits
@@ -83,6 +85,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
+  <kbd>/</kbd>: 검색 시작
 </pre>
 
 ## 메뉴
@@ -90,6 +93,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: 실행
   <kbd>&lt;esc&gt;</kbd>: 닫기
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 메인 패널 (Merging)
@@ -128,6 +132,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: 파일 편집
   <kbd>&lt;space&gt;</kbd>: Line(s)을 패치에 추가/삭제
   <kbd>&lt;esc&gt;</kbd>: Exit custom patch builder
+  <kbd>/</kbd>: 검색 시작
 </pre>
 
 ## 메인 패널 (Staging)
@@ -149,6 +154,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: 커밋 변경내용
   <kbd>w</kbd>: Commit changes without pre-commit hook
   <kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
+  <kbd>/</kbd>: 검색 시작
 </pre>
 
 ## 브랜치
@@ -172,6 +178,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: 브랜치 이름 변경
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: 커밋 보기
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 상태
@@ -195,6 +202,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: 서브모듈의 URL을 수정
   <kbd>i</kbd>: 서브모듈 초기화
   <kbd>b</kbd>: View bulk submodule options
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 원격
@@ -204,6 +212,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 새로운 Remote 추가
   <kbd>d</kbd>: Remote를 삭제
   <kbd>e</kbd>: Remote를 수정
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 원격 브랜치
@@ -216,9 +225,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
   <kbd>d</kbd>: 브랜치 삭제
   <kbd>u</kbd>: Set as upstream of checked-out branch
-  <kbd>&lt;esc&gt;</kbd>: 원격목록으로 돌아가기
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: 커밋 보기
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 커밋
@@ -252,6 +261,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: 커밋을 복사 (cherry-pick)
   <kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
+  <kbd>/</kbd>: 검색 시작
 </pre>
 
 ## 커밋 파일
@@ -266,6 +276,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>a</kbd>: Toggle all files included in patch
   <kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
   <kbd>`</kbd>: 파일 트리뷰로 전환
+  <kbd>/</kbd>: 검색 시작
 </pre>
 
 ## 커밋메시지
@@ -284,6 +295,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 태그를 생성
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: 커밋 보기
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 파일
@@ -310,6 +322,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>`</kbd>: 파일 트리뷰로 전환
   <kbd>M</kbd>: Git mergetool를 열기
   <kbd>f</kbd>: Fetch
+  <kbd>/</kbd>: 검색 시작
 </pre>
 
 ## 확인 패널
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md
index 223317778..b8277bfab 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>,</kbd>: Vorige pagina
   <kbd>.</kbd>: Volgende pagina
   <kbd>&lt;</kbd>: Scroll naar boven
-  <kbd>/</kbd>: Start met zoeken
   <kbd>&gt;</kbd>: Scroll naar beneden
+  <kbd>/</kbd>: Start met zoeken
   <kbd>H</kbd>: Scroll left
   <kbd>L</kbd>: Scroll right
   <kbd>]</kbd>: Volgende tabblad
@@ -68,6 +68,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>`</kbd>: Toggle bestandsboom weergave
   <kbd>M</kbd>: Open external merge tool (git mergetool)
   <kbd>f</kbd>: Fetch
+  <kbd>/</kbd>: Start met zoeken
 </pre>
 
 ## Bevestigingspaneel
@@ -98,6 +99,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: Hernoem branch
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: Bekijk commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Commit bericht
@@ -119,6 +121,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>a</kbd>: Toggle all files included in patch
   <kbd>&lt;enter&gt;</kbd>: Enter bestand om geselecteerde regels toe te voegen aan de patch
   <kbd>`</kbd>: Toggle bestandsboom weergave
+  <kbd>/</kbd>: Start met zoeken
 </pre>
 
 ## Commits
@@ -152,6 +155,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Kopieer commit (cherry-pick)
   <kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
   <kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden
+  <kbd>/</kbd>: Start met zoeken
 </pre>
 
 ## Menu
@@ -159,6 +163,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: Uitvoeren
   <kbd>&lt;esc&gt;</kbd>: Sluiten
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Mergen
@@ -197,6 +202,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Verander bestand
   <kbd>&lt;space&gt;</kbd>: Voeg toe/verwijder lijn(en) in patch
   <kbd>&lt;esc&gt;</kbd>: Sluit lijn-bij-lijn modus
+  <kbd>/</kbd>: Start met zoeken
 </pre>
 
 ## Reflog
@@ -212,6 +218,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (gekopieerde) commits selectie
   <kbd>&lt;enter&gt;</kbd>: Bekijk commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Remote branches
@@ -224,9 +231,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>r</kbd>: Rebase branch
   <kbd>d</kbd>: Verwijder branch
   <kbd>u</kbd>: Stel in als upstream van uitgecheckte branch
-  <kbd>&lt;esc&gt;</kbd>: Ga terug naar remotes lijst
   <kbd>g</kbd>: Bekijk reset opties
   <kbd>&lt;enter&gt;</kbd>: Bekijk commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Remotes
@@ -236,6 +243,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Voeg een nieuwe remote toe
   <kbd>d</kbd>: Verwijder remote
   <kbd>e</kbd>: Wijzig remote
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Staging
@@ -257,6 +265,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Commit veranderingen
   <kbd>w</kbd>: Commit veranderingen zonder pre-commit hook
   <kbd>C</kbd>: Commit veranderingen met de git editor
+  <kbd>/</kbd>: Start met zoeken
 </pre>
 
 ## Stash
@@ -268,6 +277,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Nieuwe branch
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Status
@@ -293,6 +303,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (gekopieerde) commits selectie
   <kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden
+  <kbd>/</kbd>: Start met zoeken
 </pre>
 
 ## Submodules
@@ -306,6 +317,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Update submodule URL
   <kbd>i</kbd>: Initialiseer submodule
   <kbd>b</kbd>: Bekijk bulk submodule opties
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Tags
@@ -317,4 +329,5 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Creëer tag
   <kbd>g</kbd>: Bekijk reset opties
   <kbd>&lt;enter&gt;</kbd>: Bekijk commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md
index 7d2fef98b..59eebbe4a 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>,</kbd>: Previous page
   <kbd>.</kbd>: Next page
   <kbd>&lt;</kbd>: Scroll to top
-  <kbd>/</kbd>: Start search
   <kbd>&gt;</kbd>: Scroll to bottom
+  <kbd>/</kbd>: Search the current view
   <kbd>H</kbd>: Scroll left
   <kbd>L</kbd>: Scroll right
   <kbd>]</kbd>: Next tab
@@ -82,6 +82,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Kopiuj commit (przebieranie)
   <kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
   <kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Confirmation panel
@@ -112,6 +113,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: Rename branch
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: View commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Main panel (patch building)
@@ -127,6 +129,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Edytuj plik
   <kbd>&lt;space&gt;</kbd>: Add/Remove line(s) to patch
   <kbd>&lt;esc&gt;</kbd>: Wyście z trybu "linia po linii"
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Menu
@@ -134,6 +137,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: Wykonaj
   <kbd>&lt;esc&gt;</kbd>: Zamknij
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Pliki
@@ -160,6 +164,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>`</kbd>: Toggle file tree view
   <kbd>M</kbd>: Open external merge tool (git mergetool)
   <kbd>f</kbd>: Pobierz
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Pliki commita
@@ -174,6 +179,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>a</kbd>: Toggle all files included in patch
   <kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
   <kbd>`</kbd>: Toggle file tree view
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Poczekalnia
@@ -195,6 +201,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Zatwierdź zmiany
   <kbd>w</kbd>: Zatwierdź zmiany bez skryptu pre-commit
   <kbd>C</kbd>: Zatwierdź zmiany używając edytora
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Reflog
@@ -210,6 +217,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: View commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Remote branches
@@ -222,9 +230,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>r</kbd>: Zmiana bazy gałęzi
   <kbd>d</kbd>: Usuń gałąź
   <kbd>u</kbd>: Set as upstream of checked-out branch
-  <kbd>&lt;esc&gt;</kbd>: Wróć do listy repozytoriów zdalnych
   <kbd>g</kbd>: Wyświetl opcje resetu
   <kbd>&lt;enter&gt;</kbd>: View commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Remotes
@@ -234,6 +242,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Add new remote
   <kbd>d</kbd>: Remove remote
   <kbd>e</kbd>: Edit remote
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Scalanie
@@ -261,6 +270,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Nowa gałąź
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Status
@@ -286,6 +296,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
+  <kbd>/</kbd>: Search the current view
 </pre>
 
 ## Submodules
@@ -299,6 +310,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Update submodule URL
   <kbd>i</kbd>: Initialize submodule
   <kbd>b</kbd>: View bulk submodule options
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Tags
@@ -310,6 +322,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Create tag
   <kbd>g</kbd>: Wyświetl opcje resetu
   <kbd>&lt;enter&gt;</kbd>: View commits
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## Zwykłe
diff --git a/docs/keybindings/Keybindings_zh-CN.md b/docs/keybindings/Keybindings_zh-CN.md
index 6275f572d..b5097a60d 100644
--- a/docs/keybindings/Keybindings_zh-CN.md
+++ b/docs/keybindings/Keybindings_zh-CN.md
@@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>,</kbd>: 上一页
   <kbd>.</kbd>: 下一页
   <kbd>&lt;</kbd>: 滚动到顶部
-  <kbd>/</kbd>: 开始搜索
   <kbd>&gt;</kbd>: 滚动到底部
+  <kbd>/</kbd>: 开始搜索
   <kbd>H</kbd>: 向左滚动
   <kbd>L</kbd>: 向右滚动
   <kbd>]</kbd>: 下一个标签
@@ -57,6 +57,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: 复制提交范围(拣选)
   <kbd>&lt;c-r&gt;</kbd>: 重置已拣选(复制)的提交
   <kbd>&lt;enter&gt;</kbd>: 查看提交
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 分支页面
@@ -80,6 +81,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: 重命名分支
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: 查看提交
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 子提交
@@ -95,6 +97,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: 复制提交范围(拣选)
   <kbd>&lt;c-r&gt;</kbd>: 重置已拣选(复制)的提交
   <kbd>&lt;enter&gt;</kbd>: 查看提交的文件
+  <kbd>/</kbd>: 开始搜索
 </pre>
 
 ## 子模块
@@ -108,6 +111,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: 更新子模块 URL
   <kbd>i</kbd>: 初始化子模块
   <kbd>b</kbd>: 查看批量子模块选项
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 提交
@@ -141,6 +145,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: 复制提交(拣选)
   <kbd>C</kbd>: 复制提交范围(拣选)
   <kbd>&lt;enter&gt;</kbd>: 查看提交的文件
+  <kbd>/</kbd>: 开始搜索
 </pre>
 
 ## 提交文件
@@ -155,6 +160,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>a</kbd>: Toggle all files included in patch
   <kbd>&lt;enter&gt;</kbd>: 输入文件以将所选行添加到补丁中(或切换目录折叠)
   <kbd>`</kbd>: 切换文件树视图
+  <kbd>/</kbd>: 开始搜索
 </pre>
 
 ## 提交讯息
@@ -188,6 +194,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>`</kbd>: 切换文件树视图
   <kbd>M</kbd>: 打开外部合并工具 (git mergetool)
   <kbd>f</kbd>: 抓取
+  <kbd>/</kbd>: 开始搜索
 </pre>
 
 ## 构建补丁中
@@ -203,6 +210,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: 编辑文件
   <kbd>&lt;space&gt;</kbd>: 添加/移除 行到补丁
   <kbd>&lt;esc&gt;</kbd>: 退出逐行模式
+  <kbd>/</kbd>: 开始搜索
 </pre>
 
 ## 标签页面
@@ -214,6 +222,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 创建标签
   <kbd>g</kbd>: 查看重置选项
   <kbd>&lt;enter&gt;</kbd>: 查看提交
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 正在合并
@@ -251,6 +260,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: 提交更改
   <kbd>w</kbd>: 提交更改而无需预先提交钩子
   <kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
+  <kbd>/</kbd>: 开始搜索
 </pre>
 
 ## 正常
@@ -282,6 +292,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: 执行
   <kbd>&lt;esc&gt;</kbd>: 关闭
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 贮藏
@@ -293,6 +304,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 新分支
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: 查看提交的文件
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 远程分支
@@ -305,9 +317,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>r</kbd>: 将已检出的分支变基到该分支
   <kbd>d</kbd>: 删除分支
   <kbd>u</kbd>: 设置为检出分支的上游
-  <kbd>&lt;esc&gt;</kbd>: 返回远程仓库列表
   <kbd>g</kbd>: 查看重置选项
   <kbd>&lt;enter&gt;</kbd>: 查看提交
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 远程页面
@@ -317,4 +329,5 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 添加新的远程仓库
   <kbd>d</kbd>: 删除远程
   <kbd>e</kbd>: 编辑远程仓库
+  <kbd>/</kbd>: Filter the current view
 </pre>
diff --git a/docs/keybindings/Keybindings_zh-TW.md b/docs/keybindings/Keybindings_zh-TW.md
index cc30d6acd..a131a9766 100644
--- a/docs/keybindings/Keybindings_zh-TW.md
+++ b/docs/keybindings/Keybindings_zh-TW.md
@@ -36,8 +36,8 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>,</kbd>: 上一頁
   <kbd>.</kbd>: 下一頁
   <kbd>&lt;</kbd>: 捲動到頂部
-  <kbd>/</kbd>: 開始搜尋
   <kbd>&gt;</kbd>: 捲動到底部
+  <kbd>/</kbd>: 開始搜尋
   <kbd>H</kbd>: 向左捲動
   <kbd>L</kbd>: 向右捲動
   <kbd>]</kbd>: 下一個索引標籤
@@ -57,6 +57,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>C</kbd>: 複製提交範圍 (揀選)
   <kbd>&lt;c-r&gt;</kbd>: 重設選定的揀選 (複製) 提交
   <kbd>&lt;enter&gt;</kbd>: 檢視提交
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 主視窗 (一般)
@@ -101,6 +102,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>c</kbd>: 提交變更
   <kbd>w</kbd>: 沒有預提交 hook 就提交更改
   <kbd>C</kbd>: 使用 git 編輯器提交變更
+  <kbd>/</kbd>: 開始搜尋
 </pre>
 
 ## 主面板 (補丁生成)
@@ -116,6 +118,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>e</kbd>: 編輯檔案
   <kbd>&lt;space&gt;</kbd>: 向 (或從) 補丁中添加/刪除行
   <kbd>&lt;esc&gt;</kbd>: 退出自訂補丁建立器
+  <kbd>/</kbd>: 開始搜尋
 </pre>
 
 ## 功能表
@@ -123,6 +126,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: 執行
   <kbd>&lt;esc&gt;</kbd>: 關閉
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 子提交
@@ -138,6 +142,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>C</kbd>: 複製提交範圍 (揀選)
   <kbd>&lt;c-r&gt;</kbd>: 重設選定的揀選 (複製) 提交
   <kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
+  <kbd>/</kbd>: 開始搜尋
 </pre>
 
 ## 子模組
@@ -151,6 +156,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>e</kbd>: 更新子模組 URL
   <kbd>i</kbd>: 初始化子模組
   <kbd>b</kbd>: 查看批量子模組選項
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 提交
@@ -184,6 +190,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>c</kbd>: 複製提交 (揀選)
   <kbd>C</kbd>: 複製提交範圍 (揀選)
   <kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
+  <kbd>/</kbd>: 開始搜尋
 </pre>
 
 ## 提交摘要
@@ -205,6 +212,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>a</kbd>: 切換所有檔案是否包含在補丁中
   <kbd>&lt;enter&gt;</kbd>: 輸入檔案以將選定的行添加至補丁(或切換目錄折疊)
   <kbd>`</kbd>: 切換檔案樹狀視圖
+  <kbd>/</kbd>: 開始搜尋
 </pre>
 
 ## 收藏 (Stash)
@@ -216,6 +224,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>n</kbd>: 新分支
   <kbd>r</kbd>: 重新命名收藏
   <kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 本地分支
@@ -239,6 +248,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>R</kbd>: 重新命名分支
   <kbd>u</kbd>: 設定/取消設定上游
   <kbd>&lt;enter&gt;</kbd>: 檢視提交
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 標籤
@@ -250,6 +260,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>n</kbd>: 建立標籤
   <kbd>g</kbd>: 檢視重設選項
   <kbd>&lt;enter&gt;</kbd>: 檢視提交
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 檔案
@@ -276,6 +287,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>`</kbd>: 切換檔案樹狀視圖
   <kbd>M</kbd>: 開啟外部合併工具 (git mergetool)
   <kbd>f</kbd>: 擷取
+  <kbd>/</kbd>: 開始搜尋
 </pre>
 
 ## 狀態
@@ -302,6 +314,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>n</kbd>: 新增遠端
   <kbd>d</kbd>: 移除遠端
   <kbd>e</kbd>: 編輯遠端
+  <kbd>/</kbd>: Filter the current view
 </pre>
 
 ## 遠端分支
@@ -314,7 +327,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>r</kbd>: 將已檢出的分支變基至此分支
   <kbd>d</kbd>: 刪除分支
   <kbd>u</kbd>: 將此分支設為當前分支之上游
-  <kbd>&lt;esc&gt;</kbd>: 返回遠端列表
   <kbd>g</kbd>: 檢視重設選項
   <kbd>&lt;enter&gt;</kbd>: 檢視提交
+  <kbd>/</kbd>: Filter the current view
 </pre>

From b625eb5323e8842e7a5e12463384411a530f067f Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sun, 2 Jul 2023 15:11:49 +1000
Subject: [PATCH 18/20] Differentiate between different filter modes

We can filter by path, by file status, and by text.
---
 docs/keybindings/Keybindings_en.md      | 32 ++++----
 docs/keybindings/Keybindings_ja.md      | 16 ++--
 docs/keybindings/Keybindings_ko.md      | 16 ++--
 docs/keybindings/Keybindings_nl.md      | 18 ++---
 docs/keybindings/Keybindings_pl.md      | 32 ++++----
 docs/keybindings/Keybindings_ru.md      | 17 ++++-
 docs/keybindings/Keybindings_zh-CN.md   | 18 ++---
 docs/keybindings/Keybindings_zh-TW.md   | 16 ++--
 pkg/gui/controllers/files_controller.go |  2 +-
 pkg/i18n/dutch.go                       |  2 +-
 pkg/i18n/english.go                     | 97 +++++++++++++------------
 pkg/i18n/japanese.go                    |  2 +-
 pkg/i18n/korean.go                      |  2 +-
 pkg/i18n/polish.go                      |  2 +-
 pkg/i18n/russian.go                     |  2 +-
 pkg/i18n/traditional_chinese.go         |  2 +-
 16 files changed, 145 insertions(+), 131 deletions(-)

diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md
index d00277ddd..5e682be7e 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -37,7 +37,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>.</kbd>: Next page
   <kbd>&lt;</kbd>: Scroll to top
   <kbd>&gt;</kbd>: Scroll to bottom
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
   <kbd>H</kbd>: Scroll left
   <kbd>L</kbd>: Scroll right
   <kbd>]</kbd>: Next tab
@@ -56,7 +56,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>a</kbd>: Toggle all files included in patch
   <kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
   <kbd>`</kbd>: Toggle file tree view
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Commit summary
@@ -97,7 +97,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Copy commit (cherry-pick)
   <kbd>C</kbd>: Copy commit range (cherry-pick)
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Confirmation panel
@@ -113,7 +113,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>&lt;c-o&gt;</kbd>: Copy the file name to the clipboard
   <kbd>d</kbd>: View 'discard changes' options
   <kbd>&lt;space&gt;</kbd>: Toggle staged
-  <kbd>&lt;c-b&gt;</kbd>: Filter files (staged/unstaged)
+  <kbd>&lt;c-b&gt;</kbd>: Filter files by status
   <kbd>c</kbd>: Commit changes
   <kbd>w</kbd>: Commit changes without pre-commit hook
   <kbd>A</kbd>: Amend last commit
@@ -131,7 +131,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>`</kbd>: Toggle file tree view
   <kbd>M</kbd>: Open external merge tool (git mergetool)
   <kbd>f</kbd>: Fetch
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Local branches
@@ -155,7 +155,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: Rename branch
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: View commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Main panel (merging)
@@ -194,7 +194,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Edit file
   <kbd>&lt;space&gt;</kbd>: Add/Remove line(s) to patch
   <kbd>&lt;esc&gt;</kbd>: Exit custom patch builder
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Main panel (staging)
@@ -216,7 +216,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Commit changes
   <kbd>w</kbd>: Commit changes without pre-commit hook
   <kbd>C</kbd>: Commit changes using git editor
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Menu
@@ -224,7 +224,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: Execute
   <kbd>&lt;esc&gt;</kbd>: Close
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Reflog
@@ -240,7 +240,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Copy commit range (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: View commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Remote branches
@@ -255,7 +255,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>u</kbd>: Set as upstream of checked-out branch
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: View commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Remotes
@@ -265,7 +265,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Add new remote
   <kbd>d</kbd>: Remove remote
   <kbd>e</kbd>: Edit remote
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Stash
@@ -277,7 +277,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: New branch
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Status
@@ -303,7 +303,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Copy commit range (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Submodules
@@ -317,7 +317,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Update submodule URL
   <kbd>i</kbd>: Initialize submodule
   <kbd>b</kbd>: View bulk submodule options
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Tags
@@ -329,5 +329,5 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Create tag
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: View commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md
index dadf78f4a..787796679 100644
--- a/docs/keybindings/Keybindings_ja.md
+++ b/docs/keybindings/Keybindings_ja.md
@@ -53,7 +53,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 新しいブランチを作成
   <kbd>r</kbd>: Stashを変更
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Sub-commits
@@ -139,7 +139,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: サブモジュールのURLを更新
   <kbd>i</kbd>: サブモジュールを初期化
   <kbd>b</kbd>: View bulk submodule options
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## ステータス
@@ -161,7 +161,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: タグを作成
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## ファイル
@@ -212,7 +212,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: ブランチ名を変更
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## メインパネル (Merging)
@@ -281,7 +281,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: 実行
   <kbd>&lt;esc&gt;</kbd>: 閉じる
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## リモート
@@ -291,7 +291,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: リモートを新規追加
   <kbd>d</kbd>: リモートを削除
   <kbd>e</kbd>: リモートを編集
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## リモートブランチ
@@ -306,7 +306,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>u</kbd>: Set as upstream of checked-out branch
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 参照ログ
@@ -322,7 +322,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: コミットを閲覧
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 確認パネル
diff --git a/docs/keybindings/Keybindings_ko.md b/docs/keybindings/Keybindings_ko.md
index e9067ca96..3ebe1f3ad 100644
--- a/docs/keybindings/Keybindings_ko.md
+++ b/docs/keybindings/Keybindings_ko.md
@@ -57,7 +57,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: 커밋 보기
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Stash
@@ -69,7 +69,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 새 브랜치 생성
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: View selected item's files
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Sub-commits
@@ -93,7 +93,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: 실행
   <kbd>&lt;esc&gt;</kbd>: 닫기
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 메인 패널 (Merging)
@@ -178,7 +178,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: 브랜치 이름 변경
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: 커밋 보기
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 상태
@@ -202,7 +202,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: 서브모듈의 URL을 수정
   <kbd>i</kbd>: 서브모듈 초기화
   <kbd>b</kbd>: View bulk submodule options
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 원격
@@ -212,7 +212,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 새로운 Remote 추가
   <kbd>d</kbd>: Remote를 삭제
   <kbd>e</kbd>: Remote를 수정
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 원격 브랜치
@@ -227,7 +227,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>u</kbd>: Set as upstream of checked-out branch
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: 커밋 보기
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 커밋
@@ -295,7 +295,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 태그를 생성
   <kbd>g</kbd>: View reset options
   <kbd>&lt;enter&gt;</kbd>: 커밋 보기
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 파일
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md
index b8277bfab..feae84761 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -50,7 +50,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>&lt;c-o&gt;</kbd>: Kopieer de bestandsnaam naar het klembord
   <kbd>d</kbd>: Bekijk 'veranderingen ongedaan maken' opties
   <kbd>&lt;space&gt;</kbd>: Toggle staged
-  <kbd>&lt;c-b&gt;</kbd>: Filter files (staged/unstaged)
+  <kbd>&lt;c-b&gt;</kbd>: Filter files by status
   <kbd>c</kbd>: Commit veranderingen
   <kbd>w</kbd>: Commit veranderingen zonder pre-commit hook
   <kbd>A</kbd>: Wijzig laatste commit
@@ -99,7 +99,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: Hernoem branch
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: Bekijk commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Commit bericht
@@ -163,7 +163,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: Uitvoeren
   <kbd>&lt;esc&gt;</kbd>: Sluiten
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Mergen
@@ -218,7 +218,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (gekopieerde) commits selectie
   <kbd>&lt;enter&gt;</kbd>: Bekijk commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Remote branches
@@ -233,7 +233,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>u</kbd>: Stel in als upstream van uitgecheckte branch
   <kbd>g</kbd>: Bekijk reset opties
   <kbd>&lt;enter&gt;</kbd>: Bekijk commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Remotes
@@ -243,7 +243,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Voeg een nieuwe remote toe
   <kbd>d</kbd>: Verwijder remote
   <kbd>e</kbd>: Wijzig remote
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Staging
@@ -277,7 +277,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Nieuwe branch
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Status
@@ -317,7 +317,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Update submodule URL
   <kbd>i</kbd>: Initialiseer submodule
   <kbd>b</kbd>: Bekijk bulk submodule opties
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Tags
@@ -329,5 +329,5 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Creëer tag
   <kbd>g</kbd>: Bekijk reset opties
   <kbd>&lt;enter&gt;</kbd>: Bekijk commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md
index 59eebbe4a..c619bfff8 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -37,7 +37,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>.</kbd>: Next page
   <kbd>&lt;</kbd>: Scroll to top
   <kbd>&gt;</kbd>: Scroll to bottom
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
   <kbd>H</kbd>: Scroll left
   <kbd>L</kbd>: Scroll right
   <kbd>]</kbd>: Next tab
@@ -82,7 +82,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Kopiuj commit (przebieranie)
   <kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
   <kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Confirmation panel
@@ -113,7 +113,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: Rename branch
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: View commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Main panel (patch building)
@@ -129,7 +129,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Edytuj plik
   <kbd>&lt;space&gt;</kbd>: Add/Remove line(s) to patch
   <kbd>&lt;esc&gt;</kbd>: Wyście z trybu "linia po linii"
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Menu
@@ -137,7 +137,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: Wykonaj
   <kbd>&lt;esc&gt;</kbd>: Zamknij
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Pliki
@@ -146,7 +146,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>&lt;c-o&gt;</kbd>: Copy the file name to the clipboard
   <kbd>d</kbd>: Pokaż opcje porzucania zmian
   <kbd>&lt;space&gt;</kbd>: Przełącz stan poczekalni
-  <kbd>&lt;c-b&gt;</kbd>: Filter files (staged/unstaged)
+  <kbd>&lt;c-b&gt;</kbd>: Filter files by status
   <kbd>c</kbd>: Zatwierdź zmiany
   <kbd>w</kbd>: Zatwierdź zmiany bez skryptu pre-commit
   <kbd>A</kbd>: Zmień ostatni commit
@@ -164,7 +164,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>`</kbd>: Toggle file tree view
   <kbd>M</kbd>: Open external merge tool (git mergetool)
   <kbd>f</kbd>: Pobierz
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Pliki commita
@@ -179,7 +179,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>a</kbd>: Toggle all files included in patch
   <kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
   <kbd>`</kbd>: Toggle file tree view
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Poczekalnia
@@ -201,7 +201,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>c</kbd>: Zatwierdź zmiany
   <kbd>w</kbd>: Zatwierdź zmiany bez skryptu pre-commit
   <kbd>C</kbd>: Zatwierdź zmiany używając edytora
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Reflog
@@ -217,7 +217,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: View commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Remote branches
@@ -232,7 +232,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>u</kbd>: Set as upstream of checked-out branch
   <kbd>g</kbd>: Wyświetl opcje resetu
   <kbd>&lt;enter&gt;</kbd>: View commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Remotes
@@ -242,7 +242,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Add new remote
   <kbd>d</kbd>: Remove remote
   <kbd>e</kbd>: Edit remote
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Scalanie
@@ -270,7 +270,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Nowa gałąź
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Status
@@ -296,7 +296,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
   <kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
   <kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
-  <kbd>/</kbd>: Search the current view
+  <kbd>/</kbd>: Search the current view by text
 </pre>
 
 ## Submodules
@@ -310,7 +310,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: Update submodule URL
   <kbd>i</kbd>: Initialize submodule
   <kbd>b</kbd>: View bulk submodule options
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Tags
@@ -322,7 +322,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: Create tag
   <kbd>g</kbd>: Wyświetl opcje resetu
   <kbd>&lt;enter&gt;</kbd>: View commits
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Zwykłe
diff --git a/docs/keybindings/Keybindings_ru.md b/docs/keybindings/Keybindings_ru.md
index 85a823ea8..cfd05fec8 100644
--- a/docs/keybindings/Keybindings_ru.md
+++ b/docs/keybindings/Keybindings_ru.md
@@ -36,8 +36,8 @@ _Связки клавиш_
   <kbd>,</kbd>: Предыдущая страница
   <kbd>.</kbd>: Следующая страница
   <kbd>&lt;</kbd>: Пролистать наверх
-  <kbd>/</kbd>: Найти
   <kbd>&gt;</kbd>: Прокрутить вниз
+  <kbd>/</kbd>: Найти
   <kbd>H</kbd>: Прокрутить влево
   <kbd>L</kbd>: Прокрутить вправо
   <kbd>]</kbd>: Следующая вкладка
@@ -63,6 +63,7 @@ _Связки клавиш_
   <kbd>c</kbd>: Сохранить изменения
   <kbd>w</kbd>: Закоммитить изменения без предварительного хука коммита
   <kbd>C</kbd>: Сохранить изменения с помощью редактора git
+  <kbd>/</kbd>: Найти
 </pre>
 
 ## Главная панель (Обычный)
@@ -101,6 +102,7 @@ _Связки клавиш_
   <kbd>e</kbd>: Редактировать файл
   <kbd>&lt;space&gt;</kbd>: Добавить/удалить строку(и) для патча
   <kbd>&lt;esc&gt;</kbd>: Выйти из сборщика пользовательских патчей
+  <kbd>/</kbd>: Найти
 </pre>
 
 ## Журнал ссылок (Reflog)
@@ -116,6 +118,7 @@ _Связки клавиш_
   <kbd>C</kbd>: Скопировать несколько отобранных коммитов (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Сбросить отобранную (скопированную | cherry-picked) выборку коммитов
   <kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Коммиты
@@ -149,6 +152,7 @@ _Связки клавиш_
   <kbd>c</kbd>: Скопировать отобранные коммит (cherry-pick)
   <kbd>C</kbd>: Скопировать несколько отобранных коммитов (cherry-pick)
   <kbd>&lt;enter&gt;</kbd>: Просмотреть файлы выбранного элемента
+  <kbd>/</kbd>: Найти
 </pre>
 
 ## Локальные Ветки
@@ -172,6 +176,7 @@ _Связки клавиш_
   <kbd>R</kbd>: Переименовать ветку
   <kbd>u</kbd>: Установить/убрать upstream-ветку
   <kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Меню
@@ -179,6 +184,7 @@ _Связки клавиш_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: Выполнить
   <kbd>&lt;esc&gt;</kbd>: Закрыть
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Панель Подтверждения
@@ -201,6 +207,7 @@ _Связки клавиш_
   <kbd>C</kbd>: Скопировать несколько отобранных коммитов (cherry-pick)
   <kbd>&lt;c-r&gt;</kbd>: Сбросить отобранную (скопированную | cherry-picked) выборку коммитов
   <kbd>&lt;enter&gt;</kbd>: Просмотреть файлы выбранного элемента
+  <kbd>/</kbd>: Найти
 </pre>
 
 ## Подмодули
@@ -214,6 +221,7 @@ _Связки клавиш_
   <kbd>e</kbd>: Обновить URL подмодуля
   <kbd>i</kbd>: Инициализировать подмодуль
   <kbd>b</kbd>: Просмотреть параметры массового подмодуля
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Сводка коммита
@@ -235,6 +243,7 @@ _Связки клавиш_
   <kbd>a</kbd>: Переключить все файлы, включённые в патч
   <kbd>&lt;enter&gt;</kbd>: Введите файл, чтобы добавить выбранные строки в патч (или свернуть каталог переключения)
   <kbd>`</kbd>: Переключить вид дерева файлов
+  <kbd>/</kbd>: Найти
 </pre>
 
 ## Статус
@@ -256,6 +265,7 @@ _Связки клавиш_
   <kbd>n</kbd>: Создать тег
   <kbd>g</kbd>: Просмотреть параметры сброса
   <kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Удалённые ветки
@@ -268,9 +278,9 @@ _Связки клавиш_
   <kbd>r</kbd>: Перебазировать переключённую ветку на эту ветку
   <kbd>d</kbd>: Удалить ветку
   <kbd>u</kbd>: Установить как upstream-ветку переключённую ветку
-  <kbd>&lt;esc&gt;</kbd>: Вернуться к списку удалённых репозитории
   <kbd>g</kbd>: Просмотреть параметры сброса
   <kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Удалённые репозитории
@@ -280,6 +290,7 @@ _Связки клавиш_
   <kbd>n</kbd>: Добавить новую удалённую ветку
   <kbd>d</kbd>: Удалить удалённую ветку
   <kbd>e</kbd>: Редактировать удалённый репозитории
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## Файлы
@@ -306,6 +317,7 @@ _Связки клавиш_
   <kbd>`</kbd>: Переключить вид дерева файлов
   <kbd>M</kbd>: Открыть внешний инструмент слияния (git mergetool)
   <kbd>f</kbd>: Получить изменения
+  <kbd>/</kbd>: Найти
 </pre>
 
 ## Хранилище
@@ -317,4 +329,5 @@ _Связки клавиш_
   <kbd>n</kbd>: Новая ветка
   <kbd>r</kbd>: Переименовать хранилище
   <kbd>&lt;enter&gt;</kbd>: Просмотреть файлы выбранного элемента
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
diff --git a/docs/keybindings/Keybindings_zh-CN.md b/docs/keybindings/Keybindings_zh-CN.md
index b5097a60d..3274b1a0c 100644
--- a/docs/keybindings/Keybindings_zh-CN.md
+++ b/docs/keybindings/Keybindings_zh-CN.md
@@ -57,7 +57,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>C</kbd>: 复制提交范围(拣选)
   <kbd>&lt;c-r&gt;</kbd>: 重置已拣选(复制)的提交
   <kbd>&lt;enter&gt;</kbd>: 查看提交
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 分支页面
@@ -81,7 +81,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>R</kbd>: 重命名分支
   <kbd>u</kbd>: Set/Unset upstream
   <kbd>&lt;enter&gt;</kbd>: 查看提交
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 子提交
@@ -111,7 +111,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>e</kbd>: 更新子模块 URL
   <kbd>i</kbd>: 初始化子模块
   <kbd>b</kbd>: 查看批量子模块选项
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 提交
@@ -176,7 +176,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>&lt;c-o&gt;</kbd>: 将文件名复制到剪贴板
   <kbd>d</kbd>: 查看'放弃更改'选项
   <kbd>&lt;space&gt;</kbd>: 切换暂存状态
-  <kbd>&lt;c-b&gt;</kbd>: Filter files (staged/unstaged)
+  <kbd>&lt;c-b&gt;</kbd>: Filter files by status
   <kbd>c</kbd>: 提交更改
   <kbd>w</kbd>: 提交更改而无需预先提交钩子
   <kbd>A</kbd>: 修补最后一次提交
@@ -222,7 +222,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 创建标签
   <kbd>g</kbd>: 查看重置选项
   <kbd>&lt;enter&gt;</kbd>: 查看提交
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 正在合并
@@ -292,7 +292,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: 执行
   <kbd>&lt;esc&gt;</kbd>: 关闭
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 贮藏
@@ -304,7 +304,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 新分支
   <kbd>r</kbd>: Rename stash
   <kbd>&lt;enter&gt;</kbd>: 查看提交的文件
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 远程分支
@@ -319,7 +319,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>u</kbd>: 设置为检出分支的上游
   <kbd>g</kbd>: 查看重置选项
   <kbd>&lt;enter&gt;</kbd>: 查看提交
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 远程页面
@@ -329,5 +329,5 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
   <kbd>n</kbd>: 添加新的远程仓库
   <kbd>d</kbd>: 删除远程
   <kbd>e</kbd>: 编辑远程仓库
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
diff --git a/docs/keybindings/Keybindings_zh-TW.md b/docs/keybindings/Keybindings_zh-TW.md
index a131a9766..68859a23d 100644
--- a/docs/keybindings/Keybindings_zh-TW.md
+++ b/docs/keybindings/Keybindings_zh-TW.md
@@ -57,7 +57,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>C</kbd>: 複製提交範圍 (揀選)
   <kbd>&lt;c-r&gt;</kbd>: 重設選定的揀選 (複製) 提交
   <kbd>&lt;enter&gt;</kbd>: 檢視提交
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 主視窗 (一般)
@@ -126,7 +126,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
 <pre>
   <kbd>&lt;enter&gt;</kbd>: 執行
   <kbd>&lt;esc&gt;</kbd>: 關閉
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 子提交
@@ -156,7 +156,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>e</kbd>: 更新子模組 URL
   <kbd>i</kbd>: 初始化子模組
   <kbd>b</kbd>: 查看批量子模組選項
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 提交
@@ -224,7 +224,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>n</kbd>: 新分支
   <kbd>r</kbd>: 重新命名收藏
   <kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 本地分支
@@ -248,7 +248,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>R</kbd>: 重新命名分支
   <kbd>u</kbd>: 設定/取消設定上游
   <kbd>&lt;enter&gt;</kbd>: 檢視提交
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 標籤
@@ -260,7 +260,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>n</kbd>: 建立標籤
   <kbd>g</kbd>: 檢視重設選項
   <kbd>&lt;enter&gt;</kbd>: 檢視提交
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 檔案
@@ -314,7 +314,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>n</kbd>: 新增遠端
   <kbd>d</kbd>: 移除遠端
   <kbd>e</kbd>: 編輯遠端
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
 
 ## 遠端分支
@@ -329,5 +329,5 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
   <kbd>u</kbd>: 將此分支設為當前分支之上游
   <kbd>g</kbd>: 檢視重設選項
   <kbd>&lt;enter&gt;</kbd>: 檢視提交
-  <kbd>/</kbd>: Filter the current view
+  <kbd>/</kbd>: Filter the current view by text
 </pre>
diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go
index 93e52c192..61d91ad69 100644
--- a/pkg/gui/controllers/files_controller.go
+++ b/pkg/gui/controllers/files_controller.go
@@ -648,7 +648,7 @@ func (self *FilesController) handleStatusFilterPressed() error {
 				},
 			},
 			{
-				Label: self.c.Tr.ResetCommitFilterState,
+				Label: self.c.Tr.ResetFilter,
 				OnPress: func() error {
 					return self.setStatusFiltering(filetree.DisplayAll)
 				},
diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go
index 6f17bb7c6..63059ee4d 100644
--- a/pkg/i18n/dutch.go
+++ b/pkg/i18n/dutch.go
@@ -35,7 +35,7 @@ func dutchTranslationSet() TranslationSet {
 		Scroll:                              "Scroll",
 		FilterStagedFiles:                   "Show only staged files",
 		FilterUnstagedFiles:                 "Show only unstaged files",
-		ResetCommitFilterState:              "Reset commit file state filter",
+		ResetFilter:                         "Reset commit file state filter",
 		MergeConflictsTitle:                 "Merge conflicten",
 		Checkout:                            "Uitchecken",
 		PullWait:                            "Pullen...",
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 62b9c0015..f916c3c4d 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -54,7 +54,7 @@ type TranslationSet struct {
 	FileFilter                          string
 	FilterStagedFiles                   string
 	FilterUnstagedFiles                 string
-	ResetCommitFilterState              string
+	ResetFilter                         string
 	MergeConflictsTitle                 string
 	Checkout                            string
 	NoChangedFiles                      string
@@ -744,10 +744,10 @@ func EnglishTranslationSet() TranslationSet {
 		Scroll:                              "Scroll",
 		MergeConflictsTitle:                 "Merge conflicts",
 		Checkout:                            "Checkout",
-		FileFilter:                          "Filter files (staged/unstaged)",
+		FileFilter:                          "Filter files by status",
 		FilterStagedFiles:                   "Show only staged files",
 		FilterUnstagedFiles:                 "Show only unstaged files",
-		ResetCommitFilterState:              "Reset filter",
+		ResetFilter:                         "Reset filter",
 		NoChangedFiles:                      "No changed files",
 		PullWait:                            "Pulling...",
 		PushWait:                            "Pushing...",
@@ -1057,51 +1057,52 @@ func EnglishTranslationSet() TranslationSet {
 		GitFlowOptions:                      "Show git-flow options",
 		NotAGitFlowBranch:                   "This does not seem to be a git flow branch",
 		NewGitFlowBranchPrompt:              "New {{.branchType}} name:",
-		IgnoreTracked:                       "Ignore tracked file",
-		IgnoreTrackedPrompt:                 "Are you sure you want to ignore a tracked file?",
-		ExcludeTracked:                      "Exclude tracked file",
-		ExcludeTrackedPrompt:                "Are you sure you want to exclude a tracked file?",
-		ViewResetToUpstreamOptions:          "View upstream reset options",
-		NextScreenMode:                      "Next screen mode (normal/half/fullscreen)",
-		PrevScreenMode:                      "Prev screen mode",
-		StartSearch:                         "Search the current view",
-		StartFilter:                         "Filter the current view",
-		Panel:                               "Panel",
-		KeybindingsLegend:                   "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
-		RenameBranch:                        "Rename branch",
-		SetUnsetUpstream:                    "Set/Unset upstream",
-		NewBranchNamePrompt:                 "Enter new branch name for branch",
-		RenameBranchWarning:                 "This branch is tracking a remote. This action will only rename the local branch name, not the name of the remote branch. Continue?",
-		OpenMenu:                            "Open menu",
-		ResetCherryPick:                     "Reset cherry-picked (copied) commits selection",
-		NextTab:                             "Next tab",
-		PrevTab:                             "Previous tab",
-		CantUndoWhileRebasing:               "Can't undo while rebasing",
-		CantRedoWhileRebasing:               "Can't redo while rebasing",
-		MustStashWarning:                    "Pulling a patch out into the index requires stashing and unstashing your changes. If something goes wrong, you'll be able to access your files from the stash. Continue?",
-		MustStashTitle:                      "Must stash",
-		ConfirmationTitle:                   "Confirmation panel",
-		PrevPage:                            "Previous page",
-		NextPage:                            "Next page",
-		GotoTop:                             "Scroll to top",
-		GotoBottom:                          "Scroll to bottom",
-		FilteringBy:                         "Filtering by",
-		ResetInParentheses:                  "(Reset)",
-		OpenFilteringMenu:                   "View filter-by-path options",
-		FilterBy:                            "Filter by",
-		ExitFilterMode:                      "Stop filtering by path",
-		FilterPathOption:                    "Enter path to filter by",
-		EnterFileName:                       "Enter path:",
-		FilteringMenuTitle:                  "Filtering",
-		MustExitFilterModeTitle:             "Command not available",
-		MustExitFilterModePrompt:            "Command not available in filtered mode. Exit filtered mode?",
-		Diff:                                "Diff",
-		EnterRefToDiff:                      "Enter ref to diff",
-		EnterRefName:                        "Enter ref:",
-		ExitDiffMode:                        "Exit diff mode",
-		DiffingMenuTitle:                    "Diffing",
-		SwapDiff:                            "Reverse diff direction",
-		OpenDiffingMenu:                     "Open diff menu",
+
+		IgnoreTracked:              "Ignore tracked file",
+		IgnoreTrackedPrompt:        "Are you sure you want to ignore a tracked file?",
+		ExcludeTracked:             "Exclude tracked file",
+		ExcludeTrackedPrompt:       "Are you sure you want to exclude a tracked file?",
+		ViewResetToUpstreamOptions: "View upstream reset options",
+		NextScreenMode:             "Next screen mode (normal/half/fullscreen)",
+		PrevScreenMode:             "Prev screen mode",
+		StartSearch:                "Search the current view by text",
+		StartFilter:                "Filter the current view by text",
+		Panel:                      "Panel",
+		KeybindingsLegend:          "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
+		RenameBranch:               "Rename branch",
+		SetUnsetUpstream:           "Set/Unset upstream",
+		NewBranchNamePrompt:        "Enter new branch name for branch",
+		RenameBranchWarning:        "This branch is tracking a remote. This action will only rename the local branch name, not the name of the remote branch. Continue?",
+		OpenMenu:                   "Open menu",
+		ResetCherryPick:            "Reset cherry-picked (copied) commits selection",
+		NextTab:                    "Next tab",
+		PrevTab:                    "Previous tab",
+		CantUndoWhileRebasing:      "Can't undo while rebasing",
+		CantRedoWhileRebasing:      "Can't redo while rebasing",
+		MustStashWarning:           "Pulling a patch out into the index requires stashing and unstashing your changes. If something goes wrong, you'll be able to access your files from the stash. Continue?",
+		MustStashTitle:             "Must stash",
+		ConfirmationTitle:          "Confirmation panel",
+		PrevPage:                   "Previous page",
+		NextPage:                   "Next page",
+		GotoTop:                    "Scroll to top",
+		GotoBottom:                 "Scroll to bottom",
+		FilteringBy:                "Filtering by",
+		ResetInParentheses:         "(Reset)",
+		OpenFilteringMenu:          "View filter-by-path options",
+		FilterBy:                   "Filter by",
+		ExitFilterMode:             "Stop filtering by path",
+		FilterPathOption:           "Enter path to filter by",
+		EnterFileName:              "Enter path:",
+		FilteringMenuTitle:         "Filtering",
+		MustExitFilterModeTitle:    "Command not available",
+		MustExitFilterModePrompt:   "Command not available in filter-by-path mode. Exit filter-by-path mode?",
+		Diff:                       "Diff",
+		EnterRefToDiff:             "Enter ref to diff",
+		EnterRefName:               "Enter ref:",
+		ExitDiffMode:               "Exit diff mode",
+		DiffingMenuTitle:           "Diffing",
+		SwapDiff:                   "Reverse diff direction",
+		OpenDiffingMenu:            "Open diff menu",
 		// the actual view is the extras view which I intend to give more tabs in future but for now we'll only mention the command log part
 		OpenExtrasMenu:                      "Open command log menu",
 		ShowingGitDiff:                      "Showing output for:",
diff --git a/pkg/i18n/japanese.go b/pkg/i18n/japanese.go
index 0c4e8aa13..a85187580 100644
--- a/pkg/i18n/japanese.go
+++ b/pkg/i18n/japanese.go
@@ -61,7 +61,7 @@ func japaneseTranslationSet() TranslationSet {
 		FileFilter:              "ファイルをフィルタ (ステージ/アンステージ)",
 		FilterStagedFiles:       "ステージされたファイルのみを表示",
 		FilterUnstagedFiles:     "ステージされていないファイルのみを表示",
-		ResetCommitFilterState:  "フィルタをリセット",
+		ResetFilter:             "フィルタをリセット",
 		// NoChangedFiles:                      "No changed files",
 		PullWait:                "Pull中...",
 		PushWait:                "Push中...",
diff --git a/pkg/i18n/korean.go b/pkg/i18n/korean.go
index 4eba85cc8..7b2c9d20d 100644
--- a/pkg/i18n/korean.go
+++ b/pkg/i18n/korean.go
@@ -60,7 +60,7 @@ func koreanTranslationSet() TranslationSet {
 		FileFilter:                          "파일을 필터하기 (Staged/unstaged)",
 		FilterStagedFiles:                   "Staged된 파일만 표시",
 		FilterUnstagedFiles:                 "Stage되지 않은 파일만 표시",
-		ResetCommitFilterState:              "필터 리셋",
+		ResetFilter:                         "필터 리셋",
 		NoChangedFiles:                      "변경된 파일이 없습니다.",
 		PullWait:                            "업데이트 중...",
 		PushWait:                            "푸시 중...",
diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go
index 5ac6b8a3a..a2ff1bb12 100644
--- a/pkg/i18n/polish.go
+++ b/pkg/i18n/polish.go
@@ -31,7 +31,7 @@ func polishTranslationSet() TranslationSet {
 		Scroll:                              "Przewiń",
 		FilterStagedFiles:                   "Pokaż tylko pliki w poczekalni",
 		FilterUnstagedFiles:                 "Pokaż tylko pliki poza poczekalnią",
-		ResetCommitFilterState:              "Resetuj filtr commitów",
+		ResetFilter:                         "Resetuj filtr commitów",
 		Checkout:                            "Przełącz",
 		NoChangedFiles:                      "Brak zmienionych plików",
 		PullWait:                            "Pobieranie zmian...",
diff --git a/pkg/i18n/russian.go b/pkg/i18n/russian.go
index 6d03bce94..d0881fb33 100644
--- a/pkg/i18n/russian.go
+++ b/pkg/i18n/russian.go
@@ -79,7 +79,7 @@ func RussianTranslationSet() TranslationSet {
 		FileFilter:                          "Фильтровать файлы (проиндексированные/непроиндексированные)",
 		FilterStagedFiles:                   "Показывать только проиндексированные файлы",
 		FilterUnstagedFiles:                 "Показывать только непроиндексированные файлы",
-		ResetCommitFilterState:              "Сбросить фильтр",
+		ResetFilter:                         "Сбросить фильтр",
 		NoChangedFiles:                      "Нет изменённых файлов",
 		PullWait:                            "Получение и слияние изменении...",
 		PushWait:                            "Отправка изменении...",
diff --git a/pkg/i18n/traditional_chinese.go b/pkg/i18n/traditional_chinese.go
index b83f344f8..5f10fd10b 100644
--- a/pkg/i18n/traditional_chinese.go
+++ b/pkg/i18n/traditional_chinese.go
@@ -112,7 +112,7 @@ func traditionalChineseTranslationSet() TranslationSet {
 		FileFilter:                          "篩選檔案 (預存/未預存)",
 		FilterStagedFiles:                   "僅顯示預存的檔案",
 		FilterUnstagedFiles:                 "僅顯示未預存的檔案",
-		ResetCommitFilterState:              "重設篩選",
+		ResetFilter:                         "重設篩選",
 		NoChangedFiles:                      "沒有變更的檔案",
 		PullWait:                            "拉取...",
 		PushWait:                            "推送...",

From 4d734d594a3adb225fd592e6649c3e32b292a88d Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Sun, 2 Jul 2023 15:38:50 +1000
Subject: [PATCH 19/20] Add filtering docs

---
 docs/README.md    |  3 ++-
 docs/Searching.md | 21 +++++++++++++++++++++
 2 files changed, 23 insertions(+), 1 deletion(-)
 create mode 100644 docs/Searching.md

diff --git a/docs/README.md b/docs/README.md
index 4f58a2a56..c0c8191d2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,7 +1,8 @@
-# Documentation Overview 
+# Documentation Overview
 
 * [Configuration](./Config.md).
 * [Custom Commands](./Custom_Command_Keybindings.md)
 * [Custom Pagers](./Custom_Pagers.md)
 * [Keybindings](./keybindings)
 * [Undo/Redo](./Undoing.md)
+* [Searching/Filtering](./Searching.md)
diff --git a/docs/Searching.md b/docs/Searching.md
new file mode 100644
index 000000000..589831c55
--- /dev/null
+++ b/docs/Searching.md
@@ -0,0 +1,21 @@
+# Searching/Filtering
+
+## View searching/filtering
+
+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.
+
+If you would like both filtering and searching to be enabled on a given view, please raise an issue for this.
+
+## Filtering files by status
+
+You can filter the files view to only show staged/unstaged files by pressing `<c-b>` in the files view.
+
+## Filtering commits by file path
+
+You can filter the commits view to only show commits which contain changes to a given file path.
+
+You can do this in a couple of ways:
+1) Start lazygit with the -f flag e.g. `lazygit -f my/path`
+2) From within lazygit, press `<c-s>` and then enter the path of the file you want to filter by

From 5d982e1d70aa9a07645a5665130f2e499b7b56c8 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Mon, 3 Jul 2023 12:40:41 +1000
Subject: [PATCH 20/20] Add mutex to filtered list to avoid concurrency issues

---
 pkg/gui/context/filtered_list.go | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go
index ce2e12590..298d3d615 100644
--- a/pkg/gui/context/filtered_list.go
+++ b/pkg/gui/context/filtered_list.go
@@ -2,6 +2,7 @@ package context
 
 import (
 	"github.com/jesseduffield/lazygit/pkg/utils"
+	"github.com/sasha-s/go-deadlock"
 )
 
 type FilteredList[T any] struct {
@@ -10,12 +11,15 @@ type FilteredList[T any] struct {
 	getList         func() []T
 	getFilterFields func(T) []string
 	filter          string
+
+	mutex *deadlock.Mutex
 }
 
 func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string) *FilteredList[T] {
 	return &FilteredList[T]{
 		getList:         getList,
 		getFilterFields: getFilterFields,
+		mutex:           &deadlock.Mutex{},
 	}
 }
 
@@ -50,6 +54,9 @@ func (self *FilteredList[T]) UnfilteredLen() int {
 }
 
 func (self *FilteredList[T]) applyFilter() {
+	self.mutex.Lock()
+	defer self.mutex.Unlock()
+
 	if self.filter == "" {
 		self.filteredIndices = nil
 	} else {
@@ -70,6 +77,9 @@ func (self *FilteredList[T]) match(haystack string, needle string) bool {
 }
 
 func (self *FilteredList[T]) UnfilteredIndex(index int) int {
+	self.mutex.Lock()
+	defer self.mutex.Unlock()
+
 	if self.filteredIndices == nil {
 		return index
 	}
@@ -79,6 +89,5 @@ func (self *FilteredList[T]) UnfilteredIndex(index int) int {
 		return -1
 	}
 
-	// TODO: mutex
 	return self.filteredIndices[index]
 }