diff --git a/pkg/gui/controllers/patch_explorer_controller.go b/pkg/gui/controllers/patch_explorer_controller.go index 393343581a3..5b832c80d34 100644 --- a/pkg/gui/controllers/patch_explorer_controller.go +++ b/pkg/gui/controllers/patch_explorer_controller.go @@ -1,8 +1,11 @@ package controllers import ( + "strings" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) type PatchExplorerControllerFactory struct { @@ -295,13 +298,49 @@ func (self *PatchExplorerController) CopySelectedToClipboard() error { selected := self.context.GetState().PlainRenderSelected() self.c.LogAction(self.c.Tr.Actions.CopySelectedTextToClipboard) - if err := self.c.OS().CopyToClipboard(selected); err != nil { + if err := self.c.OS().CopyToClipboard(dropDiffPrefix(selected)); err != nil { return err } return nil } +// Removes '+' or '-' from the beginning of each line in the diff string, except +// when both '+' and '-' lines are present, or diff header lines, in which case +// the diff is returned unchanged. This is useful for copying parts of diffs to +// the clipboard in order to paste them into code. +func dropDiffPrefix(diff string) string { + lines := strings.Split(strings.TrimRight(diff, "\n"), "\n") + + const ( + PLUS int = iota + MINUS + CONTEXT + OTHER + ) + + linesByType := lo.GroupBy(lines, func(line string) int { + switch { + case strings.HasPrefix(line, "+"): + return PLUS + case strings.HasPrefix(line, "-"): + return MINUS + case strings.HasPrefix(line, " "): + return CONTEXT + } + return OTHER + }) + + hasLinesOfType := func(lineType int) bool { return len(linesByType[lineType]) > 0 } + + keepPrefix := hasLinesOfType(OTHER) || (hasLinesOfType(PLUS) && hasLinesOfType(MINUS)) + if keepPrefix { + return diff + } + + return strings.Join(lo.Map(lines, func(line string, _ int) string { return line[1:] + "\n" }), "") +} + func (self *PatchExplorerController) isFocused() bool { return self.c.Context().Current().GetKey() == self.context.GetKey() } diff --git a/pkg/gui/controllers/patch_explorer_controller_test.go b/pkg/gui/controllers/patch_explorer_controller_test.go new file mode 100644 index 00000000000..4f815d1d390 --- /dev/null +++ b/pkg/gui/controllers/patch_explorer_controller_test.go @@ -0,0 +1,89 @@ +package controllers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_dropDiffPrefix(t *testing.T) { + scenarios := []struct { + name string + diff string + expectedResult string + }{ + { + name: "empty string", + diff: "", + expectedResult: "", + }, + { + name: "only added lines", + diff: `+line1 ++line2 +`, + expectedResult: `line1 +line2 +`, + }, + { + name: "added lines with context", + diff: ` line1 ++line2 +`, + expectedResult: `line1 +line2 +`, + }, + { + name: "only deleted lines", + diff: `-line1 +-line2 +`, + expectedResult: `line1 +line2 +`, + }, + { + name: "deleted lines with context", + diff: `-line1 + line2 +`, + expectedResult: `line1 +line2 +`, + }, + { + name: "only context", + diff: ` line1 + line2 +`, + expectedResult: `line1 +line2 +`, + }, + { + name: "added and deleted lines", + diff: `+line1 +-line2 +`, + expectedResult: `+line1 +-line2 +`, + }, + { + name: "hunk header lines", + diff: `@@ -1,8 +1,11 @@ + line1 +`, + expectedResult: `@@ -1,8 +1,11 @@ + line1 +`, + }, + } + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + assert.Equal(t, s.expectedResult, dropDiffPrefix(s.diff)) + }) + } +}