diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index 76b02512c85..0b7528f282b 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -116,6 +116,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | View files | | | `` w `` | View worktree options | | @@ -285,6 +286,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` `` | Reset copied (cherry-picked) commits selection | | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | View commits | | | `` w `` | View worktree options | | @@ -369,6 +371,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` `` | Reset copied (cherry-picked) commits selection | | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | View files | | | `` w `` | View worktree options | | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index 39758d3ed6b..2e244c1cfe2 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -96,6 +96,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味 | `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `` を押して選択をキャンセルできます。 | | `` `` | 外部差分ツールを開く(git difftool) | | | `` * `` | 現在のブランチのコミットを選択 | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | メインビューにフォーカス | | | `` `` | ファイルを表示 | | | `` w `` | ワークツリーオプションを表示 | | @@ -143,6 +144,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味 | `` `` | コピーされた(チェリーピックされた)コミットの選択をリセット | | | `` `` | 外部差分ツールを開く(git difftool) | | | `` * `` | 現在のブランチのコミットを選択 | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | メインビューにフォーカス | | | `` `` | ファイルを表示 | | | `` w `` | ワークツリーオプションを表示 | | @@ -330,6 +332,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味 | `` `` | コピーされた(チェリーピックされた)コミットの選択をリセット | | | `` `` | 外部差分ツールを開く(git difftool) | | | `` * `` | 現在のブランチのコミットを選択 | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | メインビューにフォーカス | | | `` `` | コミットを表示 | | | `` w `` | ワークツリーオプションを表示 | | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index 0860c85719c..021d2519829 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -75,6 +75,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` `` | Reset cherry-picked (copied) commits selection | | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | 커밋 보기 | | | `` w `` | View worktree options | | @@ -117,6 +118,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` `` | Reset cherry-picked (copied) commits selection | | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | View selected item's files | | | `` w `` | View worktree options | | @@ -325,6 +327,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | View selected item's files | | | `` w `` | View worktree options | | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index 0bfffcb3dea..b29adad88c7 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -187,6 +187,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Bekijk gecommite bestanden | | | `` w `` | View worktree options | | @@ -263,6 +264,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` `` | Reset cherry-picked (gekopieerde) commits selectie | | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Bekijk commits | | | `` w `` | View worktree options | | @@ -369,6 +371,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` `` | Reset cherry-picked (gekopieerde) commits selectie | | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Bekijk gecommite bestanden | | | `` w `` | View worktree options | | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index 656f16a76ed..272d61d31e4 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -89,6 +89,7 @@ _Legenda: `` oznacza ctrl+b, `` oznacza alt+b, `B` oznacza shift+b_ | `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć ``, aby anulować zaznaczenie. | | `` `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Wyświetl pliki | | | `` w `` | Zobacz opcje drzewa pracy | | @@ -303,6 +304,7 @@ _Legenda: `` oznacza ctrl+b, `` oznacza alt+b, `B` oznacza shift+b_ | `` `` | Resetuj wybrane (cherry-picked) commity | | | `` `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Pokaż commity | | | `` w `` | Zobacz opcje drzewa pracy | | @@ -348,6 +350,7 @@ _Legenda: `` oznacza ctrl+b, `` oznacza alt+b, `B` oznacza shift+b_ | `` `` | Resetuj wybrane (cherry-picked) commity | | | `` `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Wyświetl pliki | | | `` w `` | Zobacz opcje drzewa pracy | | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index c9e5d3bf47b..d86de6c4c4a 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -191,6 +191,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `` para cancelar a seleção. | | `` `` | Abrir ferramenta de diff externa (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Ver arquivos | | | `` w `` | View worktree options | | @@ -313,6 +314,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` `` | Reset copied (cherry-picked) commits selection | | | `` `` | Abrir ferramenta de diff externa (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | View commits | | | `` w `` | View worktree options | | @@ -378,6 +380,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` `` | Reset copied (cherry-picked) commits selection | | | `` `` | Abrir ferramenta de diff externa (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Ver arquivos | | | `` w `` | View worktree options | | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index df0a4ce1f1a..23f7b256637 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -156,6 +156,7 @@ _Связки клавиш_ | `` `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть коммиты | | | `` w `` | View worktree options | | @@ -197,6 +198,7 @@ _Связки клавиш_ | `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть файлы выбранного элемента | | | `` w `` | View worktree options | | @@ -263,6 +265,7 @@ _Связки клавиш_ | `` `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | | | `` `` | Open external diff tool (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | Просмотреть файлы выбранного элемента | | | `` w `` | View worktree options | | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index 37480d3c318..c54e474c2e2 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -68,6 +68,7 @@ _图例:`` 意味着ctrl+b, `意味着Alt+b, `B` 意味着shift+b_ | `` `` | 重置已拣选(复制)的提交 | | | `` `` | 使用外部差异比较工具(git difftool) | | | `` * `` | 选择当前分支的提交 | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交的文件 | | | `` w `` | 查看工作区选项 | | @@ -112,6 +113,7 @@ _图例:`` 意味着ctrl+b, `意味着Alt+b, `B` 意味着shift+b_ | `` `` | 重置已拣选(复制)的提交 | | | `` `` | 使用外部差异比较工具(git difftool) | | | `` * `` | 选择当前分支的提交 | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交 | | | `` w `` | 查看工作区选项 | | @@ -153,6 +155,7 @@ _图例:`` 意味着ctrl+b, `意味着Alt+b, `B` 意味着shift+b_ | `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `` 来取消选择。 | | `` `` | 使用外部差异比较工具(git difftool) | | | `` * `` | 选择当前分支的提交 | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | 聚焦主视图 | | | `` `` | 查看提交的文件 | | | `` w `` | 查看工作区选项 | | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index 107976c26f0..096530cf82f 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -146,6 +146,7 @@ _說明:`` 表示 Ctrl+B、`` 表示 Alt+B,`B`表示 Shift+B | `` `` | 重設選定的揀選 (複製) 提交 | | | `` `` | 開啟外部差異工具 (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | 檢視所選項目的檔案 | | | `` w `` | 檢視工作目錄選項 | | @@ -211,6 +212,7 @@ _說明:`` 表示 Ctrl+B、`` 表示 Alt+B,`B`表示 Shift+B | `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `` to cancel the selection. | | `` `` | 開啟外部差異工具 (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | 檢視所選項目的檔案 | | | `` w `` | 檢視工作目錄選項 | | @@ -272,6 +274,7 @@ _說明:`` 表示 Ctrl+B、`` 表示 Alt+B,`B`表示 Shift+B | `` `` | 重設選定的揀選 (複製) 提交 | | | `` `` | 開啟外部差異工具 (git difftool) | | | `` * `` | Select commits of current branch | | +| `` z `` | Toggle mark for selection | | | `` 0 `` | Focus main view | | | `` `` | 檢視提交 | | | `` w `` | 檢視工作目錄選項 | | diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index 18e37463ea5..28c6fd9765d 100644 --- a/docs/Custom_Command_Keybindings.md +++ b/docs/Custom_Command_Keybindings.md @@ -326,6 +326,7 @@ Your commands can contain placeholder strings using Go's [template syntax](https ``` SelectedCommit SelectedCommitRange +SelectedCommits SelectedFile SelectedPath SelectedSubmodule @@ -344,11 +345,31 @@ CheckedOutBranch To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/services/custom_commands/models.go) (all the modelling lives in the same file). -We don't support accessing all elements of a range selection yet. We might add this in the future, but as a special case you can access the range of selected commits by using `SelectedCommitRange`, which has two properties `.To` and `.From` which are the hashes of the bottom and top selected commits, respectively. This is useful for passing them to a git command that operates on a range of commits. For example, to create patches for all selected commits, you might use +### SelectedCommitRange + +You can access the range of selected commits by using `SelectedCommitRange`, which has two properties `.To` and `.From` which are the hashes of the bottom and top selected commits, respectively. This is useful for passing them to a git command that operates on a range of commits. For example, to create patches for all selected commits, you might use ```yml command: "git format-patch {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}}" ``` +### SelectedCommits + +You can select individual commits non-contiguously by: +- Pressing `z` to toggle mark on the currently selected commit +- Using `Option+Click` (Alt+Click) on commits to mark/unmark them + +The selected commits are available in `SelectedCommits` as an array. You can iterate over them using Go's template `range`: + +```yml +customCommands: + - key: 'X' + context: 'commits' + command: "git show {{ range .SelectedCommits }}{{ .Hash }} {{ end }}" + description: 'Show all selected commits' +``` + +Each commit in the array has the same fields as `SelectedCommit` (Hash, Name, Status, etc.). + We support the following functions: ### Quoting diff --git a/pkg/gui/context/list_context_trait.go b/pkg/gui/context/list_context_trait.go index 98833fdb2f7..763f7540c2d 100644 --- a/pkg/gui/context/list_context_trait.go +++ b/pkg/gui/context/list_context_trait.go @@ -78,6 +78,22 @@ func (self *ListContextTrait) refreshViewport() { startIdx, length := self.GetViewTrait().ViewPortYBounds() content := self.renderLines(startIdx, startIdx+length) self.GetViewTrait().SetViewPortContent(content) + self.applyMarkedHighlights() +} + +// applyMarkedHighlights applies highlighting to all marked lines in the view +func (self *ListContextTrait) applyMarkedHighlights() { + markedIndices := self.list.GetMarkedIndices() + startIdx, length := self.GetViewTrait().ViewPortYBounds() + endIdx := startIdx + length + + for _, modelIdx := range markedIndices { + viewIdx := self.ModelIndexToViewIndex(modelIdx) + // Only highlight if the line is visible in the viewport + if viewIdx >= startIdx && viewIdx < endIdx { + self.GetViewTrait().SetLineHighlight(viewIdx, true) + } + } } func (self *ListContextTrait) setFooter() { @@ -124,6 +140,7 @@ func (self *ListContextTrait) HandleRender() { content := self.renderLines(-1, -1) self.GetViewTrait().SetContent(content) } + self.applyMarkedHighlights() self.c.Render() self.setFooter() } diff --git a/pkg/gui/context/traits/list_cursor.go b/pkg/gui/context/traits/list_cursor.go index 3b3e5863983..bc416319864 100644 --- a/pkg/gui/context/traits/list_cursor.go +++ b/pkg/gui/context/traits/list_cursor.go @@ -1,6 +1,9 @@ package traits import ( + "slices" + + "github.com/jesseduffield/generics/set" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" @@ -24,6 +27,8 @@ type ListCursor struct { rangeSelectMode RangeSelectMode // value is ignored when rangeSelectMode is RangeSelectModeNone rangeStartIdx int + // markedIndices stores individually marked/selected indices for non-contiguous selection + markedIndices *set.Set[int] // Get the length of the list. We use this to clamp the selection so that // the selected index is always valid getLength func() int @@ -34,6 +39,7 @@ func NewListCursor(getLength func() int) *ListCursor { selectedIdx: 0, rangeStartIdx: 0, rangeSelectMode: RangeSelectModeNone, + markedIndices: set.New[int](), getLength: getLength, } } @@ -184,3 +190,42 @@ func (self *ListCursor) ExpandNonStickyRange(change int) { self.SetSelectedLineIdx(self.selectedIdx + change) } + +// ToggleMark toggles the marked state of the given index +func (self *ListCursor) ToggleMark(idx int) { + if idx < 0 || idx >= self.getLength() { + return + } + if self.markedIndices.Includes(idx) { + self.markedIndices.Remove(idx) + } else { + self.markedIndices.Add(idx) + } +} + +// IsMarked returns true if the given index is marked +func (self *ListCursor) IsMarked(idx int) bool { + return self.markedIndices.Includes(idx) +} + +// GetMarkedIndices returns all marked indices in ascending order +func (self *ListCursor) GetMarkedIndices() []int { + indices := self.markedIndices.ToSlice() + // Filter out any indices that are now out of bounds + length := self.getLength() + indices = lo.Filter(indices, func(idx int, _ int) bool { + return idx >= 0 && idx < length + }) + slices.Sort(indices) + return indices +} + +// ClearMarks removes all marks +func (self *ListCursor) ClearMarks() { + self.markedIndices = set.New[int]() +} + +// HasMarks returns true if there are any marked indices +func (self *ListCursor) HasMarks() bool { + return self.markedIndices.Len() > 0 +} diff --git a/pkg/gui/context/view_trait.go b/pkg/gui/context/view_trait.go index d3825b9cf3d..85f39714f9f 100644 --- a/pkg/gui/context/view_trait.go +++ b/pkg/gui/context/view_trait.go @@ -98,3 +98,9 @@ func (self *ViewTrait) PageDelta() int { func (self *ViewTrait) SelectedLineIdx() int { return self.view.SelectedLineIdx() } + +// SetLineHighlight sets or clears the highlight on a specific line. +// This is used for non-contiguous selection (marking individual items). +func (self *ViewTrait) SetLineHighlight(y int, on bool) { + self.view.SetHighlight(y, on) +} diff --git a/pkg/gui/controllers/basic_commits_controller.go b/pkg/gui/controllers/basic_commits_controller.go index be856a9e946..1ffa590186a 100644 --- a/pkg/gui/controllers/basic_commits_controller.go +++ b/pkg/gui/controllers/basic_commits_controller.go @@ -129,6 +129,12 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ GetDisabledReason: self.require(self.canSelectCommitsOfCurrentBranch), Description: self.c.Tr.SelectCommitsOfCurrentBranch, }, + { + Key: 'z', + Handler: self.toggleMark, + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ToggleMarkCommit, + }, } return bindings @@ -359,6 +365,13 @@ func (self *BasicCommitsController) checkout(commit *models.Commit) error { return self.c.Helpers().Refs.CreateCheckoutMenu(commit) } +func (self *BasicCommitsController) toggleMark() error { + selectedIdx := self.context.GetSelectedLineIdx() + self.context.GetList().ToggleMark(selectedIdx) + self.context.HandleRender() + return nil +} + func (self *BasicCommitsController) copyRange(*models.Commit) error { return self.c.Helpers().CherryPick.CopyRange(self.context.GetCommits(), self.context) } diff --git a/pkg/gui/controllers/list_controller.go b/pkg/gui/controllers/list_controller.go index 6da196a6009..5c51322b0de 100644 --- a/pkg/gui/controllers/list_controller.go +++ b/pkg/gui/controllers/list_controller.go @@ -241,6 +241,9 @@ func (self *ListController) HandleClick(opts gocui.ViewMouseBindingOpts) error { return nil } + // Clear any marked items when doing a normal click + self.context.GetList().ClearMarks() + self.context.GetList().SetSelection(newSelectedLineIdx) if opts.IsDoubleClick && alreadyFocused && self.context.GetOnClick() != nil { @@ -250,6 +253,37 @@ func (self *ListController) HandleClick(opts gocui.ViewMouseBindingOpts) error { return nil } +// HandleMarkClick handles Alt+Click or Ctrl+Click to toggle marking of individual items +func (self *ListController) HandleMarkClick(opts gocui.ViewMouseBindingOpts) error { + clickedLineIdx := self.context.ViewIndexToModelIndex(opts.Y) + + if err := self.pushContextIfNotFocused(); err != nil { + return err + } + + if clickedLineIdx > self.context.GetList().Len()-1 { + return nil + } + + list := self.context.GetList() + + // If this is the first mark (no marks yet), also mark the currently selected item + // This allows starting a multi-selection from the current selection + if !list.HasMarks() { + currentIdx := list.GetSelectedLineIdx() + if currentIdx != clickedLineIdx && currentIdx >= 0 && currentIdx < list.Len() { + list.ToggleMark(currentIdx) + } + } + + // Toggle the mark on the clicked item + list.ToggleMark(clickedLineIdx) + + // Trigger a re-render to show the updated marks + self.context.HandleRender() + return nil +} + func (self *ListController) pushContextIfNotFocused() error { if !self.isFocused() { self.c.Context().Push(self.context, types.OnFocusOpts{}) @@ -303,6 +337,12 @@ func (self *ListController) GetMouseKeybindings(opts types.KeybindingsOpts) []*g Key: gocui.MouseLeft, Handler: func(opts gocui.ViewMouseBindingOpts) error { return self.HandleClick(opts) }, }, + { + ViewName: self.context.GetViewName(), + Key: gocui.MouseLeft, + Modifier: gocui.ModAlt, + Handler: func(opts gocui.ViewMouseBindingOpts) error { return self.HandleMarkClick(opts) }, + }, { ViewName: self.context.GetViewName(), Key: gocui.MouseWheelDown, diff --git a/pkg/gui/services/custom_commands/session_state_loader.go b/pkg/gui/services/custom_commands/session_state_loader.go index b2a2e39c9fb..6e7f5694abd 100644 --- a/pkg/gui/services/custom_commands/session_state_loader.go +++ b/pkg/gui/services/custom_commands/session_state_loader.go @@ -190,6 +190,21 @@ func makeCommitRange(commits []*models.Commit, _ int, _ int) *CommitRange { } } +// getMarkedCommits returns all marked commits from the given context +func getMarkedCommits(allCommits []*models.Commit, markedIndices []int) []*Commit { + if len(markedIndices) == 0 { + return nil + } + + result := make([]*Commit, 0, len(markedIndices)) + for _, idx := range markedIndices { + if idx >= 0 && idx < len(allCommits) { + result = append(result, commitShimFromModelCommit(allCommits[idx])) + } + } + return result +} + // SessionState captures the current state of the application for use in custom commands type SessionState struct { SelectedLocalCommit *Commit // deprecated, use SelectedCommit @@ -197,6 +212,7 @@ type SessionState struct { SelectedSubCommit *Commit // deprecated, use SelectedCommit SelectedCommit *Commit SelectedCommitRange *CommitRange + SelectedCommits []*Commit // All marked/selected commits (non-contiguous selection) SelectedFile *File SelectedSubmodule *Submodule SelectedPath string @@ -221,12 +237,27 @@ func (self *SessionStateLoader) call() *SessionState { selectedCommit := selectedLocalCommit selectedCommitRange := selectedLocalCommitRange + var selectedCommits []*Commit if self.c.Context().IsCurrentOrParent(self.c.Contexts().ReflogCommits) { selectedCommit = selectedReflogCommit selectedCommitRange = selectedReflogCommitRange + selectedCommits = getMarkedCommits( + self.c.Contexts().ReflogCommits.GetCommits(), + self.c.Contexts().ReflogCommits.GetMarkedIndices(), + ) } else if self.c.Context().IsCurrentOrParent(self.c.Contexts().SubCommits) { selectedCommit = selectedSubCommit selectedCommitRange = selectedSubCommitRange + selectedCommits = getMarkedCommits( + self.c.Contexts().SubCommits.GetCommits(), + self.c.Contexts().SubCommits.GetMarkedIndices(), + ) + } else { + // Default to LocalCommits + selectedCommits = getMarkedCommits( + self.c.Contexts().LocalCommits.GetCommits(), + self.c.Contexts().LocalCommits.GetMarkedIndices(), + ) } selectedPath := self.c.Contexts().Files.GetSelectedPath() @@ -245,6 +276,7 @@ func (self *SessionStateLoader) call() *SessionState { SelectedSubCommit: selectedSubCommit, SelectedCommit: selectedCommit, SelectedCommitRange: selectedCommitRange, + SelectedCommits: selectedCommits, SelectedLocalBranch: branchShimFromModelBranch(self.c.Contexts().Branches.GetSelected()), SelectedRemoteBranch: remoteBranchShimFromModelRemoteBranch(self.c.Contexts().RemoteBranches.GetSelected()), SelectedRemote: remoteShimFromModelRemote(self.c.Contexts().Remotes.GetSelected()), diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 790f4c84bd1..63dbc874546 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -218,6 +218,7 @@ type IViewTrait interface { PageDelta() int SelectedLineIdx() int SetHighlight(bool) + SetLineHighlight(y int, on bool) } type OnFocusOpts struct { @@ -278,6 +279,12 @@ type IListCursor interface { AreMultipleItemsSelected() bool ToggleStickyRange() ExpandNonStickyRange(int) + // Marking methods for non-contiguous selection + ToggleMark(idx int) + IsMarked(idx int) bool + GetMarkedIndices() []int + ClearMarks() + HasMarks() bool } type IListPanelState interface { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index ce87693a24d..40824f23e87 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -374,6 +374,7 @@ type TranslationSet struct { DroppingMergeRequiresSingleSelection string CherryPickCopy string CherryPickCopyTooltip string + ToggleMarkCommit string PasteCommits string SureCherryPick string CherryPick string @@ -1482,6 +1483,7 @@ func EnglishTranslationSet() *TranslationSet { DroppingMergeRequiresSingleSelection: "Dropping a merge commit requires a single selected item", CherryPickCopy: "Copy (cherry-pick)", CherryPickCopyTooltip: "Mark commit as copied. Then, within the local commits view, you can press `{{.paste}}` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `{{.escape}}` to cancel the selection.", + ToggleMarkCommit: "Toggle mark for selection", PasteCommits: "Paste (cherry-pick)", SureCherryPick: "Are you sure you want to cherry-pick the {{.numCommits}} copied commit(s) onto this branch?", CherryPick: "Cherry-pick", diff --git a/pkg/integration/tests/custom_commands/selected_commits.go b/pkg/integration/tests/custom_commands/selected_commits.go new file mode 100644 index 00000000000..3b8ea1b1507 --- /dev/null +++ b/pkg/integration/tests/custom_commands/selected_commits.go @@ -0,0 +1,47 @@ +package custom_commands + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var SelectedCommits = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Use the {{ .SelectedCommits }} template variable for non-contiguous selection", + ExtraCmdArgs: []string{}, + Skip: false, + SetupRepo: func(shell *Shell) { + shell.CreateNCommits(5) + }, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().CustomCommands = []config.CustomCommand{ + { + Key: "X", + Context: "commits", + Command: `echo "{{ range .SelectedCommits }}{{ .Name }} {{ end }}" > file.txt`, + }, + } + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits().Focus(). + Lines( + Contains("commit 05").IsSelected(), + Contains("commit 04"), + Contains("commit 03"), + Contains("commit 02"), + Contains("commit 01"), + ). + // Mark commit 05 (currently selected) + Press("z"). + // Move down to commit 03 and mark it + NavigateToLine(Contains("commit 03")). + Press("z"). + // Move down to commit 01 and mark it + NavigateToLine(Contains("commit 01")). + Press("z") + + // Run the custom command which should output all marked commits + t.GlobalPress("X") + // The commits should be in index order (05, 03, 01) + t.FileSystem().FileContent("file.txt", Equals("commit 05 commit 03 commit 01 \n")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 4d9edaa70ab..5192d97d2f3 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -183,6 +183,7 @@ var tests = []*components.IntegrationTest{ custom_commands.RunCommand, custom_commands.SelectedCommit, custom_commands.SelectedCommitRange, + custom_commands.SelectedCommits, custom_commands.SelectedPath, custom_commands.SelectedSubmodule, custom_commands.ShowOutputInPanel,