mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-06-29 00:51:35 +02:00
allow opening files on the selected line in the staging panel
This commit is contained in:
@ -191,7 +191,7 @@ for users of VSCode
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
os:
|
os:
|
||||||
openCommand: 'code -r {{filename}}'
|
openCommand: 'code -rg {{filename}}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Color Attributes
|
## Color Attributes
|
||||||
|
@ -14,23 +14,42 @@ var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*
|
|||||||
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
|
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
|
||||||
|
|
||||||
type PatchHunk struct {
|
type PatchHunk struct {
|
||||||
header string
|
|
||||||
FirstLineIdx int
|
FirstLineIdx int
|
||||||
LastLineIdx int
|
oldStart int
|
||||||
|
newStart int
|
||||||
|
heading string
|
||||||
bodyLines []string
|
bodyLines []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHunk(header string, body string, firstLineIdx int) *PatchHunk {
|
func (hunk *PatchHunk) LastLineIdx() int {
|
||||||
bodyLines := strings.SplitAfter(header+body, "\n")[1:] // dropping the header line
|
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{
|
return &PatchHunk{
|
||||||
header: header,
|
oldStart: oldStart,
|
||||||
|
newStart: newStart,
|
||||||
|
heading: heading,
|
||||||
FirstLineIdx: firstLineIdx,
|
FirstLineIdx: firstLineIdx,
|
||||||
LastLineIdx: firstLineIdx + len(bodyLines),
|
|
||||||
bodyLines: 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 {
|
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
|
||||||
skippedNewlineMessageIndex := -1
|
skippedNewlineMessageIndex := -1
|
||||||
newLines := []string{}
|
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) {
|
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
|
||||||
changeCount := 0
|
changeCount := nLinesWithPrefix(newBodyLines, []string{"+", "-"})
|
||||||
oldLength := 0
|
oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"})
|
||||||
newLength := 0
|
newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "})
|
||||||
for _, line := range newBodyLines {
|
|
||||||
switch line[:1] {
|
|
||||||
case "+":
|
|
||||||
newLength++
|
|
||||||
changeCount++
|
|
||||||
case "-":
|
|
||||||
oldLength++
|
|
||||||
changeCount++
|
|
||||||
case " ":
|
|
||||||
oldLength++
|
|
||||||
newLength++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if changeCount == 0 {
|
if changeCount == 0 {
|
||||||
// if nothing has changed we just return nothing
|
// if nothing has changed we just return nothing
|
||||||
return startOffset, "", false
|
return startOffset, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// get oldstart, newstart, and heading from header
|
|
||||||
match := hunkHeaderRegexp.FindStringSubmatch(hunk.header)
|
|
||||||
|
|
||||||
var oldStart int
|
var oldStart int
|
||||||
if reverse {
|
if reverse {
|
||||||
oldStart = mustConvertToInt(match[2])
|
oldStart = hunk.newStart
|
||||||
} else {
|
} else {
|
||||||
oldStart = mustConvertToInt(match[1])
|
oldStart = hunk.oldStart
|
||||||
}
|
}
|
||||||
heading := match[3]
|
|
||||||
|
|
||||||
var newStartOffset int
|
var newStartOffset int
|
||||||
// if the hunk went from zero to positive length, we need to increment the starting point by one
|
// 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
|
newStart := oldStart + startOffset + newStartOffset
|
||||||
|
|
||||||
newStartOffset = startOffset + newLength - oldLength
|
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
|
return newStartOffset, formattedHeader, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,19 +164,33 @@ func GetHeaderFromDiff(diff string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetHunksFromDiff(diff string) []*PatchHunk {
|
func GetHunksFromDiff(diff string) []*PatchHunk {
|
||||||
headers := hunkHeaderRegexp.FindAllString(diff, -1)
|
hunks := []*PatchHunk{}
|
||||||
bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit
|
firstLineIdx := -1
|
||||||
|
var hunkLines []string
|
||||||
|
pastDiffHeader := false
|
||||||
|
|
||||||
headerFirstLineIndices := []int{}
|
for lineIdx, line := range strings.SplitAfter(diff, "\n") {
|
||||||
for lineIdx, line := range strings.Split(diff, "\n") {
|
isHunkHeader := strings.HasPrefix(line, "@@ -")
|
||||||
if strings.HasPrefix(line, "@@ -") {
|
|
||||||
headerFirstLineIndices = append(headerFirstLineIndices, lineIdx)
|
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))
|
if pastDiffHeader {
|
||||||
for index, header := range headers {
|
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
|
||||||
hunks[index] = newHunk(header, bodies[index], headerFirstLineIndices[index])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hunks
|
return hunks
|
||||||
@ -203,7 +219,7 @@ outer:
|
|||||||
for _, hunk := range d.hunks {
|
for _, hunk := range d.hunks {
|
||||||
// if there is any line in our lineIndices array that the hunk contains, we append it
|
// if there is any line in our lineIndices array that the hunk contains, we append it
|
||||||
for _, lineIdx := range lineIndices {
|
for _, lineIdx := range lineIndices {
|
||||||
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx {
|
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx() {
|
||||||
hunksInRange = append(hunksInRange, hunk)
|
hunksInRange = append(hunksInRange, hunk)
|
||||||
continue outer
|
continue outer
|
||||||
}
|
}
|
||||||
@ -251,7 +267,7 @@ func (d *PatchModifier) OriginalPatchLength() int {
|
|||||||
return 0
|
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 {
|
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)
|
p := NewPatchModifier(log, filename, diffText)
|
||||||
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
|
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
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -88,6 +89,15 @@ index e69de29..c6568ea 100644
|
|||||||
\ No newline at end of file
|
\ No newline at end of file
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const exampleHunk = `@@ -1,5 +1,5 @@
|
||||||
|
apple
|
||||||
|
-grape
|
||||||
|
+orange
|
||||||
|
...
|
||||||
|
...
|
||||||
|
...
|
||||||
|
`
|
||||||
|
|
||||||
// TestModifyPatchForRange is a function.
|
// TestModifyPatchForRange is a function.
|
||||||
func TestModifyPatchForRange(t *testing.T) {
|
func TestModifyPatchForRange(t *testing.T) {
|
||||||
type scenario struct {
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -63,7 +63,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
|
|||||||
}
|
}
|
||||||
|
|
||||||
for index, hunk := range p.PatchHunks {
|
for index, hunk := range p.PatchHunks {
|
||||||
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx {
|
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
|
||||||
resultIndex := index + offset
|
resultIndex := index + offset
|
||||||
if resultIndex < 0 {
|
if resultIndex < 0 {
|
||||||
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 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]
|
return p.PatchHunks[len(p.PatchHunks)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -995,6 +995,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||||||
Handler: gui.handleEscapePatchBuildingPanel,
|
Handler: gui.handleEscapePatchBuildingPanel,
|
||||||
Description: gui.Tr.SLocalize("ExitLineByLineMode"),
|
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",
|
ViewName: "main",
|
||||||
Contexts: []string{"patch-building", "staging"},
|
Contexts: []string{"patch-building", "staging"},
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-errors/errors"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"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)
|
prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||||
selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx)
|
selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx)
|
||||||
newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0)
|
newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0)
|
||||||
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
|
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
|
||||||
} else {
|
} else {
|
||||||
selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
|
selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
|
||||||
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
|
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
|
||||||
@ -121,7 +124,7 @@ func (gui *Gui) selectNewHunk(newHunk *commands.PatchHunk) error {
|
|||||||
state := gui.State.Panels.LineByLine
|
state := gui.State.Panels.LineByLine
|
||||||
state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
|
state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
|
||||||
if state.SelectMode == HUNK {
|
if state.SelectMode == HUNK {
|
||||||
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
|
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
|
||||||
} else {
|
} else {
|
||||||
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
||||||
}
|
}
|
||||||
@ -265,7 +268,7 @@ func (gui *Gui) focusSelection(includeCurrentHunk bool) error {
|
|||||||
if includeCurrentHunk {
|
if includeCurrentHunk {
|
||||||
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||||
firstLineIdx = hunk.FirstLineIdx
|
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
|
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 {
|
} else {
|
||||||
state.SelectMode = HUNK
|
state.SelectMode = HUNK
|
||||||
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
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 {
|
if err := gui.refreshMainView(); err != nil {
|
||||||
@ -325,3 +328,31 @@ func (gui *Gui) handleEscapeLineByLinePanel() {
|
|||||||
gui.changeMainViewsContext("normal")
|
gui.changeMainViewsContext("normal")
|
||||||
gui.State.Panels.LineByLine = nil
|
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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user