From ef7b3e75f4dc6b4741e9d58108833c2828627fcf Mon Sep 17 00:00:00 2001 From: Jack Mordaunt Date: Thu, 17 Sep 2020 15:48:32 +0800 Subject: [PATCH] widget: delete whole words with key modifier Delete entire words with key modifier, ie "ctrl + delete". Signed-off-by: Jack Mordaunt --- widget/editor.go | 59 +++++++++++++++++++++++++++++++++++++++++-- widget/editor_test.go | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/widget/editor.go b/widget/editor.go index 3399a2cd..69b87965 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -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] diff --git a/widget/editor_test.go b/widget/editor_test.go index 6c9b9e8d..e670d273 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -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")