From c9196812a2acdd60b52671efb035578fdf5592c2 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 15 Feb 2025 12:43:38 +0100 Subject: [PATCH] Add a "Copy to clipboard" menu to the Commit Files panel This is very similar to the same menu in the Files panel, except that it works on whatever diff is currently shown in the main view, including range diffs either in diffing mode (shift-W), or from a range selection of commits. --- docs/keybindings/Keybindings_en.md | 1 + docs/keybindings/Keybindings_ja.md | 1 + docs/keybindings/Keybindings_ko.md | 1 + docs/keybindings/Keybindings_nl.md | 1 + docs/keybindings/Keybindings_pl.md | 1 + docs/keybindings/Keybindings_pt.md | 1 + docs/keybindings/Keybindings_ru.md | 1 + docs/keybindings/Keybindings_zh-CN.md | 1 + docs/keybindings/Keybindings_zh-TW.md | 1 + .../controllers/commits_files_controller.go | 77 +++++++++++ .../tests/diff/copy_to_clipboard.go | 123 ++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 12 files changed, 210 insertions(+) create mode 100644 pkg/integration/tests/diff/copy_to_clipboard.go diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index f162014b7..8dc8b60a8 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -56,6 +56,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | Key | Action | Info | |-----|--------|-------------| | `` `` | Copy path to clipboard | | +| `` y `` | Copy to clipboard | | | `` c `` | Checkout | Checkout file. This replaces the file in your working tree with the version from the selected commit. | | `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. | | `` o `` | Open file | Open file in default application. | diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md index a1046c8dc..2e55fdee2 100644 --- a/docs/keybindings/Keybindings_ja.md +++ b/docs/keybindings/Keybindings_ja.md @@ -134,6 +134,7 @@ If you would instead like to start an interactive rebase from the selected commi | Key | Action | Info | |-----|--------|-------------| | `` `` | ファイル名をクリップボードにコピー | | +| `` y `` | Copy to clipboard | | | `` c `` | チェックアウト | Checkout file. This replaces the file in your working tree with the version from the selected commit. | | `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. | | `` o `` | ファイルを開く | Open file in default application. | diff --git a/docs/keybindings/Keybindings_ko.md b/docs/keybindings/Keybindings_ko.md index 127ac7166..e3c1c81ae 100644 --- a/docs/keybindings/Keybindings_ko.md +++ b/docs/keybindings/Keybindings_ko.md @@ -299,6 +299,7 @@ If you would instead like to start an interactive rebase from the selected commi | Key | Action | Info | |-----|--------|-------------| | `` `` | 파일명을 클립보드에 복사 | | +| `` y `` | 클립보드에 복사 | | | `` c `` | 체크아웃 | Checkout file | | `` d `` | Remove | Discard this commit's changes to this file | | `` o `` | 파일 닫기 | Open file in default application. | diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 37bacb20f..eb940bac2 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -129,6 +129,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | Key | Action | Info | |-----|--------|-------------| | `` `` | Kopieer de bestandsnaam naar het klembord | | +| `` y `` | Copy to clipboard | | | `` c `` | Uitchecken | Bestand uitchecken | | `` d `` | Remove | Uitsluit deze commit zijn veranderingen aan dit bestand | | `` o `` | Open bestand | Open file in default application. | diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index 8db7a0e73..a30007005 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -238,6 +238,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita, | Key | Action | Info | |-----|--------|-------------| | `` `` | Kopiuj ścieżkę do schowka | | +| `` y `` | Kopiuj do schowka | | | `` c `` | Przełącz | Przełącz plik. Zastępuje plik w twoim drzewie roboczym wersją z wybranego commita. | | `` d `` | Usuń | Odrzuć zmiany w tym pliku z tego commita. Uruchamia interaktywny rebase w tle, więc możesz otrzymać konflikt scalania, jeśli późniejszy commit również zmienia ten plik. | | `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. | diff --git a/docs/keybindings/Keybindings_pt.md b/docs/keybindings/Keybindings_pt.md index f256b9e78..aa933c179 100644 --- a/docs/keybindings/Keybindings_pt.md +++ b/docs/keybindings/Keybindings_pt.md @@ -135,6 +135,7 @@ Veja a documentação: | Key | Action | Info | |-----|--------|-------------| | `` `` | Copy path to clipboard | | +| `` y `` | Copy to clipboard | | | `` c `` | Verificar | Checkout file. This replaces the file in your working tree with the version from the selected commit. | | `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. | | `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. | diff --git a/docs/keybindings/Keybindings_ru.md b/docs/keybindings/Keybindings_ru.md index 9c17e4c6b..28abe78eb 100644 --- a/docs/keybindings/Keybindings_ru.md +++ b/docs/keybindings/Keybindings_ru.md @@ -261,6 +261,7 @@ If you would instead like to start an interactive rebase from the selected commi | Key | Action | Info | |-----|--------|-------------| | `` `` | Скопировать название файла в буфер обмена | | +| `` y `` | Copy to clipboard | | | `` c `` | Переключить | Переключить файл | | `` d `` | Remove | Отменить изменения коммита в этом файле | | `` o `` | Открыть файл | Open file in default application. | diff --git a/docs/keybindings/Keybindings_zh-CN.md b/docs/keybindings/Keybindings_zh-CN.md index 790a64f53..663dd98f0 100644 --- a/docs/keybindings/Keybindings_zh-CN.md +++ b/docs/keybindings/Keybindings_zh-CN.md @@ -186,6 +186,7 @@ _图例:`` 意味着ctrl+b, `意味着Alt+b, `B` 意味着shift+b_ | Key | Action | Info | |-----|--------|-------------| | `` `` | 将文件名复制到剪贴板 | | +| `` y `` | 复制到剪贴板 | | | `` c `` | 检出 | 检出文件 | | `` d `` | 删除 | 放弃对此文件的提交变更 | | `` o `` | 打开文件 | 使用默认程序打开该文件 | diff --git a/docs/keybindings/Keybindings_zh-TW.md b/docs/keybindings/Keybindings_zh-TW.md index 895a81e79..cb828d757 100644 --- a/docs/keybindings/Keybindings_zh-TW.md +++ b/docs/keybindings/Keybindings_zh-TW.md @@ -210,6 +210,7 @@ If you would instead like to start an interactive rebase from the selected commi | Key | Action | Info | |-----|--------|-------------| | `` `` | 複製檔案名稱到剪貼簿 | | +| `` y `` | 複製到剪貼簿 | | | `` c `` | 檢出 | 檢出檔案 | | `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. | | `` o `` | 開啟檔案 | 使用預設軟體開啟 | diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 61dfa1d85..0e54fb25b 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -41,6 +41,12 @@ func NewCommitFilesController( func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ + { + Key: opts.GetKey(opts.Config.Files.CopyFileInfoToClipboard), + Handler: self.openCopyMenu, + Description: self.c.Tr.CopyToClipboardMenu, + OpensMenu: true, + }, { Key: opts.GetKey(opts.Config.CommitFiles.CheckoutCommitFile), Handler: self.withItem(self.checkout), @@ -181,6 +187,77 @@ func (self *CommitFilesController) onClickMain(opts gocui.ViewMouseBindingOpts) return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y}) } +func (self *CommitFilesController) copyDiffToClipboard(path string, toastMessage string) error { + from, to := self.context().GetFromAndToForDiff() + from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from) + + cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, path, true) + diff, err := cmdObj.RunWithOutput() + if err != nil { + return err + } + if err := self.c.OS().CopyToClipboard(diff); err != nil { + return err + } + self.c.Toast(toastMessage) + return nil +} + +func (self *CommitFilesController) openCopyMenu() error { + node := self.context().GetSelected() + + copyNameItem := &types.MenuItem{ + Label: self.c.Tr.CopyFileName, + OnPress: func() error { + if err := self.c.OS().CopyToClipboard(node.Name()); err != nil { + return err + } + self.c.Toast(self.c.Tr.FileNameCopiedToast) + return nil + }, + DisabledReason: self.require(self.singleItemSelected())(), + Key: 'n', + } + copyPathItem := &types.MenuItem{ + Label: self.c.Tr.CopyFilePath, + OnPress: func() error { + if err := self.c.OS().CopyToClipboard(node.Path); err != nil { + return err + } + self.c.Toast(self.c.Tr.FilePathCopiedToast) + return nil + }, + DisabledReason: self.require(self.singleItemSelected())(), + Key: 'p', + } + copyFileDiffItem := &types.MenuItem{ + Label: self.c.Tr.CopySelectedDiff, + OnPress: func() error { + return self.copyDiffToClipboard(node.GetPath(), self.c.Tr.FileDiffCopiedToast) + }, + DisabledReason: self.require(self.singleItemSelected())(), + Key: 's', + } + copyAllDiff := &types.MenuItem{ + Label: self.c.Tr.CopyAllFilesDiff, + OnPress: func() error { + return self.copyDiffToClipboard(".", self.c.Tr.AllFilesDiffCopiedToast) + }, + DisabledReason: self.require(self.itemsSelected())(), + Key: 'a', + } + + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.CopyToClipboardMenu, + Items: []*types.MenuItem{ + copyNameItem, + copyPathItem, + copyFileDiffItem, + copyAllDiff, + }, + }) +} + func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error { self.c.LogAction(self.c.Tr.Actions.CheckoutFile) if err := self.c.Git().WorkingTree.CheckoutFile(self.context().GetRef().RefName(), node.GetPath()); err != nil { diff --git a/pkg/integration/tests/diff/copy_to_clipboard.go b/pkg/integration/tests/diff/copy_to_clipboard.go new file mode 100644 index 000000000..88c14cd48 --- /dev/null +++ b/pkg/integration/tests/diff/copy_to_clipboard.go @@ -0,0 +1,123 @@ +package diff + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +// note: this is required to simulate the clipboard during CI +func expectClipboard(t *TestDriver, matcher *TextMatcher) { + defer t.Shell().DeleteFile("clipboard") + + t.FileSystem().FileContent("clipboard", matcher) +} + +var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "The copy menu allows to copy name and diff of selected/all files", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard" + }, + SetupRepo: func(shell *Shell) { + shell.CreateDir("dir") + shell.CreateFileAndAdd("dir/file1", "1st line\n") + shell.Commit("1") + shell.CreateFileAndAdd("dir/file1", "1st line\n2nd line\n") + shell.CreateFileAndAdd("dir/file2", "file2\n") + shell.Commit("2") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("2").IsSelected(), + Contains("1"), + ). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Contains("dir").IsSelected(), + Contains("file1"), + Contains("file2"), + ). + NavigateToLine(Contains("file1")). + Press(keys.Files.CopyFileInfoToClipboard). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Copy to clipboard")). + Select(Contains("File name")). + Confirm(). + Tap(func() { + t.ExpectToast(Equals("File name copied to clipboard")) + expectClipboard(t, Equals("file1")) + }) + }). + Press(keys.Files.CopyFileInfoToClipboard). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Copy to clipboard")). + Select(Contains("Path")). + Confirm(). + Tap(func() { + t.ExpectToast(Equals("File path copied to clipboard")) + expectClipboard(t, Equals("dir/file1")) + }) + }). + Press(keys.Files.CopyFileInfoToClipboard). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Copy to clipboard")). + Select(Contains("Diff of selected file")). + Confirm(). + Tap(func() { + t.ExpectToast(Equals("File diff copied to clipboard")) + expectClipboard(t, + Contains("diff --git a/dir/file1 b/dir/file1").Contains("+2nd line").DoesNotContain("+1st line"). + DoesNotContain("diff --git a/dir/file2 b/dir/file2").DoesNotContain("+file2")) + }) + }). + Press(keys.Files.CopyFileInfoToClipboard). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Copy to clipboard")). + Select(Contains("Diff of all files")). + Confirm(). + Tap(func() { + t.ExpectToast(Equals("All files diff copied to clipboard")) + expectClipboard(t, + Contains("diff --git a/dir/file1 b/dir/file1").Contains("+2nd line").DoesNotContain("+1st line"). + Contains("diff --git a/dir/file2 b/dir/file2").Contains("+file2")) + }) + }) + + t.Views().Commits(). + Focus(). + // Select both commits + Press(keys.Universal.RangeSelectDown). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Contains("dir").IsSelected(), + Contains("file1"), + Contains("file2"), + ). + NavigateToLine(Contains("file1")). + Press(keys.Files.CopyFileInfoToClipboard). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Copy to clipboard")). + Select(Contains("Diff of selected file")). + Confirm(). + Tap(func() { + t.ExpectToast(Equals("File diff copied to clipboard")) + expectClipboard(t, + Contains("diff --git a/dir/file1 b/dir/file1").Contains("+1st line").Contains("+2nd line")) + }) + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 6a3830157..d0dc2a8a0 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -167,6 +167,7 @@ var tests = []*components.IntegrationTest{ demo.StageLines, demo.Undo, demo.WorktreeCreateFromBranches, + diff.CopyToClipboard, diff.Diff, diff.DiffAndApplyPatch, diff.DiffCommits,