package gui import ( "fmt" "github.com/go-errors/errors" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/patch" ) // 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, selectedLineIdx int, state *lBlPanelState) (bool, error) { patchParser, err := patch.NewPatchParser(gui.Log, diff) if err != nil { return false, nil } if len(patchParser.StageableLines) == 0 { return true, nil } var firstLineIdx int var lastLineIdx int selectMode := LINE // if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line if selectedLineIdx >= 0 { selectMode = RANGE firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx } else 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 } state = &lBlPanelState{ PatchParser: patchParser, SelectedLineIdx: selectedLineIdx, SelectMode: selectMode, FirstLineIdx: firstLineIdx, LastLineIdx: lastLineIdx, Diff: diff, SecondaryFocused: secondaryFocused, } gui.State.Panels.LineByLine = state if err := gui.refreshMainViewForLineByLine(state); err != nil { return false, err } if err := gui.focusSelection(selectMode == HUNK, state); err != nil { return false, err } secondaryView := gui.getSecondaryView() secondaryView.Highlight = true secondaryView.Wrap = false secondaryPatchParser, err := patch.NewPatchParser(gui.Log, secondaryDiff) if err != nil { return false, nil } gui.g.Update(func(*gocui.Gui) error { gui.setViewContent(gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil)) return nil }) return false, nil } func (gui *Gui) handleSelectPrevLine() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { return gui.LBLCycleLine(-1, state) }) } func (gui *Gui) handleSelectNextLine() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { return gui.LBLCycleLine(+1, state) }) } func (gui *Gui) handleSelectPrevHunk() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, -1) return gui.selectNewHunk(newHunk, state) }) } func (gui *Gui) handleSelectNextHunk() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 1) return gui.selectNewHunk(newHunk, state) }) } func (gui *Gui) selectNewHunk(newHunk *patch.PatchHunk, state *lBlPanelState) error { 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.refreshMainViewForLineByLine(state); err != nil { return err } return gui.focusSelection(true, state) } func (gui *Gui) LBLCycleLine(change int, state *lBlPanelState) error { if state.SelectMode == HUNK { newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, change) return gui.selectNewHunk(newHunk, state) } return gui.LBLSelectLine(state.SelectedLineIdx+change, state) } func (gui *Gui) LBLSelectLine(newSelectedLineIdx int, state *lBlPanelState) error { 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.refreshMainViewForLineByLine(state); err != nil { return err } return gui.focusSelection(false, state) } func (gui *Gui) handleLBLMouseDown(g *gocui.Gui, v *gocui.View) error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { if gui.popupPanelFocused() { return nil } newSelectedLineIdx := v.SelectedLineIdx() state.FirstLineIdx = newSelectedLineIdx state.LastLineIdx = newSelectedLineIdx state.SelectMode = RANGE return gui.LBLSelectLine(newSelectedLineIdx, state) }) } func (gui *Gui) handleMouseDrag(g *gocui.Gui, v *gocui.View) error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { if gui.popupPanelFocused() { return nil } return gui.LBLSelectLine(v.SelectedLineIdx(), state) }) } func (gui *Gui) handleMouseScrollUp(g *gocui.Gui, v *gocui.View) error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { if gui.popupPanelFocused() { return nil } state.SelectMode = LINE return gui.LBLCycleLine(-1, state) }) } func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { if gui.popupPanelFocused() { return nil } state.SelectMode = LINE return gui.LBLCycleLine(1, state) }) } func (gui *Gui) getSelectedCommitFileName() string { return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name } func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error { 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.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() { filename := gui.getSelectedCommitFileName() var err error includedLineIndices, err = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename) if err != nil { return err } } 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 { gui.setViewContent(gui.getMainView(), colorDiff) return nil }) 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, state *lBlPanelState) error { stagingView := gui.getMainView() _, 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() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { if state.SelectMode == RANGE { state.SelectMode = LINE } else { state.SelectMode = RANGE } state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx return gui.refreshMainViewForLineByLine(state) }) } func (gui *Gui) handleToggleSelectHunk() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { 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.refreshMainViewForLineByLine(state); err != nil { return err } return gui.focusSelection(state.SelectMode == HUNK, state) }) } func (gui *Gui) escapeLineByLinePanel() { gui.State.Panels.LineByLine = nil } func (gui *Gui) handleOpenFileAtLine() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { // again, would be good to use inheritance here (or maybe even composition) var filename string switch gui.State.MainContext { case gui.Contexts.PatchBuilding.Context.GetKey(): filename = gui.getSelectedCommitFileName() case gui.Contexts.Staging.Context.GetKey(): file := gui.getSelectedFile() if file == nil { return nil } filename = file.Name default: return errors.Errorf("unknown main context: %s", gui.State.MainContext) } // need to look at current index, then work out what my hunk's header information is, and see how far my line is away from the hunk header selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0) lineNumber := selectedHunk.LineNumberOfLine(state.SelectedLineIdx) filenameWithLineNum := fmt.Sprintf("%s:%d", filename, lineNumber) if err := gui.OSCommand.OpenFile(filenameWithLineNum); err != nil { return err } return nil }) } func (gui *Gui) handleLineByLineNextPage() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { newSelectedLineIdx := state.SelectedLineIdx + gui.pageDelta(gui.getMainView()) return gui.lineByLineNavigateTo(newSelectedLineIdx, state) }) } func (gui *Gui) handleLineByLinePrevPage() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { newSelectedLineIdx := state.SelectedLineIdx - gui.pageDelta(gui.getMainView()) return gui.lineByLineNavigateTo(newSelectedLineIdx, state) }) } func (gui *Gui) handleLineByLineGotoBottom() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { newSelectedLineIdx := len(state.PatchParser.PatchLines) - 1 return gui.lineByLineNavigateTo(newSelectedLineIdx, state) }) } func (gui *Gui) handleLineByLineGotoTop() error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { return gui.lineByLineNavigateTo(0, state) }) } func (gui *Gui) handlelineByLineNavigateTo(selectedLineIdx int) error { return gui.withLBLActiveCheck(func(state *lBlPanelState) error { return gui.lineByLineNavigateTo(selectedLineIdx, state) }) } func (gui *Gui) lineByLineNavigateTo(selectedLineIdx int, state *lBlPanelState) error { state.SelectMode = LINE return gui.LBLSelectLine(selectedLineIdx, state) } func (gui *Gui) withLBLActiveCheck(f func(*lBlPanelState) error) error { gui.Mutexes.LineByLinePanelMutex.Lock() defer gui.Mutexes.LineByLinePanelMutex.Unlock() state := gui.State.Panels.LineByLine if state == nil { return nil } return f(state) }