~eliasnaur/gio

ef7b3e75f4dc6b4741e9d58108833c2828627fcf — Jack Mordaunt 10 months ago d27d1a9
widget: delete whole words with key modifier

Delete entire words with key modifier, ie "ctrl + delete".

Signed-off-by: Jack Mordaunt <jackmordaunt@gmail.com>
2 files changed, 112 insertions(+), 2 deletions(-)

M widget/editor.go
M widget/editor_test.go
M widget/editor.go => widget/editor.go +57 -2
@@ 276,9 276,17 @@ func (e *Editor) command(k key.Event) bool {
	case key.NameReturn, key.NameEnter:
		e.append("\n")
	case key.NameDeleteBackward:
		e.Delete(-1)
		if k.Modifiers == modSkip {
			e.deleteWord(-1)
		} else {
			e.Delete(-1)
		}
	case key.NameDeleteForward:
		e.Delete(1)
		if k.Modifiers == modSkip {
			e.deleteWord(1)
		} else {
			e.Delete(1)
		}
	case key.NameUpArrow:
		e.moveLines(-1)
	case key.NameDownArrow:


@@ 824,6 832,53 @@ func (e *Editor) moveWord(distance int) {
	}
}

// deleteWord the next word(s) in the specified direction.
// Unlike moveWord, deleteWord treats whitespace as a word itself.
// Positive is forward, negative is backward.
// Absolute values greater than one will delete that many words.
func (e *Editor) deleteWord(distance int) {
	e.makeValid()
	// split the distance information into constituent parts to be
	// used independently.
	words, direction := distance, 1
	if distance < 0 {
		words, direction = distance*-1, -1
	}
	// atEnd if offset is at or beyond either side of the buffer.
	atEnd := func(offset int) bool {
		idx := e.rr.caret + offset*direction
		return idx <= 0 || idx >= e.rr.len()
	}
	// next returns the appropriate rune given the direction and offset.
	next := func(offset int) (r rune) {
		idx := e.rr.caret + offset*direction
		if idx < 0 {
			idx = 0
		} else if idx > e.rr.len() {
			idx = e.rr.len()
		}
		if direction < 0 {
			r, _ = e.rr.runeBefore(idx)
		} else {
			r, _ = e.rr.runeAt(idx)
		}
		return r
	}
	var runes = 1
	for ii := 0; ii < words; ii++ {
		if r := next(runes); unicode.IsSpace(r) {
			for r := next(runes); unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
				runes += 1
			}
		} else {
			for r := next(runes); !unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
				runes += 1
			}
		}
	}
	e.Delete(runes * direction)
}

func (e *Editor) scrollToCaret() {
	e.makeValid()
	l := e.lines[e.caret.line]

M widget/editor_test.go => widget/editor_test.go +55 -0
@@ 92,6 92,7 @@ const (
	moveEnd
	moveCoord
	moveWord
	deleteWord
	moveLast // Mark end; never generated.
)



@@ 143,6 144,8 @@ func TestEditorCaretConsistency(t *testing.T) {
				e.moveCoord(image.Pt(int(x), int(y)))
			case moveWord:
				e.moveWord(int(distance))
			case deleteWord:
				e.deleteWord(int(distance))
			default:
				return false
			}


@@ 203,6 206,58 @@ func TestEditorMoveWord(t *testing.T) {
		}
	}
}

func TestEditorDeleteWord(t *testing.T) {
	type Test struct {
		Text   string
		Start  int
		Delete int

		Want   int
		Result string
	}
	tests := []Test{
		{"", 0, 0, 0, ""},
		{"", 0, -1, 0, ""},
		{"", 0, 1, 0, ""},
		{"hello", 0, -1, 0, "hello"},
		{"hello", 0, 1, 0, ""},
		{"hello world", 3, 1, 3, "hel world"},
		{"hello world", 3, -1, 0, "lo world"},
		{"hello world", 8, -1, 6, "hello rld"},
		{"hello world", 8, 1, 8, "hello wo"},
		{"hello    world", 3, 1, 3, "hel    world"},
		{"hello    world", 3, 2, 3, "helworld"},
		{"hello    world", 8, 1, 8, "hello   "},
		{"hello    world", 8, -1, 5, "hello world"},
		{"hello brave new world", 0, 3, 0, " new world"},
	}
	setup := func(t string) *Editor {
		e := new(Editor)
		gtx := layout.Context{
			Ops:         new(op.Ops),
			Constraints: layout.Exact(image.Pt(100, 100)),
		}
		cache := text.NewCache(gofont.Collection())
		fontSize := unit.Px(10)
		font := text.Font{}
		e.SetText(t)
		e.Layout(gtx, cache, font, fontSize)
		return e
	}
	for ii, tt := range tests {
		e := setup(tt.Text)
		e.Move(tt.Start)
		e.deleteWord(tt.Delete)
		if e.rr.caret != tt.Want {
			t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, e.rr.caret, tt.Want)
		}
		if e.Text() != tt.Result {
			t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, e.Text(), tt.Result)
		}
	}
}

func TestEditorNoLayout(t *testing.T) {
	var e Editor
	e.SetText("hi!\n")