Skip to content

Commit 8029be2

Browse files
committed
Exclude commit trailers from line wrapping
Line-wrapping a commit trailer that contains a very long email address results in a broken trailer (trailers do support being split into several lines, but this requires continuation lines to start with a space, like in RFC-822 message headers). To avoid this, simply never wrap commit trailers. It's a bit questionable that we hard-code this behavior in TextArea, which is meant to be a general-purpose widget; but we know we only use it in lazygit's commit message panel, so don't bother making this configurable for now. Benchmark time increases from 150μs to 165μs on my machine, which is still more than acceptable.
1 parent d1fc791 commit 8029be2

File tree

2 files changed

+77
-3
lines changed

2 files changed

+77
-3
lines changed

text_area.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
7171
currentLineWidth := 0
7272
indexOfLastWhitespace := -1
7373
var footNoteMatcher footNoteMatcher
74+
var trailerMatcher trailerMatcher
7475

7576
cells := stringToTextAreaCells(content)
7677
y := 0
@@ -94,9 +95,10 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
9495
indexOfLastWhitespace = -1
9596
currentLineWidth = 0
9697
footNoteMatcher.reset()
98+
trailerMatcher.reset()
9799
} else {
98100
currentLineWidth += c.width
99-
if c.char == " " && !footNoteMatcher.isFootNote() {
101+
if c.char == " " && !footNoteMatcher.isFootNote() && !trailerMatcher.isTrailer() {
100102
indexOfLastWhitespace = currentPos + 1
101103
} else if autoWrapWidth > 0 && currentLineWidth > autoWrapWidth && indexOfLastWhitespace >= 0 {
102104
wrapAt := indexOfLastWhitespace
@@ -112,9 +114,11 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
112114
currentLineWidth += c1.width
113115
}
114116
footNoteMatcher.reset()
117+
trailerMatcher.reset()
115118
}
116119

117120
footNoteMatcher.addCharacter(c.char)
121+
trailerMatcher.addCharacter(c.char)
118122
}
119123
}
120124

@@ -167,6 +171,76 @@ func (self *footNoteMatcher) reset() {
167171
self.didFailToMatch = false
168172
}
169173

174+
var supportedTrailers = []string{
175+
"Signed-off-by:",
176+
"Co-authored-by:",
177+
}
178+
179+
type trailerMatcher struct {
180+
lineStr strings.Builder
181+
didFailToMatch bool
182+
didMatch bool
183+
}
184+
185+
func (self *trailerMatcher) addCharacter(chr string) {
186+
if self.didFailToMatch || self.didMatch {
187+
return
188+
}
189+
190+
if len(chr) != 1 {
191+
// Trailers are all ASCII, so if we get a non-ASCII UTF-8 character (or even a multi-rune
192+
// grapheme cluster), we can fail early.
193+
self.didFailToMatch = true
194+
return
195+
}
196+
197+
if self.lineStr.Len() == 0 {
198+
// If this is the first character, see if it could possibly match any supported trailer; if
199+
// not, we can fail early and stop tracking further characters for this line.
200+
if !anyOf(supportedTrailers, func(trailer string) bool { return trailer[0] == chr[0] }) {
201+
self.didFailToMatch = true
202+
return
203+
}
204+
}
205+
206+
self.lineStr.WriteString(chr)
207+
}
208+
209+
func (self *trailerMatcher) isTrailer() bool {
210+
if self.didFailToMatch {
211+
return false
212+
}
213+
214+
if self.didMatch {
215+
return true
216+
}
217+
218+
line := self.lineStr.String()
219+
if anyOf(supportedTrailers, func(trailer string) bool { return line == trailer }) {
220+
self.didMatch = true
221+
return true
222+
}
223+
224+
self.didFailToMatch = true
225+
return false
226+
}
227+
228+
func (self *trailerMatcher) reset() {
229+
self.lineStr.Reset()
230+
self.didFailToMatch = false
231+
self.didMatch = false
232+
}
233+
234+
func anyOf(strings []string, predicate func(s string) bool) bool {
235+
for _, s := range strings {
236+
if predicate(s) {
237+
return true
238+
}
239+
}
240+
241+
return false
242+
}
243+
170244
func (self *TextArea) updateCells() {
171245
width := self.AutoWrapWidth
172246
if !self.AutoWrap {

text_area_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -948,8 +948,8 @@ func Test_AutoWrapContent(t *testing.T) {
948948
name: "don't break at space after trailer",
949949
content: "abc\nSigned-off-by: John Doe <john@doe.com>\nCo-authored-by: Jane Smith <jane@smith.com>\n",
950950
autoWrapWidth: 10,
951-
expectedWrappedContent: "abc\nSigned-off-by: \nJohn Doe \n<john@doe.com>\nCo-authored-by: \nJane Smith \n<jane@smith.com>\n",
952-
expectedSoftLineBreaks: []int{19, 28, 59, 70},
951+
expectedWrappedContent: "abc\nSigned-off-by: John Doe <john@doe.com>\nCo-authored-by: Jane Smith <jane@smith.com>\n",
952+
expectedSoftLineBreaks: []int{},
953953
},
954954
{
955955
name: "do break at space after trailer if there is no space after the colon",

0 commit comments

Comments
 (0)