1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-21 21:47:32 +02:00

Merge pull request #280 from jesseduffield/hotfix/cursor-positioning

Fix cursor positioning bugs
This commit is contained in:
Jesse Duffield 2018-09-19 19:55:14 +10:00 committed by GitHub
commit 3072c93e13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 643 additions and 264 deletions

View File

@ -14,9 +14,9 @@ type Branch struct {
Recency string
}
// GetDisplayString returns the dispaly string of branch
func (b *Branch) GetDisplayString() string {
return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor())
// GetDisplayStrings returns the dispaly string of branch
func (b *Branch) GetDisplayStrings() []string {
return []string{b.Recency, utils.ColoredString(b.Name, b.GetColor())}
}
// GetColor branch color

26
pkg/commands/commit.go Normal file
View File

@ -0,0 +1,26 @@
package commands
import (
"github.com/fatih/color"
)
// Commit : A git commit
type Commit struct {
Sha string
Name string
Pushed bool
DisplayString string
}
func (c *Commit) GetDisplayStrings() []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
white := color.New(color.FgWhite)
shaColor := yellow
if c.Pushed {
shaColor = red
}
return []string{shaColor.Sprint(c.Sha), white.Sprint(c.Name)}
}

36
pkg/commands/file.go Normal file
View File

@ -0,0 +1,36 @@
package commands
import "github.com/fatih/color"
// File : A file from git status
// duplicating this for now
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
}
// GetDisplayStrings returns the display string of a file
func (f *File) GetDisplayStrings() []string {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !f.Tracked && !f.HasStagedChanges {
return []string{red.Sprint(f.DisplayString)}
}
output := green.Sprint(f.DisplayString[0:1])
output += red.Sprint(f.DisplayString[1:3])
if f.HasUnstagedChanges {
output += red.Sprint(f.Name)
} else {
output += green.Sprint(f.Name)
}
return []string{output}
}

View File

