From fce9b7e63876b2b9d117f7770b224bb9c7f23310 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 16 Aug 2026 07:30:43 +0200 Subject: [PATCH] Discard the selected diff lines from the focused main view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remove key now works on a diff selection the way it does in the staging view: on the unstaged side it throws the selected lines away, which it asks about first, and on the staged side it takes them out of the index, which is unstaging and needs no warning. It goes through the same path as staging, so that everything around the action behaves identically — the selection lands on the change that took the place of the discarded one, and the focus follows the side it acted on. Co-authored-by: Claude Opus 5 (1M context) --- docs-master/keybindings/Keybindings_en.md | 2 + docs-master/keybindings/Keybindings_ja.md | 2 + docs-master/keybindings/Keybindings_ko.md | 2 + docs-master/keybindings/Keybindings_nl.md | 2 + docs-master/keybindings/Keybindings_pl.md | 2 + docs-master/keybindings/Keybindings_pt.md | 2 + docs-master/keybindings/Keybindings_ru.md | 2 + docs-master/keybindings/Keybindings_zh-CN.md | 2 + docs-master/keybindings/Keybindings_zh-TW.md | 2 + pkg/gui/controllers/main_view_controller.go | 24 +++++++ .../controllers/working_tree_diff_actions.go | 29 ++++++++ pkg/gui/types/context.go | 4 ++ .../tests/main_view/discard_diff_lines.go | 70 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 14 files changed, 146 insertions(+) create mode 100644 pkg/integration/tests/main_view/discard_diff_lines.go diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index 749cdd9e8..63bb3a24c 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -227,6 +227,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Toggle range select | | | `` 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. | | `` `` | Copy selected text to clipboard | | | `` , h `` | Go to previous hunk | | | `` , l `` | Go to next hunk | | @@ -341,6 +342,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Toggle range select | | | `` 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. | | `` `` | 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 331becbd0..786750b16 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -196,6 +196,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | 範囲選択を切り替え | | | `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 | | `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | +| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 | | `` `` | 選択したテキストをクリップボードにコピー | | | `` , h `` | 前のハンクに移動 | | | `` , l `` | 次のハンクに移動 | | @@ -318,6 +319,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | 範囲選択を切り替え | | | `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 | | `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | +| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 | | `` `` | 選択したテキストをクリップボードにコピー | | | `` , h `` | 前のハンクに移動 | | | `` , l `` | 次のハンクに移動 | | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index adf015638..3adec5eb8 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -88,6 +88,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | 드래그 선택 전환 | | | `` 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. | | `` `` | 선택한 텍스트를 클립보드에 복사 | | | `` , h `` | 이전 hunk를 선택 | | | `` , l `` | 다음 hunk를 선택 | | @@ -175,6 +176,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | 드래그 선택 전환 | | | `` 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. | | `` `` | 선택한 텍스트를 클립보드에 복사 | | | `` , h `` | 이전 hunk를 선택 | | | `` , l `` | 다음 hunk를 선택 | | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index e2db96275..065a98df2 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -235,6 +235,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Toggle drag selecteer | | | `` 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. | | `` `` | Copy selected text to clipboard | | | `` , h `` | Selecteer de vorige hunk | | | `` , l `` | Selecteer de volgende hunk | | @@ -319,6 +320,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Toggle drag selecteer | | | `` 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. | | `` `` | 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 0b1f4c763..58d7eeb80 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -103,6 +103,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Przełącz zaznaczenie zakresu | | | `` 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. | | `` `` | Kopiuj zaznaczony tekst do schowka | | | `` , h `` | Idź do poprzedniego fragmentu | | | `` , l `` | Idź do następnego fragmentu | | @@ -214,6 +215,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Przełącz zaznaczenie zakresu | | | `` 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. | | `` `` | 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 8f8d2814b..ff80a02db 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -239,6 +239,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Toggle range select | | | `` 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. | | `` `` | Copiar texto selecionado para área de transferência | | | `` , h `` | Ir para o local anterior | | | `` , l `` | Ir para o próximo trecho | | @@ -350,6 +351,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Toggle range select | | | `` 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. | | `` `` | 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 399e86305..7545311a0 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -78,6 +78,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Переключить выборку перетаскивания | | | `` 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. | | `` `` | Скопировать выделенный текст в буфер обмена | | | `` , h `` | Выбрать предыдущую часть | | | `` , l `` | Выбрать следующую часть | | @@ -119,6 +120,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | Переключить выборку перетаскивания | | | `` 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. | | `` `` | Скопировать выделенный текст в буфер обмена | | | `` , h `` | Выбрать предыдущую часть | | | `` , l `` | Выбрать следующую часть | | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index 9e12bc5d2..a18cd0177 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -290,6 +290,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | 切换拖动选择 | | | `` e `` | 编辑文件 | 使用外部编辑器打开文件 | | `` `` | 切换暂存状态 | 切换行暂存状态 | +| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 | | `` `` | 复制选中文本到剪贴板 | | | `` , h `` | 选择上一个区块 | | | `` , l `` | 选择下一个区块 | | @@ -347,6 +348,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | 切换拖动选择 | | | `` e `` | 编辑文件 | 使用外部编辑器打开文件 | | `` `` | 切换暂存状态 | 切换行暂存状态 | +| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 | | `` `` | 复制选中文本到剪贴板 | | | `` , h `` | 选择上一个区块 | | | `` , l `` | 选择下一个区块 | | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index 2adf9ac93..7edb92940 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -79,6 +79,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | 切換拖曳選擇 | | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | +| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 | | `` `` | 複製所選文本至剪貼簿 | | | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | @@ -369,6 +370,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` v `` | 切換拖曳選擇 | | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | +| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 | | `` `` | 複製所選文本至剪貼簿 | | | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 650529f90..2e8874c6b 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -88,6 +88,15 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty Tooltip: self.c.Tr.StageSelectionTooltip, DisplayOnScreen: true, }, + { + Keys: opts.GetKeys(opts.Config.Universal.Remove), + Handler: self.discardSelection, + Description: self.c.Tr.DiscardSelection, + DescriptionFunc: self.workingTreeActionDescription(self.c.Tr.DiscardSelection), + GetDisabledReason: self.diffSelectionDisabledReason, + Tooltip: self.c.Tr.DiscardSelectionTooltip, + DisplayOnScreen: true, + }, { Keys: opts.GetKeys(opts.Config.Universal.CopyToClipboard), Handler: self.copySelection, @@ -341,6 +350,21 @@ func revealSelectionAfterAction( }) } +// discardSelection takes the selected diff lines back out of what they are part of, +// which — like the primary action — is the panel's business, and so is the re-render +// that follows. +func (self *MainViewController) discardSelection() error { + actions := self.focusedMainViewActions() + if actions == nil { + return nil + } + first, last, ok := self.context.GetView().SelectedBufferLineRange() + if !ok { + return nil + } + return actions.DiscardSelection(self.context, first, last) +} + // workingTreeActionDescription gives a command's description only where the command // applies — over the working tree's diff — so that it is listed there and nowhere else. func (self *MainViewController) workingTreeActionDescription(description string) func() string { diff --git a/pkg/gui/controllers/working_tree_diff_actions.go b/pkg/gui/controllers/working_tree_diff_actions.go index c43e85b0a..00f3fab1c 100644 --- a/pkg/gui/controllers/working_tree_diff_actions.go +++ b/pkg/gui/controllers/working_tree_diff_actions.go @@ -70,6 +70,35 @@ func (self *WorkingTreeDiffActions) PrimaryAction(pane types.DiffPaneContext, fi git_commands.ApplyPatchOpts{Reverse: onStagedSide, Cached: true}) } +// DiscardSelection takes the selected diff lines out of the working tree — or, on the +// staged side, out of the index, which is where "discard this" means "I don't want it +// staged". +func (self *WorkingTreeDiffActions) DiscardSelection(pane types.DiffPaneContext, firstBufferLine int, lastBufferLine int) error { + if self.c.UserConfig().Git.DiffContextSize == 0 { + return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToDiscard, + self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) + } + + infos, onStagedSide, ok := self.diffLineSelection(pane, firstBufferLine, lastBufferLine) + if !ok { + return nil + } + + // Either way the change is applied backwards; which side it is applied to decides + // how destructive that is. On the staged side it goes to the index; this is the + // same as unstaging, so nothing is lost. On the unstaged side it goes to the + // working tree, where the change is gone for good, so we ask first. + return self.c.ConfirmIf(!onStagedSide && !self.c.UserConfig().Gui.SkipDiscardChangeWarning, + types.ConfirmOpts{ + Title: self.c.Tr.DiscardChangeTitle, + Prompt: self.c.Tr.DiscardChangePrompt, + HandleConfirm: func() error { + return self.applyDiffLineSelection(pane, firstBufferLine, infos, onStagedSide, + git_commands.ApplyPatchOpts{Reverse: true, Cached: onStagedSide}) + }, + }) +} + // 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/gui/types/context.go b/pkg/gui/types/context.go index f468687d9..4950dfc9d 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -255,6 +255,10 @@ type FocusedMainViewActions interface { // single line, a range, or a hunk. The panel re-renders the diff itself, being the // one that knows what it did to it. PrimaryAction(pane DiffPaneContext, firstBufferLine int, lastBufferLine int) error + + // DiscardSelection takes the selected diff lines back out of whatever they are part + // of: the working tree for the files panel. + DiscardSelection(pane DiffPaneContext, firstBufferLine int, lastBufferLine int) error } type IListContext interface { diff --git a/pkg/integration/tests/main_view/discard_diff_lines.go b/pkg/integration/tests/main_view/discard_diff_lines.go new file mode 100644 index 000000000..699b4d506 --- /dev/null +++ b/pkg/integration/tests/main_view/discard_diff_lines.go @@ -0,0 +1,70 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DiscardDiffLines = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Discard a hunk of the working tree's diff from the focused main view, and unstage one from the staged half", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n") + shell.Commit("one") + + shell.UpdateFileAndAdd("file1", "one\nSTAGED\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n") + shell.UpdateFile("file1", "one\nSTAGED\ntwo\nthree\nfour\nfive\nsix\nseven\nUNSTAGED\neight\nnine\nten\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains("MM file1").IsSelected(), + ). + Press(keys.Universal.FocusMainView) + + // Discarding from the unstaged side throws the change away, so it asks first. + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+UNSTAGED"), + ). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Discard change")). + Content(Contains("Are you sure you want to discard this change")). + Confirm() + }) + + // Nothing is unstaged any more, so that pane is gone and the focus has followed + // the file's remaining changes into the staged one. + t.Views().Files().Lines( + Contains("M file1"), + ) + t.Views().Main().IsInvisible() + + // There the same key means "I don't want this staged", which is unstaging, so it + // doesn't ask. + t.Views().Secondary(). + IsFocused(). + Title(Equals("Staged changes")). + SelectedLines( + Contains("+STAGED"), + ). + Press(keys.Universal.Remove) + + t.Views().Files().Lines( + Contains(" M file1"), + ) + t.Views().Secondary().IsInvisible() + t.Views().Main(). + IsFocused(). + Title(Equals("Unstaged changes")). + Content(Contains("+STAGED")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index a02927d04..50efa3102 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -370,6 +370,7 @@ var tests = []*components.IntegrationTest{ main_view.ClickSelectsDiffLine, main_view.CopyRowsThatAreNoDiffLine, main_view.CopySelectedDiffLines, + main_view.DiscardDiffLines, main_view.DragRangeWithAutoscroll, main_view.DragSelectsDiffLineRange, main_view.EditSelectedDiffLine,