mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-15 01:34:26 +02:00
Section headers in keybindings menu (#2911)
This commit is contained in:
@ -24,7 +24,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
return presentation.GetBranchListDisplayStrings(
|
return presentation.GetBranchListDisplayStrings(
|
||||||
viewModel.GetItems(),
|
viewModel.GetItems(),
|
||||||
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
||||||
@ -45,9 +45,11 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
|
|||||||
c.UserConfig.Gui.ShowFileTree,
|
c.UserConfig.Gui.ShowFileTree,
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
if viewModel.Len() == 0 {
|
if viewModel.Len() == 0 {
|
||||||
return [][]string{{style.FgRed.Sprint("(none)")}}
|
return [][]string{{style.FgRed.Sprint("(none)")}}
|
||||||
}
|
}
|
||||||
@ -54,9 +54,11 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
|
|||||||
Transient: true,
|
Transient: true,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,17 +4,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListContextTrait struct {
|
type ListContextTrait struct {
|
||||||
types.Context
|
types.Context
|
||||||
|
ListRenderer
|
||||||
|
|
||||||
c *ContextCommon
|
c *ContextCommon
|
||||||
list types.IList
|
|
||||||
getDisplayStrings func(startIdx int, length int) [][]string
|
|
||||||
// Alignment for each column. If nil, the default is left alignment
|
|
||||||
getColumnAlignments func() []utils.Alignment
|
|
||||||
// Some contexts, like the commit context, will highlight the path from the selected commit
|
// Some contexts, like the commit context, will highlight the path from the selected commit
|
||||||
// to its parents, because it's ambiguous otherwise. For these, we need to refresh the viewport
|
// to its parents, because it's ambiguous otherwise. For these, we need to refresh the viewport
|
||||||
// so that we show the highlighted path.
|
// so that we show the highlighted path.
|
||||||
@ -26,10 +22,6 @@ type ListContextTrait struct {
|
|||||||
|
|
||||||
func (self *ListContextTrait) IsListContext() {}
|
func (self *ListContextTrait) IsListContext() {}
|
||||||
|
|
||||||
func (self *ListContextTrait) GetList() types.IList {
|
|
||||||
return self.list
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *ListContextTrait) FocusLine() {
|
func (self *ListContextTrait) FocusLine() {
|
||||||
// Doing this at the end of the layout function because we need the view to be
|
// Doing this at the end of the layout function because we need the view to be
|
||||||
// resized before we focus the line, otherwise if we're in accordion mode
|
// resized before we focus the line, otherwise if we're in accordion mode
|
||||||
@ -37,7 +29,8 @@ func (self *ListContextTrait) FocusLine() {
|
|||||||
self.c.AfterLayout(func() error {
|
self.c.AfterLayout(func() error {
|
||||||
oldOrigin, _ := self.GetViewTrait().ViewPortYBounds()
|
oldOrigin, _ := self.GetViewTrait().ViewPortYBounds()
|
||||||
|
|
||||||
self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx())
|
self.GetViewTrait().FocusPoint(
|
||||||
|
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()))
|
||||||
|
|
||||||
// If FocusPoint() caused the view to scroll (because the selected line
|
// If FocusPoint() caused the view to scroll (because the selected line
|
||||||
// was out of view before), we need to rerender the view port again.
|
// was out of view before), we need to rerender the view port again.
|
||||||
@ -59,8 +52,7 @@ func (self *ListContextTrait) FocusLine() {
|
|||||||
|
|
||||||
func (self *ListContextTrait) refreshViewport() {
|
func (self *ListContextTrait) refreshViewport() {
|
||||||
startIdx, length := self.GetViewTrait().ViewPortYBounds()
|
startIdx, length := self.GetViewTrait().ViewPortYBounds()
|
||||||
displayStrings := self.getDisplayStrings(startIdx, length)
|
content := self.renderLines(startIdx, startIdx+length)
|
||||||
content := utils.RenderDisplayStrings(displayStrings, nil)
|
|
||||||
self.GetViewTrait().SetViewPortContent(content)
|
self.GetViewTrait().SetViewPortContent(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,14 +85,7 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error
|
|||||||
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
|
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
|
||||||
func (self *ListContextTrait) HandleRender() error {
|
func (self *ListContextTrait) HandleRender() error {
|
||||||
self.list.RefreshSelectedIdx()
|
self.list.RefreshSelectedIdx()
|
||||||
var columnAlignments []utils.Alignment
|
content := self.renderLines(-1, -1)
|
||||||
if self.getColumnAlignments != nil {
|
|
||||||
columnAlignments = self.getColumnAlignments()
|
|
||||||
}
|
|
||||||
content := utils.RenderDisplayStrings(
|
|
||||||
self.getDisplayStrings(0, self.list.Len()),
|
|
||||||
columnAlignments,
|
|
||||||
)
|
|
||||||
self.GetViewTrait().SetContent(content)
|
self.GetViewTrait().SetContent(content)
|
||||||
self.c.Render()
|
self.c.Render()
|
||||||
self.setFooter()
|
self.setFooter()
|
||||||
|
124
pkg/gui/context/list_renderer.go
Normal file
124
pkg/gui/context/list_renderer.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NonModelItem struct {
|
||||||
|
// Where in the model this should be inserted
|
||||||
|
Index int
|
||||||
|
// Content to render
|
||||||
|
Content string
|
||||||
|
// The column from which to render the item
|
||||||
|
Column int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRenderer struct {
|
||||||
|
list types.IList
|
||||||
|
// Function to get the display strings for each model item in the given
|
||||||
|
// range. startIdx and endIdx are model indices. For each model item, return
|
||||||
|
// an array of strings, one for each column; the list renderer will take
|
||||||
|
// care of aligning the columns appropriately.
|
||||||
|
getDisplayStrings func(startIdx int, endIdx int) [][]string
|
||||||
|
// Alignment for each column. If nil, the default is left alignment
|
||||||
|
getColumnAlignments func() []utils.Alignment
|
||||||
|
// Function to insert non-model items (e.g. section headers). If nil, no
|
||||||
|
// such items are inserted
|
||||||
|
getNonModelItems func() []*NonModelItem
|
||||||
|
|
||||||
|
// The remaining fields are private and shouldn't be initialized by clients
|
||||||
|
numNonModelItems int
|
||||||
|
viewIndicesByModelIndex []int
|
||||||
|
modelIndicesByViewIndex []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ListRenderer) GetList() types.IList {
|
||||||
|
return self.list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ListRenderer) ModelIndexToViewIndex(modelIndex int) int {
|
||||||
|
modelIndex = lo.Clamp(modelIndex, 0, self.list.Len())
|
||||||
|
if self.viewIndicesByModelIndex != nil {
|
||||||
|
return self.viewIndicesByModelIndex[modelIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ListRenderer) ViewIndexToModelIndex(viewIndex int) int {
|
||||||
|
viewIndex = utils.Clamp(viewIndex, 0, self.list.Len()+self.numNonModelItems)
|
||||||
|
if self.modelIndicesByViewIndex != nil {
|
||||||
|
return self.modelIndicesByViewIndex[viewIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// startIdx and endIdx are view indices, not model indices. If you want to
|
||||||
|
// render the whole list, pass -1 for both.
|
||||||
|
func (self *ListRenderer) renderLines(startIdx int, endIdx int) string {
|
||||||
|
var columnAlignments []utils.Alignment
|
||||||
|
if self.getColumnAlignments != nil {
|
||||||
|
columnAlignments = self.getColumnAlignments()
|
||||||
|
}
|
||||||
|
nonModelItems := []*NonModelItem{}
|
||||||
|
self.numNonModelItems = 0
|
||||||
|
if self.getNonModelItems != nil {
|
||||||
|
nonModelItems = self.getNonModelItems()
|
||||||
|
self.prepareConversionArrays(nonModelItems)
|
||||||
|
}
|
||||||
|
startModelIdx := 0
|
||||||
|
if startIdx == -1 {
|
||||||
|
startIdx = 0
|
||||||
|
} else {
|
||||||
|
startModelIdx = self.ViewIndexToModelIndex(startIdx)
|
||||||
|
}
|
||||||
|
endModelIdx := self.list.Len()
|
||||||
|
if endIdx == -1 {
|
||||||
|
endIdx = endModelIdx + len(nonModelItems)
|
||||||
|
} else {
|
||||||
|
endModelIdx = self.ViewIndexToModelIndex(endIdx)
|
||||||
|
}
|
||||||
|
lines, columnPositions := utils.RenderDisplayStrings(
|
||||||
|
self.getDisplayStrings(startModelIdx, endModelIdx),
|
||||||
|
columnAlignments)
|
||||||
|
lines = self.insertNonModelItems(nonModelItems, endIdx, startIdx, lines, columnPositions)
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ListRenderer) prepareConversionArrays(nonModelItems []*NonModelItem) {
|
||||||
|
self.numNonModelItems = len(nonModelItems)
|
||||||
|
self.viewIndicesByModelIndex = lo.Range(self.list.Len() + 1)
|
||||||
|
self.modelIndicesByViewIndex = lo.Range(self.list.Len() + 1)
|
||||||
|
offset := 0
|
||||||
|
for _, item := range nonModelItems {
|
||||||
|
for i := item.Index; i <= self.list.Len(); i++ {
|
||||||
|
self.viewIndicesByModelIndex[i]++
|
||||||
|
}
|
||||||
|
self.modelIndicesByViewIndex = slices.Insert(
|
||||||
|
self.modelIndicesByViewIndex, item.Index+offset, self.modelIndicesByViewIndex[item.Index+offset])
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ListRenderer) insertNonModelItems(
|
||||||
|
nonModelItems []*NonModelItem, endIdx int, startIdx int, lines []string, columnPositions []int,
|
||||||
|
) []string {
|
||||||
|
offset := 0
|
||||||
|
for _, item := range nonModelItems {
|
||||||
|
if item.Index+offset >= endIdx {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if item.Index+offset >= startIdx {
|
||||||
|
padding := strings.Repeat(" ", columnPositions[item.Column])
|
||||||
|
lines = slices.Insert(lines, item.Index+offset-startIdx, padding+item.Content)
|
||||||
|
}
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
256
pkg/gui/context/list_renderer_test.go
Normal file
256
pkg/gui/context/list_renderer_test.go
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListRenderer_renderLines(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
modelStrings []string
|
||||||
|
nonModelIndices []int
|
||||||
|
startIdx int
|
||||||
|
endIdx int
|
||||||
|
expectedOutput string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Render whole list",
|
||||||
|
modelStrings: []string{"a", "b", "c"},
|
||||||
|
startIdx: 0,
|
||||||
|
endIdx: 3,
|
||||||
|
expectedOutput: `
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Partial list, beginning",
|
||||||
|
modelStrings: []string{"a", "b", "c"},
|
||||||
|
startIdx: 0,
|
||||||
|
endIdx: 2,
|
||||||
|
expectedOutput: `
|
||||||
|
a
|
||||||
|
b`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Partial list, end",
|
||||||
|
modelStrings: []string{"a", "b", "c"},
|
||||||
|
startIdx: 1,
|
||||||
|
endIdx: 3,
|
||||||
|
expectedOutput: `
|
||||||
|
b
|
||||||
|
c`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pass an endIdx greater than the model length",
|
||||||
|
modelStrings: []string{"a", "b", "c"},
|
||||||
|
startIdx: 2,
|
||||||
|
endIdx: 5,
|
||||||
|
expectedOutput: `
|
||||||
|
c`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Whole list with section headers",
|
||||||
|
modelStrings: []string{"a", "b", "c"},
|
||||||
|
nonModelIndices: []int{1, 3},
|
||||||
|
startIdx: 0,
|
||||||
|
endIdx: 5,
|
||||||
|
expectedOutput: `
|
||||||
|
a
|
||||||
|
--- 1 (0) ---
|
||||||
|
b
|
||||||
|
c
|
||||||
|
--- 3 (1) ---`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple consecutive headers",
|
||||||
|
modelStrings: []string{"a", "b", "c"},
|
||||||
|
nonModelIndices: []int{0, 0, 2, 2, 2},
|
||||||
|
startIdx: 0,
|
||||||
|
endIdx: 8,
|
||||||
|
expectedOutput: `
|
||||||
|
--- 0 (0) ---
|
||||||
|
--- 0 (1) ---
|
||||||
|
a
|
||||||
|
b
|
||||||
|
--- 2 (2) ---
|
||||||
|
--- 2 (3) ---
|
||||||
|
--- 2 (4) ---
|
||||||
|
c`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Partial list with headers, beginning",
|
||||||
|
modelStrings: []string{"a", "b", "c"},
|
||||||
|
nonModelIndices: []int{1, 3},
|
||||||
|
startIdx: 0,
|
||||||
|
endIdx: 3,
|
||||||
|
expectedOutput: `
|
||||||
|
a
|
||||||
|
--- 1 (0) ---
|
||||||
|
b`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Partial list with headers, end (beyond end index)",
|
||||||
|
modelStrings: []string{"a", "b", "c"},
|
||||||
|
nonModelIndices: []int{1, 3},
|
||||||
|
startIdx: 2,
|
||||||
|
endIdx: 7,
|
||||||
|
expectedOutput: `
|
||||||
|
b
|
||||||
|
c
|
||||||
|
--- 3 (1) ---`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
|
viewModel := NewListViewModel[string](func() []string { return s.modelStrings })
|
||||||
|
var getNonModelItems func() []*NonModelItem
|
||||||
|
if s.nonModelIndices != nil {
|
||||||
|
getNonModelItems = func() []*NonModelItem {
|
||||||
|
return lo.Map(s.nonModelIndices, func(modelIndex int, nonModelIndex int) *NonModelItem {
|
||||||
|
return &NonModelItem{
|
||||||
|
Index: modelIndex,
|
||||||
|
Content: fmt.Sprintf("--- %d (%d) ---", modelIndex, nonModelIndex),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self := &ListRenderer{
|
||||||
|
list: viewModel,
|
||||||
|
getDisplayStrings: func(startIdx int, endIdx int) [][]string {
|
||||||
|
return lo.Map(s.modelStrings[startIdx:endIdx],
|
||||||
|
func(s string, _ int) []string { return []string{s} })
|
||||||
|
},
|
||||||
|
getNonModelItems: getNonModelItems,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedOutput := strings.Join(lo.Map(
|
||||||
|
strings.Split(strings.TrimPrefix(s.expectedOutput, "\n"), "\n"),
|
||||||
|
func(line string, _ int) string { return strings.TrimSpace(line) }), "\n")
|
||||||
|
|
||||||
|
assert.Equal(t, expectedOutput, self.renderLines(s.startIdx, s.endIdx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
numModelItems int
|
||||||
|
nonModelIndices []int
|
||||||
|
|
||||||
|
modelIndices []int
|
||||||
|
expectedViewIndices []int
|
||||||
|
|
||||||
|
viewIndices []int
|
||||||
|
expectedModelIndices []int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no headers (no getNonModelItems provided)",
|
||||||
|
numModelItems: 3,
|
||||||
|
nonModelIndices: nil, // no get
|
||||||
|
|
||||||
|
modelIndices: []int{-1, 0, 1, 2, 3, 4},
|
||||||
|
expectedViewIndices: []int{0, 0, 1, 2, 3, 3},
|
||||||
|
|
||||||
|
viewIndices: []int{-1, 0, 1, 2, 3, 4},
|
||||||
|
expectedModelIndices: []int{0, 0, 1, 2, 3, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no headers (getNonModelItems returns zero items)",
|
||||||
|
numModelItems: 3,
|
||||||
|
nonModelIndices: []int{},
|
||||||
|
|
||||||
|
modelIndices: []int{-1, 0, 1, 2, 3, 4},
|
||||||
|
expectedViewIndices: []int{0, 0, 1, 2, 3, 3},
|
||||||
|
|
||||||
|
viewIndices: []int{-1, 0, 1, 2, 3, 4},
|
||||||
|
expectedModelIndices: []int{0, 0, 1, 2, 3, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic",
|
||||||
|
numModelItems: 3,
|
||||||
|
nonModelIndices: []int{1, 2},
|
||||||
|
|
||||||
|
/*
|
||||||
|
0: model 0
|
||||||
|
1: --- header 0 ---
|
||||||
|
2: model 1
|
||||||
|
3: --- header 1 ---
|
||||||
|
4: model 2
|
||||||
|
*/
|
||||||
|
|
||||||
|
modelIndices: []int{-1, 0, 1, 2, 3, 4},
|
||||||
|
expectedViewIndices: []int{0, 0, 2, 4, 5, 5},
|
||||||
|
|
||||||
|
viewIndices: []int{-1, 0, 1, 2, 3, 4, 5, 6},
|
||||||
|
expectedModelIndices: []int{0, 0, 1, 1, 2, 2, 3, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "consecutive section headers",
|
||||||
|
numModelItems: 3,
|
||||||
|
nonModelIndices: []int{0, 0, 2, 2, 2, 3, 3},
|
||||||
|
|
||||||
|
/*
|
||||||
|
0: --- header 0 ---
|
||||||
|
1: --- header 1 ---
|
||||||
|
2: model 0
|
||||||
|
3: model 1
|
||||||
|
4: --- header 2 ---
|
||||||
|
5: --- header 3 ---
|
||||||
|
6: --- header 4 ---
|
||||||
|
7: model 2
|
||||||
|
8: --- header 5 ---
|
||||||
|
9: --- header 6 ---
|
||||||
|
*/
|
||||||
|
modelIndices: []int{-1, 0, 1, 2, 3, 4},
|
||||||
|
expectedViewIndices: []int{2, 2, 3, 7, 10, 10},
|
||||||
|
|
||||||
|
viewIndices: []int{-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
|
||||||
|
expectedModelIndices: []int{0, 0, 0, 0, 1, 2, 2, 2, 2, 3, 3, 3, 3},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
|
// Expect lists of equal length for each test:
|
||||||
|
assert.Equal(t, len(s.modelIndices), len(s.expectedViewIndices))
|
||||||
|
assert.Equal(t, len(s.viewIndices), len(s.expectedModelIndices))
|
||||||
|
|
||||||
|
modelInts := lo.Range(s.numModelItems)
|
||||||
|
viewModel := NewListViewModel[int](func() []int { return modelInts })
|
||||||
|
var getNonModelItems func() []*NonModelItem
|
||||||
|
if s.nonModelIndices != nil {
|
||||||
|
getNonModelItems = func() []*NonModelItem {
|
||||||
|
return lo.Map(s.nonModelIndices, func(modelIndex int, _ int) *NonModelItem {
|
||||||
|
return &NonModelItem{Index: modelIndex, Content: ""}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self := &ListRenderer{
|
||||||
|
list: viewModel,
|
||||||
|
getDisplayStrings: func(startIdx int, endIdx int) [][]string {
|
||||||
|
return lo.Map(modelInts[startIdx:endIdx],
|
||||||
|
func(i int, _ int) []string { return []string{fmt.Sprint(i)} })
|
||||||
|
},
|
||||||
|
getNonModelItems: getNonModelItems,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to render first so that it knows the non-model items
|
||||||
|
self.renderLines(-1, -1)
|
||||||
|
|
||||||
|
for i := 0; i < len(s.modelIndices); i++ {
|
||||||
|
assert.Equal(t, s.expectedViewIndices[i], self.ModelIndexToViewIndex(s.modelIndices[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(s.viewIndices); i++ {
|
||||||
|
assert.Equal(t, s.expectedModelIndices[i], self.ViewIndexToModelIndex(s.viewIndices[i]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
|||||||
c,
|
c,
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(startIdx int, endIdx int) [][]string {
|
||||||
selectedCommitSha := ""
|
selectedCommitSha := ""
|
||||||
|
|
||||||
if c.CurrentContext().GetKey() == LOCAL_COMMITS_CONTEXT_KEY {
|
if c.CurrentContext().GetKey() == LOCAL_COMMITS_CONTEXT_KEY {
|
||||||
@ -56,7 +56,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
|||||||
c.UserConfig.Git.ParseEmoji,
|
c.UserConfig.Git.ParseEmoji,
|
||||||
selectedCommitSha,
|
selectedCommitSha,
|
||||||
startIdx,
|
startIdx,
|
||||||
length,
|
endIdx,
|
||||||
shouldShowGraph(c),
|
shouldShowGraph(c),
|
||||||
c.Model().BisectInfo,
|
c.Model().BisectInfo,
|
||||||
showYouAreHereLabel,
|
showYouAreHereLabel,
|
||||||
@ -74,8 +74,10 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
c: c,
|
c: c,
|
||||||
refreshViewportOnChange: true,
|
refreshViewportOnChange: true,
|
||||||
},
|
},
|
||||||
|
@ -34,10 +34,13 @@ func NewMenuContext(
|
|||||||
Focusable: true,
|
Focusable: true,
|
||||||
HasUncontrolledBounds: true,
|
HasUncontrolledBounds: true,
|
||||||
})),
|
})),
|
||||||
getDisplayStrings: viewModel.GetDisplayStrings,
|
ListRenderer: ListRenderer{
|
||||||
list: viewModel,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: viewModel.GetDisplayStrings,
|
||||||
getColumnAlignments: func() []utils.Alignment { return viewModel.columnAlignment },
|
getColumnAlignments: func() []utils.Alignment { return viewModel.columnAlignment },
|
||||||
|
getNonModelItems: viewModel.GetNonModelItems,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,7 +82,7 @@ func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem, columnAlignment
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move into presentation package
|
// TODO: move into presentation package
|
||||||
func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]string {
|
func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string {
|
||||||
menuItems := self.FilteredListViewModel.GetItems()
|
menuItems := self.FilteredListViewModel.GetItems()
|
||||||
showKeys := lo.SomeBy(menuItems, func(item *types.MenuItem) bool {
|
showKeys := lo.SomeBy(menuItems, func(item *types.MenuItem) bool {
|
||||||
return item.Key != nil
|
return item.Key != nil
|
||||||
@ -111,6 +114,40 @@ func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]str
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *MenuViewModel) GetNonModelItems() []*NonModelItem {
|
||||||
|
// Don't display section headers when we are filtering. The reason is that
|
||||||
|
// filtering changes the order of the items (they are sorted by best match),
|
||||||
|
// so all the sections would be messed up.
|
||||||
|
if self.FilteredListViewModel.IsFiltering() {
|
||||||
|
return []*NonModelItem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []*NonModelItem{}
|
||||||
|
menuItems := self.FilteredListViewModel.GetItems()
|
||||||
|
var prevSection *types.MenuSection = nil
|
||||||
|
for i, menuItem := range menuItems {
|
||||||
|
menuItem := menuItem
|
||||||
|
if menuItem.Section != nil && menuItem.Section != prevSection {
|
||||||
|
if prevSection != nil {
|
||||||
|
result = append(result, &NonModelItem{
|
||||||
|
Index: i,
|
||||||
|
Column: 1,
|
||||||
|
Content: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, &NonModelItem{
|
||||||
|
Index: i,
|
||||||
|
Column: 1,
|
||||||
|
Content: style.FgGreen.SetBold().Sprintf("--- %s ---", menuItem.Section.Title),
|
||||||
|
})
|
||||||
|
prevSection = menuItem.Section
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||||
basicBindings := self.ListContextTrait.GetKeybindings(opts)
|
basicBindings := self.ListContextTrait.GetKeybindings(opts)
|
||||||
menuItemsWithKeys := lo.Filter(self.menuItems, func(item *types.MenuItem, _ int) bool {
|
menuItemsWithKeys := lo.Filter(self.menuItems, func(item *types.MenuItem, _ int) bool {
|
||||||
|
@ -26,7 +26,7 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
return presentation.GetReflogCommitListDisplayStrings(
|
return presentation.GetReflogCommitListDisplayStrings(
|
||||||
viewModel.GetItems(),
|
viewModel.GetItems(),
|
||||||
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
||||||
@ -49,9 +49,11 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func NewRemoteBranchesContext(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
return presentation.GetRemoteBranchListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
|
return presentation.GetRemoteBranchListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,9 +43,11 @@ func NewRemoteBranchesContext(
|
|||||||
Focusable: true,
|
Focusable: true,
|
||||||
Transient: true,
|
Transient: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ func NewRemotesContext(c *ContextCommon) *RemotesContext {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
return presentation.GetRemoteListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
|
return presentation.GetRemoteListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,9 +38,11 @@ func NewRemotesContext(c *ContextCommon) *RemotesContext {
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ func NewStashContext(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
return presentation.GetStashEntryListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
|
return presentation.GetStashEntryListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +40,11 @@ func NewStashContext(
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ func NewSubCommitsContext(
|
|||||||
limitCommits: true,
|
limitCommits: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(startIdx int, endIdx int) [][]string {
|
||||||
// This can happen if a sub-commits view is asked to be rerendered while
|
// This can happen if a sub-commits view is asked to be rerendered while
|
||||||
// it is invisble; for example when switching screen modes, which
|
// it is invisble; for example when switching screen modes, which
|
||||||
// rerenders all views.
|
// rerenders all views.
|
||||||
@ -72,7 +72,7 @@ func NewSubCommitsContext(
|
|||||||
c.UserConfig.Git.ParseEmoji,
|
c.UserConfig.Git.ParseEmoji,
|
||||||
selectedCommitSha,
|
selectedCommitSha,
|
||||||
startIdx,
|
startIdx,
|
||||||
length,
|
endIdx,
|
||||||
shouldShowGraph(c),
|
shouldShowGraph(c),
|
||||||
git_commands.NewNullBisectInfo(),
|
git_commands.NewNullBisectInfo(),
|
||||||
false,
|
false,
|
||||||
@ -93,8 +93,10 @@ func NewSubCommitsContext(
|
|||||||
Focusable: true,
|
Focusable: true,
|
||||||
Transient: true,
|
Transient: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
c: c,
|
c: c,
|
||||||
refreshViewportOnChange: true,
|
refreshViewportOnChange: true,
|
||||||
},
|
},
|
||||||
|
@ -21,7 +21,7 @@ func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
return presentation.GetSubmoduleListDisplayStrings(viewModel.GetItems())
|
return presentation.GetSubmoduleListDisplayStrings(viewModel.GetItems())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,9 +35,11 @@ func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext {
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ func NewSuggestionsContext(
|
|||||||
return state.Suggestions
|
return state.Suggestions
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
return presentation.GetSuggestionListDisplayStrings(state.Suggestions)
|
return presentation.GetSuggestionListDisplayStrings(state.Suggestions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,9 +54,11 @@ func NewSuggestionsContext(
|
|||||||
Focusable: true,
|
Focusable: true,
|
||||||
HasUncontrolledBounds: true,
|
HasUncontrolledBounds: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ func NewTagsContext(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
return presentation.GetTagListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
|
return presentation.GetTagListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +40,11 @@ func NewTagsContext(
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
|
|||||||
c.UserConfig.Gui.ShowFileTree,
|
c.UserConfig.Gui.ShowFileTree,
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
lines := presentation.RenderFileTree(viewModel, c.Modes().Diffing.Ref, c.Model().Submodules)
|
lines := presentation.RenderFileTree(viewModel, c.Modes().Diffing.Ref, c.Model().Submodules)
|
||||||
return lo.Map(lines, func(line string, _ int) []string {
|
return lo.Map(lines, func(line string, _ int) []string {
|
||||||
return []string{line}
|
return []string{line}
|
||||||
@ -41,9 +41,11 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ func NewWorktreesContext(c *ContextCommon) *WorktreesContext {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||||
return presentation.GetWorktreeDisplayStrings(
|
return presentation.GetWorktreeDisplayStrings(
|
||||||
c.Tr,
|
c.Tr,
|
||||||
viewModel.GetFilteredList(),
|
viewModel.GetFilteredList(),
|
||||||
@ -38,9 +38,11 @@ func NewWorktreesContext(c *ContextCommon) *WorktreesContext {
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
})),
|
})),
|
||||||
list: viewModel,
|
ListRenderer: ListRenderer{
|
||||||
getDisplayStrings: getDisplayStrings,
|
list: viewModel,
|
||||||
c: c,
|
getDisplayStrings: getDisplayStrings,
|
||||||
|
},
|
||||||
|
c: c,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,9 +83,11 @@ func (self *ListController) handleLineChange(change int) error {
|
|||||||
// we're not constantly re-rendering the main view.
|
// we're not constantly re-rendering the main view.
|
||||||
if before != after {
|
if before != after {
|
||||||
if change == -1 {
|
if change == -1 {
|
||||||
checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig, before, after)
|
checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig,
|
||||||
|
self.context.ModelIndexToViewIndex(before), self.context.ModelIndexToViewIndex(after))
|
||||||
} else if change == 1 {
|
} else if change == 1 {
|
||||||
checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig, before, after)
|
checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig,
|
||||||
|
self.context.ModelIndexToViewIndex(before), self.context.ModelIndexToViewIndex(after))
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.context.HandleFocus(types.OnFocusOpts{})
|
return self.context.HandleFocus(types.OnFocusOpts{})
|
||||||
@ -112,7 +114,7 @@ func (self *ListController) HandleGotoBottom() error {
|
|||||||
|
|
||||||
func (self *ListController) HandleClick(opts gocui.ViewMouseBindingOpts) error {
|
func (self *ListController) HandleClick(opts gocui.ViewMouseBindingOpts) error {
|
||||||
prevSelectedLineIdx := self.context.GetList().GetSelectedLineIdx()
|
prevSelectedLineIdx := self.context.GetList().GetSelectedLineIdx()
|
||||||
newSelectedLineIdx := opts.Y
|
newSelectedLineIdx := self.context.ViewIndexToModelIndex(opts.Y)
|
||||||
alreadyFocused := self.isFocused()
|
alreadyFocused := self.isFocused()
|
||||||
|
|
||||||
if err := self.pushContextIfNotFocused(); err != nil {
|
if err := self.pushContextIfNotFocused(); err != nil {
|
||||||
|
@ -18,23 +18,33 @@ func (self *OptionsMenuAction) Call() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
bindings := self.getBindings(ctx)
|
local, global, navigation := self.getBindings(ctx)
|
||||||
|
|
||||||
menuItems := lo.Map(bindings, func(binding *types.Binding, _ int) *types.MenuItem {
|
menuItems := []*types.MenuItem{}
|
||||||
return &types.MenuItem{
|
|
||||||
OpensMenu: binding.OpensMenu,
|
appendBindings := func(bindings []*types.Binding, section *types.MenuSection) {
|
||||||
Label: binding.Description,
|
menuItems = append(menuItems,
|
||||||
OnPress: func() error {
|
lo.Map(bindings, func(binding *types.Binding, _ int) *types.MenuItem {
|
||||||
if binding.Handler == nil {
|
return &types.MenuItem{
|
||||||
return nil
|
OpensMenu: binding.OpensMenu,
|
||||||
|
Label: binding.Description,
|
||||||
|
OnPress: func() error {
|
||||||
|
if binding.Handler == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.Handler()
|
||||||
|
},
|
||||||
|
Key: binding.Key,
|
||||||
|
Tooltip: binding.Tooltip,
|
||||||
|
Section: section,
|
||||||
}
|
}
|
||||||
|
})...)
|
||||||
|
}
|
||||||
|
|
||||||
return binding.Handler()
|
appendBindings(local, &types.MenuSection{Title: self.c.Tr.KeybindingsMenuSectionLocal, Column: 1})
|
||||||
},
|
appendBindings(global, &types.MenuSection{Title: self.c.Tr.KeybindingsMenuSectionGlobal, Column: 1})
|
||||||
Key: binding.Key,
|
appendBindings(navigation, &types.MenuSection{Title: self.c.Tr.KeybindingsMenuSectionNavigation, Column: 1})
|
||||||
Tooltip: binding.Tooltip,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return self.c.Menu(types.CreateMenuOptions{
|
return self.c.Menu(types.CreateMenuOptions{
|
||||||
Title: self.c.Tr.Keybindings,
|
Title: self.c.Tr.Keybindings,
|
||||||
@ -44,7 +54,8 @@ func (self *OptionsMenuAction) Call() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *OptionsMenuAction) getBindings(context types.Context) []*types.Binding {
|
// Returns three slices of bindings: local, global, and navigation
|
||||||
|
func (self *OptionsMenuAction) getBindings(context types.Context) ([]*types.Binding, []*types.Binding, []*types.Binding) {
|
||||||
var bindingsGlobal, bindingsPanel, bindingsNavigation []*types.Binding
|
var bindingsGlobal, bindingsPanel, bindingsNavigation []*types.Binding
|
||||||
|
|
||||||
bindings, _ := self.c.GetInitialKeybindingsWithCustomCommands()
|
bindings, _ := self.c.GetInitialKeybindingsWithCustomCommands()
|
||||||
@ -61,14 +72,7 @@ func (self *OptionsMenuAction) getBindings(context types.Context) []*types.Bindi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resultBindings := []*types.Binding{}
|
return uniqueBindings(bindingsPanel), uniqueBindings(bindingsGlobal), uniqueBindings(bindingsNavigation)
|
||||||
resultBindings = append(resultBindings, uniqueBindings(bindingsPanel)...)
|
|
||||||
// adding a separator between the panel-specific bindings and the other bindings
|
|
||||||
resultBindings = append(resultBindings, &types.Binding{})
|
|
||||||
resultBindings = append(resultBindings, uniqueBindings(bindingsGlobal)...)
|
|
||||||
resultBindings = append(resultBindings, uniqueBindings(bindingsNavigation)...)
|
|
||||||
|
|
||||||
return resultBindings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We shouldn't really need to do this. We should define alternative keys for the same
|
// We shouldn't really need to do this. We should define alternative keys for the same
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
"fmt"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
@ -27,7 +28,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if item.OpensMenu {
|
if item.OpensMenu {
|
||||||
item.LabelColumns[0] = presentation.OpensMenuStyle(item.LabelColumns[0])
|
item.LabelColumns[0] = fmt.Sprintf("%s...", item.LabelColumns[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
maxColumnSize = utils.Max(maxColumnSize, len(item.LabelColumns))
|
maxColumnSize = utils.Max(maxColumnSize, len(item.LabelColumns))
|
||||||
|
@ -52,7 +52,7 @@ func GetCommitListDisplayStrings(
|
|||||||
parseEmoji bool,
|
parseEmoji bool,
|
||||||
selectedCommitSha string,
|
selectedCommitSha string,
|
||||||
startIdx int,
|
startIdx int,
|
||||||
length int,
|
endIdx int,
|
||||||
showGraph bool,
|
showGraph bool,
|
||||||
bisectInfo *git_commands.BisectInfo,
|
bisectInfo *git_commands.BisectInfo,
|
||||||
showYouAreHereLabel bool,
|
showYouAreHereLabel bool,
|
||||||
@ -68,11 +68,10 @@ func GetCommitListDisplayStrings(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
end := utils.Min(startIdx+length, len(commits))
|
|
||||||
// this is where my non-TODO commits begin
|
// this is where my non-TODO commits begin
|
||||||
rebaseOffset := utils.Min(indexOfFirstNonTODOCommit(commits), end)
|
rebaseOffset := utils.Min(indexOfFirstNonTODOCommit(commits), endIdx)
|
||||||
|
|
||||||
filteredCommits := commits[startIdx:end]
|
filteredCommits := commits[startIdx:endIdx]
|
||||||
|
|
||||||
bisectBounds := getbisectBounds(commits, bisectInfo)
|
bisectBounds := getbisectBounds(commits, bisectInfo)
|
||||||
|
|
||||||
@ -85,8 +84,8 @@ func GetCommitListDisplayStrings(
|
|||||||
|
|
||||||
pipeSets := loadPipesets(commits[rebaseOffset:])
|
pipeSets := loadPipesets(commits[rebaseOffset:])
|
||||||
pipeSetOffset := utils.Max(startIdx-rebaseOffset, 0)
|
pipeSetOffset := utils.Max(startIdx-rebaseOffset, 0)
|
||||||
graphPipeSets := pipeSets[pipeSetOffset:utils.Max(end-rebaseOffset, 0)]
|
graphPipeSets := pipeSets[pipeSetOffset:utils.Max(endIdx-rebaseOffset, 0)]
|
||||||
graphCommits := commits[graphOffset:end]
|
graphCommits := commits[graphOffset:endIdx]
|
||||||
graphLines := graph.RenderAux(
|
graphLines := graph.RenderAux(
|
||||||
graphPipeSets,
|
graphPipeSets,
|
||||||
graphCommits,
|
graphCommits,
|
||||||
|
@ -41,7 +41,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
parseEmoji bool
|
parseEmoji bool
|
||||||
selectedCommitSha string
|
selectedCommitSha string
|
||||||
startIdx int
|
startIdx int
|
||||||
length int
|
endIdx int
|
||||||
showGraph bool
|
showGraph bool
|
||||||
bisectInfo *git_commands.BisectInfo
|
bisectInfo *git_commands.BisectInfo
|
||||||
showYouAreHereLabel bool
|
showYouAreHereLabel bool
|
||||||
@ -52,7 +52,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
testName: "no commits",
|
testName: "no commits",
|
||||||
commits: []*models.Commit{},
|
commits: []*models.Commit{},
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 1,
|
endIdx: 1,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -66,7 +66,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit2", Sha: "sha2"},
|
{Name: "commit2", Sha: "sha2"},
|
||||||
},
|
},
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 2,
|
endIdx: 2,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -83,7 +83,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit2", Sha: "sha2"},
|
{Name: "commit2", Sha: "sha2"},
|
||||||
},
|
},
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 2,
|
endIdx: 2,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -110,7 +110,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
currentBranchName: "current-branch",
|
currentBranchName: "current-branch",
|
||||||
hasUpdateRefConfig: true,
|
hasUpdateRefConfig: true,
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 4,
|
endIdx: 4,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -135,7 +135,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
currentBranchName: "current-branch",
|
currentBranchName: "current-branch",
|
||||||
hasUpdateRefConfig: true,
|
hasUpdateRefConfig: true,
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 2,
|
endIdx: 2,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -158,7 +158,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
currentBranchName: "current-branch",
|
currentBranchName: "current-branch",
|
||||||
hasUpdateRefConfig: false,
|
hasUpdateRefConfig: false,
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 2,
|
endIdx: 2,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -179,7 +179,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "some-branch", CommitHash: "sha2"},
|
{Name: "some-branch", CommitHash: "sha2"},
|
||||||
},
|
},
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 3,
|
endIdx: 3,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -200,7 +200,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||||
},
|
},
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 5,
|
endIdx: 5,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -223,7 +223,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||||
},
|
},
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 5,
|
endIdx: 5,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -247,7 +247,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||||
},
|
},
|
||||||
startIdx: 1,
|
startIdx: 1,
|
||||||
length: 10,
|
endIdx: 5,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -270,7 +270,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||||
},
|
},
|
||||||
startIdx: 3,
|
startIdx: 3,
|
||||||
length: 2,
|
endIdx: 5,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -291,7 +291,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||||
},
|
},
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 2,
|
endIdx: 2,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -312,7 +312,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||||
},
|
},
|
||||||
startIdx: 4,
|
startIdx: 4,
|
||||||
length: 2,
|
endIdx: 5,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -332,7 +332,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||||
},
|
},
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 2,
|
endIdx: 2,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -351,7 +351,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}},
|
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}},
|
||||||
},
|
},
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 5,
|
endIdx: 3,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -373,7 +373,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
timeFormat: "2006-01-02",
|
timeFormat: "2006-01-02",
|
||||||
shortTimeFormat: "3:04PM",
|
shortTimeFormat: "3:04PM",
|
||||||
startIdx: 0,
|
startIdx: 0,
|
||||||
length: 2,
|
endIdx: 2,
|
||||||
showGraph: false,
|
showGraph: false,
|
||||||
bisectInfo: git_commands.NewNullBisectInfo(),
|
bisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
cherryPickedCommitShaSet: set.New[string](),
|
cherryPickedCommitShaSet: set.New[string](),
|
||||||
@ -416,13 +416,14 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
|||||||
s.parseEmoji,
|
s.parseEmoji,
|
||||||
s.selectedCommitSha,
|
s.selectedCommitSha,
|
||||||
s.startIdx,
|
s.startIdx,
|
||||||
s.length,
|
s.endIdx,
|
||||||
s.showGraph,
|
s.showGraph,
|
||||||
s.bisectInfo,
|
s.bisectInfo,
|
||||||
s.showYouAreHereLabel,
|
s.showYouAreHereLabel,
|
||||||
)
|
)
|
||||||
|
|
||||||
renderedResult := utils.RenderDisplayStrings(result, nil)
|
renderedLines, _ := utils.RenderDisplayStrings(result, nil)
|
||||||
|
renderedResult := strings.Join(renderedLines, "\n")
|
||||||
t.Logf("\n%s", renderedResult)
|
t.Logf("\n%s", renderedResult)
|
||||||
|
|
||||||
assert.EqualValues(t, s.expected, renderedResult)
|
assert.EqualValues(t, s.expected, renderedResult)
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package presentation
|
|
||||||
|
|
||||||
import "github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
||||||
|
|
||||||
func OpensMenuStyle(str string) string {
|
|
||||||
return style.FgMagenta.Sprintf("%s...", str)
|
|
||||||
}
|
|
@ -177,6 +177,11 @@ type PromptOpts struct {
|
|||||||
Mask bool
|
Mask bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MenuSection struct {
|
||||||
|
Title string
|
||||||
|
Column int // The column that this section title should be aligned with
|
||||||
|
}
|
||||||
|
|
||||||
type MenuItem struct {
|
type MenuItem struct {
|
||||||
Label string
|
Label string
|
||||||
|
|
||||||
@ -194,6 +199,14 @@ type MenuItem struct {
|
|||||||
|
|
||||||
// The tooltip will be displayed upon highlighting the menu item
|
// The tooltip will be displayed upon highlighting the menu item
|
||||||
Tooltip string
|
Tooltip string
|
||||||
|
|
||||||
|
// Can be used to group menu items into sections with headers. MenuItems
|
||||||
|
// with the same Section should be contiguous, and will automatically get a
|
||||||
|
// section header. If nil, the item is not part of a section.
|
||||||
|
// Note that pointer comparison is used to determine whether two menu items
|
||||||
|
// belong to the same section, so make sure all your items in a given
|
||||||
|
// section point to the same MenuSection instance.
|
||||||
|
Section *MenuSection
|
||||||
}
|
}
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
|
@ -124,6 +124,8 @@ type IListContext interface {
|
|||||||
GetSelectedItemId() string
|
GetSelectedItemId() string
|
||||||
|
|
||||||
GetList() IList
|
GetList() IList
|
||||||
|
ViewIndexToModelIndex(int) int
|
||||||
|
ModelIndexToViewIndex(int) int
|
||||||
|
|
||||||
FocusLine()
|
FocusLine()
|
||||||
IsListContext() // used for type switch
|
IsListContext() // used for type switch
|
||||||
|
@ -388,6 +388,9 @@ type TranslationSet struct {
|
|||||||
Panel string
|
Panel string
|
||||||
Keybindings string
|
Keybindings string
|
||||||
KeybindingsLegend string
|
KeybindingsLegend string
|
||||||
|
KeybindingsMenuSectionLocal string
|
||||||
|
KeybindingsMenuSectionGlobal string
|
||||||
|
KeybindingsMenuSectionNavigation string
|
||||||
RenameBranch string
|
RenameBranch string
|
||||||
SetUnsetUpstream string
|
SetUnsetUpstream string
|
||||||
NewGitFlowBranchPrompt string
|
NewGitFlowBranchPrompt string
|
||||||
@ -986,6 +989,9 @@ func EnglishTranslationSet() TranslationSet {
|
|||||||
ConflictsResolved: "All merge conflicts resolved. Continue?",
|
ConflictsResolved: "All merge conflicts resolved. Continue?",
|
||||||
Continue: "Continue",
|
Continue: "Continue",
|
||||||
Keybindings: "Keybindings",
|
Keybindings: "Keybindings",
|
||||||
|
KeybindingsMenuSectionLocal: "Local",
|
||||||
|
KeybindingsMenuSectionGlobal: "Global",
|
||||||
|
KeybindingsMenuSectionNavigation: "Navigation",
|
||||||
RebasingTitle: "Rebase '{{.checkedOutBranch}}' onto '{{.ref}}'",
|
RebasingTitle: "Rebase '{{.checkedOutBranch}}' onto '{{.ref}}'",
|
||||||
RebasingFromBaseCommitTitle: "Rebase '{{.checkedOutBranch}}' from marked base onto '{{.ref}}'",
|
RebasingFromBaseCommitTitle: "Rebase '{{.checkedOutBranch}}' from marked base onto '{{.ref}}'",
|
||||||
SimpleRebase: "Simple rebase",
|
SimpleRebase: "Simple rebase",
|
||||||
|
@ -456,13 +456,13 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
|
|||||||
self.IsFocused()
|
self.IsFocused()
|
||||||
|
|
||||||
view := self.getView()
|
view := self.getView()
|
||||||
|
lines := view.BufferLines()
|
||||||
|
|
||||||
var matchIndex int
|
var matchIndex int
|
||||||
|
|
||||||
self.t.assertWithRetries(func() (bool, string) {
|
self.t.assertWithRetries(func() (bool, string) {
|
||||||
matchIndex = -1
|
matchIndex = -1
|
||||||
var matches []string
|
var matches []string
|
||||||
lines := view.BufferLines()
|
|
||||||
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
|
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
ok, _ := matcher.test(line)
|
ok, _ := matcher.test(line)
|
||||||
@ -486,19 +486,38 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
if selectedLineIdx == matchIndex {
|
if selectedLineIdx == matchIndex {
|
||||||
self.SelectedLine(matcher)
|
return self.SelectedLine(matcher)
|
||||||
} else if selectedLineIdx < matchIndex {
|
|
||||||
for i := selectedLineIdx; i < matchIndex; i++ {
|
|
||||||
self.SelectNextItem()
|
|
||||||
}
|
|
||||||
self.SelectedLine(matcher)
|
|
||||||
} else {
|
|
||||||
for i := selectedLineIdx; i > matchIndex; i-- {
|
|
||||||
self.SelectPreviousItem()
|
|
||||||
}
|
|
||||||
self.SelectedLine(matcher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// At this point we can't just take the difference of selected and matched
|
||||||
|
// index and press up or down arrow this many times. The reason is that
|
||||||
|
// there might be section headers between those lines, and these will be
|
||||||
|
// skipped when pressing up or down arrow. So we must keep pressing the
|
||||||
|
// arrow key in a loop, and check after each one whether we now reached the
|
||||||
|
// target line.
|
||||||
|
var maxNumKeyPresses int
|
||||||
|
var keyPress func()
|
||||||
|
if selectedLineIdx < matchIndex {
|
||||||
|
maxNumKeyPresses = matchIndex - selectedLineIdx
|
||||||
|
keyPress = func() { self.SelectNextItem() }
|
||||||
|
} else {
|
||||||
|
maxNumKeyPresses = selectedLineIdx - matchIndex
|
||||||
|
keyPress = func() { self.SelectPreviousItem() }
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < maxNumKeyPresses; i++ {
|
||||||
|
keyPress()
|
||||||
|
idx, err := self.getSelectedLineIdx()
|
||||||
|
if err != nil {
|
||||||
|
self.t.fail(err.Error())
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
if ok, _ := matcher.test(lines[idx]); ok {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.t.fail(fmt.Sprintf("Could not navigate to item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n")))
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Alignment int
|
type Alignment int
|
||||||
@ -36,10 +37,14 @@ func WithPadding(str string, padding int, alignment Alignment) string {
|
|||||||
|
|
||||||
// defaults to left-aligning each column. If you want to set the alignment of
|
// defaults to left-aligning each column. If you want to set the alignment of
|
||||||
// each column, pass in a slice of Alignment values.
|
// each column, pass in a slice of Alignment values.
|
||||||
func RenderDisplayStrings(displayStringsArr [][]string, columnAlignments []Alignment) string {
|
// returns a list of strings that should be joined with "\n", and an array of
|
||||||
displayStringsArr = excludeBlankColumns(displayStringsArr)
|
// the column positions
|
||||||
|
func RenderDisplayStrings(displayStringsArr [][]string, columnAlignments []Alignment) ([]string, []int) {
|
||||||
|
displayStringsArr, columnAlignments, removedColumns := excludeBlankColumns(displayStringsArr, columnAlignments)
|
||||||
padWidths := getPadWidths(displayStringsArr)
|
padWidths := getPadWidths(displayStringsArr)
|
||||||
columnConfigs := make([]ColumnConfig, len(padWidths))
|
columnConfigs := make([]ColumnConfig, len(padWidths))
|
||||||
|
columnPositions := make([]int, len(padWidths)+1)
|
||||||
|
columnPositions[0] = 0
|
||||||
for i, padWidth := range padWidths {
|
for i, padWidth := range padWidths {
|
||||||
// gracefully handle when columnAlignments is shorter than padWidths
|
// gracefully handle when columnAlignments is shorter than padWidths
|
||||||
alignment := AlignLeft
|
alignment := AlignLeft
|
||||||
@ -51,16 +56,23 @@ func RenderDisplayStrings(displayStringsArr [][]string, columnAlignments []Align
|
|||||||
Width: padWidth,
|
Width: padWidth,
|
||||||
Alignment: alignment,
|
Alignment: alignment,
|
||||||
}
|
}
|
||||||
|
columnPositions[i+1] = columnPositions[i] + padWidth + 1
|
||||||
}
|
}
|
||||||
output := getPaddedDisplayStrings(displayStringsArr, columnConfigs)
|
// Add the removed columns back into columnPositions (a removed column gets
|
||||||
|
// the same position as the following column); clients should be able to rely
|
||||||
return output
|
// on them all to be there
|
||||||
|
for _, removedColumn := range removedColumns {
|
||||||
|
if removedColumn < len(columnPositions) {
|
||||||
|
columnPositions = slices.Insert(columnPositions, removedColumn, columnPositions[removedColumn])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getPaddedDisplayStrings(displayStringsArr, columnConfigs), columnPositions
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: this mutates the input slice for the sake of performance
|
// NOTE: this mutates the input slice for the sake of performance
|
||||||
func excludeBlankColumns(displayStringsArr [][]string) [][]string {
|
func excludeBlankColumns(displayStringsArr [][]string, columnAlignments []Alignment) ([][]string, []Alignment, []int) {
|
||||||
if len(displayStringsArr) == 0 {
|
if len(displayStringsArr) == 0 {
|
||||||
return displayStringsArr
|
return displayStringsArr, columnAlignments, []int{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if all rows share a blank column, we want to remove that column
|
// if all rows share a blank column, we want to remove that column
|
||||||
@ -76,26 +88,33 @@ outer:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(toRemove) == 0 {
|
if len(toRemove) == 0 {
|
||||||
return displayStringsArr
|
return displayStringsArr, columnAlignments, []int{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the columns
|
// remove the columns
|
||||||
for i, strings := range displayStringsArr {
|
for i, strings := range displayStringsArr {
|
||||||
for j := len(toRemove) - 1; j >= 0; j-- {
|
for j := len(toRemove) - 1; j >= 0; j-- {
|
||||||
strings = append(strings[:toRemove[j]], strings[toRemove[j]+1:]...)
|
strings = slices.Delete(strings, toRemove[j], toRemove[j]+1)
|
||||||
}
|
}
|
||||||
displayStringsArr[i] = strings
|
displayStringsArr[i] = strings
|
||||||
}
|
}
|
||||||
|
|
||||||
return displayStringsArr
|
for j := len(toRemove) - 1; j >= 0; j-- {
|
||||||
|
if columnAlignments != nil && toRemove[j] < len(columnAlignments) {
|
||||||
|
columnAlignments = slices.Delete(columnAlignments, toRemove[j], toRemove[j]+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayStringsArr, columnAlignments, toRemove
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPaddedDisplayStrings(stringArrays [][]string, columnConfigs []ColumnConfig) string {
|
func getPaddedDisplayStrings(stringArrays [][]string, columnConfigs []ColumnConfig) []string {
|
||||||
builder := strings.Builder{}
|
result := make([]string, 0, len(stringArrays))
|
||||||
for i, stringArray := range stringArrays {
|
for _, stringArray := range stringArrays {
|
||||||
if len(stringArray) == 0 {
|
if len(stringArray) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
builder := strings.Builder{}
|
||||||
for j, columnConfig := range columnConfigs {
|
for j, columnConfig := range columnConfigs {
|
||||||
if len(stringArray)-1 < j {
|
if len(stringArray)-1 < j {
|
||||||
continue
|
continue
|
||||||
@ -107,12 +126,9 @@ func getPaddedDisplayStrings(stringArrays [][]string, columnConfigs []ColumnConf
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
builder.WriteString(stringArray[len(columnConfigs)])
|
builder.WriteString(stringArray[len(columnConfigs)])
|
||||||
|
result = append(result, builder.String())
|
||||||
if i < len(stringArrays)-1 {
|
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return builder.String()
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPadWidths(stringArrays [][]string) []int {
|
func getPadWidths(stringArrays [][]string) []int {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -157,66 +158,90 @@ func TestTruncateWithEllipsis(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderDisplayStrings(t *testing.T) {
|
func TestRenderDisplayStrings(t *testing.T) {
|
||||||
type scenario struct {
|
type scenario struct {
|
||||||
input [][]string
|
input [][]string
|
||||||
columnAlignments []Alignment
|
columnAlignments []Alignment
|
||||||
expected string
|
expectedOutput string
|
||||||
|
expectedColumnPositions []int
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []scenario{
|
tests := []scenario{
|
||||||
{
|
{
|
||||||
input: [][]string{{""}, {""}},
|
input: [][]string{{""}, {""}},
|
||||||
columnAlignments: nil,
|
columnAlignments: nil,
|
||||||
expected: "",
|
expectedOutput: "",
|
||||||
|
expectedColumnPositions: []int{0, 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [][]string{{"a"}, {""}},
|
input: [][]string{{"a"}, {""}},
|
||||||
columnAlignments: nil,
|
columnAlignments: nil,
|
||||||
expected: "a\n",
|
expectedOutput: "a\n",
|
||||||
|
expectedColumnPositions: []int{0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [][]string{{"a"}, {"b"}},
|
input: [][]string{{"a"}, {"b"}},
|
||||||
columnAlignments: nil,
|
columnAlignments: nil,
|
||||||
expected: "a\nb",
|
expectedOutput: "a\nb",
|
||||||
|
expectedColumnPositions: []int{0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [][]string{{"a", "b"}, {"c", "d"}},
|
input: [][]string{{"a", "b"}, {"c", "d"}},
|
||||||
columnAlignments: nil,
|
columnAlignments: nil,
|
||||||
expected: "a b\nc d",
|
expectedOutput: "a b\nc d",
|
||||||
|
expectedColumnPositions: []int{0, 2},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [][]string{{"a", "", "c"}, {"d", "", "f"}},
|
input: [][]string{{"a", "", "c"}, {"d", "", "f"}},
|
||||||
columnAlignments: nil,
|
columnAlignments: nil,
|
||||||
expected: "a c\nd f",
|
expectedOutput: "a c\nd f",
|
||||||
|
expectedColumnPositions: []int{0, 2, 2},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [][]string{{"a", "", "c", ""}, {"d", "", "f", ""}},
|
input: [][]string{{"a", "", "c", ""}, {"d", "", "f", ""}},
|
||||||
columnAlignments: nil,
|
columnAlignments: nil,
|
||||||
expected: "a c\nd f",
|
expectedOutput: "a c\nd f",
|
||||||
|
expectedColumnPositions: []int{0, 2, 2},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}},
|
input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}},
|
||||||
columnAlignments: nil,
|
columnAlignments: nil,
|
||||||
expected: "abc d\ne f",
|
expectedOutput: "abc d\ne f",
|
||||||
|
expectedColumnPositions: []int{0, 4, 4},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}},
|
input: [][]string{{"", "abc", "", "", "d", "e"}, {"", "f", "", "", "g", "h"}},
|
||||||
columnAlignments: []Alignment{AlignLeft, AlignLeft}, // same as nil (default)
|
columnAlignments: nil,
|
||||||
expected: "abc d\ne f",
|
expectedOutput: "abc d e\nf g h",
|
||||||
|
expectedColumnPositions: []int{0, 0, 4, 4, 4, 6},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}},
|
input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}},
|
||||||
columnAlignments: []Alignment{AlignRight, AlignLeft},
|
columnAlignments: []Alignment{AlignLeft, AlignLeft}, // same as nil (default)
|
||||||
expected: "abc d\n e f",
|
expectedOutput: "abc d\ne f",
|
||||||
|
expectedColumnPositions: []int{0, 4, 4},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}},
|
input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}},
|
||||||
columnAlignments: []Alignment{AlignRight}, // gracefully defaults unspecified columns to left-align
|
columnAlignments: []Alignment{AlignRight, AlignLeft},
|
||||||
expected: "abc d\n e f",
|
expectedOutput: "abc d\n e f",
|
||||||
|
expectedColumnPositions: []int{0, 4, 4},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: [][]string{{"a", "", "bcd", "efg", "h"}, {"i", "", "j", "k", "l"}},
|
||||||
|
columnAlignments: []Alignment{AlignLeft, AlignLeft, AlignRight, AlignLeft},
|
||||||
|
expectedOutput: "a bcd efg h\ni j k l",
|
||||||
|
expectedColumnPositions: []int{0, 2, 2, 6, 10},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}},
|
||||||
|
columnAlignments: []Alignment{AlignRight}, // gracefully defaults unspecified columns to left-align
|
||||||
|
expectedOutput: "abc d\n e f",
|
||||||
|
expectedColumnPositions: []int{0, 4, 4},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
output := RenderDisplayStrings(test.input, test.columnAlignments)
|
output, columnPositions := RenderDisplayStrings(test.input, test.columnAlignments)
|
||||||
assert.EqualValues(t, test.expected, output)
|
assert.EqualValues(t, test.expectedOutput, strings.Join(output, "\n"))
|
||||||
|
assert.EqualValues(t, test.expectedColumnPositions, columnPositions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user