widget: extract seeking logic from Editor.closestPosition

We'd like to re-use the Editor.closestPosition seeking for
segmentIterator.Next; this change extracts the state-less logic
into functions.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-02-16 11:39:41 +01:00
parent 2df3db361f
commit b7341672e3
2 changed files with 114 additions and 67 deletions
+111 -64
View File
@@ -69,6 +69,9 @@ type Editor struct {
// spaced intervals to speed up caret seeking.
index []combinedPos
// offIndex is an index of rune index to byte offsets.
offIndex []offEntry
// ime tracks the state relevant to input methods.
ime struct {
imeState
@@ -102,6 +105,11 @@ type Editor struct {
prevEvents int
}
type offEntry struct {
runes int
bytes int
}
type imeState struct {
selection struct {
rng key.Range
@@ -123,8 +131,6 @@ type maskReader struct {
// combinedPos is a point in the editor.
type combinedPos struct {
// ofs is the offset into the editorBuffer in bytes. The other three fields are based off of this one.
ofs int
// runes is the offset in runes.
runes int
@@ -573,8 +579,10 @@ func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
imeEnd := e.closestPosition(combinedPos{runes: end})
e.ime.start = imeStart.runes
e.ime.end = imeEnd.runes
e.rr.Seek(int64(imeStart.ofs), io.SeekStart)
n := imeEnd.ofs - imeStart.ofs
startOff := e.runeOffset(imeStart.runes)
endOff := e.runeOffset(imeEnd.runes)
e.rr.Seek(int64(startOff), io.SeekStart)
n := endOff - startOff
if n > len(e.ime.scratch) {
e.ime.scratch = make([]byte, n)
}
@@ -888,7 +896,7 @@ func (e *Editor) indexPosition(pos combinedPos) combinedPos {
})
}
i := sort.Search(len(e.index), func(i int) bool {
return e.positionGreaterOrEqual(e.index[i], pos)
return positionGreaterOrEqual(e.lines, e.index[i], pos)
})
// Return position just before pos, which is guaranteed to be less than or equal to pos.
if i > 0 {
@@ -899,10 +907,10 @@ func (e *Editor) indexPosition(pos combinedPos) combinedPos {
// positionGreaterOrEqual reports whether p1 >= p2 according to the non-zero fields
// of p2. All fields of p1 must be a consistent and valid.
func (e *Editor) positionGreaterOrEqual(p1, p2 combinedPos) bool {
l := e.lines[p1.lineCol.Y]
func positionGreaterOrEqual(lines []text.Line, p1, p2 combinedPos) bool {
l := lines[p1.lineCol.Y]
endCol := len(l.Layout.Advances) - 1
if lastLine := p1.lineCol.Y == len(e.lines)-1; lastLine {
if lastLine := p1.lineCol.Y == len(lines)-1; lastLine {
endCol++
}
eol := p1.lineCol.X == endCol
@@ -918,7 +926,7 @@ func (e *Editor) positionGreaterOrEqual(p1, p2 combinedPos) bool {
ly := p1.y + l.Descent.Ceil()
prevy := p1.y - l.Ascent.Ceil()
switch {
case ly < p2.y && p1.lineCol.Y < len(e.lines)-1:
case ly < p2.y && p1.lineCol.Y < len(lines)-1:
// p1 is on a line before p2.y.
return false
case prevy >= p2.y && p1.lineCol.Y > 0:
@@ -935,46 +943,91 @@ func (e *Editor) positionGreaterOrEqual(p1, p2 combinedPos) bool {
}
// closestPosition takes a position and returns its closest valid position.
// Zero fields of pos and pos.ofs are ignored.
// Zero fields of pos are ignored.
func (e *Editor) closestPosition(pos combinedPos) combinedPos {
closest := e.indexPosition(pos)
l := e.lines[closest.lineCol.Y]
count := 0
const runesPerIndexEntry = 50
for {
var done bool
closest, done = seekPosition(e.lines, closest, pos, e.Alignment, e.viewSize.X, runesPerIndexEntry)
if done {
return closest
}
e.index = append(e.index, closest)
}
}
// seekPosition seeks to the position closest to needle, starting at start and returns true.
// If limit is non-zero, seekPosition stops seeks after limit runes and returns false.
func seekPosition(lines []text.Line, start, needle combinedPos, alignment text.Alignment, width, limit int) (combinedPos, bool) {
l := lines[start.lineCol.Y]
count := 0
// Advance next and prev until next is greater than or equal to pos.
for {
for ; closest.lineCol.X < len(l.Layout.Advances); closest.lineCol.X++ {
if count == runesPerIndexEntry {
e.index = append(e.index, closest)
count = 0
for ; start.lineCol.X < len(l.Layout.Advances); start.lineCol.X++ {
if limit != 0 && count == limit {
return start, false
}
count++
if e.positionGreaterOrEqual(closest, pos) {
return closest
if positionGreaterOrEqual(lines, start, needle) {
return start, true
}
adv := l.Layout.Advances[closest.lineCol.X]
closest.x += adv
_, s := e.rr.runeAt(closest.ofs)
closest.ofs += s
closest.runes++
adv := l.Layout.Advances[start.lineCol.X]
start.x += adv
start.runes++
}
if closest.lineCol.Y == len(e.lines)-1 {
if start.lineCol.Y == len(lines)-1 {
// End of file.
return closest
return start, true
}
prevDesc := l.Descent
closest.lineCol.Y++
closest.lineCol.X = 0
l = e.lines[closest.lineCol.Y]
closest.x = align(e.Alignment, l.Width, e.viewSize.X)
closest.y += (prevDesc + l.Ascent).Ceil()
start.lineCol.Y++
start.lineCol.X = 0
l = lines[start.lineCol.Y]
start.x = align(alignment, l.Width, width)
start.y += (prevDesc + l.Ascent).Ceil()
}
}
// indexRune returns the latest rune index and byte offset no later than r.
func (e *Editor) indexRune(r int) offEntry {
// Initialize index.
if len(e.offIndex) == 0 {
e.offIndex = append(e.offIndex, offEntry{})
}
i := sort.Search(len(e.offIndex), func(i int) bool {
entry := e.offIndex[i]
return entry.runes >= r
})
// Return the entry guaranteed to be less than or equal to r.
if i > 0 {
i--
}
return e.offIndex[i]
}
// runeOffset returns the byte offset into e.rr of the r'th rune.
// r must be a valid rune index, usually returned by closestPosition.
func (e *Editor) runeOffset(r int) int {
const runesPerIndexEntry = 50
entry := e.indexRune(r)
lastEntry := e.offIndex[len(e.offIndex)-1].runes
for entry.runes < r {
if entry.runes > lastEntry && entry.runes%runesPerIndexEntry == runesPerIndexEntry-1 {
e.offIndex = append(e.offIndex, entry)
}
_, s := e.rr.runeAt(entry.bytes)
entry.bytes += s
entry.runes++
}
return entry.bytes
}
func (e *Editor) invalidate() {
e.index = e.index[:0]
e.offIndex = e.offIndex[:0]
e.valid = false
}
@@ -1030,8 +1083,9 @@ func (e *Editor) replace(start, end int, s string) {
}
startPos := e.closestPosition(combinedPos{runes: start})
endPos := e.closestPosition(combinedPos{runes: end})
e.rr.deleteRunes(startPos.ofs, endPos.runes-startPos.runes)
e.rr.prepend(startPos.ofs, s)
startOff := e.runeOffset(startPos.runes)
e.rr.deleteRunes(startOff, endPos.runes-startPos.runes)
e.rr.prepend(startOff, s)
newEnd := startPos.runes + utf8.RuneCountInString(s)
adjust := func(pos int) int {
switch {
@@ -1100,14 +1154,15 @@ func (e *Editor) moveWord(distance int, selAct selectionAction) {
// atEnd if caret is at either side of the buffer.
caret := e.closestPosition(combinedPos{runes: e.caret.start})
atEnd := func() bool {
return caret.ofs == 0 || caret.ofs == e.rr.len()
return caret.runes == 0 || caret.runes == e.Len()
}
// next returns the appropriate rune given the direction.
next := func() (r rune) {
off := e.runeOffset(caret.runes)
if direction < 0 {
r, _ = e.rr.runeBefore(caret.ofs)
r, _ = e.rr.runeBefore(off)
} else {
r, _ = e.rr.runeAt(caret.ofs)
r, _ = e.rr.runeAt(off)
}
return r
}
@@ -1152,41 +1207,33 @@ func (e *Editor) deleteWord(distance int) {
}
// atEnd if offset is at or beyond either side of the buffer.
caret := e.closestPosition(combinedPos{runes: e.caret.start})
atEnd := func(offset int) bool {
idx := caret.ofs + offset*direction
return idx <= 0 || idx >= e.rr.len()
atEnd := func(runes int) bool {
idx := caret.runes + runes*direction
return idx <= 0 || idx >= e.Len()
}
// next returns the appropriate rune and length given the direction and offset (in bytes).
next := func(offset int) (r rune, l int) {
idx := caret.ofs + offset*direction
// next returns the appropriate rune given the direction and offset in runes).
next := func(runes int) rune {
idx := caret.runes + runes*direction
if idx < 0 {
idx = 0
} else if idx > e.rr.len() {
idx = e.rr.len()
} else if idx > e.Len() {
idx = e.Len()
}
off := e.runeOffset(idx)
var r rune
if direction < 0 {
r, l = e.rr.runeBefore(idx)
r, _ = e.rr.runeBefore(off)
} else {
r, l = e.rr.runeAt(idx)
r, _ = e.rr.runeAt(off)
}
return
return r
}
var runes = 1
_, bytes := e.rr.runeAt(caret.ofs)
if direction < 0 {
_, bytes = e.rr.runeBefore(caret.ofs)
}
for ii := 0; ii < words; ii++ {
if r, _ := next(bytes); unicode.IsSpace(r) {
for r, lg := next(bytes); unicode.IsSpace(r) && !atEnd(bytes); r, lg = next(bytes) {
runes += 1
bytes += lg
}
} else {
for r, lg := next(bytes); !unicode.IsSpace(r) && !atEnd(bytes); r, lg = next(bytes) {
runes += 1
bytes += lg
}
r := next(runes)
wantSpace := unicode.IsSpace(r)
for r := next(runes); unicode.IsSpace(r) == wantSpace && !atEnd(runes); r = next(runes) {
runes += 1
}
}
e.Delete(runes * direction)
@@ -1245,10 +1292,10 @@ func (e *Editor) SetCaret(start, end int) {
// SelectedText returns the currently selected text (if any) from the editor.
func (e *Editor) SelectedText() string {
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
caretEnd := e.closestPosition(combinedPos{runes: e.caret.end})
start := min(caretStart.ofs, caretEnd.ofs)
end := max(caretStart.ofs, caretEnd.ofs)
startOff := e.runeOffset(e.caret.start)
endOff := e.runeOffset(e.caret.end)
start := min(startOff, endOff)
end := max(startOff, endOff)
buf := make([]byte, end-start)
e.rr.Seek(int64(start), io.SeekStart)
_, err := e.rr.Read(buf)
+3 -3
View File
@@ -159,7 +159,7 @@ func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
if gotLine != line || gotCol != col {
t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, col)
}
caretBytes := e.closestPosition(combinedPos{runes: e.caret.start}).ofs
caretBytes := e.runeOffset(e.caret.start)
if bytes != caretBytes {
t.Errorf("caret at buffer position %d, expected %d", caretBytes, bytes)
}
@@ -291,7 +291,7 @@ func TestEditorMoveWord(t *testing.T) {
e := setup(tt.Text)
e.MoveCaret(tt.Start, tt.Start)
e.moveWord(tt.Skip, selectionClear)
caretBytes := e.closestPosition(combinedPos{runes: e.caret.start}).ofs
caretBytes := e.runeOffset(e.caret.start)
if caretBytes != tt.Want {
t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
}
@@ -485,7 +485,7 @@ func TestEditorDeleteWord(t *testing.T) {
e.MoveCaret(tt.Start, tt.Start)
e.MoveCaret(0, tt.Selection)
e.deleteWord(tt.Delete)
caretBytes := e.closestPosition(combinedPos{runes: e.caret.start}).ofs
caretBytes := e.runeOffset(e.caret.start)
if caretBytes != tt.Want {
t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
}