mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 02:15:34 +00:00
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:
+111
-64
@@ -69,6 +69,9 @@ type Editor struct {
|
|||||||
// spaced intervals to speed up caret seeking.
|
// spaced intervals to speed up caret seeking.
|
||||||
index []combinedPos
|
index []combinedPos
|
||||||
|
|
||||||
|
// offIndex is an index of rune index to byte offsets.
|
||||||
|
offIndex []offEntry
|
||||||
|
|
||||||
// ime tracks the state relevant to input methods.
|
// ime tracks the state relevant to input methods.
|
||||||
ime struct {
|
ime struct {
|
||||||
imeState
|
imeState
|
||||||
@@ -102,6 +105,11 @@ type Editor struct {
|
|||||||
prevEvents int
|
prevEvents int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type offEntry struct {
|
||||||
|
runes int
|
||||||
|
bytes int
|
||||||
|
}
|
||||||
|
|
||||||
type imeState struct {
|
type imeState struct {
|
||||||
selection struct {
|
selection struct {
|
||||||
rng key.Range
|
rng key.Range
|
||||||
@@ -123,8 +131,6 @@ type maskReader struct {
|
|||||||
|
|
||||||
// combinedPos is a point in the editor.
|
// combinedPos is a point in the editor.
|
||||||
type combinedPos struct {
|
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 is the offset in runes.
|
||||||
runes int
|
runes int
|
||||||
|
|
||||||
@@ -573,8 +579,10 @@ func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
|
|||||||
imeEnd := e.closestPosition(combinedPos{runes: end})
|
imeEnd := e.closestPosition(combinedPos{runes: end})
|
||||||
e.ime.start = imeStart.runes
|
e.ime.start = imeStart.runes
|
||||||
e.ime.end = imeEnd.runes
|
e.ime.end = imeEnd.runes
|
||||||
e.rr.Seek(int64(imeStart.ofs), io.SeekStart)
|
startOff := e.runeOffset(imeStart.runes)
|
||||||
n := imeEnd.ofs - imeStart.ofs
|
endOff := e.runeOffset(imeEnd.runes)
|
||||||
|
e.rr.Seek(int64(startOff), io.SeekStart)
|
||||||
|
n := endOff - startOff
|
||||||
if n > len(e.ime.scratch) {
|
if n > len(e.ime.scratch) {
|
||||||
e.ime.scratch = make([]byte, n)
|
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 {
|
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.
|
// Return position just before pos, which is guaranteed to be less than or equal to pos.
|
||||||
if i > 0 {
|
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
|
// positionGreaterOrEqual reports whether p1 >= p2 according to the non-zero fields
|
||||||
// of p2. All fields of p1 must be a consistent and valid.
|
// of p2. All fields of p1 must be a consistent and valid.
|
||||||
func (e *Editor) positionGreaterOrEqual(p1, p2 combinedPos) bool {
|
func positionGreaterOrEqual(lines []text.Line, p1, p2 combinedPos) bool {
|
||||||
l := e.lines[p1.lineCol.Y]
|
l := lines[p1.lineCol.Y]
|
||||||
endCol := len(l.Layout.Advances) - 1
|
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++
|
endCol++
|
||||||
}
|
}
|
||||||
eol := p1.lineCol.X == endCol
|
eol := p1.lineCol.X == endCol
|
||||||
@@ -918,7 +926,7 @@ func (e *Editor) positionGreaterOrEqual(p1, p2 combinedPos) bool {
|
|||||||
ly := p1.y + l.Descent.Ceil()
|
ly := p1.y + l.Descent.Ceil()
|
||||||
prevy := p1.y - l.Ascent.Ceil()
|
prevy := p1.y - l.Ascent.Ceil()
|
||||||
switch {
|
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.
|
// p1 is on a line before p2.y.
|
||||||
return false
|
return false
|
||||||
case prevy >= p2.y && p1.lineCol.Y > 0:
|
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.
|
// 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 {
|
func (e *Editor) closestPosition(pos combinedPos) combinedPos {
|
||||||
closest := e.indexPosition(pos)
|
closest := e.indexPosition(pos)
|
||||||
l := e.lines[closest.lineCol.Y]
|
|
||||||
count := 0
|
|
||||||
const runesPerIndexEntry = 50
|
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.
|
// Advance next and prev until next is greater than or equal to pos.
|
||||||
for {
|
for {
|
||||||
for ; closest.lineCol.X < len(l.Layout.Advances); closest.lineCol.X++ {
|
for ; start.lineCol.X < len(l.Layout.Advances); start.lineCol.X++ {
|
||||||
if count == runesPerIndexEntry {
|
if limit != 0 && count == limit {
|
||||||
e.index = append(e.index, closest)
|
return start, false
|
||||||
count = 0
|
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
if e.positionGreaterOrEqual(closest, pos) {
|
if positionGreaterOrEqual(lines, start, needle) {
|
||||||
return closest
|
return start, true
|
||||||
}
|
}
|
||||||
|
|
||||||
adv := l.Layout.Advances[closest.lineCol.X]
|
adv := l.Layout.Advances[start.lineCol.X]
|
||||||
closest.x += adv
|
start.x += adv
|
||||||
_, s := e.rr.runeAt(closest.ofs)
|
start.runes++
|
||||||
closest.ofs += s
|
|
||||||
closest.runes++
|
|
||||||
}
|
}
|
||||||
if closest.lineCol.Y == len(e.lines)-1 {
|
if start.lineCol.Y == len(lines)-1 {
|
||||||
// End of file.
|
// End of file.
|
||||||
return closest
|
return start, true
|
||||||
}
|
}
|
||||||
|
|
||||||
prevDesc := l.Descent
|
prevDesc := l.Descent
|
||||||
closest.lineCol.Y++
|
start.lineCol.Y++
|
||||||
closest.lineCol.X = 0
|
start.lineCol.X = 0
|
||||||
l = e.lines[closest.lineCol.Y]
|
l = lines[start.lineCol.Y]
|
||||||
closest.x = align(e.Alignment, l.Width, e.viewSize.X)
|
start.x = align(alignment, l.Width, width)
|
||||||
closest.y += (prevDesc + l.Ascent).Ceil()
|
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() {
|
func (e *Editor) invalidate() {
|
||||||
e.index = e.index[:0]
|
e.index = e.index[:0]
|
||||||
|
e.offIndex = e.offIndex[:0]
|
||||||
e.valid = false
|
e.valid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,8 +1083,9 @@ func (e *Editor) replace(start, end int, s string) {
|
|||||||
}
|
}
|
||||||
startPos := e.closestPosition(combinedPos{runes: start})
|
startPos := e.closestPosition(combinedPos{runes: start})
|
||||||
endPos := e.closestPosition(combinedPos{runes: end})
|
endPos := e.closestPosition(combinedPos{runes: end})
|
||||||
e.rr.deleteRunes(startPos.ofs, endPos.runes-startPos.runes)
|
startOff := e.runeOffset(startPos.runes)
|
||||||
e.rr.prepend(startPos.ofs, s)
|
e.rr.deleteRunes(startOff, endPos.runes-startPos.runes)
|
||||||
|
e.rr.prepend(startOff, s)
|
||||||
newEnd := startPos.runes + utf8.RuneCountInString(s)
|
newEnd := startPos.runes + utf8.RuneCountInString(s)
|
||||||
adjust := func(pos int) int {
|
adjust := func(pos int) int {
|
||||||
switch {
|
switch {
|
||||||
@@ -1100,14 +1154,15 @@ func (e *Editor) moveWord(distance int, selAct selectionAction) {
|
|||||||
// atEnd if caret is at either side of the buffer.
|
// atEnd if caret is at either side of the buffer.
|
||||||
caret := e.closestPosition(combinedPos{runes: e.caret.start})
|
caret := e.closestPosition(combinedPos{runes: e.caret.start})
|
||||||
atEnd := func() bool {
|
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 returns the appropriate rune given the direction.
|
||||||
next := func() (r rune) {
|
next := func() (r rune) {
|
||||||
|
off := e.runeOffset(caret.runes)
|
||||||
if direction < 0 {
|
if direction < 0 {
|
||||||
r, _ = e.rr.runeBefore(caret.ofs)
|
r, _ = e.rr.runeBefore(off)
|
||||||
} else {
|
} else {
|
||||||
r, _ = e.rr.runeAt(caret.ofs)
|
r, _ = e.rr.runeAt(off)
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -1152,41 +1207,33 @@ func (e *Editor) deleteWord(distance int) {
|
|||||||
}
|
}
|
||||||
// atEnd if offset is at or beyond either side of the buffer.
|
// atEnd if offset is at or beyond either side of the buffer.
|
||||||
caret := e.closestPosition(combinedPos{runes: e.caret.start})
|
caret := e.closestPosition(combinedPos{runes: e.caret.start})
|
||||||
atEnd := func(offset int) bool {
|
atEnd := func(runes int) bool {
|
||||||
idx := caret.ofs + offset*direction
|
idx := caret.runes + runes*direction
|
||||||
return idx <= 0 || idx >= e.rr.len()
|
return idx <= 0 || idx >= e.Len()
|
||||||
}
|
}
|
||||||
// next returns the appropriate rune and length given the direction and offset (in bytes).
|
// next returns the appropriate rune given the direction and offset in runes).
|
||||||
next := func(offset int) (r rune, l int) {
|
next := func(runes int) rune {
|
||||||
idx := caret.ofs + offset*direction
|
idx := caret.runes + runes*direction
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
idx = 0
|
idx = 0
|
||||||
} else if idx > e.rr.len() {
|
} else if idx > e.Len() {
|
||||||
idx = e.rr.len()
|
idx = e.Len()
|
||||||
}
|
}
|
||||||
|
off := e.runeOffset(idx)
|
||||||
|
var r rune
|
||||||
if direction < 0 {
|
if direction < 0 {
|
||||||
r, l = e.rr.runeBefore(idx)
|
r, _ = e.rr.runeBefore(off)
|
||||||
} else {
|
} else {
|
||||||
r, l = e.rr.runeAt(idx)
|
r, _ = e.rr.runeAt(off)
|
||||||
}
|
}
|
||||||
return
|
return r
|
||||||
}
|
}
|
||||||
var runes = 1
|
var runes = 1
|
||||||
_, bytes := e.rr.runeAt(caret.ofs)
|
|
||||||
if direction < 0 {
|
|
||||||
_, bytes = e.rr.runeBefore(caret.ofs)
|
|
||||||
}
|
|
||||||
for ii := 0; ii < words; ii++ {
|
for ii := 0; ii < words; ii++ {
|
||||||
if r, _ := next(bytes); unicode.IsSpace(r) {
|
r := next(runes)
|
||||||
for r, lg := next(bytes); unicode.IsSpace(r) && !atEnd(bytes); r, lg = next(bytes) {
|
wantSpace := unicode.IsSpace(r)
|
||||||
runes += 1
|
for r := next(runes); unicode.IsSpace(r) == wantSpace && !atEnd(runes); r = next(runes) {
|
||||||
bytes += lg
|
runes += 1
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for r, lg := next(bytes); !unicode.IsSpace(r) && !atEnd(bytes); r, lg = next(bytes) {
|
|
||||||
runes += 1
|
|
||||||
bytes += lg
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.Delete(runes * direction)
|
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.
|
// SelectedText returns the currently selected text (if any) from the editor.
|
||||||
func (e *Editor) SelectedText() string {
|
func (e *Editor) SelectedText() string {
|
||||||
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
|
startOff := e.runeOffset(e.caret.start)
|
||||||
caretEnd := e.closestPosition(combinedPos{runes: e.caret.end})
|
endOff := e.runeOffset(e.caret.end)
|
||||||
start := min(caretStart.ofs, caretEnd.ofs)
|
start := min(startOff, endOff)
|
||||||
end := max(caretStart.ofs, caretEnd.ofs)
|
end := max(startOff, endOff)
|
||||||
buf := make([]byte, end-start)
|
buf := make([]byte, end-start)
|
||||||
e.rr.Seek(int64(start), io.SeekStart)
|
e.rr.Seek(int64(start), io.SeekStart)
|
||||||
_, err := e.rr.Read(buf)
|
_, err := e.rr.Read(buf)
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
|
|||||||
if gotLine != line || gotCol != col {
|
if gotLine != line || gotCol != col {
|
||||||
t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, 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 {
|
if bytes != caretBytes {
|
||||||
t.Errorf("caret at buffer position %d, expected %d", caretBytes, bytes)
|
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 := setup(tt.Text)
|
||||||
e.MoveCaret(tt.Start, tt.Start)
|
e.MoveCaret(tt.Start, tt.Start)
|
||||||
e.moveWord(tt.Skip, selectionClear)
|
e.moveWord(tt.Skip, selectionClear)
|
||||||
caretBytes := e.closestPosition(combinedPos{runes: e.caret.start}).ofs
|
caretBytes := e.runeOffset(e.caret.start)
|
||||||
if caretBytes != tt.Want {
|
if caretBytes != tt.Want {
|
||||||
t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, 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(tt.Start, tt.Start)
|
||||||
e.MoveCaret(0, tt.Selection)
|
e.MoveCaret(0, tt.Selection)
|
||||||
e.deleteWord(tt.Delete)
|
e.deleteWord(tt.Delete)
|
||||||
caretBytes := e.closestPosition(combinedPos{runes: e.caret.start}).ofs
|
caretBytes := e.runeOffset(e.caret.start)
|
||||||
if caretBytes != tt.Want {
|
if caretBytes != tt.Want {
|
||||||
t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
|
t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user