browsh/interfacer/src/browsh/input_multiline.go
Thomas Buckley-Houston 527df8bb16 Fix for non-ASCII chars generating extra chars
This was caused by misunderstanding of the finer details of strings,
runes and slices. I just changed the input_box struct to use runes and
then followed the trail of type errors until input_boxes only ever used
strings to send their text outside themselves.

Closes #93
2018-07-16 17:58:53 +08:00

237 lines
5 KiB
Go

package browsh
import (
"strings"
"unicode"
"unicode/utf8"
)
type multiLine struct {
inputBox *inputBox
index int
finalText []string
previousCharacter string
currentCharacter string
currentWordish string
currentLine string
userAddedLines []int
}
func (m *multiLine) convert() []rune {
var aRune rune
m.reset()
for m.index, aRune = range append(m.inputBox.text, ' ') {
m.previousCharacter = m.currentCharacter
m.currentCharacter = string(aRune)
if m.isWordishReady() {
m.addWordish()
}
if m.isInsideWord() {
// TODO: This sometimes causes a panic :/
m.currentWordish += m.currentCharacter
} else {
m.addWhitespace()
}
if m.isFinalCharacter() {
m.finish()
}
}
finalText := []rune(strings.Join(m.finalText, "\n"))
return finalText
}
func (m *multiLine) reset() {
m.finalText = nil
m.previousCharacter = ""
m.currentCharacter = ""
m.currentWordish = ""
m.currentLine = ""
m.userAddedLines = nil
}
func (m *multiLine) isInsideWord() bool {
return !m.isCurrentCharacterWhitespace()
}
func (m *multiLine) isPreviousCharacterWhitespace() bool {
// TODO: Not certain returning `true` for emptiness is best
if m.previousCharacter == "" {
return true
}
runes := []rune(m.previousCharacter)
if len(runes) == 0 {
return true
}
return unicode.IsSpace(runes[0])
}
func (m *multiLine) isCurrentCharacterWhitespace() bool {
if len([]rune(m.currentCharacter)) == 0 {
return false
}
return unicode.IsSpace([]rune(m.currentCharacter)[0])
}
func (m *multiLine) isWordishReady() bool {
return m.isNaturalWordEnding() || m.isProjectedLineFull()
}
func (m *multiLine) isNaturalWordEnding() bool {
return !m.isPreviousCharacterWhitespace() && m.isCurrentCharacterWhitespace()
}
func (m *multiLine) isForcedWordEnding() bool {
return m.isCurrentWordishFillingLine() && m.isProjectedLineFull()
}
func (m *multiLine) isCurrentWordishFillingLine() bool {
return m.currentWordishLength() == m.inputBox.Width
}
func (m *multiLine) currentWordishLength() int {
return utf8.RuneCountInString(m.currentWordish)
}
func (m *multiLine) currentLineLength() int {
return utf8.RuneCountInString(m.currentLine)
}
func (m *multiLine) isProjectedLineFull() bool {
return m.currentLineLength()+m.currentWordishLength() >= m.inputBox.Width
}
func (m *multiLine) addWordish() {
if m.isProjectedLineFull() {
if m.isForcedWordEnding() {
m.addLineWithTruncatedWordish()
} else {
m.addLineButWrapWord()
}
} else {
m.appendWordToLine()
}
}
func (m *multiLine) addLineWithTruncatedWordish() {
m.currentLine += m.currentWordish
m.currentWordish = ""
m.addLine()
}
func (m *multiLine) addLineButWrapWord() {
m.addLine()
if m.isNaturalWordEnding() {
m.appendWordToLine()
}
}
func (m *multiLine) noteUserAddedLineIndex() {
m.userAddedLines = append(m.userAddedLines, m.lineCount()-1)
}
func (m *multiLine) appendWordToLine() {
m.currentLine += m.currentWordish
m.currentWordish = ""
}
func (m *multiLine) addLine() {
m.finalText = append(m.finalText, m.currentLine)
m.currentLine = ""
}
func (m *multiLine) addWhitespace() {
if m.isNaturalLineBreak() {
m.addLine()
m.noteUserAddedLineIndex()
} else {
m.currentLine += string(m.currentCharacter)
}
}
func (m *multiLine) isNaturalLineBreak() bool {
return isLineBreak(m.currentCharacter)
}
func (m *multiLine) isFinalCharacter() bool {
return m.index+1 == len(m.inputBox.text)+1
}
func (m *multiLine) lineCount() int {
return len(m.finalText)
}
func (m *multiLine) finish() {
m.finalText = append(m.finalText, m.currentLine)
}
func (m *multiLine) updateCursor() {
xCursor := 0
yCursor := 0
index := 0
m.convert()
for lineIndex, line := range m.finalText {
for range line + " " {
if index == m.inputBox.textCursor {
m.inputBox.xCursor = xCursor
m.inputBox.yCursor = yCursor
}
xCursor++
index++
}
if !m.isUserAddedLine(lineIndex) {
index--
}
xCursor = 0
yCursor++
}
}
func (m *multiLine) moveYCursorBy(magnitude int) {
if !m.inputBox.isMultiLine() {
return
}
m.convert()
m.updateCursor()
lastLineIndex := m.lineCount() - 1
m.inputBox.yCursor += magnitude
if m.inputBox.yCursor < 0 {
m.inputBox.yCursor = 0
}
if m.inputBox.yCursor > lastLineIndex {
m.inputBox.yCursor = lastLineIndex
}
targetLineLength := utf8.RuneCountInString(m.finalText[m.inputBox.yCursor])
if m.inputBox.xCursor > targetLineLength-1 {
m.inputBox.xCursor = targetLineLength
if !m.isUserAddedLine(m.inputBox.yCursor) {
m.inputBox.xCursor--
}
if m.inputBox.xCursor < 0 {
m.inputBox.xCursor = 0
}
}
m.convertXYCursorToTextCursor()
}
func (m *multiLine) convertXYCursorToTextCursor() {
newTextCursor := 0
for i := 0; i < m.inputBox.yCursor; i++ {
newTextCursor += utf8.RuneCountInString(m.finalText[i])
if m.isUserAddedLine(i) {
newTextCursor++
}
}
newTextCursor += m.inputBox.xCursor
m.inputBox.textCursor = newTextCursor
m.updateCursor()
}
func (m *multiLine) isUserAddedLine(index int) bool {
for i := 0; i < len(m.userAddedLines); i++ {
if m.userAddedLines[i] == index {
return true
}
}
return false
}