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: '' prevBlock-alt: 'h' nextBlock-alt: 'l' + nextMatch: 'n' + prevMatch: 'N' + startSearch: '/' optionMenu: 'x' optionMenu-alt1: '?' select: '' 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}) + } + } + } + } + +} + // draw re-draws the view's contents. func (v *View) draw() error { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + v.updateSearchPositions() maxX, maxY := v.Size() if v.Wrap { @@ -392,6 +551,13 @@ func (v *View) draw() error { if bgColor == ColorDefault { bgColor = v.BgColor } + if matched, selected := v.isPatternMatchedRune(x, y); matched { + if selected { + bgColor = ColorCyan + } else { + bgColor = ColorYellow + } + } if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil { return err @@ -403,6 +569,18 @@ func (v *View) draw() error { return nil } +func (v *View) isPatternMatchedRune(x, y int) (bool, bool) { + searchStringLength := len(v.searcher.searchString) + for i, pos := range v.searcher.searchPositions { + adjustedY := y + v.oy + adjustedX := x + v.ox + if adjustedY == pos.y && adjustedX >= pos.x && adjustedX < pos.x+searchStringLength { + return true, i == v.searcher.currentSearchIndex + } + } + return false, false +} + // realPosition returns the position in the internal buffer corresponding to the // point (x, y) of the view. func (v *View) realPosition(vx, vy int) (x, y int, err error) { diff --git a/vendor/modules.txt b/vendor/modules.txt index bebe13c4d..b90da203a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -32,7 +32,7 @@ github.com/hashicorp/hcl/json/token github.com/integrii/flaggy # github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 github.com/jbenet/go-context/io -# github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5 +# github.com/jesseduffield/gocui v0.3.1-0.20200223105115-3e1f0f7c3efe github.com/jesseduffield/gocui # github.com/jesseduffield/pty v1.2.1 github.com/jesseduffield/pty