|
@@ -0,0 +1,488 @@
|
|
|
+package editor
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "strings"
|
|
|
+
|
|
|
+ "github.com/emirpasic/gods/lists/arraylist"
|
|
|
+ "golang.org/x/term"
|
|
|
+)
|
|
|
+
|
|
|
+type Buffer struct {
|
|
|
+ PosX int
|
|
|
+ PosY int
|
|
|
+ Buf []*arraylist.List
|
|
|
+ Prompt *Prompt
|
|
|
+ WordWrap int
|
|
|
+ ScreenWidth int
|
|
|
+ ScreenHeight int
|
|
|
+}
|
|
|
+
|
|
|
+func NewBuffer(prompt *Prompt) (*Buffer, error) {
|
|
|
+ width, height, err := term.GetSize(0)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Println("Error getting size:", err)
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ b := &Buffer{
|
|
|
+ PosX: 0,
|
|
|
+ PosY: 0,
|
|
|
+ Buf: []*arraylist.List{arraylist.New()},
|
|
|
+ Prompt: prompt,
|
|
|
+ ScreenWidth: width,
|
|
|
+ ScreenHeight: height,
|
|
|
+ }
|
|
|
+
|
|
|
+ return b, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) LineWidth() int {
|
|
|
+ return b.ScreenWidth - len(b.Prompt.Prompt)
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) findWordAtPos(line string, pos int) string {
|
|
|
+ return ""
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) addLine(row int) {
|
|
|
+ if row+1 == len(b.Buf) {
|
|
|
+ b.Buf = append(b.Buf, arraylist.New())
|
|
|
+ } else {
|
|
|
+ b.Buf = append(b.Buf, nil)
|
|
|
+ copy(b.Buf[row+2:], b.Buf[row+1:])
|
|
|
+ b.Buf[row+1] = arraylist.New()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) Add(r rune) {
|
|
|
+ switch r {
|
|
|
+ case CharCtrlJ, CharEnter:
|
|
|
+ b.addLine(b.PosY)
|
|
|
+
|
|
|
+ // handle Ctrl-J in the middle of a line
|
|
|
+ var remainingText string
|
|
|
+ if b.PosX < b.Buf[b.PosY].Size() {
|
|
|
+ fmt.Print(ClearToEOL)
|
|
|
+ remainingText = b.StringLine(b.PosX, b.PosY)
|
|
|
+ for cnt := 0; cnt < len(remainingText); cnt++ {
|
|
|
+ b.Buf[b.PosY].Remove(b.Buf[b.PosY].Size() - 1)
|
|
|
+ b.Buf[b.PosY+1].Add(rune(remainingText[cnt]))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ b.PosY++
|
|
|
+ b.PosX = 0
|
|
|
+ fmt.Printf("\n... " + ClearToEOL)
|
|
|
+ b.drawRemaining()
|
|
|
+ default:
|
|
|
+ if b.PosX == b.Buf[b.PosY].Size() {
|
|
|
+ fmt.Printf("%c", r)
|
|
|
+ b.PosX++
|
|
|
+ b.Buf[b.PosY].Add(r)
|
|
|
+ wrap, prefix, offset := b.splitLineInsert(b.PosY, b.PosX)
|
|
|
+ if wrap {
|
|
|
+ fmt.Print(CursorHide + cursorLeftN(len(prefix)+1) + ClearToEOL)
|
|
|
+ fmt.Printf("\n%s... %s%c", ClearToEOL, prefix, r)
|
|
|
+ b.PosY++
|
|
|
+ b.PosX = offset
|
|
|
+ b.ResetCursor()
|
|
|
+ b.drawRemaining()
|
|
|
+ fmt.Print(CursorShow)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ fmt.Printf("%c", r)
|
|
|
+ b.Buf[b.PosY].Insert(b.PosX, r)
|
|
|
+ b.PosX++
|
|
|
+ _, prefix, offset := b.splitLineInsert(b.PosY, b.PosX)
|
|
|
+ fmt.Print(CursorHide)
|
|
|
+ if b.PosX > b.Buf[b.PosY].Size() {
|
|
|
+ if offset > 0 {
|
|
|
+ fmt.Print(cursorLeftN(offset))
|
|
|
+ }
|
|
|
+ fmt.Print(ClearToEOL + CursorDown + CursorBOL + ClearToEOL)
|
|
|
+ fmt.Printf("... %s", prefix[:offset])
|
|
|
+ b.PosY++
|
|
|
+ b.PosX = offset
|
|
|
+ b.ResetCursor()
|
|
|
+ }
|
|
|
+ b.drawRemaining()
|
|
|
+ fmt.Print(CursorShow)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) ResetCursor() {
|
|
|
+ fmt.Print(CursorHide + CursorBOL)
|
|
|
+ fmt.Print(cursorRightN(b.PosX + len(b.Prompt.Prompt)))
|
|
|
+ fmt.Print(CursorShow)
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) splitLineInsert(posY, posX int) (bool, string, int) {
|
|
|
+ line := b.StringLine(0, posY)
|
|
|
+ screenEdge := b.LineWidth() - 5
|
|
|
+
|
|
|
+ // if the current line doesn't need to be reflowed, none of the other
|
|
|
+ // lines will either
|
|
|
+ if len(line) <= screenEdge {
|
|
|
+ return false, "", 0
|
|
|
+ }
|
|
|
+
|
|
|
+ // we know we're going to have to insert onto the next line, so
|
|
|
+ // add another line if there isn't one already
|
|
|
+ if posY == len(b.Buf)-1 {
|
|
|
+ b.Buf = append(b.Buf, arraylist.New())
|
|
|
+ }
|
|
|
+
|
|
|
+ // make a truncated version of the current line
|
|
|
+ currLine := line[:screenEdge]
|
|
|
+
|
|
|
+ // figure out where the last space in the line is
|
|
|
+ idx := strings.LastIndex(currLine, " ")
|
|
|
+
|
|
|
+ // deal with strings that don't have spaces in them
|
|
|
+ if idx == -1 {
|
|
|
+ idx = len(currLine) - 1
|
|
|
+ }
|
|
|
+
|
|
|
+ // if the next line already has text on it, we need
|
|
|
+ // to add a space to insert our new word
|
|
|
+ if b.Buf[posY+1].Size() > 0 {
|
|
|
+ b.Buf[posY+1].Insert(0, ' ')
|
|
|
+ }
|
|
|
+
|
|
|
+ // calculate the number of characters we need to remove
|
|
|
+ // from the current line to add to the next one
|
|
|
+ totalChars := len(line) - idx - 1
|
|
|
+
|
|
|
+ for cnt := 0; cnt < totalChars; cnt++ {
|
|
|
+ b.Buf[posY].Remove(b.Buf[posY].Size() - 1)
|
|
|
+ b.Buf[posY+1].Insert(0, rune(line[len(line)-1-cnt]))
|
|
|
+ }
|
|
|
+ // remove the trailing space
|
|
|
+ b.Buf[posY].Remove(b.Buf[posY].Size() - 1)
|
|
|
+
|
|
|
+ // wrap any further lines
|
|
|
+ if b.Buf[posY+1].Size() > b.LineWidth()-5 {
|
|
|
+ b.splitLineInsert(posY+1, 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ return true, currLine[idx+1:], posX - idx - 1
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) drawRemaining() {
|
|
|
+ remainingText := b.StringFromRow(b.PosY)
|
|
|
+ remainingText = remainingText[b.PosX:]
|
|
|
+
|
|
|
+ fmt.Print(CursorHide + ClearToEOL)
|
|
|
+
|
|
|
+ var rowCount int
|
|
|
+ for _, c := range remainingText {
|
|
|
+ fmt.Print(string(c))
|
|
|
+ if c == '\n' {
|
|
|
+ fmt.Print("... " + ClearToEOL)
|
|
|
+ rowCount++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if rowCount > 0 {
|
|
|
+ fmt.Print(cursorUpN(rowCount))
|
|
|
+ }
|
|
|
+ b.ResetCursor()
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) findWordBeginning(posX int) int {
|
|
|
+ for {
|
|
|
+ if posX < 0 {
|
|
|
+ return -1
|
|
|
+ }
|
|
|
+ r, ok := b.Buf[b.PosY].Get(posX)
|
|
|
+ if !ok {
|
|
|
+ return -1
|
|
|
+ } else if r.(rune) == ' ' {
|
|
|
+ return posX
|
|
|
+ }
|
|
|
+ posX--
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) Delete() {
|
|
|
+ if b.PosX < b.Buf[b.PosY].Size()-1 {
|
|
|
+ b.Buf[b.PosY].Remove(b.PosX)
|
|
|
+ b.drawRemaining()
|
|
|
+ } else {
|
|
|
+ b.joinLines()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) joinLines() {
|
|
|
+ lineLen := b.Buf[b.PosY].Size()
|
|
|
+ for cnt := 0; cnt < lineLen; cnt++ {
|
|
|
+ r, _ := b.Buf[b.PosY].Get(0)
|
|
|
+ b.Buf[b.PosY].Remove(0)
|
|
|
+ b.Buf[b.PosY-1].Add(r)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) Remove() {
|
|
|
+ if b.PosX > 0 {
|
|
|
+ fmt.Print(CursorLeft + " " + CursorLeft)
|
|
|
+ b.PosX--
|
|
|
+ b.Buf[b.PosY].Remove(b.PosX)
|
|
|
+ if b.PosX < b.Buf[b.PosY].Size() {
|
|
|
+ fmt.Print(ClearToEOL)
|
|
|
+ b.drawRemaining()
|
|
|
+ }
|
|
|
+ } else if b.PosX == 0 && b.PosY > 0 {
|
|
|
+ b.joinLines()
|
|
|
+
|
|
|
+ lastPos := b.Buf[b.PosY-1].Size()
|
|
|
+ var cnt int
|
|
|
+ b.PosX = lastPos
|
|
|
+ b.PosY--
|
|
|
+
|
|
|
+ fmt.Print(CursorHide)
|
|
|
+ for {
|
|
|
+ if b.PosX+cnt > b.LineWidth()-5 {
|
|
|
+ // the concatenated line won't fit, so find the beginning of the word
|
|
|
+ // and copy the rest of the string from there
|
|
|
+ idx := b.findWordBeginning(b.PosX)
|
|
|
+ lineLen := b.Buf[b.PosY].Size()
|
|
|
+ for offset := idx + 1; offset < lineLen; offset++ {
|
|
|
+ r, _ := b.Buf[b.PosY].Get(idx + 1)
|
|
|
+ b.Buf[b.PosY].Remove(idx + 1)
|
|
|
+ b.Buf[b.PosY+1].Add(r)
|
|
|
+ }
|
|
|
+ // remove the trailing space
|
|
|
+ b.Buf[b.PosY].Remove(idx)
|
|
|
+ fmt.Print(CursorUp + ClearToEOL)
|
|
|
+ b.PosX = 0
|
|
|
+ b.drawRemaining()
|
|
|
+ fmt.Print(CursorDown)
|
|
|
+ if idx > 0 {
|
|
|
+ if lastPos-idx-1 > 0 {
|
|
|
+ b.PosX = lastPos - idx - 1
|
|
|
+ b.ResetCursor()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ b.PosY++
|
|
|
+ break
|
|
|
+ }
|
|
|
+ r, ok := b.Buf[b.PosY].Get(b.PosX + cnt)
|
|
|
+ if !ok {
|
|
|
+ // found the end of the string
|
|
|
+ fmt.Print(CursorUp + cursorRightN(b.PosX) + ClearToEOL)
|
|
|
+ b.drawRemaining()
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if r == ' ' {
|
|
|
+ // found the end of the word
|
|
|
+ lineLen := b.Buf[b.PosY].Size()
|
|
|
+ for offset := b.PosX + cnt + 1; offset < lineLen; offset++ {
|
|
|
+ r, _ := b.Buf[b.PosY].Get(b.PosX + cnt + 1)
|
|
|
+ b.Buf[b.PosY].Remove(b.PosX + cnt + 1)
|
|
|
+ b.Buf[b.PosY+1].Add(r)
|
|
|
+ }
|
|
|
+ fmt.Print(CursorUp + cursorRightN(b.PosX) + ClearToEOL)
|
|
|
+ b.drawRemaining()
|
|
|
+ break
|
|
|
+ }
|
|
|
+ cnt++
|
|
|
+ }
|
|
|
+ fmt.Print(CursorShow)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) RemoveBefore() {
|
|
|
+ for {
|
|
|
+ if b.PosX == 0 && b.PosY == 0 {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ b.Remove()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) RemoveWordBefore() {
|
|
|
+ if b.PosX > 0 || b.PosY > 0 {
|
|
|
+ var foundNonspace bool
|
|
|
+ for {
|
|
|
+ xPos := b.PosX
|
|
|
+ yPos := b.PosY
|
|
|
+
|
|
|
+ v, _ := b.Buf[yPos].Get(xPos - 1)
|
|
|
+ if v == ' ' {
|
|
|
+ if !foundNonspace {
|
|
|
+ b.Remove()
|
|
|
+ } else {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ foundNonspace = true
|
|
|
+ b.Remove()
|
|
|
+ }
|
|
|
+
|
|
|
+ if xPos == 0 && yPos == 0 {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) StringLine(x, y int) string {
|
|
|
+ if y >= len(b.Buf) {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+ var output string
|
|
|
+
|
|
|
+ for cnt := x; cnt < b.Buf[y].Size(); cnt++ {
|
|
|
+ r, _ := b.Buf[y].Get(cnt)
|
|
|
+ output += string(r.(rune))
|
|
|
+ }
|
|
|
+ return output
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) String() string {
|
|
|
+ return b.StringFromRow(0)
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) StringFromRow(n int) string {
|
|
|
+ var output []string
|
|
|
+ for _, row := range b.Buf[n:] {
|
|
|
+ var currLine string
|
|
|
+ for cnt := 0; cnt < row.Size(); cnt++ {
|
|
|
+ r, _ := row.Get(cnt)
|
|
|
+ currLine += string(r.(rune))
|
|
|
+ }
|
|
|
+ currLine = strings.TrimRight(currLine, " ")
|
|
|
+ output = append(output, currLine)
|
|
|
+ }
|
|
|
+ return strings.Join(output, "\n")
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) cursorUp() {
|
|
|
+ fmt.Print(CursorUp)
|
|
|
+ b.ResetCursor()
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) cursorDown() {
|
|
|
+ fmt.Print(CursorDown)
|
|
|
+ b.ResetCursor()
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) MoveUp() {
|
|
|
+ if b.PosY > 0 {
|
|
|
+ b.PosY--
|
|
|
+ if b.Buf[b.PosY].Size() < b.PosX {
|
|
|
+ b.PosX = b.Buf[b.PosY].Size()
|
|
|
+ }
|
|
|
+ b.cursorUp()
|
|
|
+ } else {
|
|
|
+ fmt.Print("\a")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) MoveDown() {
|
|
|
+ if b.PosY < len(b.Buf)-1 {
|
|
|
+ b.PosY++
|
|
|
+ if b.Buf[b.PosY].Size() < b.PosX {
|
|
|
+ b.PosX = b.Buf[b.PosY].Size()
|
|
|
+ }
|
|
|
+ b.cursorDown()
|
|
|
+ } else {
|
|
|
+ fmt.Print("\a")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) MoveLeft() {
|
|
|
+ if b.PosX > 0 {
|
|
|
+ b.PosX--
|
|
|
+ fmt.Print(CursorLeft)
|
|
|
+ } else if b.PosY > 0 {
|
|
|
+ b.PosX = b.Buf[b.PosY-1].Size()
|
|
|
+ b.PosY--
|
|
|
+ b.cursorUp()
|
|
|
+ } else if b.PosX == 0 && b.PosY == 0 {
|
|
|
+ fmt.Print("\a")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) MoveRight() {
|
|
|
+ if b.PosX < b.Buf[b.PosY].Size() {
|
|
|
+ b.PosX++
|
|
|
+ fmt.Print(CursorRight)
|
|
|
+ } else if b.PosY < len(b.Buf)-1 {
|
|
|
+ b.PosY++
|
|
|
+ b.PosX = 0
|
|
|
+ b.cursorDown()
|
|
|
+ } else {
|
|
|
+ fmt.Print("\a")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) MoveToBOL() {
|
|
|
+ if b.PosX > 0 {
|
|
|
+ b.PosX = 0
|
|
|
+ b.ResetCursor()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) MoveToEOL() {
|
|
|
+ if b.PosX < b.Buf[b.PosY].Size() {
|
|
|
+ b.PosX = b.Buf[b.PosY].Size()
|
|
|
+ b.ResetCursor()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) MoveToEnd() {
|
|
|
+ fmt.Print(CursorHide)
|
|
|
+ yDiff := len(b.Buf)-1 - b.PosY
|
|
|
+ if yDiff > 0 {
|
|
|
+ fmt.Print(cursorDownN(yDiff))
|
|
|
+ }
|
|
|
+ b.PosY = len(b.Buf)-1
|
|
|
+ b.MoveToEOL()
|
|
|
+ fmt.Print(CursorShow)
|
|
|
+}
|
|
|
+
|
|
|
+func cursorLeftN(n int) string {
|
|
|
+ return fmt.Sprintf(CursorLeftN, n)
|
|
|
+}
|
|
|
+
|
|
|
+func cursorRightN(n int) string {
|
|
|
+ return fmt.Sprintf(CursorRightN, n)
|
|
|
+}
|
|
|
+
|
|
|
+func cursorUpN(n int) string {
|
|
|
+ return fmt.Sprintf(CursorUpN, n)
|
|
|
+}
|
|
|
+
|
|
|
+func cursorDownN(n int) string {
|
|
|
+ return fmt.Sprintf(CursorDownN, n)
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) ClearScreen() {
|
|
|
+ fmt.Printf(CursorHide + ClearScreen + CursorReset + b.Prompt.Prompt)
|
|
|
+ if b.IsEmpty() {
|
|
|
+ ph := b.Prompt.Placeholder
|
|
|
+ fmt.Printf(ColorGrey + ph + cursorLeftN(len(ph)) + ColorDefault)
|
|
|
+ } else {
|
|
|
+ currPosX := b.PosX
|
|
|
+ currPosY := b.PosY
|
|
|
+ b.PosX = 0
|
|
|
+ b.PosY = 0
|
|
|
+ b.drawRemaining()
|
|
|
+ b.PosX = currPosX
|
|
|
+ b.PosY = currPosY
|
|
|
+ fmt.Print(CursorReset + cursorRightN(len(b.Prompt.Prompt)))
|
|
|
+ if b.PosY > 0 {
|
|
|
+ fmt.Print(cursorDownN(b.PosY))
|
|
|
+ }
|
|
|
+ if b.PosX > 0 {
|
|
|
+ fmt.Print(cursorRightN(b.PosX))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fmt.Print(CursorShow)
|
|
|
+}
|
|
|
+
|
|
|
+func (b *Buffer) IsEmpty() bool {
|
|
|
+ return len(b.Buf) == 1 && b.Buf[0].Empty()
|
|
|
+}
|