Skip to content

Commit ac4767b

Browse files
authored
Auto-wrap commit message while typing (#3173)
- **PR Description** Add new config settings `git.commit.autoWrapCommitMessage` (default true) and `git.commit.autoWrapWidth` (default 72), which allow automatic as-you-type wrapping of the commit message body to the specified width. There are occasional situations where this wrapping is in the way, for example when you need to have longer lines in the message for some reason (perhaps because you have a very wide ASCII art picture or table), and you'll have to resort to switching to the editor in that case. However, in my experience these cases are quite rare.
2 parents dc9ee18 + d1f8c45 commit ac4767b

File tree

24 files changed

+538
-55
lines changed

24 files changed

+538
-55
lines changed

docs/Config.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ git:
9292
useConfig: false
9393
commit:
9494
signOff: false
95+
autoWrapCommitMessage: true # automatic WYSIWYG wrapping of the commit message as you type
96+
autoWrapWidth: 72 # if autoWrapCommitMessage is true, the width to wrap to
9597
merging:
9698
# only applicable to unix users
9799
manualCommit: false

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ require (
99
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
1010
github.com/creack/pty v1.1.11
1111
github.com/fsmiamoto/git-todo-parser v0.0.5
12-
github.com/gdamore/tcell/v2 v2.7.3
12+
github.com/gdamore/tcell/v2 v2.7.4
1313
github.com/go-errors/errors v1.5.1
1414
github.com/gookit/color v1.4.2
1515
github.com/imdario/mergo v0.3.11
1616
github.com/integrii/flaggy v1.4.0
1717
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
1818
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
19-
github.com/jesseduffield/gocui v0.3.1-0.20240303173746-f2b0f1f68dd8
19+
github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de
2020
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
2121
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
2222
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@@ -74,8 +74,8 @@ require (
7474
github.com/xanzy/ssh-agent v0.2.1 // indirect
7575
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
7676
golang.org/x/net v0.7.0 // indirect
77-
golang.org/x/sys v0.17.0 // indirect
78-
golang.org/x/term v0.17.0 // indirect
77+
golang.org/x/sys v0.18.0 // indirect
78+
golang.org/x/term v0.18.0 // indirect
7979
golang.org/x/text v0.14.0 // indirect
8080
gopkg.in/warnings.v0 v0.1.2 // indirect
8181
)

go.sum

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ github.com/fsmiamoto/git-todo-parser v0.0.5/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLI
8989
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
9090
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
9191
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
92-
github.com/gdamore/tcell/v2 v2.7.3 h1:YLQlOj5F0hSlKy5TJvlych29+WTcJzbElnLYwx8gvdg=
93-
github.com/gdamore/tcell/v2 v2.7.3/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
92+
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
93+
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
9494
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
9595
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
9696
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -187,8 +187,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
187187
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
188188
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
189189
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
190-
github.com/jesseduffield/gocui v0.3.1-0.20240303173746-f2b0f1f68dd8 h1:DGyAjpaAnxDuKO4MEoFjifhkUV7sU6znMR9eRfjjvn0=
191-
github.com/jesseduffield/gocui v0.3.1-0.20240303173746-f2b0f1f68dd8/go.mod h1:lLLfxEGyIvvkzzpHdKkfgIVFmxqEejeACxKMVxSHLeM=
190+
github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de h1:2ww1SWgakihE8hFxZ7L3agVeGpA6qwW5vdnhFUXKMQo=
191+
github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
192192
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
193193
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
194194
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
@@ -470,13 +470,15 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
470470
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
471471
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
472472
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
473-
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
474473
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
474+
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
475+
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
475476
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
476477
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
477478
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
478-
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
479479
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
480+
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
481+
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
480482
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
481483
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
482484
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

pkg/commands/git_commands/commit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) {
142142
ToArgv()
143143

144144
message, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
145-
return strings.TrimSpace(message), err
145+
return strings.ReplaceAll(strings.TrimSpace(message), "\r\n", "\n"), err
146146
}
147147

148148
func (self *CommitCommands) GetCommitSubject(commitSha string) (string, error) {

pkg/config/user_config.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ type PagingConfig struct {
236236
type CommitConfig struct {
237237
// If true, pass '--signoff' flag when committing
238238
SignOff bool `yaml:"signOff"`
239+
// Automatic WYSIWYG wrapping of the commit message as you type
240+
AutoWrapCommitMessage bool `yaml:"autoWrapCommitMessage"`
241+
// If autoWrapCommitMessage is true, the width to wrap to
242+
AutoWrapWidth int `yaml:"autoWrapWidth"`
239243
}
240244

241245
type MergingConfig struct {
@@ -658,7 +662,9 @@ func GetDefaultConfig() *UserConfig {
658662
ExternalDiffCommand: "",
659663
},
660664
Commit: CommitConfig{
661-
SignOff: false,
665+
SignOff: false,
666+
AutoWrapCommitMessage: true,
667+
AutoWrapWidth: 72,
662668
},
663669
Merging: MergingConfig{
664670
ManualCommit: false,

pkg/gui/controllers.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,14 @@ func (gui *Gui) resetHelpersAndControllers() {
3838
getCommitDescription := func() string {
3939
return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent())
4040
}
41+
getUnwrappedCommitDescription := func() string {
42+
return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetUnwrappedContent())
43+
}
4144
commitsHelper := helpers.NewCommitsHelper(helperCommon,
4245
getCommitSummary,
4346
setCommitSummary,
4447
getCommitDescription,
48+
getUnwrappedCommitDescription,
4549
setCommitDescription,
4650
)
4751

pkg/gui/controllers/commit_message_controller.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package controllers
33
import (
44
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
55
"github.com/jesseduffield/lazygit/pkg/gui/context"
6+
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
67
"github.com/jesseduffield/lazygit/pkg/gui/types"
78
)
89

@@ -100,7 +101,7 @@ func (self *CommitMessageController) handleCommitIndexChange(value int) error {
100101
self.c.Helpers().Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage())
101102
return nil
102103
} else if currentIndex == context.NoCommitIndex {
103-
self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndDescription())
104+
self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndUnwrappedDescription())
104105
}
105106

106107
validCommit, err := self.setCommitMessageAtIndex(newIndex)
@@ -119,6 +120,9 @@ func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, e
119120
}
120121
return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
121122
}
123+
if self.c.UserConfig.Git.Commit.AutoWrapCommitMessage {
124+
commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig.Git.Commit.AutoWrapWidth)
125+
}
122126
self.c.Helpers().Commits.UpdateCommitPanelView(commitMessage)
123127
return true, nil
124128
}

