mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +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 | ||||
|   os: | ||||
|     openCommand: 'code -r {{filename}}' | ||||
|     openCommand: 'code -rg {{filename}}' | ||||
| ``` | ||||
|  | ||||
| ## Color Attributes | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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] | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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"}, | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user