Completly revamped the editor.
* Now properly handles cursor on empty newlines
* Selection behaviour now consistent and correct
* doesn't use weird string magic anymore, instead it uses
two string buffers, one for view and for data.
MODIFY
go.mod
go.mod
@@ -5,6 +5,7 @@ go 1.12
require (
github.com/Bios-Marcel/discordemojimap v1.0.0
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103003547-0699c2def4b0
+ github.com/Bios-Marcel/femto v0.0.0-20191108211110-a645bf3d3602
github.com/Bios-Marcel/goclipimg v0.0.0-20190417192721-b58a8831f27d
github.com/Bios-Marcel/shortnotforlong v1.0.0
github.com/Bios-Marcel/tview v0.0.0-20191024171520-41147a2f8cf9
@@ -15,7 +16,7 @@ require (
github.com/godbus/dbus v4.1.0+incompatible // indirect
github.com/google/go-github/v28 v28.1.1
github.com/gopherjs/gopherwasm v1.1.0 // indirect
- github.com/mattn/go-runewidth v0.0.4
+ github.com/mattn/go-runewidth v0.0.5
github.com/mdp/qrterminal/v3 v3.0.0
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/pkg/errors v0.8.1

MODIFY
go.sum
go.sum
@@ -18,6 +18,8 @@ github.com/Bios-Marcel/discordgo v0.20.4-0.20191103001117-f2fe84a74c31 h1:P6AYRO
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103001117-f2fe84a74c31/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103003547-0699c2def4b0 h1:wnLeeQhagkXoTJQ681D25HBxVSefFO7KMB0txBJMqTE=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103003547-0699c2def4b0/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
+github.com/Bios-Marcel/femto v0.0.0-20191108211110-a645bf3d3602 h1:6HucH+TXFs25D2KsAkVtO0TWr5i9/7iN1B3NlbpuPdU=
+github.com/Bios-Marcel/femto v0.0.0-20191108211110-a645bf3d3602/go.mod h1:1WS6wqbby5poRu+cBtJJzXrjqrqaFHz9mDE/1+pksAA=
github.com/Bios-Marcel/goclipimg v0.0.0-20190417192721-b58a8831f27d h1:vfrX8l3fHuaP7gRgEc7mX/lHVTlfQcQeaIdSPmT6Ej0=
github.com/Bios-Marcel/goclipimg v0.0.0-20190417192721-b58a8831f27d/go.mod h1:u7z9t086HoIbA/uuoA2KcRDhKS47DRYCDZ39aegTR4c=
github.com/Bios-Marcel/shortnotforlong v1.0.0 h1:K4JJ5U3+D8LXoAiH0QbfMujWuqKo+NAEZKuQ2RHPqqE=
@@ -84,6 +86,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.5 h1:jrGtp51JOKTWgvLFzfG6OtZOJcK2sEnzc/U+zw7TtbA=
+github.com/mattn/go-runewidth v0.0.5/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c=
github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ=
github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ=
@@ -108,10 +112,13 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+github.com/zyedidia/micro v1.4.1 h1:OuszISyaEPK/8xxkklkh7dp2ragvKDEnr4RyHfJcQdo=
+github.com/zyedidia/micro v1.4.1/go.mod h1:/wcvhlXPvvvb6v176yUQE4gNzr+Erwz4pWfx7PU/cuE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190403202508-8e1b8d32e692 h1:GRhHqDOgeDr6QDTtq9gn2O4iKvm5dsbfqD/TXb0KLX0=
golang.org/x/crypto v0.0.0-20190403202508-8e1b8d32e692/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
@@ -136,9 +143,13 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 h1:MZF6J7CV6s/h0HBkfqebrYfKCVEo5iN+wzE4QhV3Evo=
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2/go.mod h1:s1Sn2yZos05Qfs7NKt867Xe18emOmtsO3eAKbDaon0o=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

MODIFY
ui/editor.go
ui/editor.go
package ui
import (
- "fmt"
- "regexp"
- "strings"
-
"github.com/Bios-Marcel/cordless/shortcuts"
"github.com/Bios-Marcel/cordless/ui/tviewutil"
-
+ "github.com/Bios-Marcel/femto"
"github.com/Bios-Marcel/tview"
"github.com/atotto/clipboard"
"github.com/gdamore/tcell"
)
-var (
- // Temporary solution, so not every function has to handle the selection
- // character placement.
- multiSelectionCharWithSelectionToLeftPattern = regexp.MustCompile(selectionChar + "*" + regexp.QuoteMeta(selRegion) + selectionChar + "*" + regexp.QuoteMeta(endRegion))
-)
-
-const (
- selectionChar = string('\u205F')
- emptyText = "[\"selection\"]\u205F[\"\"]"
- leftRegion = "[\"left\"]"
- rightRegion = "[\"right\"]"
- selRegion = "[\"selection\"]"
- endRegion = "[\"\"]"
-)
-
// Editor is a simple component that wraps tview.TextView in order to gove the
// user minimal text edit functionality.
type Editor struct {
internalTextView *tview.TextView
+ buffer *femto.Buffer
+ tempBuffer *femto.Buffer
inputCapture func(event *tcell.EventKey) *tcell.EventKey
mentionShowHandler func(namePart string)
@@ -42,209 +25,177 @@ type Editor struct {
currentMentionEndIndex int
}
-func (e *Editor) ExpandSelectionToLeft(left, right, selection []rune) {
- if len(left) > 0 {
- newText := leftRegion + string(left[:len(left)-1]) + selRegion
-
- currentSelection := string(selection)
- if currentSelection == selectionChar {
- currentSelection = ""
- }
+func (editor *Editor) applyBuffer() {
+ bufferToString := editor.buffer.String()
+ selectionStart := editor.buffer.Cursor.CurSelection[0]
+ selectionEnd := editor.buffer.Cursor.CurSelection[1]
- newText = newText + string(left[len(left)-1]) + currentSelection + rightRegion + string(right) + endRegion
- e.internalTextView.SetText(newText)
- }
-}
+ editor.tempBuffer.Replace(editor.tempBuffer.Start(), editor.tempBuffer.End(), bufferToString)
+ editor.tempBuffer.Cursor.GotoLoc(editor.buffer.Cursor.Loc)
+ editor.tempBuffer.Cursor.SetSelectionStart(selectionStart)
+ editor.tempBuffer.Cursor.SetSelectionEnd(selectionEnd)
-func (e *Editor) ExpandSelectionToRight(left, right, selection []rune) {
- newText := leftRegion + string(left)
- if len(right) > 0 {
- newText = newText + selRegion + string(selection) + string(right[0]) + rightRegion + string(right[1:])
+ if editor.buffer.Cursor.HasSelection() {
+ editor.tempBuffer.Insert(selectionEnd, "[\"\"]")
+ editor.tempBuffer.Insert(selectionStart, "[\"selection\"]")
+ } else {
+ cursorToRight := editor.tempBuffer.Cursor.Loc.Move(1, editor.tempBuffer)
+ cursorText := editor.buffer.Substr(editor.tempBuffer.Cursor.Loc, cursorToRight)
+ //Little workaround, since tviews textview actually trims whitespace on
+ //newline, therefore selection would be gone
+ if cursorText == "\n" || cursorText == "" {
+ editor.tempBuffer.Insert(editor.tempBuffer.Cursor.Loc, "[\"selection\"] [\"\"]\x04")
} else {
- endsWithSelectionChar := strings.HasSuffix(string(selection), selectionChar)
- newText = newText + selRegion + string(selection)
- if !endsWithSelectionChar {
- newText = newText + selectionChar
+ editor.tempBuffer.Insert(cursorToRight, "[\"\"]")
+ editor.tempBuffer.Insert(editor.tempBuffer.Cursor.Loc, "[\"selection\"]")
}
}
- newText = newText + endRegion
- e.setAndFixText(newText)
-}
-
-func (e *Editor) MoveCursorLeft(left, right, selection []rune) {
- var newText string
- if len(left) > 0 {
- newText = leftRegion + string(left[:len(left)-1]) + selRegion
-
- currentSelection := string(selection)
- if currentSelection == selectionChar {
- currentSelection = ""
+ editor.internalTextView.SetText(editor.tempBuffer.String())
}
- newText = newText + string(left[len(left)-1]) + rightRegion + currentSelection + string(right) + endRegion
-
- e.internalTextView.SetText(newText)
- } else if len(selection) > 0 {
- if len(right) > 0 {
- newText = selRegion + string(selection[0]) + rightRegion + string(selection[1:]) + string(right) + endRegion
+func (editor *Editor) MoveCursorLeft() {
+ if editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Cursor.GotoLoc(editor.buffer.Cursor.CurSelection[0])
} else {
- newText = selRegion + string(selection[0]) + rightRegion + string(selection[1:]) + endRegion
- }
- e.setAndFixText(newText)
+ editor.buffer.Cursor.Left()
}
+
+ editor.buffer.Cursor.ResetSelection()
+ editor.applyBuffer()
}
-func (e *Editor) MoveCursorRight(left, right, selection []rune) {
- newText := leftRegion + string(left)
- if len(right) > 0 {
- newText = newText + string(selection) + selRegion + string(right[0]) + rightRegion + string(right[1:])
- } else {
- endsWithSelectionChar := strings.HasSuffix(string(selection), selectionChar)
- if !endsWithSelectionChar {
- newText = newText + string(selection) + selRegion + selectionChar
+func (editor *Editor) MoveCursorRight() {
+ if editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Cursor.GotoLoc(editor.buffer.Cursor.CurSelection[1])
} else {
- newText = newText + string(selection[:len(selection)-1]) + selRegion + selectionChar
- }
+ editor.buffer.Cursor.Right()
}
- newText = newText + endRegion
- e.setAndFixText(newText)
+ editor.buffer.Cursor.ResetSelection()
+ editor.applyBuffer()
}
-func (e *Editor) MoveCursorToIndex(text string, index int) {
- // Bound the index to the string length
- if index < 0 {
- index = 0
- } else if index >= len(text) {
- index = len(text) - 1
+func (editor *Editor) SelectionToLeft() {
+ editor.selectLeft(false)
}
- left := string(text[:index])
- var right string
- if index == len(text)-1 {
- right = selectionChar
- e.setAndFixText(leftRegion + left + selRegion + right + endRegion)
- } else {
- right = string(text[index:])
- e.setAndFixText(leftRegion + left + selRegion + string(right[0]) + rightRegion + right[1:] + endRegion)
- }
+func (editor *Editor) SelectWordLeft() {
+ editor.selectLeft(true)
}
-func (e *Editor) SelectWordLeft(left, right, selection []rune) {
- if len(left) > 0 {
- selectionFrom := 0
- for i := len(left) - 2; /*Skip space left to selection*/ i >= 0; i-- {
- if left[i] == ' ' || left[i] == '\n' {
- selectionFrom = i
- break
- }
- }
+func (editor *Editor) selectLeft(word bool) {
+ oldCursor := editor.buffer.Cursor.Loc
+ selectionStart := editor.buffer.Cursor.CurSelection[0]
+ selectionEnd := editor.buffer.Cursor.CurSelection[1]
- var newText string
- if selectionFrom != 0 {
- newText = leftRegion + string(left[:selectionFrom+1]) + selRegion + string(left[selectionFrom+1:]) + string(string(selection)) + rightRegion + string(right) + endRegion
+ if word {
+ editor.buffer.Cursor.WordLeft()
} else {
- newText = selRegion + string(left) + string(string(selection)) + rightRegion + string(right) + endRegion
- }
- e.setAndFixText(newText)
+ editor.buffer.Cursor.Left()
+ }
+ newCursor := editor.buffer.Cursor.Loc
+ if !editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Cursor.SetSelectionStart(newCursor)
+ editor.buffer.Cursor.SetSelectionEnd(oldCursor)
+ } else if oldCursor.GreaterEqual(selectionStart) {
+ if newCursor.GreaterEqual(selectionStart) {
+ editor.buffer.Cursor.SetSelectionStart(selectionStart)
+ editor.buffer.Cursor.SetSelectionEnd(newCursor)
+ } else {
+ editor.buffer.Cursor.SetSelectionStart(newCursor)
+ editor.buffer.Cursor.SetSelectionEnd(selectionEnd)
}
+ } else {
+ editor.buffer.Cursor.SetSelectionStart(newCursor)
+ editor.buffer.Cursor.SetSelectionEnd(selectionEnd)
}
-func (e *Editor) SelectWordRight(left, right, selection []rune) {
- if len(right) > 0 {
- selectionFrom := len(right) - 1
- for i := 1; /*Skip space right to selection*/ i < len(right)-1; i++ {
- if right[i] == ' ' || right[i] == '\n' {
- selectionFrom = i
- break
- }
+ editor.applyBuffer()
}
- var newText string
- if selectionFrom != len(right)-1 {
- newText = leftRegion + string(left) + selRegion + string(string(selection)) + string(right[:selectionFrom]) + rightRegion + string(right[selectionFrom:]) + endRegion
- } else {
- newText = leftRegion + string(left) + selRegion + string(string(selection)) + string(right) + endRegion
- }
- e.setAndFixText(newText)
- }
+func (editor *Editor) SelectionToRight() {
+ editor.selectRight(false)
}
-func (e *Editor) MoveCursorWordLeft(left, right, selection []rune) {
- if len(left) > 0 {
- selectionAt := 0
- for i := len(left) - 2; /*Skip space left to selection*/ i >= 0; i-- {
- if left[i] == ' ' || left[i] == '\n' {
- selectionAt = i
- break
- }
+func (editor *Editor) SelectWordRight() {
+ editor.selectRight(true)
}
- var newText string
- if selectionAt != 0 {
- newText = leftRegion + string(left[:selectionAt]) + selRegion + string(left[selectionAt]) + rightRegion + string(left[selectionAt+1:]) + string(string(selection)) + string(right) + endRegion
+func (editor *Editor) selectRight(word bool) {
+ oldCursor := editor.buffer.Cursor.Loc
+ selectionStart := editor.buffer.Cursor.CurSelection[0]
+ selectionEnd := editor.buffer.Cursor.CurSelection[1]
+
+ if word {
+ editor.buffer.Cursor.WordRight()
} else {
- if len(left) > 1 {
- newText = selRegion + string(left[0]) + rightRegion + string(left[1:]) + string(selection) + string(right) + endRegion
+ editor.buffer.Cursor.Right()
+ }
+ newCursor := editor.buffer.Cursor.Loc
+ if !editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Cursor.SetSelectionStart(oldCursor)
+ editor.buffer.Cursor.SetSelectionEnd(newCursor)
+ } else if newCursor.LessThan(selectionEnd) {
+ editor.buffer.Cursor.SetSelectionStart(newCursor)
+ editor.buffer.Cursor.SetSelectionEnd(selectionEnd)
} else {
- newText = selRegion + string(left[0]) + rightRegion + string(selection) + string(right) + endRegion
- }
- }
- e.setAndFixText(newText)
+ editor.buffer.Cursor.SetSelectionStart(selectionStart)
+ editor.buffer.Cursor.SetSelectionEnd(newCursor)
}
+
+ editor.applyBuffer()
}
-func (e *Editor) MoveCursorWordRight(left, right, selection []rune) {
- if len(right) > 0 {
- selectionAt := len(right) - 1
- for i := 1; /*Skip space right to selection*/ i < len(right)-1; i++ {
- if right[i] == ' ' || right[i] == '\n' {
- selectionAt = i
- break
+func (editor *Editor) MoveCursorWordLeft() {
+ if editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Cursor.GotoLoc(editor.buffer.Cursor.CurSelection[0])
}
+ editor.buffer.Cursor.WordLeft()
+ editor.buffer.Cursor.ResetSelection()
+ editor.applyBuffer()
}
- var newText string
- if selectionAt != len(right)-1 {
- newText = leftRegion + string(left) + string(string(selection)) + string(right[:selectionAt]) + selRegion + string(right[selectionAt]) + rightRegion + string(right[selectionAt+1:]) + endRegion
- } else {
- newText = leftRegion + string(left) + string(selection) + string(right) + selRegion + selectionChar + endRegion
+func (editor *Editor) MoveCursorWordRight() {
+ if editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Cursor.GotoLoc(editor.buffer.Cursor.CurSelection[1])
}
- e.setAndFixText(newText)
+ editor.buffer.Cursor.WordRight()
+ editor.buffer.Cursor.ResetSelection()
+ editor.applyBuffer()
}
+
+func (editor *Editor) SelectAll() {
+ start := editor.buffer.Start()
+ editor.buffer.Cursor.SetSelectionStart(start)
+ end := editor.buffer.End()
+ editor.buffer.Cursor.SetSelectionEnd(end)
+ editor.applyBuffer()
}
-func (e *Editor) SelectAll(left, right, selection []rune) {
- if len(left) > 0 || len(right) > 0 {
- e.setAndFixText(selRegion + string(left) + string(selection) + string(right) + endRegion)
+func (editor *Editor) Backspace() {
+ if editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Remove(editor.buffer.Cursor.CurSelection[0], editor.buffer.Cursor.CurSelection[1])
+ editor.applyBuffer()
+ } else if editor.buffer.Cursor.Loc.X > 0 || editor.buffer.Cursor.Loc.Y > 0 {
+ editor.buffer.Remove(editor.buffer.Cursor.Loc.Move(-1, editor.buffer), editor.buffer.Cursor.Loc)
+ editor.applyBuffer()
}
}
-func (e *Editor) DeleteRight(left, right, selection []rune) {
- if len(selection) >= 1 && strings.HasSuffix(string(selection), selectionChar) {
- e.setAndFixText(leftRegion + string(left) + selRegion + selectionChar + endRegion)
- } else if string(selection) != selectionChar {
- var newText string
- newText = leftRegion + string(left) + selRegion
- if len(right) == 0 {
- newText = newText + selectionChar
+func (editor *Editor) DeleteRight() {
+ if editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Remove(editor.buffer.Cursor.CurSelection[0], editor.buffer.Cursor.CurSelection[1])
} else {
- newText = newText + string(right[0])
+ editor.buffer.Remove(editor.buffer.Cursor.Loc, editor.buffer.Cursor.Loc.Move(1, editor.buffer))
}
- if len(right) > 1 {
- newText = newText + rightRegion + string(right[1:])
- }
-
- newText = newText + endRegion
- e.setAndFixText(newText)
- }
+ editor.applyBuffer()
}
-func (e *Editor) Paste(left, right, selection []rune, event *tcell.EventKey) {
- if e.inputCapture != nil {
- result := e.inputCapture(event)
+func (editor *Editor) Paste(event *tcell.EventKey) {
+ if editor.inputCapture != nil {
+ result := editor.inputCapture(event)
if result == nil {
//Early exit, as even has been handled.
return
@@ -253,56 +204,34 @@ func (e *Editor) Paste(left, right, selection []rune, event *tcell.EventKey) {
clipBoardContent, clipError := clipboard.ReadAll()
if clipError == nil {
- var newText string
- if string(selection) == selectionChar {
- newText = leftRegion + string(left) + clipBoardContent + selRegion + string(selection)
+ if editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Replace(editor.buffer.Cursor.CurSelection[0], editor.buffer.Cursor.CurSelection[1], clipBoardContent)
} else {
- newText = leftRegion + string(left) + clipBoardContent
- if len(selection) == 1 {
- newText = newText + selRegion + string(selection) + rightRegion + string(right)
- } else {
- newText = newText + selRegion
- if len(right) == 0 {
- newText = newText + selectionChar
- } else if len(right) == 0 {
- newText = newText + string(right[0])
- } else {
- newText = newText + string(right[0]) + rightRegion + string(right[1:])
- }
- }
+ editor.buffer.Insert(editor.buffer.Cursor.Loc, clipBoardContent)
}
- e.setAndFixText(newText + endRegion)
- e.triggerHeightRequestIfNeccessary()
+ editor.applyBuffer()
}
}
-func (e *Editor) InsertCharacter(left, right, selection []rune, character rune) {
- if len(right) == 0 {
- if len(selection) == 1 {
- if string(selection) == selectionChar {
- e.setAndFixText(fmt.Sprintf("[\"left\"]%s%c[\"\"][\"selection\"]%s[\"\"]", string(left), character, string(selectionChar)))
+func (editor *Editor) InsertCharacter(character rune) {
+ selectionEnd := editor.buffer.Cursor.CurSelection[1]
+ selectionStart := editor.buffer.Cursor.CurSelection[0]
+ if editor.buffer.Cursor.HasSelection() {
+ editor.buffer.Replace(selectionStart, selectionEnd, string(character))
} else {
- e.setAndFixText(fmt.Sprintf("[\"left\"]%s%c[\"\"][\"selection\"]%s[\"\"]", string(left), character, string(selection)))
- }
- } else {
- e.setAndFixText(fmt.Sprintf("[\"left\"]%s%c[\"\"][\"selection\"]%s[\"\"]", string(left), character, string(selectionChar)))
- }
- } else {
- if len(selection) == 1 {
- e.setAndFixText(fmt.Sprintf("[\"left\"]%s%c[\"\"][\"selection\"]%s[\"\"][\"right\"]%s[\"\"]",
- string(left), character, string(selection), string(right)))
- } else {
- e.setAndFixText(fmt.Sprintf("[\"left\"]%s%c[\"\"][\"selection\"]%s[\"\"][\"right\"]%s[\"\"]",
- string(left), character, string(right[0]), string(right[1:])))
- }
+ editor.buffer.Insert(editor.buffer.Cursor.Loc, string(character))
}
+ editor.buffer.Cursor.ResetSelection()
+ editor.applyBuffer()
}
-// NewEditor Instanciates a ready to use text editor.
+// NewEditor instantiates a ready to use text editor.
func NewEditor() *Editor {
editor := Editor{
internalTextView: tview.NewTextView(),
requestedHeight: 3,
+ buffer: femto.NewBufferFromString("", ""),
+ tempBuffer: femto.NewBufferFromString("", ""),
}
editor.internalTextView.SetWrap(true)
@@ -310,68 +239,75 @@ func NewEditor() *Editor {
editor.internalTextView.SetBorder(true)
editor.internalTextView.SetRegions(true)
editor.internalTextView.SetScrollable(true)
- editor.internalTextView.SetText(emptyText)
editor.internalTextView.Highlight("selection")
+ editor.applyBuffer()
- editor.internalTextView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
- // Since characters can have different widths, we can't directly
- // access the string, as it is basically handled like a byte array.
- left := []rune(editor.internalTextView.GetRegionText("left"))
- right := []rune(editor.internalTextView.GetRegionText("right"))
- selection := []rune(editor.internalTextView.GetRegionText("selection"))
+ editor.buffer.Cursor.SetSelectionStart(editor.buffer.Start())
+ editor.buffer.Cursor.SetSelectionEnd(editor.buffer.End())
+ editor.internalTextView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
// TODO: This entire chunk could be cleaned up by assigning handlers to each event type,
// e.g. event.trigger()
if shortcuts.MoveCursorLeft.Equals(event) {
- editor.MoveCursorLeft(left, right, selection)
+ editor.MoveCursorLeft()
} else if shortcuts.ExpandSelectionToLeft.Equals(event) {
- editor.ExpandSelectionToLeft(left, right, selection)
+ editor.SelectionToLeft()
} else if shortcuts.MoveCursorRight.Equals(event) {
- editor.MoveCursorRight(left, right, selection)
+ editor.MoveCursorRight()
} else if shortcuts.ExpandSelectionToRight.Equals(event) {
- editor.ExpandSelectionToRight(left, right, selection)
+ editor.SelectionToRight()
} else if shortcuts.SelectWordLeft.Equals(event) {
- editor.SelectWordLeft(left, right, selection)
+ editor.SelectWordLeft()
} else if shortcuts.SelectWordRight.Equals(event) {
- editor.SelectWordRight(left, right, selection)
+ editor.SelectWordRight()
} else if shortcuts.MoveCursorWordLeft.Equals(event) {
- editor.MoveCursorWordLeft(left, right, selection)
+ editor.MoveCursorWordLeft()
} else if shortcuts.MoveCursorWordRight.Equals(event) {
- editor.MoveCursorWordRight(left, right, selection)
+ editor.MoveCursorWordRight()
} else if shortcuts.SelectAll.Equals(event) {
- editor.SelectAll(left, right, selection)
+ editor.SelectAll()
} else if shortcuts.DeleteRight.Equals(event) {
- editor.DeleteRight(left, right, selection)
+ editor.DeleteRight()
} else if event.Key() == tcell.KeyBackspace2 ||
event.Key() == tcell.KeyBackspace {
// FIXME Legacy, has to be replaced when there is N-1 Keybind-Mapping.
- editor.Backspace(left, right, selection)
+ editor.Backspace()
} else if shortcuts.CopySelection.Equals(event) {
- clipboard.WriteAll(string(selection))
+ clipboard.WriteAll(editor.buffer.Cursor.GetSelection())
//Returning nil, as copying won't do anything than filling the
//clipboard buffer.
return nil
} else if shortcuts.PasteAtSelection.Equals(event) {
- editor.Paste(left, right, selection, event)
+ editor.Paste(event)
return nil
} else if shortcuts.InputNewLine.Equals(event) {
- editor.InsertCharacter(left, right, selection, '\n')
- } else if shortcuts.SendMessage.Equals(event) {
+ editor.InsertCharacter('\n')
+ } else if shortcuts.SendMessage.Equals(event) && editor.inputCapture != nil {
return editor.inputCapture(event)
} else if (editor.inputCapture == nil || editor.inputCapture(event) != nil) && event.Rune() != 0 {
- editor.InsertCharacter(left, right, selection, event.Rune())
+ editor.InsertCharacter(event.Rune())
} else {
return event
}
editor.UpdateMentionHandler()
- editor.triggerHeightRequestIfNeccessary()
+ editor.triggerHeightRequestIfNecessary()
editor.internalTextView.ScrollToHighlight()
return nil
})
return &editor
}
+func (editor *Editor) GetTextLeftOfSelection() string {
+ var to femto.Loc
+ if editor.buffer.Cursor.HasSelection() {
+ to = editor.buffer.Cursor.CurSelection[1]
+ } else {
+ to = editor.buffer.End()
+ }
+ return editor.buffer.Substr(editor.buffer.Start(), to)
+}
+
func (editor *Editor) UpdateMentionHandler() {
atSymbolIndex := editor.FindAtSymbolIndexInCurrentWord()
if atSymbolIndex == -1 {
@@ -382,7 +318,7 @@ func (editor *Editor) UpdateMentionHandler() {
}
func (editor *Editor) ShowMentionHandler(atSymbolIndex int) {
- text := editor.internalTextView.GetRegionText("left")
+ text := editor.GetTextLeftOfSelection()
lookupKeyword := text[atSymbolIndex+1:]
editor.currentMentionBeginIndex = atSymbolIndex + 1
editor.currentMentionEndIndex = len(lookupKeyword) + atSymbolIndex
@@ -400,7 +336,7 @@ func (editor *Editor) HideAndResetMentionHandler() {
}
func (editor *Editor) FindAtSymbolIndexInCurrentWord() int {
- newLeft := editor.internalTextView.GetRegionText("left")
+ newLeft := editor.GetTextLeftOfSelection()
for i := len(newLeft) - 1; i >= 0; i-- {
if newLeft[i] == '@' && (i == 0 || newLeft[i-1] == ' ' || newLeft[i-1] == '\n') {
return i
@@ -409,35 +345,12 @@ func (editor *Editor) FindAtSymbolIndexInCurrentWord() int {
return -1
}
-func (editor *Editor) Backspace(left, right, selection []rune) {
- var newText string
-
- if len(selection) == 1 && len(left) >= 1 {
- newText = leftRegion + string(left[:len(left)-1]) + selRegion + string(selection) + rightRegion + string(right) + endRegion
- editor.internalTextView.SetText(newText)
- } else if len(selection) > 1 {
- newText = leftRegion + string(left) + selRegion
- if len(right) > 0 {
- newText = newText + string(right[0]) + rightRegion + string(right[1:])
- } else {
- newText = newText + selectionChar
- }
- newText = newText + endRegion
- editor.setAndFixText(newText)
- }
-}
-
-func (editor *Editor) setAndFixText(text string) {
- newText := multiSelectionCharWithSelectionToLeftPattern.ReplaceAllString(text, selRegion+selectionChar+endRegion)
- editor.internalTextView.SetText(newText)
-}
-
func (editor *Editor) countRows(text string) int {
_, _, width, _ := editor.internalTextView.GetInnerRect()
return tviewutil.CalculateNeccessaryHeight(width, text)
}
-func (editor *Editor) triggerHeightRequestIfNeccessary() {
+func (editor *Editor) triggerHeightRequestIfNecessary() {
if editor.heightRequestHandler == nil {
return
}
@@ -470,13 +383,9 @@ func (editor *Editor) SetBackgroundColor(color tcell.Color) {
// SetText sets the texts of the internal TextView, but also sets the selection
// and necessary groups for the navigation behaviour.
func (editor *Editor) SetText(text string) {
- if text == "" {
- editor.internalTextView.SetText(emptyText)
- } else {
- editor.internalTextView.SetText(fmt.Sprintf("[\"left\"]%s[\"\"][\"selection\"]%s[\"\"]", text, string(selectionChar)))
- }
-
- editor.triggerHeightRequestIfNeccessary()
+ editor.buffer.Replace(editor.buffer.Start(), editor.buffer.End(), text)
+ editor.applyBuffer()
+ editor.triggerHeightRequestIfNecessary()
}
// SetBorderFocusColor delegates to the underlying components
@@ -515,15 +424,7 @@ func (editor *Editor) GetCurrentMentionIndices() (int, int) {
// GetText returns the text without color tags, region tags and so on.
func (editor *Editor) GetText() string {
- left := editor.internalTextView.GetRegionText("left")
- right := editor.internalTextView.GetRegionText("right")
- selection := editor.internalTextView.GetRegionText("selection")
-
- if right == "" && selection == string(selectionChar) {
- return left
- }
-
- return left + selection + right
+ return editor.buffer.String()
}
// GetPrimitive returnbs the internal component that can be added to a layout

MODIFY
ui/window.go
ui/window.go
@@ -863,34 +863,22 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
// Invoked when enter is pressed
mentionWindow.SetSelectedFunc(func(node *tview.TreeNode) {
beginIndex, endIndex := window.messageInput.GetCurrentMentionIndices()
- data, ok := node.GetReference().(string)
+ var mentionInsertion string
oldText := window.messageInput.GetText()
- if ok {
- left := oldText[:beginIndex] + strings.TrimSpace(data) + " "
- right := oldText[endIndex+1:]
- var text string
- if len(right) == 0 {
- text = left + " "
- } else {
- text = left + right
- }
- window.messageInput.SetText(text)
- window.messageInput.MoveCursorToIndex(text, len(left))
- } else {
- role, ok := node.GetReference().(*discordgo.Role)
- if ok {
- left := "<@&" + strings.TrimSpace(role.ID) + "> "
- right := oldText[endIndex+1:]
- var text string
- if len(right) == 0 {
- text = left + " "
- } else {
- text = left + right
- }
- window.messageInput.SetText(text)
- window.messageInput.MoveCursorToIndex(text, len(left))
- }
+ reference := node.GetReference()
+ switch reference.(type) {
+ case string:
+ data := reference.(string)
+ mentionInsertion = strings.TrimSpace(data)
+ case *discordgo.Role:
+ role := reference.(*discordgo.Role)
+ mentionInsertion = "<@&" + strings.TrimSpace(role.ID) + ">"
+ default:
+ panic(fmt.Sprintf("Invalid data type in mention handler: %t:%v", reference, reference))
}
+
+ newMessageText := oldText[:beginIndex] + mentionInsertion + " " + oldText[endIndex+1:]
+ window.messageInput.SetText(newMessageText)
window.messageInput.mentionHideHandler()
})
@@ -957,18 +945,15 @@ func (window *Window) loadPrivateChannel(channel *discordgo.Channel) {
}
func (window *Window) insertNewLineAtCursor() {
- left := window.messageInput.internalTextView.GetRegionText("left")
- right := window.messageInput.internalTextView.GetRegionText("right")
- selection := window.messageInput.internalTextView.GetRegionText("selection")
- window.messageInput.InsertCharacter([]rune(left), []rune(right), []rune(selection), '\n')
+ window.messageInput.InsertCharacter('\n')
window.app.QueueUpdateDraw(func() {
- window.messageInput.triggerHeightRequestIfNeccessary()
+ window.messageInput.triggerHeightRequestIfNecessary()
window.messageInput.internalTextView.ScrollToHighlight()
})
}
func (window *Window) IsCursorInsideCodeBlock() bool {
- left := window.messageInput.internalTextView.GetRegionText("left")
+ left := window.messageInput.GetTextLeftOfSelection()
leftSplit := strings.Split(left, "```")
return len(leftSplit)%2 == 0
}

Completly revamped the editor.

* Now properly handles cursor on empty newlines
* Selection behaviour now consistent and correct
* doesn't use weird string magic anymore, instead it uses
    two string buffers, one for view and for data.