pkg/gui/controllers/helpers/commits_helper.go

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66
"time"
77

8+
"github.com/jesseduffield/gocui"
89
"github.com/jesseduffield/lazygit/pkg/gui/types"
910
"github.com/samber/lo"
1011
)
@@ -16,10 +17,11 @@ type ICommitsHelper interface {
1617
type CommitsHelper struct {
1718
c *HelperCommon
1819

19-
getCommitSummary func() string
20-
setCommitSummary func(string)
21-
getCommitDescription func() string
22-
setCommitDescription func(string)
20+
getCommitSummary func() string
21+
setCommitSummary func(string)
22+
getCommitDescription func() string
23+
getUnwrappedCommitDescription func() string
24+
setCommitDescription func(string)
2325
}
2426

2527
var _ ICommitsHelper = &CommitsHelper{}
@@ -29,14 +31,16 @@ func NewCommitsHelper(
2931
getCommitSummary func() string,
3032
setCommitSummary func(string),
3133
getCommitDescription func() string,
34+
getUnwrappedCommitDescription func() string,
3235
setCommitDescription func(string),
3336
) *CommitsHelper {
3437
return &CommitsHelper{
35-
c: c,
36-
getCommitSummary: getCommitSummary,
37-
setCommitSummary: setCommitSummary,
38-
getCommitDescription: getCommitDescription,
39-
setCommitDescription: setCommitDescription,
38+
c: c,
39+
getCommitSummary: getCommitSummary,
40+
setCommitSummary: setCommitSummary,
41+
getCommitDescription: getCommitDescription,
42+
getUnwrappedCommitDescription: getUnwrappedCommitDescription,
43+
setCommitDescription: setCommitDescription,
4044
}
4145
}
4246

@@ -53,11 +57,36 @@ func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) {
5357
self.c.Contexts().CommitMessage.RenderCommitLength()
5458
}
5559

