Compare commits

...
7 Commits
17 changed files with 693 additions and 298 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ require (
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
-4
View File
@@ -102,10 +102,6 @@ github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07 h1:BymGR28au
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772 h1:dg9krj10Udac4IcvlVCOAPktQkfggkgtqRmbDKk7Pzw=
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b h1:3+4+muhhikpls5FePXSRNFgcdoPx8dTdqaCy3AqLz98=
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064 h1:Oe+QJuUIOd2TU+A3BW5sT1eXqceoBcOOfyoHlGf7F8Y=
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
+10 -3
View File
@@ -16,8 +16,13 @@ func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
if err != nil {
return false, gui.surfaceError(err)
}
if sub != nil {
return false, gui.runSubprocessWithSuspense(sub)
if sub == nil {
return true, nil
}
err = gui.runSubprocessWithSuspense(sub)
if err != nil {
return false, err
}
return true, nil
}
@@ -36,12 +41,14 @@ func (gui *Gui) handleCommitConfirm() error {
if err != nil {
return err
}
_ = gui.returnFromContext()
if !ok {
return nil
}
gui.clearEditorView(gui.Views.CommitMessage)
_ = gui.returnFromContext()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
+2 -1
View File
@@ -48,7 +48,7 @@ func (gui *Gui) handleSubmitCredential() error {
return err
}
return gui.refreshSidePanels(refreshOptions{})
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleCloseCredentialsView() error {
@@ -78,6 +78,7 @@ func (gui *Gui) handleCredentialsPopup(cmdErr error) {
if strings.Contains(errMessage, "Invalid username, password or passphrase") {
errMessage = gui.Tr.PassUnameWrong
}
_ = gui.returnFromContext()
// we are not logging this error because it may contain a password or a passphrase
_ = gui.createErrorPanel(errMessage)
} else {
+4
View File
@@ -729,6 +729,10 @@ func (gui *Gui) pushFiles() error {
// if we have pullables we'll ask if the user wants to force push
currentBranch := gui.currentBranch()
if currentBranch == nil {
// need to wait for branches to refresh
return nil
}
if currentBranch.Pullables == "?" {
// see if we have this branch in our config with an upstream
+16 -11
View File
@@ -1,8 +1,6 @@
package filetree
import (
"os"
"path/filepath"
"sort"
"strings"
@@ -14,18 +12,17 @@ func BuildTreeFromFiles(files []*models.File) *FileNode {
var curr *FileNode
for _, file := range files {
split := strings.Split(file.Name, string(os.PathSeparator))
splitPath := split(file.Name)
curr = root
outer:
for i := range split {
for i := range splitPath {
var setFile *models.File
isFile := i == len(split)-1
isFile := i == len(splitPath)-1
if isFile {
setFile = file
}
path := filepath.Join(split[:i+1]...)
path := join(splitPath[:i+1])
for _, existingChild := range curr.Children {
if existingChild.Path == path {
curr = existingChild
@@ -61,17 +58,17 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
var curr *CommitFileNode
for _, file := range files {
split := strings.Split(file.Name, string(os.PathSeparator))
splitPath := split(file.Name)
curr = root
outer:
for i := range split {
for i := range splitPath {
var setFile *models.CommitFile
isFile := i == len(split)-1
isFile := i == len(splitPath)-1
if isFile {
setFile = file
}
path := filepath.Join(split[:i+1]...)
path := join(splitPath[:i+1])
for _, existingChild := range curr.Children {
if existingChild.Path == path {
@@ -108,3 +105,11 @@ func BuildFlatTreeFromFiles(files []*models.File) *FileNode {
return &FileNode{Children: sortedFiles}
}
func split(str string) []string {
return strings.Split(str, "/")
}
func join(strs []string) string {
return strings.Join(strs, "/")
}
+484
View File
@@ -0,0 +1,484 @@
package filetree
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestBuildTreeFromFiles(t *testing.T) {
scenarios := []struct {
name string
files []*models.File
expected *FileNode
}{
{
name: "no files",
files: []*models.File{},
expected: &FileNode{
Path: "",
Children: []*FileNode{},
},
},
{
name: "files in same directory",
files: []*models.File{
{
Name: "dir1/a",
},
{
Name: "dir1/b",
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{
Path: "dir1",
Children: []*FileNode{
{
File: &models.File{Name: "dir1/a"},
Path: "dir1/a",
},
{
File: &models.File{Name: "dir1/b"},
Path: "dir1/b",
},
},
},
},
},
},
{
name: "paths that can be compressed",
files: []*models.File{
{
Name: "dir1/a",
},
{
Name: "dir2/b",
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{
File: &models.File{Name: "dir1/a"},
Path: "dir1/a",
CompressionLevel: 1,
},
{
File: &models.File{Name: "dir2/b"},
Path: "dir2/b",
CompressionLevel: 1,
},
},
},
},
{
name: "paths that can be sorted",
files: []*models.File{
{
Name: "b",
},
{
Name: "a",
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{
File: &models.File{Name: "a"},
Path: "a",
},
{
File: &models.File{Name: "b"},
Path: "b",
},
},
},
},
{
name: "paths that can be sorted including a merge conflict file",
files: []*models.File{
{
Name: "b",
},
{
Name: "z",
HasMergeConflicts: true,
},
{
Name: "a",
},
},
expected: &FileNode{
Path: "",
// it is a little strange that we're not bubbling up our merge conflict
// here but we are technically still in in tree mode and that's the rule
Children: []*FileNode{
{
File: &models.File{Name: "a"},
Path: "a",
},
{
File: &models.File{Name: "b"},
Path: "b",
},
{
File: &models.File{Name: "z", HasMergeConflicts: true},
Path: "z",
},
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
result := BuildTreeFromFiles(s.files)
assert.EqualValues(t, s.expected, result)
})
}
}
func TestBuildFlatTreeFromFiles(t *testing.T) {
scenarios := []struct {
name string
files []*models.File
expected *FileNode
}{
{
name: "no files",
files: []*models.File{},
expected: &FileNode{
Path: "",
Children: []*FileNode{},
},
},
{
name: "files in same directory",
files: []*models.File{
{
Name: "dir1/a",
},
{
Name: "dir1/b",
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{
File: &models.File{Name: "dir1/a"},
Path: "dir1/a",
CompressionLevel: 0,
},
{
File: &models.File{Name: "dir1/b"},
Path: "dir1/b",
CompressionLevel: 0,
},
},
},
},
{
name: "paths that can be compressed",
files: []*models.File{
{
Name: "dir1/a",
},
{
Name: "dir2/b",
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{
File: &models.File{Name: "dir1/a"},
Path: "dir1/a",
CompressionLevel: 1,
},
{
File: &models.File{Name: "dir2/b"},
Path: "dir2/b",
CompressionLevel: 1,
},
},
},
},
{
name: "paths that can be sorted",
files: []*models.File{
{
Name: "b",
},
{
Name: "a",
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{
File: &models.File{Name: "a"},
Path: "a",
},
{
File: &models.File{Name: "b"},
Path: "b",
},
},
},
},
{
name: "paths that can be sorted including a merge conflict file",
files: []*models.File{
{
Name: "z",
HasMergeConflicts: true,
},
{
Name: "b",
},
{
Name: "a",
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{
File: &models.File{Name: "z", HasMergeConflicts: true},
Path: "z",
},
{
File: &models.File{Name: "a"},
Path: "a",
},
{
File: &models.File{Name: "b"},
Path: "b",
},
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
result := BuildFlatTreeFromFiles(s.files)
assert.EqualValues(t, s.expected, result)
})
}
}
func TestBuildTreeFromCommitFiles(t *testing.T) {
scenarios := []struct {
name string
files []*models.CommitFile
expected *CommitFileNode
}{
{
name: "no files",
files: []*models.CommitFile{},
expected: &CommitFileNode{
Path: "",
Children: []*CommitFileNode{},
},
},
{
name: "files in same directory",
files: []*models.CommitFile{
{
Name: "dir1/a",
},
{
Name: "dir1/b",
},
},
expected: &CommitFileNode{
Path: "",
Children: []*CommitFileNode{
{
Path: "dir1",
Children: []*CommitFileNode{
{
File: &models.CommitFile{Name: "dir1/a"},
Path: "dir1/a",
},
{
File: &models.CommitFile{Name: "dir1/b"},
Path: "dir1/b",
},
},
},
},
},
},
{
name: "paths that can be compressed",
files: []*models.CommitFile{
{
Name: "dir1/a",
},
{
Name: "dir2/b",
},
},
expected: &CommitFileNode{
Path: "",
Children: []*CommitFileNode{
{
File: &models.CommitFile{Name: "dir1/a"},
Path: "dir1/a",
CompressionLevel: 1,
},
{
File: &models.CommitFile{Name: "dir2/b"},
Path: "dir2/b",
CompressionLevel: 1,
},
},
},
},
{
name: "paths that can be sorted",
files: []*models.CommitFile{
{
Name: "b",
},
{
Name: "a",
},
},
expected: &CommitFileNode{
Path: "",
Children: []*CommitFileNode{
{
File: &models.CommitFile{Name: "a"},
Path: "a",
},
{
File: &models.CommitFile{Name: "b"},
Path: "b",
},
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
result := BuildTreeFromCommitFiles(s.files)
assert.EqualValues(t, s.expected, result)
})
}
}
func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
scenarios := []struct {
name string
files []*models.CommitFile
expected *CommitFileNode
}{
{
name: "no files",
files: []*models.CommitFile{},
expected: &CommitFileNode{
Path: "",
Children: []*CommitFileNode{},
},
},
{
name: "files in same directory",
files: []*models.CommitFile{
{
Name: "dir1/a",
},
{
Name: "dir1/b",
},
},
expected: &CommitFileNode{
Path: "",
Children: []*CommitFileNode{
{
File: &models.CommitFile{Name: "dir1/a"},
Path: "dir1/a",
CompressionLevel: 0,
},
{
File: &models.CommitFile{Name: "dir1/b"},
Path: "dir1/b",
CompressionLevel: 0,
},
},
},
},
{
name: "paths that can be compressed",
files: []*models.CommitFile{
{
Name: "dir1/a",
},
{
Name: "dir2/b",
},
},
expected: &CommitFileNode{
Path: "",
Children: []*CommitFileNode{
{
File: &models.CommitFile{Name: "dir1/a"},
Path: "dir1/a",
CompressionLevel: 1,
},
{
File: &models.CommitFile{Name: "dir2/b"},
Path: "dir2/b",
CompressionLevel: 1,
},
},
},
},
{
name: "paths that can be sorted",
files: []*models.CommitFile{
{
Name: "b",
},
{
Name: "a",
},
},
expected: &CommitFileNode{
Path: "",
Children: []*CommitFileNode{
{
File: &models.CommitFile{Name: "a"},
Path: "a",
},
{
File: &models.CommitFile{Name: "b"},
Path: "b",
},
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
result := BuildFlatTreeFromCommitFiles(s.files)
assert.EqualValues(t, s.expected, result)
})
}
}
+3 -8
View File
@@ -1,17 +1,12 @@
package filetree
import (
"os"
"strings"
)
type CollapsedPaths map[string]bool
func (cp CollapsedPaths) ExpandToPath(path string) {
// need every directory along the way
split := strings.Split(path, string(os.PathSeparator))
for i := range split {
dir := strings.Join(split[0:i+1], string(os.PathSeparator))
splitPath := split(path)
for i := range splitPath {
dir := join(splitPath[0 : i+1])
cp[dir] = false
}
}
+2 -6
View File
@@ -1,10 +1,6 @@
package filetree
import (
"os"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
@@ -169,8 +165,8 @@ func (s *CommitFileNode) AnyFile(test func(file *models.CommitFile) bool) bool {
}
func (s *CommitFileNode) NameAtDepth(depth int) string {
splitName := strings.Split(s.Path, string(os.PathSeparator))
name := filepath.Join(splitName[depth:]...)
splitName := split(s.Path)
name := join(splitName[depth:])
return name
}
+5 -8
View File
@@ -2,9 +2,6 @@ package filetree
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
@@ -171,18 +168,18 @@ func (s *FileNode) AnyFile(test func(file *models.File) bool) bool {
}
func (s *FileNode) NameAtDepth(depth int) string {
splitName := strings.Split(s.Path, string(os.PathSeparator))
name := filepath.Join(splitName[depth:]...)
splitName := split(s.Path)
name := join(splitName[depth:])
if s.File != nil && s.File.IsRename() {
splitPrevName := strings.Split(s.File.PreviousName, string(os.PathSeparator))
splitPrevName := split(s.File.PreviousName)
prevName := s.File.PreviousName
// if the file has just been renamed inside the same directory, we can shave off
// the prefix for the previous path too. Otherwise we'll keep it unchanged
sameParentDir := len(splitName) == len(splitPrevName) && filepath.Join(splitName[0:depth]...) == filepath.Join(splitPrevName[0:depth]...)
sameParentDir := len(splitName) == len(splitPrevName) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
if sameParentDir {
prevName = filepath.Join(splitPrevName[depth:]...)
prevName = join(splitPrevName[depth:])
}
return fmt.Sprintf("%s%s%s", prevName, " → ", name)
+2
View File
@@ -1,3 +1,5 @@
// +build !windows
package gui
import (
+150 -195
View File
@@ -24,21 +24,140 @@ func (gui *Gui) informationStr() string {
}
}
func (gui *Gui) createAllViews() error {
viewNameMappings := []struct {
viewPtr **gocui.View
name string
}{
{viewPtr: &gui.Views.Status, name: "status"},
{viewPtr: &gui.Views.Files, name: "files"},
{viewPtr: &gui.Views.Branches, name: "branches"},
{viewPtr: &gui.Views.Commits, name: "commits"},
{viewPtr: &gui.Views.Stash, name: "stash"},
{viewPtr: &gui.Views.CommitFiles, name: "commitFiles"},
{viewPtr: &gui.Views.Main, name: "main"},
{viewPtr: &gui.Views.Secondary, name: "secondary"},
{viewPtr: &gui.Views.Options, name: "options"},
{viewPtr: &gui.Views.AppStatus, name: "appStatus"},
{viewPtr: &gui.Views.Information, name: "information"},
{viewPtr: &gui.Views.Search, name: "search"},
{viewPtr: &gui.Views.SearchPrefix, name: "searchPrefix"},
{viewPtr: &gui.Views.CommitMessage, name: "commitMessage"},
{viewPtr: &gui.Views.Credentials, name: "credentials"},
{viewPtr: &gui.Views.Menu, name: "menu"},
{viewPtr: &gui.Views.Suggestions, name: "suggestions"},
{viewPtr: &gui.Views.Confirmation, name: "confirmation"},
{viewPtr: &gui.Views.Limit, name: "limit"},
}
var err error
for _, mapping := range viewNameMappings {
*mapping.viewPtr, err = gui.prepareView(mapping.name)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
}
gui.Views.Options.Frame = false
gui.Views.Options.FgColor = theme.OptionsColor
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
gui.Views.SearchPrefix.Frame = false
gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX)
gui.Views.Stash.Title = gui.Tr.StashTitle
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
gui.Views.Stash.ContainsList = true
gui.Views.Commits.Title = gui.Tr.CommitsTitle
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
gui.Views.Commits.ContainsList = true
gui.Views.CommitFiles.Title = gui.Tr.CommitFiles
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitFiles.ContainsList = true
gui.Views.Branches.Title = gui.Tr.BranchesTitle
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
gui.Views.Branches.ContainsList = true
gui.Views.Files.Highlight = true
gui.Views.Files.Title = gui.Tr.FilesTitle
gui.Views.Files.FgColor = theme.GocuiDefaultTextColor
gui.Views.Files.ContainsList = true
gui.Views.Secondary.Title = gui.Tr.DiffTitle
gui.Views.Secondary.Wrap = true
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.IgnoreCarriageReturns = true
gui.Views.Main.Title = gui.Tr.DiffTitle
gui.Views.Main.Wrap = true
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
gui.Views.Main.IgnoreCarriageReturns = true
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
gui.Views.Limit.Wrap = true
gui.Views.Status.Title = gui.Tr.StatusTitle
gui.Views.Status.FgColor = theme.GocuiDefaultTextColor
gui.Views.Search.BgColor = gocui.ColorDefault
gui.Views.Search.FgColor = gocui.ColorGreen
gui.Views.Search.Frame = false
gui.Views.Search.Editable = true
gui.Views.AppStatus.BgColor = gocui.ColorDefault
gui.Views.AppStatus.FgColor = gocui.ColorCyan
gui.Views.AppStatus.Frame = false
gui.Views.AppStatus.Visible = false
gui.Views.CommitMessage.Visible = false
gui.Views.CommitMessage.Title = gui.Tr.CommitMessage
gui.Views.CommitMessage.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitMessage.Editable = true
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
gui.Views.Confirmation.Visible = false
gui.Views.Credentials.Visible = false
gui.Views.Credentials.Title = gui.Tr.CredentialsUsername
gui.Views.Credentials.FgColor = theme.GocuiDefaultTextColor
gui.Views.Credentials.Editable = true
gui.Views.Suggestions.Visible = false
gui.Views.Menu.Visible = false
gui.Views.Information.BgColor = gocui.ColorDefault
gui.Views.Information.FgColor = gocui.ColorGreen
gui.Views.Information.Frame = false
if _, err := gui.g.SetCurrentView(gui.defaultSideContext().GetViewName()); err != nil {
return err
}
return nil
}
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
if !gui.ViewsSetup {
if err := gui.createAllViews(); err != nil {
return err
}
}
g.Highlight = true
width, height := g.Size()
minimumHeight := 9
minimumWidth := 10
var err error
gui.Views.Limit, err = g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
gui.Views.Limit.Wrap = true
_, err = g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Limit.Visible = height < minimumHeight || width < minimumWidth
@@ -98,140 +217,36 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return view, err
}
gui.Views.Main, err = setViewFromDimensions("main", "main", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Main.Title = gui.Tr.DiffTitle
gui.Views.Main.Wrap = true
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
gui.Views.Main.IgnoreCarriageReturns = true
args := []struct {
viewName string
windowName string
frame bool
}{
{viewName: "main", windowName: "main", frame: true},
{viewName: "secondary", windowName: "secondary", frame: true},
{viewName: "status", windowName: "status", frame: true},
{viewName: "files", windowName: "files", frame: true},
{viewName: "branches", windowName: "branches", frame: true},
{viewName: "commitFiles", windowName: gui.State.Contexts.CommitFiles.GetWindowName(), frame: true},
{viewName: "commits", windowName: "commits", frame: true},
{viewName: "stash", windowName: "stash", frame: true},
{viewName: "options", windowName: "options", frame: false},
{viewName: "searchPrefix", windowName: "searchPrefix", frame: false},
{viewName: "search", windowName: "search", frame: false},
{viewName: "appStatus", windowName: "appStatus", frame: false},
{viewName: "information", windowName: "information", frame: false},
}
gui.Views.Secondary, err = setViewFromDimensions("secondary", "secondary", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
for _, arg := range args {
_, err = setViewFromDimensions(arg.viewName, arg.windowName, arg.frame)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Secondary.Title = gui.Tr.DiffTitle
gui.Views.Secondary.Wrap = true
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.IgnoreCarriageReturns = true
}
if gui.Views.Status, err = setViewFromDimensions("status", "status", true); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Status.Title = gui.Tr.StatusTitle
gui.Views.Status.FgColor = theme.GocuiDefaultTextColor
}
gui.Views.Files, err = setViewFromDimensions("files", "files", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Files.Highlight = true
gui.Views.Files.Title = gui.Tr.FilesTitle
gui.Views.Files.FgColor = theme.GocuiDefaultTextColor
gui.Views.Files.ContainsList = true
}
gui.Views.Branches, err = setViewFromDimensions("branches", "branches", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Branches.Title = gui.Tr.BranchesTitle
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
gui.Views.Branches.ContainsList = true
}
gui.Views.CommitFiles, err = setViewFromDimensions("commitFiles", gui.State.Contexts.CommitFiles.GetWindowName(), true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.CommitFiles.Title = gui.Tr.CommitFiles
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitFiles.ContainsList = true
}
// if the commit files view is the view to be displayed for its window, we'll display it
gui.Views.CommitFiles.Visible = gui.getViewNameForWindow(gui.State.Contexts.CommitFiles.GetWindowName()) == "commitFiles"
gui.Views.Commits, err = setViewFromDimensions("commits", "commits", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Commits.Title = gui.Tr.CommitsTitle
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
gui.Views.Commits.ContainsList = true
}
gui.Views.Stash, err = setViewFromDimensions("stash", "stash", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Stash.Title = gui.Tr.StashTitle
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
gui.Views.Stash.ContainsList = true
}
if gui.Views.Options, err = setViewFromDimensions("options", "options", false); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Options.Frame = false
gui.Views.Options.FgColor = theme.OptionsColor
}
// this view takes up one character. Its only purpose is to show the slash when searching
if gui.Views.SearchPrefix, err = setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
gui.Views.SearchPrefix.Frame = false
gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX)
}
if gui.Views.Search, err = setViewFromDimensions("search", "search", false); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Search.BgColor = gocui.ColorDefault
gui.Views.Search.FgColor = gocui.ColorGreen
gui.Views.Search.Frame = false
gui.Views.Search.Editable = true
}
if gui.Views.AppStatus, err = setViewFromDimensions("appStatus", "appStatus", false); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.AppStatus.BgColor = gocui.ColorDefault
gui.Views.AppStatus.FgColor = gocui.ColorCyan
gui.Views.AppStatus.Frame = false
gui.Views.AppStatus.Visible = false
}
gui.Views.Information, err = setViewFromDimensions("information", "information", false)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Information.BgColor = gocui.ColorDefault
gui.Views.Information.FgColor = gocui.ColorGreen
gui.Views.Information.Frame = false
gui.renderString(gui.Views.Information, INFO_SECTION_PADDING+informationStr)
}
if gui.State.OldInformation != informationStr {
gui.setViewContent(gui.Views.Information, informationStr)
gui.State.OldInformation = informationStr
@@ -291,7 +306,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return gui.resizeCurrentPopupPanel()
}
func (gui *Gui) setHiddenView(viewName string) (*gocui.View, error) {
func (gui *Gui) prepareView(viewName string) (*gocui.View, error) {
// arbitrarily giving the view enough size so that we don't get an error, but
// it's expected that the view will be given the correct size before being shown
return gui.g.SetView(viewName, 0, 0, 10, 10, 0)
@@ -317,11 +332,6 @@ func (gui *Gui) onInitialViewsCreationForRepo() error {
}
func (gui *Gui) onInitialViewsCreation() error {
// creating some views which are hidden at the start but we need to exist so that we can set an initial ordering
if err := gui.createHiddenViews(); err != nil {
return err
}
// now we order the views (in order of bottom first)
layerOneViews := []*gocui.View{
// first layer. Ordering within this layer does not matter because there are
@@ -340,15 +350,15 @@ func (gui *Gui) onInitialViewsCreation() error {
gui.Views.AppStatus,
gui.Views.Information,
gui.Views.Search,
gui.Views.SearchPrefix,
gui.Views.SearchPrefix, // this view takes up one character. Its only purpose is to show the slash when searching
// popups. Ordering within this layer does not matter because there should
// only be one popup shown at a time
gui.Views.CommitMessage,
gui.Views.Credentials,
gui.Views.Menu,
gui.Views.Suggestions,
gui.Views.Confirmation,
gui.Views.Credentials,
// this guy will cover everything else when it appears
gui.Views.Limit,
@@ -397,58 +407,3 @@ func (gui *Gui) onInitialViewsCreation() error {
return nil
}
func (gui *Gui) createHiddenViews() error {
// doesn't matter where this view starts because it will be hidden
var err error
if gui.Views.CommitMessage, err = gui.setHiddenView("commitMessage"); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.CommitMessage.Visible = false
gui.Views.CommitMessage.Title = gui.Tr.CommitMessage
gui.Views.CommitMessage.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitMessage.Editable = true
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
}
// doesn't matter where this view starts because it will be hidden
if gui.Views.Credentials, err = gui.setHiddenView("credentials"); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Credentials.Visible = false
gui.Views.Credentials.Title = gui.Tr.CredentialsUsername
gui.Views.Credentials.FgColor = theme.GocuiDefaultTextColor
gui.Views.Credentials.Editable = true
}
// not worrying about setting attributes because that will be done when the view is actually shown
gui.Views.Confirmation, err = gui.setHiddenView("confirmation")
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Confirmation.Visible = false
}
// not worrying about setting attributes because that will be done when the view is actually shown
gui.Views.Suggestions, err = gui.setHiddenView("suggestions")
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Suggestions.Visible = false
}
// not worrying about setting attributes because that will be done when the view is actually shown
gui.Views.Menu, err = gui.setHiddenView("menu")
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Menu.Visible = false
}
return nil
}
+2 -12
View File
@@ -83,23 +83,13 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
gui.Log,
view,
func() {
// we could clear here, but that actually has the effect of causing a flicker
// where the view may contain no content momentarily as the gui refreshes.
// Instead, we're rewinding the write pointer so that we will just start
// overwriting the existing content from the top down. Once we've reached
// the end of the content do display, we call view.FlushStaleCells() to
// clear out the remaining content from the previous render.
view.Rewind()
view.Clear()
},
func() {
gui.g.Update(func(*gocui.Gui) error {
return nil
})
},
func() {
view.FlushStaleCells()
},
)
})
gui.viewBufferManagerMap[view.Name()] = manager
}
+1 -1
View File
@@ -71,7 +71,7 @@ func RunTests(
logf("path: %s", testPath)
// three retries at normal speed for the sake of flakey tests
speeds = append(speeds, 1, 1, 1, 0.5, 0.5)
speeds = append(speeds, 1)
for i, speed := range speeds {
logf("%s: attempting test at speed %f\n", test.Name, speed)
+5 -9
View File
@@ -33,13 +33,12 @@ type ViewBufferManager struct {
readLines chan int
// beforeStart is the function that is called before starting a new task
beforeStart func()
refreshView func()
flushStaleCells func()
beforeStart func()
refreshView func()
}
func NewViewBufferManager(log *logrus.Entry, writer io.Writer, beforeStart func(), refreshView func(), flushStaleCells func()) *ViewBufferManager {
return &ViewBufferManager{Log: log, writer: writer, beforeStart: beforeStart, refreshView: refreshView, flushStaleCells: flushStaleCells, readLines: make(chan int, 1024)}
func NewViewBufferManager(log *logrus.Entry, writer io.Writer, beforeStart func(), refreshView func()) *ViewBufferManager {
return &ViewBufferManager{Log: log, writer: writer, beforeStart: beforeStart, refreshView: refreshView, readLines: make(chan int, 1024)}
}
func (m *ViewBufferManager) ReadLines(n int) {
@@ -76,7 +75,7 @@ func (m *ViewBufferManager) NewCmdTask(r io.Reader, cmd *exec.Cmd, prefix string
loaded := false
go utils.Safe(func() {
ticker := time.NewTicker(time.Millisecond * 200)
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
select {
case <-ticker.C:
@@ -115,9 +114,6 @@ func (m *ViewBufferManager) NewCmdTask(r io.Reader, cmd *exec.Cmd, prefix string
default:
}
if !ok {
// if we're here then there's nothing left to scan from the source
// so we're at the EOF and can flush the stale content
m.flushStaleCells()
m.refreshView()
break outer
}
+5 -38
View File
@@ -518,20 +518,16 @@ func (v *View) writeCells(x, y int, cells []cell) {
// of functions like fmt.Fprintf, fmt.Fprintln, io.Copy, etc. Clear must
// be called to clear the view's buffer.
func (v *View) Write(p []byte) (n int, err error) {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.tainted = true
v.writeMutex.Lock()
v.makeWriteable(v.wx, v.wy)
v.writeRunes(bytes.Runes(p))
v.writeMutex.Unlock()
return len(p), nil
}
func (v *View) WriteRunes(p []rune) {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.tainted = true
// Fill with empty cells, if writing outside current view buffer
@@ -549,8 +545,6 @@ func (v *View) writeRunes(p []rune) {
for _, r := range p {
switch r {
case '\n':
// clear the rest of the line
v.lines[v.wy] = v.lines[v.wy][0:v.wx]
v.wy++
if v.wy >= len(v.lines) {
v.lines = append(v.lines, nil)
@@ -648,15 +642,6 @@ func (v *View) Read(p []byte) (n int, err error) {
// Rewind sets read and write pos to (0, 0).
func (v *View) Rewind() {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.rewind()
}
func (v *View) rewind() {
v.ei.reset()
if err := v.SetReadPos(0, 0); err != nil {
// SetReadPos returns error only if x and y are negative
// we are passing 0, 0, thus no error should occur.
@@ -847,31 +832,13 @@ func (v *View) realPosition(vx, vy int) (x, y int, err error) {
// And resets reading and writing offsets.
func (v *View) Clear() {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.rewind()
v.Rewind()
v.tainted = true
v.ei.reset()
v.lines = nil
v.viewLines = nil
v.clearRunes()
}
// This is for when we've done a rewind for the sake of avoiding a flicker and
// we've reached the end of the new content to display: we need to clear the remaining
// content from the previous round.
func (v *View) FlushStaleCells() {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
// need to wipe the end of the current line and all following lines
if len(v.lines) > 0 && v.wy < len(v.lines) {
// why this needs to be +1 but the 0:v.wx part doesn't, I'm not sure
v.lines = v.lines[0 : v.wy+1]
if len(v.lines[v.wy]) > 0 && v.wx < len(v.lines[v.wy]) {
v.lines[v.wy] = v.lines[v.wy][0:v.wx]
}
}
v.writeMutex.Unlock()
}
// clearRunes erases all the cells in the view.
+1 -1
View File
@@ -149,7 +149,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
# github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064
# github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772
## explicit
github.com/jesseduffield/gocui
# github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe