~eliasnaur/gio-patches

1

[PATCH] widget: make editor skip words with ctrl key modifier

Details
Message ID
<157E85D3-46F7-44F8-AEC6-47D7BB087A8D@getmailspring.com>
DKIM signature
pass
Download raw message
From 276298f20ac87c8bdea5126ba6ea0deecee4e396 Mon Sep 17 00:00:00 2001
From: Jack Mordaunt <jackmordaunt@gmail.com>
Date: Tue, 15 Sep 2020 20:49:30 +0800
Subject: [PATCH] widget: make editor skip words with ctrl key modifier

Signed-off-by: Jack Mordaunt <jackmordaunt@gmail.com>
---
 widget/editor.go      | 53 +++++++++++++++++++++++++++++++++++++++++--
 widget/editor_test.go | 49 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 100 insertions(+), 2 deletions(-)

Skip words in the direction of the arrow key when `ctrl` modifier is
active. 
Added test harness for the `moveWord` method specifically, as well as
adding a call in the consistency test. 
Behaviour modelled on commond text editors: eats initial whitespace,
then jumps to the end of the word.

diff --git a/widget/editor.go b/widget/editor.go
index 7e8fddb..b14c6cd 100644
--- a/widget/editor.go
+++ b/widget/editor.go
@@ -9,6 +9,7 @@ import (
 	"math"
 	"strings"
 	"time"
+	"unicode"
 	"unicode/utf8"
 
 	"gioui.org/f32"
@@ -278,9 +279,17 @@ func (e *Editor) command(k key.Event) bool {
 	case key.NameDownArrow:
 		e.moveLines(+1)
 	case key.NameLeftArrow:
-		e.Move(-1)
+		if k.Modifiers.Contain(key.ModCtrl) {
+			e.moveWord(-1)
+		} else {
+			e.Move(-1)
+		}
 	case key.NameRightArrow:
-		e.Move(1)
+		if k.Modifiers.Contain(key.ModCtrl) {
+			e.moveWord(1)
+		} else {
+			e.Move(1)
+		}
 	case key.NamePageUp:
 		e.movePages(-1)
 	case key.NamePageDown:
@@ -775,6 +784,46 @@ 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()
+	var (
+		// split the distance information into constituent parts to be
+		// used independently.
+		words, direction = func(n int) (int, int) {
+			if n < 0 {
+				return n * -1, -1
+			} else {
+				return n, 1
+			}
+		}(distance)
+		// 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 7f51297..71b4590 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,52 @@ 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")
-- 
2.28.0.windows.1
Details
Message ID
<CAE_4BPA6up02VCyQTvWD2C_pfDM1TP_7hLG9uGD0_a1wuGwq3w@mail.gmail.com>
In-Reply-To
<157E85D3-46F7-44F8-AEC6-47D7BB087A8D@getmailspring.com> (view parent)
DKIM signature
missing
Download raw message
Please use ModAlt on Macs.  On my Mac, in Gmail on Chrome, Pages, and
Evernote, for example, Ctrl-Left & Ctrl-Right are
move-to-start-/end-of-line, respectively.  (As is Cmd-Left & -Right,
so you can't use ModShortcut.)  Thanks.

Also: Thanks for this!  Been missing this functionality a lot!

-- Larry

On Tue, Sep 15, 2020 at 9:03 AM Jack Mordaunt <jackmordaunt@gmail.com> wrote:
>
> >From 276298f20ac87c8bdea5126ba6ea0deecee4e396 Mon Sep 17 00:00:00 2001
> From: Jack Mordaunt <jackmordaunt@gmail.com>
> Date: Tue, 15 Sep 2020 20:49:30 +0800
> Subject: [PATCH] widget: make editor skip words with ctrl key modifier
>
> Signed-off-by: Jack Mordaunt <jackmordaunt@gmail.com>
> ---
>  widget/editor.go      | 53 +++++++++++++++++++++++++++++++++++++++++--
>  widget/editor_test.go | 49 +++++++++++++++++++++++++++++++++++++++
>  2 files changed, 100 insertions(+), 2 deletions(-)
>
> Skip words in the direction of the arrow key when `ctrl` modifier is
> active.
> Added test harness for the `moveWord` method specifically, as well as
> adding a call in the consistency test.
> Behaviour modelled on commond text editors: eats initial whitespace,
> then jumps to the end of the word.
>
> diff --git a/widget/editor.go b/widget/editor.go
> index 7e8fddb..b14c6cd 100644
> --- a/widget/editor.go
> +++ b/widget/editor.go
> @@ -9,6 +9,7 @@ import (
>         "math"
>         "strings"
>         "time"
> +       "unicode"
>         "unicode/utf8"
>
>         "gioui.org/f32"
> @@ -278,9 +279,17 @@ func (e *Editor) command(k key.Event) bool {
>         case key.NameDownArrow:
>                 e.moveLines(+1)
>         case key.NameLeftArrow:
> -               e.Move(-1)
> +               if k.Modifiers.Contain(key.ModCtrl) {
> +                       e.moveWord(-1)
> +               } else {
> +                       e.Move(-1)
> +               }
>         case key.NameRightArrow:
> -               e.Move(1)
> +               if k.Modifiers.Contain(key.ModCtrl) {
> +                       e.moveWord(1)
> +               } else {
> +                       e.Move(1)
> +               }
>         case key.NamePageUp:
>                 e.movePages(-1)
>         case key.NamePageDown:
> @@ -775,6 +784,46 @@ 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()
> +       var (
> +               // split the distance information into constituent parts to be
> +               // used independently.
> +               words, direction = func(n int) (int, int) {
> +                       if n < 0 {
> +                               return n * -1, -1
> +                       } else {
> +                               return n, 1
> +                       }
> +               }(distance)
> +               // 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 7f51297..71b4590 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,52 @@ 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")
> --
> 2.28.0.windows.1
Export thread (mbox)