56-
func (self *CommitsHelper) JoinCommitMessageAndDescription() string {
57-
if len(self.getCommitDescription()) == 0 {
60+
func (self *CommitsHelper) JoinCommitMessageAndUnwrappedDescription() string {
61+
if len(self.getUnwrappedCommitDescription()) == 0 {
5862
return self.getCommitSummary()
5963
}
60-
return self.getCommitSummary() + "\n" + self.getCommitDescription()
64+
return self.getCommitSummary() + "\n" + self.getUnwrappedCommitDescription()
65+
}
66+
67+
func TryRemoveHardLineBreaks(message string, autoWrapWidth int) string {
68+
messageRunes := []rune(message)
69+
lastHardLineStart := 0
70+
for i, r := range messageRunes {
71+
if r == '\n' {
72+
// Try to make this a soft linebreak by turning it into a space, and
73+
// checking whether it still wraps to the same result then.
74+
messageRunes[i] = ' '
75+
76+
_, cursorMapping := gocui.AutoWrapContent(messageRunes[lastHardLineStart:], autoWrapWidth)
77+
78+
// Look at the cursorMapping to check whether auto-wrapping inserted
79+
// a line break. If it did, there will be a cursorMapping entry with
80+
// Orig pointing to the position after the inserted line break.
81+
if len(cursorMapping) == 0 || cursorMapping[0].Orig != i-lastHardLineStart+1 {
82+
// It didn't, so change it back to a newline
83+
messageRunes[i] = '\n'
84+
}
85+
lastHardLineStart = i + 1
86+
}
87+
}
88+
89+
return string(messageRunes)
6190
}
6291

6392
func (self *CommitsHelper) SwitchToEditor() error {
@@ -154,7 +183,7 @@ func (self *CommitsHelper) HandleCommitConfirm() error {
154183

155184
func (self *CommitsHelper) CloseCommitMessagePanel() error {
156185
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
157-
message := self.JoinCommitMessageAndDescription()
186+
message := self.JoinCommitMessageAndUnwrappedDescription()
158187

159188
self.c.Contexts().CommitMessage.SetPreservedMessage(message)
160189
} else {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package helpers
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestTryRemoveHardLineBreaks(t *testing.T) {
10+
scenarios := []struct {
11+
name string
12+
message string
13+
autoWrapWidth int
14+
expectedResult string
15+
}{
16+
{
17+
name: "empty",
18+
message: "",
19+
autoWrapWidth: 7,
20+
expectedResult: "",
21+
},
22+
{
23+
name: "all line breaks are needed",
24+
message: "abc\ndef\n\nxyz",
25+
autoWrapWidth: 7,
26+
expectedResult: "abc\ndef\n\nxyz",
27+
},
28+
{
29+
name: "some can be unwrapped",
30+
message: "123\nabc def\nghi jkl\nmno\n456\n",
31+
autoWrapWidth: 7,
32+
expectedResult: "123\nabc def ghi jkl mno\n456\n",
33+
},
34+
}
35+
for _, s := range scenarios {
36+
t.Run(s.name, func(t *testing.T) {
37+
actualResult := TryRemoveHardLineBreaks(s.message, s.autoWrapWidth)
38+
assert.Equal(t, s.expectedResult, actualResult)
39+
})
40+
}
41+
}

pkg/gui/controllers/local_commits_controller.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,9 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
354354
if err != nil {
355355
return self.c.Error(err)
356356
}
357-
357+
if self.c.UserConfig.Git.Commit.AutoWrapCommitMessage {
358+
commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig.Git.Commit.AutoWrapWidth)
359+
}
358360
return self.c.Helpers().Commits.OpenCommitMessagePanel(
359361
&helpers.OpenCommitMessagePanelOpts{
360362
CommitIndex: self.context().GetSelectedLineIdx(),

0 commit comments

Comments
 (0)