1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-31 22:22:14 +02:00

allow opening files on the selected line in the staging panel

This commit is contained in:
Jesse Duffield 2020-08-15 10:58:29 +10:00
parent f02ccca0e0
commit 291a8e4de0
6 changed files with 159 additions and 47 deletions

View File

@ -191,7 +191,7 @@ for users of VSCode
```yaml
os:
openCommand: 'code -r {{filename}}'
openCommand: 'code -rg {{filename}}'
```
## Color Attributes

View File

@ -14,23 +14,42 @@ var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
type PatchHunk struct {
header string
FirstLineIdx int
LastLineIdx int
oldStart int
newStart int
heading string
bodyLines []string
}
func newHunk(header string, body string, firstLineIdx int) *PatchHunk {
bodyLines := strings.SplitAfter(header+body, "\n")[1:] // dropping the header line
func (hunk *PatchHunk) LastLineIdx() int {
return hunk.FirstLineIdx + len(hunk.bodyLines)
}
func newHunk(lines []string, firstLineIdx int) *PatchHunk {
header := lines[0]
bodyLines := lines[1:]
oldStart, newStart, heading := headerInfo(header)
return &PatchHunk{
header: header,
oldStart: oldStart,
newStart: newStart,
heading: heading,
FirstLineIdx: firstLineIdx,
LastLineIdx: firstLineIdx + len(bodyLines),
bodyLines: bodyLines,
}
}
func headerInfo(header string) (int, int, string) {
match := hunkHeaderRegexp.FindStringSubmatch(header)
oldStart := mustConvertToInt(match[1])
newStart := mustConvertToInt(match[2])
heading := match[3]
return oldStart, newStart, heading
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
@ -94,38 +113,21 @@ func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startO
}
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
changeCount := 0
oldLength := 0
newLength := 0
for _, line := range newBodyLines {
switch line[:1] {
case "+":
newLength++
changeCount++
case "-":
oldLength++
changeCount++
case " ":
oldLength++
newLength++
}
}
changeCount := nLinesWithPrefix(newBodyLines, []string{"+", "-"})
oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"})
newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "})
if changeCount == 0 {
// if nothing has changed we just return nothing
return startOffset, "", false
}
// get oldstart, newstart, and heading from header
match := hunkHeaderRegexp.FindStringSubmatch(hunk.header)
var oldStart int
if reverse {
oldStart = mustConvertToInt(match[2])
oldStart = hunk.newStart
} else {
oldStart = mustConvertToInt(match[1])
oldStart = hunk.oldStart
}
heading := match[3]
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
@ -141,7 +143,7 @@ func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, rev
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, heading)
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, hunk.heading)
return newStartOffset, formattedHeader, true
}
@ -162,19 +164,33 @@ func GetHeaderFromDiff(diff string) string {
}
func GetHunksFromDiff(diff string) []*PatchHunk {
headers := hunkHeaderRegexp.FindAllString(diff, -1)
bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit
hunks := []*PatchHunk{}
firstLineIdx := -1
var hunkLines []string
pastDiffHeader := false
headerFirstLineIndices := []int{}
for lineIdx, line := range strings.Split(diff, "\n") {
if strings.HasPrefix(line, "@@ -") {
headerFirstLineIndices = append(headerFirstLineIndices, lineIdx)
for lineIdx, line := range strings.SplitAfter(diff, "\n") {
isHunkHeader := strings.HasPrefix(line, "@@ -")
if isHunkHeader {
if pastDiffHeader { // we need to persist the current hunk
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
pastDiffHeader = true
firstLineIdx = lineIdx
hunkLines = []string{line}
continue
}
if !pastDiffHeader { // skip through the stuff that precedes the first hunk
continue
}
hunkLines = append(hunkLines, line)
}
hunks := make([]*PatchHunk, len(headers))
for index, header := range headers {
hunks[index] = newHunk(header, bodies[index], headerFirstLineIndices[index])
if pastDiffHeader {
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
return hunks
@ -203,7 +219,7 @@ outer:
for _, hunk := range d.hunks {
// if there is any line in our lineIndices array that the hunk contains, we append it
for _, lineIdx := range lineIndices {
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx {
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx() {
hunksInRange = append(hunksInRange, hunk)
continue outer
}
@ -251,7 +267,7 @@ func (d *PatchModifier) OriginalPatchLength() int {
return 0
}
return d.hunks[len(d.hunks)-1].LastLineIdx
return d.hunks[len(d.hunks)-1].LastLineIdx()
}
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
@ -263,3 +279,24 @@ func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string,
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
}
// I want to know, given a hunk, what line a given index is on
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
lines := hunk.bodyLines[0 : idx-hunk.FirstLineIdx-1]
offset := nLinesWithPrefix(lines, []string{"+", " "})
return hunk.newStart + offset
}
func nLinesWithPrefix(lines []string, chars []string) int {
result := 0
for _, line := range lines {
for _, char := range chars {
if line[:1] == char {
result++
}
}
}
return result
}

View File

@ -2,6 +2,7 @@ package commands
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -88,6 +89,15 @@ index e69de29..c6568ea 100644
\ No newline at end of file
`
const exampleHunk = `@@ -1,5 +1,5 @@
apple
-grape
+orange
...
...
...
`
// TestModifyPatchForRange is a function.
func TestModifyPatchForRange(t *testing.T) {
type scenario struct {
@ -509,3 +519,30 @@ func TestModifyPatchForRange(t *testing.T) {
})
}
}
func TestLineNumberOfLine(t *testing.T) {
type scenario struct {
testName string
hunk *PatchHunk
idx int
expected int
}
scenarios := []scenario{
{
testName: "nothing selected",
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
idx: 15,
expected: 3,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
result := s.hunk.LineNumberOfLine(s.idx)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
})
}
}

View File

@ -63,7 +63,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
}
for index, hunk := range p.PatchHunks {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
resultIndex := index + offset
if resultIndex < 0 {
resultIndex = 0
@ -75,7 +75,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
}
// if your cursor is past the last hunk, select the last hunk
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx {
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
return p.PatchHunks[len(p.PatchHunks)-1]
}

View File

@ -995,6 +995,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleEscapePatchBuildingPanel,
Description: gui.Tr.SLocalize("ExitLineByLineMode"),
},
{
ViewName: "main",
Contexts: []string{"patch-building", "staging"},
Key: gui.getKey("universal.openFile"),
Handler: gui.wrappedHandler(gui.handleOpenFileAtLine),
// Description: gui.Tr.SLocalize("PrevLine"),
},
{
ViewName: "main",
Contexts: []string{"patch-building", "staging"},

View File

@ -1,6 +1,9 @@
package gui
import (
"fmt"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
@ -50,7 +53,7 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx)
newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0)
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
} else {
selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
@ -121,7 +124,7 @@ func (gui *Gui) selectNewHunk(newHunk *commands.PatchHunk) error {
state := gui.State.Panels.LineByLine
state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
if state.SelectMode == HUNK {
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
} else {
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
}
@ -265,7 +268,7 @@ func (gui *Gui) focusSelection(includeCurrentHunk bool) error {
if includeCurrentHunk {
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
firstLineIdx = hunk.FirstLineIdx
lastLineIdx = hunk.LastLineIdx
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
@ -311,7 +314,7 @@ func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
} else {
state.SelectMode = HUNK
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx()
}
if err := gui.refreshMainView(); err != nil {
@ -325,3 +328,31 @@ func (gui *Gui) handleEscapeLineByLinePanel() {
gui.changeMainViewsContext("normal")
gui.State.Panels.LineByLine = nil
}
func (gui *Gui) handleOpenFileAtLine() error {
// again, would be good to use inheritance here (or maybe even composition)
var filename string
switch gui.State.MainContext {
case "patch-building":
filename = gui.getSelectedCommitFileName()
case "staging":
file, err := gui.getSelectedFile()
if err != nil {
return nil
}
filename = file.Name
default:
return errors.Errorf("unknown main context: %s", gui.State.MainContext)
}
state := gui.State.Panels.LineByLine
// 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
}