From 73019574a886a825a09dbc8619f883983cd39278 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 20 Mar 2024 21:01:20 +0100 Subject: 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. --- pkg/commands/git_commands/file.go | 10 +++++++--- pkg/commands/git_commands/file_test.go | 20 ++++++++++++++------ pkg/gui/controllers/commits_files_controller.go | 22 ++++++++++++++++------ pkg/gui/controllers/files_controller.go | 22 ++++++++++++++++------ pkg/gui/controllers/helpers/files_helper.go | 19 ++++++++++++------- pkg/gui/controllers/status_controller.go | 4 +++- pkg/i18n/english.go | 2 +- 7 files changed, 69 insertions(+), 30 deletions(-) diff --git a/pkg/commands/git_commands/file.go b/pkg/commands/git_commands/file.go index 173e342f7..9401e6d54 100644 --- a/pkg/commands/git_commands/file.go +++ b/pkg/commands/git_commands/file.go @@ -8,6 +8,7 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" ) type FileCommands struct { @@ -75,18 +76,21 @@ func (self *FileCommands) GetEditCmdStrLegacy(filename string, lineNumber int) ( 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 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 } } 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{ - "filename": self.cmd.Quote(filename), + "filename": strings.Join(quotedFilenames, " "), } cmdStr := utils.ResolvePlaceholderString(template, templateValues) diff --git a/pkg/commands/git_commands/file_test.go b/pkg/commands/git_commands/file_test.go index c87e56683..25dd0a5d0 100644 --- a/pkg/commands/git_commands/file_test.go +++ b/pkg/commands/git_commands/file_test.go @@ -177,9 +177,9 @@ func TestEditFileCmdStrLegacy(t *testing.T) { } } -func TestEditFileCmd(t *testing.T) { +func TestEditFilesCmd(t *testing.T) { type scenario struct { - filename string + filenames []string osConfig config.OSConfig expectedCmdStr string suspend bool @@ -187,13 +187,13 @@ func TestEditFileCmd(t *testing.T) { scenarios := []scenario{ { - filename: "test", + filenames: []string{"test"}, osConfig: config.OSConfig{}, expectedCmdStr: `vim -- "test"`, suspend: true, }, { - filename: "test", + filenames: []string{"test"}, osConfig: config.OSConfig{ Edit: "nano {{filename}}", }, @@ -201,13 +201,21 @@ func TestEditFileCmd(t *testing.T) { suspend: true, }, { - filename: "file/with space", + filenames: []string{"file/with space"}, osConfig: config.OSConfig{ EditPreset: "sublime", }, expectedCmdStr: `subl -- "file/with space"`, suspend: false, }, + { + filenames: []string{"multiple", "files"}, + osConfig: config.OSConfig{ + EditPreset: "sublime", + }, + expectedCmdStr: `subl -- "multiple" "files"`, + suspend: false, + }, } for _, s := range scenarios { @@ -218,7 +226,7 @@ func TestEditFileCmd(t *testing.T) { userConfig: userConfig, }) - cmdStr, suspend := instance.GetEditCmdStr(s.filename) + cmdStr, suspend := instance.GetEditCmdStr(s.filenames) assert.Equal(t, s.expectedCmdStr, cmdStr) assert.Equal(t, s.suspend, suspend) } diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 59e0a7f4e..75524938c 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -65,8 +65,8 @@ func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) [] }, { Key: opts.GetKey(opts.Config.Universal.Edit), - Handler: self.withItem(self.edit), - GetDisabledReason: self.require(self.singleItemSelected()), + Handler: self.withItems(self.edit), + GetDisabledReason: self.require(self.itemsSelected(self.canEditFiles)), Description: self.c.Tr.Edit, Tooltip: self.c.Tr.EditFileTooltip, DisplayOnScreen: true, @@ -230,12 +230,22 @@ func (self *CommitFilesController) open(node *filetree.CommitFileNode) error { return self.c.Helpers().Files.OpenFile(node.GetPath()) } -func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error { - if node.File == nil { - return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory) +func (self *CommitFilesController) edit(nodes []*filetree.CommitFileNode) error { + return self.c.Helpers().Files.EditFiles(lo.FilterMap(nodes, + func(node *filetree.CommitFileNode, _ int) (string, bool) { + return node.GetPath(), node.IsFile() + })) +} + +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 self.c.Helpers().Files.EditFile(node.GetPath()) + return nil } func (self *CommitFilesController) openDiffTool(node *filetree.CommitFileNode) error { diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 504617218..313c23b42 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -86,8 +86,8 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types }, { Key: opts.GetKey(opts.Config.Universal.Edit), - Handler: self.withItem(self.edit), - GetDisabledReason: self.require(self.singleItemSelected()), + Handler: self.withItems(self.edit), + GetDisabledReason: self.require(self.itemsSelected(self.canEditFiles)), Description: self.c.Tr.Edit, Tooltip: self.c.Tr.EditFileTooltip, DisplayOnScreen: true, @@ -714,12 +714,22 @@ func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayF return self.c.PostRefreshUpdate(self.context()) } -func (self *FilesController) edit(node *filetree.FileNode) error { - if node.File == nil { - return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory) +func (self *FilesController) edit(nodes []*filetree.FileNode) error { + return self.c.Helpers().Files.EditFiles(lo.FilterMap(nodes, + func(node *filetree.FileNode, _ int) (string, bool) { + return node.GetPath(), node.IsFile() + })) +} + +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 self.c.Helpers().Files.EditFile(node.GetPath()) + return nil } func (self *FilesController) Open() error { diff --git a/pkg/gui/controllers/helpers/files_helper.go b/pkg/gui/controllers/helpers/files_helper.go index 35cd9d0c9..dded9877f 100644 --- a/pkg/gui/controllers/helpers/files_helper.go +++ b/pkg/gui/controllers/helpers/files_helper.go @@ -2,10 +2,12 @@ package helpers import ( "path/filepath" + + "github.com/samber/lo" ) type IFilesHelper interface { - EditFile(filename string) error + EditFiles(filenames []string) error EditFileAtLine(filename string, lineNumber int) error OpenFile(filename string) error } @@ -22,12 +24,15 @@ func NewFilesHelper(c *HelperCommon) *FilesHelper { var _ IFilesHelper = &FilesHelper{} -func (self *FilesHelper) EditFile(filename string) error { - absPath, err := filepath.Abs(filename) - if err != nil { - return err - } - cmdStr, suspend := self.c.Git().File.GetEditCmdStr(absPath) +func (self *FilesHelper) EditFiles(filenames []string) error { + absPaths := lo.Map(filenames, func(filename string, _ int) string { + absPath, err := filepath.Abs(filename) + if err != nil { + return filename + } + return absPath + }) + cmdStr, suspend := self.c.Git().File.GetEditCmdStr(absPaths) return self.callEditor(cmdStr, suspend) } diff --git a/pkg/gui/controllers/status_controller.go b/pkg/gui/controllers/status_controller.go index f0289bf3c..e8455fe22 100644 --- a/pkg/gui/controllers/status_controller.go +++ b/pkg/gui/controllers/status_controller.go @@ -217,7 +217,9 @@ func (self *StatusController) openConfig() 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 { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 7715001aa..6716673b3 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -1596,7 +1596,7 @@ func EnglishTranslationSet() TranslationSet { CommitAuthorCopiedToClipboard: "Commit author copied to clipboard", PatchCopiedToClipboard: "Patch 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", ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯", CommandLog: "Command log", -- cgit v1.2.3