1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-23 12:18:51 +02:00

Support editing multiple files at once using range selection

We pass all of them to a single editor command, hoping that the editor will be
able to handle multiple files (VS Code and vim do).

We ignore directories that happen to be in the selection range; this makes it
easier to edit multiple files in different folders in tree view. We show an
error if only directories are selected, though.
This commit is contained in:
Stefan Haller 2024-03-20 21:01:20 +01:00
parent 9b5269b490
commit 73019574a8
7 changed files with 69 additions and 30 deletions

View File

@ -8,6 +8,7 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
) )
type FileCommands struct { type FileCommands struct {
@ -75,18 +76,21 @@ func (self *FileCommands) GetEditCmdStrLegacy(filename string, lineNumber int) (
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
} }
func (self *FileCommands) GetEditCmdStr(filename string) (string, bool) { func (self *FileCommands) GetEditCmdStr(filenames []string) (string, bool) {
// Legacy support for old config; to be removed at some point // Legacy support for old config; to be removed at some point
if self.UserConfig.OS.Edit == "" && self.UserConfig.OS.EditCommandTemplate != "" { if self.UserConfig.OS.Edit == "" && self.UserConfig.OS.EditCommandTemplate != "" {
if cmdStr, err := self.GetEditCmdStrLegacy(filename, 1); err == nil { // If multiple files are selected, we'll simply edit just the first one.
// It's not worth fixing this for the legacy support.
if cmdStr, err := self.GetEditCmdStrLegacy(filenames[0], 1); err == nil {
return cmdStr, true return cmdStr, true
} }
} }
template, suspend := config.GetEditTemplate(&self.UserConfig.OS, self.guessDefaultEditor) template, suspend := config.GetEditTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
quotedFilenames := lo.Map(filenames, func(filename string, _ int) string { return self.cmd.Quote(filename) })
templateValues := map[string]string{ templateValues := map[string]string{
"filename": self.cmd.Quote(filename), "filename": strings.Join(quotedFilenames, " "),
} }
cmdStr := utils.ResolvePlaceholderString(template, templateValues) cmdStr := utils.ResolvePlaceholderString(template, templateValues)

View File

@ -177,9 +177,9 @@ func TestEditFileCmdStrLegacy(t *testing.T) {
} }
} }
func TestEditFileCmd(t *testing.T) { func TestEditFilesCmd(t *testing.T) {
type scenario struct { type scenario struct {
filename string filenames []string
osConfig config.OSConfig osConfig config.OSConfig
expectedCmdStr string expectedCmdStr string
suspend bool suspend bool
@ -187,13 +187,13 @@ func TestEditFileCmd(t *testing.T) {
scenarios := []scenario{ scenarios := []scenario{
{ {
filename: "test", filenames: []string{"test"},
osConfig: config.OSConfig{}, osConfig: config.OSConfig{},
expectedCmdStr: `vim -- "test"`, expectedCmdStr: `vim -- "test"`,
suspend: true, suspend: true,
}, },
{ {
filename: "test", filenames: []string{"test"},
osConfig: config.OSConfig{ osConfig: config.OSConfig{
Edit: "nano {{filename}}", Edit: "nano {{filename}}",
}, },
@ -201,13 +201,21 @@ func TestEditFileCmd(t *testing.T) {
suspend: true, suspend: true,
}, },
{ {
filename: "file/with space", filenames: []string{"file/with space"},
osConfig: config.OSConfig{ osConfig: config.OSConfig{
EditPreset: "sublime", EditPreset: "sublime",
}, },
expectedCmdStr: `subl -- "file/with space"`, expectedCmdStr: `subl -- "file/with space"`,
suspend: false, suspend: false,
}, },
{
filenames: []string{"multiple", "files"},
osConfig: config.OSConfig{
EditPreset: "sublime",
},
expectedCmdStr: `subl -- "multiple" "files"`,
suspend: false,
},
} }
for _, s := range scenarios { for _, s := range scenarios {
@ -218,7 +226,7 @@ func TestEditFileCmd(t *testing.T) {
userConfig: userConfig, userConfig: userConfig,
}) })
cmdStr, suspend := instance.GetEditCmdStr(s.filename) cmdStr, suspend := instance.GetEditCmdStr(s.filenames)
assert.Equal(t, s.expectedCmdStr, cmdStr) assert.Equal(t, s.expectedCmdStr, cmdStr)
assert.Equal(t, s.suspend, suspend) assert.Equal(t, s.suspend, suspend)
} }

View File

@ -65,8 +65,8 @@ func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Edit), Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.withItem(self.edit), Handler: self.withItems(self.edit),
GetDisabledReason: self.require(self.singleItemSelected()), GetDisabledReason: self.require(self.itemsSelected(self.canEditFiles)),
Description: self.c.Tr.Edit, Description: self.c.Tr.Edit,
Tooltip: self.c.Tr.EditFileTooltip, Tooltip: self.c.Tr.EditFileTooltip,
DisplayOnScreen: true, DisplayOnScreen: true,
@ -230,12 +230,22 @@ func (self *CommitFilesController) open(node *filetree.CommitFileNode) error {
return self.c.Helpers().Files.OpenFile(node.GetPath()) return self.c.Helpers().Files.OpenFile(node.GetPath())
} }
func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error { func (self *CommitFilesController) edit(nodes []*filetree.CommitFileNode) error {
if node.File == nil { return self.c.Helpers().Files.EditFiles(lo.FilterMap(nodes,
return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory) func(node *filetree.CommitFileNode, _ int) (string, bool) {
return node.GetPath(), node.IsFile()
}))
} }
return self.c.Helpers().Files.EditFile(node.GetPath()) func (self *CommitFilesController) canEditFiles(nodes []*filetree.CommitFileNode) *types.DisabledReason {
if lo.NoneBy(nodes, func(node *filetree.CommitFileNode) bool { return node.IsFile() }) {
return &types.DisabledReason{
Text: self.c.Tr.ErrCannotEditDirectory,
ShowErrorInPanel: true,
}
}
return nil
} }
func (self *CommitFilesController) openDiffTool(node *filetree.CommitFileNode) error { func (self *CommitFilesController) openDiffTool(node *filetree.CommitFileNode) error {

View File

@ -86,8 +86,8 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Edit), Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.withItem(self.edit), Handler: self.withItems(self.edit),
GetDisabledReason: self.require(self.singleItemSelected()), GetDisabledReason: self.require(self.itemsSelected(self.canEditFiles)),
Description: self.c.Tr.Edit, Description: self.c.Tr.Edit,
Tooltip: self.c.Tr.EditFileTooltip, Tooltip: self.c.Tr.EditFileTooltip,
DisplayOnScreen: true, DisplayOnScreen: true,
@ -714,12 +714,22 @@ func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayF
return self.c.PostRefreshUpdate(self.context()) return self.c.PostRefreshUpdate(self.context())
} }
func (self *FilesController) edit(node *filetree.FileNode) error { func (self *FilesController) edit(nodes []*filetree.FileNode) error {
if node.File == nil { return self.c.Helpers().Files.EditFiles(lo.FilterMap(nodes,
return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory) func(node *filetree.FileNode, _ int) (string, bool) {
return node.GetPath(), node.IsFile()
}))
} }
return self.c.Helpers().Files.EditFile(node.GetPath()) func (self *FilesController) canEditFiles(nodes []*filetree.FileNode) *types.DisabledReason {
if lo.NoneBy(nodes, func(node *filetree.FileNode) bool { return node.IsFile() }) {
return &types.DisabledReason{
Text: self.c.Tr.ErrCannotEditDirectory,
ShowErrorInPanel: true,
}
}
return nil
} }
func (self *FilesController) Open() error { func (self *FilesController) Open() error {

View File

@ -2,10 +2,12 @@ package helpers
import ( import (
"path/filepath" "path/filepath"
"github.com/samber/lo"
) )
type IFilesHelper interface { type IFilesHelper interface {
EditFile(filename string) error EditFiles(filenames []string) error
EditFileAtLine(filename string, lineNumber int) error EditFileAtLine(filename string, lineNumber int) error
OpenFile(filename string) error OpenFile(filename string) error
} }
@ -22,12 +24,15 @@ func NewFilesHelper(c *HelperCommon) *FilesHelper {
var _ IFilesHelper = &FilesHelper{} var _ IFilesHelper = &FilesHelper{}
func (self *FilesHelper) EditFile(filename string) error { func (self *FilesHelper) EditFiles(filenames []string) error {
absPaths := lo.Map(filenames, func(filename string, _ int) string {
absPath, err := filepath.Abs(filename) absPath, err := filepath.Abs(filename)
if err != nil { if err != nil {
return err return filename
} }
cmdStr, suspend := self.c.Git().File.GetEditCmdStr(absPath) return absPath
})
cmdStr, suspend := self.c.Git().File.GetEditCmdStr(absPaths)
return self.callEditor(cmdStr, suspend) return self.callEditor(cmdStr, suspend)
} }

View File

@ -217,7 +217,9 @@ func (self *StatusController) openConfig() error {
} }
func (self *StatusController) editConfig() error { func (self *StatusController) editConfig() error {
return self.askForConfigFile(self.c.Helpers().Files.EditFile) return self.askForConfigFile(func(file string) error {
return self.c.Helpers().Files.EditFiles([]string{file})
})
} }
func (self *StatusController) showAllBranchLogs() error { func (self *StatusController) showAllBranchLogs() error {

View File

@ -1596,7 +1596,7 @@ func EnglishTranslationSet() TranslationSet {
CommitAuthorCopiedToClipboard: "Commit author copied to clipboard", CommitAuthorCopiedToClipboard: "Commit author copied to clipboard",
PatchCopiedToClipboard: "Patch copied to clipboard", PatchCopiedToClipboard: "Patch copied to clipboard",
CopiedToClipboard: "Copied to clipboard", CopiedToClipboard: "Copied to clipboard",
ErrCannotEditDirectory: "Cannot edit directory: you can only edit individual files", ErrCannotEditDirectory: "Cannot edit directories: you can only edit individual files",
ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first", ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first",
ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯", ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯",
CommandLog: "Command log", CommandLog: "Command log",