diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index 20bca0c61..bd1322940 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -228,6 +228,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Edit file | Open file in external editor. | | `` `` | Stage | Toggle selection staged / unstaged. | | `` d `` | Discard | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` E `` | Edit hunk | Edit selected hunk in external editor. | | `` `` | Copy selected text to clipboard | | | `` , h `` | Go to previous hunk | | | `` , l `` | Go to next hunk | | @@ -347,6 +348,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Edit file | Open file in external editor. | | `` `` | Stage | Toggle selection staged / unstaged. | | `` d `` | Discard | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` E `` | Edit hunk | Edit selected hunk in external editor. | | `` `` | Copy selected text to clipboard | | | `` , h `` | Go to previous hunk | | | `` , l `` | Go to next hunk | | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index 6d2767953..7acb4556d 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -197,6 +197,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 | | `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | | `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 | +| `` E `` | ハンクを編集 | 選択したハンクを外部エディタで編集します。 | | `` `` | 選択したテキストをクリップボードにコピー | | | `` , h `` | 前のハンクに移動 | | | `` , l `` | 次のハンクに移動 | | @@ -324,6 +325,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 | | `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | | `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 | +| `` E `` | ハンクを編集 | 選択したハンクを外部エディタで編集します。 | | `` `` | 選択したテキストをクリップボードにコピー | | | `` , h `` | 前のハンクに移動 | | | `` , l `` | 次のハンクに移動 | | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index dab679603..f83706741 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -89,6 +89,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | 파일 편집 | Open file in external editor. | | `` `` | Staged 전환 | 선택한 행을 staged / unstaged | | `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` E `` | Edit hunk | Edit selected hunk in external editor. | | `` `` | 선택한 텍스트를 클립보드에 복사 | | | `` , h `` | 이전 hunk를 선택 | | | `` , l `` | 다음 hunk를 선택 | | @@ -181,6 +182,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | 파일 편집 | Open file in external editor. | | `` `` | Staged 전환 | 선택한 행을 staged / unstaged | | `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` E `` | Edit hunk | Edit selected hunk in external editor. | | `` `` | 선택한 텍스트를 클립보드에 복사 | | | `` , h `` | 이전 hunk를 선택 | | | `` , l `` | 다음 hunk를 선택 | | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index f336abffa..2520842b4 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -236,6 +236,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Verander bestand | Open bestand in externe editor. | | `` `` | Toggle staged | Toggle lijnen staged / unstaged | | `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` E `` | Edit hunk | Edit selected hunk in external editor. | | `` `` | Copy selected text to clipboard | | | `` , h `` | Selecteer de vorige hunk | | | `` , l `` | Selecteer de volgende hunk | | @@ -325,6 +326,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Verander bestand | Open bestand in externe editor. | | `` `` | Toggle staged | Toggle lijnen staged / unstaged | | `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` E `` | Edit hunk | Edit selected hunk in external editor. | | `` `` | Copy selected text to clipboard | | | `` , h `` | Selecteer de vorige hunk | | | `` , l `` | Selecteer de volgende hunk | | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index b9f928561..846194c46 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -104,6 +104,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. | | `` `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. | | `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. | +| `` E `` | Edytuj fragment | Edytuj wybrany fragment w zewnętrznym edytorze. | | `` `` | Kopiuj zaznaczony tekst do schowka | | | `` , h `` | Idź do poprzedniego fragmentu | | | `` , l `` | Idź do następnego fragmentu | | @@ -220,6 +221,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. | | `` `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. | | `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. | +| `` E `` | Edytuj fragment | Edytuj wybrany fragment w zewnętrznym edytorze. | | `` `` | Kopiuj zaznaczony tekst do schowka | | | `` , h `` | Idź do poprzedniego fragmentu | | | `` , l `` | Idź do następnego fragmentu | | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index 4e4f954e1..f18c1b83f 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -240,6 +240,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Editar arquivo | Abrir arquivo no editor externo. | | `` `` | Etapa | Ativar/desativar seleção em staged/unstaged | | `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. | +| `` E `` | Editar hunk | Editar o local selecionado no editor externo. | | `` `` | Copiar texto selecionado para área de transferência | | | `` , h `` | Ir para o local anterior | | | `` , l `` | Ir para o próximo trecho | | @@ -356,6 +357,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Editar arquivo | Abrir arquivo no editor externo. | | `` `` | Etapa | Ativar/desativar seleção em staged/unstaged | | `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. | +| `` E `` | Editar hunk | Editar o local selecionado no editor externo. | | `` `` | Copiar texto selecionado para área de transferência | | | `` , h `` | Ir para o local anterior | | | `` , l `` | Ir para o próximo trecho | | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index 93b252eae..5b61abaa0 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -79,6 +79,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Редактировать файл | Open file in external editor. | | `` `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные | | `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` E `` | Изменить эту часть | Edit selected hunk in external editor. | | `` `` | Скопировать выделенный текст в буфер обмена | | | `` , h `` | Выбрать предыдущую часть | | | `` , l `` | Выбрать следующую часть | | @@ -125,6 +126,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | Редактировать файл | Open file in external editor. | | `` `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные | | `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | +| `` E `` | Изменить эту часть | Edit selected hunk in external editor. | | `` `` | Скопировать выделенный текст в буфер обмена | | | `` , h `` | Выбрать предыдущую часть | | | `` , l `` | Выбрать следующую часть | | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index 499651f96..831c0724a 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -291,6 +291,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | 编辑文件 | 使用外部编辑器打开文件 | | `` `` | 切换暂存状态 | 切换行暂存状态 | | `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 | +| `` E `` | 编辑代码块 | 在外部编辑器中编辑选中的代码块 | | `` `` | 复制选中文本到剪贴板 | | | `` , h `` | 选择上一个区块 | | | `` , l `` | 选择下一个区块 | | @@ -353,6 +354,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | 编辑文件 | 使用外部编辑器打开文件 | | `` `` | 切换暂存状态 | 切换行暂存状态 | | `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 | +| `` E `` | 编辑代码块 | 在外部编辑器中编辑选中的代码块 | | `` `` | 复制选中文本到剪贴板 | | | `` , h `` | 选择上一个区块 | | | `` , l `` | 选择下一个区块 | | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index 8d6994350..83c46bcd4 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -80,6 +80,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | | `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 | +| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 | | `` `` | 複製所選文本至剪貼簿 | | | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | @@ -375,6 +376,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | | `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 | +| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 | | `` `` | 複製所選文本至剪貼簿 | | | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 949fae006..a4b5787ce 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -97,6 +97,14 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty Tooltip: self.c.Tr.DiscardSelectionTooltip, DisplayOnScreen: true, }, + { + Keys: opts.GetKeys(opts.Config.Main.EditSelectHunk), + Handler: self.editHunk, + Description: self.c.Tr.EditHunk, + DescriptionFunc: self.workingTreeActionDescription(self.c.Tr.EditHunk), + GetDisabledReason: self.diffSelectionDisabledReason, + Tooltip: self.c.Tr.EditHunkTooltip, + }, { Keys: opts.GetKeys(opts.Config.Universal.CopyToClipboard), Handler: self.copySelection, @@ -396,6 +404,21 @@ func (self *MainViewController) discardSelection() error { return actions.DiscardSelection(self.context, first, last) } +// editHunk hands the hunk around the selection to an editor, and is only offered over +// the working tree's diff: what comes back is applied to the index, which is not +// something a commit's diff has any use for. +func (self *MainViewController) editHunk() error { + actions, ok := self.diffSource().(*WorkingTreeDiffActions) + if !ok { + return nil + } + first, last, ok := self.context.GetView().SelectedBufferLineRange() + if !ok { + return nil + } + return actions.EditHunk(self.context, first, last) +} + // workingTreeAction wraps a command that acts on the working tree — committing, finding // the commit to fix up — so that it only runs while the focused main view is showing the // working tree's diff. Over a commit's diff the key does nothing, so that browsing diff --git a/pkg/gui/controllers/working_tree_diff_actions.go b/pkg/gui/controllers/working_tree_diff_actions.go index 233ecec13..0dd1383da 100644 --- a/pkg/gui/controllers/working_tree_diff_actions.go +++ b/pkg/gui/controllers/working_tree_diff_actions.go @@ -3,6 +3,7 @@ package controllers import ( "fmt" "path/filepath" + "strings" "github.com/jesseduffield/generics/set" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" @@ -99,6 +100,87 @@ func (self *WorkingTreeDiffActions) DiscardSelection(pane types.DiffPaneContext, }) } +// EditHunk opens the git hunk holding the selection in an editor, as a patch against +// the index, and applies whatever comes back. It is how you stage something the diff +// can't express — half of a changed line, or a change written differently from either +// side — since what the editor hands back is applied rather than matched against the +// file's own diff. +// +// The hunk is the git one, context and all, rather than lazygit's block of adjacent +// changes: an editable patch is one that still applies, and the context lines are what +// let git place it. +func (self *WorkingTreeDiffActions) EditHunk( + pane types.DiffPaneContext, firstBufferLine int, lastBufferLine int, +) error { + infos, onStagedSide, ok := self.diffLineSelection(pane, firstBufferLine, lastBufferLine) + if !ok { + return nil + } + file := self.fileForDiffLinePath(infos[0].Path) + if file == nil { + return nil + } + + parsedPatch := patch.Parse(self.c.Git().WorkingTree.WorktreeFileDiff(file, git_commands.DiffModePlain, onStagedSide)) + lineIndices := changeLineIndices(parsedPatch, infos[:1]) + if len(lineIndices) == 0 { + return nil + } + + hunkIdx := parsedPatch.HunkContainingLine(lineIndices[0]) + hunkStartIdx := parsedPatch.HunkStartIdx(hunkIdx) + patchText := parsedPatch. + Transform(patch.TransformOpts{ + Reverse: onStagedSide, + IncludedLineIndices: patch.ExpandRange(hunkStartIdx, parsedPatch.HunkEndIdx(hunkIdx)), + FileNameOverride: file.GetPath(), + }). + FormatPlain() + + patchFilepath, err := self.c.Git().Patch.SaveTemporaryPatch(patchText) + if err != nil { + return err + } + + // The patch is written with a two-line header before its hunk, so the line the + // user was on sits that much further down the file they are about to edit. + const headerLineCount = 2 + if err := self.c.Helpers().Files.EditFileAtLineAndWait(patchFilepath, + lineIndices[0]-hunkStartIdx+headerLineCount+1); err != nil { + return err + } + + editedPatchText, err := self.c.Git().File.Cat(patchFilepath) + if err != nil { + return err + } + + self.c.LogAction(self.c.Tr.Actions.ApplyPatch) + + // Everything the editor left behind is taken, this being a patch the user wrote + // rather than a selection out of one of ours. + lineCount := strings.Count(editedPatchText, "\n") + 1 + newPatchText := patch. + Parse(editedPatchText). + Transform(patch.TransformOpts{ + IncludedLineIndices: patch.ExpandRange(0, lineCount), + FileNameOverride: file.GetPath(), + }). + FormatPlain() + + if err := self.c.Git().Patch.ApplyPatch(newPatchText, git_commands.ApplyPatchOpts{ + Reverse: onStagedSide, + Cached: true, + }); err != nil { + return err + } + + // Block input until the refresh has landed, as the staging commands do: the diff is + // about to be rebuilt from a file that no longer looks the way it did. + self.c.RefreshBlockingInput(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) + return nil +} + // diffLineSelection resolves what the user has selected in a pane of the focused main // view to the change lines to act on, and reports whether they are the staged side of // the diff — which is a question about the pane, so it is the same for every file of a diff --git a/pkg/integration/tests/main_view/edit_hunk_in_focused_diff.go b/pkg/integration/tests/main_view/edit_hunk_in_focused_diff.go new file mode 100644 index 000000000..9b5b52347 --- /dev/null +++ b/pkg/integration/tests/main_view/edit_hunk_in_focused_diff.go @@ -0,0 +1,59 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var EditHunkInFocusedDiff = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Edit the hunk around the selection in an editor, and stage what comes back", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + // Stand in for the editor: record the line it was pointed at, outside the + // repo so that the files panel keeps saying what the test is about, then + // write a patch that stages something neither side of the diff says. That + // is the point of editing a hunk. + cfg.GetUserConfig().OS.EditAtLineAndWait = "echo {{line}} > ../edit-line && " + + "printf '%s\\n' '--- a/file1' '+++ b/file1' '@@ -1,3 +1,3 @@' " + + "' one' '-two' '+TWO_EDITED' ' three' > {{filename}}" + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n") + shell.Commit("one") + + shell.UpdateFile("file1", "one\nTWO\nthree\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains("file1").IsSelected(), + ). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectedLines(Contains("-two")). + Press(keys.Main.EditSelectHunk) + + // The patch is written with a two-line header, so the deletion the cursor was + // on is its fifth line. + t.FileSystem().FileContent("../edit-line", Equals("5\n")) + + // What the editor wrote went into the index, leaving the working tree as it + // was: the file is changed on both sides now, differently. + t.Views().Files().Lines( + Contains("MM").Contains("file1"), + ) + t.Views().Secondary().ContainsLines( + Contains("-two"), + Contains("+TWO_EDITED"), + ) + t.Views().Main().ContainsLines( + Contains("-TWO_EDITED"), + Contains("+TWO"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index eceaa02e6..72006a733 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -375,6 +375,7 @@ var tests = []*components.IntegrationTest{ main_view.DiscardDiffLines, main_view.DragRangeWithAutoscroll, main_view.DragSelectsDiffLineRange, + main_view.EditHunkInFocusedDiff, main_view.EditSelectedDiffLine, main_view.EscapeDismissesSelection, main_view.FileNavigationScrollsToTheTop,