1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-10 04:07:18 +02:00
lazygit/pkg/gui/view_helpers.go
Jesse Duffield 2e05ac0c90 paging keybindings for line by line panel
support searching in line by line panel

move mutexes into their own struct

add line by line panel mutex

apply LBL panel mutex

bump gocui to prevent crashing when search item count decreases
2020-10-10 00:23:01 +11:00

448 lines
10 KiB
Go

package gui
import (
"fmt"
"sort"
"strings"
"sync"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spkg/bom"
)
func (gui *Gui) getCyclableWindows() []string {
return []string{"status", "files", "branches", "commits", "stash"}
}
// models/views that we can refresh
const (
COMMITS = iota
BRANCHES
FILES
STASH
REFLOG
TAGS
REMOTES
STATUS
SUBMODULES
)
func getScopeNames(scopes []int) []string {
scopeNameMap := map[int]string{
COMMITS: "commits",
BRANCHES: "branches",
FILES: "files",
SUBMODULES: "submodules",
STASH: "stash",
REFLOG: "reflog",
TAGS: "tags",
REMOTES: "remotes",
STATUS: "status",
}
scopeNames := make([]string, len(scopes))
for i, scope := range scopes {
scopeNames[i] = scopeNameMap[scope]
}
return scopeNames
}
func getModeName(mode int) string {
switch mode {
case SYNC:
return "sync"
case ASYNC:
return "async"
case BLOCK_UI:
return "block-ui"
default:
return "unknown mode"
}
}
const (
SYNC = iota // wait until everything is done before returning
ASYNC // return immediately, allowing each independent thing to update itself
BLOCK_UI // wrap code in an update call to ensure UI updates all at once and keybindings aren't executed till complete
)
type refreshOptions struct {
then func()
scope []int // e.g. []int{COMMITS, BRANCHES}. Leave empty to refresh everything
mode int // one of SYNC (default), ASYNC, and BLOCK_UI
}
func intArrToMap(arr []int) map[int]bool {
output := map[int]bool{}
for _, el := range arr {
output[el] = true
}
return output
}
func (gui *Gui) refreshSidePanels(options refreshOptions) error {
if options.scope == nil {
gui.Log.Infof(
"refreshing all scopes in %s mode",
getModeName(options.mode),
)
} else {
gui.Log.Infof(
"refreshing the following scopes in %s mode: %s",
getModeName(options.mode),
strings.Join(getScopeNames(options.scope), ","),
)
}
wg := sync.WaitGroup{}
f := func() {
var scopeMap map[int]bool
if len(options.scope) == 0 {
scopeMap = intArrToMap([]int{COMMITS, BRANCHES, FILES, STASH, REFLOG, TAGS, REMOTES, STATUS})
} else {
scopeMap = intArrToMap(options.scope)
}
if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshCommits()
} else {
gui.refreshCommits()
}
wg.Done()
}()
}
if scopeMap[FILES] || scopeMap[SUBMODULES] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshFilesAndSubmodules()
} else {
gui.refreshFilesAndSubmodules()
}
wg.Done()
}()
}
if scopeMap[STASH] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshStashEntries()
} else {
gui.refreshStashEntries()
}
wg.Done()
}()
}
if scopeMap[TAGS] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshTags()
} else {
gui.refreshTags()
}
wg.Done()
}()
}
if scopeMap[REMOTES] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshRemotes()
} else {
gui.refreshRemotes()
}
wg.Done()
}()
}
wg.Wait()
gui.refreshStatus()
if options.then != nil {
options.then()
}
}
if options.mode == BLOCK_UI {
gui.g.Update(func(g *gocui.Gui) error {
f()
return nil
})
} else {
f()
}
return nil
}
func (gui *Gui) resetOrigin(v *gocui.View) error {
_ = v.SetCursor(0, 0)
return v.SetOrigin(0, 0)
}
func (gui *Gui) cleanString(s string) string {
output := string(bom.Clean([]byte(s)))
return utils.NormalizeLinefeeds(output)
}
func (gui *Gui) setViewContent(v *gocui.View, s string) {
v.Clear()
fmt.Fprint(v, gui.cleanString(s))
}
// renderString resets the origin of a view and sets its content
func (gui *Gui) renderString(viewName, s string) {
gui.g.Update(func(*gocui.Gui) error {
return gui.renderStringSync(viewName, s)
})
}
func (gui *Gui) renderStringSync(viewName, s string) error {
v, err := gui.g.View(viewName)
if err != nil {
return nil // return gracefully if view has been deleted
}
if err := v.SetOrigin(0, 0); err != nil {
return err
}
if err := v.SetCursor(0, 0); err != nil {
return err
}
gui.setViewContent(v, s)
return nil
}
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
optionsArray := make([]string, 0)
for key, description := range optionsMap {
optionsArray = append(optionsArray, key+": "+description)
}
sort.Strings(optionsArray)
return strings.Join(optionsArray, ", ")
}
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) {
gui.renderString("options", gui.optionsMapToString(optionsMap))
}
// TODO: refactor properly
// i'm so sorry but had to add this getBranchesView
func (gui *Gui) getFilesView() *gocui.View {
v, _ := gui.g.View("files")
return v
}
func (gui *Gui) getCommitsView() *gocui.View {
v, _ := gui.g.View("commits")
return v
}
func (gui *Gui) getCommitMessageView() *gocui.View {
v, _ := gui.g.View("commitMessage")
return v
}
func (gui *Gui) getBranchesView() *gocui.View {
v, _ := gui.g.View("branches")
return v
}
func (gui *Gui) getMainView() *gocui.View {
v, _ := gui.g.View("main")
return v
}
func (gui *Gui) getSecondaryView() *gocui.View {
v, _ := gui.g.View("secondary")
return v
}
func (gui *Gui) getStashView() *gocui.View {
v, _ := gui.g.View("stash")
return v
}
func (gui *Gui) getCommitFilesView() *gocui.View {
v, _ := gui.g.View("commitFiles")
return v
}
func (gui *Gui) getMenuView() *gocui.View {
v, _ := gui.g.View("menu")
return v
}
func (gui *Gui) getSearchView() *gocui.View {
v, _ := gui.g.View("search")
return v
}
func (gui *Gui) getStatusView() *gocui.View {
v, _ := gui.g.View("status")
return v
}
func (gui *Gui) getConfirmationView() *gocui.View {
v, _ := gui.g.View("confirmation")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
func (gui *Gui) currentViewName() string {
currentView := gui.g.CurrentView()
if currentView == nil {
return ""
}
return currentView.Name()
}
func (gui *Gui) resizeCurrentPopupPanel() error {
v := gui.g.CurrentView()
if v == nil {
return nil
}
if gui.isPopupPanel(v.Name()) {
return gui.resizePopupPanel(v)
}
return nil
}
func (gui *Gui) resizePopupPanel(v *gocui.View) error {
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
content := v.Buffer()
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
_, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
func (gui *Gui) changeSelectedLine(panelState IListPanelState, total int, change int) {
// TODO: find out why we're doing this
line := panelState.GetSelectedLineIdx()
if line == -1 {
return
}
var newLine int
if line+change < 0 {
newLine = 0
} else if line+change >= total {
newLine = total - 1
} else {
newLine = line + change
}
panelState.SetSelectedLineIdx(newLine)
}
func (gui *Gui) refreshSelectedLine(panelState IListPanelState, total int) {
line := panelState.GetSelectedLineIdx()
if line == -1 && total > 0 {
panelState.SetSelectedLineIdx(0)
} else if total-1 < line {
panelState.SetSelectedLineIdx(total - 1)
}
}
func (gui *Gui) renderDisplayStrings(v *gocui.View, displayStrings [][]string) {
gui.g.Update(func(g *gocui.Gui) error {
list := utils.RenderDisplayStrings(displayStrings)
v.Clear()
fmt.Fprint(v, list)
return nil
})
}
func (gui *Gui) globalOptionsMap() map[string]string {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
return map[string]string{
fmt.Sprintf("%s/%s", gui.getKeyDisplay(keybindingConfig.Universal.ScrollUpMain), gui.getKeyDisplay(keybindingConfig.Universal.ScrollDownMain)): gui.Tr.LcScroll,
fmt.Sprintf("%s %s %s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock), gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.Tr.LcNavigate,
gui.getKeyDisplay(keybindingConfig.Universal.Return): gui.Tr.LcCancel,
gui.getKeyDisplay(keybindingConfig.Universal.Quit): gui.Tr.LcQuit,
gui.getKeyDisplay(keybindingConfig.Universal.OptionMenu): gui.Tr.LcMenu,
"1-5": gui.Tr.LcJump,
}
}
func (gui *Gui) isPopupPanel(viewName string) bool {
return viewName == "commitMessage" || viewName == "credentials" || viewName == "confirmation" || viewName == "menu"
}
func (gui *Gui) popupPanelFocused() bool {
return gui.isPopupPanel(gui.currentViewName())
}
// often gocui wants functions in the form `func(g *gocui.Gui, v *gocui.View) error`
// but sometimes we just have a function that returns an error, so this is a
// convenience wrapper to give gocui what it wants.
func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return f()
}
}
// secondaryViewFocused tells us whether it appears that the secondary view is focused. The view is actually never focused for real: we just swap the main and secondary views and then you're still focused on the main view so that we can give you access to all its keybindings for free. I will probably regret this design decision soon enough.
func (gui *Gui) secondaryViewFocused() bool {
return gui.State.Panels.LineByLine != nil && gui.State.Panels.LineByLine.SecondaryFocused
}
func (gui *Gui) clearEditorView(v *gocui.View) {
v.Clear()
_ = v.SetCursor(0, 0)
_ = v.SetOrigin(0, 0)
}
func (gui *Gui) onViewTabClick(viewName string, tabIndex int) error {
context := gui.ViewTabContextMap[viewName][tabIndex].contexts[0]
return gui.switchContext(context)
}
func (gui *Gui) handleNextTab(g *gocui.Gui, v *gocui.View) error {
return gui.onViewTabClick(
v.Name(),
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevTab(g *gocui.Gui, v *gocui.View) error {
return gui.onViewTabClick(
v.Name(),
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
// this is the distance we will move the cursor when paging up or down in a view
func (gui *Gui) pageDelta(view *gocui.View) int {
_, height := view.Size()
delta := height - 1
if delta == 0 {
return 1
}
return delta
}