mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-01-04 03:48:07 +02:00
feat: introduce a copy menu into the file view
This commit is contained in:
parent
2162e5ff64
commit
c7012528fc
@ -17,3 +17,26 @@ func (self *DiffCommands) DiffCmdObj(diffArgs []string) oscommands.ICmdObj {
|
||||
NewGitCmd("diff").Arg("--submodule", "--no-ext-diff", "--color").Arg(diffArgs...).ToArgv(),
|
||||
)
|
||||
}
|
||||
|
||||
func (self *DiffCommands) internalDiffCmdObj(diffArgs ...string) *GitCommandBuilder {
|
||||
return NewGitCmd("diff").
|
||||
Arg("--no-ext-diff", "--no-color").
|
||||
Arg(diffArgs...)
|
||||
}
|
||||
|
||||
func (self *DiffCommands) GetPathDiff(path string, staged bool) (string, error) {
|
||||
return self.cmd.New(
|
||||
self.internalDiffCmdObj().
|
||||
ArgIf(staged, "--staged").
|
||||
Arg(path).
|
||||
ToArgv(),
|
||||
).RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *DiffCommands) GetAllDiff(staged bool) (string, error) {
|
||||
return self.cmd.New(
|
||||
self.internalDiffCmdObj().
|
||||
ArgIf(staged, "--staged").
|
||||
ToArgv(),
|
||||
).RunWithOutput()
|
||||
}
|
||||
|
@ -371,6 +371,7 @@ type KeybindingFilesConfig struct {
|
||||
ToggleTreeView string `yaml:"toggleTreeView"`
|
||||
OpenMergeTool string `yaml:"openMergeTool"`
|
||||
OpenStatusFilter string `yaml:"openStatusFilter"`
|
||||
CopyFileInfoToClipboard string `yaml:"copyFileInfoToClipboard"`
|
||||
}
|
||||
|
||||
type KeybindingBranchesConfig struct {
|
||||
@ -763,6 +764,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
OpenMergeTool: "M",
|
||||
OpenStatusFilter: "<c-b>",
|
||||
ConfirmDiscard: "x",
|
||||
CopyFileInfoToClipboard: "y",
|
||||
},
|
||||
Branches: KeybindingBranchesConfig{
|
||||
CopyPullRequestURL: "<c-y>",
|
||||
|
@ -37,6 +37,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
||||
Handler: self.handleStatusFilterPressed,
|
||||
Description: self.c.Tr.FileFilter,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Files.CopyFileInfoToClipboard),
|
||||
Handler: self.openCopyMenu,
|
||||
Description: self.c.Tr.CopyToClipboardMenu,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Files.CommitChanges),
|
||||
Handler: self.c.Helpers().WorkingTree.HandleCommitPress,
|
||||
@ -748,6 +754,103 @@ func (self *FilesController) createStashMenu() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) 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 self.c.Error(err)
|
||||
}
|
||||
self.c.Toast(self.c.Tr.FileNameCopiedToast)
|
||||
return nil
|
||||
},
|
||||
Key: 'n',
|
||||
}
|
||||
copyPathItem := &types.MenuItem{
|
||||
Label: self.c.Tr.CopyFilePath,
|
||||
OnPress: func() error {
|
||||
if err := self.c.OS().CopyToClipboard(node.Path); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.c.Toast(self.c.Tr.FilePathCopiedToast)
|
||||
return nil
|
||||
},
|
||||
Key: 'p',
|
||||
}
|
||||
copyFileDiffItem := &types.MenuItem{
|
||||
Label: self.c.Tr.CopySelectedDiff,
|
||||
Tooltip: self.c.Tr.CopyFileDiffTooltip,
|
||||
OnPress: func() error {
|
||||
path := self.context().GetSelectedPath()
|
||||
hasStaged := self.hasPathStagedChanges(node)
|
||||
diff, err := self.c.Git().Diff.GetPathDiff(path, hasStaged)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
if err := self.c.OS().CopyToClipboard(diff); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.c.Toast(self.c.Tr.FileDiffCopiedToast)
|
||||
return nil
|
||||
},
|
||||
Key: 's',
|
||||
}
|
||||
copyAllDiff := &types.MenuItem{
|
||||
Label: self.c.Tr.CopyAllFilesDiff,
|
||||
Tooltip: self.c.Tr.CopyFileDiffTooltip,
|
||||
OnPress: func() error {
|
||||
hasStaged := self.c.Helpers().WorkingTree.AnyStagedFiles()
|
||||
diff, err := self.c.Git().Diff.GetAllDiff(hasStaged)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
if err := self.c.OS().CopyToClipboard(diff); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.c.Toast(self.c.Tr.AllFilesDiffCopiedToast)
|
||||
return nil
|
||||
},
|
||||
Key: 'a',
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
copyNameItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
copyPathItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
copyFileDiffItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
}
|
||||
if node != nil && !node.GetHasStagedOrTrackedChanges() {
|
||||
copyFileDiffItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
}
|
||||
if !self.anyStagedOrTrackedFile() {
|
||||
copyAllDiff.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
}
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.CopyToClipboardMenu,
|
||||
Items: []*types.MenuItem{
|
||||
copyNameItem,
|
||||
copyPathItem,
|
||||
copyFileDiffItem,
|
||||
copyAllDiff,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) anyStagedOrTrackedFile() bool {
|
||||
if !self.c.Helpers().WorkingTree.AnyStagedFiles() {
|
||||
return self.c.Helpers().WorkingTree.AnyTrackedFiles()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *FilesController) hasPathStagedChanges(node *filetree.FileNode) bool {
|
||||
return node.SomeFile(func(t *models.File) bool {
|
||||
return t.HasStagedChanges
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) stash() error {
|
||||
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashAllChanges)
|
||||
}
|
||||
|
@ -30,6 +30,15 @@ func (self *FileNode) GetHasUnstagedChanges() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.HasUnstagedChanges })
|
||||
}
|
||||
|
||||
func (self *FileNode) GetHasStagedOrTrackedChanges() bool {
|
||||
if !self.GetHasStagedChanges() {
|
||||
return self.SomeFile(func(t *models.File) bool {
|
||||
return t.Tracked
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *FileNode) GetHasStagedChanges() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.HasStagedChanges })
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
@ -300,3 +302,7 @@ func (self *Node[T]) ID() string {
|
||||
func (self *Node[T]) Description() string {
|
||||
return self.GetPath()
|
||||
}
|
||||
|
||||
func (self *Node[T]) Name() string {
|
||||
return path.Base(self.Path)
|
||||
}
|
||||
|
@ -52,6 +52,17 @@ type TranslationSet struct {
|
||||
Pull string
|
||||
Scroll string
|
||||
FileFilter string
|
||||
CopyToClipboardMenu string
|
||||
CopyFileName string
|
||||
CopyFilePath string
|
||||
CopyFileDiffTooltip string
|
||||
CopySelectedDiff string
|
||||
CopyAllFilesDiff string
|
||||
NoContentToCopyError string
|
||||
FileNameCopiedToast string
|
||||
FilePathCopiedToast string
|
||||
FileDiffCopiedToast string
|
||||
AllFilesDiffCopiedToast string
|
||||
FilterStagedFiles string
|
||||
FilterUnstagedFiles string
|
||||
ResetFilter string
|
||||
@ -851,6 +862,17 @@ func EnglishTranslationSet() TranslationSet {
|
||||
CantCheckoutBranchWhilePulling: "You cannot checkout another branch while pulling the current branch",
|
||||
CantPullOrPushSameBranchTwice: "You cannot push or pull a branch while it is already being pushed or pulled",
|
||||
FileFilter: "Filter files by status",
|
||||
CopyToClipboardMenu: "Copy to clipboard",
|
||||
CopyFileName: "File name",
|
||||
CopyFilePath: "Path",
|
||||
CopyFileDiffTooltip: "If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.",
|
||||
CopySelectedDiff: "Diff of selected file",
|
||||
CopyAllFilesDiff: "Diff of all files",
|
||||
NoContentToCopyError: "Nothing to copy",
|
||||
FileNameCopiedToast: "File name copied to clipboard",
|
||||
FilePathCopiedToast: "File path copied to clipboard",
|
||||
FileDiffCopiedToast: "File diff copied to clipboard",
|
||||
AllFilesDiffCopiedToast: "All files diff copied to clipboard",
|
||||
FilterStagedFiles: "Show only staged files",
|
||||
FilterUnstagedFiles: "Show only unstaged files",
|
||||
ResetFilter: "Reset filter",
|
||||
|
185
pkg/integration/tests/file/copy_menu.go
Normal file
185
pkg/integration/tests/file/copy_menu.go
Normal file
@ -0,0 +1,185 @@
|
||||
package file
|
||||
|
||||
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 CopyMenu = 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.UserConfig.OS.CopyToClipboardCmd = "echo {{text}} > clipboard"
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
// Disabled item
|
||||
t.Views().Files().
|
||||
IsEmpty().
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("File name")).
|
||||
Tooltip(Equals("Disabled: Nothing to copy")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("Nothing to copy")).
|
||||
Confirm()
|
||||
})
|
||||
|
||||
t.Shell().
|
||||
CreateDir("dir").
|
||||
CreateFile("dir/1-unstaged_file", "unstaged content")
|
||||
|
||||
// Empty content (new file)
|
||||
t.Views().Files().
|
||||
Press(keys.Universal.Refresh).
|
||||
Lines(
|
||||
Contains("dir").IsSelected(),
|
||||
Contains("unstaged_file"),
|
||||
).
|
||||
SelectNextItem().
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Diff of selected file")).
|
||||
Tooltip(Contains("Disabled: Nothing to copy")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("Nothing to copy")).
|
||||
Confirm()
|
||||
}).
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Diff of all files")).
|
||||
Tooltip(Contains("Disabled: Nothing to copy")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("Nothing to copy")).
|
||||
Confirm()
|
||||
})
|
||||
|
||||
t.Shell().
|
||||
GitAdd("dir/1-unstaged_file").
|
||||
Commit("commit-unstaged").
|
||||
UpdateFile("dir/1-unstaged_file", "unstaged content (new)").
|
||||
CreateFileAndAdd("dir/2-staged_file", "staged content").
|
||||
Commit("commit-staged").
|
||||
UpdateFile("dir/2-staged_file", "staged content (new)").
|
||||
GitAdd("dir/2-staged_file")
|
||||
|
||||
// Copy file name
|
||||
t.Views().Files().
|
||||
Press(keys.Universal.Refresh).
|
||||
Lines(
|
||||
Contains("dir"),
|
||||
Contains("unstaged_file").IsSelected(),
|
||||
Contains("staged_file"),
|
||||
).
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("File name")).
|
||||
Confirm()
|
||||
|
||||
expectClipboard(t, Contains("unstaged_file"))
|
||||
})
|
||||
|
||||
// Copy file path
|
||||
t.Views().Files().
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Path")).
|
||||
Confirm()
|
||||
|
||||
expectClipboard(t, Contains("dir/1-unstaged_file"))
|
||||
})
|
||||
|
||||
// Selected path diff on a single (unstaged) file
|
||||
t.Views().Files().
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Diff of selected file")).
|
||||
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||
Confirm()
|
||||
|
||||
expectClipboard(t, Contains("+unstaged content (new)"))
|
||||
})
|
||||
|
||||
// Selected path diff with staged and unstaged files
|
||||
t.Views().Files().
|
||||
SelectPreviousItem().
|
||||
Lines(
|
||||
Contains("dir").IsSelected(),
|
||||
Contains("unstaged_file"),
|
||||
Contains("staged_file"),
|
||||
).
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Diff of selected file")).
|
||||
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||
Confirm()
|
||||
|
||||
expectClipboard(t, Contains("+staged content (new)"))
|
||||
})
|
||||
|
||||
// All files diff with staged files
|
||||
t.Views().Files().
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Diff of all files")).
|
||||
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||
Confirm()
|
||||
|
||||
expectClipboard(t, Contains("+staged content (new)"))
|
||||
})
|
||||
|
||||
// All files diff with no staged files
|
||||
t.Views().Files().
|
||||
SelectNextItem().
|
||||
SelectNextItem().
|
||||
Lines(
|
||||
Contains("dir"),
|
||||
Contains("unstaged_file"),
|
||||
Contains("staged_file").IsSelected(),
|
||||
).
|
||||
Press(keys.Universal.Select).
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Diff of all files")).
|
||||
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||
Confirm()
|
||||
|
||||
expectClipboard(t, Contains("+staged content (new)").Contains("+unstaged content (new)"))
|
||||
})
|
||||
},
|
||||
})
|
@ -117,6 +117,7 @@ var tests = []*components.IntegrationTest{
|
||||
diff.DiffAndApplyPatch,
|
||||
diff.DiffCommits,
|
||||
diff.IgnoreWhitespace,
|
||||
file.CopyMenu,
|
||||
file.DirWithUntrackedFile,
|
||||
file.DiscardAllDirChanges,
|
||||
file.DiscardChanges,
|
||||
|
Loading…
Reference in New Issue
Block a user