pkg/strings: add SliceRunes

There was some discussions on slack about this and turns out there is no real solution to slice strings easily in cue.

Added a builtin which works internally on runes to work properly on strings with unicode chars.

Not sure about the name of the builtin.

Closes #424
https://github.com/cuelang/cue/pull/424

GitOrigin-RevId: 55640e7cfdf8878e5a90b386fc43b9650656d1bc
Change-Id: I40293d2e1646d1eee2e9074af9954a4be339de78
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6400
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin_test.go b/cue/builtin_test.go
index a165a4c..1901d34 100644
--- a/cue/builtin_test.go
+++ b/cue/builtin_test.go
@@ -395,6 +395,9 @@
 		test("strings", `strings.ByteSlice("Hello", 2, 5)`),
 		`'llo'`,
 	}, {
+		test("strings", `strings.SliceRunes("✓ Hello", 0, 3)`),
+		`"✓ H"`,
+	}, {
 		test("strings", `strings.Runes("Café")`),
 		strings.Replace(fmt.Sprint([]rune{'C', 'a', 'f', 'é'}), " ", ",", -1),
 	}, {
diff --git a/cue/builtins.go b/cue/builtins.go
index 73b80eb..747c846 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -2978,6 +2978,22 @@
 				}
 			},
 		}, {
+			Name:   "SliceRunes",
+			Params: []kind{stringKind, intKind, intKind},
+			Result: stringKind,
+			Func: func(c *callCtxt) {
+				s, start, end := c.string(0), c.int(1), c.int(2)
+				if c.do() {
+					c.ret, c.err = func() (interface{}, error) {
+						runes := []rune(s)
+						if start < 0 || start > end || end > len(runes) {
+							return "", fmt.Errorf("index out of range")
+						}
+						return string(runes[start:end]), nil
+					}()
+				}
+			},
+		}, {
 			Name:   "Compare",
 			Params: []kind{stringKind, stringKind},
 			Result: intKind,
diff --git a/pkg/strings/manual.go b/pkg/strings/manual.go
index 508466d..9f53870 100644
--- a/pkg/strings/manual.go
+++ b/pkg/strings/manual.go
@@ -108,3 +108,13 @@
 		},
 		s)
 }
+
+// SliceRunes returns a string of the underlying string data from the start index
+// up to but not including the end index.
+func SliceRunes(s string, start, end int) (string, error) {
+	runes := []rune(s)
+	if start < 0 || start > end || end > len(runes) {
+		return "", fmt.Errorf("index out of range")
+	}
+	return string(runes[start:end]), nil
+}