github.com/utopiagio/gio@v0.0.8/app/ime_test.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  //go:build go1.18
     4  // +build go1.18
     5  
     6  package app
     7  
     8  import (
     9  	"testing"
    10  	"unicode/utf8"
    11  
    12  	"github.com/utopiagio/gio/font"
    13  	"github.com/utopiagio/gio/font/gofont"
    14  	"github.com/utopiagio/gio/io/input"
    15  	"github.com/utopiagio/gio/io/key"
    16  	"github.com/utopiagio/gio/layout"
    17  	"github.com/utopiagio/gio/op"
    18  	"github.com/utopiagio/gio/text"
    19  	"github.com/utopiagio/gio/unit"
    20  	"github.com/utopiagio/gio/widget"
    21  
    22  )
    23  
    24  func FuzzIME(f *testing.F) {
    25  	runes := []rune("Hello, 世界! 🤬 علي،الحسنب北查爾斯頓工廠的安全漏洞已")
    26  	f.Add([]byte("20\x0010"))
    27  	f.Add([]byte("80000"))
    28  	f.Add([]byte("2008\"80\r00"))
    29  	f.Add([]byte("20007900002\x02000"))
    30  	f.Add([]byte("20007800002\x02000"))
    31  	f.Add([]byte("200A02000990\x19002\x17\x0200"))
    32  	f.Fuzz(func(t *testing.T, cmds []byte) {
    33  		cache := text.NewShaper(text.WithCollection(gofont.Collection()))
    34  		e := new(widget.Editor)
    35  
    36  		var r input.Router
    37  		gtx := layout.Context{Ops: new(op.Ops), Source: r.Source()}
    38  		gtx.Execute(key.FocusCmd{Tag: e})
    39  		// Layout once to register focus.
    40  		e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
    41  		r.Frame(gtx.Ops)
    42  
    43  		var state editorState
    44  		const (
    45  			cmdReplace = iota
    46  			cmdSelect
    47  			cmdSnip
    48  			maxCmd
    49  		)
    50  		const cmdLen = 5
    51  		for len(cmds) >= cmdLen {
    52  			n := e.Len()
    53  			rng := key.Range{
    54  				Start: int(cmds[1]) % (n + 1),
    55  				End:   int(cmds[2]) % (n + 1),
    56  			}
    57  			switch cmds[0] % cmdLen {
    58  			case cmdReplace:
    59  				rstart := int(cmds[3]) % len(runes)
    60  				rend := int(cmds[4]) % len(runes)
    61  				if rstart > rend {
    62  					rstart, rend = rend, rstart
    63  				}
    64  				replacement := string(runes[rstart:rend])
    65  				state.Replace(rng, replacement)
    66  				r.Queue(key.EditEvent{Range: rng, Text: replacement})
    67  				r.Queue(key.SnippetEvent(state.Snippet.Range))
    68  			case cmdSelect:
    69  				r.Queue(key.SelectionEvent(rng))
    70  				runes := []rune(e.Text())
    71  				if rng.Start < 0 {
    72  					rng.Start = 0
    73  				}
    74  				if rng.End < 0 {
    75  					rng.End = 0
    76  				}
    77  				if rng.Start > len(runes) {
    78  					rng.Start = len(runes)
    79  				}
    80  				if rng.End > len(runes) {
    81  					rng.End = len(runes)
    82  				}
    83  				state.Selection.Range = rng
    84  			case cmdSnip:
    85  				r.Queue(key.SnippetEvent(rng))
    86  				runes := []rune(e.Text())
    87  				if rng.Start > rng.End {
    88  					rng.Start, rng.End = rng.End, rng.Start
    89  				}
    90  				if rng.Start < 0 {
    91  					rng.Start = 0
    92  				}
    93  				if rng.End < 0 {
    94  					rng.End = 0
    95  				}
    96  				if rng.Start > len(runes) {
    97  					rng.Start = len(runes)
    98  				}
    99  				if rng.End > len(runes) {
   100  					rng.End = len(runes)
   101  				}
   102  				state.Snippet = key.Snippet{
   103  					Range: rng,
   104  					Text:  string(runes[rng.Start:rng.End]),
   105  				}
   106  			}
   107  			cmds = cmds[cmdLen:]
   108  			e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
   109  			r.Frame(gtx.Ops)
   110  			newState := r.EditorState()
   111  			// We don't track caret position.
   112  			state.Selection.Caret = newState.Selection.Caret
   113  			// Expanded snippets are ok.
   114  			their, our := newState.Snippet, state.EditorState.Snippet
   115  			beforeLen := 0
   116  			for before := our.Start - their.Start; before > 0; before-- {
   117  				_, n := utf8.DecodeRuneInString(their.Text[beforeLen:])
   118  				beforeLen += n
   119  			}
   120  			afterLen := 0
   121  			for after := their.End - our.End; after > 0; after-- {
   122  				_, n := utf8.DecodeLastRuneInString(their.Text[:len(their.Text)-afterLen])
   123  				afterLen += n
   124  			}
   125  			if beforeLen > 0 {
   126  				our.Text = their.Text[:beforeLen] + our.Text
   127  				our.Start = their.Start
   128  			}
   129  			if afterLen > 0 {
   130  				our.Text = our.Text + their.Text[len(their.Text)-afterLen:]
   131  				our.End = their.End
   132  			}
   133  			state.EditorState.Snippet = our
   134  			if newState != state.EditorState {
   135  				t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState)
   136  			}
   137  		}
   138  	})
   139  }
   140  
   141  func TestEditorIndices(t *testing.T) {
   142  	var s editorState
   143  	const str = "Hello, 😀"
   144  	s.Snippet = key.Snippet{
   145  		Text: str,
   146  		Range: key.Range{
   147  			Start: 10,
   148  			End:   utf8.RuneCountInString(str),
   149  		},
   150  	}
   151  	utf16Indices := [...]struct {
   152  		Runes, UTF16 int
   153  	}{
   154  		{0, 0}, {10, 10}, {17, 17}, {18, 19}, {30, 31},
   155  	}
   156  	for _, p := range utf16Indices {
   157  		if want, got := p.UTF16, s.UTF16Index(p.Runes); want != got {
   158  			t.Errorf("UTF16Index(%d) = %d, wanted %d", p.Runes, got, want)
   159  		}
   160  		if want, got := p.Runes, s.RunesIndex(p.UTF16); want != got {
   161  			t.Errorf("RunesIndex(%d) = %d, wanted %d", p.UTF16, got, want)
   162  		}
   163  	}
   164  }