summaryrefslogtreecommitdiffstats
path: root/pkg/gui
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2018-12-02 19:57:01 +1100
committerJesse Duffield <jessedduffield@gmail.com>2018-12-04 22:11:48 +1100
commit658e5a9faf8409c62f11f3ad6d636d0255e450f4 (patch)
tree5914a060bfdaaf28064326e66f9a207e2dd8dd0c /pkg/gui
parent99824c8a7b840cc2fa8c06f248acfaf01a3f964b (diff)
initial support for staging individual lines
Diffstat (limited to 'pkg/gui')
-rw-r--r--pkg/gui/files_panel.go22
-rw-r--r--pkg/gui/gui.go22
-rw-r--r--pkg/gui/keybindings.go27
-rw-r--r--pkg/gui/staging_panel.go163
-rw-r--r--pkg/gui/view_helpers.go12
5 files changed, 244 insertions, 2 deletions
diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go
index a00bd2843..3404903ef 100644
--- a/pkg/gui/files_panel.go
+++ b/pkg/gui/files_panel.go
@@ -45,6 +45,25 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
return gui.GitCommand.StageFile(file.Name)
}
+func (gui *Gui) handleSwitchToStagingPanel(g *gocui.Gui, v *gocui.View) error {
+ stagingView, err := g.View("staging")
+ if err != nil {
+ return err
+ }
+ file, err := gui.getSelectedFile(g)
+ if err != nil {
+ if err != gui.Errors.ErrNoFiles {
+ return err
+ }
+ return nil
+ }
+ if !file.Tracked || !file.HasUnstagedChanges {
+ return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
+ }
+ gui.switchFocus(g, v, stagingView)
+ return gui.refreshStagingPanel()
+}
+
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
@@ -188,12 +207,11 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.renderfilesOptions(g, file); err != nil {
return err
}
- var content string
if file.HasMergeConflicts {
return gui.refreshMergePanel(g)
}
- content = gui.GitCommand.Diff(file)
+ content := gui.GitCommand.Diff(file, false)
return gui.renderString(g, "main", content)
}
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index ee8fb1165..67415c5ce 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -72,6 +72,13 @@ type Gui struct {
statusManager *statusManager
}
+type stagingState struct {
+ StageableLines []int
+ HunkStarts []int
+ CurrentLineIndex int
+ Diff string
+}
+
type guiState struct {
Files []*commands.File
Branches []*commands.Branch
@@ -85,6 +92,7 @@ type guiState struct {
EditHistory *stack.Stack
Platform commands.Platform
Updating bool
+ StagingState *stagingState
}
// NewGui builds a new gui handler
@@ -208,6 +216,20 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.FgColor = gocui.ColorWhite
}
+ v, err = g.SetView("staging", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = gui.Tr.SLocalize("StagingTitle")
+ v.Wrap = true
+ v.Highlight = true
+ v.FgColor = gocui.ColorWhite
+ if _, err := g.SetViewOnBottom("staging"); err != nil {
+ return err
+ }
+ }
+
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 1b656659d..2c47ab4f3 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -213,6 +213,13 @@ func (gui *Gui) GetKeybindings() []*Binding {
Handler: gui.handleResetHard,
Description: gui.Tr.SLocalize("resetHard"),
}, {
+ ViewName: "files",
+ Key: gocui.KeyEnter,
+ Modifier: gocui.ModNone,
+ Handler: gui.handleSwitchToStagingPanel,
+ Description: gui.Tr.SLocalize("StageLines"),
+ KeyReadable: "enter",
+ }, {
ViewName: "main",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
@@ -384,6 +391,26 @@ func (gui *Gui) GetKeybindings() []*Binding {
Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
+ }, {
+ ViewName: "staging",
+ Key: gocui.KeyEsc,
+ Modifier: gocui.ModNone,
+ Handler: gui.handleStagingEscape,
+ }, {
+ ViewName: "staging",
+ Key: gocui.KeyArrowUp,
+ Modifier: gocui.ModNone,
+ Handler: gui.handleStagingKeyUp,
+ }, {
+ ViewName: "staging",
+ Key: gocui.KeyArrowDown,
+ Modifier: gocui.ModNone,
+ Handler: gui.handleStagingKeyDown,
+ }, {
+ ViewName: "staging",
+ Key: gocui.KeySpace,
+ Modifier: gocui.ModNone,
+ Handler: gui.handleStageLine,
},
}
diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go
new file mode 100644
index 000000000..be207c2fb
--- /dev/null
+++ b/pkg/gui/staging_panel.go
@@ -0,0 +1,163 @@
+package gui
+
+import (
+ "errors"
+ "io/ioutil"
+
+ "github.com/davecgh/go-spew/spew"
+
+ "github.com/jesseduffield/gocui"
+ "github.com/jesseduffield/lazygit/pkg/git"
+)
+
+func (gui *Gui) refreshStagingPanel() error {
+ // get the currently selected file. Get the diff of that file directly, not
+ // using any custom diff tools.
+ // parse the file to find out where the chunks and unstaged changes are
+
+ file, err := gui.getSelectedFile(gui.g)
+ if err != nil {
+ if err != gui.Errors.ErrNoFiles {
+ return err
+ }
+ return gui.handleStagingEscape(gui.g, nil)
+ }
+
+ if !file.HasUnstagedChanges {
+ return gui.handleStagingEscape(gui.g, nil)
+ }
+
+ // note for custom diffs, we'll need to send a flag here saying not to use the custom diff
+ diff := gui.GitCommand.Diff(file, true)
+ colorDiff := gui.GitCommand.Diff(file, false)
+
+ gui.Log.WithField("staging", "staging").Info("DIFF IS:")
+ gui.Log.WithField("staging", "staging").Info(spew.Sdump(diff))
+ gui.Log.WithField("staging", "staging").Info("hello")
+
+ if len(diff) < 2 {
+ return gui.handleStagingEscape(gui.g, nil)
+ }
+
+ // parse the diff and store the line numbers of hunks and stageable lines
+ // TODO: maybe instantiate this at application start
+ p, err := git.NewPatchParser(gui.Log)
+ if err != nil {
+ return nil
+ }
+ hunkStarts, stageableLines, err := p.ParsePatch(diff)
+ if err != nil {
+ return nil
+ }
+
+ var currentLineIndex int
+ if gui.State.StagingState != nil {
+ end := len(stageableLines) - 1
+ if end < gui.State.StagingState.CurrentLineIndex {
+ currentLineIndex = end
+ } else {
+ currentLineIndex = gui.State.StagingState.CurrentLineIndex
+ }
+ } else {
+ currentLineIndex = 0
+ }
+
+ gui.State.StagingState = &stagingState{
+ StageableLines: stageableLines,
+ HunkStarts: hunkStarts,
+ CurrentLineIndex: currentLineIndex,
+ Diff: diff,
+ }
+
+ if len(stageableLines) == 0 {
+ return errors.New("No lines to stage")
+ }
+
+ stagingView := gui.getStagingView(gui.g)
+ stagingView.SetCursor(0, stageableLines[currentLineIndex])
+ stagingView.SetOrigin(0, 0)
+ return gui.renderString(gui.g, "staging", colorDiff)
+}
+
+func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
+ if _, err := gui.g.SetViewOnBottom("staging"); err != nil {
+ return err
+ }
+
+ return gui.switchFocus(gui.g, nil, gui.getFilesView(gui.g))
+}
+
+// nextNumber returns the next index, cycling if we reach the end
+func nextIndex(numbers []int, currentNumber int) int {
+ for index, number := range numbers {
+ if number > currentNumber {
+ return index
+ }
+ }
+ return 0
+}
+
+// prevNumber returns the next number, cycling if we reach the end
+func prevIndex(numbers []int, currentNumber int) int {
+ end := len(numbers) - 1
+ for i := end; i >= 0; i -= 1 {
+ if numbers[i] < currentNumber {
+ return i
+ }
+ }
+ return end
+}
+
+func (gui *Gui) handleStagingKeyUp(g *gocui.Gui, v *gocui.View) error {
+ return gui.handleCycleLine(true)
+}
+
+func (gui *Gui) handleStagingKeyDown(g *gocui.Gui, v *gocui.View) error {
+ return gui.handleCycleLine(false)
+}
+
+func (gui *Gui) handleCycleLine(up bool) error {
+ state := gui.State.StagingState
+ lineNumbers := state.StageableLines
+ currentLine := lineNumbers[state.CurrentLineIndex]
+ var newIndex int
+ if up {
+ newIndex = prevIndex(lineNumbers, currentLine)
+ } else {
+ newIndex = nextIndex(lineNumbers, currentLine)
+ }
+
+ state.CurrentLineIndex = newIndex
+ stagingView := gui.getStagingView(gui.g)
+ stagingView.SetCursor(0, lineNumbers[newIndex])
+ stagingView.SetOrigin(0, 0)
+ return nil
+}
+
+func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error {
+ state := gui.State.StagingState
+ p, err := git.NewPatchModifier(gui.Log)
+ if err != nil {
+ return err
+ }
+
+ currentLine := state.StageableLines[state.CurrentLineIndex]
+ patch, err := p.ModifyPatch(state.Diff, currentLine)
+ if err != nil {
+ return err
+ }
+
+ // for logging purposes
+ ioutil.WriteFile("patch.diff", []byte(patch), 0600)
+
+ // apply the patch then refresh this panel
+ // create a new temp file with the patch, then call git apply with that patch
+ _, err = gui.GitCommand.ApplyPatch(patch)
+ if err != nil {
+ panic(err)
+ }
+
+ gui.refreshStagingPanel()
+ gui.refreshFiles(gui.g)
+ return nil
+}
diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go
index 6c3e5505c..e6970d92f 100644
--- a/pkg/gui/view_helpers.go
+++ b/pkg/gui/view_helpers.go
@@ -103,6 +103,9 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
return gui.handleCommitSelect(g, v)
case "stash":
return gui.handleStashEntrySelect(g, v)
+ case "staging":
+ return nil
+ // return gui.handleStagingSelect(g, v)
default:
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
@@ -153,6 +156,10 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
if _, err := g.SetCurrentView(newView.Name()); err != nil {
return err
}
+ if _, err := g.SetViewOnTop(newView.Name()); err != nil {
+ return err
+ }
+
g.Cursor = newView.Editable
return gui.newLineFocused(g, newView)
@@ -293,6 +300,11 @@ func (gui *Gui) getBranchesView(g *gocui.Gui) *gocui.View {
return v
}
+func (gui *Gui) getStagingView(g *gocui.Gui) *gocui.View {
+ v, _ := g.View("staging")
+ return v
+}
+
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}