Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@ func (e *Editor) GetReader() io.ReadSeeker {
// before populate the internal text buffer.
func (e *Editor) SetText(s string) {
e.initBuffer()
e.resetIME()
clear(e.autoInsertions)
e.lastInput = nil

indent, _, size := GuessIndentation(s)
e.text.SoftTab = indent == Spaces
Expand All @@ -378,12 +381,17 @@ func (e *Editor) SetText(s string) {
e.lineEnding = DetectLineEnding(s)

e.text.SetText(StripLineEnding(s))
e.ime.start = 0
e.ime.end = 0
// Reset xoff and move the caret to the beginning.
e.SetCaret(0, 0)
}

func (e *Editor) resetIME() {
if e.ime.isComposing {
e.buffer.UnGroupOp()
}
e.ime.imeState = imeState{}
}

// CaretPos returns the line & column numbers of the caret.
func (e *Editor) CaretPos() (line, col int) {
e.initBuffer()
Expand Down Expand Up @@ -576,6 +584,18 @@ func (e *Editor) replace(start, end int, s string) int {

sc := e.text.Replace(start, end, s)
newEnd := start + sc
if len(e.autoInsertions) > 0 && (start != end || sc != 0) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enables the auto-inserted pairs to be deleted even if there's insertion before them, right? This changes the editor behaviour which I think should be documented in your PR.

The auto-insertion behaviour was aligned with vscode. While I think the new feature is smarter, but sometimes it may be not desired.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right. The marker now moves when text is inserted before the pair,
so the pair can still be skipped or deleted later.

I added a test for this in b65ee58. I'll also mention it in the PR description.

updated := make(map[int]rune, len(e.autoInsertions))
for pos, r := range e.autoInsertions {
switch {
case pos < start:
updated[pos] = r
case pos >= end:
updated[pos+newEnd-end] = r
}
}
e.autoInsertions = updated
}
adjust := func(pos int) int {
switch {
case newEnd < pos && pos <= end:
Expand Down
26 changes: 17 additions & 9 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@ func (e *Editor) processEvents(gtx layout.Context) (ev EditorEvent, ok bool) {
}
}

switch ev.(type) {
case ChangeEvent:
if isChangeEvent(ev) {
e.wordHighlighter.MarkActive(false)
e.updateCompletor()
case SelectEvent:
} else if _, ok := ev.(SelectEvent); ok {
e.updateCompletor()
}
}()
Expand Down Expand Up @@ -215,6 +214,11 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
}

if evt := e.processCommands(gtx); evt != nil {
// The command already reported this change; consume the buffer flag so
// the next Update call does not report it again.
if isChangeEvent(evt) {
e.text.Changed()
}
return evt, true
}

Expand All @@ -225,6 +229,15 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
return nil, false
}

func isChangeEvent(evt EditorEvent) bool {
switch evt.(type) {
case ChangeEvent, *ChangeEvent:
return true
default:
return false
}
}

func (e *Editor) processEditEvents(gtx layout.Context) EditorEvent {
filters := []event.Filter{
key.FocusFilter{Target: e},
Expand All @@ -241,8 +254,7 @@ func (e *Editor) processEditEvents(gtx layout.Context) EditorEvent {

switch ke := evt.(type) {
case key.FocusEvent:
// Reset IME state.
e.ime.imeState = imeState{}
e.resetIME()
if ke.Focus && e.mode != ModeReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
Expand Down Expand Up @@ -481,9 +493,6 @@ func (e *Editor) onTextInput(ke key.EditEvent) {
e.text.MoveCaret(-1, -1)
start, _ := e.text.Selection() // start and end should be the same
e.autoInsertions[start] = counterpart
} else {
// If only the opening char was inserted, ensure it's not tracked
delete(e.autoInsertions, ke.Range.Start)
}

} else if counterpart > 0 {
Expand All @@ -500,7 +509,6 @@ func (e *Editor) onTextInput(ke key.EditEvent) {
e.replace(ke.Range.Start, ke.Range.End, ke.Text)
}
} else {
delete(e.autoInsertions, ke.Range.Start)
e.replace(ke.Range.Start, ke.Range.End, ke.Text)
}

Expand Down
200 changes: 200 additions & 0 deletions event_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,214 @@
package gvcode

import (
"image"
"strings"
"testing"

"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"github.com/oligo/gvcode/textview"
)

func BenchmarkDeleteBackwardLargeDocument(b *testing.B) {
doc := strings.Repeat("func main() { println(\"hello\") }\n", 5000)
editor := &Editor{}
editor.WithOptions(WithTextSize(unit.Sp(14)))
editor.SetText(doc)
gtx := layout.Context{Constraints: layout.Exact(image.Pt(1200, 800))}
editor.text.Layout(gtx, text.NewShaper())
editor.SetCaret(editor.Len(), editor.Len())

b.ResetTimer()
for range b.N {
editor.Delete(-1)
}
}

func BenchmarkTextInputLargeDocument(b *testing.B) {
doc := strings.Repeat("func main() { println(\"hello\") }\n", 5000)
editor := &Editor{}
editor.WithOptions(WithTextSize(unit.Sp(14)))
editor.SetText(doc)
textGtx := layout.Context{Constraints: layout.Exact(image.Pt(1200, 800))}
shaper := text.NewShaper()
editor.text.Layout(textGtx, shaper)
editor.SetCaret(editor.Len(), editor.Len())

router := new(input.Router)
gtx := layout.Context{Ops: new(op.Ops), Source: router.Source()}
editor.Update(gtx)
router.Frame(new(op.Ops))
router.Source().Execute(key.FocusCmd{Tag: editor})
for {
if _, ok := editor.Update(gtx); !ok {
break
}
}

b.ResetTimer()
for range b.N {
b.StopTimer()
pos := editor.Len()
router.Queue(key.EditEvent{Range: key.Range{Start: pos, End: pos}, Text: "a"})
b.StartTimer()
evt, ok := editor.Update(gtx)
b.StopTimer()
if !ok || !isChangeEvent(evt) {
b.Fatalf("Update() = (%T, %v), want ChangeEvent", evt, ok)
}

for {
if _, ok := editor.Update(gtx); !ok {
break
}
}
if _, ok := editor.undo(); !ok {
b.Fatal("undo failed")
}
editor.text.Changed()
editor.text.Layout(textGtx, shaper)
}
}

func TestDeleteBackwardEmitsOneChangeEvent(t *testing.T) {
editor := &Editor{}
editor.SetText("abc")
editor.SetCaret(editor.Len(), editor.Len())

router := new(input.Router)
gtx := layout.Context{Ops: new(op.Ops), Source: router.Source()}
editor.Update(gtx)
router.Frame(new(op.Ops))

router.Source().Execute(key.FocusCmd{Tag: editor})
router.Queue(key.Event{Name: key.NameDeleteBackward, State: key.Press})

gtx.Source = router.Source()
changes := 0
for {
evt, ok := editor.Update(gtx)
if !ok {
break
}
if _, ok := evt.(ChangeEvent); ok {
changes++
}
}

if changes != 1 {
t.Fatalf("DeleteBackward emitted %d ChangeEvents, want 1", changes)
}
if got := editor.Len(); got != 2 {
t.Fatalf("DeleteBackward left %d runes, want 2", got)
}
}

func TestIsChangeEventAcceptsValueAndPointer(t *testing.T) {
if !isChangeEvent(ChangeEvent{}) {
t.Fatal("value ChangeEvent was not recognized")
}
if !isChangeEvent(&ChangeEvent{}) {
t.Fatal("pointer ChangeEvent was not recognized")
}
if isChangeEvent(SelectEvent{}) {
t.Fatal("SelectEvent was recognized as a ChangeEvent")
}
}

func TestEditEventsEmitOneChangeEventPerFrame(t *testing.T) {
editor := &Editor{}
editor.SetText("")

router := new(input.Router)
gtx := layout.Context{Ops: new(op.Ops), Source: router.Source()}
editor.Update(gtx)
router.Frame(new(op.Ops))

router.Source().Execute(key.FocusCmd{Tag: editor})
router.Queue(
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "a"},
key.EditEvent{Range: key.Range{Start: 1, End: 1}, Text: "b"},
)

gtx.Source = router.Source()
changes := 0
for {
evt, ok := editor.Update(gtx)
if !ok {
break
}
if _, ok := evt.(ChangeEvent); ok {
changes++
}
}

if changes != 1 {
t.Fatalf("two EditEvents emitted %d ChangeEvents, want 1", changes)
}
if got := editor.Text(); got != "ab" {
t.Fatalf("text = %q, want %q", got, "ab")
}
}

func TestResetIMEClosesUndoGroup(t *testing.T) {
editor := &Editor{}
editor.SetText("")
editor.ime.isComposing = true
editor.buffer.GroupOp()
editor.replace(0, 0, "中")

editor.resetIME()
editor.replace(1, 1, " ")

if _, ok := editor.undo(); !ok {
t.Fatal("undo failed")
}
if got := editor.Text(); got != "中" {
t.Fatalf("undo after IME reset left %q, want %q", got, "中")
}
}

func TestAutoInsertionTracksEditsBeforeClosingRune(t *testing.T) {
editor := &Editor{}
editor.WithOptions(WithTextSize(unit.Sp(14)))
editor.SetText("")
editor.text.Layout(layout.Context{Constraints: layout.Exact(image.Pt(800, 600))}, text.NewShaper())

editor.onTextInput(key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "("})
editor.onTextInput(key.EditEvent{Range: key.Range{Start: 1, End: 1}, Text: "x"})
editor.onTextInput(key.EditEvent{Range: key.Range{Start: 2, End: 2}, Text: ")"})

if got := editor.Text(); got != "(x)" {
t.Fatalf("text = %q, want %q", got, "(x)")
}
start, end := editor.Selection()
if start != 3 || end != 3 {
t.Fatalf("selection = (%d, %d), want (3, 3)", start, end)
}
}

func TestAutoInsertionTracksPairAfterEarlierEdit(t *testing.T) {
editor := &Editor{}
editor.WithOptions(WithTextSize(unit.Sp(14)))
editor.SetText("")
editor.text.Layout(layout.Context{Constraints: layout.Exact(image.Pt(800, 600))}, text.NewShaper())

editor.onTextInput(key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "("})
editor.onTextInput(key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "x"})
if deleted := editor.Delete(-1); deleted != 2 {
t.Fatalf("deleted runes = %d, want 2", deleted)
}

if got := editor.Text(); got != "x" {
t.Fatalf("text = %q, want %q", got, "x")
}
}

func TestOnDeleteBackward_Indentation(t *testing.T) {
setup := func(input string, cursorPos, tabWidth int) *Editor {
vw := textview.NewTextView()
Expand Down
Loading
Loading