1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-27 12:32:37 +02:00

Add gui.tabWidth config (#4291)

- **PR Description**

This adds a config `gui.tabWidth` that can be used to change the tab
width from the default of 4 to a different number. It affects everything
that's shown in the main view, but is probably mostly relevant for
diffs.

When using a pager, the pager needs to be configured separately for the
same tab width.

This may often be different for different repos, so it may make sense to
put it in a repo-local config file.

In some cases this may still not be flexible enough, e.g. in
multi-language projects that use different tab widths for the different
file types; but it's better than before and was easy to do...

Addresses #4290.
This commit is contained in:
Stefan Haller 2025-02-20 09:17:05 +01:00 committed by GitHub
commit 52b1c42d38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 69 additions and 25 deletions

View File

@ -47,6 +47,10 @@ gui:
# One of: 'margin' (default) | 'jump'
scrollOffBehavior: margin
# The number of spaces per tab; used for everything that's shown in the main view, but probably mostly relevant for diffs.
# Note that when using a pager, the pager has its own tab width setting, so you need to pass it separately in the pager command.
tabWidth: 4
# If true, capture mouse events.
# When mouse events are captured, it's a little harder to select text: e.g. requiring you to hold the option key when on macOS.
mouseEvents: true

2
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20250210123912-aba68ae65951
github.com/jesseduffield/gocui v0.3.1-0.20250220081214-b376cb0857ac
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e

4
go.sum
View File

@ -188,8 +188,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20250210123912-aba68ae65951 h1:7/3M0yosAM9/aLAjTfzSJWhsWjT860ZVe4T76RPwE2k=
github.com/jesseduffield/gocui v0.3.1-0.20250210123912-aba68ae65951/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/gocui v0.3.1-0.20250220081214-b376cb0857ac h1:vUNTiVEB9Bz16pTJ5kNgb/1HhnWdSA1P0GfFLUJeITI=
github.com/jesseduffield/gocui v0.3.1-0.20250220081214-b376cb0857ac/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a h1:UDeJ3EBk04bXDLOPvuqM3on8HvyJfISw0+UMqW+0a4g=
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a/go.mod h1:FSWDLKT0NQpntbDd1H3lbz51fhCVlMzy/J0S6nM727Q=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=

View File

@ -64,6 +64,9 @@ type GuiConfig struct {
ScrollOffMargin int `yaml:"scrollOffMargin"`
// One of: 'margin' (default) | 'jump'
ScrollOffBehavior string `yaml:"scrollOffBehavior"`
// The number of spaces per tab; used for everything that's shown in the main view, but probably mostly relevant for diffs.
// Note that when using a pager, the pager has its own tab width setting, so you need to pass it separately in the pager command.
TabWidth int `yaml:"tabWidth" jsonschema:"minimum=1"`
// If true, capture mouse events.
// When mouse events are captured, it's a little harder to select text: e.g. requiring you to hold the option key when on macOS.
MouseEvents bool `yaml:"mouseEvents"`
@ -693,6 +696,7 @@ func GetDefaultConfig() *UserConfig {
ScrollPastBottom: true,
ScrollOffMargin: 2,
ScrollOffBehavior: "margin",
TabWidth: 4,
MouseEvents: true,
SkipDiscardChangeWarning: false,
SkipStashWarning: false,

View File

@ -56,8 +56,8 @@ func (self *ConfirmationHelper) DeactivateConfirmationPrompt() {
self.clearConfirmationViewKeyBindings()
}
func getMessageHeight(wrap bool, editable bool, message string, width int) int {
wrappedLines, _, _ := utils.WrapViewLinesToWidth(wrap, editable, message, width)
func getMessageHeight(wrap bool, editable bool, message string, width int, tabWidth int) int {
wrappedLines, _, _ := utils.WrapViewLinesToWidth(wrap, editable, message, width, tabWidth)
return len(wrappedLines)
}
@ -265,7 +265,7 @@ func (self *ConfirmationHelper) resizeMenu(parentPopupContext types.Context) {
if selectedItem != nil {
tooltip = self.TooltipForMenuItem(selectedItem)
}
tooltipHeight := getMessageHeight(true, false, tooltip, contentWidth) + 2 // plus 2 for the frame
tooltipHeight := getMessageHeight(true, false, tooltip, contentWidth, self.c.Views().Menu.TabWidth) + 2 // plus 2 for the frame
_, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
}
@ -276,7 +276,7 @@ func (self *ConfirmationHelper) layoutMenuPrompt(contentWidth int) int {
var promptLines []string
prompt := self.c.Contexts().Menu.GetPrompt()
if len(prompt) > 0 {
promptLines, _, _ = utils.WrapViewLinesToWidth(true, false, prompt, contentWidth)
promptLines, _, _ = utils.WrapViewLinesToWidth(true, false, prompt, contentWidth, self.c.Views().Menu.TabWidth)
promptLines = append(promptLines, "")
}
self.c.Contexts().Menu.SetPromptLines(promptLines)
@ -305,17 +305,18 @@ func (self *ConfirmationHelper) resizeConfirmationPanel(parentPopupContext types
}
panelWidth := self.getPopupPanelWidth()
contentWidth := panelWidth - 2 // minus 2 for the frame
prompt := self.c.Views().Confirmation.Buffer()
confirmationView := self.c.Views().Confirmation
prompt := confirmationView.Buffer()
wrap := true
editable := self.c.Views().Confirmation.Editable
editable := confirmationView.Editable
if editable {
prompt = self.c.Views().Confirmation.TextArea.GetContent()
prompt = confirmationView.TextArea.GetContent()
wrap = false
}
panelHeight := getMessageHeight(wrap, editable, prompt, contentWidth) + suggestionsViewHeight
panelHeight := getMessageHeight(wrap, editable, prompt, contentWidth, confirmationView.TabWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight, parentPopupContext)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
_, _ = self.c.GocuiGui().SetView(confirmationView.Name(), x0, y0, x1, confirmationViewBottom, 0)
suggestionsViewTop := confirmationViewBottom + 1
_, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
@ -325,7 +326,7 @@ func (self *ConfirmationHelper) ResizeCommitMessagePanels(parentPopupContext typ
panelWidth := self.getPopupPanelWidth()
content := self.c.Views().CommitDescription.TextArea.GetContent()
summaryViewHeight := 3
panelHeight := getMessageHeight(false, true, content, panelWidth)
panelHeight := getMessageHeight(false, true, content, panelWidth, self.c.Views().CommitDescription.TabWidth)
minHeight := 7
if panelHeight < minHeight {
panelHeight = minHeight

View File

@ -323,6 +323,6 @@ func (s *State) CalculateOrigin(currentOrigin int, bufferHeight int, numLines in
func wrapPatchLines(diff string, view *gocui.View) ([]int, []int) {
_, viewLineIndices, patchLineIndices := utils.WrapViewLinesToWidth(
view.Wrap, view.Editable, strings.TrimSuffix(diff, "\n"), view.InnerWidth())
view.Wrap, view.Editable, strings.TrimSuffix(diff, "\n"), view.InnerWidth(), view.TabWidth)
return viewLineIndices, patchLineIndices
}

View File

@ -203,6 +203,7 @@ func (gui *Gui) configureViewProperties() {
for _, view := range []*gocui.View{gui.Views.Main, gui.Views.Secondary, gui.Views.Staging, gui.Views.StagingSecondary, gui.Views.PatchBuilding, gui.Views.PatchBuildingSecondary, gui.Views.MergeConflicts} {
view.CanScrollPastBottom = gui.c.UserConfig().Gui.ScrollPastBottom
view.TabWidth = gui.c.UserConfig().Gui.TabWidth
}
gui.Views.CommitDescription.FgColor = theme.GocuiDefaultTextColor

View File

@ -109,7 +109,7 @@ func ScanLinesAndTruncateWhenLongerThanBuffer(maxBufferSize int) func(data []byt
// - the line indices of the original lines, indexed by the wrapped line indices
// If wrap is false, the text is returned as is.
// This code needs to behave the same as `gocui.lineWrap` does.
func WrapViewLinesToWidth(wrap bool, editable bool, text string, width int) ([]string, []int, []int) {
func WrapViewLinesToWidth(wrap bool, editable bool, text string, width int, tabWidth int) ([]string, []int, []int) {
if !editable {
text = strings.TrimSuffix(text, "\n")
}
@ -126,14 +126,18 @@ func WrapViewLinesToWidth(wrap bool, editable bool, text string, width int) ([]s
wrappedLineIndices := make([]int, 0, len(lines))
originalLineIndices := make([]int, 0, len(lines))
if tabWidth < 1 {
tabWidth = 4
}
for originalLineIdx, line := range lines {
wrappedLineIndices = append(wrappedLineIndices, len(wrappedLines))
// convert tabs to spaces
for i := 0; i < len(line); i++ {
if line[i] == '\t' {
numSpaces := 4 - (i % 4)
line = line[:i] + " "[:numSpaces] + line[i+1:]
numSpaces := tabWidth - (i % tabWidth)
line = line[:i] + strings.Repeat(" ", numSpaces) + line[i+1:]
i += numSpaces - 1
}
}

View File

@ -173,6 +173,7 @@ func TestWrapViewLinesToWidth(t *testing.T) {
editable bool
text string
width int
tabWidth int
expectedWrappedLines []string
expectedWrappedLinesIndices []int
expectedOriginalLinesIndices []int
@ -353,14 +354,25 @@ func TestWrapViewLinesToWidth(t *testing.T) {
},
},
{
name: "Tabs",
wrap: true,
text: "\ta\tbb\tccc\tdddd\teeeee",
width: 50,
name: "Tabs, width 4",
wrap: true,
text: "\ta\tbb\tccc\tdddd\teeeee",
width: 50,
tabWidth: 4,
expectedWrappedLines: []string{
" a bb ccc dddd eeeee",
},
},
{
name: "Tabs, width 8",
wrap: true,
text: "\ta\tbb\tccc\tdddddddd\teeeee",
width: 100,
tabWidth: 8,
expectedWrappedLines: []string{
" a bb ccc dddddddd eeeee",
},
},
{
name: "Multiple lines",
wrap: true,
@ -425,7 +437,11 @@ func TestWrapViewLinesToWidth(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wrappedLines, wrappedLinesIndices, originalLinesIndices := WrapViewLinesToWidth(tt.wrap, tt.editable, tt.text, tt.width)
tabWidth := tt.tabWidth
if tabWidth == 0 {
tabWidth = 4
}
wrappedLines, wrappedLinesIndices, originalLinesIndices := WrapViewLinesToWidth(tt.wrap, tt.editable, tt.text, tt.width, tabWidth)
assert.Equal(t, tt.expectedWrappedLines, wrappedLines)
if tt.expectedWrappedLinesIndices != nil {
assert.Equal(t, tt.expectedWrappedLinesIndices, wrappedLinesIndices)
@ -436,6 +452,7 @@ func TestWrapViewLinesToWidth(t *testing.T) {
// As a sanity check, also test that gocui's line wrapping behaves the same way
view := gocui.NewView("", 0, 0, tt.width+1, 1000, gocui.OutputNormal)
view.TabWidth = tabWidth
assert.Equal(t, tt.width, view.InnerWidth())
view.Wrap = tt.wrap
view.Editable = tt.editable

View File

@ -46,6 +46,12 @@
"description": "One of: 'margin' (default) | 'jump'",
"default": "margin"
},
"tabWidth": {
"type": "integer",
"minimum": 1,
"description": "The number of spaces per tab; used for everything that's shown in the main view, but probably mostly relevant for diffs.\nNote that when using a pager, the pager has its own tab width setting, so you need to pass it separately in the pager command.",
"default": 4
},
"mouseEvents": {
"type": "boolean",
"description": "If true, capture mouse events.\nWhen mouse events are captured, it's a little harder to select text: e.g. requiring you to hold the option key when on macOS.",

View File

@ -195,6 +195,9 @@ type View struct {
// if true, the view will underline hyperlinks only when the cursor is on
// them; otherwise, they will always be underlined
UnderlineHyperLinksOnlyOnHover bool
// number of spaces per \t character, defaults to 4
TabWidth int
}
type pos struct {
@ -424,6 +427,7 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
searcher: &searcher{},
TextArea: &TextArea{},
rangeSelectStartY: -1,
TabWidth: 4,
}
v.FgColor, v.BgColor = ColorDefault, ColorDefault
@ -923,9 +927,12 @@ func (v *View) parseInput(ch rune, x int, _ int) (bool, []cell) {
return truncateLine, nil
} else if ch == '\t' {
// fill tab-sized space
const tabStop = 4
tabWidth := v.TabWidth
if tabWidth < 1 {
tabWidth = 4
}
ch = ' '
repeatCount = tabStop - (x % tabStop)
repeatCount = tabWidth - (x % tabWidth)
}
c := cell{
fgColor: v.ei.curFgColor,

2
vendor/modules.txt vendored
View File

@ -171,7 +171,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
# github.com/jesseduffield/gocui v0.3.1-0.20250210123912-aba68ae65951
# github.com/jesseduffield/gocui v0.3.1-0.20250220081214-b376cb0857ac
## explicit; go 1.12
github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a