Repository: superlistapp/super_editor (dart)

Issue:
[attributed_text] - Return substrings given a SpanRange
We often work with `SpanRange`s when dealing with `AttributedText`. Currently, to get a substring, we have to access the inner plain text string, and then unpack the `SpanRange` into a start and end value.

Consider offering a plain text substring method on `AttributedText` that takes a `SpanRange` directly.

Also, consider offering a top-level `length` method on `AttributedText`.

Pull request description:
Added substring and length APIs to AttributedText (Resolves #1200)

Patch:
diff --git a/attributed_text/lib/src/attributed_text.dart b/attributed_text/lib/src/attributed_text.dart
index 7ec519cc89..7b7ad7fe9b 100644
--- a/attributed_text/lib/src/attributed_text.dart
+++ b/attributed_text/lib/src/attributed_text.dart
@@ -30,6 +30,11 @@ class AttributedText {
   /// The text that this [AttributedText] attributes.
   final String text;
 
+  /// Returns the `length` of this [AttributedText]'s [text] `String`.
+  ///
+  /// This accessor is a convenience to avoid writing `myAttText.text.length`.
+  int get length => text.length;
+
   /// The attributes applied to [text].
   final AttributedSpans spans;
 
@@ -197,8 +202,12 @@ class AttributedText {
     _notifyListeners();
   }
 
+  /// Copies all text and attributions from [range.start] to [range.end] (exclusive),
+  /// and returns them as a new [AttributedText].
+  AttributedText copyTextInRange(SpanRange range) => copyText(range.start, range.end);
+
   /// Copies all text and attributions from [startOffset] to
-  /// [endOffset], inclusive, and returns them as a new [AttributedText].
+  /// [endOffset], exclusive, and returns them as a new [AttributedText].
   AttributedText copyText(int startOffset, [int? endOffset]) {
     _log.fine('start: $startOffset, end: $endOffset');
 
@@ -221,6 +230,15 @@ class AttributedText {
     );
   }
 
+  /// Returns a plain-text substring of [text], from [range.start] to [range.end] (exclusive).
+  String substringInRange(SpanRange range) => substring(range.start, range.end);
+
+  /// Returns a plain-text substring of [text], from [start] to [end] (exclusive), or the end of
+  /// [text] if [end] isn't provided.
+  String substring(int start, [int? end]) {
+    return text.substring(start, end);
+  }
+
   /// Returns a copy of this [AttributedText] with the [other] text
   /// and attributions appended to the end.
   AttributedText copyAndAppend(AttributedText other) {


Test patch:
diff --git a/attributed_text/test/attributed_text_test.dart b/attributed_text/test/attributed_text_test.dart
index 3903f06278..6c06d8c4ac 100644
--- a/attributed_text/test/attributed_text_test.dart
+++ b/attributed_text/test/attributed_text_test.dart
@@ -27,6 +27,82 @@ void main() {
       );
     });
 
+    group('fragments', () {
+      test('can be copied as attributed text with a SpanRange', () {
+        final text = AttributedText(
+          "this that other",
+          AttributedSpans(
+            attributions: [
+              SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+              SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+              SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.start),
+              SpanMarker(attribution: ExpectedSpans.italics, offset: 14, markerType: SpanMarkerType.end),
+            ],
+          ),
+        );
+
+        final slice = text.copyTextInRange(SpanRange(start: 5, end: 9));
+        expect(slice.text, "that");
+        expect(slice.length, 4);
+        expect(slice.getAttributedRange({ExpectedSpans.bold}, 0), SpanRange(start: 0, end: 1));
+        expect(slice.getAttributedRange({ExpectedSpans.italics}, 2), SpanRange(start: 2, end: 3));
+      });
+
+      test('can be copied as attributed text with start and end bounds', () {
+        final text = AttributedText(
+          "this that other",
+          AttributedSpans(
+            attributions: [
+              SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+              SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+              SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.start),
+              SpanMarker(attribution: ExpectedSpans.italics, offset: 14, markerType: SpanMarkerType.end),
+            ],
+          ),
+        );
+
+        final slice = text.copyText(5, 9);
+        expect(slice.text, "that");
+        expect(slice.length, 4);
+        expect(slice.getAttributedRange({ExpectedSpans.bold}, 0), SpanRange(start: 0, end: 1));
+        expect(slice.getAttributedRange({ExpectedSpans.italics}, 2), SpanRange(start: 2, end: 3));
+      });
+
+      test('can be copied as plain text with a SpanRange', () {
+        final text = AttributedText(
+          "this that other",
+          AttributedSpans(
+            attributions: [
+              SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+              SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+              SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.start),
+              SpanMarker(attribution: ExpectedSpans.italics, offset: 14, markerType: SpanMarkerType.end),
+            ],
+          ),
+        );
+
+        final substring = text.substringInRange(SpanRange(start: 5, end: 9));
+        expect(substring, "that");
+      });
+
+      test('can be copied as plain text with start and end bounds', () {
+        final text = AttributedText(
+          "this that other",
+          AttributedSpans(
+            attributions: [
+              SpanMarker(attribution: ExpectedSpans.bold, offset: 0, markerType: SpanMarkerType.start),
+              SpanMarker(attribution: ExpectedSpans.bold, offset: 6, markerType: SpanMarkerType.end),
+              SpanMarker(attribution: ExpectedSpans.italics, offset: 7, markerType: SpanMarkerType.start),
+              SpanMarker(attribution: ExpectedSpans.italics, offset: 14, markerType: SpanMarkerType.end),
+            ],
+          ),
+        );
+
+        final substring = text.substring(5, 9);
+        expect(substring, "that");
+      });
+    });
+
     group('span manipulation', () {
       test('combines overlapping spans when adding from left to right', () {
         // Note: span overlaps at the boundary had a bug that was filed in #582.