From 340a145bc8af32123550f6b4db5104f61417c019 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 20 Mar 2022 13:59:33 +1100 Subject: [PATCH] refactor cheatsheet generator --- docs/keybindings/Keybindings_en.md | 1 - docs/keybindings/Keybindings_nl.md | 81 ++++----- docs/keybindings/Keybindings_pl.md | 213 +++++++++++----------- docs/keybindings/Keybindings_zh.md | 245 +++++++++++++------------ pkg/cheatsheet/generate.go | 203 ++++++++------------- pkg/cheatsheet/generate_test.go | 281 +++++++++++++++++++++++++++++ pkg/utils/slice.go | 16 ++ 7 files changed, 640 insertions(+), 400 deletions(-) create mode 100644 pkg/cheatsheet/generate_test.go diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index 662631386..6f8c12966 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -269,7 +269,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct : select next hunk ctrl+o: copy the selected text to the clipboard e: edit file - o: open file v: toggle drag select V: toggle drag select a: toggle select hunk diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index dd9c45ad2..c0accbd44 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -41,6 +41,46 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct [: vorige tabblad +## Bestanden Paneel (Bestanden) + +
+  ctrl+o: kopieer de bestandsnaam naar het klembord
+  ctrl+w: Toggle whether or not whitespace changes are shown in the diff view
+  d: bekijk 'veranderingen ongedaan maken' opties
+  space: toggle staged
+  ctrl+b: Filter files (staged/unstaged)
+  c: commit veranderingen
+  w: commit veranderingen zonder pre-commit hook
+  A: wijzig laatste commit
+  C: commit veranderingen met de git editor
+  e: verander bestand
+  o: open bestand
+  i: voeg toe aan .gitignore
+  r: refresh bestanden
+  s: stash-bestanden
+  S: bekijk stash opties
+  a: toggle staged alle
+  enter: stage individuele hunks/lijnen
+  g: bekijk upstream reset opties
+  D: bekijk reset opties
+  `: toggle bestandsboom weergave
+  M: open external merge tool (git mergetool)
+  f: fetch
+
+ +## Bestanden Paneel (Submodules) + +
+  ctrl+o: kopieer submodule naam naar klembord
+  enter: enter submodule
+  d: remove submodule
+  u: update submodule
+  n: voeg nieuwe submodule toe
+  e: update submodule URL
+  i: initialiseer submodule
+  b: bekijk bulk submodule opties
+
+ ## Branches Paneel (Branches Tabblad)
@@ -178,46 +218,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   @: open command log menu
 
-## Bestanden Paneel (Bestanden) - -
-  ctrl+o: kopieer de bestandsnaam naar het klembord
-  ctrl+w: Toggle whether or not whitespace changes are shown in the diff view
-  d: bekijk 'veranderingen ongedaan maken' opties
-  space: toggle staged
-  ctrl+b: Filter files (staged/unstaged)
-  c: commit veranderingen
-  w: commit veranderingen zonder pre-commit hook
-  A: wijzig laatste commit
-  C: commit veranderingen met de git editor
-  e: verander bestand
-  o: open bestand
-  i: voeg toe aan .gitignore
-  r: refresh bestanden
-  s: stash-bestanden
-  S: bekijk stash opties
-  a: toggle staged alle
-  enter: stage individuele hunks/lijnen
-  g: bekijk upstream reset opties
-  D: bekijk reset opties
-  `: toggle bestandsboom weergave
-  M: open external merge tool (git mergetool)
-  f: fetch
-
- -## Bestanden Paneel (Submodules) - -
-  ctrl+o: kopieer submodule naam naar klembord
-  enter: enter submodule
-  d: remove submodule
-  u: update submodule
-  n: voeg nieuwe submodule toe
-  e: update submodule URL
-  i: initialiseer submodule
-  b: bekijk bulk submodule opties
-
- ## Hoofd Paneel (Mergen)
@@ -269,7 +269,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   : selecteer de volgende hunk
   ctrl+o: copy the selected text to the clipboard
   e: verander bestand
-  o: open bestand
   v: toggle drag selecteer
   V: toggle drag selecteer
   a: toggle selecteer hunk
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md
index 2d032e5e3..9c619f6b6 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -41,6 +41,56 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   [: previous tab
 
+## Commity Panel (Commity) + +
+  ctrl+o: copy commit SHA to clipboard
+  ctrl+r: reset cherry-picked (copied) commits selection
+  b: view bisect options
+  s: ściśnij
+  f: napraw commit
+  r: zmień nazwę commita
+  R: zmień nazwę commita w edytorze
+  d: usuń commit
+  e: edytuj commit
+  p: wybierz commit (podczas zmiany bazy)
+  F: utwórz commit naprawczy dla tego commita
+  S: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
+  ctrl+j: przenieś commit 1 w dół
+  ctrl+k: przenieś commit 1 w górę
+  A: popraw commit zmianami z poczekalni
+  t: odwróć commit
+  n: create new branch off of commit
+  c: kopiuj commit (przebieranie)
+  C: kopiuj zakres commitów (przebieranie)
+  v: wklej commity (przebieranie)
+  ctrl+l: open log menu
+  g: zresetuj do tego commita
+  space: checkout commit
+  T: tag commit
+  ctrl+y: copy commit message to clipboard
+  o: open commit in browser
+  enter: przeglądaj pliki commita
+
+ +## Commity Panel (Reflog Tab) + +
+  ctrl+o: copy commit SHA to clipboard
+  space: checkout commit
+  g: wyświetl opcje resetu
+  c: kopiuj commit (przebieranie)
+  C: kopiuj zakres commitów (przebieranie)
+  ctrl+r: reset cherry-picked (copied) commits selection
+  enter: przeglądaj pliki commita
+
+ +## Extras Panel + +
+  @: open command log menu
+
+ ## Gałęzie Panel (Branches Tab)
@@ -109,73 +159,69 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   enter: view commits
 
-## Pliki commita Panel +## Główne Panel (Patch Building)
-  ctrl+o: copy the committed file name to the clipboard
-
- -## Pliki commita Panel (Pliki commita) - -
-  c: plik wybierania
-  d: porzuć zmiany commita dla tego pliku
+  esc: wyście z trybu "linia po linii"
   o: otwórz plik
+  : poprzednia linia
+  : następna linia
+  : poprzedni kawałek
+  : następny kawałek
+  ctrl+o: copy the selected text to the clipboard
+  space: add/remove line(s) to patch
+  v: toggle drag select
+  V: toggle drag select
+  a: toggle select hunk
+
+ +## Główne Panel (Poczekalnia) + +
+  esc: wróć do panelu plików
+  space: toggle line staged / unstaged
+  d: delete change (git reset)
+  tab: switch to other panel
+  o: otwórz plik
+  : poprzednia linia
+  : następna linia
+  : poprzedni kawałek
+  : następny kawałek
+  ctrl+o: copy the selected text to the clipboard
   e: edytuj plik
-  space: toggle file included in patch
-  a: toggle all files included in patch
-  enter: enter file to add selected lines to the patch (or toggle directory collapsed)
-  `: toggle file tree view
+  v: toggle drag select
+  V: toggle drag select
+  a: toggle select hunk
+  c: Zatwierdź zmiany
+  w: zatwierdź zmiany bez skryptu pre-commit
+  C: Zatwierdź zmiany używając edytora
 
-## Commity Panel (Commity) +## Główne Panel (Scalanie)
-  ctrl+o: copy commit SHA to clipboard
-  ctrl+r: reset cherry-picked (copied) commits selection
-  b: view bisect options
-  s: ściśnij
-  f: napraw commit
-  r: zmień nazwę commita
-  R: zmień nazwę commita w edytorze
-  d: usuń commit
-  e: edytuj commit
-  p: wybierz commit (podczas zmiany bazy)
-  F: utwórz commit naprawczy dla tego commita
-  S: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
-  ctrl+j: przenieś commit 1 w dół
-  ctrl+k: przenieś commit 1 w górę
-  A: popraw commit zmianami z poczekalni
-  t: odwróć commit
-  n: create new branch off of commit
-  c: kopiuj commit (przebieranie)
-  C: kopiuj zakres commitów (przebieranie)
-  v: wklej commity (przebieranie)
-  ctrl+l: open log menu
-  g: zresetuj do tego commita
-  space: checkout commit
-  T: tag commit
-  ctrl+y: copy commit message to clipboard
-  o: open commit in browser
-  enter: przeglądaj pliki commita
+  esc: wróć do panelu plików
+  M: open external merge tool (git mergetool)
+  space: wybierz kawałek
+  b: wybierz wszystkie kawałki
+  : poprzedni konflikt
+  : następny konflikt
+  : wybierz poprzedni kawałek
+  : wybierz następny kawałek
+  z: cofnij
 
-## Commity Panel (Reflog Tab) +## Główne Panel (Zwykłe)
-  ctrl+o: copy commit SHA to clipboard
-  space: checkout commit
-  g: wyświetl opcje resetu
-  c: kopiuj commit (przebieranie)
-  C: kopiuj zakres commitów (przebieranie)
-  ctrl+r: reset cherry-picked (copied) commits selection
-  enter: przeglądaj pliki commita
+  mouse wheel down: przewiń w dół (fn+up)
+  mouse wheel up: przewiń w górę (fn+down)
 
-## Extras Panel +## Menu Panel
-  @: open command log menu
+  esc: close menu
 
## Pliki Panel (Pliki) @@ -218,70 +264,23 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct b: view bulk submodule options -## Główne Panel (Scalanie) +## Pliki commita Panel
-  esc: wróć do panelu plików
-  M: open external merge tool (git mergetool)
-  space: wybierz kawałek
-  b: wybierz wszystkie kawałki
-  : poprzedni konflikt
-  : następny konflikt
-  : wybierz poprzedni kawałek
-  : wybierz następny kawałek
-  z: cofnij
+  ctrl+o: copy the committed file name to the clipboard
 
-## Główne Panel (Zwykłe) +## Pliki commita Panel (Pliki commita)
-  mouse wheel down: przewiń w dół (fn+up)
-  mouse wheel up: przewiń w górę (fn+down)
-
- -## Główne Panel (Patch Building) - -
-  esc: wyście z trybu "linia po linii"
+  c: plik wybierania
+  d: porzuć zmiany commita dla tego pliku
   o: otwórz plik
-  : poprzednia linia
-  : następna linia
-  : poprzedni kawałek
-  : następny kawałek
-  ctrl+o: copy the selected text to the clipboard
-  space: add/remove line(s) to patch
-  v: toggle drag select
-  V: toggle drag select
-  a: toggle select hunk
-
- -## Główne Panel (Poczekalnia) - -
-  esc: wróć do panelu plików
-  space: toggle line staged / unstaged
-  d: delete change (git reset)
-  tab: switch to other panel
-  o: otwórz plik
-  : poprzednia linia
-  : następna linia
-  : poprzedni kawałek
-  : następny kawałek
-  ctrl+o: copy the selected text to the clipboard
   e: edytuj plik
-  o: otwórz plik
-  v: toggle drag select
-  V: toggle drag select
-  a: toggle select hunk
-  c: Zatwierdź zmiany
-  w: zatwierdź zmiany bez skryptu pre-commit
-  C: Zatwierdź zmiany używając edytora
-
- -## Menu Panel - -
-  esc: close menu
+  space: toggle file included in patch
+  a: toggle all files included in patch
+  enter: enter file to add selected lines to the patch (or toggle directory collapsed)
+  `: toggle file tree view
 
## Schowek Panel (Schowek) diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md index 12b9a90f7..d477edb40 100644 --- a/docs/keybindings/Keybindings_zh.md +++ b/docs/keybindings/Keybindings_zh.md @@ -41,6 +41,71 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct [: 上一个标签 +## Extras 面板 + +
+  @: 打开命令日志菜单
+
+ +## 主要 面板 (合并中) + +
+  esc: 返回文件面板
+  M: 打开合并工具
+  space: 选中区块
+  b: 选中所有区块
+  : 选择上一个冲突
+  : 选择下一个冲突
+  : 选择顶部块
+  : 选择底部块
+  z: 撤销
+
+ +## 主要 面板 (构建补丁中) + +
+  esc: 退出逐行模式
+  o: 打开文件
+  : 选择上一行
+  : 选择下一行
+  : 选择上一个区块
+  : 选择下一个区块
+  ctrl+o: copy the selected text to the clipboard
+  space: 添加/移除 行到补丁
+  v: 切换拖动选择
+  V: 切换拖动选择
+  a: 切换选择区块
+
+ +## 主要 面板 (正在暂存) + +
+  esc: 返回文件面板
+  space: 切换行暂存状态
+  d: 取消变更 (git reset)
+  tab: 切换到其他面板
+  o: 打开文件
+  : 选择上一行
+  : 选择下一行
+  : 选择上一个区块
+  : 选择下一个区块
+  ctrl+o: copy the selected text to the clipboard
+  e: 编辑文件
+  v: 切换拖动选择
+  V: 切换拖动选择
+  a: 切换选择区块
+  c: 提交更改
+  w: 提交更改而无需预先提交钩子
+  C: 提交更改(使用编辑器编辑提交信息)
+
+ +## 主要 面板 (正常) + +
+  mouse wheel down: 向下滚动 (fn+up)
+  mouse wheel up: 向上滚动 (fn+down)
+
+ ## 分支 面板 (分支标签)
@@ -62,29 +127,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   enter: 查看提交
 
-## 分支 面板 (远程分支(在远程页面中)) - -
-  space: 检出
-  n: 新分支
-  M: 合并到当前检出的分支
-  r: 将已检出的分支变基到该分支
-  d: 删除分支
-  u: 设置为检出分支的上游
-  esc: 返回远程仓库列表
-  g: 查看重置选项
-  enter: 查看提交
-
- -## 分支 面板 (远程页面) - -
-  f: 抓取远程仓库
-  n: 添加新的远程仓库
-  d: 删除远程
-  e: 编辑远程仓库
-
- ## 分支 面板 (子提交)
@@ -109,23 +151,39 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   enter: 查看提交
 
-## 提交文件 面板 +## 分支 面板 (远程分支(在远程页面中))
-  ctrl+o: 将提交的文件名复制到剪贴板
+  space: 检出
+  n: 新分支
+  M: 合并到当前检出的分支
+  r: 将已检出的分支变基到该分支
+  d: 删除分支
+  u: 设置为检出分支的上游
+  esc: 返回远程仓库列表
+  g: 查看重置选项
+  enter: 查看提交
 
-## 提交文件 面板 (提交文件) +## 分支 面板 (远程页面)
-  c: 检出文件
-  d: 放弃对此文件的提交更改
-  o: 打开文件
-  e: 编辑文件
-  space: 补丁中包含的切换文件
-  a: toggle all files included in patch
-  enter: 输入文件以将所选行添加到补丁中(或切换目录折叠)
-  `: 切换文件树视图
+  f: 抓取远程仓库
+  n: 添加新的远程仓库
+  d: 删除远程
+  e: 编辑远程仓库
+
+ +## 提交 面板 (Reflog) + +
+  ctrl+o: 将提交的 SHA 复制到剪贴板
+  space: 检出提交
+  g: 查看重置选项
+  c: 复制提交(拣选)
+  C: 复制提交范围(拣选)
+  ctrl+r: 重置已拣选(复制)的提交
+  enter: 查看提交的文件
 
## 提交 面板 (提交) @@ -160,22 +218,36 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct enter: 查看提交的文件 -## 提交 面板 (Reflog) +## 提交文件 面板
-  ctrl+o: 将提交的 SHA 复制到剪贴板
-  space: 检出提交
-  g: 查看重置选项
-  c: 复制提交(拣选)
-  C: 复制提交范围(拣选)
-  ctrl+r: 重置已拣选(复制)的提交
-  enter: 查看提交的文件
+  ctrl+o: 将提交的文件名复制到剪贴板
 
-## Extras 面板 +## 提交文件 面板 (提交文件)
-  @: 打开命令日志菜单
+  c: 检出文件
+  d: 放弃对此文件的提交更改
+  o: 打开文件
+  e: 编辑文件
+  space: 补丁中包含的切换文件
+  a: toggle all files included in patch
+  enter: 输入文件以将所选行添加到补丁中(或切换目录折叠)
+  `: 切换文件树视图
+
+ +## 文件 面板 (子模块) + +
+  ctrl+o: 将子模块名称复制到剪贴板
+  enter: 输入子模块
+  d: 删除子模块
+  u: 更新子模块
+  n: 添加新的子模块
+  e: 更新子模块 URL
+  i: 初始化子模块
+  b: 查看批量子模块选项
 
## 文件 面板 (文件) @@ -205,77 +277,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct f: 抓取 -## 文件 面板 (子模块) +## 状态 面板 (状态)
-  ctrl+o: 将子模块名称复制到剪贴板
-  enter: 输入子模块
-  d: 删除子模块
-  u: 更新子模块
-  n: 添加新的子模块
-  e: 更新子模块 URL
-  i: 初始化子模块
-  b: 查看批量子模块选项
-
- -## 主要 面板 (合并中) - -
-  esc: 返回文件面板
-  M: 打开合并工具
-  space: 选中区块
-  b: 选中所有区块
-  : 选择上一个冲突
-  : 选择下一个冲突
-  : 选择顶部块
-  : 选择底部块
-  z: 撤销
-
- -## 主要 面板 (正常) - -
-  mouse wheel down: 向下滚动 (fn+up)
-  mouse wheel up: 向上滚动 (fn+down)
-
- -## 主要 面板 (构建补丁中) - -
-  esc: 退出逐行模式
-  o: 打开文件
-  : 选择上一行
-  : 选择下一行
-  : 选择上一个区块
-  : 选择下一个区块
-  ctrl+o: copy the selected text to the clipboard
-  space: 添加/移除 行到补丁
-  v: 切换拖动选择
-  V: 切换拖动选择
-  a: 切换选择区块
-
- -## 主要 面板 (正在暂存) - -
-  esc: 返回文件面板
-  space: 切换行暂存状态
-  d: 取消变更 (git reset)
-  tab: 切换到其他面板
-  o: 打开文件
-  : 选择上一行
-  : 选择下一行
-  : 选择上一个区块
-  : 选择下一个区块
-  ctrl+o: copy the selected text to the clipboard
-  e: 编辑文件
-  o: 打开文件
-  v: 切换拖动选择
-  V: 切换拖动选择
-  a: 切换选择区块
-  c: 提交更改
-  w: 提交更改而无需预先提交钩子
-  C: 提交更改(使用编辑器编辑提交信息)
+  e: 编辑配置文件
+  o: 打开配置文件
+  u: 检查更新
+  enter: 切换到最近的仓库
+  a: 显示所有分支的日志
 
## 菜单 面板 @@ -293,13 +302,3 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct n: 新分支 enter: 查看提交的文件 - -## 状态 面板 (状态) - -
-  e: 编辑配置文件
-  o: 打开配置文件
-  u: 检查更新
-  enter: 切换到最近的仓库
-  a: 显示所有分支的日志
-
diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index 04d8d3fd5..d20a0c71a 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -21,6 +21,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/integration" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" ) type bindingSection struct { @@ -28,6 +30,17 @@ type bindingSection struct { bindings []*types.Binding } +type header struct { + // priority decides the order of the headers in the cheatsheet (lower means higher) + priority int + title string +} + +type headerWithBindings struct { + header header + bindings []*types.Binding +} + func CommandToRun() string { return "go run scripts/cheatsheet/main.go generate" } @@ -49,7 +62,8 @@ func generateAtDir(cheatsheetDir string) { panic(err) } - bindingSections := getBindingSections(mApp) + bindings := mApp.Gui.GetCheatsheetKeybindings() + bindingSections := getBindingSections(bindings, mApp.Tr) content := formatSections(mApp.Tr, bindingSections) content = fmt.Sprintf("_This file is auto-generated. To update, make the changes in the "+ "pkg/i18n directory and then run `%s` from the project root._\n\n%s", CommandToRun(), content) @@ -68,9 +82,7 @@ func writeString(file *os.File, str string) { } } -func localisedTitle(mApp *app.App, str string) string { - tr := mApp.Tr - +func localisedTitle(tr *i18n.TranslationSet, str string) string { contextTitleMap := map[string]string{ "global": tr.GlobalTitle, "navigation": tr.NavigationTitle, @@ -110,142 +122,66 @@ func localisedTitle(mApp *app.App, str string) string { return title } -func formatTitle(title string) string { - return fmt.Sprintf("\n## %s\n\n", title) -} - -func formatBinding(binding *types.Binding) string { - if binding.Alternative != "" { - return fmt.Sprintf(" %s: %s (%s)\n", gui.GetKeyDisplay(binding.Key), binding.Description, binding.Alternative) - } - return fmt.Sprintf(" %s: %s\n", gui.GetKeyDisplay(binding.Key), binding.Description) -} - -func getBindingSections(mApp *app.App) []*bindingSection { - bindingSections := []*bindingSection{} - - bindings := mApp.Gui.GetCheatsheetKeybindings() - - type contextAndViewType struct { - subtitle string - title string - } - - contextAndViewBindingMap := map[contextAndViewType][]*types.Binding{} - -outer: - for _, binding := range bindings { - if binding.Tag == "navigation" { - key := contextAndViewType{subtitle: "", title: "navigation"} - existing := contextAndViewBindingMap[key] - if existing == nil { - contextAndViewBindingMap[key] = []*types.Binding{binding} - } else { - if !slices.Some(contextAndViewBindingMap[key], func(navBinding *types.Binding) bool { - return navBinding.Description == binding.Description - }) { - contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding) - } - } - - continue outer - } - - contexts := []string{} - if len(binding.Contexts) == 0 { - contexts = append(contexts, "") - } else { - contexts = append(contexts, binding.Contexts...) - } - - for _, context := range contexts { - key := contextAndViewType{subtitle: context, title: binding.ViewName} - existing := contextAndViewBindingMap[key] - if existing == nil { - contextAndViewBindingMap[key] = []*types.Binding{binding} - } else { - contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding) - } - } - } - - type groupedBindingsType struct { - contextAndView contextAndViewType - bindings []*types.Binding - } - - groupedBindings := maps.MapToSlice( - contextAndViewBindingMap, - func(contextAndView contextAndViewType, contextBindings []*types.Binding) groupedBindingsType { - return groupedBindingsType{contextAndView: contextAndView, bindings: contextBindings} - }, - ) - - slices.SortFunc(groupedBindings, func(a, b groupedBindingsType) bool { - first := a.contextAndView - second := b.contextAndView - if first.title == "" { - return true - } - if second.title == "" { - return false - } - if first.title == "navigation" { - return true - } - if second.title == "navigation" { - return false - } - return first.title < second.title || (first.title == second.title && first.subtitle < second.subtitle) +func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*bindingSection { + bindingsToDisplay := slices.Filter(bindings, func(binding *types.Binding) bool { + return binding.Description != "" || binding.Alternative != "" }) - for _, group := range groupedBindings { - contextAndView := group.contextAndView - contextBindings := group.bindings - mApp.Log.Info("viewname: " + contextAndView.title + ", context: " + contextAndView.subtitle) - viewName := contextAndView.title - if viewName == "" { - viewName = "global" - } - translatedView := localisedTitle(mApp, viewName) - var title string - if contextAndView.subtitle == "" { - addendum := " " + mApp.Tr.Panel - if viewName == "global" || viewName == "navigation" { - addendum = "" - } - title = fmt.Sprintf("%s%s", translatedView, addendum) - } else { - translatedContextName := localisedTitle(mApp, contextAndView.subtitle) - title = fmt.Sprintf("%s %s (%s)", translatedView, mApp.Tr.Panel, translatedContextName) - } + bindingsByHeader := utils.MuiltiGroupBy(bindingsToDisplay, func(binding *types.Binding) []header { + return getHeaders(binding, tr) + }) - for _, binding := range contextBindings { - bindingSections = addBinding(title, bindingSections, binding) - } - } + bindingGroups := maps.MapToSlice(bindingsByHeader, func(header header, hBindings []*types.Binding) headerWithBindings { + uniqBindings := lo.UniqBy(hBindings, func(binding *types.Binding) string { + return binding.Description + gui.GetKeyDisplay(binding.Key) + }) - return bindingSections + return headerWithBindings{ + header: header, + bindings: uniqBindings, + } + }) + + slices.SortFunc(bindingGroups, func(a, b headerWithBindings) bool { + if a.header.priority != b.header.priority { + return a.header.priority > b.header.priority + } + return a.header.title < b.header.title + }) + + return slices.Map(bindingGroups, func(hb headerWithBindings) *bindingSection { + return &bindingSection{ + title: hb.header.title, + bindings: hb.bindings, + } + }) } -func addBinding(title string, bindingSections []*bindingSection, binding *types.Binding) []*bindingSection { - if binding.Description == "" && binding.Alternative == "" { - return bindingSections +// a binding may belong to multiple headers if it is applicable to multiple contexts, +// for example the copy-to-clipboard binding. +func getHeaders(binding *types.Binding, tr *i18n.TranslationSet) []header { + if binding.Tag == "navigation" { + return []header{{priority: 2, title: localisedTitle(tr, "navigation")}} } - for _, section := range bindingSections { - if title == section.title { - section.bindings = append(section.bindings, binding) - return bindingSections - } + if binding.ViewName == "" { + return []header{{priority: 3, title: localisedTitle(tr, "global")}} } - section := &bindingSection{ - title: title, - bindings: []*types.Binding{binding}, + if len(binding.Contexts) == 0 { + translatedView := localisedTitle(tr, binding.ViewName) + title := fmt.Sprintf("%s %s", translatedView, tr.Panel) + + return []header{{priority: 1, title: title}} } - return append(bindingSections, section) + return slices.Map(binding.Contexts, func(context string) header { + translatedView := localisedTitle(tr, binding.ViewName) + translatedContextName := localisedTitle(tr, context) + title := fmt.Sprintf("%s %s (%s)", translatedView, tr.Panel, translatedContextName) + + return header{priority: 1, title: title} + }) } func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string { @@ -262,3 +198,14 @@ func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) return content } + +func formatTitle(title string) string { + return fmt.Sprintf("\n## %s\n\n", title) +} + +func formatBinding(binding *types.Binding) string { + if binding.Alternative != "" { + return fmt.Sprintf(" %s: %s (%s)\n", gui.GetKeyDisplay(binding.Key), binding.Description, binding.Alternative) + } + return fmt.Sprintf(" %s: %s\n", gui.GetKeyDisplay(binding.Key), binding.Description) +} diff --git a/pkg/cheatsheet/generate_test.go b/pkg/cheatsheet/generate_test.go new file mode 100644 index 000000000..94b571454 --- /dev/null +++ b/pkg/cheatsheet/generate_test.go @@ -0,0 +1,281 @@ +package cheatsheet + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/stretchr/testify/assert" +) + +func TestGetBindingSections(t *testing.T) { + tr := i18n.EnglishTranslationSet() + + tests := []struct { + testName string + bindings []*types.Binding + expected []*bindingSection + }{ + { + testName: "no bindings", + bindings: []*types.Binding{}, + expected: []*bindingSection{}, + }, + { + testName: "one binding", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + }, + }, + expected: []*bindingSection{ + { + title: "Files Panel", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + }, + }, + }, + }, + }, + { + testName: "one binding with context", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + Contexts: []string{"submodules"}, + }, + }, + expected: []*bindingSection{ + { + title: "Files Panel (Submodules)", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + Contexts: []string{"submodules"}, + }, + }, + }, + }, + }, + { + testName: "global binding", + bindings: []*types.Binding{ + { + ViewName: "", + Description: "quit", + }, + }, + expected: []*bindingSection{ + { + title: "Global Keybindings", + bindings: []*types.Binding{ + { + ViewName: "", + Description: "quit", + }, + }, + }, + }, + }, + { + testName: "grouped bindings", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + Contexts: []string{"files"}, + }, + { + ViewName: "files", + Description: "unstage file", + Contexts: []string{"files"}, + }, + { + ViewName: "files", + Description: "drop submodule", + Contexts: []string{"submodules"}, + }, + { + ViewName: "commits", + Description: "revert commit", + }, + }, + expected: []*bindingSection{ + { + title: "Commits Panel", + bindings: []*types.Binding{ + { + ViewName: "commits", + Description: "revert commit", + }, + }, + }, + { + title: "Files Panel (Files)", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + Contexts: []string{"files"}, + }, + { + ViewName: "files", + Description: "unstage file", + Contexts: []string{"files"}, + }, + }, + }, + { + title: "Files Panel (Submodules)", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "drop submodule", + Contexts: []string{"submodules"}, + }, + }, + }, + }, + }, + { + testName: "with navigation bindings", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + }, + { + ViewName: "files", + Description: "unstage file", + }, + { + ViewName: "files", + Description: "scroll", + Tag: "navigation", + }, + { + ViewName: "commits", + Description: "revert commit", + }, + }, + expected: []*bindingSection{ + { + title: "List Panel Navigation", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "scroll", + Tag: "navigation", + }, + }, + }, + { + title: "Commits Panel", + bindings: []*types.Binding{ + { + ViewName: "commits", + Description: "revert commit", + }, + }, + }, + { + title: "Files Panel", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + }, + { + ViewName: "files", + Description: "unstage file", + }, + }, + }, + }, + }, + { + testName: "with duplicate navigation bindings", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + }, + { + ViewName: "files", + Description: "unstage file", + }, + { + ViewName: "files", + Description: "scroll", + Tag: "navigation", + }, + { + ViewName: "commits", + Description: "revert commit", + }, + { + ViewName: "commits", + Description: "scroll", + Tag: "navigation", + }, + { + ViewName: "commits", + Description: "page up", + Tag: "navigation", + }, + }, + expected: []*bindingSection{ + { + title: "List Panel Navigation", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "scroll", + Tag: "navigation", + }, + { + ViewName: "commits", + Description: "page up", + Tag: "navigation", + }, + }, + }, + { + title: "Commits Panel", + bindings: []*types.Binding{ + { + ViewName: "commits", + Description: "revert commit", + }, + }, + }, + { + title: "Files Panel", + bindings: []*types.Binding{ + { + ViewName: "files", + Description: "stage file", + }, + { + ViewName: "files", + Description: "unstage file", + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + actual := getBindingSections(test.bindings, &tr) + assert.EqualValues(t, test.expected, actual) + }) + } +} diff --git a/pkg/utils/slice.go b/pkg/utils/slice.go index 6971c9367..2281d8a73 100644 --- a/pkg/utils/slice.go +++ b/pkg/utils/slice.go @@ -76,3 +76,19 @@ func LimitStr(value string, limit int) string { } return value } + +// Similar to a regular GroupBy, except that each item can be grouped under multiple keys, +// so the callback returns a slice of keys instead of just one key. +func MuiltiGroupBy[T any, K comparable](slice []T, f func(T) []K) map[K][]T { + result := map[K][]T{} + for _, item := range slice { + for _, key := range f(item) { + if _, ok := result[key]; !ok { + result[key] = []T{item} + } else { + result[key] = append(result[key], item) + } + } + } + return result +}