From 159bbb082594a0214a30744222d019a6e347f5ab Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 27 Apr 2025 12:02:56 +0200 Subject: [PATCH] Strip the '+' and '-' characters when copying parts of a diff to the clipboard This makes it easier to copy diff hunks and paste them into code. We only strip the prefixes if the copied lines are either all '+' or all '-' (possibly including context lines), otherwise we keep them. We also keep them when parts of a hunk header is included in the selection; this is useful for copying a diff hunk and pasting it into a github comment, for example. A not-quite-correct edge case is when you select the '--- a/file.txt' line of a diff header on its own; in this case we copy it as '-- a/file.txt' (same for the '+++' line). This is probably uncommon enough that it's not worth fixing (it's not trivial to fix because we don't know that we're in a header). --- .../controllers/patch_explorer_controller.go | 41 ++++++++- .../patch_explorer_controller_test.go | 89 +++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 pkg/gui/controllers/patch_explorer_controller_test.go 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)) + }) + } +}