diff --git a/widget/editor.go b/widget/editor.go index 7e8fddb9..3399a2cd 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -7,8 +7,10 @@ import ( "image" "io" "math" + "runtime" "strings" "time" + "unicode" "unicode/utf8" "gioui.org/f32" @@ -266,6 +268,10 @@ func (e *Editor) moveLines(distance int) { } func (e *Editor) command(k key.Event) bool { + modSkip := key.ModCtrl + if runtime.GOOS == "darwin" { + modSkip = key.ModAlt + } switch k.Name { case key.NameReturn, key.NameEnter: e.append("\n") @@ -278,9 +284,17 @@ func (e *Editor) command(k key.Event) bool { case key.NameDownArrow: e.moveLines(+1) case key.NameLeftArrow: - e.Move(-1) + if k.Modifiers == modSkip { + e.moveWord(-1) + } else { + e.Move(-1) + } case key.NameRightArrow: - e.Move(1) + if k.Modifiers == modSkip { + e.moveWord(1) + } else { + e.Move(1) + } case key.NamePageUp: e.movePages(-1) case key.NamePageDown: @@ -775,6 +789,41 @@ func (e *Editor) moveEnd() { e.caret.xoff = l.Width + a - e.caret.x } +// moveWord moves the caret to the next word in the specified direction. +// Positive is forward, negative is backward. +// Absolute values greater than one will skip that many words. +func (e *Editor) moveWord(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 caret is at either side of the buffer. + atEnd := func() bool { + return e.rr.caret == 0 || e.rr.caret == e.rr.len() + } + // next returns the appropriate rune given the direction. + next := func() (r rune) { + if direction < 0 { + r, _ = e.rr.runeBefore(e.rr.caret) + } else { + r, _ = e.rr.runeAt(e.rr.caret) + } + return r + } + for ii := 0; ii < words; ii++ { + for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() { + e.Move(direction) + } + e.Move(direction) + for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() { + e.Move(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 7f512971..6c9b9e8d 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -91,6 +91,7 @@ const ( moveStart moveEnd moveCoord + moveWord moveLast // Mark end; never generated. ) @@ -140,6 +141,8 @@ func TestEditorCaretConsistency(t *testing.T) { e.moveEnd() case moveCoord: e.moveCoord(image.Pt(int(x), int(y))) + case moveWord: + e.moveWord(int(distance)) default: return false } @@ -155,6 +158,51 @@ func TestEditorCaretConsistency(t *testing.T) { } } +func TestEditorMoveWord(t *testing.T) { + type Test struct { + Text string + Start int + Skip int + Want int + } + tests := []Test{ + {"", 0, 0, 0}, + {"", 0, -1, 0}, + {"", 0, 1, 0}, + {"hello", 0, -1, 0}, + {"hello", 0, 1, 5}, + {"hello world", 3, 1, 5}, + {"hello world", 3, -1, 0}, + {"hello world", 8, -1, 6}, + {"hello world", 8, 1, 11}, + {"hello world", 3, 1, 5}, + {"hello world", 3, 2, 14}, + {"hello world", 8, 1, 14}, + {"hello world", 8, -1, 0}, + {"hello brave new world", 0, 3, 15}, + } + 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.moveWord(tt.Skip) + if e.rr.caret != tt.Want { + t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, e.rr.caret, tt.Want) + } + } +} func TestEditorNoLayout(t *testing.T) { var e Editor e.SetText("hi!\n")