diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 55a3e5bfa..aa678f263 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -89,6 +89,10 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel { }) } + if item.FilterColumns != nil { + return item.FilterColumns + } + return item.LabelColumns }, ) diff --git a/pkg/gui/controllers/helpers/repos_helper.go b/pkg/gui/controllers/helpers/repos_helper.go index c280d7153..b8e39af76 100644 --- a/pkg/gui/controllers/helpers/repos_helper.go +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -103,6 +103,35 @@ func (self *ReposHelper) getCurrentBranch(path string) string { return self.c.Tr.BranchUnknown } +// The maximum width of the repo name and branch name columns of the recent +// repos menu. Without a limit, one long name pushes the path column off the +// right edge of the menu for every entry, because each column is padded to the +// width of its widest entry. +const recentReposColumnMaxWidth = 30 + +// Spells out the repo name and the branch name of a recent repos menu item, +// but only the ones that the menu shows in truncated form. +func (self *ReposHelper) recentRepoTooltip(repoName string, branchName string) string { + type field struct { + label string + value string + } + + fields := []field{} + if utils.StringWidth(repoName) > recentReposColumnMaxWidth { + fields = append(fields, field{self.c.Tr.RecentReposRepoLabel, repoName}) + } + if utils.StringWidth(branchName) > recentReposColumnMaxWidth { + fields = append(fields, field{self.c.Tr.RecentReposBranchLabel, branchName}) + } + + labelWidth := utils.MaxFn(fields, func(f field) int { return utils.StringWidth(f.label) }) + + return strings.Join(lo.Map(fields, func(f field, _ int) string { + return utils.WithPadding(f.label, labelWidth, utils.AlignLeft) + " " + f.value + }), "\n") +} + func (self *ReposHelper) CreateRecentReposMenu() error { // we'll show an empty panel if there are no recent repos recentRepoPaths := []string{} @@ -126,20 +155,27 @@ func (self *ReposHelper) CreateRecentReposMenu() error { wg.Wait() menuItems := lo.Map(recentRepoPaths, func(path string, i int) *types.MenuItem { + repoName := filepath.Base(path) branchName := currentBranches[i] + + displayedBranchName := utils.TruncateWithEllipsis(branchName, recentReposColumnMaxWidth) if icons.IsIconEnabled() { - branchName = icons.BRANCH_ICON + " " + branchName + displayedBranchName = icons.BRANCH_ICON + " " + displayedBranchName } return &types.MenuItem{ LabelColumns: []string{ - filepath.Base(path), - style.FgCyan.Sprint(branchName), + utils.TruncateWithEllipsis(repoName, recentReposColumnMaxWidth), + style.FgCyan.Sprint(displayedBranchName), // The last segment of the path is already in the first column, // so showing the directory that contains the repo is enough to // tell repos with the same name apart. style.FgMagenta.Sprint(utils.ContractTilde(filepath.Dir(path))), }, + // Filtering matches the full text, including the parts that the + // columns above truncate or leave out. + FilterColumns: []string{repoName, branchName, path}, + Tooltip: self.recentRepoTooltip(repoName, branchName), OnPress: func() error { // Check before clearing the stack, so a refused switch doesn't // forget the submodule breadcrumb (which would leave escape diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 58ccf6d60..1603df9bb 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -310,6 +310,12 @@ type MenuItem struct { // alternative to Label. Allows specifying columns which will be auto-aligned LabelColumns []string + // The strings that filtering the menu matches against, for menus that + // abbreviate their columns to keep them narrow. If nil, LabelColumns are + // matched, so that a menu only needs to set this if what it displays is not + // the full text. + FilterColumns []string + OnPress func() error // Only applies when Label is used diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 9c72b53df..918247902 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -332,6 +332,8 @@ type TranslationSet struct { NotMidRebase string MustSelectFixupCommit string RecentRepos string + RecentReposRepoLabel string + RecentReposBranchLabel string MergeOptionsTitle string RebaseOptionsTitle string CherryPickOptionsTitle string @@ -1490,6 +1492,8 @@ func EnglishTranslationSet() *TranslationSet { NotMidRebase: "This action only works during an interactive rebase", MustSelectFixupCommit: "This action only works on fixup commits", RecentRepos: "Recent repositories", + RecentReposRepoLabel: "Repo:", + RecentReposBranchLabel: "Branch:", MergeOptionsTitle: "Merge options", RebaseOptionsTitle: "Rebase options", CherryPickOptionsTitle: "Cherry-pick options", diff --git a/pkg/integration/tests/misc/recent_repos_with_long_names.go b/pkg/integration/tests/misc/recent_repos_with_long_names.go new file mode 100644 index 000000000..7396faff3 --- /dev/null +++ b/pkg/integration/tests/misc/recent_repos_with_long_names.go @@ -0,0 +1,46 @@ +package misc + +import ( + "path/filepath" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var RecentReposWithLongNames = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Long repo and branch names are truncated in the recent repositories menu, and shown in full in the tooltip", + ExtraCmdArgs: []string{}, + ExtraEnvVars: map[string]string{ + "SHOW_RECENT_REPOS": "true", + }, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + // the first entry is the repo we're in, so it isn't offered + current, _ := filepath.Abs(".") + target, _ := filepath.Abs("../repo-with-a-name-that-is-far-too-long") + cfg.GetAppState().RecentRepos = []string{current, target} + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + shell.NewBranch("branch-with-a-name-that-is-too-long") + shell.CloneNonBare("repo-with-a-name-that-is-far-too-long") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.ExpectPopup().Menu(). + Title(Equals("Recent repositories")). + Lines( + Contains("repo-with-a-name-that-is-far-… branch-with-a-name-that-is-to…").IsSelected(), + Contains("Cancel"), + ). + Tooltip(Equals("Repo: repo-with-a-name-that-is-far-too-long\nBranch: branch-with-a-name-that-is-too-long")). + // Filtering matches the full names, including the part that the + // menu truncates + Filter("too-long"). + Lines( + Contains("repo-with-a-name-that-is-far-…").IsSelected(), + ). + Confirm() + + t.Views().Status().Content(Contains("repo-with-a-name")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index a4e732cf0..6dcb226f1 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -366,6 +366,7 @@ var tests = []*components.IntegrationTest{ misc.FilterRecentRepos, misc.InitialOpen, misc.RecentReposOnLaunch, + misc.RecentReposWithLongNames, misc.StartInGitDir, patch_building.Apply, patch_building.ApplyInReverse,