From 820f3d5cbb556f1c117906e4174f35ecf71e2ed5 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 30 Oct 2019 20:23:25 +1100 Subject: support split view in staging panel and staging ranges --- pkg/commands/git.go | 18 ++- pkg/commands/git_test.go | 24 ++-- pkg/gui/files_panel.go | 27 +++- pkg/gui/gui.go | 47 ++++++- pkg/gui/keybindings.go | 26 +++- pkg/gui/staging_panel.go | 327 +++++++++++++++++++++++++++++++---------------- pkg/gui/view_helpers.go | 5 + pkg/i18n/english.go | 27 +++- pkg/theme/theme.go | 4 + pkg/utils/utils.go | 6 +- pkg/utils/utils_test.go | 8 +- 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, }, { "three elements, giving second one", -- cgit v1.2.3