Add SourceSpanWithContextExtension.subspan (#81)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0096b7..f906a87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 1.9.0
+
+* Add `SourceSpanWithContextExtension.subspan` that returns a
+  `SourceSpanWithContext` rather than a plain `SourceSpan`.
+
 # 1.8.2
 
 * Fix a bug where highlighting multiple spans with `null` URLs could cause an
diff --git a/lib/src/span.dart b/lib/src/span.dart
index 30590ea..05fd340 100644
--- a/lib/src/span.dart
+++ b/lib/src/span.dart
@@ -5,12 +5,12 @@
 import 'package:path/path.dart' as p;
 import 'package:term_glyph/term_glyph.dart' as glyph;
 
-import 'charcode.dart';
 import 'file.dart';
 import 'highlighter.dart';
 import 'location.dart';
 import 'span_mixin.dart';
 import 'span_with_context.dart';
+import 'utils.dart';
 
 /// A class that describes a segment of source text.
 abstract class SourceSpan implements Comparable<SourceSpan> {
@@ -187,48 +187,7 @@
     RangeError.checkValidRange(start, end, length);
     if (start == 0 && (end == null || end == length)) return this;
 
-    final text = this.text;
-    final startLocation = this.start;
-    var line = startLocation.line;
-    var column = startLocation.column;
-
-    // Adjust [line] and [column] as necessary if the character at [i] in [text]
-    // is a newline.
-    void consumeCodePoint(int i) {
-      final codeUnit = text.codeUnitAt(i);
-      if (codeUnit == $lf ||
-          // A carriage return counts as a newline, but only if it's not
-          // followed by a line feed.
-          (codeUnit == $cr &&
-              (i + 1 == text.length || text.codeUnitAt(i + 1) != $lf))) {
-        line += 1;
-        column = 0;
-      } else {
-        column += 1;
-      }
-    }
-
-    for (var i = 0; i < start; i++) {
-      consumeCodePoint(i);
-    }
-
-    final newStartLocation = SourceLocation(startLocation.offset + start,
-        sourceUrl: sourceUrl, line: line, column: column);
-
-    SourceLocation newEndLocation;
-    if (end == null || end == length) {
-      newEndLocation = this.end;
-    } else if (end == start) {
-      newEndLocation = newStartLocation;
-    } else {
-      for (var i = start; i < end; i++) {
-        consumeCodePoint(i);
-      }
-      newEndLocation = SourceLocation(startLocation.offset + end,
-          sourceUrl: sourceUrl, line: line, column: column);
-    }
-
-    return SourceSpan(
-        newStartLocation, newEndLocation, text.substring(start, end));
+    final locations = subspanLocations(this, start, end);
+    return SourceSpan(locations[0], locations[1], text.substring(start, end));
   }
 }
diff --git a/lib/src/span_with_context.dart b/lib/src/span_with_context.dart
index da09cc0..776c789 100644
--- a/lib/src/span_with_context.dart
+++ b/lib/src/span_with_context.dart
@@ -34,3 +34,18 @@
     }
   }
 }
+
+// TODO(#52): Move these to instance methods in the next breaking release.
+/// Extension methods on the base [SourceSpan] API.
+extension SourceSpanWithContextExtension on SourceSpanWithContext {
+  /// Returns a span from [start] code units (inclusive) to [end] code units
+  /// (exclusive) after the beginning of this span.
+  SourceSpanWithContext subspan(int start, [int? end]) {
+    RangeError.checkValidRange(start, end, length);
+    if (start == 0 && (end == null || end == length)) return this;
+
+    final locations = subspanLocations(this, start, end);
+    return SourceSpanWithContext(
+        locations[0], locations[1], text.substring(start, end), context);
+  }
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index ccc88bd..7df0baf 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -2,7 +2,10 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'charcode.dart';
+import 'location.dart';
 import 'span.dart';
+import 'span_with_context.dart';
 
 /// Returns the minimum of [obj1] and [obj2] according to
 /// [Comparable.compareTo].
@@ -89,3 +92,54 @@
   // ignore: avoid_returning_null
   return null;
 }
+
+/// Returns a two-element list containing the start and end locations of the
+/// span from [start] code units (inclusive) to [end] code units (exclusive)
+/// after the beginning of [span].
+///
+/// This is factored out so it can be shared between
+/// [SourceSpanExtension.subspan] and [SourceSpanWithContextExtension.subspan].
+List<SourceLocation> subspanLocations(SourceSpan span, int start, [int? end]) {
+  final text = span.text;
+  final startLocation = span.start;
+  var line = startLocation.line;
+  var column = startLocation.column;
+
+  // Adjust [line] and [column] as necessary if the character at [i] in [text]
+  // is a newline.
+  void consumeCodePoint(int i) {
+    final codeUnit = text.codeUnitAt(i);
+    if (codeUnit == $lf ||
+        // A carriage return counts as a newline, but only if it's not
+        // followed by a line feed.
+        (codeUnit == $cr &&
+            (i + 1 == text.length || text.codeUnitAt(i + 1) != $lf))) {
+      line += 1;
+      column = 0;
+    } else {
+      column += 1;
+    }
+  }
+
+  for (var i = 0; i < start; i++) {
+    consumeCodePoint(i);
+  }
+
+  final newStartLocation = SourceLocation(startLocation.offset + start,
+      sourceUrl: span.sourceUrl, line: line, column: column);
+
+  SourceLocation newEndLocation;
+  if (end == null || end == span.length) {
+    newEndLocation = span.end;
+  } else if (end == start) {
+    newEndLocation = newStartLocation;
+  } else {
+    for (var i = start; i < end; i++) {
+      consumeCodePoint(i);
+    }
+    newEndLocation = SourceLocation(startLocation.offset + end,
+        sourceUrl: span.sourceUrl, line: line, column: column);
+  }
+
+  return [newStartLocation, newEndLocation];
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index ad81db6..84db52e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: source_span
-version: 1.8.2
+version: 1.9.0
 
 description: A library for identifying source spans and locations.
 homepage: https://github.com/dart-lang/source_span
diff --git a/test/span_test.dart b/test/span_test.dart
index 348cc9e..22c498e 100644
--- a/test/span_test.dart
+++ b/test/span_test.dart
@@ -206,6 +206,13 @@
       expect(result.end.sourceUrl, equals(span.sourceUrl));
     });
 
+    test('preserves the context', () {
+      final start = SourceLocation(2);
+      final end = SourceLocation(5);
+      final span = SourceSpanWithContext(start, end, 'abc', '--abc--');
+      expect(span.subspan(1, 2).context, equals('--abc--'));
+    });
+
     group('returns the original span', () {
       test('with an implicit end', () => expect(span.subspan(0), equals(span)));