Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion text_area.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gocui

import (
"regexp"
"strings"

"github.com/mattn/go-runewidth"
Expand Down Expand Up @@ -33,14 +34,16 @@ func AutoWrapContent(content []rune, autoWrapWidth int) ([]rune, []CursorMapping
wrappedContent := make([]rune, 0, len(content)+estimatedNumberOfSoftLineBreaks)
startOfLine := 0
indexOfLastWhitespace := -1
var footNoteMatcher footNoteMatcher

for currentPos, r := range content {
if r == '\n' {
wrappedContent = append(wrappedContent, content[startOfLine:currentPos+1]...)
startOfLine = currentPos + 1
indexOfLastWhitespace = -1
footNoteMatcher.reset()
} else {
if r == ' ' {
if r == ' ' && !footNoteMatcher.isFootNote() {
indexOfLastWhitespace = currentPos + 1
} else if currentPos-startOfLine >= autoWrapWidth && indexOfLastWhitespace >= 0 {
wrapAt := indexOfLastWhitespace
Expand All @@ -49,7 +52,9 @@ func AutoWrapContent(content []rune, autoWrapWidth int) ([]rune, []CursorMapping
cursorMapping = append(cursorMapping, CursorMapping{wrapAt, len(wrappedContent)})
startOfLine = wrapAt
indexOfLastWhitespace = -1
footNoteMatcher.reset()
}
footNoteMatcher.addRune(r)
}
}

Expand All @@ -58,6 +63,50 @@ func AutoWrapContent(content []rune, autoWrapWidth int) ([]rune, []CursorMapping
return wrappedContent, cursorMapping
}

var footNoteRe = regexp.MustCompile(`^\[\d+\]:\s*$`)

type footNoteMatcher struct {
lineStr strings.Builder
didFailToMatch bool
}

func (self *footNoteMatcher) addRune(r rune) {
if self.didFailToMatch {
// don't bother tracking the rune if we know it can't possibly match any more
return
}

if self.lineStr.Len() == 0 && r != '[' {
// fail early if the first rune of a line isn't a '['; this is mainly to avoid a (possibly
// expensive) regex match
self.didFailToMatch = true
return
}

self.lineStr.WriteRune(r)
}

func (self *footNoteMatcher) isFootNote() bool {
if self.didFailToMatch {
return false
}

if footNoteRe.MatchString(self.lineStr.String()) {
// it's a footnote, so treat spaces as non-breaking. It's important not to reset the matcher
// here, because there could be multiple spaces after a footnote.
return true
}

// no need to check again for this line
self.didFailToMatch = true
return false
}

func (self *footNoteMatcher) reset() {
self.lineStr.Reset()
self.didFailToMatch = false
}

func (self *TextArea) autoWrapContent() {
if self.AutoWrap {
self.wrappedContent, self.cursorMapping = AutoWrapContent(self.content, self.AutoWrapWidth)
Expand Down
21 changes: 21 additions & 0 deletions text_area_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,27 @@ func Test_AutoWrapContent(t *testing.T) {
expectedWrappedContent: "abc \ndefghijklmn \nopq",
expectedCursorMapping: []CursorMapping{{4, 5}, {16, 18}},
},
{
name: "don't break at space after footnote symbol",
content: "abc\n[1]: https://long/link\ndef",
autoWrapWidth: 7,
expectedWrappedContent: "abc\n[1]: https://long/link\ndef",
expectedCursorMapping: []CursorMapping{},
},
{
name: "don't break at space after footnote symbol at soft line start",
content: "abc def [1]: https://long/link\nghi",
autoWrapWidth: 7,
expectedWrappedContent: "abc def \n[1]: https://long/link\nghi",
expectedCursorMapping: []CursorMapping{{8, 9}},
},
{
name: "do break at subsequent space after footnote symbol",
content: "abc\n[1]: normal text follows\ndef",
autoWrapWidth: 7,
expectedWrappedContent: "abc\n[1]: normal \ntext \nfollows\ndef",
expectedCursorMapping: []CursorMapping{{16, 17}, {21, 23}},
},
{
name: "hard line breaks",
content: "abc\ndef\n",
Expand Down