Repository: superlistapp/super_editor (dart)

Issue:
AttributedText: `collapsedSpans` returns overly large `MultiAttributionSpan` for unattributed string
Here's my expectation encoded as a test. 

```dart
    test('Simple AttributedText', () {
      final text = AttributedText(text: 'Hello World');
      final spans = text.spans.collapseSpans(contentLength: text.text.length);
      expect(spans.length, equals(1));
      expect(spans[0].attributions.isEmpty, equals(true));
      expect(spans[0].start, equals(0));
      expect(spans[0].end, equals(text.text.length));
    });
```

The background use case is that I'm trying to convert an AttributedText back into `TextSpan`s for display purposes. Halp?

Pull request description:
[attributed_text] Fix invalid end index with an marker that exceeds text length. Resolves #632

When an `AttributedText` has a `SpanMarker` with an end index equals to the text length (which is an invalid index, because the spans are inclusive), `collapseSpans` returns a span with an end index equal to the text length, instead of the text length -1, which is the expected index that would point to the last character.

This PR fixes the comparison that causes this behavior.

Patch:
diff --git a/attributed_text/lib/src/attributed_spans.dart b/attributed_text/lib/src/attributed_spans.dart
index 92e7864dba..9bc6af3e36 100644
--- a/attributed_text/lib/src/attributed_spans.dart
+++ b/attributed_text/lib/src/attributed_spans.dart
@@ -921,7 +921,7 @@ class AttributedSpans {
 
     _log.fine('walking list of markers to determine collapsed spans.');
     for (final marker in _markers) {
-      if (marker.offset > contentLength) {
+      if (marker.offset > contentLength - 1) {
         // There are markers to process but we ran off the end of the requested content. Break early and handle
         // committing the last span if necessary below.
         _log.fine('ran out of markers within the requested contentLength, breaking early.');


Test patch:
diff --git a/attributed_text/test/attributed_text_test.dart b/attributed_text/test/attributed_text_test.dart
index 5e60a6402a..40943936b2 100644
--- a/attributed_text/test/attributed_text_test.dart
+++ b/attributed_text/test/attributed_text_test.dart
@@ -681,6 +681,117 @@ void main() {
         expect(expectedVisits, isEmpty);
       });
     });
+
+    group('collapseSpans', () {
+      const boldAttribution = NamedAttribution('bold');
+
+      test('returns a single span for text without attributions', () {
+        final text = AttributedText('Hello World');
+
+        final spans = text.spans.collapseSpans(contentLength: text.text.length);
+
+        // Ensure a single span containing the whole text was returned.
+        expect(spans.length, 1);
+        expect(spans[0].attributions, isEmpty);
+        expect(spans[0].start, 0);
+        expect(spans[0].end, text.text.length - 1);
+      });
+
+      test('returns a single span for text with an attribution containing the whole text', () {
+        final text = AttributedText(
+          'Hello World',
+          AttributedSpans(
+            attributions: const [
+              SpanMarker(
+                attribution: boldAttribution,
+                markerType: SpanMarkerType.start,
+                offset: 0,
+              ),
+              SpanMarker(
+                attribution: boldAttribution,
+                markerType: SpanMarkerType.end,
+                offset: 10,
+              ),
+            ],
+          ),
+        );
+
+        final spans = text.spans.collapseSpans(contentLength: text.text.length);
+
+        // Ensure a single span containing the whole text was returned.
+        expect(spans.length, 1);
+        expect(spans[0].attributions, isNotEmpty);
+        expect(spans[0].start, 0);
+        expect(spans[0].end, text.text.length - 1);
+      });
+
+      test('returns two spans for text with an attribution from the beginning until half of the text', () {
+        // Create a text with a bold attribution in "Hello ".
+        final text = AttributedText(
+          'Hello World',
+          AttributedSpans(
+            attributions: const [
+              SpanMarker(
+                attribution: boldAttribution,
+                markerType: SpanMarkerType.start,
+                offset: 0,
+              ),
+              SpanMarker(
+                attribution: boldAttribution,
+                markerType: SpanMarkerType.end,
+                offset: 5,
+              ),
+            ],
+          ),
+        );
+
+        final spans = text.spans.collapseSpans(contentLength: text.text.length);
+
+        // Ensure two spans were returned.
+        // The first containing the attribution and the second without any attributions.
+        expect(spans.length, 2);
+        expect(spans[0].attributions, isNotEmpty);
+        expect(spans[0].start, 0);
+        expect(spans[0].end, 5);
+        expect(spans[1].attributions, isEmpty);
+        expect(spans[1].start, 6);
+        expect(spans[1].end, text.text.length - 1);
+      });
+
+      test('handles markers which end after the end of the text', () {
+        // Create a text with a bold attribution in "World".
+        // The marker end offset is bigger than the last character index (the text lenght is 11).
+        final text = AttributedText(
+          'Hello World',
+          AttributedSpans(
+            attributions: const [
+              SpanMarker(
+                attribution: boldAttribution,
+                markerType: SpanMarkerType.start,
+                offset: 6,
+              ),
+              SpanMarker(
+                attribution: boldAttribution,
+                markerType: SpanMarkerType.end,
+                offset: 11,
+              ),
+            ],
+          ),
+        );
+
+        final spans = text.spans.collapseSpans(contentLength: text.text.length);
+
+        // Ensure two spans were returned. The first containing no attributions and
+        // the second containing the attribution.
+        expect(spans.length, 2);
+        expect(spans[0].attributions, isEmpty);
+        expect(spans[0].start, 0);
+        expect(spans[0].end, 5);
+        expect(spans[1].attributions, isNotEmpty);
+        expect(spans[1].start, 6);
+        expect(spans[1].end, 10);
+      });
+    });
   });
 }