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..3821c5768 100644 --- a/pkg/gui/controllers/helpers/repos_helper.go +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -103,6 +103,76 @@ 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 + +func (self *ReposHelper) recentRepoMenuItem(path string, branchName string) *types.MenuItem { + repoName := filepath.Base(path) + displayedRepoName := utils.TruncateWithEllipsis(repoName, recentReposColumnMaxWidth) + + // The icon is part of the column, so it counts towards the maximum width. + branchColumn := branchName + if icons.IsIconEnabled() { + branchColumn = icons.BRANCH_ICON + " " + branchName + } + displayedBranchColumn := utils.TruncateWithEllipsis(branchColumn, recentReposColumnMaxWidth) + + // Spell out whatever the columns show in truncated form + type tooltipField struct { + label string + value string + } + fields := []tooltipField{} + addTooltipField := func(label string, value string) { + fields = append(fields, tooltipField{label: label, value: value}) + } + + if displayedRepoName != repoName { + addTooltipField(self.c.Tr.RecentReposRepoLabel, repoName) + } + if displayedBranchColumn != branchColumn { + addTooltipField(self.c.Tr.RecentReposBranchLabel, branchName) + } + + // Line the values up behind the widest of the labels that are there + labelWidth := utils.MaxFn(fields, func(field tooltipField) int { + return utils.StringWidth(field.label) + }) + tooltipLines := lo.Map(fields, func(field tooltipField, _ int) string { + return utils.WithPadding(field.label, labelWidth, utils.AlignLeft) + " " + field.value + }) + + return &types.MenuItem{ + LabelColumns: []string{ + displayedRepoName, + style.FgCyan.Sprint(displayedBranchColumn), + // 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: strings.Join(tooltipLines, "\n"), + OnPress: func() error { + // Check before clearing the stack, so a refused switch doesn't + // forget the submodule breadcrumb (which would leave escape + // unable to return to the parent repo). + if self.switchRefusedBecauseBusy() { + return nil + } + // if we were in a submodule, we want to forget about that stack of repos + // so that hitting escape in the new repo does nothing + self.c.State().GetRepoPathStack().Clear() + return self.switchTo(path, self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT) + }, + } +} + func (self *ReposHelper) CreateRecentReposMenu() error { // we'll show an empty panel if there are no recent repos recentRepoPaths := []string{} @@ -126,33 +196,7 @@ func (self *ReposHelper) CreateRecentReposMenu() error { wg.Wait() menuItems := lo.Map(recentRepoPaths, func(path string, i int) *types.MenuItem { - branchName := currentBranches[i] - if icons.IsIconEnabled() { - branchName = icons.BRANCH_ICON + " " + branchName - } - - return &types.MenuItem{ - LabelColumns: []string{ - filepath.Base(path), - style.FgCyan.Sprint(branchName), - // 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))), - }, - OnPress: func() error { - // Check before clearing the stack, so a refused switch doesn't - // forget the submodule breadcrumb (which would leave escape - // unable to return to the parent repo). - if self.switchRefusedBecauseBusy() { - return nil - } - // if we were in a submodule, we want to forget about that stack of repos - // so that hitting escape in the new repo does nothing - self.c.State().GetRepoPathStack().Clear() - return self.switchTo(path, self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT) - }, - } + return self.recentRepoMenuItem(path, currentBranches[i]) }) return self.c.Menu(types.CreateMenuOptions{ 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,