summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2020-02-23 21:53:30 +1100
committerJesse Duffield <jessedduffield@gmail.com>2020-02-24 22:18:04 +1100
commit46be280c921994ba783b8c53232d118f0547ad14 (patch)
tree0f2e637414a54e2dd034081f0fce888346e31655
parent2a5763a77190a579ae3cb57926a0d907964fcccb (diff)
support searching in side panels
For now we're just doing side panels, because it will take more work to support this in the various main panel contexts
-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 :=