mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-27 13:48:49 +08:00
Enhancing the editor to correctly handle unicode.
This commit is contained in:
parent
e49a4438b1
commit
32c9c724f2
@ -60,6 +60,7 @@ func (fd *fieldData) cellsBefore(cells, endIdx int) int {
|
|||||||
for i := endIdx; i > 0; i-- {
|
for i := endIdx; i > 0; i-- {
|
||||||
prev := (*fd)[i-1]
|
prev := (*fd)[i-1]
|
||||||
width := runewidth.RuneWidth(prev)
|
width := runewidth.RuneWidth(prev)
|
||||||
|
|
||||||
if usedCells+width > cells {
|
if usedCells+width > cells {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@ -88,165 +89,131 @@ func (fd *fieldData) cellsAfter(cells, startIdx int) int {
|
|||||||
return len(*fd)
|
return len(*fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startVisible asserts whether the first rune is within the visible range.
|
|
||||||
func (fd *fieldData) startVisible(vr *visibleRange) bool {
|
|
||||||
return vr.startIdx == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// endVisible asserts whether the last rune is within the visible range.
|
|
||||||
// The last position in the visible range is reserved for the cursor or an
|
|
||||||
// arrow.
|
|
||||||
func (fd *fieldData) endVisible(vr *visibleRange) bool {
|
|
||||||
return vr.endIdx-1 >= len(*fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// minForArrows is the smallest number of cells in the window where we can
|
// minForArrows is the smallest number of cells in the window where we can
|
||||||
// indicate hidden text with left and right arrow.
|
// indicate hidden text with left and right arrow.
|
||||||
const minForArrows = 3
|
const minForArrows = 3
|
||||||
|
|
||||||
|
// curMinIdx returns the lowest acceptable index for cursor position that is
|
||||||
|
// still within the visible range.
|
||||||
|
func curMinIdx(start, cells int) int {
|
||||||
|
if start == 0 || cells < minForArrows {
|
||||||
|
// The very first rune is visible, so the cursor can go all the way to
|
||||||
|
// the start.
|
||||||
|
return start
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the first rune isn't visible, the cursor cannot go on the first
|
||||||
|
// cell in the visible range since it contains the left arrow.
|
||||||
|
return start + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// curMaxIdx returns the highest acceptable index for cursor position that is
|
||||||
|
// still within the visible range given the number of runes in data.
|
||||||
|
func curMaxIdx(start, end, cells, runeCount int) int {
|
||||||
|
if end == runeCount+1 || cells < minForArrows {
|
||||||
|
// The last rune is visible, so the cursor can go all the way to the
|
||||||
|
// end.
|
||||||
|
return end - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the last rune isn't visible, the cursor cannot go on the last cell
|
||||||
|
// in the window that is reserved for appending text, since it contains the
|
||||||
|
// right arrow.
|
||||||
|
return end - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// shiftLeft shifts the visible range left so that it again contains the
|
||||||
|
// cursor.
|
||||||
|
func (fd *fieldData) shiftLeft(start, end, cells, curDataPos int) (int, int) {
|
||||||
|
var startIdx int
|
||||||
|
switch {
|
||||||
|
case curDataPos == 0 || cells < minForArrows:
|
||||||
|
startIdx = curDataPos
|
||||||
|
|
||||||
|
default:
|
||||||
|
startIdx = curDataPos - 1
|
||||||
|
}
|
||||||
|
forRunes := cells - 1
|
||||||
|
endIdx := fd.cellsAfter(forRunes, startIdx)
|
||||||
|
endIdx++ // Space for the cursor.
|
||||||
|
|
||||||
|
return startIdx, endIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
// shiftRight shifts the visible range right so that it again contains the
|
||||||
|
// cursor.
|
||||||
|
func (fd *fieldData) shiftRight(start, end, cells, curDataPos int) (int, int) {
|
||||||
|
var endIdx int
|
||||||
|
switch dataLen := len(*fd); {
|
||||||
|
case curDataPos == dataLen:
|
||||||
|
// Cursor is in the empty space after the data.
|
||||||
|
// Print all runes until the end of data.
|
||||||
|
endIdx = dataLen
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Cursor is within the data, print all runes including the one the
|
||||||
|
// cursor is on.
|
||||||
|
endIdx = curDataPos + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
forRunes := cells - 1
|
||||||
|
startIdx := fd.cellsBefore(forRunes, endIdx)
|
||||||
|
endIdx = fd.cellsAfter(forRunes, startIdx)
|
||||||
|
endIdx++ // Space for the cursor.
|
||||||
|
|
||||||
|
return startIdx, endIdx
|
||||||
|
}
|
||||||
|
|
||||||
// runesIn returns runes that are in the visible range.
|
// runesIn returns runes that are in the visible range.
|
||||||
func (fd *fieldData) runesIn(vr *visibleRange) string {
|
// This might return smaller number of runes than the size of the range,
|
||||||
|
// depending on the width of the individual runes.
|
||||||
|
func (fd *fieldData) runesIn(firstRune, curPos, cells int) (string, int) {
|
||||||
|
forRunes := cells - 1 // One cell reserved for the cursor when appending.
|
||||||
|
|
||||||
|
start := firstRune
|
||||||
|
end := fd.cellsAfter(forRunes, start)
|
||||||
|
end++
|
||||||
|
|
||||||
|
if start > 0 && end-1 >= len(*fd) {
|
||||||
|
end = len(*fd)
|
||||||
|
start = fd.cellsBefore(forRunes, end)
|
||||||
|
end++ // Space for the cursor within the visible range.
|
||||||
|
}
|
||||||
|
|
||||||
|
if curPos < curMinIdx(start, cells) {
|
||||||
|
start, end = fd.shiftLeft(start, end, cells, curPos)
|
||||||
|
} else if curPos > curMaxIdx(start, end, cells, len(*fd)) {
|
||||||
|
start, end = fd.shiftRight(start, end, cells, curPos)
|
||||||
|
}
|
||||||
|
|
||||||
var runes []rune
|
var runes []rune
|
||||||
for i, r := range (*fd)[vr.startIdx:] {
|
for i, r := range (*fd)[start:] {
|
||||||
if i+vr.startIdx > vr.endIdx-2 {
|
if i+start > end-2 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
runes = append(runes, r)
|
runes = append(runes, r)
|
||||||
}
|
}
|
||||||
|
//log.Printf("runes: %v", string(runes))
|
||||||
|
|
||||||
useArrows := vr.cells() >= minForArrows
|
useArrows := cells >= minForArrows
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i, r := range runes {
|
for i, r := range runes {
|
||||||
switch {
|
switch {
|
||||||
case useArrows && i == 0 && !fd.startVisible(vr):
|
case useArrows && i == 0 && start > 0:
|
||||||
b.WriteRune('⇦')
|
b.WriteRune('⇦')
|
||||||
|
if rw := runewidth.RuneWidth(r); rw == 2 {
|
||||||
|
b.WriteRune('⇦')
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
b.WriteRune(r)
|
b.WriteRune(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if useArrows && !fd.endVisible(vr) {
|
if useArrows && end-1 < len(*fd) {
|
||||||
b.WriteRune('⇨')
|
b.WriteRune('⇨')
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String(), start
|
||||||
}
|
|
||||||
|
|
||||||
// visibleRange represents a range of currently visible cells.
|
|
||||||
// Visible cells are all cells whose index falls within:
|
|
||||||
// startIdx <= idx < endIdx
|
|
||||||
// Not all of these cells are available for runes, the last cell is reserved
|
|
||||||
// for the cursor to append data or for an arrow indicating that the text is
|
|
||||||
// scrolling. See forRunes().
|
|
||||||
type visibleRange struct {
|
|
||||||
startIdx int
|
|
||||||
endIdx int
|
|
||||||
}
|
|
||||||
|
|
||||||
// forRunes returns the number of cells that are usable for runes.
|
|
||||||
// Part of the visible range is reserved for the cursor at the end of the data.
|
|
||||||
func (vr *visibleRange) forRunes() int {
|
|
||||||
cells := vr.cells()
|
|
||||||
if cells < 1 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return cells - 1 // One cell reserved for the cursor at the end.
|
|
||||||
}
|
|
||||||
|
|
||||||
// cells returns the number of cells in the range.
|
|
||||||
func (vr *visibleRange) cells() int {
|
|
||||||
return vr.endIdx - vr.startIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
// contains asserts whether the provided index is in the range.
|
|
||||||
func (vr *visibleRange) contains(idx int) bool {
|
|
||||||
return idx >= vr.startIdx && idx < vr.endIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
// set sets the visible range from the start to the end index.
|
|
||||||
func (vr *visibleRange) set(startIdx, endIdx int) {
|
|
||||||
vr.startIdx = startIdx
|
|
||||||
vr.endIdx = endIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
// curMinIdx returns the lowest acceptable index for cursor position that is
|
|
||||||
// still within the visible range.
|
|
||||||
func (vr *visibleRange) curMinIdx() int {
|
|
||||||
if vr.cells() == 0 {
|
|
||||||
return vr.startIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
if vr.startIdx == 0 || vr.cells() < minForArrows {
|
|
||||||
// The very first rune is visible, so the cursor can go all the way to
|
|
||||||
// the start.
|
|
||||||
return vr.startIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the first rune isn't visible, the cursor cannot go on the first
|
|
||||||
// cell in the visible range since it contains the left arrow.
|
|
||||||
return vr.startIdx + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// curMaxIdx returns the highest acceptable index for cursor position that is
|
|
||||||
// still within the visible range given the number of runes in data.
|
|
||||||
func (vr *visibleRange) curMaxIdx(runeCount int) int {
|
|
||||||
if vr.cells() == 0 {
|
|
||||||
return vr.startIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
if vr.endIdx == runeCount || vr.endIdx == runeCount+1 || vr.cells() < minForArrows {
|
|
||||||
// The last rune is visible, so the cursor can go all the way to the
|
|
||||||
// end.
|
|
||||||
return vr.endIdx - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the last rune isn't visible, the cursor cannot go on the last cell
|
|
||||||
// in the window that is reserved for appending text, since it contains the
|
|
||||||
// right arrow.
|
|
||||||
return vr.endIdx - 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalizeToWidth normalizes the visible range, handles cases where the width of the
|
|
||||||
// text input field changed (terminal resize).
|
|
||||||
func (vr *visibleRange) normalizeToWidth(width int) {
|
|
||||||
switch {
|
|
||||||
case width < vr.cells():
|
|
||||||
diff := vr.cells() - width
|
|
||||||
vr.startIdx += diff
|
|
||||||
|
|
||||||
case width > vr.cells():
|
|
||||||
diff := width - vr.cells()
|
|
||||||
vr.startIdx -= diff
|
|
||||||
}
|
|
||||||
|
|
||||||
if vr.startIdx < 0 {
|
|
||||||
vr.endIdx += -1 * vr.startIdx
|
|
||||||
vr.startIdx = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalizeToiData normalizes the visible range, handles cases where the
|
|
||||||
// length of the data decreased due to deletion of some runes.
|
|
||||||
func (vr *visibleRange) normalizeToData(fd fieldData) {
|
|
||||||
if vr.endIdx <= len(fd) || vr.startIdx == 0 {
|
|
||||||
// Nothing to do when data fills the range or the range already starts
|
|
||||||
// all the way left.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
endIdx := len(fd)
|
|
||||||
startIdx := fd.cellsBefore(vr.forRunes(), endIdx)
|
|
||||||
endIdx++ // Space for the cursor within the visible range.
|
|
||||||
vr.set(startIdx, endIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// curRelative returns the relative position of the cursor within the visible
|
|
||||||
// range. Returns an error if the cursos isn't inside the visible range.
|
|
||||||
func (vr *visibleRange) curRelative(curDataPos int) (int, error) {
|
|
||||||
if !vr.contains(curDataPos) {
|
|
||||||
return 0, fmt.Errorf("curDataPos %d isn't inside %#v", curDataPos, *vr)
|
|
||||||
}
|
|
||||||
return curDataPos - vr.startIdx, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fieldEditor maintains the cursor position and allows editing of the data in
|
// fieldEditor maintains the cursor position and allows editing of the data in
|
||||||
@ -261,93 +228,51 @@ type fieldEditor struct {
|
|||||||
// possible.
|
// possible.
|
||||||
curDataPos int
|
curDataPos int
|
||||||
|
|
||||||
// visible is the currently visible range.
|
// firstRune is the index of the first displayed rune in the text input
|
||||||
visible *visibleRange
|
// field.
|
||||||
|
firstRune int
|
||||||
}
|
}
|
||||||
|
|
||||||
// newFieldEditor returns a new fieldEditor instance.
|
// newFieldEditor returns a new fieldEditor instance.
|
||||||
func newFieldEditor() *fieldEditor {
|
func newFieldEditor() *fieldEditor {
|
||||||
return &fieldEditor{
|
return &fieldEditor{}
|
||||||
visible: &visibleRange{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// shiftLeft shifts the visible range left so that it again contains the
|
|
||||||
// cursor.
|
|
||||||
func (fe *fieldEditor) shiftLeft() {
|
|
||||||
var startIdx int
|
|
||||||
switch {
|
|
||||||
case fe.curDataPos == 0 || fe.visible.cells() < minForArrows:
|
|
||||||
startIdx = fe.curDataPos
|
|
||||||
|
|
||||||
default:
|
|
||||||
startIdx = fe.curDataPos - 1
|
|
||||||
}
|
|
||||||
endIdx := fe.data.cellsAfter(fe.visible.forRunes(), startIdx)
|
|
||||||
endIdx++ // Space for the cursor.
|
|
||||||
|
|
||||||
gotCells := endIdx - startIdx
|
|
||||||
if fe.visible.cells() >= minForArrows && gotCells < minForArrows {
|
|
||||||
// The plan was to hide the first rune under an arrow.
|
|
||||||
// However after looking at the actual runes in the range, some took
|
|
||||||
// more space than one cell (full-width runes) and we have lost the
|
|
||||||
// space for the arrow, so shift the range by one.
|
|
||||||
startIdx++
|
|
||||||
endIdx++
|
|
||||||
}
|
|
||||||
fe.visible.set(startIdx, endIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// shiftRight shifts the visible range right so that it again contains the
|
|
||||||
// cursor.
|
|
||||||
func (fe *fieldEditor) shiftRight() {
|
|
||||||
var endIdx int
|
|
||||||
switch dataLen := len(fe.data); {
|
|
||||||
case fe.curDataPos == dataLen:
|
|
||||||
// Cursor is in the empty space after the data.
|
|
||||||
// Print all runes until the end of data.
|
|
||||||
endIdx = dataLen
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Cursor is within the data, print all runes including the one the
|
|
||||||
// cursor is on.
|
|
||||||
endIdx = fe.curDataPos + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
startIdx := fe.data.cellsBefore(fe.visible.forRunes(), endIdx)
|
|
||||||
endIdx++ // Space for the cursor within the visible range.
|
|
||||||
fe.visible.set(startIdx, endIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// toCursor shifts the visible range to the cursor if it scrolled out of view.
|
|
||||||
// This is a no-op if the cursor is inside the range.
|
|
||||||
func (fe *fieldEditor) toCursor() {
|
|
||||||
switch {
|
|
||||||
case fe.curDataPos < fe.visible.curMinIdx():
|
|
||||||
fe.shiftLeft()
|
|
||||||
case fe.curDataPos > fe.visible.curMaxIdx(len(fe.data)):
|
|
||||||
fe.shiftRight()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// minFieldWidth is the minimum supported width of the text input field.
|
// minFieldWidth is the minimum supported width of the text input field.
|
||||||
const minFieldWidth = 4
|
const minFieldWidth = 4
|
||||||
|
|
||||||
|
// curCell returns the index of the cell the cursor is in within the text input field.
|
||||||
|
func (fe *fieldEditor) curCell(width int) int {
|
||||||
|
if width == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// The index of rune within the visible range the cursor is at.
|
||||||
|
runeNum := fe.curDataPos - fe.firstRune
|
||||||
|
|
||||||
|
cellNum := 0
|
||||||
|
rn := 0
|
||||||
|
for i, r := range fe.data {
|
||||||
|
if i < fe.firstRune {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rn >= runeNum {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
rn++
|
||||||
|
cellNum += runewidth.RuneWidth(r)
|
||||||
|
}
|
||||||
|
return cellNum
|
||||||
|
}
|
||||||
|
|
||||||
// viewFor returns the currently visible data inside a text field with the
|
// viewFor returns the currently visible data inside a text field with the
|
||||||
// specified width and the cursor position within the field.
|
// specified width and the cursor position within the field.
|
||||||
func (fe *fieldEditor) viewFor(width int) (string, int, error) {
|
func (fe *fieldEditor) viewFor(width int) (string, int, error) {
|
||||||
if min := minFieldWidth; width < min { // One for left arrow, two for one full-width rune and one for the cursor.
|
if min := minFieldWidth; width < min { // One for left arrow, two for one full-width rune and one for the cursor.
|
||||||
return "", -1, fmt.Errorf("width %d is too small, the minimum is %d", width, min)
|
return "", -1, fmt.Errorf("width %d is too small, the minimum is %d", width, min)
|
||||||
}
|
}
|
||||||
fe.visible.normalizeToWidth(width)
|
runes, start := fe.data.runesIn(fe.firstRune, fe.curDataPos, width)
|
||||||
fe.visible.normalizeToData(fe.data)
|
fe.firstRune = start
|
||||||
fe.toCursor()
|
return runes, fe.curCell(width), nil
|
||||||
|
|
||||||
cur, err := fe.visible.curRelative(fe.curDataPos)
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, err
|
|
||||||
}
|
|
||||||
return fe.data.runesIn(fe.visible), cur, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// content returns the string content in the field editor.
|
// content returns the string content in the field editor.
|
||||||
@ -368,7 +293,7 @@ func (fe *fieldEditor) insert(r rune) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fe.data.insertAt(fe.curDataPos, r)
|
fe.data.insertAt(fe.curDataPos, r)
|
||||||
fe.curDataPos += rw
|
fe.curDataPos++
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete deletes the rune at the current position of the cursor.
|
// delete deletes the rune at the current position of the cursor.
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user