diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2019-11-05 14:21:19 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2019-11-05 19:22:01 +1100 |
commit | 6d5d054c30050bc66c9c3e8697982c6c801a2a62 (patch) | |
tree | 58e08dbfadbfb0af55799e810b1f684e8e1d5527 /pkg | |
parent | 234415537943c090a3f9c3fa3ad7889b6998f9d2 (diff) |
support line by line additions in staging and patch building contexts
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/gui/commit_files_panel.go | 4 | ||||
-rw-r--r-- | pkg/gui/gui.go | 6 | ||||
-rw-r--r-- | pkg/gui/keybindings.go | 113 | ||||
-rw-r--r-- | pkg/gui/line_by_line_panel.go | 254 | ||||
-rw-r--r-- | pkg/gui/patch_building_panel.go | 88 | ||||
-rw-r--r-- | pkg/gui/staging_panel.go | 345 | ||||
-rw-r--r-- | pkg/i18n/dutch.go | 5 | ||||
-rw-r--r-- | pkg/i18n/english.go | 7 | ||||
-rw-r--r-- | pkg/i18n/polish.go | 5 |
9 files changed, 497 insertions, 330 deletions
diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index 43bc8ee22..d6350ce18 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -181,13 +181,13 @@ func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error { } } - if err := gui.changeContext("main", "staging"); err != nil { + if err := gui.changeContext("main", "patch-building"); err != nil { return err } if err := gui.switchFocus(g, v, gui.getMainView()); err != nil { return err } - return gui.refreshStagingPanel() + return gui.refreshPatchBuildingPanel() } if gui.GitCommand.PatchManager != nil && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 54c85bfe4..43a2478a8 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -82,7 +82,7 @@ type Gui struct { // for now the staging panel state, unlike the other panel states, is going to be // non-mutative, so that we don't accidentally end up // with mismatches of data. We might change this in the future -type stagingPanelState struct { +type lineByLinePanelState struct { SelectedLineIdx int FirstLineIdx int LastLineIdx int @@ -130,7 +130,7 @@ type panelStates struct { Commits *commitPanelState Stash *stashPanelState Menu *menuPanelState - Staging *stagingPanelState + LineByLine *lineByLinePanelState Merging *mergingPanelState CommitFiles *commitFilesPanelState } @@ -391,7 +391,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { main := "main" secondary := "secondary" - swappingMainPanels := gui.State.Panels.Staging != nil && gui.State.Panels.Staging.SecondaryFocused + swappingMainPanels := gui.State.Panels.LineByLine != nil && gui.State.Panels.LineByLine.SecondaryFocused if swappingMainPanels { main = "secondary" secondary = "main" diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 7718535ed..fadf0c0c6 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -58,6 +58,8 @@ func (b *Binding) GetKey() string { return "PgUp" case 65507: return "PgDn" + case 9: + return "tab" } return string(key) @@ -638,61 +640,61 @@ func (gui *Gui) GetContextMap() map[string]map[string][]*Binding { Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleStagingEscape, - Description: gui.Tr.SLocalize("EscapeStaging"), + Description: gui.Tr.SLocalize("ReturnToFilesPanel"), }, { ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, - Handler: gui.handleStagingPrevLine, + Handler: gui.handleSelectPrevLine, Description: gui.Tr.SLocalize("PrevLine"), }, { ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, - Handler: gui.handleStagingNextLine, + Handler: gui.handleSelectNextLine, Description: gui.Tr.SLocalize("NextLine"), }, { ViewName: "main", Key: 'k', Modifier: gocui.ModNone, - Handler: gui.handleStagingPrevLine, + Handler: gui.handleSelectPrevLine, }, { ViewName: "main", Key: 'j', Modifier: gocui.ModNone, - Handler: gui.handleStagingNextLine, + Handler: gui.handleSelectNextLine, }, { ViewName: "main", Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, - Handler: gui.handleStagingPrevLine, + Handler: gui.handleSelectPrevLine, }, { ViewName: "main", Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, - Handler: gui.handleStagingNextLine, + Handler: gui.handleSelectNextLine, }, { ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, - Handler: gui.handleStagingPrevHunk, + Handler: gui.handleSelectPrevHunk, Description: gui.Tr.SLocalize("PrevHunk"), }, { ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, - Handler: gui.handleStagingNextHunk, + Handler: gui.handleSelectNextHunk, Description: gui.Tr.SLocalize("NextHunk"), }, { ViewName: "main", Key: 'h', Modifier: gocui.ModNone, - Handler: gui.handleStagingPrevHunk, + Handler: gui.handleSelectPrevHunk, }, { ViewName: "main", Key: 'l', Modifier: gocui.ModNone, - Handler: gui.handleStagingNextHunk, + Handler: gui.handleSelectNextHunk, }, { ViewName: "main", Key: gocui.KeySpace, @@ -725,13 +727,100 @@ func (gui *Gui) GetContextMap() map[string]map[string][]*Binding { Description: gui.Tr.SLocalize("TogglePanel"), }, }, + "patch-building": { + { + ViewName: "main", + Key: gocui.KeyEsc, + Modifier: gocui.ModNone, + Handler: gui.handleEscapePatchBuildingPanel, + Description: gui.Tr.SLocalize("ExitLineByLineMode"), + }, { + ViewName: "main", + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + Handler: gui.handleSelectPrevLine, + Description: gui.Tr.SLocalize("PrevLine"), + }, { + ViewName: "main", + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + Handler: gui.handleSelectNextLine, + Description: gui.Tr.SLocalize("NextLine"), + }, { + ViewName: "main", + Key: 'k', + Modifier: gocui.ModNone, + Handler: gui.handleSelectPrevLine, + }, { + ViewName: "main", + Key: 'j', + Modifier: gocui.ModNone, + Handler: gui.handleSelectNextLine, + }, { + ViewName: "main", + Key: gocui.MouseWheelUp, + Modifier: gocui.ModNone, + Handler: gui.handleSelectPrevLine, + }, { + ViewName: "main", + Key: gocui.MouseWheelDown, + Modifier: gocui.ModNone, + Handler: gui.handleSelectNextLine, + }, { + ViewName: "main", + Key: gocui.KeyArrowLeft, + Modifier: gocui.ModNone, + Handler: gui.handleSelectPrevHunk, + Description: gui.Tr.SLocalize("PrevHunk"), + }, { + ViewName: "main", + Key: gocui.KeyArrowRight, + Modifier: gocui.ModNone, + Handler: gui.handleSelectNextHunk, + Description: gui.Tr.SLocalize("NextHunk"), + }, { + ViewName: "main", + Key: 'h', + Modifier: gocui.ModNone, + Handler: gui.handleSelectPrevHunk, + }, { + ViewName: "main", + Key: 'l', + Modifier: gocui.ModNone, + Handler: gui.handleSelectNextHunk, + }, { + ViewName: "main", + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handleAddSelectionToPatch, + Description: gui.Tr.SLocalize("StageSelection"), + }, { + ViewName: "main", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleRemoveSelectionFromPatch, + Description: gui.Tr.SLocalize("ResetSelection"), + }, { + ViewName: "main", + Key: 'v', + Modifier: gocui.ModNone, + Handler: gui.handleToggleSelectRange, + Description: gui.Tr.SLocalize("ToggleDragSelect"), + }, { + ViewName: "main", + Key: 'a', + Modifier: gocui.ModNone, + Handler: gui.handleToggleSelectHunk, + Description: gui.Tr.SLocalize("ToggleSelectHunk"), + }, + }, "merging": { { ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleEscapeMerge, - Description: gui.Tr.SLocalize("EscapeStaging"), + Description: gui.Tr.SLocalize("ReturnToFilesPanel"), }, { ViewName: "main", Key: gocui.KeySpace, diff --git a/pkg/gui/line_by_line_panel.go b/pkg/gui/line_by_line_panel.go new file mode 100644 index 000000000..f36441823 --- /dev/null +++ b/pkg/gui/line_by_line_panel.go @@ -0,0 +1,254 @@ +package gui + +import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" +) + +// Currently there are two 'pseudo-panels' that make use of this 'pseudo-panel'. +// One is the staging panel where we stage files line-by-line, the other is the +// patch building panel where we add lines of an old commit's file to a patch. +// This file contains the logic around selecting lines and displaying the diffs +// staging_panel.go and patch_building_panel.go have functions specific to their +// use cases + +// these represent what select mode we're in +const ( + LINE = iota + RANGE + HUNK +) + +// returns whether the patch is empty so caller can escape if necessary +// both diffs should be non-coloured because we'll parse them and colour them here +func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool) (bool, error) { + state := gui.State.Panels.LineByLine + + patchParser, err := commands.NewPatchParser(gui.Log, diff) + if err != nil { + return false, nil + } + + if len(patchParser.StageableLines) == 0 { + return true, nil + } + + var selectedLineIdx int + var firstLineIdx int + var lastLineIdx int + selectMode := LINE + if state != nil { + if state.SelectMode == HUNK { + // this is tricky: we need to find out which hunk we just staged based on our old `state.PatchParser` (as opposed to the new `patchParser`) + // we do this by getting the first line index of the original hunk, then + // finding the next stageable line, then getting its containing hunk + // in the new diff + selectMode = HUNK + prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0) + selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx) + newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0) + firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx + } else { + selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx) + firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx + } + } else { + selectedLineIdx = patchParser.StageableLines[0] + firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx + } + + gui.State.Panels.LineByLine = &lineByLinePanelState{ + PatchParser: patchParser, + SelectedLineIdx: selectedLineIdx, + SelectMode: selectMode, + FirstLineIdx: firstLineIdx, + LastLineIdx: lastLineIdx, + Diff: diff, + SecondaryFocused: secondaryFocused, + } + + if err := gui.refreshMainView(); err != nil { + return false, err + } + + if err := gui.focusSelection(selectMode == HUNK); err != nil { + return false, err + } + + secondaryView := gui.getSecondaryView() + secondaryView.Highlight = true + secondaryView.Wrap = false + + secondaryPatchParser, err := commands.NewPatchParser(gui.Log, secondaryDiff) + if err != nil { + return false, nil + } + + gui.g.Update(func(*gocui.Gui) error { + return gui.setViewContent(gui.g, gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil)) + }) + + return false, nil +} + +func (gui *Gui) handleSelectPrevLine(g *gocui.Gui, v *gocui.View) error { + return gui.handleCycleLine(-1) +} + +func (gui *Gui) handleSelectNextLine(g *gocui.Gui, v *gocui.View) error { + return gui.handleCycleLine(1) +} + +func (gui *Gui) handleSelectPrevHunk(g *gocui.Gui, v *gocui.View) error { + return gui.handleCycleHunk(-1) +} + +func (gui *Gui) handleSelectNextHunk(g *gocui.Gui, v *gocui.View) error { + return gui.handleCycleHunk(1) +} + +func (gui *Gui) handleCycleHunk(change int) error { + state := gui.State.Panels.LineByLine + newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, change) + state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx) + if state.SelectMode == HUNK { + state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx + } else { + state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx + } + + if err := gui.refreshMainView(); err != nil { + return err + } + + return gui.focusSelection(true) +} + +func (gui *Gui) handleCycleLine(change int) error { + state := gui.State.Panels.LineByLine + + if state.SelectMode == HUNK { + return gui.handleCycleHunk(change) + } + + newSelectedLineIdx := state.SelectedLineIdx + change + if newSelectedLineIdx < 0 { + newSelectedLineIdx = 0 + } else if newSelectedLineIdx > len(state.PatchParser.PatchLines)-1 { + newSelectedLineIdx = len(state.PatchParser.PatchLines) - 1 + } + + state.SelectedLineIdx = newSelectedLineIdx + + if state.SelectMode == RANGE { + if state.SelectedLineIdx < state.FirstLineIdx { + state.FirstLineIdx = state.SelectedLineIdx + } else { + state.LastLineIdx = state.SelectedLineIdx + } + } else { + state.LastLineIdx = state.SelectedLineIdx + state.FirstLineIdx = state.SelectedLineIdx + } + + if err := gui.refreshMainView(); err != nil { + return err + } + + return gui.focusSelection(false) +} + +func (gui *Gui) refreshMainView() error { + state := gui.State.Panels.LineByLine + + var includedLineIndices []int + // I'd prefer not to have knowledge of contexts using this file but I'm not sure + // how to get around this + if gui.State.Contexts["main"] == "patch-building" { + filename := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name + includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename) + } + colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices) + + mainView := gui.getMainView() + mainView.Highlight = true + mainView.Wrap = false + + gui.g.Update(func(*gocui.Gui) error { + return gui.setViewContent(gui.g, gui.getMainView(), colorDiff) + }) + + return nil +} + +// focusSelection works out the best focus for the staging panel given the +// selected line and size of the hunk +func (gui *Gui) focusSelection(includeCurrentHunk bool) error { + stagingView := gui.getMainView() + state := gui.State.Panels.LineByLine + + _, viewHeight := stagingView.Size() + bufferHeight := viewHeight - 1 + _, origin := stagingView.Origin() + + firstLineIdx := state.SelectedLineIdx + lastLineIdx := state.SelectedLineIdx + + if includeCurrentHunk { + hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0) + firstLineIdx = hunk.FirstLineIdx + lastLineIdx = hunk.LastLineIdx + } + + margin := 0 // we may want to have a margin in place to show context but right now I'm thinking we keep this at zero + + var newOrigin int + if firstLineIdx-origin < margin { + newOrigin = firstLineIdx - margin + } else if lastLineIdx-origin > bufferHeight-margin { + newOrigin = lastLineIdx - bufferHeight + margin + } else { + newOrigin = origin + } + + gui.g.Update(func(*gocui.Gui) error { + if err := stagingView.SetOrigin(0, newOrigin); err != nil { + return err + } + + return stagingView.SetCursor(0, state.SelectedLineIdx-newOrigin) + }) + + return nil +} + +func (gui *Gui) handleToggleSelectRange(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.LineByLine + if state.SelectMode == RANGE { + state.SelectMode = LINE + } else { + state.SelectMode = RANGE + } + state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx + + return gui.refreshMainView() +} + +func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.LineByLine + + if state.SelectMode == HUNK { + state.SelectMode = LINE + state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx + } else { + state.SelectMode = HUNK + selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0) + state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx + } + + if err := gui.refreshMainView(); err != nil { + return err + } + + return gui.focusSelection(state.SelectMode == HUNK) +} diff --git a/pkg/gui/patch_building_panel.go b/pkg/gui/patch_building_panel.go new file mode 100644 index 000000000..cc16111b7 --- /dev/null +++ b/pkg/gui/patch_building_panel.go @@ -0,0 +1,88 @@ +package gui + +import ( + "github.com/jesseduffield/gocui" +) + +func (gui *Gui) refreshPatchBuildingPanel() error { + gui.State.SplitMainPanel = true + + // get diff from commit file that's currently selected + commitFile := gui.getSelectedCommitFile(gui.g) + if commitFile == nil { + return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) + } + + diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true) + if err != nil { + return err + } + + secondaryDiff := gui.GitCommand.PatchManager.RenderPatchForFile(commitFile.Name, true, false, true) + if err != nil { + return err + } + + gui.Log.Warn(secondaryDiff) + + empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false) + if err != nil { + return err + } + + if empty { + return gui.handleStagingEscape(gui.g, nil) + } + + return nil +} + +func (gui *Gui) handleAddSelectionToPatch(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.LineByLine + + // add range of lines to those set for the file + commitFile := gui.getSelectedCommitFile(gui.g) + if commitFile == nil { + return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) + } + + gui.GitCommand.PatchManager.AddFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx) + + if err := gui.refreshCommitFilesView(); err != nil { + return err + } + + if err := gui.refreshPatchBuildingPanel(); err != nil { + return err + } + + return nil +} + +func (gui *Gui) handleRemoveSelectionFromPatch(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.LineByLine + + // add range of lines to those set for the file + commitFile := gui.getSelectedCommitFile(gui.g) + if commitFile == nil { + return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) + } + + gui.GitCommand.PatchManager.RemoveFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx) + + if err := gui.refreshCommitFilesView(); err != nil { + return err + } + + if err := gui.refreshPatchBuildingPanel(); err != nil { + return err + } + + return nil +} + +func (gui *Gui) handleEscapePatchBuildingPanel(g *gocui.Gui, v *gocui.View) error { + gui.State.Panels.LineByLine = nil + + return gui.switchFocus(gui.g, nil, gui.getCommitFilesView()) +} diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go index 47240d87e..55d38f282 100644 --- a/pkg/gui/staging_panel.go +++ b/pkg/gui/staging_panel.go @@ -1,329 +1,87 @@ package gui import ( + "strings" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" ) -// these represent what select mode we're in -const ( - LINE = iota - RANGE - HUNK -) - func (gui *Gui) refreshStagingPanel() error { - state := gui.State.Panels.Staging - - // file, err := gui.getSelectedFile(gui.g) - // if err != nil { - // if err != gui.Errors.ErrNoFiles { - // return err - // } - // return gui.handleStagingEscape(gui.g, nil) - // } - gui.State.SplitMainPanel = true - secondaryFocused := false - if state != nil { - secondaryFocused = state.SecondaryFocused - } - - // if !file.HasUnstagedChanges && !file.HasStagedChanges { - // return gui.handleStagingEscape(gui.g, nil) - // } - - // if (secondaryFocused && !file.HasStagedChanges) || (!secondaryFocused && !file.HasUnstagedChanges) { - // secondaryFocused = !secondaryFocused - // } - - // getDiffs := func() (string, string) { - // // 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, secondaryFocused) - // secondaryColorDiff := gui.GitCommand.Diff(file, false, !secondaryFocused) - // return diff, secondaryColorDiff - // } - - // diff, secondaryColorDiff := getDiffs() - - // // if we have e.g. a deleted file with nothing else to the diff will have only - // // 4-5 lines in which case we'll swap panels - // if len(strings.Split(diff, "\n")) < 5 { - // if len(strings.Split(secondaryColorDiff, "\n")) < 5 { - // return gui.handleStagingEscape(gui.g, nil) - // } - // secondaryFocused = !secondaryFocused - // diff, secondaryColorDiff = getDiffs() - // } - - // get diff from commit file that's currently selected - commitFile := gui.getSelectedCommitFile(gui.g) - if commitFile == nil { - return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) - } - - diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true) - if err != nil { - return err - } + state := gui.State.Panels.LineByLine - secondaryColorDiff := gui.GitCommand.PatchManager.RenderPatchForFile(commitFile.Name, false, false, true) - if err != nil { - return err - } - - patchParser, err := commands.NewPatchParser(gui.Log, diff) + file, err := gui.getSelectedFile(gui.g) if err != nil { - return nil + if err != gui.Errors.ErrNoFiles { + return err + } + return gui.handleStagingEscape(gui.g, nil) } - if len(patchParser.StageableLines) == 0 { + if !file.HasUnstagedChanges && !file.HasStagedChanges { return gui.handleStagingEscape(gui.g, nil) } - var selectedLineIdx int - var firstLineIdx int - var lastLineIdx int - selectMode := LINE + secondaryFocused := false if state != nil { - if state.SelectMode == HUNK { - // this is tricky: we need to find out which hunk we just staged based on our old `state.PatchParser` (as opposed to the new `patchParser`) - // we do this by getting the first line index of the original hunk, then - // finding the next stageable line, then getting its containing hunk - // in the new diff - selectMode = HUNK - prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0) - selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx) - newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0) - firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx - } else { - selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx) - firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx - } - } else { - selectedLineIdx = patchParser.StageableLines[0] - firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx + secondaryFocused = state.SecondaryFocused } - gui.State.Panels.Staging = &stagingPanelState{ - PatchParser: patchParser, - SelectedLineIdx: selectedLineIdx, - SelectMode: selectMode, - FirstLineIdx: firstLineIdx, - LastLineIdx: lastLineIdx, - Diff: diff, - SecondaryFocused: secondaryFocused, + if (secondaryFocused && !file.HasStagedChanges) || (!secondaryFocused && !file.HasUnstagedChanges) { + secondaryFocused = !secondaryFocused } - if err := gui.refreshView(); err != nil { - return err + // 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, secondaryFocused) + secondaryDiff := gui.GitCommand.Diff(file, true, !secondaryFocused) + + // if we have e.g. a deleted file with nothing else to the diff will have only + // 4-5 lines in which case we'll swap panels + if len(strings.Split(diff, "\n")) < 5 { + if len(strings.Split(secondaryDiff, "\n")) < 5 { + return gui.handleStagingEscape(gui.g, nil) + } + secondaryFocused = !secondaryFocused + diff, secondaryDiff = secondaryDiff, diff } - if err := gui.focusSelection(selectMode == HUNK); err != nil { + empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, secondaryFocused) + if err != nil { return err } - secondaryView := gui.getSecondaryView() - secondaryView.Highlight = true - secondaryView.Wrap = false - - gui.g.Update(func(*gocui.Gui) error { - return gui.setViewContent(gui.g, gui.getSecondaryView(), secondaryColorDiff) - }) + if empty { + return gui.handleStagingEscape(gui.g, nil) + } return nil } func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error { - state := gui.State.Panels.Staging + state := gui.State.Panels.LineByLine state.SecondaryFocused = !state.SecondaryFocused return gui.refreshStagingPanel() } func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error { - gui.State.Panels.Staging = nil - - return gui.switchFocus(gui.g, nil, gui.getCommitFilesView()) -} - -func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error { - return gui.handleCycleLine(-1) -} + gui.State.Panels.LineByLine = nil -func (gui *Gui) handleStagingNextLine(g *gocui.Gui, v *gocui.View) error { - return gui.handleCycleLine(1) -} - -func (gui *Gui) handleStagingPrevHunk(g *gocui.Gui, v *gocui.View) error { - return gui.handleCycleHunk(-1) -} - -func (gui *Gui) handleStagingNextHunk(g *gocui.Gui, v *gocui.View) error { - return gui.handleCycleHunk(1) -} - -func (gui *Gui) handleCycleHunk(change int) error { - state := gui.State.Panels.Staging - newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, change) - state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx) - if state.SelectMode == HUNK { - state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx - } else { - state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx - } - - if err := gui.refreshView(); err != nil { - return err - } - - return gui.focusSelection(true) -} - -func (gui *Gui) handleCycleLine(change int) error { - state := gui.State.Panels.Staging - - if state.SelectMode == HUNK { - return gui.handleCycleHunk(change) - } - - newSelectedLineIdx := state.SelectedLineIdx + change - if newSelectedLineIdx < 0 { - newSelectedLineIdx = 0 - } else if newSelectedLineIdx > len(state.PatchParser.PatchLines)-1 { - newSelectedLineIdx = len(state.PatchParser.PatchLines) - 1 - } - - state.SelectedLineIdx = newSelectedLineIdx - - if state.SelectMode == RANGE { - if state.SelectedLineIdx < state.FirstLineIdx { - state.FirstLineIdx = state.SelectedLineIdx - } else { - state.LastLineIdx = state.SelectedLineIdx - } - } else { - state.LastLineIdx = state.SelectedLineIdx - state.FirstLineIdx = state.SelectedLineIdx - } - - if err := gui.refreshView(); err != nil { - return err - } - - return gui.focusSelection(false) -} - -func (gui *Gui) refreshView() error { - state := gui.State.Panels.Staging - - filename := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name - - colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)) - - mainView := gui.getMainView() - mainView.Highlight = true - mainView.Wrap = false - - gui.g.Update(func(*gocui.Gui) error { - return gui.setViewContent(gui.g, gui.getMainView(), colorDiff) - }) - - return nil -} - -// focusSelection works out the best focus for the staging panel given the -// selected line and size of the hunk -func (gui *Gui) focusSelection(includeCurrentHunk bool) error { - stagingView := gui.getMainView() - state := gui.State.Panels.Staging - - _, viewHeight := stagingView.Size() - bufferHeight := viewHeight - 1 - _, origin := stagingView.Origin() - - firstLineIdx := state.SelectedLineIdx - lastLineIdx := state.SelectedLineIdx - - if includeCurrentHunk { - hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0) - firstLineIdx = hunk.FirstLineIdx - lastLineIdx = hunk.LastLineIdx - } - - margin := 0 // we may want to have a margin in place to show context but right now I'm thinking we keep this at zero - - var newOrigin int - if firstLineIdx-origin < margin { - newOrigin = firstLineIdx - margin - } else if lastLineIdx-origin > bufferHeight-margin { - newOrigin = lastLineIdx - bufferHeight + margin - } else { - newOrigin = origin - } - - gui.g.Update(func(*gocui.Gui) error { - if err := stagingView.SetOrigin(0, newOrigin); err != nil { - return err - } - - return stagingView.SetCursor(0, state.SelectedLineIdx-newOrigin) - }) - - return nil + return gui.switchFocus(gui.g, nil, gui.getFilesView()) } func (gui *Gui) handleStageSelection(g *gocui.Gui, v *gocui.View) error { - state := gui.State.Panels.Staging - - // add range of lines to those set for the file - commitFile := gui.getSelectedCommitFile(gui.g) - if commitFile == nil { - return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) - } - - gui.GitCommand.PatchManager.AddFileLineRange(commi |