summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--pkg/config/app_config.go3
-rw-r--r--pkg/gui/branches_panel.go17
-rw-r--r--pkg/gui/commit_files_panel.go5
-rw-r--r--pkg/gui/commits_panel.go14
-rw-r--r--pkg/gui/files_panel.go5
-rw-r--r--pkg/gui/gui.go57
-rw-r--r--pkg/gui/keybindings.go13
-rw-r--r--pkg/gui/searching.go89
-rw-r--r--pkg/gui/stash_panel.go5
-rw-r--r--pkg/gui/view_helpers.go37
-rw-r--r--pkg/theme/theme.go6
-rw-r--r--vendor/github.com/jesseduffield/gocui/escape.go14
-rw-r--r--vendor/github.com/jesseduffield/gocui/gui.go28
-rw-r--r--vendor/github.com/jesseduffield/gocui/keybinding.go14
-rw-r--r--vendor/github.com/jesseduffield/gocui/view.go196
-rw-r--r--vendor/modules.txt2
18 files changed, 456 insertions, 53 deletions
diff --git a/go.mod b/go.mod
index a7d2ab3ef..85dbc1939 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/integrii/flaggy v1.4.0
- github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5
+ github.com/jesseduffield/gocui v0.3.1-0.20200223105115-3e1f0f7c3efe
github.com/jesseduffield/pty v1.2.1
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 // indirect
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 // indirect
diff --git a/go.sum b/go.sum
index dfb2d7849..c88f65216 100644
--- a/go.sum
+++ b/go.sum
@@ -87,6 +87,8 @@ github.com/jesseduffield/gocui v0.3.1-0.20200131131454-a319843434ac h1:vp7I0RpFq
github.com/jesseduffield/gocui v0.3.1-0.20200131131454-a319843434ac/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5 h1:tE0w3tuL/bj1o5VMhjjE0ep6i7Fva+RYjKcMFcniJEY=
github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
+github.com/jesseduffield/gocui v0.3.1-0.20200223105115-3e1f0f7c3efe h1:UQyebauOcBzbGq32kTXwEyuJaqp3BkI8JoCrGs2jijU=
+github.com/jesseduffield/gocui v0.3.1-0.20200223105115-3e1f0f7c3efe/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/pty v1.2.1 h1:7xYBiwNH0PpWqC8JmvrPq1a/ksNqyCavzWu9pbBGYWI=
github.com/jesseduffield/pty v1.2.1/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00 h1:+JaOkfBNYQYlGD7dgru8mCwYNEc5tRRI8mThlVANhSM=
diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go
index 6db7322c7..a0de1fb4e 100644
--- a/pkg/config/app_config.go
+++ b/pkg/config/app_config.go
@@ -283,6 +283,9 @@ keybinding:
nextBlock: '<right>'
prevBlock-alt: 'h'
nextBlock-alt: 'l'
+ nextMatch: 'n'
+ prevMatch: 'N'
+ startSearch: '/'
optionMenu: 'x'
optionMenu-alt1: '?'
select: '<space>'
diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go
index a683a2342..251b112d8 100644
--- a/pkg/gui/branches_panel.go
+++ b/pkg/gui/branches_panel.go
@@ -400,6 +400,7 @@ func (gui *Gui) onBranchesTabClick(tabIndex int) error {
func (gui *Gui) switchBranchesPanelContext(context string) error {
branchesView := gui.getBranchesView()
branchesView.Context = context
+ branchesView.ClearSearch()
contextTabIndexMap := map[string]int{
"local-branches": 0,
@@ -444,3 +445,19 @@ func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error
return gui.createResetMenu(branch.Name)
}
+
+func (gui *Gui) onBranchesPanelSearchSelect(selectedLine int) error {
+ branchesView := gui.getBranchesView()
+ switch branchesView.Context {
+ case "local-branches":
+ gui.State.Panels.Branches.SelectedLine = selectedLine
+ return gui.handleBranchSelect(gui.g, branchesView)
+ case "remotes":
+ gui.State.Panels.Remotes.SelectedLine = selectedLine
+ return gui.handleRemoteSelect(gui.g, branchesView)
+ case "remote-branches":
+ gui.State.Panels.RemoteBranches.SelectedLine = selectedLine
+ return gui.handleRemoteBranchSelect(gui.g, branchesView)
+ }
+ return nil
+}
diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go
index b4a08625a..fbfeeb11c 100644
--- a/pkg/gui/commit_files_panel.go
+++ b/pkg/gui/commit_files_panel.go
@@ -212,3 +212,8 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return enterTheFile(selectedLineIdx)
}
+
+func (gui *Gui) onCommitFilesPanelSearchSelect(selectedLine int) error {
+ gui.State.Panels.CommitFiles.SelectedLine = selectedLine
+ return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
+}
diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go
index 150a7ae16..714f886b6 100644
--- a/pkg/gui/commits_panel.go
+++ b/pkg/gui/commits_panel.go
@@ -623,6 +623,7 @@ func (gui *Gui) onCommitsTabClick(tabIndex int) error {
func (gui *Gui) switchCommitsPanelContext(context string) error {
commitsView := gui.getCommitsView()
commitsView.Context = context
+ commitsView.ClearSearch()
contextTabIndexMap := map[string]int{
"branch-commits": 0,
@@ -661,3 +662,16 @@ func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu(commit.Sha)
}
+
+func (gui *Gui) onCommitsPanelSearchSelect(selectedLine int) error {
+ commitsView := gui.getCommitsView()
+ switch commitsView.Context {
+ case "branch-commits":
+ gui.State.Panels.Commits.SelectedLine = selectedLine
+ return gui.handleCommitSelect(gui.g, commitsView)
+ case "reflog-commits":
+ gui.State.Panels.ReflogCommits.SelectedLine = selectedLine
+ return gui.handleReflogCommitSelect(gui.g, commitsView)
+ }
+ return nil
+}
diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go
index 1ca3538a2..9e81009ea 100644
--- a/pkg/gui/files_panel.go
+++ b/pkg/gui/files_panel.go
@@ -574,3 +574,8 @@ func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu("@{upstream}")
}
+
+func (gui *Gui) onFilesPanelSearchSelect(selectedLine int) error {
+ gui.State.Panels.Files.SelectedLine = selectedLine
+ return gui.focusAndSelectFile(gui.g, gui.getFilesView())
+}
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 929c4ba88..015e3ebfe 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -172,6 +172,12 @@ type panelStates struct {
Status *statusPanelState
}
+type searchingState struct {
+ view *gocui.View
+ isSearching bool
+ searchString string
+}
+
type guiState struct {
Files []*commands.File
Branches []*commands.Branch
@@ -195,6 +201,7 @@ type guiState struct {
RetainOriginalDir bool
IsRefreshingFiles bool
RefreshingFilesMutex sync.Mutex
+ Searching searchingState
}
// for now the split view will always be on
@@ -338,6 +345,10 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
+ if v.IsSearching() && newView.Name() != "search" {
+ gui.State.Searching.isSearching = false
+ v.ClearSearch()
+ }
switch v.Name() {
case "branches":
if v.Context == "local-branches" {
@@ -500,7 +511,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
- userConfig := gui.Config.GetUserConfig()
v, err := g.SetView(main, leftSideWidth+panelSpacing, 0, mainPanelRight, mainPanelBottom, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
@@ -510,6 +520,9 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.Wrap = true
v.FgColor = textColor
v.IgnoreCarriageReturns = true
+ v.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
+ return nil
+ }))
}
hiddenViewOffset := 0
@@ -542,6 +555,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
filesView.Highlight = true
filesView.Title = gui.Tr.SLocalize("FilesTitle")
+ filesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onFilesPanelSearchSelect))
}
branchesView, err := g.SetViewBeneath("branches", "files", vHeights["branches"])
@@ -552,6 +566,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.Tabs = []string{"Local Branches", "Remotes", "Tags"}
branchesView.FgColor = textColor
+ branchesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onBranchesPanelSearchSelect))
}
if v, err := g.SetViewBeneath("commitFiles", "branches", vHeights["commits"]); err != nil {
@@ -560,6 +575,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
v.Title = gui.Tr.SLocalize("CommitFiles")
v.FgColor = textColor
+ v.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitFilesPanelSearchSelect))
}
commitsView, err := g.SetViewBeneath("commits", "branches", vHeights["commits"])
@@ -570,6 +586,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.Tabs = []string{"Commits", "Reflog"}
commitsView.FgColor = textColor
+ commitsView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitsPanelSearchSelect))
}
stashView, err := g.SetViewBeneath("stash", "commits", vHeights["stash"])
@@ -579,6 +596,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
stashView.Title = gui.Tr.SLocalize("StashTitle")
stashView.FgColor = textColor
+ stashView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onStashPanelSearchSelect))
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
@@ -586,7 +604,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return err
}
v.Frame = false
- v.FgColor = theme.GetGocuiColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
+ v.FgColor = theme.OptionsColor
}
if gui.getCommitMessageView() == nil {
@@ -619,6 +637,35 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
+ searchViewOffset := hiddenViewOffset
+ if gui.State.Searching.isSearching {
+ searchViewOffset = 0
+ }
+
+ // this view takes up one character. Its only purpose is to show the slash when searching
+ searchPrefix := "search: "
+ if searchPrefixView, err := g.SetView("searchPrefix", appStatusOptionsBoundary-1+searchViewOffset, height-2+searchViewOffset, len(searchPrefix)+searchViewOffset, height+searchViewOffset, 0); err != nil {
+ if err.Error() != "unknown view" {
+ return err
+ }
+
+ searchPrefixView.BgColor = gocui.ColorDefault
+ searchPrefixView.FgColor = gocui.ColorGreen
+ searchPrefixView.Frame = false
+ gui.setViewContent(gui.g, searchPrefixView, searchPrefix)
+ }
+
+ if searchView, err := g.SetView("search", appStatusOptionsBoundary-1+searchViewOffset+len(searchPrefix), height-2+searchViewOffset, optionsVersionBoundary+searchViewOffset, height+searchViewOffset, 0); err != nil {
+ if err.Error() != "unknown view" {
+ return err
+ }
+
+ searchView.BgColor = gocui.ColorDefault
+ searchView.FgColor = gocui.ColorGreen
+ searchView.Frame = false
+ searchView.Editable = true
+ }
+
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
@@ -826,6 +873,12 @@ func (gui *Gui) Run() error {
return err
}
defer g.Close()
+
+ g.OnSearchEscape = gui.onSearchEscape
+ g.SearchEscapeKey = gui.getKey("universal.return")
+ g.NextSearchMatchKey = gui.getKey("universal.nextMatch")
+ g.PrevSearchMatchKey = gui.getKey("universal.prevMatch")
+
gui.stopChan = make(chan struct{})
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 3286c702e..bd050557a 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -1398,6 +1398,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleCommitFilesClick,
},
+ {
+ ViewName: "search",
+ Key: gocui.KeyEnter,
+ Modifier: gocui.ModNone,
+ Handler: gui.handleSearch,
+ },
+ {
+ ViewName: "search",
+ Key: gui.getKey("universal.return"),
+ Modifier: gocui.ModNone,
+ Handler: gui.handleSearchEscape,
+ },
}
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {
@@ -1424,6 +1436,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gui.getKey("universal.nextItem"), Modifier: gocui.ModNone, Handler: listView.handleNextLine},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listView.handleNextLine},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listView.handleClick},
+ {ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gui.getKey("universal.startSearch"), Modifier: gocui.ModNone, Handler: gui.handleOpenSearch},
}...)
}
diff --git a/pkg/gui/searching.go b/pkg/gui/searching.go
new file mode 100644
index 000000000..6e8cd2f64
--- /dev/null
+++ b/pkg/gui/searching.go
@@ -0,0 +1,89 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/jesseduffield/gocui"
+ "github.com/jesseduffield/lazygit/pkg/theme"
+ "github.com/jesseduffield/lazygit/pkg/utils"
+)
+
+func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
+ gui.State.Searching.isSearching = true
+ gui.State.Searching.view = v
+ gui.renderString(gui.g, "search", "")
+ gui.switchFocus(gui.g, v, gui.getSearchView())
+
+ return nil
+}
+
+func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error {
+ gui.State.Searching.searchString = gui.getSearchView().Buffer()
+ gui.switchFocus(gui.g, nil, gui.State.Searching.view)
+ if err := gui.State.Searching.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 {
+ return func(y int, index int, total int) error {
+ if total == 0 {
+ gui.renderString(
+ gui.g,
+ "search",
+ fmt.Sprintf(
+ "no matches for '%s' %s",
+ gui.State.Searching.searchString,
+ utils.ColoredString(
+ fmt.Sprintf("%s: exit search mode", gui.getKeyDisplay("universal.return")),
+ theme.OptionsFgColor,
+ ),
+ ),
+ )
+ return nil
+ }
+ gui.renderString(
+ gui.g,
+ "search",
+ fmt.Sprintf(
+ "matches for '%s' (%d of %d) %s",
+ gui.State.Searching.searchString,
+ index+1,
+ total,
+ utils.ColoredString(
+ fmt.Sprintf(
+ "%s: next match, %s: previous match, %s: exit search mode",
+ gui.getKeyDisplay("universal.nextMatch"),
+ gui.getKeyDisplay("universal.prevMatch"),
+ gui.getKeyDisplay("universal.return"),
+ ),
+ theme.OptionsFgColor,
+ ),
+ ),
+ )
+ if err := innerFunc(y); err != nil {
+ return err
+ }
+ return nil
+ }
+}
+
+func (gui *Gui) onSearchEscape() error {
+ gui.State.Searching.isSearching = false
+ gui.State.Searching.view = nil
+ return nil
+}
+
+func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error {
+ if err := gui.switchFocus(gui.g, nil, gui.State.Searching.view); err != nil {
+ return err
+ }
+
+ if err := gui.onSearchEscape(); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go
index 23ecaba60..9ca72dee0 100644
--- a/pkg/gui/stash_panel.go
+++ b/pkg/gui/stash_panel.go
@@ -122,3 +122,8 @@ func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
return gui.refreshFiles()
})
}
+
+func (gui *Gui) onStashPanelSearchSelect(selectedLine int) error {
+ gui.State.Panels.Stash.SelectedLine = selectedLine
+ return gui.handleStashEntrySelect(gui.g, gui.getStashView())
+}
diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go
index bf25dbdbb..5452f1754 100644
--- a/pkg/gui/view_helpers.go
+++ b/pkg/gui/view_helpers.go
@@ -135,6 +135,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
}
v.Highlight = false
return nil
+ case "search":
+ return nil
default:
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
@@ -218,32 +220,7 @@ func (gui *Gui) resetOrigin(v *gocui.View) error {
// if the cursor down past the last item, move it to the last line
func (gui *Gui) focusPoint(cx int, cy int, lineCount int, v *gocui.View) error {
- if cy < 0 || cy > lineCount {
- return nil
- }
- ox, oy := v.Origin()
- _, height := v.Size()
-
- ly := height - 1
- if ly == -1 {
- ly = 0
- }
-
- // if line is above origin, move origin and set cursor to zero
- // if line is below origin + height, move origin and set cursor to max
- // otherwise set cursor to value - origin
- if ly > lineCount {
- _ = v.SetCursor(cx, cy)
- _ = v.SetOrigin(ox, 0)
- } else if cy < oy {
- _ = v.SetCursor(cx, 0)
- _ = v.SetOrigin(ox, cy)
- } else if cy > oy+ly {
- _ = v.SetCursor(cx, ly)
- _ = v.SetOrigin(ox, cy-ly)
- } else {
- _ = v.SetCursor(cx, cy-oy)
- }
+ v.FocusPoint(cx, cy)
return nil
}
@@ -268,6 +245,9 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
if err := v.SetOrigin(0, 0); err != nil {
return err
}
+ if err := v.SetCursor(0, 0); err != nil {
+ return err
+ }
return gui.setViewContent(gui.g, v, s)
})
return nil
@@ -333,6 +313,11 @@ func (gui *Gui) getMenuView() *gocui.View {
return v
}
+func (gui *Gui) getSearchView() *gocui.View {
+ v, _ := gui.g.View("search")
+ return v
+}
+
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
diff --git a/pkg/theme/theme.go b/pkg/theme/theme.go
index c5e49accd..04b49a555 100644
--- a/pkg/theme/theme.go
+++ b/pkg/theme/theme.go
@@ -23,6 +23,10 @@ var (
// SelectedLineBgColor is the background color for the selected line
SelectedLineBgColor color.Attribute
+
+ OptionsFgColor color.Attribute
+
+ OptionsColor gocui.Attribute
)
// UpdateTheme updates all theme variables
@@ -30,6 +34,8 @@ func UpdateTheme(userConfig *viper.Viper) {
ActiveBorderColor = GetGocuiColor(userConfig.GetStringSlice("gui.theme.activeBorderColor"))
InactiveBorderColor = GetGocuiColor(userConfig.GetStringSlice("gui.theme.inactiveBorderColor"))
SelectedLineBgColor = GetBgColor(userConfig.GetStringSlice("gui.theme.selectedLineBgColor"))
+ OptionsColor = GetGocuiColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
+ OptionsFgColor = GetFgColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
isLightTheme := userConfig.GetBool("gui.theme.lightTheme")
if isLightTheme {
diff --git a/vendor/github.com/jesseduffield/gocui/escape.go b/vendor/github.com/jesseduffield/gocui/escape.go
index c09003e31..10972bff2 100644
--- a/vendor/github.com/jesseduffield/gocui/escape.go
+++ b/vendor/github.com/jesseduffield/gocui/escape.go
@@ -6,7 +6,6 @@ package gocui
import (
"strconv"
- "sync"
"github.com/go-errors/errors"
)
@@ -17,7 +16,6 @@ type escapeInterpreter struct {
csiParam []string
curFgColor, curBgColor Attribute
mode OutputMode
- mutex sync.Mutex
}
type escapeState int
@@ -37,9 +35,6 @@ var (
// runes in case of error will output the non-parsed runes as a string.
func (ei *escapeInterpreter) runes() []rune {
- ei.mutex.Lock()
- defer ei.mutex.Unlock()
-
switch ei.state {
case stateNone:
return []rune{0x1b}
@@ -72,9 +67,6 @@ func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
// reset sets the escapeInterpreter in initial state.
func (ei *escapeInterpreter) reset() {
- ei.mutex.Lock()
- defer ei.mutex.Unlock()
-
ei.state = stateNone
ei.curFgColor = ColorDefault
ei.curBgColor = ColorDefault
@@ -85,9 +77,6 @@ func (ei *escapeInterpreter) reset() {
// of an escape sequence, and as such should not be printed verbatim. Otherwise,
// it's not an escape sequence.
func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
- ei.mutex.Lock()
- defer ei.mutex.Unlock()
-
// Sanity checks
if len(ei.csiParam) > 20 {
return false, errCSITooLong
@@ -191,9 +180,6 @@ func (ei *escapeInterpreter) outputNormal() error {
// 0x11 - 0xe8: 216 different colors
// 0xe9 - 0x1ff: 24 different shades of grey
func (ei *escapeInterpreter) output256() error {
- ei.mutex.Lock()
- defer ei.mutex.Unlock()
-
if len(ei.csiParam) < 3 {
return ei.outputNormal()
}
diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go
index c4ac34780..93f7606b0 100644
--- a/vendor/github.com/jesseduffield/gocui/gui.go
+++ b/vendor/github.com/jesseduffield/gocui/gui.go
@@ -94,6 +94,12 @@ type Gui struct {
// tickingMutex ensures we don't have two loops ticking. The point of 'ticking'
// is to refresh the gui rapidly so that loader characters can be animated.
tickingMutex sync.Mutex
+
+ OnSearchEscape func() error
+ // these keys must either be of type Key of rune
+ SearchEscapeKey interface{}
+ NextSearchMatchKey interface{}
+ PrevSearchMatchKey interface{}
}
// NewGui returns a new Gui object with a given output mode.
@@ -124,6 +130,11 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) {
// view edges
g.SupportOverlaps = supportOverlaps
+ // default keys for when searching strings in a view
+ g.SearchEscapeKey = KeyEsc
+ g.NextSearchMatchKey = 'n'
+ g.PrevSearchMatchKey = 'N'
+
return g, nil
}
@@ -803,6 +814,23 @@ func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err err
var globalKb *keybinding
var matchingParentViewKb *keybinding
+ // if we're searching, and we've hit n/N/Esc, we ignore the default keybinding
+ if v.IsSearching() && Modifier(ev.Mod) == ModNone {
+ if eventMatchesKey(ev, g.NextSearchMatchKey) {
+ return true, v.gotoNextMatch()
+ } else if eventMatchesKey(ev, g.PrevSearchMatchKey) {
+ return true, v.gotoPreviousMatch()
+ } else if eventMatchesKey(ev, g.SearchEscapeKey) {
+ v.searcher.clearSearch()
+ if g.OnSearchEscape != nil {
+ if err := g.OnSearchEscape(); err != nil {
+ return true, err
+ }
+ }
+ return true, nil
+ }
+ }
+
for _, kb := range g.keybindings {
if kb.handler == nil {
continue
diff --git a/vendor/github.com/jesseduffield/gocui/keybinding.go b/vendor/github.com/jesseduffield/gocui/keybinding.go
index d3b8904c8..6e4040e10 100644
--- a/vendor/github.com/jesseduffield/gocui/keybinding.go
+++ b/vendor/github.com/jesseduffield/gocui/keybinding.go
@@ -29,6 +29,20 @@ func newKeybinding(viewname string, contexts []string, key Key, ch rune, mod Mod
return kb
}
+func eventMatchesKey(ev *termbox.Event, key interface{}) bool {
+ // assuming ModNone for now
+ if Modifier(ev.Mod) != ModNone {
+ return false
+ }
+
+ k, ch, err := getKey(key)
+ if err != nil {
+ return false
+ }
+
+ return k == Key(ev.Key) && ch == ev.Ch
+}
+
// matchKeypress returns if the keybinding matches the keypress.
func (kb *keybinding) matchKeypress(key Key, ch rune, mod Modifier) bool {
return kb.key == key && kb.ch == ch && kb.mod == mod
diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go
index ee3657eb3..1277e3c51 100644
--- a/vendor/github.com/jesseduffield/gocui/view.go
+++ b/vendor/github.com/jesseduffield/gocui/view.go
@@ -105,6 +105,137 @@ type View struct {
ParentView *View
Context string // this is for assigning keybindings to a view only in certain contexts
+
+ searcher *searcher
+}
+
+type searcher struct {
+ searchString string
+ searchPositions []cellPos
+ currentSearchIndex int
+ onSelectItem func(int, int, int) error
+}
+
+func (v *View) SetOnSelectItem(onSelectItem func(int, int, int) error) {
+ v.searcher.onSelectItem = onSelectItem
+}
+
+func (v *View) gotoNextMatch() error {
+ if len(v.searcher.searchPositions) == 0 {
+ return nil
+ }
+ if v.searcher.currentSearchIndex == len(v.searcher.searchPositions)-1 {
+ v.searcher.currentSearchIndex = 0
+ } else {
+ v.searcher.currentSearchIndex++
+ }
+ return v.SelectSearchResult(v.searcher.currentSearchIndex)
+}
+
+func (v *View) gotoPreviousMatch() error {
+ if len(v.searcher.searchPositions) == 0 {
+ return nil
+ }
+ if v.searcher.currentSearchIndex == 0 {
+ if len(v.searcher.searchPositions) > 0 {
+ v.searcher.currentSearchIndex = len(v.searcher.searchPositions) - 1
+ }
+ } else {
+ v.searcher.currentSearchIndex--
+ }
+ return v.SelectSearchResult(v.searcher.currentSearchIndex)
+}
+
+func (v *View) SelectSearchResult(index int) error {
+ y := v.searcher.searchPositions[index].y
+ v.FocusPoint(0, y)
+ if v.searcher.onSelectItem != nil {
+ return v.searcher.onSelectItem(y, index, len(v.searcher.searchPositions))
+ }
+ return nil
+}
+
+func (v *View) Search(str string) error {
+ v.writeMutex.Lock()
+ defer v.writeMutex.Unlock()
+
+ v.searcher.search(str)
+ v.updateSearchPositions()
+ if len(v.searcher.searchPositions) > 0 {
+ // get the first result past the current cursor
+ currentIndex := 0
+ adjustedY := v.oy + v.cy
+ adjustedX := v.ox + v.cx
+ for i, pos := range v.searcher.searchPositions {
+ if pos.y > adjustedY || (pos.y == adjustedY && pos.x > adjustedX) {
+ currentIndex = i
+ break
+ }
+ }
+ v.searcher.currentSearchIndex = currentIndex
+ return v.SelectSearchResult(currentIndex)
+ } else {
+ return v.searcher.onSelectItem(-1, -1, 0)
+ }
+ return nil
+}
+
+func (v *View) ClearSearch() {
+ v.searcher.clearSearch()
+}
+
+func (v *View) IsSearching() bool {
+ return v.searcher.searchString != ""
+}
+
+func (v *View) FocusPoint(cx int, cy int) {
+ lineCount := len(v.lines)
+ if cy < 0 || cy > lineCount {
+ return
+ }
+ _, height := v.Size()
+
+ ly := height - 1
+ if ly == -1 {
+ ly = 0
+ }
+
+ // if line is above origin, move origin and set cursor to zero
+ // if line is below origin + height, move origin and set cursor to max
+ // otherwise set cursor to value - origin
+ if ly > lineCount {
+ v.cx = cx
+ v.cy = cy
+ v.oy = 0
+ } else if cy < v.oy {
+ v.cx = cx
+ v.cy = 0
+ v.oy = cy
+ } else if cy > v.oy+ly {
+ v.cx = cx
+ v.cy = ly
+ v.oy = cy - ly
+ } else {
+ v.cx = cx
+ v.cy = cy - v.oy
+ }
+}
+
+func (s *searcher) search(str string) {
+ s.searchString = str
+ s.searchPositions = []cellPos{}
+ s.currentSearchIndex = 0
+}
+
+func (s *searcher) clearSearch() {
+ s.searchString = ""
+ s.searchPositions = []cellPos{}
+ s.currentSearchIndex = 0
+}
+
+type cellPos struct {
+ x int
+ y int
}
type viewLine struct {
@@ -131,15 +262,16 @@ func (l lineType) String() string {
// newView returns a new View object.
func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
v := &View{
- name: name,
- x0: x0,
- y0: y0,
- x1: x1,
- y1: y1,
- Frame: true,
- Editor: DefaultEditor,
- tainted: true,
- ei: newEscapeInterpreter(mode),
+ name: name,
+ x0: x0,
+ y0: y0,
+ x1: x1,
+ y1: y1,
+ Frame: true,
+ Editor: DefaultEditor,
+ tainted: true,
+ ei: newEscapeInterpreter(mode),
+ searcher: &searcher{},
}
return v
}
@@ -331,8 +463,35 @@ func (v *View) Rewind() {
v.readOffset = 0
}
+func (v *View) updateSearchPositions() {
+ if v.searcher.searchString != "" {
+ v.searcher.searchPositions = []cellPos{}
+ for y, line := range v.lines {
+ lineLoop:
+ for x, _ := range line {
+ if line[x].chr == rune(v.searcher.searchString[0]) {
+ for offset := 1; offset < len(v.searcher.searchString); offset++ {
+ if len(line)-1 < x+offset {
+ continue lineLoop
+ }
+ if line[x+offset].chr != rune(v.searcher.searchString[offset]) {
+ continue lineLoop
+ }
+ }
+ v.searcher.searchPositions = append(v.searcher.searchPositions, cellPos{x: x, y: y})
+ }
+ }
+ }
+ }
+
+}