summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2019-10-30 20:23:25 +1100
committerJesse Duffield <jessedduffield@gmail.com>2019-11-05 19:22:01 +1100
commit820f3d5cbb556f1c117906e4174f35ecf71e2ed5 (patch)
tree591c593fff6636707d06860cc7b918a92c573de0
parent081598d98944cdb95bfa649812565127c0592f5e (diff)
support split view in staging panel and staging ranges
-rw-r--r--pkg/commands/git.go18
-rw-r--r--pkg/commands/git_test.go24
-rw-r--r--pkg/gui/files_panel.go27
-rw-r--r--pkg/gui/gui.go47
-rw-r--r--pkg/gui/keybindings.go26
-rw-r--r--pkg/gui/staging_panel.go327
-rw-r--r--pkg/gui/view_helpers.go5
-rw-r--r--pkg/i18n/english.go27
-rw-r--r--pkg/theme/theme.go4
-rw-r--r--pkg/utils/utils.go6
-rw-r--r--pkg/utils/utils_test.go8
11 files changed, 372 insertions, 147 deletions
diff --git a/pkg/commands/git.go b/pkg/commands/git.go
index 5e2e7e33c..6e86fe0b5 100644
--- a/pkg/commands/git.go
+++ b/pkg/commands/git.go
@@ -582,13 +582,13 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
}
// Diff returns the diff of a file
-func (c *GitCommand) Diff(file *File, plain bool) string {
+func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := "--color"
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
fileName := c.OSCommand.Quote(split[len(split)-1])
- if file.HasStagedChanges && !file.HasUnstagedChanges {
+ if cached {
cachedArg = "--cached"
}
if !file.Tracked && !file.HasStagedChanges {
@@ -605,7 +605,7 @@ func (c *GitCommand) Diff(file *File, plain bool) string {
return s
}
-func (c *GitCommand) ApplyPatch(patch string) (string, error) {
+func (c *GitCommand) ApplyPatch(patch string, reverse bool, cached bool) (string, error) {
filename, err := c.OSCommand.CreateTempFile("patch", patch)
if err != nil {
c.Log.Error(err)
@@ -614,7 +614,17 @@ func (c *GitCommand) ApplyPatch(patch string) (string, error) {
defer func() { _ = c.OSCommand.Remove(filename) }()
- return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", c.OSCommand.Quote(filename)))
+ reverseFlag := ""
+ if reverse {
+ reverseFlag = "--reverse"
+ }
+
+ cachedFlag := ""
+ if cached {
+ cachedFlag = "--cached"
+ }
+
+ return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply %s %s %s", cachedFlag, reverseFlag, c.OSCommand.Quote(filename)))
}
func (c *GitCommand) FastForward(branchName string) error {
diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go
index 51188f14d..c132de98a 100644
--- a/pkg/commands/git_test.go
+++ b/pkg/commands/git_test.go
@@ -1539,6 +1539,7 @@ func TestGitCommandDiff(t *testing.T) {
command func(string, ...string) *exec.Cmd
file *File
plain bool
+ cached bool
}
scenarios := []scenario{
@@ -1556,12 +1557,13 @@ func TestGitCommandDiff(t *testing.T) {
Tracked: true,
},
false,
+ false,
},
{
- "Default case",
+ "cached",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
- assert.EqualValues(t, []string{"diff", "--", "test.txt"}, args)
+ assert.EqualValues(t, []string{"diff", "--color", "--cached", "--", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1570,22 +1572,23 @@ func TestGitCommandDiff(t *testing.T) {
HasStagedChanges: false,
Tracked: true,
},
+ false,
true,
},
{
- "All changes staged",
+ "plain",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
- assert.EqualValues(t, []string{"diff", "--color", "--cached", "--", "test.txt"}, args)
+ assert.EqualValues(t, []string{"diff", "--", "test.txt"}, args)
return exec.Command("echo")
},
&File{
- Name: "test.txt",
- HasStagedChanges: true,
- HasUnstagedChanges: false,
- Tracked: true,
+ Name: "test.txt",
+ HasStagedChanges: false,
+ Tracked: true,
},
+ true,
false,
},
{
@@ -1602,6 +1605,7 @@ func TestGitCommandDiff(t *testing.T) {
Tracked: false,
},
false,
+ false,
},
}
@@ -1609,7 +1613,7 @@ func TestGitCommandDiff(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
- gitCmd.Diff(s.file, s.plain)
+ gitCmd.Diff(s.file, s.plain, s.cached)
})
}
}
@@ -1730,7 +1734,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
- s.test(gitCmd.ApplyPatch("test"))
+ s.test(gitCmd.ApplyPatch("test", false, true))
})
}
}
diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go
index 043c6ceab..76bfe93af 100644
--- a/pkg/gui/files_panel.go
+++ b/pkg/gui/files_panel.go
@@ -72,14 +72,33 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bo
return gui.refreshMergePanel()
}
- content := gui.GitCommand.Diff(file, false)
+ content := gui.GitCommand.Diff(file, false, false)
+ contentCached := gui.GitCommand.Diff(file, false, true)
+ leftContent := content
+ if file.HasStagedChanges && file.HasUnstagedChanges {
+ gui.State.SplitMainPanel = true
+ } else {
+ gui.State.SplitMainPanel = false
+ if file.HasUnstagedChanges {
+ leftContent = content
+ } else {
+ leftContent = contentCached
+ }
+ }
+
if alreadySelected {
g.Update(func(*gocui.Gui) error {
- return gui.setViewContent(gui.g, gui.getMainView(), content)
+ if err := gui.setViewContent(gui.g, gui.getSecondaryView(), contentCached); err != nil {
+ return err
+ }
+ return gui.setViewContent(gui.g, gui.getMainView(), leftContent)
})
return nil
}
- return gui.renderString(g, "main", content)
+ if err := gui.renderString(g, "secondary", contentCached); err != nil {
+ return err
+ }
+ return gui.renderString(g, "main", leftContent)
}
func (gui *Gui) refreshFiles() error {
@@ -180,7 +199,7 @@ func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
- if !file.HasUnstagedChanges || file.HasMergeConflicts {
+ if file.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
}
if err := gui.changeContext("main", "staging"); err != nil {
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 21dde6dd9..f499895a1 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -23,6 +23,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
+ "github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
@@ -83,10 +84,13 @@ type Gui struct {
// non-mutative, so that we don't accidentally end up
// with mismatches of data. We might change this in the future
type stagingPanelState struct {
- SelectedLine int
- StageableLines []int
- HunkStarts []int
- Diff string
+ SelectedLineIdx int
+ FirstLineIdx int
+ LastLineIdx int
+ Diff string
+ PatchParser *git.PatchParser
+ SelectMode int // one of LINE, HUNK, or RANGE
+ IndexFocused bool // this is for if we show the left or right panel
}
type mergingPanelState struct {
@@ -147,6 +151,7 @@ type guiState struct {
WorkingTreeState string // one of "merging", "rebasing", "normal"
Contexts map[string]string
CherryPickedCommits []*commands.Commit
+ SplitMainPanel bool
}
// NewGui builds a new gui handler
@@ -258,6 +263,7 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
+ gui.State.SplitMainPanel = false
if v.Name() == "branches" {
// This stops the branches panel from showing the upstream/downstream changes to the selected branch, when it loses focus
// inside renderListPanel it checks to see if the panel has focus
@@ -359,7 +365,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
- leftSideWidth := width / 3
appStatus := gui.statusManager.getStatusString()
appStatusOptionsBoundary := 0
@@ -376,7 +381,23 @@ func (gui *Gui) layout(g *gocui.Gui) error {
g.DeleteView("limit")
textColor := theme.GocuiDefaultTextColor
- v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, height-2, gocui.LEFT)
+ leftSideWidth := width / 3
+ panelSplitX := width - 1
+ if gui.State.SplitMainPanel {
+ units := 7
+ leftSideWidth = width / units
+ panelSplitX = (1 + ((units - 1) / 2)) * width / units
+ }
+
+ main := "main"
+ secondary := "secondary"
+ swappingMainPanels := gui.State.Panels.Staging != nil && gui.State.Panels.Staging.IndexFocused
+ if swappingMainPanels {
+ main = "secondary"
+ secondary = "main"
+ }
+
+ v, err := g.SetView(main, leftSideWidth+panelSpacing, 0, panelSplitX, height-2, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
@@ -386,6 +407,20 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.FgColor = textColor
}
+ hiddenViewOffset := 0
+ if !gui.State.SplitMainPanel {
+ hiddenViewOffset = 9999
+ }
+ secondaryView, err := g.SetView(secondary, panelSplitX+1+hiddenViewOffset, hiddenViewOffset, width-1+hiddenViewOffset, height-2+hiddenViewOffset, gocui.LEFT)
+ if err != nil {
+ if err.Error() != "unknown view" {
+ return err
+ }
+ secondaryView.Title = gui.Tr.SLocalize("DiffTitle")
+ secondaryView.Wrap = true
+ secondaryView.FgColor = gocui.ColorWhite
+ }
+
if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err.Error() != "unknown view" {
return err
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 24fd5a854..c8adac11c 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -678,14 +678,32 @@ func (gui *Gui) GetContextMap() map[string]map[string][]*Binding {
ViewName: "main",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
- Handler: gui.handleStageLine,
- Description: gui.Tr.SLocalize("StageLine"),
+ Handler: gui.handleStageSelection,
+ Description: gui.Tr.SLocalize("StageSelection"),
+ }, {
+ ViewName: "main",
+ Key: 'd',
+ Modifier: gocui.ModNone,
+ Handler: gui.handleResetSelection,
+ Description: gui.Tr.SLocalize("ResetSelection"),
+ }, {
+ ViewName: "main",
+ Key: 'c',
+ Modifier: gocui.ModNone,
+ Handler: gui.handleToggleSelectRange,
+ Description: gui.Tr.SLocalize("ToggleDragSelect"),
}, {
ViewName: "main",
Key: 'a',
Modifier: gocui.ModNone,
- Handler: gui.handleStageHunk,
- Description: gui.Tr.SLocalize("StageHunk"),
+ Handler: gui.handleToggleSelectHunk,
+ Description: gui.Tr.SLocalize("ToggleSelectHunk"),
+ }, {
+ ViewName: "main",
+ Key: gocui.KeyTab,
+ Modifier: gocui.ModNone,
+ Handler: gui.handleTogglePanel,
+ Description: gui.Tr.SLocalize("TogglePanel"),
},
},
"merging": {
diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go
index a24becbd2..93e1d46c5 100644
--- a/pkg/gui/staging_panel.go
+++ b/pkg/gui/staging_panel.go
@@ -1,12 +1,21 @@
package gui
import (
+ "strings"
+
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/git"
- "github.com/jesseduffield/lazygit/pkg/utils"
+)
+
+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 {
@@ -15,67 +24,109 @@ func (gui *Gui) refreshStagingPanel() error {
return gui.handleStagingEscape(gui.g, nil)
}
- if !file.HasUnstagedChanges {
+ gui.State.SplitMainPanel = true
+
+ indexFocused := false
+ if state != nil {
+ indexFocused = state.IndexFocused
+ }
+
+ if !file.HasUnstagedChanges && !file.HasStagedChanges {
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)
+ if (indexFocused && !file.HasStagedChanges) || (!indexFocused && !file.HasUnstagedChanges) {
+ indexFocused = !indexFocused
+ }
- if len(diff) < 2 {
- return gui.handleStagingEscape(gui.g, nil)
+ 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, indexFocused)
+ secondaryColorDiff := gui.GitCommand.Diff(file, false, !indexFocused)
+ return diff, secondaryColorDiff
}
- // 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
+ 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)
+ }
+ indexFocused = !indexFocused
+ diff, secondaryColorDiff = getDiffs()
}
- hunkStarts, stageableLines, err := p.ParsePatch(diff)
+
+ patchParser, err := git.NewPatchParser(gui.Log, diff)
if err != nil {
return nil
}
- var selectedLine int
- if gui.State.Panels.Staging != nil {
- end := len(stageableLines) - 1
- if end < gui.State.Panels.Staging.SelectedLine {
- selectedLine = end
+ if len(patchParser.StageableLines) == 0 {
+ return gui.handleStagingEscape(gui.g, 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 {
- selectedLine = gui.State.Panels.Staging.SelectedLine
+ selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
+ firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
}
} else {
- selectedLine = 0
+ selectedLineIdx = patchParser.StageableLines[0]
+ firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
}
gui.State.Panels.Staging = &stagingPanelState{
- StageableLines: stageableLines,
- HunkStarts: hunkStarts,
- SelectedLine: selectedLine,
- Diff: diff,
+ PatchParser: patchParser,
+ SelectedLineIdx: selectedLineIdx,
+ SelectMode: selectMode,
+ FirstLineIdx: firstLineIdx,
+ LastLineIdx: lastLineIdx,
+ Diff: diff,
+ IndexFocused: indexFocused,
}
- if len(stageableLines) == 0 {
- return gui.createErrorPanel(gui.g, "No lines to stage")
+ if err := gui.refreshView(); err != nil {
+ return err
}
- if err := gui.focusLineAndHunk(); err != nil {
+ if err := gui.focusSelection(selectMode == HUNK); err != nil {
return err
}
- mainView := gui.getMainView()
- mainView.Highlight = true
- mainView.Wrap = false
+ secondaryView := gui.getSecondaryView()
+ secondaryView.Highlight = true
+ secondaryView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
- return gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
+ return gui.setViewContent(gui.g, gui.getSecondaryView(), secondaryColorDiff)
})
return nil
}
+func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error {
+ state := gui.State.Panels.Staging
+
+ state.IndexFocused = !state.IndexFocused
+ return gui.refreshStagingPanel()
+}
+
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Staging = nil
@@ -83,135 +134,166 @@ func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error {
- return gui.handleCycleLine(true)
+ return gui.handleCycleLine(-1)
}
func (gui *Gui) handleStagingNextLine(g *gocui.Gui, v *gocui.View) error {
- return gui.handleCycleLine(false)
+ return gui.handleCycleLine(1)
}
func (gui *Gui) handleStagingPrevHunk(g *gocui.Gui, v *gocui.View) error {
- return gui.handleCycleHunk(true)
+ return gui.handleCycleHunk(-1)
}
func (gui *Gui) handleStagingNextHunk(g *gocui.Gui, v *gocui.View) error {
- return gui.handleCycleHunk(false)
+ return gui.handleCycleHunk(1)
}
-func (gui *Gui) handleCycleHunk(prev bool) error {
+func (gui *Gui) handleCycleHunk(change int) error {
state := gui.State.Panels.Staging
- lineNumbers := state.StageableLines
- currentLine := lineNumbers[state.SelectedLine]
- currentHunkIndex := utils.PrevIndex(state.HunkStarts, currentLine)
- var newHunkIndex int
- if prev {
- if currentHunkIndex == 0 {
- newHunkIndex = len(state.HunkStarts) - 1
- } else {
- newHunkIndex = currentHunkIndex - 1
- }
+ 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 {
- if currentHunkIndex == len(state.HunkStarts)-1 {
- newHunkIndex = 0
- } else {
- newHunkIndex = currentHunkIndex + 1
- }
+ state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
}
- state.SelectedLine = utils.NextIndex(lineNumbers, state.HunkStarts[newHunkIndex])
+ if err := gui.refreshView(); err != nil {
+ return err
+ }
- return gui.focusLineAndHunk()
+ return gui.focusSelection(true)
}
-func (gui *Gui) handleCycleLine(prev bool) error {
+func (gui *Gui) handleCycleLine(change int) error {
state := gui.State.Panels.Staging
- lineNumbers := state.StageableLines
- currentLine := lineNumbers[state.SelectedLine]
- var newIndex int
- if prev {
- newIndex = utils.PrevIndex(lineNumbers, currentLine)
+
+ 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 {
- newIndex = utils.NextIndex(lineNumbers, currentLine)
+ state.LastLineIdx = state.SelectedLineIdx
+ state.FirstLineIdx = state.SelectedLineIdx
+ }
+
+ if err := gui.refreshView(); err != nil {
+ return err
}
- state.SelectedLine = newIndex
- return gui.focusLineAndHunk()
+ return gui.focusSelection(false)
}
-// focusLineAndHunk works out the best focus for the staging panel given the
+func (gui *Gui) refreshView() error {
+ state := gui.State.Panels.Staging
+
+ colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx)
+
+ 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) focusLineAndHunk() error {
+func (gui *Gui) focusSelection(includeCurrentHunk bool) error {
stagingView := gui.getMainView()
state := gui.State.Panels.Staging
- lineNumber := state.StageableLines[state.SelectedLine]
-
- // we want the bottom line of the view buffer to ideally be the bottom line
- // of the hunk, but if the hunk is too big we'll just go three lines beyond
- // the currently selected line so that the user can see the context
- var bottomLine int
- nextHunkStartIndex := utils.NextIndex(state.HunkStarts, lineNumber)
- if nextHunkStartIndex == 0 {
- // for now linesHeight is an efficient means of getting the number of lines
- // in the patch. However if we introduce word wrap we'll need to update this
- bottomLine = stagingView.LinesHeight() - 1
- } else {
- bottomLine = state.HunkStarts[nextHunkStartIndex] - 1
- }
+ _, viewHeight := stagingView.Size()
+ bufferHeight := viewHeight - 1
+ _, origin := stagingView.Origin()
+
+ firstLineIdx := state.SelectedLineIdx
+ lastLineIdx := state.SelectedLineIdx
- hunkStartIndex := utils.PrevIndex(state.HunkStarts, lineNumber)
- hunkStart := state.HunkStarts[hunkStartIndex]
- // if it's the first hunk we'll also show the diff header
- if hunkStartIndex == 0 {
- hunkStart = 0
+ if includeCurrentHunk {
+ hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
+ firstLineIdx = hunk.FirstLineIdx
+ lastLineIdx = hunk.LastLineIdx
}
- _, height := stagingView.Size()
- // if this hunk is too big, we will just ensure that the user can at least
- // see three lines of context below the cursor
- if bottomLine-hunkStart > height {
- bottomLine = lineNumber + 3
+ 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
}
- return gui.generalFocusLine(lineNumber, bottomLine, stagingView)
+ 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) handleStageHunk(g *gocui.Gui, v *gocui.View) error {
- return gui.handleStageLineOrHunk(true)
+func (gui *Gui) handleStageSelection(g *gocui.Gui, v *gocui.View) error {
+ return gui.applySelection(false)
}
-func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error {
- return gui.handleStageLineOrHunk(false)
+func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
+ return gui.applySelection(true)
}
-func (gui *Gui) handleStageLineOrHunk(hunk bool) error {
+func (gui *Gui) applySelection(reverse bool) error {
state := gui.State.Panels.Staging
- p, err := git.NewPatchModifier(gui.Log)
- if err != nil {
- return err
- }
- currentLine := state.StageableLines[state.SelectedLine]
- var patch string
- if hunk {
- patch, err = p.ModifyPatchForHunk(state.Diff, state.HunkStarts, currentLine)
- } else {
- patch, err = p.ModifyPatchForLine(state.Diff, currentLine)
+ if !reverse && state.IndexFocused {
+ return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantStageStaged"))
}
+
+ file, err := gui.getSelectedFile(gui.g)
if err != nil {
return err
}
- // for logging purposes
- // ioutil.WriteFile("patch.diff", []byte(patch), 0600)
+ patch := git.ModifiedPatch(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse)
+
+ if patch == "" {
+ return nil
+ }
// 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)
+ _, err = gui.GitCommand.ApplyPatch(patch, false, !reverse || state.IndexFocused)
if err != nil {
return err
}
+ if state.SelectMode == RANGE {
+ state.SelectMode = LINE
+ }
+
if err := gui.refreshFiles(); err != nil {
return err
}
@@ -220,3 +302,34 @@ func (gui *Gui) handleStageLineOrHunk(hunk bool) error {
}
return nil
}
+
+func (gui *Gui) handleToggleSelectRange(g *gocui.Gui, v *gocui.View) error {
+ state := gui.State.Panels.Staging
+ if state.SelectMode == RANGE {
+ state.SelectMode = LINE
+ } else {
+ state.SelectMode = RANGE
+ }
+ state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
+
+ return gui.refreshView()
+}
+
+func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
+ state := gui.State.Panels.Staging
+
+ 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.refreshView(); err != nil {
+ return err
+ }
+
+ return gui.focusSelection(state.SelectMode == HUNK)
+}
diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go
index 97f394ec3..5c5678a14 100644
--- a/pkg/gui/view_helpers.go
+++ b/pkg/gui/view_helpers.go
@@ -300,6 +300,11 @@ func (gui *Gui) getMainView() *gocui.View {
return v
}
+func (gui *Gui) getSecondaryView() *gocui.View {
+ v, _ := gui.g.View("secondary")
+ return v
+}
+
func (gui *Gui) getStashView() *gocui.View {
v, _ := gui.g.View("stash")
return v
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 2de69d4d6..c250dcbd4 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -482,13 +482,30 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: `stage individual hunks/lines`,
}, &i18n.Message{
ID: "FileStagingRequirements",
- Other: `Can only stage individual lines for tracked files with unstaged changes`,
+ Other: `Can only stage individual lines for tracked files`,
}, &i18n.Message{
- ID: "StageHunk",
- Other: `stage hunk`,
+ ID: "SelectHunk",
+ Other: `select hunk`,
}, &i18n.Message{
- ID: "StageLine",
- Other: `stage line`,
+ ID: "StageSelection",
+ Other: `stage selection`,
+ }, &i18n.Message{
+ ID: "ResetSelection",
+ Other: `reset selection`,
+ }, &i18n.Message{
+ ID: "ToggleDragSelect",
+ Other: `toggle drag select`,
+ }, &i18n.Message{
+ ID: "ToggleSelectHunk",
+ Other: `toggle select hunk`,
+ },
+ &i18n.Message{
+ ID: "TogglePanel",
+ Other: `toggle staged/unstaged panel focus`,
+ },
+ &i18n.Message{
+ ID: "CantStageStaged",
+ Other: `You can't stage an already staged change!`,
}, &i18n.Message{
ID: "EscapeStaging",
Other: `return to files panel`,
diff --git a/pkg/theme/theme.go b/pkg/theme/theme.go
index a991db91a..0b4179cce 100644
--- a/pkg/theme/theme.go
+++ b/pkg/theme/theme.go
@@ -9,6 +9,8 @@ import (
var (
// DefaultTextColor is the default text color
DefaultTextColor = color.FgWhite
+ // DefaultHiTextColor is the default highlighted text color
+ DefaultHiTextColor = color.FgHiWhite
// GocuiDefaultTextColor does the same as DefaultTextColor but this one only colors gocui default text colors
GocuiDefaultTextColor gocui.Attribute
@@ -28,9 +30,11 @@ func UpdateTheme(userConfig *viper.Viper) {
isLightTheme := userConfig.GetBool("gui.theme.lightTheme")
if isLightTheme {
DefaultTextColor = color.FgBlack
+ DefaultHiTextColor = color.FgHiBlack
GocuiDefaultTextColor = gocui.ColorBlack
} else {
DefaultTextColor = color.FgWhite
+ DefaultHiTextColor = color.FgHiWhite
GocuiDefaultTextColor = gocui.ColorWhite
}
}
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 3350e2d53..42470df88 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -233,18 +233,18 @@ func NextIndex(numbers []int, currentNumber int) int {
return index
}
}
- return 0
+ return len(numbers) - 1
}
// PrevIndex returns the index that comes before the given 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 {
+ for i := end; i >= 0; i-- {
if numbers[i] < currentNumber {
return i
}
}
- return end
+ return 0
}
func AsJson(i interface{}) string {
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
index ed131aaca..704a7c56f 100644
--- a/pkg/utils/utils_test.go
+++ b/pkg/utils/utils_test.go
@@ -485,7 +485,7 @@ func TestNextIndex(t *testing.T) {
"no elements",
[]int{},
1,
- 0,
+ -1,
},
{
"one element",
@@ -503,7 +503,7 @@ func TestNextIndex(t *testing.T) {
"two elements, giving second one",
[]int{1, 2},
2,
- 0,
+ 1,
},
{
"three elements, giving second one",
@@ -534,7 +534,7 @@ func TestPrevIndex(t *testing.T) {
"no elements",
[]int{},
1,
- -1,
+ 0,
},
{
"one element",
@@ -546,7 +546,7 @@ func TestPrevIndex(t *testing.T) {
"two elements",
[]int{1, 2},
1,
- 1,
+ 0,