1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-25 12:24:47 +02:00
Stefan Haller 99ad6005e8 Bump gocui
2024-03-09 10:00:07 +01:00

460 lines
11 KiB
Go

package gocui
import (
"strings"
"github.com/mattn/go-runewidth"
)
const (
WHITESPACES = " \t"
WORD_SEPARATORS = "*?_+-.[]~=/&;!#$%^(){}<>"
)
type CursorMapping struct {
Orig int
Wrapped int
}
type TextArea struct {
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) {
if self.overwrite && !self.atEnd() {
self.content[self.cursor] = r
} else {
self.content = append(
self.content[:self.cursor],
append([]rune{r}, self.content[self.cursor:]...)...,
)
}
self.autoWrapContent()
self.cursor++
}
func (self *TextArea) BackSpaceChar() {
if self.cursor == 0 {
return
}
self.content = append(self.content[:self.cursor-1], self.content[self.cursor:]...)
self.autoWrapContent()
self.cursor--
}
func (self *TextArea) DeleteChar() {
if self.atEnd() {
return
}
self.content = append(self.content[:self.cursor], self.content[self.cursor+1:]...)
self.autoWrapContent()
}
func (self *TextArea) MoveCursorLeft() {
if self.cursor == 0 {
return
}
self.cursor--
}
func (self *TextArea) MoveCursorRight() {
if self.cursor == len(self.content) {
return
}
self.cursor++
}
func (self *TextArea) MoveLeftWord() {
if self.cursor == 0 {
return
}
if self.atLineStart() {
self.cursor--
return
}
for !self.atLineStart() && strings.ContainsRune(WHITESPACES, self.content[self.cursor-1]) {
self.cursor--
}
separators := false
for !self.atLineStart() && strings.ContainsRune(WORD_SEPARATORS, self.content[self.cursor-1]) {
self.cursor--
separators = true
}
if !separators {
for !self.atLineStart() && !strings.ContainsRune(WHITESPACES+WORD_SEPARATORS, self.content[self.cursor-1]) {
self.cursor--
}
}
}
func (self *TextArea) MoveRightWord() {
if self.atEnd() {
return
}
if self.atLineEnd() {
self.cursor++
return
}
for !self.atLineEnd() && strings.ContainsRune(WHITESPACES, self.content[self.cursor]) {
self.cursor++
}
separators := false
for !self.atLineEnd() && strings.ContainsRune(WORD_SEPARATORS, self.content[self.cursor]) {
self.cursor++
separators = true
}
if !separators {
for !self.atLineEnd() && !strings.ContainsRune(WHITESPACES+WORD_SEPARATORS, self.content[self.cursor]) {
self.cursor++
}
}
}
func (self *TextArea) MoveCursorUp() {
x, y := self.GetCursorXY()
self.SetCursor2D(x, y-1)
}
func (self *TextArea) MoveCursorDown() {
x, y := self.GetCursorXY()
self.SetCursor2D(x, y+1)
}
func (self *TextArea) GetContent() string {
return string(self.wrappedContent)
}
func (self *TextArea) GetUnwrappedContent() string {
return string(self.content)
}
func (self *TextArea) ToggleOverwrite() {
self.overwrite = !self.overwrite
}
func (self *TextArea) atEnd() bool {
return self.cursor == len(self.content)
}
func (self *TextArea) DeleteToStartOfLine() {
// copying vim's logic: if you're at the start of the line, you delete the newline
// character and go to the end of the previous line
if self.atLineStart() {
if self.cursor == 0 {
return
}
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
}
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.atSoftLineStart() {
return
}
// otherwise, you delete everything up to the start of the current line, without
// deleting the newline character
newlineIndex := self.closestNewlineOnLeft()
self.cursor = newlineIndex + 1
}
func (self *TextArea) closestNewlineOnLeft() int {
wrappedCursor := self.origCursorToWrappedCursor(self.cursor)
newlineIndex := -1
for i, r := range self.wrappedContent[0:wrappedCursor] {
if r == '\n' {
newlineIndex = i
}
}
unwrappedNewlineIndex := self.wrappedCursorToOrigCursor(newlineIndex)
if unwrappedNewlineIndex >= 0 && self.content[unwrappedNewlineIndex] != '\n' {
unwrappedNewlineIndex--
}
return unwrappedNewlineIndex
}
func (self *TextArea) GoToEndOfLine() {
if self.atEnd() {
return
}
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 newline character
// doesn't really exist in the real content.
if self.cursor < len(self.content) && self.content[self.cursor] != '\n' {
self.cursor--
}
}
func (self *TextArea) closestNewlineOnRight() int {
wrappedCursor := self.origCursorToWrappedCursor(self.cursor)
for i, r := range self.wrappedContent[wrappedCursor:] {
if r == '\n' {
return self.wrappedCursorToOrigCursor(wrappedCursor + i)
}
}
return len(self.content)
}
func (self *TextArea) atLineStart() bool {
return self.cursor == 0 ||
(len(self.content) > self.cursor-1 && self.content[self.cursor-1] == '\n')
}
func (self *TextArea) atSoftLineStart() bool {
wrappedCursor := self.origCursorToWrappedCursor(self.cursor)
return wrappedCursor == 0 ||
(len(self.wrappedContent) > wrappedCursor-1 && self.wrappedContent[wrappedCursor-1] == '\n')
}
func (self *TextArea) atLineEnd() bool {
return self.atEnd() ||
(len(self.content) > self.cursor && self.content[self.cursor] == '\n')
}
func (self *TextArea) atSoftLineEnd() bool {
wrappedCursor := self.origCursorToWrappedCursor(self.cursor)
return wrappedCursor == len(self.wrappedContent) ||
(len(self.wrappedContent) > wrappedCursor+1 && self.wrappedContent[wrappedCursor+1] == '\n')
}
func (self *TextArea) BackSpaceWord() {
if self.cursor == 0 {
return
}
if self.atLineStart() {
self.BackSpaceChar()
return
}
right := self.cursor
for !self.atLineStart() && strings.ContainsRune(WHITESPACES, self.content[self.cursor-1]) {
self.cursor--
}
separators := false
for !self.atLineStart() && strings.ContainsRune(WORD_SEPARATORS, self.content[self.cursor-1]) {
self.cursor--
separators = true
}
if !separators {
for !self.atLineStart() && !strings.ContainsRune(WHITESPACES+WORD_SEPARATORS, self.content[self.cursor-1]) {
self.cursor--
}
}
self.clipboard = string(self.content[self.cursor:right])
self.content = append(self.content[:self.cursor], self.content[right:]...)
self.autoWrapContent()
}
func (self *TextArea) Yank() {
self.TypeString(self.clipboard)
}
func origCursorToWrappedCursor(origCursor int, cursorMapping []CursorMapping) int {
prevMapping := CursorMapping{0, 0}
for _, mapping := range cursorMapping {
if origCursor < mapping.Orig {
break
}
prevMapping = mapping
}
return origCursor + prevMapping.Wrapped - prevMapping.Orig
}
func (self *TextArea) origCursorToWrappedCursor(origCursor int) int {
return origCursorToWrappedCursor(origCursor, self.cursorMapping)
}
func wrappedCursorToOrigCursor(wrappedCursor int, cursorMapping []CursorMapping) int {
prevMapping := CursorMapping{0, 0}
for _, mapping := range cursorMapping {
if wrappedCursor < mapping.Wrapped {
break
}
prevMapping = mapping
}
return wrappedCursor + prevMapping.Orig - prevMapping.Wrapped
}
func (self *TextArea) wrappedCursorToOrigCursor(wrappedCursor int) int {
return wrappedCursorToOrigCursor(wrappedCursor, self.cursorMapping)
}
func (self *TextArea) GetCursorXY() (int, int) {
cursorX := 0
cursorY := 0
wrappedCursor := self.origCursorToWrappedCursor(self.cursor)
for _, r := range self.wrappedContent[0:wrappedCursor] {
if r == '\n' {
cursorY++
cursorX = 0
} else {
chWidth := runewidth.RuneWidth(r)
cursorX += chWidth
}
}
return cursorX, cursorY
}
// takes an x,y position and maps it to a 1D cursor position
func (self *TextArea) SetCursor2D(x int, y int) {
if y < 0 {
y = 0
}
if x < 0 {
x = 0
}
newCursor := 0
for _, r := range self.wrappedContent {
if x <= 0 && y == 0 {
self.cursor = self.wrappedCursorToOrigCursor(newCursor)
return
}
if r == '\n' {
if y == 0 {
self.cursor = self.wrappedCursorToOrigCursor(newCursor)
return
}
y--
} else if y == 0 {
chWidth := runewidth.RuneWidth(r)
x -= chWidth
}
newCursor++
}
// if we weren't able to run-down our arg, the user is trying to move out of
// bounds so we'll just return
if y > 0 {
return
}
self.cursor = self.wrappedCursorToOrigCursor(newCursor)
}
func (self *TextArea) Clear() {
self.content = []rune{}
self.wrappedContent = []rune{}
self.cursor = 0
}
func (self *TextArea) TypeString(str string) {
for _, r := range str {
self.TypeRune(r)
}
}