@ -105,17 +105,17 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer)
}
// GetStashEntries stash entryies
func (c *GitCommand) GetStashEntries() []StashEntry {
func (c *GitCommand) GetStashEntries() []*StashEntry {
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
stashEntries := []StashEntry{}
stashEntries := []*StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
func stashEntryFromLine(line string, index int) StashEntry {
return StashEntry{
func stashEntryFromLine(line string, index int) *StashEntry {
return &StashEntry{
Name: line,
Index: index,
DisplayString: line,
@ -128,10 +128,10 @@ func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
}
// GetStatusFiles git status files
func (c *GitCommand) GetStatusFiles() []File {
func (c *GitCommand) GetStatusFiles() []*File {
statusOutput, _ := c.GitStatus()
statusStrings := utils.SplitLines(statusOutput)
files := []File{}
files := []*File{}
for _, statusString := range statusStrings {
change := statusString[0:2]
@ -141,7 +141,7 @@ func (c *GitCommand) GetStatusFiles() []File {
_, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
file := File{
file := &File{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !hasNoStagedChanges,
@ -169,7 +169,7 @@ func (c *GitCommand) StashSave(message string) error {
}
// MergeStatusFiles merge status files
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
if len(oldFiles) == 0 {
return newFiles
}
@ -177,7 +177,7 @@ func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
appendedIndexes := []int{}
// retain position of files we already could see
result := []File{}
result := []*File{}
for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles {
if oldFile.Name == newFile.Name {
@ -231,9 +231,9 @@ func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
// getCommitsToPush Returns the sha's of the commits that have not yet been pushed
// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch, a map is returned to ease look up
func (c *GitCommand) getCommitsToPush() map[string]bool {
func (c *GitCommand) GetCommitsToPush() map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
if err != nil {
@ -413,7 +413,7 @@ func (c *GitCommand) IsInMergeState() (bool, error) {
}
// RemoveFile directly
func (c *GitCommand) RemoveFile(file File) error {
func (c *GitCommand) RemoveFile(file *File) error {
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges {
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", file.Name)); err != nil {
@ -460,16 +460,16 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
}
// GetCommits obtains the commits of the current branch
func (c *GitCommand) GetCommits() []Commit {
pushables := c.getCommitsToPush()
func (c *GitCommand) GetCommits() []*Commit {
pushables := c.GetCommitsToPush()
log := c.GetLog()
commits := []Commit{}
commits := []*Commit{}
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
_, pushed := pushables[sha]
commits = append(commits, Commit{
commits = append(commits, &Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Pushed: pushed,
@ -508,7 +508,7 @@ func (c *GitCommand) Show(sha string) string {
}
// Diff returns the diff of a file
func (c *GitCommand) Diff(file File) string {
func (c *GitCommand) Diff(file *File) string {
cachedArg := ""
fileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges && !file.HasUnstagedChanges {

View File

@ -1,32 +1,5 @@
package commands
// File : A staged/unstaged file
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
}
// Commit : A git commit
type Commit struct {
Sha string
Name string
Pushed bool
DisplayString string
}
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// Conflict : A git conflict with a start middle and end corresponding to line
// numbers in the file where the conflict bars appear
type Conflict struct {

View File

@ -276,7 +276,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]StashEntry)
test func([]*StashEntry)
}
scenarios := []scenario{
@ -285,7 +285,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
func(string, ...string) *exec.Cmd {
return exec.Command("echo")
},
func(entries []StashEntry) {
func(entries []*StashEntry) {
assert.Len(t, entries, 0)
},
},
@ -294,8 +294,8 @@ func TestGitCommandGetStashEntries(t *testing.T) {
func(string, ...string) *exec.Cmd {
return exec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
},
func(entries []StashEntry) {
expected := []StashEntry{
func(entries []*StashEntry) {
expected := []*StashEntry{
{
0,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
@ -342,7 +342,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]File)
test func([]*File)
}
scenarios := []scenario{
@ -351,7 +351,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo")
},
func(files []File) {
func(files []*File) {
assert.Len(t, files, 0)
},
},
@ -363,10 +363,10 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt",
)
},
func(files []File) {
func(files []*File) {
assert.Len(t, files, 4)
expected := []File{
expected := []*File{
{
Name: "file1.txt",
HasStagedChanges: true,
@ -464,22 +464,22 @@ func TestGitCommandCommitAmend(t *testing.T) {
func TestGitCommandMergeStatusFiles(t *testing.T) {
type scenario struct {
testName string
oldFiles []File
newFiles []File
test func([]File)
oldFiles []*File
newFiles []*File
test func([]*File)
}
scenarios := []scenario{
{
"Old file and new file are the same",
[]File{},
[]File{
[]*File{},
[]*File{
{
Name: "new_file.txt",
},
},
func(files []File) {
expected := []File{
func(files []*File) {
expected := []*File{
{
Name: "new_file.txt",
},
@ -491,7 +491,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
},
{
"Several files to merge, with some identical",
[]File{
[]*File{
{
Name: "new_file1.txt",
},
@ -502,7 +502,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
Name: "new_file3.txt",
},
},
[]File{
[]*File{
{
Name: "new_file4.txt",
},
@ -513,8 +513,8 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
Name: "new_file1.txt",
},
},
func(files []File) {
expected := []File{
func(files []*File) {
expected := []*File{
{
Name: "new_file1.txt",
},
@ -631,7 +631,7 @@ func TestGitCommandGetCommitsToPush(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.getCommitsToPush())
s.test(gitCmd.GetCommitsToPush())
})
}
}
@ -1225,7 +1225,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
testName string
command func() (func(string, ...string) *exec.Cmd, *[][]string)
test func(*[][]string, error)
file File
file *File
removeFile func(string) error
}
@ -1247,7 +1247,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"reset", "--", "test"},
})
},
File{
&File{
Name: "test",
HasStagedChanges: true,
},
@ -1270,7 +1270,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
assert.EqualError(t, err, "an error occurred when removing file")
assert.Len(t, *cmdsCalled, 0)
},
File{
&File{
Name: "test",
Tracked: false,
},
@ -1295,7 +1295,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"checkout", "--", "test"},
})
},
File{
&File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
@ -1321,7 +1321,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"checkout", "--", "test"},
})
},
File{
&File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
@ -1348,7 +1348,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"checkout", "--", "test"},
})
},
File{
&File{
Name: "test",
Tracked: true,
HasStagedChanges: true,
@ -1374,7 +1374,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
{"reset", "--", "test"},
})
},
File{
&File{
Name: "test",
Tracked: false,
HasStagedChanges: true,
@ -1398,7 +1398,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, *cmdsCalled, 0)
},
File{
&File{
Name: "test",
Tracked: false,
HasStagedChanges: false,
@ -1484,7 +1484,7 @@ func TestGitCommandGetCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]Commit)
test func([]*Commit)
}
scenarios := []scenario{
@ -1504,7 +1504,7 @@ func TestGitCommandGetCommits(t *testing.T) {
return nil
},
func(commits []Commit) {
func(commits []*Commit) {
assert.Len(t, commits, 0)
},
},
@ -1524,9 +1524,9 @@ func TestGitCommandGetCommits(t *testing.T) {
return nil
},
func(commits []Commit) {
func(commits []*Commit) {
assert.Len(t, commits, 2)
assert.EqualValues(t, []Commit{
assert.EqualValues(t, []*Commit{
{
Sha: "8a2bb0e",
Name: "commit 1",
@ -1557,7 +1557,7 @@ func TestGitCommandDiff(t *testing.T) {
gitCommand := newDummyGitCommand()
assert.NoError(t, test.GenerateRepo("lots_of_diffs.sh"))
files := []File{
files := []*File{
{
Name: "deleted_staged",
HasStagedChanges: false,

View File

@ -0,0 +1,13 @@
package commands
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// GetDisplayStrings returns the dispaly string of branch
func (s *StashEntry) GetDisplayStrings() []string {
return []string{s.DisplayString}
}

View File

@ -34,7 +34,7 @@ func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*
}, nil
}
func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
// I used go-git for this, but that breaks if you've just done a git init,
// even though you're on 'master'
branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
@ -44,11 +44,11 @@ func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
panic(err.Error())
}
}
return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
return &commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
}
func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
branches := make([]commands.Branch, 0)
func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
branches := make([]*commands.Branch, 0)
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil {
return branches
@ -58,14 +58,14 @@ func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch)
}
return branches
}
func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
branches := make([]commands.Branch, 0)
func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
branches := make([]*commands.Branch, 0)
bIter, err := b.GitCommand.Repo.Branches()
if err != nil {
@ -73,14 +73,14 @@ func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
}
err = bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short()
branches = append(branches, commands.Branch{Name: name})
branches = append(branches, &commands.Branch{Name: name})
return nil
})
return branches
}
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []commands.Branch, included bool) []commands.Branch {
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*commands.Branch, included bool) []*commands.Branch {
for _, newBranch := range newBranches {
if included == branchIncluded(newBranch.Name, existingBranches) {
finalBranches = append(finalBranches, newBranch)
@ -89,7 +89,7 @@ func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existi
return finalBranches
}
func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string {
func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string {
for _, safeBranch := range safeBranches {
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
return safeBranch.Name
@ -99,15 +99,15 @@ func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.B
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []commands.Branch {
branches := make([]commands.Branch, 0)
func (b *BranchListBuilder) Build() []*commands.Branch {
branches := make([]*commands.Branch, 0)
head := b.obtainCurrentBranch()
safeBranches := b.obtainSafeBranches()
if len(safeBranches) == 0 {
return append(branches, head)
}
reflogBranches := b.obtainReflogBranches()
reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...))
reflogBranches = uniqueByName(append([]*commands.Branch{head}, reflogBranches...))
for i, reflogBranch := range reflogBranches {
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
}
@ -118,7 +118,7 @@ func (b *BranchListBuilder) Build() []commands.Branch {
return branches
}
func branchIncluded(branchName string, branches []commands.Branch) bool {
func branchIncluded(branchName string, branches []*commands.Branch) bool {
for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true
@ -127,8 +127,8 @@ func branchIncluded(branchName string, branches []commands.Branch) bool {
return false
}
func uniqueByName(branches []commands.Branch) []commands.Branch {
finalBranches := make([]commands.Branch, 0)
func uniqueByName(branches []*commands.Branch) []*commands.Branch {
finalBranches := make([]*commands.Branch, 0)
for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) {
continue

View File

@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
@ -109,7 +110,7 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch {
func (gui *Gui) getSelectedBranch(v *gocui.View) *commands.Branch {
lineNumber := gui.getItemPosition(v)
return gui.State.Branches[lineNumber]
}
@ -151,10 +152,15 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error {
return err
}
gui.State.Branches = builder.Build()
v.Clear()
for _, branch := range gui.State.Branches {
fmt.Fprintln(v, branch.GetDisplayString())
list, err := utils.RenderList(gui.State.Branches)
if err != nil {
return err
}
fmt.Fprint(v, list)
gui.resetOrigin(v)
return gui.refreshStatus(g)
})

View File

@ -2,10 +2,11 @@ package gui
import (
"errors"
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
@ -15,20 +16,14 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
if err != nil {
panic(err)
}
v.Clear()
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
white := color.New(color.FgWhite)
shaColor := white
for _, commit := range gui.State.Commits {
if commit.Pushed {
shaColor = red
} else {
shaColor = yellow
}
shaColor.Fprint(v, commit.Sha+" ")
white.Fprintln(v, commit.Name)
list, err := utils.RenderList(gui.State.Commits)
if err != nil {
return err
}
fmt.Fprint(v, list)
gui.refreshStatus(g)
if g.CurrentView().Name() == "commits" {
gui.handleCommitSelect(g, v)
@ -99,7 +94,7 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
}
// TODO: move to files panel
func (gui *Gui) anyUnStagedChanges(files []commands.File) bool {
func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool {
for _, file := range files {
if file.Tracked && file.HasUnstagedChanges {
return true
@ -161,13 +156,13 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) getSelectedCommit(g *gocui.Gui) (commands.Commit, error) {
func (gui *Gui) getSelectedCommit(g *gocui.Gui) (*commands.Commit, error) {
v, err := g.View("commits")
if err != nil {
panic(err)
}
if len(gui.State.Commits) == 0 {
return commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
return &commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
}
lineNumber := gui.getItemPosition(v)
if lineNumber > len(gui.State.Commits)-1 {

View File

@ -7,16 +7,17 @@ import (
// "strings"
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) stagedFiles() []commands.File {
func (gui *Gui) stagedFiles() []*commands.File {
files := gui.State.Files
result := make([]commands.File, 0)
result := make([]*commands.File, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
@ -25,9 +26,9 @@ func (gui *Gui) stagedFiles() []commands.File {
return result
}
func (gui *Gui) trackedFiles() []commands.File {
func (gui *Gui) trackedFiles() []*commands.File {
files := gui.State.Files
result := make([]commands.File, 0)
result := make([]*commands.File, 0)
for _, file := range files {
if file.Tracked {
result = append(result, file)
@ -116,9 +117,9 @@ func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
return gui.Errors.ErrSubProcess
}
func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) {
func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
if len(gui.State.Files) == 0 {
return commands.File{}, gui.Errors.ErrNoFiles
return &commands.File{}, gui.Errors.ErrNoFiles
}
filesView, err := g.View("files")
if err != nil {
@ -184,7 +185,9 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
return gui.renderfilesOptions(g, nil)
}
gui.renderfilesOptions(g, &file)
if err := gui.renderfilesOptions(g, file); err != nil {
return err
}
var content string
if file.HasMergeConflicts {
return gui.refreshMergePanel(g)
@ -275,24 +278,6 @@ func (gui *Gui) updateHasMergeConflictStatus() error {
return nil
}
func (gui *Gui) renderFile(file commands.File, filesView *gocui.View) {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !file.Tracked && !file.HasStagedChanges {
red.Fprintln(filesView, file.DisplayString)
return
}
green.Fprint(filesView, file.DisplayString[0:1])
red.Fprint(filesView, file.DisplayString[1:3])
if file.HasUnstagedChanges {
red.Fprintln(filesView, file.Name)
} else {
green.Fprintln(filesView, file.Name)
}
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
item, err := gui.getSelectedFile(g)
if err != nil {
@ -318,10 +303,14 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error {
return err
}
gui.refreshStateFiles()
filesView.Clear()
for _, file := range gui.State.Files {
gui.renderFile(file, filesView)
list, err := utils.RenderList(gui.State.Files)
if err != nil {
return err
}
fmt.Fprint(filesView, list)
gui.correctCursor(filesView)
if filesView == g.CurrentView() {
gui.handleFileSelect(g, filesView)

View File

@ -71,10 +71,10 @@ type Gui struct {
}
type guiState struct {
Files []commands.File
Branches []commands.Branch
Commits []commands.Commit
StashEntries []commands.StashEntry
Files []*commands.File
Branches []*commands.Branch
Commits []*commands.Commit
StashEntries []*commands.StashEntry
PreviousView string
HasMergeConflicts bool
ConflictIndex int
@ -83,17 +83,16 @@ type guiState struct {
EditHistory *stack.Stack
Platform commands.Platform
Updating bool
Keys []Binding
}
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
initialState := guiState{
Files: make([]commands.File, 0),
Files: make([]*commands.File, 0),
PreviousView: "files",
Commits: make([]commands.Commit, 0),
StashEntries: make([]commands.StashEntry, 0),
Commits: make([]*commands.Commit, 0),
StashEntries: make([]*commands.StashEntry, 0),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: make([]commands.Conflict, 0),

View File

@ -1,6 +1,8 @@
package gui
import "github.com/jesseduffield/gocui"
import (
"github.com/jesseduffield/gocui"
)
// Binding - a keybinding mapping a key and modifier to a handler. The keypress
// is only handled if the given view has focus, or handled globally if the view
@ -14,8 +16,26 @@ type Binding struct {
Description string
}
func (gui *Gui) GetKeybindings() []Binding {
bindings := []Binding{
// GetDisplayStrings returns the display string of a file
func (b *Binding) GetDisplayStrings() []string {
return []string{b.GetKey(), b.Description}
}
func (b *Binding) GetKey() string {
r, ok := b.Key.(rune)
key := ""
if ok {
key = string(r)
} else if b.KeyReadable != "" {
key = b.KeyReadable
}
return key
}
func (gui *Gui) GetKeybindings() []*Binding {
bindings := []*Binding{
{
ViewName: "",
Key: 'q',
@ -73,7 +93,7 @@ func (gui *Gui) GetKeybindings() []Binding {
ViewName: "",
Key: 'x',
Modifier: gocui.ModNone,
Handler: gui.handleMenu,
Handler: gui.handleCreateOptionsMenu,
}, {
ViewName: "status",
Key: 'e',
@ -349,18 +369,13 @@ func (gui *Gui) GetKeybindings() []Binding {
Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
}, {
ViewName: "menu",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleMenuPress,
},
}
// Would make these keybindings global but that interferes with editing
// input in the confirmation panel
for _, viewName := range []string{"status", "files", "branches", "commits", "stash", "menu"} {
bindings = append(bindings, []Binding{
bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},

View File

@ -8,21 +8,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) handleMenuPress(g *gocui.Gui, v *gocui.View) error {
lineNumber := gui.getItemPosition(v)
if gui.State.Keys[lineNumber].Key == nil {
return nil
}
if len(gui.State.Keys) > lineNumber {
err := gui.handleMenuClose(g, v)
if err != nil {
return err
}
return gui.State.Keys[lineNumber].Handler(g, v)
}
return nil
}
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
// doing nothing for now
// but it is needed for switch in newLineFocused
@ -39,9 +24,9 @@ func (gui *Gui) renderMenuOptions(g *gocui.Gui) error {
}
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
// better to delete because for example after closing update confirmation panel,
// the focus isn't set back to any of panels and one is unable to even quit
//_, err := g.SetViewOnBottom(v.Name())
if err := g.DeleteKeybinding("menu", gocui.KeySpace, gocui.ModNone); err != nil {
return err
}
err := g.DeleteView("menu")
if err != nil {
return err
@ -49,83 +34,38 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
return gui.returnFocus(g, v)
}
func (gui *Gui) GetKey(binding Binding) string {
r, ok := binding.Key.(rune)
key := ""
if ok {
key = string(r)
} else if binding.KeyReadable != "" {
key = binding.KeyReadable
}
return key
}
func (gui *Gui) GetMaxKeyLength(bindings []Binding) int {
max := 0
for _, binding := range bindings {
keyLength := len(gui.GetKey(binding))
if keyLength > max {
max = keyLength
}
}
return max
}
func (gui *Gui) handleMenu(g *gocui.Gui, v *gocui.View) error {
var (
contentGlobal, contentPanel []string
bindingsGlobal, bindingsPanel []Binding
)
// clear keys slice, so we don't have ghost elements
gui.State.Keys = gui.State.Keys[:0]
bindings := gui.GetKeybindings()
padWidth := gui.GetMaxKeyLength(bindings)
for _, binding := range bindings {
key := gui.GetKey(binding)
if key != "" && binding.Description != "" {
content := fmt.Sprintf("%s %s", utils.WithPadding(key, padWidth), binding.Description)
switch binding.ViewName {
case "":
contentGlobal = append(contentGlobal, content)
bindingsGlobal = append(bindingsGlobal, binding)
case v.Name():
contentPanel = append(contentPanel, content)
bindingsPanel = append(bindingsPanel, binding)
}
}
}
// append dummy element to have a separator between
// panel and global keybindings
contentPanel = append(contentPanel, "")
bindingsPanel = append(bindingsPanel, Binding{})
content := append(contentPanel, contentGlobal...)
gui.State.Keys = append(bindingsPanel, bindingsGlobal...)
// append newline at the end so the last line would be selectable
contentJoined := strings.Join(content, "\n") + "\n"
// y1-1 so there will not be an extra space at the end of panel
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, contentJoined)
menuView, _ := g.SetView("menu", x0, y0, x1, y1-1, 0)
menuView.Title = strings.Title(gui.Tr.SLocalize("menu"))
menuView.FgColor = gocui.ColorWhite
if err := gui.renderMenuOptions(g); err != nil {
func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error {
list, err := utils.RenderList(items)
if err != nil {
return err
}
fmt.Fprint(menuView, contentJoined)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = strings.Title(gui.Tr.SLocalize("menu"))
menuView.FgColor = gocui.ColorWhite
menuView.Clear()
fmt.Fprint(menuView, list)
g.Update(func(g *gocui.Gui) error {
_, err := g.SetViewOnTop("menu")
if err != nil {
if err := gui.renderMenuOptions(gui.g); err != nil {
return err
}
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
lineNumber := gui.getItemPosition(v)
return handlePress(lineNumber)
}
if err := gui.g.SetKeybinding("menu", gocui.KeySpace, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
gui.g.Update(func(g *gocui.Gui) error {
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
}
return gui.switchFocus(g, v, menuView)
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView)
})
return nil
}

View File

@ -0,0 +1,51 @@
package gui
import (
"errors"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
var (
bindingsGlobal, bindingsPanel []*Binding
)
bindings := gui.GetKeybindings()
for _, binding := range bindings {
if binding.GetKey() != "" && binding.Description != "" {
switch binding.ViewName {
case "":
bindingsGlobal = append(bindingsGlobal, binding)
case v.Name():
bindingsPanel = append(bindingsPanel, binding)
}
}
}
// append dummy element to have a separator between
// panel and global keybindings
bindingsPanel = append(bindingsPanel, &Binding{})
return append(bindingsPanel, bindingsGlobal...)
}
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
bindings := gui.getBindings(v)
handleOptionsMenuPress := func(index int) error {
if bindings[index].Key == nil {
return nil
}
if index <= len(bindings) {
return errors.New("Index is greater than size of bindings")
}
err := gui.handleMenuClose(g, v)
if err != nil {
return err
}
return bindings[index].Handler(g, v)
}
return gui.createMenu(bindings, handleOptionsMenuPress)
}

View File

@ -5,6 +5,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
@ -14,10 +15,14 @@ func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
panic(err)
}
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
v.Clear()
for _, stashEntry := range gui.State.StashEntries {
fmt.Fprintln(v, stashEntry.DisplayString)
list, err := utils.RenderList(gui.State.StashEntries)
if err != nil {
return err
}
fmt.Fprint(v, list)
return gui.resetOrigin(v)
})
return nil
@ -29,7 +34,7 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
}
stashView, _ := gui.g.View("stash")
lineNumber := gui.getItemPosition(stashView)
return &gui.State.StashEntries[lineNumber]
return gui.State.StashEntries[lineNumber]
}
func (gui *Gui) renderStashOptions(g *gocui.Gui) error {

View File

@ -160,7 +160,6 @@ func (gui *Gui) getItemPosition(v *gocui.View) int {
func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" {
return nil
}
@ -179,19 +178,28 @@ func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" {
return nil
}
cx, cy := v.Cursor()
ox, oy := v.Origin()
if cy+oy >= len(v.BufferLines())-2 {
ly := len(v.BufferLines()) - 1
_, height := v.Size()
maxY := height - 1
// if we are at the end we just return
if cy+oy == ly {
return nil
}
if err := v.SetCursor(cx, cy+1); err != nil {
if err := v.SetOrigin(ox, oy+1); err != nil {
return err
}
var err error
if cy < maxY {
err = v.SetCursor(cx, cy+1)
} else {
err = v.SetOrigin(ox, oy+1)
}
if err != nil {
return err
}
gui.newLineFocused(g, v)
@ -208,10 +216,19 @@ 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) correctCursor(v *gocui.View) error {
cx, cy := v.Cursor()
_, oy := v.Origin()
lineCount := len(v.BufferLines()) - 2
if cy >= lineCount-oy {
return v.SetCursor(cx, lineCount-oy)
ox, oy := v.Origin()
_, height := v.Size()
maxY := height - 1
ly := len(v.BufferLines()) - 1
if oy+cy <= ly {
return nil
}
newCy := utils.Min(ly, maxY)
if err := v.SetCursor(cx, newCy); err != nil {
return err
}
if err := v.SetOrigin(ox, ly-newCy); err != nil {
return err
}
return nil
}

View File

@ -1,10 +1,12 @@
package utils
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"time"
@ -99,3 +101,106 @@ func ResolvePlaceholderString(str string, arguments map[string]string) string {
}
return str
}
// Min returns the minimum of two integers
func Min(x, y int) int {
if x < y {
return x
}
return y
}
type Displayable interface {
GetDisplayStrings() []string
}
// RenderList takes a slice of items, confirms they implement the Displayable
// interface, then generates a list of their displaystrings to write to a panel's
// buffer
func RenderList(slice interface{}) (string, error) {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
return "", errors.New("RenderList given a non-slice type")
}
displayables := make([]Displayable, s.Len())
for i := 0; i < s.Len(); i++ {
value, ok := s.Index(i).Interface().(Displayable)
if !ok {
return "", errors.New("item does not implement the Displayable interface")
}
displayables[i] = value
}
return renderDisplayableList(displayables)
}
// renderDisplayableList takes a list of displayable items, obtains their display
// strings via GetDisplayStrings() and then returns a single string containing
// each item's string representation on its own line, with appropriate horizontal
// padding between the item's own strings
func renderDisplayableList(items []Displayable) (string, error) {
if len(items) == 0 {
return "", nil
}
stringArrays := getDisplayStringArrays(items)
if !displayArraysAligned(stringArrays) {
return "", errors.New("Each item must return the same number of strings to display")
}
padWidths := getPadWidths(stringArrays)
paddedDisplayStrings := getPaddedDisplayStrings(stringArrays, padWidths)
return strings.Join(paddedDisplayStrings, "\n"), nil
}
func getPadWidths(stringArrays [][]string) []int {
if len(stringArrays[0]) <= 1 {
return []int{}
}
padWidths := make([]int, len(stringArrays[0])-1)
for i := range padWidths {
for _, strings := range stringArrays {
if len(strings[i]) > padWidths[i] {
padWidths[i] = len(strings[i])
}
}
}
return padWidths
}
func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string {
paddedDisplayStrings := make([]string, len(stringArrays))
for i, stringArray := range stringArrays {
if len(stringArray) == 0 {
continue
}
for j, padWidth := range padWidths {
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
}
paddedDisplayStrings[i] += stringArray[len(padWidths)]
}
return paddedDisplayStrings
}
// displayArraysAligned returns true if every string array returned from our
// list of displayables has the same length
func displayArraysAligned(stringArrays [][]string) bool {
for _, strings := range stringArrays {
if len(strings) != len(stringArrays[0]) {
return false
}
}
return true
}
func getDisplayStringArrays(displayables []Displayable) [][]string {
stringArrays := make([][]string, len(displayables))
for i, item := range displayables {
stringArrays[i] = item.GetDisplayStrings()
}
return stringArrays
}

View File

@ -1,6 +1,7 @@
package utils
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
@ -167,3 +168,211 @@ func TestResolvePlaceholderString(t *testing.T) {
assert.EqualValues(t, string(s.expected), ResolvePlaceholderString(s.templateString, s.arguments))
}
}
func TestDisplayArraysAligned(t *testing.T) {
type scenario struct {
input [][]string
expected bool
}
scenarios := []scenario{
{
[][]string{{"", ""}, {"", ""}},
true,
},
{
[][]string{{""}, {"", ""}},
false,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, displayArraysAligned(s.input))
}
}
type myDisplayable struct {
strings []string
}
type myStruct struct{}
func (d *myDisplayable) GetDisplayStrings() []string {
return d.strings
}
func TestGetDisplayStringArrays(t *testing.T) {
type scenario struct {
input []Displayable
expected [][]string
}
scenarios := []scenario{
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
[][]string{{"a", "b"}, {"c", "d"}},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getDisplayStringArrays(s.input))
}
}
func TestRenderDisplayableList(t *testing.T) {
type scenario struct {
input []Displayable
expectedString string
expectedError error
}
scenarios := []scenario{
{
[]Displayable{
Displayable(&myDisplayable{[]string{}}),
Displayable(&myDisplayable{[]string{}}),
},
"\n",
nil,
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"aa", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
"aa b\nc d",
nil,
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b", "c"}}),
},
"",
errors.New("Each item must return the same number of strings to display"),
},
}
for _, s := range scenarios {
str, err := renderDisplayableList(s.input)
assert.EqualValues(t, s.expectedString, str)
assert.EqualValues(t, s.expectedError, err)
}
}
func TestRenderList(t *testing.T) {
type scenario struct {
input interface{}
expectedString string
expectedError error
}
scenarios := []scenario{
{
[]*myDisplayable{
{[]string{"aa", "b"}},
{[]string{"c", "d"}},
},
"aa b\nc d",
nil,
},
{
[]*myStruct{
{},
{},
},
"",
errors.New("item does not implement the Displayable interface"),
},
{
&myStruct{},
"",
errors.New("RenderList given a non-slice type"),
},
}
for _, s := range scenarios {
str, err := RenderList(s.input)
assert.EqualValues(t, s.expectedString, str)
assert.EqualValues(t, s.expectedError, err)
}
}
func TestGetPaddedDisplayStrings(t *testing.T) {
type scenario struct {
stringArrays [][]string
padWidths []int
expected []string
}
scenarios := []scenario{
{
[][]string{{"a", "b"}, {"c", "d"}},
[]int{1},
[]string{"a b", "c d"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPaddedDisplayStrings(s.stringArrays, s.padWidths))
}
}
func TestGetPadWidths(t *testing.T) {
type scenario struct {
stringArrays [][]string
expected []int
}
scenarios := []scenario{
{
[][]string{{""}, {""}},
[]int{},
},
{
[][]string{{"a"}, {""}},
[]int{},
},
{
[][]string{{"aa", "b", "ccc"}, {"c", "d", "e"}},
[]int{2, 1},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPadWidths(s.stringArrays))
}
}
func TestMin(t *testing.T) {
type scenario struct {
a int
b int
expected int
}
scenarios := []scenario{
{
1,
1,
1,
},
{
1,
2,
1,
},
{
2,
1,
1,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, Min(s.a, s.b))
}
}