summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStefan Haller <stefan@haller-berlin.de>2024-03-09 10:06:29 +0100
committerGitHub <noreply@github.com>2024-03-09 10:06:29 +0100
commitac4767bb2a2fe7e2b342c856566ac06db0c21137 (patch)
treefe50974220d30f70e6d661d66373ea50efa4c020
parentdc9ee186f4d56f5a971012063ae27d2dd3ce276a (diff)
parentd1f8c450995ace6b1b0e352ba10dcc0653da4030 (diff)
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.
-rw-r--r--docs/Config.md2
-rw-r--r--go.mod8
-rw-r--r--go.sum14
-rw-r--r--pkg/commands/git_commands/commit.go2
-rw-r--r--pkg/config/user_config.go8
-rw-r--r--pkg/gui/controllers.go4
-rw-r--r--pkg/gui/controllers/commit_message_controller.go6
-rw-r--r--pkg/gui/controllers/helpers/commits_helper.go55
-rw-r--r--pkg/gui/controllers/helpers/commits_helper_test.go41
-rw-r--r--pkg/gui/controllers/local_commits_controller.go4
-rw-r--r--pkg/gui/views.go2
-rw-r--r--pkg/integration/components/commit_description_panel_driver.go10
-rw-r--r--pkg/integration/tests/commit/auto_wrap_message.go59
-rw-r--r--pkg/integration/tests/test_list.go1
-rw-r--r--schema/config.json10
-rw-r--r--vendor/github.com/gdamore/tcell/v2/console_win.go6
-rw-r--r--vendor/github.com/jesseduffield/gocui/text_area.go168
-rw-r--r--vendor/golang.org/x/sys/unix/aliases.go2
-rw-r--r--vendor/golang.org/x/sys/unix/syscall_darwin_libSystem.go2
-rw-r--r--vendor/golang.org/x/sys/unix/syscall_freebsd.go12
-rw-r--r--vendor/golang.org/x/sys/unix/syscall_linux.go99
-rw-r--r--vendor/golang.org/x/sys/unix/zsyscall_linux.go10
-rw-r--r--vendor/golang.org/x/sys/unix/ztypes_linux.go60
-rw-r--r--vendor/modules.txt8
24 files changed, 538 insertions, 55 deletions
diff --git a/docs/Config.md b/docs/Config.md
index 5b686fc19..4b26c5f84 100644
--- a/docs/Config.md
+++ b/docs/Config.md
@@ -92,6 +92,8 @@ git:
useConfig: false
commit:
signOff: false
+ autoWrapCommitMessage: true # automatic WYSIWYG wrapping of the commit message as you type
+ autoWrapWidth: 72 # if autoWrapCommitMessage is true, the width to wrap to
merging:
# only applicable to unix users
manualCommit: false
diff --git a/go.mod b/go.mod
index bc039bc57..58ad19c3c 100644
--- a/go.mod
+++ b/go.mod
@@ -9,14 +9,14 @@ require (
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/fsmiamoto/git-todo-parser v0.0.5
- github.com/gdamore/tcell/v2 v2.7.3
+ github.com/gdamore/tcell/v2 v2.7.4
github.com/go-errors/errors v1.5.1
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
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.20240303173746-f2b0f1f68dd8
+ github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@@ -74,8 +74,8 @@ require (
github.com/xanzy/ssh-agent v0.2.1 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/net v0.7.0 // indirect
- golang.org/x/sys v0.17.0 // indirect
- golang.org/x/term v0.17.0 // indirect
+ golang.org/x/sys v0.18.0 // indirect
+ golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
diff --git a/go.sum b/go.sum
index 84e171721..0e728ff52 100644
--- a/go.sum
+++ b/go.sum
@@ -89,8 +89,8 @@ github.com/fsmiamoto/git-todo-parser v0.0.5/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLI
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
-github.com/gdamore/tcell/v2 v2.7.3 h1:YLQlOj5F0hSlKy5TJvlych29+WTcJzbElnLYwx8gvdg=
-github.com/gdamore/tcell/v2 v2.7.3/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
+github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
+github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
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
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.20240303173746-f2b0f1f68dd8 h1:DGyAjpaAnxDuKO4MEoFjifhkUV7sU6znMR9eRfjjvn0=
-github.com/jesseduffield/gocui v0.3.1-0.20240303173746-f2b0f1f68dd8/go.mod h1:lLLfxEGyIvvkzzpHdKkfgIVFmxqEejeACxKMVxSHLeM=
+github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de h1:2ww1SWgakihE8hFxZ7L3agVeGpA6qwW5vdnhFUXKMQo=
+github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
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
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go
index 12806dafc..960fab811 100644
--- a/pkg/commands/git_commands/commit.go
+++ b/pkg/commands/git_commands/commit.go
@@ -142,7 +142,7 @@ func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) {
ToArgv()
message, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
- return strings.TrimSpace(message), err
+ return strings.ReplaceAll(strings.TrimSpace(message), "\r\n", "\n"), err
}
func (self *CommitCommands) GetCommitSubject(commitSha string) (string, error) {
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index 9cb758259..6a4efe78a 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -236,6 +236,10 @@ type PagingConfig struct {
type CommitConfig struct {
// If true, pass '--signoff' flag when committing
SignOff bool `yaml:"signOff"`
+ // Automatic WYSIWYG wrapping of the commit message as you type
+ AutoWrapCommitMessage bool `yaml:"autoWrapCommitMessage"`
+ // If autoWrapCommitMessage is true, the width to wrap to
+ AutoWrapWidth int `yaml:"autoWrapWidth"`
}
type MergingConfig struct {
@@ -658,7 +662,9 @@ func GetDefaultConfig() *UserConfig {
ExternalDiffCommand: "",
},
Commit: CommitConfig{
- SignOff: false,
+ SignOff: false,
+ AutoWrapCommitMessage: true,
+ AutoWrapWidth: 72,
},
Merging: MergingConfig{
ManualCommit: false,
diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go
index 30caae930..1dbf9b7d7 100644
--- a/pkg/gui/controllers.go
+++ b/pkg/gui/controllers.go
@@ -38,10 +38,14 @@ func (gui *Gui) resetHelpersAndControllers() {
getCommitDescription := func() string {
return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent())
}
+ getUnwrappedCommitDescription := func() string {
+ return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetUnwrappedContent())
+ }
commitsHelper := helpers.NewCommitsHelper(helperCommon,
getCommitSummary,
setCommitSummary,
getCommitDescription,
+ getUnwrappedCommitDescription,
setCommitDescription,
)
diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go
index c52a8038f..756b240e6 100644
--- a/pkg/gui/controllers/commit_message_controller.go
+++ b/pkg/gui/controllers/commit_message_controller.go
@@ -3,6 +3,7 @@ package controllers
import (
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/context"
+ "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@@ -100,7 +101,7 @@ func (self *CommitMessageController) handleCommitIndexChange(value int) error {
self.c.Helpers().Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage())
return nil
} else if currentIndex == context.NoCommitIndex {
- self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndDescription())
+ self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndUnwrappedDescription())
}
validCommit, err := self.setCommitMessageAtIndex(newIndex)
@@ -119,6 +120,9 @@ func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, e
}
return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
}
+ if self.c.UserConfig.Git.Commit.AutoWrapCommitMessage {
+ commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig.Git.Commit.AutoWrapWidth)
+ }
self.c.Helpers().Commits.UpdateCommitPanelView(commitMessage)
return true, nil
}
diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go
index 8691518cd..0801d5742 100644
--- a/pkg/gui/controllers/helpers/commits_helper.go
+++ b/pkg/gui/controllers/helpers/commits_helper.go
@@ -5,6 +5,7 @@ import (
"strings"
"time"
+ "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
@@ -16,10 +17,11 @@ type ICommitsHelper interface {
type CommitsHelper struct {
c *HelperCommon
- getCommitSummary func() string
- setCommitSummary func(string)
- getCommitDescription func() string
- setCommitDescription func(string)
+ getCommitSummary func() string
+ setCommitSummary func(string)
+ getCommitDescription func() string
+ getUnwrappedCommitDescription func() string
+ setCommitDescription func(string)
}
var _ ICommitsHelper = &CommitsHelper{}
@@ -29,14 +31,16 @@ func NewCommitsHelper(
getCommitSummary func() string,
setCommitSummary func(string),
getCommitDescription func() string,
+ getUnwrappedCommitDescription func() string,
setCommitDescription func(string),
) *CommitsHelper {
return &CommitsHelper{
- c: c,
- getCommitSummary: getCommitSummary,
- setCommitSummary: setCommitSummary,
- getCommitDescription: getCommitDescription,
- setCommitDescription: setCommitDescription,
+ c: c,
+ getCommitSummary: getCommitSummary,
+ setCommitSummary: setCommitSummary,
+ getCommitDescription: getCommitDescription,
+ getUnwrappedCommitDescription: getUnwrappedCommitDescription,
+ setCommitDescription: setCommitDescription,
}
}
@@ -53,11 +57,36 @@ func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) {
self.c.Contexts().CommitMessage.RenderCommitLength()
}
-func (self *CommitsHelper) JoinCommitMessageAndDescription() string {
- if len(self.getCommitDescription()) == 0 {
+func (self *CommitsHelper) JoinCommitMessageAndUnwrappedDescription() string {
+ if len(self.getUnwrappedCommitDescription()) == 0 {
return self.getCommitSummary()
}
- return self.getCommitSummary() + "\n" + self.getCommitDescription()
+ return self.getCommitSummary() + "\n" + self.getUnwrappedCommitDescription()
+}
+
+func TryRemoveHardLineBreaks(message string, autoWrapWidth int) string {
+ messageRunes := []rune(message)
+ lastHardLineStart := 0
+ for i, r := range messageRunes {
+ if r == '\n' {
+ // Try to make this a soft linebreak by turning it into a space, and
+ // checking whether it still wraps to the same result then.
+ messageRunes[i] = ' '
+
+ _, cursorMapping := gocui.AutoWrapContent(messageRunes[lastHardLineStart:], autoWrapWidth)
+
+ // Look at the cursorMapping to check whether auto-wrapping inserted
+ // a line break. If it did, there will be a cursorMapping entry with
+ // Orig pointing to the position after the inserted line break.
+ if len(cursorMapping) == 0 || cursorMapping[0].Orig != i-lastHardLineStart+1 {
+ // It didn't, so change it back to a newline
+ messageRunes[i] = '\n'
+ }
+ lastHardLineStart = i + 1
+ }
+ }
+
+ return string(messageRunes)
}
func (self *CommitsHelper) SwitchToEditor() error {
@@ -154,7 +183,7 @@ func (self *CommitsHelper) HandleCommitConfirm() error {
func (self *CommitsHelper) CloseCommitMessagePanel() error {
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
- message := self.JoinCommitMessageAndDescription()
+ message := self.JoinCommitMessageAndUnwrappedDescription()
self.c.Contexts().CommitMessage.SetPreservedMessage(message)
} else {
diff --git a/pkg/gui/controllers/helpers/commits_helper_test.go b/pkg/gui/controllers/helpers/commits_helper_test.go
new file mode 100644
index 000000000..6197c3916
--- /dev/null
+++ b/pkg/gui/controllers/helpers/commits_helper_test.go
@@ -0,0 +1,41 @@
+package helpers
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTryRemoveHardLineBreaks(t *testing.T) {
+ scenarios := []struct {
+ name string
+ message string
+ autoWrapWidth int
+ expectedResult string
+ }{
+ {
+ name: "empty",
+ message: "",
+ autoWrapWidth: 7,
+ expectedResult: "",
+ },
+ {
+ name: "all line breaks are needed",
+ message: "abc\ndef\n\nxyz",
+ autoWrapWidth: 7,
+ expectedResult: "abc\ndef\n\nxyz",
+ },
+ {
+ name: "some can be unwrapped",
+ message: "123\nabc def\nghi jkl\nmno\n456\n",
+ autoWrapWidth: 7,
+ expectedResult: "123\nabc def ghi jkl mno\n456\n",
+ },
+ }
+ for _, s := range scenarios {
+ t.Run(s.name, func(t *testing.T) {
+ actualResult := TryRemoveHardLineBreaks(s.message, s.autoWrapWidth)
+ assert.Equal(t, s.expectedResult, actualResult)
+ })
+ }
+}
diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go
index ef6b5be80..8c99d7586 100644
--- a/pkg/gui/controllers/local_commits_controller.go
+++ b/pkg/gui/controllers/local_commits_controller.go
@@ -354,7 +354,9 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
if err != nil {
return self.c.Error(err)
}
-
+ if self.c.UserConfig.Git.Commit.AutoWrapCommitMessage {
+ commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig.Git.Commit.AutoWrapWidth)
+ }
return self.c.Helpers().Commits.OpenCommitMessagePanel(
&helpers.OpenCommitMessagePanelOpts{
CommitIndex: self.context().GetSelectedLineIdx(),
diff --git a/pkg/gui/views.go b/pkg/gui/views.go
index 13caa9c7f..9fd775764 100644
--- a/pkg/gui/views.go
+++ b/pkg/gui/views.go
@@ -168,6 +168,8 @@ func (gui *Gui) createAllViews() error {
gui.Views.CommitDescription.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitDescription.Editable = true
gui.Views.CommitDescription.Editor = gocui.EditorFunc(gui.commitDescriptionEditor)
+ gui.Views.CommitDescription.TextArea.AutoWrap = gui.c.UserConfig.Git.Commit.AutoWrapCommitMessage
+ gui.Views.CommitDescription.TextArea.AutoWrapWidth = gui.c.UserConfig.Git.Commit.AutoWrapWidth
gui.Views.Confirmation.Visible = false
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.promptEditor)
diff --git a/pkg/integration/components/commit_description_panel_driver.go b/pkg/integration/components/commit_description_panel_driver.go
index 0c4b2cfbb..eddc53533 100644
--- a/pkg/integration/components/commit_description_panel_driver.go
+++ b/pkg/integration/components/commit_description_panel_driver.go
@@ -31,6 +31,16 @@ func (self *CommitDescriptionPanelDriver) AddNewline() *CommitDescriptionPanelDr
return self
}
+func (self *CommitDescriptionPanelDriver) GoToBeginning() *CommitDescriptionPanelDriver {
+ numLines := len(self.getViewDriver().getView().BufferLines())
+ for i := 0; i < numLines; i++ {
+ self.t.pressFast("<up>")
+ }
+
+ self.t.pressFast("<c-a>")
+ return self
+}
+
func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver {
self.getViewDriver().Title(expected)
diff --git a/pkg/integration/tests/commit/auto_wrap_message.go b/pkg/integration/tests/commit/auto_wrap_message.go
new file mode 100644
index 000000000..477bfae62
--- /dev/null
+++ b/pkg/integration/tests/commit/auto_wrap_message.go
@@ -0,0 +1,59 @@
+package commit
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var AutoWrapMessage = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Commit, and test how the commit message body is auto-wrapped",
+ ExtraCmdArgs: []string{},
+ Skip: false,
+ SetupConfig: func(config *config.AppConfig) {
+ // Use a ridiculously small width so that we don't have to use so much test data
+ config.UserConfig.Git.Commit.AutoWrapWidth = 20
+ },
+ SetupRepo: func(shell *Shell) {
+ shell.CreateFile("file", "file content")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Commits().
+ IsEmpty()
+
+ t.Views().Files().
+ IsFocused().
+ PressPrimaryAction(). // stage file
+ Press(keys.Files.CommitChanges)
+
+ t.ExpectPopup().CommitMessagePanel().
+ Type("subject").
+ SwitchToDescription().
+ Type("Lorem ipsum dolor sit amet, consectetur adipiscing elit.").
+ // See how it automatically inserted line feeds to wrap the text:
+ Content(Equals("Lorem ipsum dolor \nsit amet, \nconsectetur \nadipiscing elit.")).
+ SwitchToSummary().
+ Confirm()
+
+ t.Views().Commits().
+ Lines(
+ Contains("subject"),
+ ).
+ Focus().
+ Tap(func() {
+ t.Views().Main().Content(Contains(
+ "subject\n \n Lorem ipsum dolor\n sit amet,\n consectetur\n adipiscing elit."))
+ }).
+ Press(keys.Commits.RenameCommit)
+
+ // Test that when rewording, the hard line breaks are turned back into
+ // soft ones, so that we can insert text at the beginning and have the
+ // paragraph reflow nicely.
+ t.ExpectPopup().CommitMessagePanel().
+ InitialText(Equals("subject")).
+ SwitchToDescription().
+ Content(Equals("Lorem ipsum dolor \nsit amet, \nconsectetur \nadipiscing elit.")).
+ GoToBeginning().
+ Type("More text. ").
+ Content(Equals("More text. Lorem \nipsum dolor sit \namet, consectetur \nadipiscing elit."))
+ },
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 402a40acf..8c2891d47 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -65,6 +65,7 @@ var tests = []*components.IntegrationTest{
cherry_pick.CherryPickRange,
commit.AddCoAuthor,
commit.Amend,
+ commit.AutoWrapMessage,
commit.Commit,
commit.CommitMultiline,
commit.CommitSwitchToEditor,
diff --git a/schema/config.json b/schema/config.json
index b95381302..44d8e0cb4 100644
--- a/schema/config.json
+++ b/schema/config.json
@@ -405,6 +405,16 @@
"signOff": {
"type": "boolean",
"description": "If true, pass '--signoff' flag when committing"
+ },
+ "autoWrapCommitMessage": {
+ "type": "boolean",
+ "description": "Automatic WYSIWYG wrapping of the commit message as you type",
+ "default": true
+ },
+ "autoWrapWidth": {
+ "type": "integer",
+ "description": "If autoWrapCommitMessage is true, the width to wrap to",
+ "default": 72
}
},
"additionalProperties": false,
diff --git a/vendor/github.com/gdamore/tcell/v2/console_win.go b/vendor/github.com/gdamore/tcell/v2/console_win.go
index 92ae4e5c2..e2652509e 100644
--- a/vendor/github.com/gdamore/tcell/v2/console_win.go
+++ b/vendor/github.com/gdamore/tcell/v2/console_win.go
@@ -341,12 +341,12 @@ func (s *cScreen) disengage() {
}
} else if !s.disableAlt {
s.clearScreen(StyleDefault, s.vten)
+ s.setCursorPos(0, 0, false)
}
+ s.setCursorInfo(&s.ocursor)
+ s.setBufferSize(int(s.oscreen.size.x), int(s.oscreen.size.y))
s.setInMode(s.oimode)
s.setOutMode(s.oomode)
- s.setBufferSize(int(s.oscreen.size.x), int(s.oscreen.size.y))
- s.setCursorPos(0, 0, false)
- s.setCursorInfo(&s.ocursor)
_, _, _ = procSetConsoleTextAttribute.Call(
uintptr(s.out),
uintptr(s.mapStyle(StyleDefault)))
diff --git a/vendor/github.com/jesseduffield/gocui/text_area.go b/vendor/github.com/jesseduffield/gocui/text_area.go
index ca4809107..ebd6a6bfa 100644
--- a/vendor/github.com/jesseduffield/gocui/text_area.go
+++ b/vendor/github.com/jesseduffield/gocui/text_area.go
@@ -11,11 +11,59 @@ const (
WORD_SEPARATORS = "*?_+-.[]~=/&;!#$%^(){}<>"
)
+type CursorMapping struct {
+ Orig int
+ Wrapped int
+}
+
type TextArea struct {
- content []rune
- cursor int
- overwrite bool
- clipboard string
+ content []rune
+ wrappedContent []rune
+ cursorMapping []CursorMapping
+ cursor int
+ overwrite bool
+ clipboard string
+ AutoWrap bool
+ AutoWrapWidth int
+}
+
+func AutoWrapContent(content []rune, autoWrapWidth int) ([]rune, []CursorMapping) {
+ estimatedNumberOfSoftLineBreaks := len(content) / autoWrapWidth
+ cursorMapping := make([]CursorMapping, 0, estimatedNumberOfSoftLineBreaks)
+ wrappedContent := make([]rune, 0, len(content)+estimatedNumberOfSoftLineBreaks)
+ startOfLine := 0
+ indexOfLastWhitespace := -1
+
+ for currentPos, r := range content {
+ if r == '\n' {
+ wrappedContent = append(wrappedContent, content[startOfLine:currentPos+1]...)
+ startOfLine = currentPos + 1
+ indexOfLastWhitespace = -1
+ } else {
+ if r == ' ' {
+ indexOfLastWhitespace = currentPos + 1
+ } else if currentPos-startOfLine >= autoWrapWidth && indexOfLastWhitespace >= 0 {
+ wrapAt := indexOfLastWhitespace
+ wrappedContent = append(wrappedContent, content[startOfLine:wrapAt]...)
+ wrappedContent = append(wrappedContent, '\n')
+ cursorMapping = append(cursorMapping, CursorMapping{wrapAt, len(wrappedContent)})
+ startOfLine = wrapAt
+ indexOfLastWhitespace = -1
+ }
+ }
+ }
+
+ wrappedContent = append(wrappedContent, content[startOfLine:]...)
+
+ return wrappedContent, cursorMapping
+}
+
+func (self *TextArea) autoWrapContent() {
+ if self.AutoWrap {
+ self.wrappedContent, self.cursorMapping = AutoWrapContent(self.content, self.AutoWrapWidth)
+ } else {
+ self.wrappedContent, self.cursorMapping = self.content, []CursorMapping{}
+ }
}
func (self *TextArea) TypeRune(r rune) {
@@ -27,6 +75,7 @@ func (self *TextArea) TypeRune(r rune) {
append([]rune{r}, self.content[self.cursor:]...)...,
)
}
+ self.autoWrapContent()
self.cursor++
}
@@ -37,6 +86,7 @@ func (self *TextArea) BackSpaceChar() {
}
self.content = append(self.content[:self.cursor-1], self.content[self.cursor:]...)
+ self.autoWrapContent()
self.cursor--
}
@@ -46,6 +96,7 @@ func (self *TextArea) DeleteChar() {
}
self.content = append(self.content[:self.cursor], self.content[self.cursor+1:]...)
+ self.autoWrapContent()
}
func (self *TextArea) MoveCursorLeft() {
@@ -123,6 +174,10 @@ func (self *TextArea) MoveCursorDown() {
}
func (self *TextArea) GetContent() string {
+ return string(self.wrappedContent)
+}
+
+func (self *TextArea) GetUnwrappedContent() string {
return string(self.content)
}
@@ -144,14 +199,24 @@ func (self *TextArea) DeleteToStartOfLine() {
self.content = append(self.content[:self.cursor-1], self.content[self.cursor:]...)
self.cursor--
+ self.autoWrapContent()
return
}
+ // otherwise, if we're at a soft line start, skip left past the soft line
+ // break, so we'll end up deleting the previous line. This seems like the
+ // only reasonable behavior in this case, as you can't delete just the soft
+ // line break.
+ if self.atSoftLineStart() {
+ self.cursor--
+ }
+
// otherwise, you delete everything up to the start of the current line, without
// deleting the newline character
newlineIndex := self.closestNewlineOnLeft()
self.clipboard = string(self.content[newlineIndex+1 : self.cursor])
self.content = append(self.content[:newlineIndex+1], self.content[self.cursor:]...)
+ self.autoWrapContent()
self.cursor = newlineIndex + 1
}
@@ -159,18 +224,30 @@ func (self *TextArea) DeleteToEndOfLine() {
if self.atEnd() {
return
}
+
+ // if we're at the end of the line, delete just the newline character
if self.atLineEnd() {
self.content = append(self.content[:self.cursor], self.content[self.cursor+1:]...)
+ self.autoWrapContent()
return
}
+ // otherwise, if we're at a soft line end, skip right past the soft line
+ // break, so we'll end up deleting the next line. This seems like the
+ // only reasonable behavior in this case, as you can't delete just the soft
+ // line break.
+ if self.atSoftLineEnd() {
+ self.cursor++
+ }
+
lineEndIndex := self.closestNewlineOnRight()
self.clipboard = string(self.content[self.cursor:lineEndIndex])
self.content = append(self.content[:self.cursor], self.content[lineEndIndex:]...)
+ self.autoWrapContent()
}
func (self *TextArea) GoToStartOfLine() {
- if self.atLineStart() {
+ if self.atSoftLineStart() {
return
}
@@ -181,15 +258,21 @@ func (self *TextArea) GoToStartOfLine() {
}
func (self *TextArea) closestNewlineOnLeft() int {
+ wrappedCursor := self.origCursorToWrappedCursor(self.cursor)
+
newlineIndex := -1
- for i, r := range self.content[0:self.cursor] {
+ for i, r := range self.wrappedContent[0:wrappedCursor] {
if r == '\n' {
newlineIndex = i
}
}
- return newlineIndex
+ unwrappedNewlineIndex := self.wrappedCursorToOrigCursor(newlineIndex)
+ if unwrappedNewlineIndex >= 0 && self.content[unwrappedNewlineIndex] != '\n' {
+ unwrappedNewlineIndex--
+ }
+ return unwrappedNewlineIndex
}
func (self *TextArea) GoToEndOfLine() {
@@ -198,12 +281,22 @@ func (self *TextArea) GoToEndOfLine() {
}
self.cursor = self.closestNewlineOnRight()
+
+ // If the end of line is a soft line break, we need to move left by one so
+ // that we end up at the last whitespace before the line break. Otherwise
+ // we'd be at the start of the next line, since the